diff --git a/docs/archive/proposal_sources/0114_ontology_owner_decision_contract.md b/docs/archive/proposal_sources/0114_ontology_owner_decision_contract.md new file mode 100644 index 0000000..69f1c34 --- /dev/null +++ b/docs/archive/proposal_sources/0114_ontology_owner_decision_contract.md @@ -0,0 +1,41 @@ +# Ontology Owner Decision Contract + +Source artifact class: working draft + +## Motivating concern + +SpecGraph can display closed-loop Ontology evidence, but it does not yet have a +typed report contract for the accepted/rejected decisions that Ontology owners +will return for reviewed delta candidates. + +## Bounded scope + +Add a deterministic `ontology_owner_decision_report` artifact under `runs/` that +models Ontology owner decisions as read-only evidence. The report carries +decision ids, candidate ids, intake ids, decision state, Ontology decision refs, +decision actor/time, accepted/rejected state, matched closed-loop evidence +state, and explicit false SpecGraph import, gate-close, and canonical mutation +flags. Decisions that do not match pending Ontology owner closed-loop evidence +are ignored with diagnostics rather than emitted as valid decision evidence. + +This slice must not import decisions into SpecGraph, mark candidates accepted, +close semantic gates, write Ontology packages, update ontology lockfiles, mutate +canonical specs, invoke prompt agents, parse arbitrary text, or run ontologyc. + +## Acceptance sketch + +- Declare the owner decision report layout and contract in + `tools/ontology_semantic_control_policy.json`. +- Build `runs/ontology_owner_decision_report.json` from a typed owner-decision + fixture and existing closed-loop evidence source refs. +- Validate accepted/rejected/clarification states against pending closed-loop + evidence and reject import authority. +- Cover artifact shape, write path, authority rejection, and `make + ontology-imports` output in focused tests. +- Register proposal `0114` in promotion and runtime registries. + +## Next gap + +```text +build_ontology_decision_import_preview +``` diff --git a/docs/proposals/0114_ontology_owner_decision_contract.md b/docs/proposals/0114_ontology_owner_decision_contract.md new file mode 100644 index 0000000..222ba34 --- /dev/null +++ b/docs/proposals/0114_ontology_owner_decision_contract.md @@ -0,0 +1,129 @@ +# Ontology Owner Decision Contract + +RFC: SG-RFC-0114 +Version: 0.1.0 + +## Status + +Implemented + +Decision scope: typed read-only owner decision report for accepted/rejected +Ontology owner decisions. + +This document does not import owner decisions into SpecGraph, write Ontology +packages, update ontology lockfiles, mutate canonical SpecGraph specs, mark +candidate terms accepted, close semantic gates, invoke prompt agents, parse +arbitrary text, or run ontologyc. + +## Source Material + +This proposal implements the next bounded runtime slice after +`0113_ontology_review_dashboard`. + +Source draft: + +- `docs/archive/proposal_sources/0114_ontology_owner_decision_contract.md` + +## Summary + +SpecGraph now emits a deterministic owner decision report artifact: + +```text +runs/ontology_owner_decision_report.json +``` + +The artifact provides a typed contract for Ontology-supplied accepted/rejected +decisions while keeping those decisions as review evidence only. Decisions must +match closed-loop evidence that is pending Ontology owner review; stale, +blocked, or unmatched fixture decisions are reported as ignored inputs instead +of being emitted as usable decision evidence. A later import preview slice +decides how valid decisions would affect SpecGraph. + +## Goals + +- Add `ontology_owner_decision_report` to the semantic policy layout. +- Define accepted, rejected, and clarification decision states. +- Preserve Ontology decision refs, candidate ids, intake ids, decision actor, + decision time, accepted-delta status, and matched closed-loop evidence state. +- Filter blocked, stale, or unmatched fixture decisions into ignored decision + diagnostics. +- Reject SpecGraph import, semantic gate closure, canonical mutation, prompt + execution, and Ontology package/lockfile write authority. +- Cover report shape, write path, authority boundary, and registry trace in + tests. + +## Non-Goals + +- Applying owner decisions to SpecGraph specs. +- Marking candidates accepted in canonical SpecGraph state. +- Closing semantic gates. +- Writing Ontology packages or lockfiles. +- Adding SpecSpace mutation UI. + +## Runtime Contract + +The report artifact declares: + +```json +{ + "artifact_kind": "ontology_owner_decision_report", + "schema_version": 1, + "proposal_id": "0114", + "source_artifacts": { + "ontology_closed_loop_evidence": "runs/ontology_closed_loop_evidence.json" + }, + "canonical_mutations_allowed": false, + "tracked_artifacts_written": false, + "decisions": [], + "ignored_decisions": [] +} +``` + +Each decision records: + +- decision id; +- candidate id; +- intake id; +- decision state; +- Ontology decision ref; +- decision actor and timestamp; +- accepted-delta flag; +- matched closed-loop evidence id; +- matched evidence and intake states; +- explicit false SpecGraph import, gate-close, and canonical mutation flags. + +Ignored decisions record decision identity, source state when present, and the +reason they were not emitted as decision evidence. + +## Authority Boundary + +The report may be used as owner-decision evidence by later import previews. + +The report may not: + +- import decisions into SpecGraph; +- mark candidates accepted; +- close semantic gates; +- mutate canonical specs; +- write Ontology packages; +- update ontology lockfiles; +- execute prompt agents. + +## Acceptance + +This slice is complete when: + +- `tools/ontology_semantic_control_policy.json` declares + `ontology_owner_decision_report`; +- `tools/ontology_imports.py` builds and validates the owner decision report; +- `make ontology-imports` writes `runs/ontology_owner_decision_report.json`; +- focused tests cover accepted/rejected report shape, ignored invalid decisions, + write path, and authority boundary; +- proposal `0114` is tracked in promotion and runtime registries; +- proposal gates, DocC sync, and focused Python tests pass. + +## Next Gap + +```text +build_ontology_decision_import_preview +``` diff --git a/docs/supervisor_manual.md b/docs/supervisor_manual.md index 61832bf..de81ac6 100644 --- a/docs/supervisor_manual.md +++ b/docs/supervisor_manual.md @@ -1646,6 +1646,10 @@ authoritative. - richer read-only SpecGraph/SpecSpace dashboard projection combining the semantic review surface, supervisor semantic gate, delta draft intake, and closed-loop evidence without importing owner decisions or mutating specs +- `runs/ontology_owner_decision_report.json` + - typed read-only Ontology owner decision report carrying accepted/rejected + decision evidence for later import previews without closing gates or + mutating canonical specs ### Queue and proposal surfaces diff --git a/tests/test_ontology_import_policy.py b/tests/test_ontology_import_policy.py index 08dff9d..dbbb2fe 100644 --- a/tests/test_ontology_import_policy.py +++ b/tests/test_ontology_import_policy.py @@ -142,6 +142,9 @@ def test_ontology_semantic_control_policy_defines_review_only_contract() -> None assert policy["repository_layout"]["ontology_review_dashboard"] == ( "runs/ontology_review_dashboard.json" ) + assert policy["repository_layout"]["ontology_owner_decision_report"] == ( + "runs/ontology_owner_decision_report.json" + ) assert policy["repository_layout"]["ontology_delta_candidate_review_packet"] == ( "runs/ontology_delta_candidate_review_packet.json" ) @@ -355,6 +358,49 @@ def test_ontology_semantic_control_policy_defines_review_only_contract() -> None assert dashboard_contract["consumer_boundary"]["may_mark_candidate_accepted"] is False assert dashboard_contract["consumer_boundary"]["may_import_owner_decision"] is False assert dashboard_contract["consumer_boundary"]["may_close_semantic_gate"] is False + owner_decision_contract = policy["ontology_owner_decision_report_contract"] + assert owner_decision_contract["artifact_kind"] == "ontology_owner_decision_report" + assert owner_decision_contract["source_closed_loop_evidence_artifact_kind"] == ( + "ontology_closed_loop_evidence" + ) + assert owner_decision_contract["target"] == { + "target_kind": "proposal", + "target_ref": "SG-RFC-0114", + } + assert { + "accepted", + "rejected", + "needs_clarification", + }.issubset(set(owner_decision_contract["decision_states"])) + assert { + "decision_id", + "candidate_id", + "intake_id", + "decision_state", + "ontology_decision_ref", + "accepted_ontology_delta", + "imports_into_specgraph", + "closes_semantic_gate", + "mutates_canonical_specs", + }.issubset(set(owner_decision_contract["required_decision_fields"])) + assert ( + owner_decision_contract["consumer_boundary"]["for_specgraph_decision_import_preview"] + is True + ) + assert owner_decision_contract["consumer_boundary"]["for_specspace_review_dashboard"] is True + assert owner_decision_contract["consumer_boundary"]["may_execute_prompt_agent"] is False + assert owner_decision_contract["consumer_boundary"]["may_write_ontology_package"] is False + assert owner_decision_contract["consumer_boundary"]["may_update_ontology_lockfile"] is False + assert owner_decision_contract["consumer_boundary"]["may_mutate_canonical_specs"] is False + assert owner_decision_contract["consumer_boundary"]["may_mark_candidate_accepted"] is False + assert owner_decision_contract["consumer_boundary"]["may_import_into_specgraph"] is False + assert owner_decision_contract["consumer_boundary"]["may_close_semantic_gate"] is False + owner_decision_fixture = policy["owner_decision_fixture"] + assert owner_decision_fixture["artifact_kind"] == "ontology_owner_decision_fixture" + assert {decision["decision_state"] for decision in owner_decision_fixture["decisions"]} == { + "accepted", + "rejected", + } contract = policy["semantic_lint_contract"] assert contract["smoke_artifact_kind"] == "ontology_semantic_lint_smoke" assert { @@ -372,6 +418,7 @@ def test_ontology_semantic_control_policy_defines_review_only_contract() -> None assert boundary["ontology_delta_draft_intake_is_authority"] is False assert boundary["ontology_closed_loop_evidence_is_authority"] is False assert boundary["ontology_review_dashboard_is_authority"] is False + assert boundary["ontology_owner_decision_report_is_authority"] is False assert boundary["prompt_agent_execution_allowed"] is False assert boundary["automatic_canonical_node_update"] is False @@ -1091,6 +1138,54 @@ def test_ontology_review_dashboard_builds_rich_review_projection() -> None: assert dashboard["authority_boundary"]["ontology_review_dashboard_is_authority"] is False +def test_ontology_owner_decision_report_builds_read_only_decision_contract() -> None: + module = load_ontology_imports_module() + + surfaces = module.build_ontology_import_surfaces(FIXTURE) + + report = surfaces["ontology_owner_decision_report"] + assert report["artifact_kind"] == "ontology_owner_decision_report" + assert report["proposal_id"] == "0114" + assert report["target"] == { + "target_kind": "proposal", + "target_ref": "SG-RFC-0114", + } + assert report["source_artifacts"]["ontology_closed_loop_evidence"] == ( + "runs/ontology_closed_loop_evidence.json" + ) + assert report["canonical_mutations_allowed"] is False + assert report["tracked_artifacts_written"] is False + assert report["summary"] == { + "status": "no_decisions", + "decision_count": 0, + "accepted_count": 0, + "rejected_count": 0, + "clarification_count": 0, + "ignored_decision_count": 2, + "next_gap": "build_ontology_decision_import_preview", + } + assert report["decisions"] == [] + ignored = {entry["decision_id"]: entry for entry in report["ignored_decisions"]} + assert ( + ignored["ontology-owner-decision-accept-casfunction"]["reason"] + == "closed_loop_evidence_not_pending_owner_decision" + ) + assert ( + ignored["ontology-owner-decision-reject-legacyterm"]["reason"] + == "missing_closed_loop_evidence" + ) + assert report["consumer_boundary"]["for_specgraph_decision_import_preview"] is True + assert report["consumer_boundary"]["for_specspace_review_dashboard"] is True + assert report["consumer_boundary"]["may_execute_prompt_agent"] is False + assert report["consumer_boundary"]["may_write_ontology_package"] is False + assert report["consumer_boundary"]["may_update_ontology_lockfile"] is False + assert report["consumer_boundary"]["may_mutate_canonical_specs"] is False + assert report["consumer_boundary"]["may_mark_candidate_accepted"] is False + assert report["consumer_boundary"]["may_import_into_specgraph"] is False + assert report["consumer_boundary"]["may_close_semantic_gate"] is False + assert report["authority_boundary"]["ontology_owner_decision_report_is_authority"] is False + + def test_ontology_review_dashboard_keeps_blocked_gate_above_no_candidates() -> None: module = load_ontology_imports_module() semantic_policy = json.loads( @@ -1212,6 +1307,66 @@ def test_ontology_review_dashboard_uses_gate_action_for_review_pending_no_candid ) +def test_ontology_owner_decision_report_builds_matched_pending_decisions() -> None: + module = load_ontology_imports_module() + semantic_policy = json.loads( + (ROOT / "tools" / "ontology_semantic_control_policy.json").read_text() + ) + surfaces = module.build_ontology_import_surfaces(FIXTURE) + closed_loop_evidence = json.loads(json.dumps(surfaces["ontology_closed_loop_evidence"])) + cas_entry = closed_loop_evidence["evidence_entries"][0] + cas_entry["evidence_state"] = "pending_ontology_owner_decision" + cas_entry["source_intake_state"] = "awaiting_ontology_owner_review" + cas_entry["required_human_action"] = "collect_ontology_owner_delta_decisions" + legacy_entry = json.loads(json.dumps(cas_entry)) + legacy_entry["candidate_id"] = "ontology-delta-candidate-examcalc-legacyterm" + legacy_entry["intake_id"] = ( + "ontology-delta-draft-intake-ontology-delta-candidate-examcalc-legacyterm" + ) + legacy_entry["evidence_id"] = ( + "ontology-closed-loop-evidence-ontology-delta-candidate-examcalc-legacyterm" + ) + legacy_entry["term"] = "LegacyTerm" + closed_loop_evidence["evidence_entries"].append(legacy_entry) + closed_loop_evidence["summary"]["status"] = "pending_ontology_owner_decision" + closed_loop_evidence["summary"]["evidence_entry_count"] = 2 + closed_loop_evidence["summary"]["pending_decision_count"] = 2 + closed_loop_evidence["summary"]["blocked_entry_count"] = 0 + closed_loop_evidence["summary"]["required_human_action"] = ( + "collect_ontology_owner_delta_decisions" + ) + + report = module.build_ontology_owner_decision_report( + semantic_policy, + semantic_policy_path=ROOT / "tools" / "ontology_semantic_control_policy.json", + closed_loop_evidence=closed_loop_evidence, + ) + + assert report["summary"] == { + "status": "decisions_available", + "decision_count": 2, + "accepted_count": 1, + "rejected_count": 1, + "clarification_count": 0, + "ignored_decision_count": 0, + "next_gap": "build_ontology_decision_import_preview", + } + assert report["ignored_decisions"] == [] + decisions = {entry["decision_id"]: entry for entry in report["decisions"]} + accepted = decisions["ontology-owner-decision-accept-casfunction"] + assert accepted["candidate_id"] == "ontology-delta-candidate-examcalc-casfunction" + assert accepted["decision_state"] == "accepted" + assert accepted["accepted_ontology_delta"] is True + assert accepted["source_evidence_state"] == "pending_ontology_owner_decision" + assert accepted["source_intake_state"] == "awaiting_ontology_owner_review" + assert accepted["imports_into_specgraph"] is False + assert accepted["closes_semantic_gate"] is False + assert accepted["mutates_canonical_specs"] is False + rejected = decisions["ontology-owner-decision-reject-legacyterm"] + assert rejected["decision_state"] == "rejected" + assert rejected["accepted_ontology_delta"] is False + + def test_ontology_semantic_review_surface_rejects_authority_expansion( tmp_path: Path, ) -> None: @@ -1470,6 +1625,68 @@ def test_ontology_review_dashboard_rejects_source_decision_authority() -> None: ) +def test_ontology_owner_decision_report_rejects_policy_import_authority( + tmp_path: Path, +) -> None: + module = load_ontology_imports_module() + module.ROOT = tmp_path + fixture_path = write_temp_fixture(tmp_path, load_fixture_payload()) + policy_path = write_temp_policy(tmp_path) + semantic_policy = json.loads( + (ROOT / "tools" / "ontology_semantic_control_policy.json").read_text() + ) + semantic_policy["ontology_owner_decision_report_contract"]["consumer_boundary"][ + "may_import_into_specgraph" + ] = True + semantic_policy_path = write_temp_semantic_control_policy(tmp_path, semantic_policy) + + with pytest.raises(ValueError, match="may_import_into_specgraph"): + module.build_ontology_import_surfaces( + fixture_path, + policy_path=policy_path, + semantic_policy_path=semantic_policy_path, + ) + + +def test_ontology_owner_decision_report_rejects_inconsistent_accepted_flag( + tmp_path: Path, +) -> None: + module = load_ontology_imports_module() + module.ROOT = tmp_path + fixture_path = write_temp_fixture(tmp_path, load_fixture_payload()) + policy_path = write_temp_policy(tmp_path) + semantic_policy = json.loads( + (ROOT / "tools" / "ontology_semantic_control_policy.json").read_text() + ) + semantic_policy["owner_decision_fixture"]["decisions"][1]["accepted_ontology_delta"] = True + semantic_policy_path = write_temp_semantic_control_policy(tmp_path, semantic_policy) + + with pytest.raises(ValueError, match="accepted_ontology_delta"): + module.build_ontology_import_surfaces( + fixture_path, + policy_path=policy_path, + semantic_policy_path=semantic_policy_path, + ) + + +def test_ontology_owner_decision_report_requires_decision_identity_fields() -> None: + module = load_ontology_imports_module() + surfaces = module.build_ontology_import_surfaces(FIXTURE) + report = json.loads(json.dumps(surfaces["ontology_owner_decision_report"])) + report["decisions"] = [ + { + "decision_state": "accepted", + "accepted_ontology_delta": True, + "imports_into_specgraph": False, + "closes_semantic_gate": False, + "mutates_canonical_specs": False, + } + ] + + with pytest.raises(ValueError, match=r"decisions\[0\]\.decision_id"): + module.require_ontology_owner_decision_report(report) + + def test_ontology_delta_candidate_review_packet_uses_policy_review_action_order( tmp_path: Path, ) -> None: @@ -1576,6 +1793,9 @@ def test_ontology_semantic_write_uses_surface_output_artifact(tmp_path: Path) -> semantic_policy["repository_layout"]["ontology_review_dashboard"] = ( "runs/custom_ontology_review_dashboard.json" ) + semantic_policy["repository_layout"]["ontology_owner_decision_report"] = ( + "runs/custom_ontology_owner_decision_report.json" + ) semantic_policy["repository_layout"]["semantic_lint_smoke"] = ( "runs/custom_semantic_lint_smoke.json" ) @@ -1612,6 +1832,7 @@ def test_ontology_semantic_write_uses_surface_output_artifact(tmp_path: Path) -> assert "runs/custom_delta_draft_intake.json" in written_paths assert "runs/custom_closed_loop_evidence.json" in written_paths assert "runs/custom_ontology_review_dashboard.json" in written_paths + assert "runs/custom_ontology_owner_decision_report.json" in written_paths assert "runs/custom_semantic_lint_smoke.json" in written_paths assert not (tmp_path / "runs" / "ontology_delta_candidate_review_packet.json").exists() assert not (tmp_path / "runs" / "ontology_semantic_context_pack.json").exists() @@ -1621,6 +1842,7 @@ def test_ontology_semantic_write_uses_surface_output_artifact(tmp_path: Path) -> assert not (tmp_path / "runs" / "ontology_delta_draft_intake.json").exists() assert not (tmp_path / "runs" / "ontology_closed_loop_evidence.json").exists() assert not (tmp_path / "runs" / "ontology_review_dashboard.json").exists() + assert not (tmp_path / "runs" / "ontology_owner_decision_report.json").exists() assert not (tmp_path / "runs" / "ontology_semantic_lint_smoke.json").exists() @@ -1791,6 +2013,7 @@ def test_make_ontology_imports_writes_declared_surfaces() -> None: "runs/ontology_delta_draft_intake.json": "ontology_delta_draft_intake", "runs/ontology_closed_loop_evidence.json": "ontology_closed_loop_evidence", "runs/ontology_review_dashboard.json": "ontology_review_dashboard", + "runs/ontology_owner_decision_report.json": "ontology_owner_decision_report", "runs/ontology_semantic_lint_smoke.json": "ontology_semantic_lint_smoke", } for relative_path, artifact_kind in expected.items(): @@ -1805,6 +2028,7 @@ def test_make_ontology_imports_writes_declared_surfaces() -> None: "ontology_delta_draft_intake": "0110", "ontology_closed_loop_evidence": "0111", "ontology_review_dashboard": "0113", + "ontology_owner_decision_report": "0114", "ontology_semantic_lint_smoke": "0103", }.get(artifact_kind, "0060") assert payload["proposal_id"] == expected_proposal_id diff --git a/tools/README.md b/tools/README.md index c630fa3..21c1c99 100644 --- a/tools/README.md +++ b/tools/README.md @@ -137,7 +137,10 @@ Supervisor modes: loop surface for those intake requests, `runs/ontology_review_dashboard.json` as the richer read-only SpecGraph/SpecSpace dashboard projection over semantic review, gate, intake, - and closed-loop evidence, plus + and closed-loop evidence, + `runs/ontology_owner_decision_report.json` as the typed read-only + accepted/rejected Ontology owner decision report for later import previews, + plus `runs/ontology_semantic_lint_smoke.json`, classifying accepted, alias, unknown, deprecated, and relation-conflict terms against the imported ontology fixture. These surfaces resolve known imported diff --git a/tools/ontology_imports.py b/tools/ontology_imports.py index 7f8e78e..bf3a091 100755 --- a/tools/ontology_imports.py +++ b/tools/ontology_imports.py @@ -644,6 +644,7 @@ def require_semantic_control_policy(policy: dict[str, Any]) -> dict[str, Any]: require_layout_path(layout, "ontology_delta_draft_intake") require_layout_path(layout, "ontology_closed_loop_evidence") require_layout_path(layout, "ontology_review_dashboard") + require_layout_path(layout, "ontology_owner_decision_report") require_layout_path(layout, "semantic_lint_smoke") boundary = require_object(policy, "authority_boundary", "semantic_control_policy") for field in ( @@ -656,6 +657,7 @@ def require_semantic_control_policy(policy: dict[str, Any]) -> dict[str, Any]: "ontology_delta_draft_intake_is_authority", "ontology_closed_loop_evidence_is_authority", "ontology_review_dashboard_is_authority", + "ontology_owner_decision_report_is_authority", "prompt_agent_execution_allowed", "automatic_import_lock_update", "automatic_canonical_node_update", @@ -1730,9 +1732,205 @@ def require_semantic_control_policy(policy: dict[str, Any]) -> dict[str, Any]: "next_gap", "semantic_control_policy.ontology_review_dashboard_contract", ) - require_layout_path( - require_object(policy, "repository_layout", "semantic_control_policy"), - "ontology_review_dashboard", + owner_decision_contract = require_object( + policy, "ontology_owner_decision_report_contract", "semantic_control_policy" + ) + if ( + require_string( + owner_decision_contract, + "artifact_kind", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + != "ontology_owner_decision_report" + ): + raise ValueError( + "semantic_control_policy.ontology_owner_decision_report_contract.artifact_kind " + "must be ontology_owner_decision_report" + ) + if ( + require_string( + owner_decision_contract, + "source_closed_loop_evidence_artifact_kind", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + != "ontology_closed_loop_evidence" + ): + raise ValueError( + "semantic_control_policy.ontology_owner_decision_report_contract." + "source_closed_loop_evidence_artifact_kind must be ontology_closed_loop_evidence" + ) + owner_decision_target = require_object( + owner_decision_contract, + "target", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + require_string( + owner_decision_target, + "target_kind", + "semantic_control_policy.ontology_owner_decision_report_contract.target", + ) + require_string( + owner_decision_target, + "target_ref", + "semantic_control_policy.ontology_owner_decision_report_contract.target", + ) + decision_states = set( + require_string_list( + owner_decision_contract, + "decision_states", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + ) + required_decision_states = {"accepted", "rejected", "needs_clarification"} + missing_decision_states = sorted(required_decision_states - decision_states) + if missing_decision_states: + raise ValueError( + "semantic_control_policy.ontology_owner_decision_report_contract.decision_states " + f"missing: {', '.join(missing_decision_states)}" + ) + required_decision_fields = set( + require_string_list( + owner_decision_contract, + "required_decision_fields", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + ) + expected_decision_fields = { + "decision_id", + "candidate_id", + "intake_id", + "decision_state", + "ontology_decision_ref", + "decided_by", + "decided_at", + "accepted_ontology_delta", + "imports_into_specgraph", + "closes_semantic_gate", + "mutates_canonical_specs", + } + missing_decision_fields = sorted(expected_decision_fields - required_decision_fields) + if missing_decision_fields: + raise ValueError( + "semantic_control_policy.ontology_owner_decision_report_contract." + f"required_decision_fields missing: {', '.join(missing_decision_fields)}" + ) + owner_decision_consumer_boundary = require_object( + owner_decision_contract, + "consumer_boundary", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + for field in ("for_specgraph_decision_import_preview", "for_specspace_review_dashboard"): + if ( + require_bool( + owner_decision_consumer_boundary, + field, + "semantic_control_policy.ontology_owner_decision_report_contract.consumer_boundary", + ) + is not True + ): + raise ValueError( + "semantic_control_policy.ontology_owner_decision_report_contract." + f"consumer_boundary.{field} must be true" + ) + for field in ( + "may_execute_prompt_agent", + "may_write_ontology_package", + "may_update_ontology_lockfile", + "may_mutate_canonical_specs", + "may_mark_candidate_accepted", + "may_import_into_specgraph", + "may_close_semantic_gate", + ): + if ( + require_bool( + owner_decision_consumer_boundary, + field, + "semantic_control_policy.ontology_owner_decision_report_contract.consumer_boundary", + ) + is not False + ): + raise ValueError( + "semantic_control_policy.ontology_owner_decision_report_contract." + f"consumer_boundary.{field} must be false" + ) + owner_decision_fixture = require_object( + policy, "owner_decision_fixture", "semantic_control_policy" + ) + if ( + require_string( + owner_decision_fixture, + "artifact_kind", + "semantic_control_policy.owner_decision_fixture", + ) + != "ontology_owner_decision_fixture" + ): + raise ValueError( + "semantic_control_policy.owner_decision_fixture.artifact_kind must be " + "ontology_owner_decision_fixture" + ) + owner_fixture_decisions = owner_decision_fixture.get("decisions") + if not isinstance(owner_fixture_decisions, list): + raise ValueError("semantic_control_policy.owner_decision_fixture.decisions must be a list") + for index, raw_decision in enumerate(owner_fixture_decisions): + if not isinstance(raw_decision, dict): + raise ValueError( + f"semantic_control_policy.owner_decision_fixture.decisions[{index}] " + "must be an object" + ) + for field in sorted(required_decision_fields): + if field in { + "accepted_ontology_delta", + "imports_into_specgraph", + "closes_semantic_gate", + "mutates_canonical_specs", + }: + require_bool( + raw_decision, + field, + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + else: + require_string( + raw_decision, + field, + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + decision_state = require_string( + raw_decision, + "decision_state", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + if decision_state not in decision_states: + raise ValueError( + "semantic_control_policy.owner_decision_fixture.decisions" + f"[{index}].decision_state must be declared by decision_states" + ) + if require_bool( + raw_decision, + "accepted_ontology_delta", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) != (decision_state == "accepted"): + raise ValueError( + "semantic_control_policy.owner_decision_fixture.decisions" + f"[{index}].accepted_ontology_delta must match accepted decision_state" + ) + for field in ("imports_into_specgraph", "closes_semantic_gate", "mutates_canonical_specs"): + if ( + require_bool( + raw_decision, + field, + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + is not False + ): + raise ValueError( + "semantic_control_policy.owner_decision_fixture.decisions" + f"[{index}].{field} must be false" + ) + require_string( + owner_decision_contract, + "next_gap", + "semantic_control_policy.ontology_owner_decision_report_contract", ) return policy @@ -3904,6 +4102,366 @@ def require_ontology_review_dashboard( return dashboard +def build_ontology_owner_decision_report( + semantic_policy: dict[str, Any], + *, + semantic_policy_path: Path, + closed_loop_evidence: dict[str, Any], +) -> dict[str, Any]: + require_semantic_control_policy(semantic_policy) + require_ontology_closed_loop_evidence(closed_loop_evidence) + + owner_decision_contract = require_object( + semantic_policy, "ontology_owner_decision_report_contract", "semantic_control_policy" + ) + semantic_layout = require_object( + semantic_policy, "repository_layout", "semantic_control_policy" + ) + owner_decision_fixture = require_object( + semantic_policy, "owner_decision_fixture", "semantic_control_policy" + ) + raw_decisions = owner_decision_fixture.get("decisions") + if not isinstance(raw_decisions, list): + raise ValueError("semantic_control_policy.owner_decision_fixture.decisions must be a list") + + decision_states = set( + require_string_list( + owner_decision_contract, + "decision_states", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + ) + evidence_entries = closed_loop_evidence.get("evidence_entries") + if not isinstance(evidence_entries, list): + raise ValueError("closed_loop_evidence.evidence_entries must be a list") + closed_loop_by_decision_key: dict[tuple[str, str], dict[str, Any]] = {} + for index, raw_entry in enumerate(evidence_entries): + if not isinstance(raw_entry, dict): + raise ValueError(f"closed_loop_evidence.evidence_entries[{index}] must be an object") + candidate_id = require_string( + raw_entry, "candidate_id", f"closed_loop_evidence.evidence_entries[{index}]" + ) + intake_id = require_string( + raw_entry, "intake_id", f"closed_loop_evidence.evidence_entries[{index}]" + ) + closed_loop_by_decision_key[(candidate_id, intake_id)] = raw_entry + + decisions: list[dict[str, Any]] = [] + ignored_decisions: list[dict[str, Any]] = [] + for index, raw_decision in enumerate(raw_decisions): + if not isinstance(raw_decision, dict): + raise ValueError( + f"semantic_control_policy.owner_decision_fixture.decisions[{index}] " + "must be an object" + ) + decision_state = require_string( + raw_decision, + "decision_state", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + if decision_state not in decision_states: + raise ValueError( + "semantic_control_policy.owner_decision_fixture.decisions" + f"[{index}].decision_state must be declared by decision_states" + ) + accepted_ontology_delta = require_bool( + raw_decision, + "accepted_ontology_delta", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + if accepted_ontology_delta != (decision_state == "accepted"): + raise ValueError( + "semantic_control_policy.owner_decision_fixture.decisions" + f"[{index}].accepted_ontology_delta must match accepted decision_state" + ) + decision_id = require_string( + raw_decision, + "decision_id", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + candidate_id = require_string( + raw_decision, + "candidate_id", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + intake_id = require_string( + raw_decision, + "intake_id", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ) + decision = { + "decision_id": decision_id, + "candidate_id": candidate_id, + "intake_id": intake_id, + "decision_state": decision_state, + "ontology_decision_ref": require_string( + raw_decision, + "ontology_decision_ref", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + "decided_by": require_string( + raw_decision, + "decided_by", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + "decided_at": require_string( + raw_decision, + "decided_at", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + "reason": optional_string( + raw_decision, + "reason", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + "accepted_ontology_delta": accepted_ontology_delta, + "imports_into_specgraph": require_bool( + raw_decision, + "imports_into_specgraph", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + "closes_semantic_gate": require_bool( + raw_decision, + "closes_semantic_gate", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + "mutates_canonical_specs": require_bool( + raw_decision, + "mutates_canonical_specs", + f"semantic_control_policy.owner_decision_fixture.decisions[{index}]", + ), + } + for field in ("imports_into_specgraph", "closes_semantic_gate", "mutates_canonical_specs"): + if decision[field] is not False: + raise ValueError( + "semantic_control_policy.owner_decision_fixture.decisions" + f"[{index}].{field} must be false" + ) + matched_entry = closed_loop_by_decision_key.get((candidate_id, intake_id)) + if matched_entry is None: + ignored_decisions.append( + { + "decision_id": decision_id, + "candidate_id": candidate_id, + "intake_id": intake_id, + "decision_state": decision_state, + "reason": "missing_closed_loop_evidence", + } + ) + continue + source_evidence_state = require_string( + matched_entry, + "evidence_state", + f"closed_loop_evidence.evidence_entries[{candidate_id}]", + ) + source_intake_state = require_string( + matched_entry, + "source_intake_state", + f"closed_loop_evidence.evidence_entries[{candidate_id}]", + ) + if ( + source_evidence_state != "pending_ontology_owner_decision" + or source_intake_state != "awaiting_ontology_owner_review" + ): + ignored_decisions.append( + { + "decision_id": decision_id, + "candidate_id": candidate_id, + "intake_id": intake_id, + "decision_state": decision_state, + "source_evidence_state": source_evidence_state, + "source_intake_state": source_intake_state, + "reason": "closed_loop_evidence_not_pending_owner_decision", + } + ) + continue + decision["source_evidence_id"] = require_string( + matched_entry, + "evidence_id", + f"closed_loop_evidence.evidence_entries[{candidate_id}]", + ) + decision["source_evidence_state"] = source_evidence_state + decision["source_intake_state"] = source_intake_state + decisions.append(decision) + + source_artifacts = copy_json_object( + require_object(closed_loop_evidence, "source_artifacts", "closed_loop_evidence") + ) + source_artifacts["ontology_closed_loop_evidence"] = require_surface_output_artifact( + closed_loop_evidence, "ontology_closed_loop_evidence" + ) + accepted_count = sum(1 for decision in decisions if decision["decision_state"] == "accepted") + rejected_count = sum(1 for decision in decisions if decision["decision_state"] == "rejected") + clarification_count = sum( + 1 for decision in decisions if decision["decision_state"] == "needs_clarification" + ) + status = "decisions_available" if decisions else "no_decisions" + return { + "artifact_kind": require_string( + owner_decision_contract, + "artifact_kind", + "semantic_control_policy.ontology_owner_decision_report_contract", + ), + "schema_version": 1, + "proposal_id": "0114", + "policy_basis": semantic_policy["policy_basis"], + "source_policy": relative_path(semantic_policy_path), + "source_artifacts": source_artifacts, + "target": copy_json_object( + require_object( + owner_decision_contract, + "target", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + ), + "canonical_mutations_allowed": False, + "tracked_artifacts_written": False, + "decisions": decisions, + "ignored_decisions": ignored_decisions, + "consumer_boundary": copy_json_object( + require_object( + owner_decision_contract, + "consumer_boundary", + "semantic_control_policy.ontology_owner_decision_report_contract", + ) + ), + "authority_boundary": copy_json_object( + require_object(semantic_policy, "authority_boundary", "semantic_control_policy") + ), + "summary": { + "status": status, + "decision_count": len(decisions), + "accepted_count": accepted_count, + "rejected_count": rejected_count, + "clarification_count": clarification_count, + "ignored_decision_count": len(ignored_decisions), + "next_gap": require_string( + owner_decision_contract, + "next_gap", + "semantic_control_policy.ontology_owner_decision_report_contract", + ), + }, + "output_artifact": require_layout_path(semantic_layout, "ontology_owner_decision_report"), + } + + +def require_ontology_owner_decision_report( + owner_decision_report: dict[str, Any], +) -> dict[str, Any]: + if owner_decision_report.get("artifact_kind") != "ontology_owner_decision_report": + raise ValueError( + "owner_decision_report.artifact_kind must be ontology_owner_decision_report" + ) + if require_int(owner_decision_report, "schema_version", "owner_decision_report") != 1: + raise ValueError("owner_decision_report.schema_version must be 1") + if require_string(owner_decision_report, "proposal_id", "owner_decision_report") != "0114": + raise ValueError("owner_decision_report.proposal_id must be 0114") + if ( + require_bool(owner_decision_report, "canonical_mutations_allowed", "owner_decision_report") + is not False + ): + raise ValueError("owner_decision_report.canonical_mutations_allowed must be false") + if ( + require_bool(owner_decision_report, "tracked_artifacts_written", "owner_decision_report") + is not False + ): + raise ValueError("owner_decision_report.tracked_artifacts_written must be false") + require_surface_output_artifact(owner_decision_report, "ontology_owner_decision_report") + require_object(owner_decision_report, "source_artifacts", "owner_decision_report") + require_object(owner_decision_report, "summary", "owner_decision_report") + ignored_decisions = owner_decision_report.get("ignored_decisions", []) + if not isinstance(ignored_decisions, list): + raise ValueError("owner_decision_report.ignored_decisions must be a list") + decisions = owner_decision_report.get("decisions") + if not isinstance(decisions, list): + raise ValueError("owner_decision_report.decisions must be a list") + for index, raw_decision in enumerate(decisions): + if not isinstance(raw_decision, dict): + raise ValueError(f"owner_decision_report.decisions[{index}] must be an object") + for field in ( + "decision_id", + "candidate_id", + "intake_id", + "ontology_decision_ref", + "decided_by", + "decided_at", + "source_evidence_id", + "source_evidence_state", + "source_intake_state", + ): + require_string(raw_decision, field, f"owner_decision_report.decisions[{index}]") + if ( + raw_decision["source_evidence_state"] != "pending_ontology_owner_decision" + or raw_decision["source_intake_state"] != "awaiting_ontology_owner_review" + ): + raise ValueError( + "owner_decision_report.decisions" + f"[{index}] must reference pending owner-decision evidence" + ) + decision_state = require_string( + raw_decision, "decision_state", f"owner_decision_report.decisions[{index}]" + ) + if decision_state not in {"accepted", "rejected", "needs_clarification"}: + raise ValueError( + f"owner_decision_report.decisions[{index}].decision_state must be supported" + ) + accepted_ontology_delta = require_bool( + raw_decision, + "accepted_ontology_delta", + f"owner_decision_report.decisions[{index}]", + ) + if accepted_ontology_delta != (decision_state == "accepted"): + raise ValueError( + "owner_decision_report.decisions" + f"[{index}].accepted_ontology_delta must match accepted decision_state" + ) + for field in ("imports_into_specgraph", "closes_semantic_gate", "mutates_canonical_specs"): + if ( + require_bool(raw_decision, field, f"owner_decision_report.decisions[{index}]") + is not False + ): + raise ValueError(f"owner_decision_report.decisions[{index}].{field} must be false") + consumer_boundary = require_object( + owner_decision_report, "consumer_boundary", "owner_decision_report" + ) + for field in ("for_specgraph_decision_import_preview", "for_specspace_review_dashboard"): + if ( + require_bool(consumer_boundary, field, "owner_decision_report.consumer_boundary") + is not True + ): + raise ValueError(f"owner_decision_report.consumer_boundary.{field} must be true") + for field in ( + "may_execute_prompt_agent", + "may_write_ontology_package", + "may_update_ontology_lockfile", + "may_mutate_canonical_specs", + "may_mark_candidate_accepted", + "may_import_into_specgraph", + "may_close_semantic_gate", + ): + if ( + require_bool(consumer_boundary, field, "owner_decision_report.consumer_boundary") + is not False + ): + raise ValueError(f"owner_decision_report.consumer_boundary.{field} must be false") + authority_boundary = require_object( + owner_decision_report, "authority_boundary", "owner_decision_report" + ) + for field in ( + "ontology_owner_decision_report_is_authority", + "prompt_agent_execution_allowed", + "automatic_import_lock_update", + "automatic_canonical_node_update", + "canonical_mutations_allowed", + ): + if ( + require_bool(authority_boundary, field, "owner_decision_report.authority_boundary") + is not False + ): + raise ValueError(f"owner_decision_report.authority_boundary.{field} must be false") + return owner_decision_report + + def build_ontology_semantic_lint_smoke( semantic_policy: dict[str, Any], *, @@ -4252,6 +4810,11 @@ def build_ontology_import_surfaces( draft_intake=surfaces["ontology_delta_draft_intake"], closed_loop_evidence=surfaces["ontology_closed_loop_evidence"], ) + surfaces["ontology_owner_decision_report"] = build_ontology_owner_decision_report( + semantic_policy, + semantic_policy_path=semantic_policy_path, + closed_loop_evidence=surfaces["ontology_closed_loop_evidence"], + ) surfaces["semantic_lint_smoke"] = build_ontology_semantic_lint_smoke( semantic_policy, semantic_policy_path=semantic_policy_path, @@ -4317,6 +4880,10 @@ def write_ontology_import_surfaces( destinations["ontology_review_dashboard"] = require_surface_output_artifact( surfaces["ontology_review_dashboard"], "ontology_review_dashboard" ) + if "ontology_owner_decision_report" in surfaces: + destinations["ontology_owner_decision_report"] = require_surface_output_artifact( + surfaces["ontology_owner_decision_report"], "ontology_owner_decision_report" + ) if "semantic_lint_smoke" in surfaces: destinations["semantic_lint_smoke"] = require_surface_output_artifact( surfaces["semantic_lint_smoke"], "semantic_lint_smoke" diff --git a/tools/ontology_semantic_control_policy.json b/tools/ontology_semantic_control_policy.json index 1f26d87..533155e 100644 --- a/tools/ontology_semantic_control_policy.json +++ b/tools/ontology_semantic_control_policy.json @@ -11,7 +11,8 @@ "docs/proposals/0109_ontology_supervisor_semantic_gate.md", "docs/proposals/0110_ontology_delta_draft_intake.md", "docs/proposals/0111_ontology_closed_loop_evidence.md", - "docs/proposals/0113_ontology_review_dashboard.md" + "docs/proposals/0113_ontology_review_dashboard.md", + "docs/proposals/0114_ontology_owner_decision_contract.md" ], "policy_basis": "docs/proposals/0100_ontology_grounded_semantic_control.md", "repository_layout": { @@ -23,6 +24,7 @@ "ontology_delta_draft_intake": "runs/ontology_delta_draft_intake.json", "ontology_closed_loop_evidence": "runs/ontology_closed_loop_evidence.json", "ontology_review_dashboard": "runs/ontology_review_dashboard.json", + "ontology_owner_decision_report": "runs/ontology_owner_decision_report.json", "semantic_lint_smoke": "runs/ontology_semantic_lint_smoke.json" }, "derived_output_contract": { @@ -320,6 +322,44 @@ }, "next_gap": "build_specspace_rich_ontology_review_panel" }, + "ontology_owner_decision_report_contract": { + "artifact_kind": "ontology_owner_decision_report", + "source_closed_loop_evidence_artifact_kind": "ontology_closed_loop_evidence", + "target": { + "target_kind": "proposal", + "target_ref": "SG-RFC-0114" + }, + "decision_states": [ + "accepted", + "rejected", + "needs_clarification" + ], + "required_decision_fields": [ + "decision_id", + "candidate_id", + "intake_id", + "decision_state", + "ontology_decision_ref", + "decided_by", + "decided_at", + "accepted_ontology_delta", + "imports_into_specgraph", + "closes_semantic_gate", + "mutates_canonical_specs" + ], + "consumer_boundary": { + "for_specgraph_decision_import_preview": true, + "for_specspace_review_dashboard": true, + "may_execute_prompt_agent": false, + "may_write_ontology_package": false, + "may_update_ontology_lockfile": false, + "may_mutate_canonical_specs": false, + "may_mark_candidate_accepted": false, + "may_import_into_specgraph": false, + "may_close_semantic_gate": false + }, + "next_gap": "build_ontology_decision_import_preview" + }, "semantic_lint_contract": { "future_artifact_kind": "ontology_semantic_lint_report", "smoke_artifact_kind": "ontology_semantic_lint_smoke", @@ -413,6 +453,39 @@ } ] }, + "owner_decision_fixture": { + "artifact_kind": "ontology_owner_decision_fixture", + "decisions": [ + { + "decision_id": "ontology-owner-decision-accept-casfunction", + "candidate_id": "ontology-delta-candidate-examcalc-casfunction", + "intake_id": "ontology-delta-draft-intake-ontology-delta-candidate-examcalc-casfunction", + "decision_state": "accepted", + "ontology_decision_ref": "ontology-decision://edu.university.examcalc/0.1.0/casfunction/accepted", + "decided_by": "ontology-owner", + "decided_at": "2026-06-13T00:00:00Z", + "reason": "Accepted as an owner-reviewed package draft candidate for CASFunction.", + "accepted_ontology_delta": true, + "imports_into_specgraph": false, + "closes_semantic_gate": false, + "mutates_canonical_specs": false + }, + { + "decision_id": "ontology-owner-decision-reject-legacyterm", + "candidate_id": "ontology-delta-candidate-examcalc-legacyterm", + "intake_id": "ontology-delta-draft-intake-ontology-delta-candidate-examcalc-legacyterm", + "decision_state": "rejected", + "ontology_decision_ref": "ontology-decision://edu.university.examcalc/0.1.0/legacyterm/rejected", + "decided_by": "ontology-owner", + "decided_at": "2026-06-13T00:00:00Z", + "reason": "Rejected because the term is outside the accepted package vocabulary.", + "accepted_ontology_delta": false, + "imports_into_specgraph": false, + "closes_semantic_gate": false, + "mutates_canonical_specs": false + } + ] + }, "authority_boundary": { "context_pack_is_authority": false, "lint_report_is_authority": false, @@ -423,6 +496,7 @@ "ontology_delta_draft_intake_is_authority": false, "ontology_closed_loop_evidence_is_authority": false, "ontology_review_dashboard_is_authority": false, + "ontology_owner_decision_report_is_authority": false, "ontology_governance_required": true, "specgraph_proposal_review_required": true, "prompt_agent_execution_allowed": false, diff --git a/tools/proposal_promotion_registry.json b/tools/proposal_promotion_registry.json index 7b7356a..d387ebb 100644 --- a/tools/proposal_promotion_registry.json +++ b/tools/proposal_promotion_registry.json @@ -1467,5 +1467,18 @@ "required_provenance_links": [ "source_draft_ref" ] + }, + { + "proposal_id": "0114", + "source_artifact_class": "working_draft", + "source_refs": [ + "docs/archive/proposal_sources/0114_ontology_owner_decision_contract.md" + ], + "motivating_concern": "SpecGraph can display closed-loop Ontology evidence, but it lacks a typed read-only contract for accepted/rejected owner decisions returned by Ontology before any decision import preview can be built safely.", + "normalized_title": "Ontology Owner Decision Contract", + "bounded_scope": "Add a deterministic ontology_owner_decision_report artifact that carries accepted/rejected/clarification owner decisions, decision refs, candidate ids, intake ids, decision actor/time, and explicit false SpecGraph import, semantic-gate closure, and canonical mutation flags under runs/ without importing decisions into SpecGraph, marking candidates accepted, closing gates, writing Ontology packages or lockfiles, mutating canonical specs, invoking prompt agents, parsing arbitrary text, or running ontologyc.", + "required_provenance_links": [ + "source_draft_ref" + ] } ] diff --git a/tools/proposal_runtime_registry.json b/tools/proposal_runtime_registry.json index 3b2b3e8..495be3f 100644 --- a/tools/proposal_runtime_registry.json +++ b/tools/proposal_runtime_registry.json @@ -6184,5 +6184,70 @@ "pattern": "build_specspace_rich_ontology_review_panel" } ] + }, + { + "proposal_id": "0114", + "posture": "bounded_runtime_followup", + "runtime_surfaces": [ + "docs/proposals/0114_ontology_owner_decision_contract.md", + "docs/archive/proposal_sources/0114_ontology_owner_decision_contract.md", + "tools/ontology_semantic_control_policy.json", + "tools/ontology_imports.py", + "tools/README.md", + "docs/supervisor_manual.md", + "Makefile", + "tests/test_ontology_import_policy.py", + "runs/ontology_owner_decision_report.json" + ], + "runtime_markers": [ + { + "path": "tools/ontology_semantic_control_policy.json", + "pattern": "ontology_owner_decision_report_contract" + }, + { + "path": "tools/ontology_imports.py", + "pattern": "def build_ontology_owner_decision_report(" + }, + { + "path": "tools/ontology_imports.py", + "pattern": "def require_ontology_owner_decision_report(" + }, + { + "path": "tools/ontology_imports.py", + "pattern": "\"ontology_owner_decision_report\"" + } + ], + "validation_markers": [ + { + "path": "tests/test_ontology_import_policy.py", + "pattern": "def test_ontology_owner_decision_report_builds_read_only_decision_contract(" + }, + { + "path": "tests/test_ontology_import_policy.py", + "pattern": "def test_ontology_owner_decision_report_rejects_policy_import_authority(" + }, + { + "path": "tests/test_ontology_import_policy.py", + "pattern": "runs/ontology_owner_decision_report.json" + } + ], + "observation_markers": [ + { + "path": "Makefile", + "pattern": "ontology-imports:" + }, + { + "path": "tools/README.md", + "pattern": "runs/ontology_owner_decision_report.json" + }, + { + "path": "docs/supervisor_manual.md", + "pattern": "runs/ontology_owner_decision_report.json" + }, + { + "path": "docs/proposals/0114_ontology_owner_decision_contract.md", + "pattern": "build_ontology_decision_import_preview" + } + ] } ]