Implements the PropositionGraph epistemic carrier (ADR-0144): recognition/carrier.py — EpistemicTransition, EpistemicNode, EpistemicGraph. Frozen, JSON-serializable, byte-deterministic. EpistemicNode wraps a RecognitionOutcome with an append-only provenance chain; epistemic_state property tracks last transition's to_state or outcome.state when empty. recognition/connector.py — epistemic_node_to_graph_node(). Maps an admitted EpistemicNode's FeatureBundle (agent/relation/count/unit) to a GraphNode for the generation-side articulation planner. CognitiveTurnPipeline gains a recognizer: DerivedRecognizer | None param (default None — all existing callers unaffected). When attached, run() calls recognize() at the top of every turn and wraps admitted outcomes in an EpistemicGraph. CognitiveTurnResult.epistemic_graph carries it. RuntimeConfig.recognition_grounded_graph: bool = False — opt-in flag that replaces the intent-derived PropositionGraph with one derived from the admitted EpistemicNode via the connector. RatificationOutcome gains three specific PASSTHROUGH sub-values (PASSTHROUGH_NO_FIELD / NO_VOCAB / NO_VERSOR) for _ratify_intent observability (ADR-0142 debt 1). All normalise to "passthrough" before trace_hash so pre-ADR-0144 hashes are byte-identical. 24/24 acceptance tests pass; 67/67 smoke tests pass; no regressions.
14 KiB
Scope: PropositionGraph — Epistemic Carrier for ADR-0144
Status: Draft / scope-only — prerequisite for ADR-0144 Date: 2026-05-24 Author: CORE agents Anchor: thesis-decoding-not-generating (memory) Related: ADR-0142 (epistemic state taxonomy), ADR-0143 (recognition spike) Gated by: Recognition spike complete (PRs #225, #224, #226 merged)
Why this document exists
ADR-0142 and ADR-0143 each defer their integration work to ADR-0144, naming
the PropositionGraph as the missing carrier. The recognition spike is now
complete — recognition/outcome.py defines the stable output contract,
recognition/anti_unifier.py implements Phases 1 and 2, and 8/8 tests
pass. The carrier question can no longer be deferred.
But "PropositionGraph" is currently ambiguous: the name already exists in the codebase with a different meaning.
What "PropositionGraph" means today vs. what ADR-0144 needs
Current generate/graph_planner.py::PropositionGraph is a
generation-side articulation planner. It holds:
GraphNode:
node_id: str
subject: str # raw text fragment from intent classification
predicate: str # intent-derived predicate label
obj: str # "<pending>" until grounded from vault recall
source_intent: IntentTag
Its purpose is to determine what to say and in what order. It drives
plan_articulation() → ArticulationTarget → realize_semantic().
What ADR-0144 needs is a carrier that holds propositions as they are known — not how they will be voiced. That carrier must:
- Accept a
RecognitionOutcome(fromrecognition/anti_unifier.py) as the epistemic content of a node. - Carry the
EpistemicStatethat applies to this proposition at each pipeline stage. - Record provenance: which evidence spans, which recognizer, which verification step moved the state.
- Allow downstream stages (verifier, vault) to transition the state and append provenance without mutating the original record.
- Be serializable for replay (determinism guarantee from ADR-0143).
These two things — articulation planner and epistemic carrier — solve different problems. Whether they should be the same object is the first design question this scope must answer.
The load-bearing question
What structure should carry a recognized proposition from recognition through the engine's subsystems (recognition → verifier → vault → articulation) such that:
- The
RecognitionOutcome(including all feature evidence spans) is preserved and accessible at every stage,- Epistemic state transitions are themselves deterministic, typed, and carry provenance (what caused the transition),
- The carrier is serializable to/from JSON for replay,
- Cold-start turns (where recognition produces UNDETERMINED) leave the existing pipeline path unchanged, and
- The articulation layer can still derive what to say, either from the epistemic carrier or from a parallel intent-derived graph?
Three open questions
Q1 — Carrier structure: one graph or two?
Option A — Extend GraphNode
Add recognition_outcome: RecognitionOutcome | None and
epistemic_state: EpistemicState to the existing GraphNode. The
generation-side graph absorbs epistemic tracking.
Pros: minimal new API surface; CognitiveTurnResult.proposition_graph
already exists.
Cons: mixes articulation planning (string fields: subject, predicate, obj)
with epistemic tracking (feature bundle, evidence spans, state history)
into one class. The two concerns have different mutation rules — articulation
fields are set once at planning time; epistemic state transitions on every
subsystem boundary.
Option B — Separate EpistemicGraph
A new EpistemicNode / EpistemicGraph type lives in recognition/ or a
new cognition/ carrier module. It carries the recognition outcome and
epistemic provenance chain. At articulation time, a connector maps
EpistemicNode → GraphNode (deriving subject/predicate/obj from the
feature bundle).
Pros: clean separation of concerns; neither class pollutes the other's invariants; the generation-side graph keeps working as-is. Cons: a connector must be written and tested; two graphs travel together through the pipeline.
Option C — Replace GraphNode string fields
GraphNode string fields (subject, predicate, obj) are replaced
with feature-bundle representations. The proposition IS a feature bundle,
not a text fragment.
Pros: most thesis-aligned long-term — the engine stops carrying text
fragments as stand-ins for decoded propositions.
Cons: largest change surface; breaks every existing caller of GraphNode;
requires all existing tests to be updated.
Recommendation candidate: Option B. Option A mingles invariants that have different mutation rules. Option C is the right long-term direction but requires retiring the entire generation-side graph contract in one move — too large a blast radius before the PropositionGraph has even been defined. Option B lets the epistemic carrier evolve independently while the existing articulation path continues to pass its tests. The connector is the one new seam.
The scope does not commit to Option B — the ADR decides.
Q2 — Session lifetime: per-turn or persistent?
The existing PropositionGraph is rebuilt every turn from intent
classification. The _last_node_id in CognitiveTurnPipeline threads a
single pointer across turns (for correction chaining), but not the full
graph.
For an epistemic carrier, the question is harder:
- Per-turn: Each turn derives its own epistemic carrier from the recognized proposition. State from prior turns is not carried forward in the graph. Simple; matches current session semantics.
- Session-persistent: The epistemic graph grows across turns. Propositions from earlier turns remain accessible and can be VERIFIED or DECODED in later turns (e.g., the engine verifies a proposition from turn 3 after receiving correction in turn 5). Required by ADR-0142's "transition history" provenance requirement in the full-provenance case.
Per-turn is sufficient for the ADR-0144 gate. Session-persistent is required by ADR-0142's full provenance enforcement but is gated on the graph having a session home (vault? session context?).
Recommendation candidate: Per-turn for ADR-0144; session-persistent is post-ADR-0144 scope.
The scope does not commit — the ADR decides.
Q3 — Cold-start behavior: what happens when recognition refuses?
When the recognizer returns state=UNDETERMINED, there is no feature
bundle to put in an epistemic node. The pipeline must still:
- Route the turn through the existing intent-classification path
- Emit a
CognitiveTurnResultwith the refusal reason accessible - Not drop the refusal — it is teaching signal (ADR-0143 §Consequences)
Two options:
- Empty-carrier: The epistemic carrier exists but its node has
proposition=Noneandstate=UNDETERMINED. The existing pipeline path handles surface generation; the carrier is observability only. - No-carrier: If recognition refuses, the epistemic carrier is not
created and
CognitiveTurnResult.epistemic_graphisNone. The refusal reason is attached toCognitiveTurnResult.refusal_reasondirectly (which already exists).
The no-carrier option requires no new CognitiveTurnResult field and
is backward compatible. The empty-carrier option keeps the graph
always-present, which simplifies callers.
The scope does not commit — the ADR decides.
Subsystem wiring (what ADR-0144 must specify)
Regardless of which option answers Q1–Q3, ADR-0144 must wire the following path end-to-end and verify it with a determinism test:
text
└─ tokenize()
└─ recognize(recognizer, tokens) # recognition/anti_unifier.py
└─ RecognitionOutcome
└─ EpistemicNode(state=EVIDENCED, bundle=..., provenance=...)
└─ [verifier] → state transition: EVIDENCED → VERIFIED
└─ [vault cross-ref] → state transition: VERIFIED → DECODED (when replay-equal)
└─ [connector] → GraphNode(subject, predicate, obj derived from bundle)
└─ plan_articulation() → ArticulationTarget
└─ realize_semantic() → surface
Three integration points the ADR must specify:
- Recognition → carrier: How
RecognitionOutcomeis wrapped into an epistemic node. Which field carries theDerivedRecognizerused (for replay)? - Verifier → carrier: How the verifier transitions state and appends provenance. What triggers verification (all EVIDENCED propositions? intent-filtered?)?
- Carrier → articulation: How the connector derives
subject,predicate,objfrom aFeatureBundle. Feature bundle hasagent,relation,count,unit— the articulation planner currently expects free-text strings. The mapping must be deterministic.
Three implementation debts that become actionable here
From the ADR-0142 audit, three debts were deferred to ADR-0144:
-
_ratify_intentPASSTHROUGH collapse (pipeline.py:390–430). Three distinct cold-start conditions —field_state is None,vocab is None,prompt_versor is None— all produce the samePASSTHROUGHoutcome with no way to distinguish them. Fix: extendRatificationOutcomewith three distinct enum values (PASSTHROUGH_NO_FIELD,PASSTHROUGH_NO_VOCAB,PASSTHROUGH_NO_VERSOR). Unblocked by ADR-0144 since the wiring change will touch_ratify_intent's callers. -
Chat runtime grounding-source dispatcher (
runtime.py:831–1012). Six provenance gaps: the dispatcher does not record which grounding sources were attempted or why each fell through. Once the PropositionGraph is the carrier, the dispatcher can attach a dispatch trace to the graph node instead of losing it. Blocked until the node exists. -
Teaching pipeline
watched-metricstuple (replay.py). Should be a named, versionedMetricSetdataclass. Survives future metric additions without breaking trace byte-identity. Not directly dependent on ADR-0144 but the ADR's determinism gate is the right moment to fix it.
What the smallest provable test looks like
Phase 1 — recognition feeds the carrier (no verifier, no vault):
Given a Phase 1 or Phase 2 DerivedRecognizer and an admissible input:
recognize(recognizer, tokens)returnsRecognitionOutcome(state=EVIDENCED, ...)- The carrier wraps it as an
EpistemicNodewithstate=EVIDENCED - The connector derives a
GraphNodefrom the feature bundle plan_articulation(graph_with_derived_node)returns a validArticulationTargetCognitiveTurnResultcarries the epistemic node (or graph) with the originalRecognitionProvenanceintact- Two runs produce byte-identical
CognitiveTurnResultrecords
Phase 2 — refused input does not break the pipeline:
Given an inadmissible input:
recognize(recognizer, tokens)returnsRecognitionOutcome(state=UNDETERMINED, ...)- The pipeline routes through the existing intent-classification path
CognitiveTurnResult.refusal_reasoncarries the typedShapeRefusaltrace_hashis byte-identical across two runs
What this scope does NOT commit
- Option selection for Q1–Q3. The ADR decides.
- Storage layer for derived recognizers. Deferred from ADR-0143 — where recognizers live (pack / vault / substrate) is still open.
- Full session-persistent provenance. Per-turn carrier is the ADR-0144 gate; session persistence is post-ADR-0144.
- Verifier implementation. ADR-0144 wires the integration point; it does not implement the verifier.
- Lens-conditional recognition. How anchor lenses interact with derived recognizers is deferred (named in ADR-0143 §What this ADR does NOT commit).
EpistemicNodeserialization format. Defined by the ADR, not this scope.
Risks
-
Connector complexity. Mapping a
FeatureBundletoGraphNodestring fields (subject,predicate,obj) is straightforward for Phase 1/2 examples but may not generalize cleanly to all future proposition types. The ADR must either commit to a general mapping rule or scope the first connector narrowly to thehas-relation feature bundles that exist today. -
Trace hash breakage. Every change to the fields folded into
compute_trace_hash()breaks byte-identity for all prior turns. The ADR must specify which new fields (if any) are folded in, and whether they are gated on non-emptiness (asrefusal_reasonis) to preserve pre-ADR-0144 hashes. -
_ratify_intentPASSTHROUGH fires on every cold-start turn. If ADR-0144 wires recognition before intent ratification, the cold-start path must handle the case where the recognizer itself is not yet derived — i.e., there is noDerivedRecognizerfor this proposition type yet. The engine must refuse cleanly, not fail. -
mainis Codex's checked-out branch. Branch deletion via--delete-branchon any PR may fail. Usegh pr merge --squashwithout--delete-branch.
Summary
The load-bearing question for ADR-0144 is what structure carries a
recognized proposition through the engine — from RecognitionOutcome
through verifier and vault to articulation — while preserving all evidence
spans and epistemic state provenance.
Three design questions are open:
- One graph (extend
GraphNode) or two (separateEpistemicGraph)? - Per-turn carrier or session-persistent?
- Empty-carrier or no-carrier on recognition refusal?
The scope recommends two-graph and per-turn as the lower-blast-radius options for the first integration gate, but the ADR decides.
Minimum deliverable for ADR-0144 acceptance: one recognized proposition
travels from recognize() through the carrier to a CognitiveTurnResult
with the original RecognitionProvenance intact, verified byte-identical
across two runs.