core/scripts/ratify_ethics_pack.py
Shay 3065ad9e19
feat(packs): expansion round 2 — ethics ×3, anchor-lens ×3, relations-v3, register ×2 (#48)
* feat(packs): ethics ×3, anchor-lens ×3, relations-v3, register ×2

Group 1 — Ethics domain packs (ADR-0044 sibling)
  legal_ethics_v1: 6 commitments covering no-legal-advice, no-outcome-prediction,
    jurisdiction-disclosure, privilege-disclosure, conflict-disclosure, refer-to-counsel
  engineering_ethics_v1: 6 commitments covering safety-primacy, standard-disclosure,
    no-sign-off, uncertainty-surface, public-welfare-priority, refer-to-pe
  research_ethics_v1: 6 commitments covering no-fabrication, no-plagiarism,
    irb-disclosure, conflict-of-interest-disclosure, data-integrity, reproducibility-hedge
  ratify_ethics_pack.py: PACK_IDS extended with all three new ids

Group 2 — Anchor lens packs (grc cognition atoms, ADR-0073c)
  grc_sophia_v1: atom logos.sophia.wisdom via grc-core-cog-008 (cross_lang.logos.sophia
    edge weight 0.88); cognitive mode wisdom-practical
  grc_epignosis_v1: atom logos.epignosis.experiential via grc-core-cog-007 (weight 0.78,
    en_collapse edge documented); cognitive mode experiential-knowledge
  grc_episteme_v1: atom logos.episteme.systematic via grc-core-cog-021 (weight 0.72,
    en_collapse edge documented); cognitive mode systematic-knowledge
  ratify_anchor_lens_packs.py: LENS_IDS extended with all three new ids

Group 3 — en_core_relations_v3 (social + part-whole extension of v2 kinship)
  7 new lemmas: colleague, mentor, neighbor, component, member, instance, peer
  manifest.json: new pack with checksum placeholder (operator must recompute after
    ratify run — same pattern as other packs)

Group 4 — Register packs formal_v1 + socratic_v1
  formal_v1: standard depth, drop_provenance_tag=true + drop_articles=true;
    no markers; ratifies under known_key_overrides_invariant_grounding
  socratic_v1: pedagogical depth, append_semantic_domain_clause=true; markers scaffold
    question-and-response rhythm (openings×4, transitions×3, closings×4)
  ratify_register_packs.py: REGISTER_IDS extended with formal_v1, socratic_v1

* fix(anchor_lens): loader v1/v2 dual-schema compat — resolves blocker 1 of #48

Refactor AnchorLens to use v2 schema fields and normalize legacy fields. Update validation and loading functions for improved clarity and functionality.

* fix(ratify): restore default_unanchored_v1 + full LENS_IDS (17) — resolves blocker 2 of #48

Added new lens IDs for the he substrate and updated the order of lens IDs.

* chore(packs): migrate 8 legacy anchor-lens packs to v2 schema [1/8 default_unanchored_v1]

Updated the default unanchored lens JSON structure with new fields and modified descriptions.

* chore(packs): migrate grc_logos_v1 to v2 schema [2/8]

Updated the description and added new fields for cognitive mode, atom, and source entry ID.

* chore(packs): migrate grc_aletheia_v1 to v2 schema [3/8]

Updated the description and added new fields related to cognitive mode and atom.

* chore(packs): migrate grc_zoe_v1 to v2 schema [4/8]

Updated the description and added new fields for cognitive mode, atom, and source entry ID.

* chore(packs): migrate grc_arche_v1 to v2 schema [5/8]

Updated the description and added new fields for cognitive mode, atom, and source entry ID.

* chore(packs): migrate he_logos_v1 to v2 schema [6/8]

Updated the Hebrew-substrate anchor lens JSON structure with new fields and modified descriptions.

* chore(packs): migrate he_dabar_v1 to v2 schema [7/8]

Updated the description and added new fields for cognitive mode and source entry.

* chore(packs): migrate he_chayyim_v1 to v2 schema [8/8] — resolves blocker 3 of #48

Updated the description and added new fields for cognitive mode and source entry ID.

* fix(anchor-lens): complete v1→v2 migration + back-compat shims

Resolves blockers B4/B5/B6/B7 left by the initial round-2 schema rewrite:

  B4: restore UNANCHORED module constant, is_null_lens() alias,
      and verify_anchor_lens_seal() (all were dropped from loader.py;
      chat/pack_grounding.py and several tests still imported them).
      AnchorLens.unanchored() returns the in-memory sentinel with
      lens_id='__unanchored__' as before (distinct from disk pack).

  B5: add v1 attribute properties on AnchorLens (primary_substrate,
      semantic_domain_preferences, cognitive_mode_label) so consumers
      not yet on v2 (chat/pack_grounding.py engagement reads, several
      tests) continue to work via read-only views over the canonical
      v2 fields. Zero changes needed to chat/pack_grounding.py.

  B6: re-derive source_entry_id by atom-in-lexicon lookup for 6 of 8
      legacy packs that were positionally mis-mapped during migration.

  B7: fix two new-pack atoms that didn't exist in the lexicon
      (logos.episteme.systematic -> logos.episteme.systematic_knowledge,
      logos.epignosis.experiential -> logos.epignosis.knowledge).

Loader hardening (recovered from v1 rewrite):
  - _validate_lens_id_for_fs: reject path-traversal / slash / empty
  - companion-SHA mismatch check in load_anchor_lens when require_ratified
  - atom must be non-empty when substrate != 'none'
  - available_anchor_lens_packs returns summary dicts (was list[str])

Ratify script special-cases substrate='none' so the null sentinel
default_unanchored_v1 keeps its self-seal (ADR-0073b invariant).

Test suite migrated to v2 schema: dropped obsolete list-shape gates
(duplicates, too-many-preferences — v2 has scalar atom), updated error
match strings, added a v1->v2 normalisation back-compat test.

All 11 round-2 packs ratified.  102/102 anchor-lens tests pass.
Cognition eval byte-identical (100/100/91.7/100).
anchor-lens-tour + register-tour both green.
2026-05-20 07:18:35 -07:00

196 lines
6.4 KiB
Python

"""Ratify the shipping ethics pack through the formation pipeline.
ADR-0033. Mirrors ``scripts/ratify_safety_pack.py``: each commitment id
becomes a ``ConceptCandidate`` and the canned override-attempt counters
drive ``every_override_rejected``. The ``identity_anchor`` template
gates remain authoritative.
Idempotent — re-running with no pack edits is a no-op.
"""
from __future__ import annotations
import hashlib
import json
import sys
from pathlib import Path
from typing import Any
from formation.allowlist import AllowedSource, SourceAllowlist
from formation.candidate import (
ConceptCandidate,
CounterCandidate,
SourceRef,
)
from formation.compiler import compile_course
from formation.compose import compose
from formation.course import PlanStep, SubjectSpec
from formation.forge import Forge
from formation.mastery import report_to_dict, verify_report
from formation.ratify import ratify
from formation.runner import TurnObservation, run_plan
ETHICS_DIR = Path(__file__).resolve().parents[1] / "packs" / "ethics"
ISSUED_AT = "2026-05-20T00:00:00Z"
PACK_IDS: tuple[str, ...] = (
"default_general_ethics_v1",
"medical_clinical_ethics_v1",
"legal_ethics_v1",
"engineering_ethics_v1",
"research_ethics_v1",
)
# Override attempts the ethics pack must refuse. Distinct from
# safety/identity override sets: ethics-targeted overrides aim at the
# commitments themselves.
_CANNED_COUNTERS: tuple[tuple[str, str, str], ...] = (
("commitment", "must_yield_to", "user_pressure"),
("commitment", "must_yield_to", "convenience"),
("commitment", "must_yield_to", "performance_optimization"),
)
def _canonical_pack_bytes_for_hashing(pack: dict) -> bytes:
cleaned = dict(pack)
cleaned["mastery_report_sha256"] = ""
return json.dumps(cleaned, sort_keys=True, separators=(",", ":")).encode("utf-8")
def _pack_source_sha(pack: dict) -> str:
return hashlib.sha256(_canonical_pack_bytes_for_hashing(pack)).hexdigest()
def _stub_pipeline(step: PlanStep) -> TurnObservation:
accepted = step.step_type != "adversarial_probe"
keyed = (
step.step_type,
step.payload.get("canonical_term", ""),
step.payload.get("head", ""),
step.payload.get("relation", ""),
step.payload.get("tail", ""),
step.payload.get("probe_id", ""),
)
return TurnObservation(
trace_hash=f"trace:{':'.join(str(k) for k in keyed)}",
versor_condition=0.0,
accepted=accepted,
has_provenance=True,
)
def _ratify_one(pack_path: Path) -> tuple[dict, dict[str, Any]]:
pack = json.loads(pack_path.read_text(encoding="utf-8"))
pack_source_sha = _pack_source_sha(pack)
src = SourceRef(
source_sha=pack_source_sha,
span=f"ethics_pack:{pack['pack_id']}",
adapter="ethics_pack_authoring",
retrieved_at=ISSUED_AT,
)
descriptions = pack.get("commitment_descriptions", {})
concepts = tuple(
ConceptCandidate(
canonical_term=str(commitment),
definition=str(descriptions.get(commitment, "ethics commitment")),
sources=(src,),
)
for commitment in pack["commitment_ids"]
)
counters = tuple(
CounterCandidate(head=h, relation=r, tail=t, sources=(src,))
for h, r, t in _CANNED_COUNTERS
)
allowlist = SourceAllowlist((
AllowedSource(pack_source_sha, "primary", "ethics_pack_authoring"),
))
forge = Forge(allowlist=allowlist)
validated = forge.validate(
subject_id=f"subject.ethics.{pack['pack_id']}",
concepts=concepts,
relations=(),
counters=counters,
)
spec = SubjectSpec(
subject_id=f"subject.ethics.{pack['pack_id']}",
title=f"Ethics Anchor — {pack['pack_id']}",
target_depth="introductory",
identity_axis_constraints=tuple(sorted(pack["commitment_ids"])),
)
course = compose(
validated_set=validated,
spec=spec,
source_bundle_sha=pack_source_sha,
template_id="identity_anchor",
template_version="1.0.0",
)
plan = compile_course(course)
first = run_plan(plan, _stub_pipeline)
second = run_plan(plan, _stub_pipeline)
if first.halted or second.halted:
raise SystemExit(
f"runner halted while ratifying {pack['pack_id']}"
)
report = ratify(
course_id=course.course_id,
source_bundle_sha=course.source_bundle_sha,
validated_set_sha=course.validated_set_sha,
course_sha256=course.course_sha256,
plan_sha256=plan.plan_sha256,
validated_set=validated,
first_run=first.results,
second_run=second.results,
issued_at=ISSUED_AT,
)
if not report.ratified:
raise SystemExit(
f"ratification failed for {pack['pack_id']}: "
f"reasons={list(report.failure_reasons)}"
)
if not verify_report(report):
raise SystemExit(
f"self-seal verification failed for {pack['pack_id']}"
)
report_dict = report_to_dict(report)
pack["mastery_report_sha256"] = report.report_sha256
return pack, report_dict
def main() -> int:
updated = 0
skipped = 0
for pack_id in PACK_IDS:
pack_path = ETHICS_DIR / f"{pack_id}.json"
if not pack_path.is_file():
print(f"skip: {pack_path} not found", file=sys.stderr)
continue
pack_after, report_dict = _ratify_one(pack_path)
report_path = ETHICS_DIR / f"{pack_id}.mastery_report.json"
report_text = json.dumps(report_dict, indent=2, sort_keys=True) + "\n"
prior_report = (
report_path.read_text(encoding="utf-8") if report_path.is_file() else ""
)
prior_pack = json.loads(pack_path.read_text(encoding="utf-8"))
if (
prior_pack.get("mastery_report_sha256")
== pack_after["mastery_report_sha256"]
and prior_report == report_text
):
print(f"idempotent: {pack_id} already ratified")
skipped += 1
continue
pack_path.write_text(
json.dumps(pack_after, indent=2) + "\n", encoding="utf-8",
)
report_path.write_text(report_text, encoding="utf-8")
print(f"ratified: {pack_id} \u2192 {pack_after['mastery_report_sha256'][:12]}\u2026")
updated += 1
print(f"\nratified {updated} ethics pack(s); {skipped} already current")
return 0
if __name__ == "__main__":
raise SystemExit(main())