feat(ADR-0167/W2-C): domain discriminator + cross-domain audit (#351)
* 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)
This commit is contained in:
parent
99c11d160a
commit
05aaff224e
4 changed files with 230 additions and 0 deletions
|
|
@ -1338,6 +1338,7 @@ def _load_candidate_jsonl(path: str) -> Any:
|
||||||
pack_consistent=bool(payload.get("pack_consistent", True)),
|
pack_consistent=bool(payload.get("pack_consistent", True)),
|
||||||
boundary_clean=bool(payload.get("boundary_clean", True)),
|
boundary_clean=bool(payload.get("boundary_clean", True)),
|
||||||
review_state=payload.get("review_state", "unreviewed"),
|
review_state=payload.get("review_state", "unreviewed"),
|
||||||
|
domain=payload.get("domain", "cognition"),
|
||||||
polarity=payload.get("polarity", "undetermined"),
|
polarity=payload.get("polarity", "undetermined"),
|
||||||
claim_domain=payload.get("claim_domain", "factual"),
|
claim_domain=payload.get("claim_domain", "factual"),
|
||||||
evidence=evidence,
|
evidence=evidence,
|
||||||
|
|
|
||||||
133
docs/handoff/ADR-0167-W2C-cross-domain-audit.md
Normal file
133
docs/handoff/ADR-0167-W2C-cross-domain-audit.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -160,6 +160,7 @@ class DiscoveryCandidate:
|
||||||
pack_consistent: bool
|
pack_consistent: bool
|
||||||
boundary_clean: bool
|
boundary_clean: bool
|
||||||
review_state: Literal["unreviewed"] = "unreviewed"
|
review_state: Literal["unreviewed"] = "unreviewed"
|
||||||
|
domain: Literal["cognition", "math"] = "cognition"
|
||||||
# Phase C1 fields. Defaults preserve byte-equality with Phase B
|
# Phase C1 fields. Defaults preserve byte-equality with Phase B
|
||||||
# ``as_dict`` output when the candidate has not been contemplated.
|
# ``as_dict`` output when the candidate has not been contemplated.
|
||||||
polarity: Literal["affirms", "falsifies", "undetermined"] = "undetermined"
|
polarity: Literal["affirms", "falsifies", "undetermined"] = "undetermined"
|
||||||
|
|
@ -179,6 +180,8 @@ class DiscoveryCandidate:
|
||||||
"boundary_clean": self.boundary_clean,
|
"boundary_clean": self.boundary_clean,
|
||||||
"review_state": self.review_state,
|
"review_state": self.review_state,
|
||||||
}
|
}
|
||||||
|
if self.domain != "cognition":
|
||||||
|
out["domain"] = self.domain
|
||||||
# Phase C1 fields are emitted only when contemplation has
|
# Phase C1 fields are emitted only when contemplation has
|
||||||
# produced non-default content. This keeps a freshly-emitted
|
# produced non-default content. This keeps a freshly-emitted
|
||||||
# Phase B candidate's JSONL line byte-identical to the pre-C1
|
# Phase B candidate's JSONL line byte-identical to the pre-C1
|
||||||
|
|
@ -209,6 +212,7 @@ class DiscoveryCandidate:
|
||||||
pack_consistent=payload["pack_consistent"],
|
pack_consistent=payload["pack_consistent"],
|
||||||
boundary_clean=payload["boundary_clean"],
|
boundary_clean=payload["boundary_clean"],
|
||||||
review_state=payload.get("review_state", "unreviewed"),
|
review_state=payload.get("review_state", "unreviewed"),
|
||||||
|
domain=payload.get("domain", "cognition"),
|
||||||
polarity=payload.get("polarity", "undetermined"),
|
polarity=payload.get("polarity", "undetermined"),
|
||||||
claim_domain=payload.get("claim_domain", "factual"),
|
claim_domain=payload.get("claim_domain", "factual"),
|
||||||
evidence=tuple(
|
evidence=tuple(
|
||||||
|
|
|
||||||
92
tests/test_candidate_domain_partition.py
Normal file
92
tests/test_candidate_domain_partition.py
Normal file
|
|
@ -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"
|
||||||
Loading…
Reference in a new issue