From 05aaff224ecca6ca99f60b69e3adbf2ba2e5a29b Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 27 May 2026 06:44:29 -0700 Subject: [PATCH] feat(ADR-0167/W2-C): domain discriminator + cross-domain audit (#351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ADR-0167/W1-A): MathReaderRefusalEvidence schema + canonical-bytes Foundation type for routing comprehension-reader refusals into the teaching corridor. Frozen dataclass with sha256 evidence_hash computed from deterministic canonical bytes (mirrors state.to_canonical_bytes pattern). Includes SUB_TYPE_FOR_OPERATOR mapping table covering all 13 missing_operator values in the current audit artifact. Wave 1 only — no runtime mutation, no teaching-store integration, no admission path. Downstream W2-A/B/C/D type-import from this module. * feat(ADR-0167/W2-C): domain discriminator + cross-domain audit - Links to the audit doc: docs/handoff/ADR-0167-W2C-cross-domain-audit.md - Inventory details: 5 construction sites, 8 consumption sites - Verification: 0 cognition test files were modified; all tests are green - Downstream partition work flagged: contemplation indexing (in teaching/contemplation.py) and replay gate (in teaching/proposals.py) --- core/cli.py | 1 + .../ADR-0167-W2C-cross-domain-audit.md | 133 ++++++++++++++++++ teaching/discovery.py | 4 + tests/test_candidate_domain_partition.py | 92 ++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 docs/handoff/ADR-0167-W2C-cross-domain-audit.md create mode 100644 tests/test_candidate_domain_partition.py diff --git a/core/cli.py b/core/cli.py index 6a17a251..14bb08b5 100644 --- a/core/cli.py +++ b/core/cli.py @@ -1338,6 +1338,7 @@ def _load_candidate_jsonl(path: str) -> Any: pack_consistent=bool(payload.get("pack_consistent", True)), boundary_clean=bool(payload.get("boundary_clean", True)), review_state=payload.get("review_state", "unreviewed"), + domain=payload.get("domain", "cognition"), polarity=payload.get("polarity", "undetermined"), claim_domain=payload.get("claim_domain", "factual"), evidence=evidence, diff --git a/docs/handoff/ADR-0167-W2C-cross-domain-audit.md b/docs/handoff/ADR-0167-W2C-cross-domain-audit.md new file mode 100644 index 00000000..d1aa5758 --- /dev/null +++ b/docs/handoff/ADR-0167-W2C-cross-domain-audit.md @@ -0,0 +1,133 @@ +# ADR-0167-W2C: Cross-Domain Partition Audit + +This document surveys the usage of `DiscoveryCandidate` across the codebase to analyze how it partitions between the **cognition** and **math** domains, identifying construction sites, consumption patterns, test coverage, and downstream risks. + +## 1. Inventory + +The following files import, construct, or reference `DiscoveryCandidate`: + +### Implementation Files +1. **[teaching/discovery.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/teaching/discovery.py)** + - Line 47: `from typing import Any, Literal` + - Line 146: Class definition `class DiscoveryCandidate` + - Line 203: Deserialization `from_dict(cls, payload)` + - Line 274: Function `extract_discovery_candidates` returning `tuple[DiscoveryCandidate, ...]` + - Line 344: Instantiates `DiscoveryCandidate` + - Line 356: Serialisation `format_candidate_jsonl(candidate)` +2. **[teaching/contemplation.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/teaching/contemplation.py)** + - Line 43: Imports `DiscoveryCandidate` + - Line 192: Parameter type annotation in `_decompose` + - Line 332: Parameter/return type annotation in `_materialise_sub_candidate` + - Line 403: Parameter type annotation in `contemplate` + - Line 525: Return type annotation in `contemplate_exemplar_corpus` + - Line 590: Instantiates `DiscoveryCandidate` +3. **[teaching/proposals.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/teaching/proposals.py)** + - Line 38: Imports `DiscoveryCandidate` + - Line 165: Parameter type annotation in `check_eligibility` + - Line 212: Parameter type annotation in `propose_from_candidate` + - Line 478: Parameter type annotation in `_write_contemplation_report` +4. **[teaching/discovery_sink.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/teaching/discovery_sink.py)** + - Line 29: Type annotation for `DiscoveryCandidateSink(Protocol)` +5. **[chat/runtime.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/chat/runtime.py)** + - Line 49: Imports `DiscoveryCandidate` + - Line 111: Parameter type annotation in `_auto_propose_from_candidates` + - Line 661: Property `self._pending_candidates: list[DiscoveryCandidate]` + - Line 857: Parameter type annotation in `attach_discovery_sink` +6. **[core/cli.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/core/cli.py)** + - Line 1306: Imports `DiscoveryCandidate` in `_load_candidate_jsonl` + - Line 1333: Instantiates `DiscoveryCandidate` +7. **[engine_state/__init__.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/engine_state/__init__.py)** + - Line 26: Imports `DiscoveryCandidate` + - Line 132: Parameter type annotation in `save_discovery_candidates` + - Line 143: Return type annotation in `load_discovery_candidates` + - Line 148: Deserialization `DiscoveryCandidate.from_dict` +8. **[benchmarks/teaching_loop.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/benchmarks/teaching_loop.py)** + - Line 33: Imports `DiscoveryCandidate` + - Line 44: Return type annotation in `_canonical_candidate` + - Line 45: Instantiates `DiscoveryCandidate` + +### Test Files +1. **[tests/test_contemplation.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_contemplation.py)** (Lines 26, 39, 117, 146, 194, 244, 283, 341) +2. **[tests/test_discovery_candidates.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_discovery_candidates.py)** (Lines 28, 230) +3. **[tests/test_teaching_proposals.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_teaching_proposals.py)** (Lines 24, 55) +4. **[tests/test_teaching_queue.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_teaching_queue.py)** (Lines 13, 19) +5. **[tests/test_hitl_queue_backpressure.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_hitl_queue_backpressure.py)** (Lines 13, 27) +6. **[tests/test_hitl_queue_submission_invariants.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_hitl_queue_submission_invariants.py)** (Lines 11, 36) +7. **[tests/test_contemplation_pipeline_convergence.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_contemplation_pipeline_convergence.py)** (Lines 39, 129) +8. **[tests/test_adr_0146_engine_state.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_adr_0146_engine_state.py)** (Lines 9, 44, 137) +9. **[tests/test_adr_0150_autonomous_contemplation.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_adr_0150_autonomous_contemplation.py)** (Lines 8, 14) +10. **[tests/test_adr_0151_auto_proposal.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_adr_0151_auto_proposal.py)** (Lines 10, 45, 64) +11. **[tests/test_adr_0153_trace_hash_backstamp.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_adr_0153_trace_hash_backstamp.py)** (Lines 2, 7, 104) +12. **[tests/test_adr_0158_reboot_event.py](file:///Users/kaizenpro/.gemini/antigravity/worktrees/core/audit-cross-domain-partition/tests/test_adr_0158_reboot_event.py)** (Lines 24, 62) + +--- + +## 2. Construction Sites + +There are **5 main construction sites** of `DiscoveryCandidate` in implementation code: + +1. **`teaching/discovery.py::extract_discovery_candidates`** (Shared/Cognition) + - *Nature*: Cognition-specific turn-loop discovery predicate. Fires when safety/ethics boundaries are clean and a (subject, intent) falls through to the universal disclosure. +2. **`teaching/contemplation.py::contemplate_exemplar_corpus`** (Math/Admissibility) + - *Nature*: Math-specific synthesis pathway. Turns an admissibility exemplar corpus into a candidate that proposes a recognizer. +3. **`teaching/contemplation.py::_materialise_sub_candidate`** (Shared/Cognition) + - *Nature*: Cognition-specific sub-candidate decomposition helper. Used during recursive search in inline contemplation. +4. **`core/cli.py::_load_candidate_jsonl`** (Shared) + - *Nature*: Deserializer helper in the proposal CLI commands. Used for constructing candidate objects from JSONL inputs. +5. **`benchmarks/teaching_loop.py::_canonical_candidate`** (Cognition) + - *Nature*: Cognition-specific benchmark helper. Constructs a static candidate to benchmark proposal and review throughput. + +--- + +## 3. Consumption Sites + +There are **8 main consumption sites**: + +1. **`chat/runtime.py`** (Domain-Agnostic) + - Manages lists of `_pending_candidates` and handles checkpointing. +2. **`engine_state/__init__.py`** (Domain-Agnostic) + - Writes `discovery_candidates.jsonl` files to the local checkpoint directory. +3. **`teaching/contemplation.py::contemplate`** (Cognition-Specific) + - Resolves claims using `_pack_index()` and `_corpus_index()`. Currently hardcoded to the cognition pack and cognition teaching corpus. +4. **`teaching/proposals.py::propose_from_candidate`** (Cognition/Math) + - Inspects candidate validity, checks capacity/duplicates, and executes a replay gate. +5. **`teaching/gaps.py::aggregate_gaps`** (Domain-Agnostic) + - Reads files written by the discovery sinks to report on-disk gaps. +6. **`teaching/promotion.py`** (Domain-Agnostic) + - Promotes gaps to mutation proposals. +7. **`core/cli.py`** (Shared) + - Converts candidates into proposal entries using the CLI lanes. +8. **`tests/`** (Domain-Agnostic / Cognition) + - Unit tests validating serialization, contemplation convergence, and auto-proposals. + +--- + +## 4. Test Coverage + +- **`tests/test_discovery_candidates.py`**: Verifies candidate extraction rules (verdicts, intents, safety/ethics interaction). +- **`tests/test_contemplation.py`**: Verifies contemplation loop determinism, direct affirmations, direct contradictions, mixed evidence, and recursion limits. +- **`tests/test_teaching_proposals.py`**: Validates the proposal creation lifecycle. +- **`tests/test_contemplation_pipeline_convergence.py`**: Validates the `DiscoveryCandidateSink` integration. +- **`tests/test_adr_0146_engine_state.py`**: Verifies deserialization/serialization round-trips from dictionary payloads. +- **`tests/test_adr_0153_trace_hash_backstamp.py`**: Verifies trace hash back-stamping. + +--- + +## 5. Recommendation + +### Files to Modify: +1. **`teaching/discovery.py`**: Add the `domain: Literal["cognition", "math"]` field with `"cognition"` default. Update `as_dict()` conditionally and `from_dict()` unconditionally. +2. **`core/cli.py`**: Ensure `_load_candidate_jsonl` reads and populates the `domain` field. + +### Files to NOT Modify: +- Do **not** modify any file under `tests/` except the new test file `tests/test_candidate_domain_partition.py`. +- Do **not** modify `teaching/contemplation.py` or `teaching/proposals.py` as part of this PR. + +--- + +## 6. Risks + +### Downstream Risks: +1. **Contemplation Indexing**: `teaching/contemplation.py`'s `_pack_index()` and `_corpus_index()` are cognition-hardcoded. If a math candidate is passed to `contemplate()`, it will probe the cognition pack instead of the math pack, leading to false negatives or incorrect polarity results. Follow-up work must redirect index lookups based on the `candidate.domain`. +2. **Replay Gate**: The replay gate in `teaching/proposals.py` defaults to `run_cognition_replay_gate`. Math candidates must use `run_admissibility_replay_gate` to prevent false rejections. +3. **Workbench Display**: The workbench display will need to filter and color-code candidates by their domain discriminator so operators can review them separately. diff --git a/teaching/discovery.py b/teaching/discovery.py index a681f47d..17d6d19a 100644 --- a/teaching/discovery.py +++ b/teaching/discovery.py @@ -160,6 +160,7 @@ class DiscoveryCandidate: pack_consistent: bool boundary_clean: bool review_state: Literal["unreviewed"] = "unreviewed" + domain: Literal["cognition", "math"] = "cognition" # Phase C1 fields. Defaults preserve byte-equality with Phase B # ``as_dict`` output when the candidate has not been contemplated. polarity: Literal["affirms", "falsifies", "undetermined"] = "undetermined" @@ -179,6 +180,8 @@ class DiscoveryCandidate: "boundary_clean": self.boundary_clean, "review_state": self.review_state, } + if self.domain != "cognition": + out["domain"] = self.domain # Phase C1 fields are emitted only when contemplation has # produced non-default content. This keeps a freshly-emitted # Phase B candidate's JSONL line byte-identical to the pre-C1 @@ -209,6 +212,7 @@ class DiscoveryCandidate: pack_consistent=payload["pack_consistent"], boundary_clean=payload["boundary_clean"], review_state=payload.get("review_state", "unreviewed"), + domain=payload.get("domain", "cognition"), polarity=payload.get("polarity", "undetermined"), claim_domain=payload.get("claim_domain", "factual"), evidence=tuple( diff --git a/tests/test_candidate_domain_partition.py b/tests/test_candidate_domain_partition.py new file mode 100644 index 00000000..4b6dd74a --- /dev/null +++ b/tests/test_candidate_domain_partition.py @@ -0,0 +1,92 @@ +"""Tests for W2-C — candidate domain partition.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path +from typing import Any + +from core.protocol import canonical_bytes +from teaching.discovery import DiscoveryCandidate + + +def _candidate(*, domain: str | None = None) -> DiscoveryCandidate: + kwargs: dict[str, Any] = { + "candidate_id": "cand_1", + "proposed_chain": { + "subject": "wisdom", + "intent": "cause", + "connective": None, + "object": None, + }, + "trigger": "would_have_grounded", + "source_turn_trace": "t1", + "pack_consistent": True, + "boundary_clean": True, + } + if domain is not None: + kwargs["domain"] = domain + return DiscoveryCandidate(**kwargs) + + +def test_default_domain_is_cognition(): + """Bare DiscoveryCandidate has domain == 'cognition'.""" + cand = _candidate() + assert cand.domain == "cognition" + + +def test_math_domain_can_be_set(): + """Explicit domain='math' constructs successfully.""" + cand = _candidate(domain="math") + assert cand.domain == "math" + + +def test_round_trip_preserves_domain(): + """Serialization/deserialization preserves the domain field.""" + # Cognition domain + cand_cog = _candidate(domain="cognition") + dict_cog = cand_cog.as_dict() + assert "domain" not in dict_cog # Omitted to preserve legacy keys + roundtrip_cog = DiscoveryCandidate.from_dict(dict_cog) + assert roundtrip_cog.domain == "cognition" + + # Math domain + cand_math = _candidate(domain="math") + dict_math = cand_math.as_dict() + assert dict_math["domain"] == "math" # Included for math + roundtrip_math = DiscoveryCandidate.from_dict(dict_math) + assert roundtrip_math.domain == "math" + + +def test_canonical_bytes_includes_domain_deterministically(): + """Same domain value -> same canonical bytes contribution.""" + cand_math_1 = _candidate(domain="math") + cand_math_2 = _candidate(domain="math") + bytes_1 = canonical_bytes(cand_math_1) + bytes_2 = canonical_bytes(cand_math_2) + assert bytes_1 == bytes_2 + + cand_cog = _candidate(domain="cognition") + bytes_cog = canonical_bytes(cand_cog) + assert bytes_cog != bytes_1 + assert b'"domain":"math"' in bytes_1 + assert b'"domain":"cognition"' in bytes_cog + + +def test_existing_cognition_tests_untouched(): + """Assert zero modifications to pre-existing cognition test files.""" + # Find modified or untracked test files in git status + result = subprocess.run( + ["git", "status", "--porcelain", "tests/"], + capture_output=True, + text=True, + check=True, + ) + lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] + for line in lines: + path = line.split()[-1] + # We only expect test_candidate_domain_partition.py as new/modified file. + # Note: test_math_evidence_schema.py was added by W1-A and merged, so + # depending on if we are checking against HEAD, we should verify it is untouched by us. + # git status --porcelain shows changes compared to HEAD. + assert Path(path).name == "test_candidate_domain_partition.py"