499 lines
21 KiB
Markdown
499 lines
21 KiB
Markdown
# ADR-0056 — Contemplation Loop: Question Decomposition + Polarity + Domain Typing (Phase C1)
|
|
|
|
**Status:** Accepted (implemented at `4eecf73`, 2026-05-18)
|
|
**Date:** 2026-05-18
|
|
**Author:** Shay
|
|
**Supersedes part of:** ADR-0055 §Decision Phase C (split into C1 + C2)
|
|
|
|
---
|
|
|
|
## Context — how we got here
|
|
|
|
ADR-0055 originally proposed a single Phase C: "`TeachingChainProposal`,
|
|
a sibling to `PackMutationProposal`, sits between discovery and the
|
|
active corpus." Conversation while landing Phase B exposed that this
|
|
single-phase framing collapsed two distinct concerns:
|
|
|
|
1. **Cognitive work** — taking a posed question (a Phase B candidate)
|
|
and *thinking through it*: decomposing into sub-questions,
|
|
gathering both confirming and falsifying evidence, composing the
|
|
result.
|
|
2. **Review surface** — taking a fully-composed proposal and
|
|
gating it through human review + eval-lane replay-equivalence
|
|
before any corpus mutation.
|
|
|
|
Conflating these into one phase had three concrete problems:
|
|
|
|
- The cognitive surface (the part where the system actually *thinks*)
|
|
would be invisible — only the input (a Phase B candidate) and the
|
|
output (an accepted corpus entry) would be auditable. The thinking
|
|
in between would happen during review prep, not as a first-class,
|
|
inspectable runtime artifact.
|
|
- The riskiest machinery (auto-apply on replay-equivalence pass)
|
|
would ship at the same time as the most interesting, least-tested
|
|
cognitive machinery. Both new at once.
|
|
- Three load-bearing distinctions the user surfaced ("contemplation
|
|
starts with a question," "truths AND falsities both count,"
|
|
"domain-relative humility") have no natural home in a pure review
|
|
ADR — they belong to the cognitive surface.
|
|
|
|
The user's framing was: *"contemplation always starts with a question
|
|
… recursion … refining by finding truths and/or finding falsities …
|
|
remain humble and think and reason with humility."*
|
|
|
|
That language is precise enough to drive architecture. This ADR
|
|
extracts the cognitive half as Phase C1; the review-and-apply half
|
|
moves to a separate future ADR (Phase C2).
|
|
|
|
### Decision record — four load-bearing calls
|
|
|
|
These were debated in conversation; recording them here so future
|
|
sessions can re-derive the reasoning instead of re-arguing it.
|
|
|
|
#### Call 1 — Stopping condition for recursive sub-question decomposition
|
|
|
|
**Choice:** Epistemic rule = "stop when the sub-question cannot be
|
|
decomposed further." Engineering failsafe = bounded depth ceiling
|
|
whose hit emits a telemetry signal.
|
|
|
|
**Why:**
|
|
|
|
- A "success" rule alone ("stop when every sub-question grounded")
|
|
cannot terminate on un-decomposable gaps — the loop spins forever
|
|
on questions whose answers don't exist yet.
|
|
- A pure depth-ceiling rule pretends the limit is epistemically
|
|
meaningful — it is not. Depth 5 may be productive; depth 3 may
|
|
fully ground. Hardcoding a number is arbitrary.
|
|
- (c) "record the gap and stop" is doctrinally right: a recorded
|
|
gap *is information* — the system has truthfully reported what it
|
|
does not yet know, and that gap becomes a new Phase B candidate
|
|
on its own merits. This composes with the rest of the loop.
|
|
- The depth ceiling stays in the design as a *failsafe* whose
|
|
triggering is itself an audit event (`recursion_overflow`),
|
|
never as the "real" stop. Silent truncation of the system's own
|
|
thinking is exactly the opaque shortcut CLAUDE.md forbids.
|
|
|
|
**Rejected alternatives:**
|
|
|
|
- (a) bounded depth alone — discussed above.
|
|
- (b) "all sub-questions grounded" — success condition masquerading
|
|
as stop condition.
|
|
- "stop on first sub-question failure" — too eager; throws away
|
|
partial structure that may itself be promotable.
|
|
|
|
#### Call 2 — What counts as falsification evidence
|
|
|
|
**Choice:** Only **reviewed evidence** in the *same pack family* —
|
|
a corpus chain with opposite connective on the same `(subject,
|
|
object)`, or a ratified pack contradiction within the cognition-pack
|
|
family — falsifies a claim. Session-tier evidence contests but does
|
|
not falsify. Cross-pack falsification (ethics-pack vs cognition-pack)
|
|
is out of scope.
|
|
|
|
**Why:**
|
|
|
|
- CLAUDE.md: "session memory may be immediate; reviewed memory must
|
|
go through `teaching/*`." Allowing session-tier corrections to
|
|
falsify by themselves would smuggle in a parallel learning path.
|
|
- ADR-0021's `EpistemicStatus` already encodes this split:
|
|
`COHERENT` promotes, `FALSIFIED` falsifies, `SPECULATIVE` and
|
|
`CONTESTED` do neither. C1 doesn't add a new tier; it uses the
|
|
existing one.
|
|
- Cross-pack arbitration is a real future problem (does an ethics
|
|
commitment override a cognition claim?) but mixing it into C1
|
|
would make the loop un-shippable. Same-pack falsification is the
|
|
90% case; cross-pack is its own ADR.
|
|
|
|
**Rejected alternatives:**
|
|
|
|
- Pack-grounded surfaces as falsification evidence — pack
|
|
`semantic_domains` don't actually express negation; they describe
|
|
a subject's facets. Treating a pack-grounded surface as
|
|
falsification would over-claim what the pack asserts.
|
|
- User corrections as direct falsification — corrections must go
|
|
through `teaching/correction.py` review first. They become
|
|
reviewed evidence only after that path completes.
|
|
|
|
#### Call 3 — Order: C1 (cognitive) before C2 (review-and-apply)
|
|
|
|
**Choice:** C1 lands first. C1's output is an *enriched*
|
|
`DiscoveryCandidate` with `proposed_chain.connective`,
|
|
`proposed_chain.object`, `polarity`, `claim_domain`, and accumulated
|
|
evidence pointers populated by the contemplation loop. Still
|
|
`review_state="unreviewed"`. **C1 never mutates the corpus.** C2
|
|
ships later as the review surface that finally permits append-on-
|
|
accept.
|
|
|
|
**Why:**
|
|
|
|
- The interesting work is the cognitive surface. Landing it first
|
|
means the contemplation loop is exercised, inspected, and tested
|
|
while it is *physically incapable* of touching the active corpus.
|
|
- The risky work is auto-apply on replay-equivalence pass. Landing
|
|
it last means it ships with the maximum testing lead time and
|
|
with an already-populated backlog of enriched candidates ready
|
|
for review.
|
|
- Matches CLAUDE.md sequencing: "Expand curriculum teaching after
|
|
replay/eval/calibration remain deterministic." C1 IS the
|
|
curriculum-teaching surface; C2 is the bigger downstream commit.
|
|
- Reverses the earlier (instinctive) "land the small thing first"
|
|
inclination. The "small" review surface is *useless* without
|
|
enriched input; the cognitive surface is *useful* even without
|
|
auto-apply (humans can review the JSONL pile directly).
|
|
|
|
**Rejected alternative:** C2 first (the original instinct). It
|
|
would ship as machinery with no real input — only Phase B's
|
|
partials with `connective=None, object=None`. A human reviewer
|
|
could hand-author the answer, but then C2 is a glorified discovery
|
|
log, not learning.
|
|
|
|
#### Call 4 — Sync vs async for the contemplation loop
|
|
|
|
**Choice:** Synchronous probe-list iteration. **No `asyncio.gather`,
|
|
no concurrency primitives, no thread pool.**
|
|
|
|
**Why:**
|
|
|
|
- CORE's hot path is deterministic by construction (exact CGA
|
|
recall, no stochastic sampling, no clock-time reads). Async
|
|
introduces nondeterministic completion order; tests that rely on
|
|
trace-hash equivalence across runs would become flaky-by-design.
|
|
- Every grounding probe C1 runs is *fast and local*: vault recall
|
|
is one matrix sweep (ADR-0054), pack lookup is a dict get,
|
|
corpus lookup is a dict get. Concurrency overhead exceeds probe
|
|
cost on a 32-component vault under a few thousand entries.
|
|
- Sync iteration preserves the canonical order
|
|
(vault → pack → teaching corpus → gap) declared in
|
|
`_maybe_pack_grounded_surface`. That order is itself
|
|
audit-relevant — the first probe that grounds wins, and "which
|
|
source grounded first" is part of the candidate's provenance.
|
|
- Async is a real architectural option *only* when a probe blocks
|
|
on I/O (future remote pack fetch, network-backed knowledge
|
|
source, etc.). None exist today. When one does, the right move
|
|
is a separate ADR introducing async at that boundary, not
|
|
retrofitting it everywhere.
|
|
|
|
**Rejected alternative:** `asyncio.gather` over sub-question probes.
|
|
Deferred to a future ADR if/when a blocking probe surface exists.
|
|
|
|
---
|
|
|
|
## Decision — Phase C1 spec
|
|
|
|
### Data shape
|
|
|
|
C1 enriches `DiscoveryCandidate` with four typed fields. The
|
|
existing Phase B fields stay; the new fields default to values
|
|
that make a Phase B candidate trivially valid as an unenriched
|
|
C1 candidate:
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class DiscoveryCandidate:
|
|
# Phase B fields (unchanged)
|
|
candidate_id: str
|
|
proposed_chain: dict[str, Any] # subject, intent, connective?, object?
|
|
trigger: DiscoveryTrigger
|
|
source_turn_trace: str
|
|
pack_consistent: bool
|
|
boundary_clean: bool
|
|
review_state: Literal["unreviewed"] = "unreviewed"
|
|
|
|
# Phase C1 fields (NEW)
|
|
polarity: Literal["affirms", "falsifies", "undetermined"] = "undetermined"
|
|
claim_domain: ClaimDomain = "factual"
|
|
evidence: tuple[EvidencePointer, ...] = ()
|
|
sub_questions: tuple[SubQuestion, ...] = ()
|
|
contemplation_depth: int = 0
|
|
recursion_overflow: bool = False
|
|
```
|
|
|
|
#### `polarity`
|
|
|
|
Three values:
|
|
- `"affirms"` — composed evidence supports the proposed
|
|
`(subject, connective, object)` relation.
|
|
- `"falsifies"` — composed evidence supports the *negation* of the
|
|
proposed relation. Falsified candidates are first-class: the
|
|
corpus learns what is NOT the case as much as what is.
|
|
- `"undetermined"` — the contemplation loop terminated without
|
|
enough reviewed evidence on either side. The candidate records
|
|
the gap but does not assert direction. Phase B candidates that
|
|
have not yet been contemplated start here.
|
|
|
|
Placing polarity *on the chain itself* (not in a separate "anti-
|
|
chain" file) keeps the corpus single-source-of-truth and lets the
|
|
existing `superseded_by` mechanism work uniformly across affirming
|
|
and falsifying entries.
|
|
|
|
#### `claim_domain` taxonomy
|
|
|
|
```python
|
|
ClaimDomain = Literal["factual", "relational", "evaluative"]
|
|
```
|
|
|
|
- **`factual`** — claims whose truth-value is independent of
|
|
context, observer, or value judgment. Example:
|
|
`light reveals truth` is factual within the cognition pack:
|
|
pack atoms compose; the relation either holds or it doesn't.
|
|
Evidence threshold: **one consistent line of reviewed evidence.**
|
|
- **`relational`** — claims whose truth-value depends on the
|
|
relation/context between the subject and the surrounding frame.
|
|
Example: `wisdom orders judgment` is partly relational —
|
|
whether wisdom *orders* judgment vs *informs* it vs *constrains*
|
|
it depends on which other concepts are co-active in the frame.
|
|
Evidence threshold: **multiple consistent reviewed lines AND no
|
|
reviewed contradictions.**
|
|
- **`evaluative`** — claims that carry a value or aesthetic
|
|
judgment, especially about people, style, intent, character.
|
|
Example: `this user is direct` or `this argument is elegant`.
|
|
Evidence threshold: **highest** — multiple reviewed lines, no
|
|
reviewed contradictions, AND a strong-tier hedge surface MUST
|
|
be attached. The doctrinal commitment is humility: in evaluative
|
|
territory the system speaks hedged or not at all.
|
|
|
|
The thresholds are *guidance for the future C2 review gate* —
|
|
C1 does not enforce them. C1 only *classifies*. Classification
|
|
itself is deterministic:
|
|
|
|
- Default `"factual"` for pack-resident cognition lemmas where
|
|
both subject and object are in `en_core_cognition_v1`.
|
|
- `"relational"` triggered by intent ∈ {COMPARISON, CAUSE} with
|
|
a frame-dependent connective (e.g., `orders`, `grounds`,
|
|
`informs`) — a small reviewed table in the cognition pack
|
|
declares which connectives are frame-dependent.
|
|
- `"evaluative"` triggered by intent classification surfacing a
|
|
person/style/intent referent (today: not classified — reserved
|
|
for when intent classification grows that axis). Until then,
|
|
`evaluative` is only assignable by an operator on review.
|
|
|
|
The classification table is itself versioned pack data, not code
|
|
constants, so refining the taxonomy doesn't require a code change.
|
|
|
|
#### `EvidencePointer`
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class EvidencePointer:
|
|
source: Literal["corpus", "pack", "vault_coherent"]
|
|
ref: str # chain_id, pack_lemma, or vault_index
|
|
polarity: Literal["affirms", "falsifies"]
|
|
epistemic_status: str # mirrors ADR-0021 EpistemicStatus
|
|
```
|
|
|
|
Only `"corpus"` (reviewed teaching chains), `"pack"` (ratified pack
|
|
contradictions within the same family), and `"vault_coherent"`
|
|
(session vault entries stamped `EpistemicStatus.COHERENT`) are
|
|
admissible evidence pointers. SPECULATIVE / CONTESTED /
|
|
FALSIFIED vault entries are ignored — they contest but do not
|
|
contribute as evidence.
|
|
|
|
#### `SubQuestion`
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class SubQuestion:
|
|
sub_id: str # deterministic hash; suffix on parent candidate_id
|
|
proposed_subject: str
|
|
proposed_intent: str
|
|
outcome: Literal["grounded", "gap_recorded", "depth_failsafe"]
|
|
evidence: tuple[EvidencePointer, ...] = ()
|
|
```
|
|
|
|
`outcome="gap_recorded"` is the load-bearing case from Call 1: the
|
|
sub-question couldn't be decomposed further, so the system records
|
|
the gap as its own first-class artifact. Gap-recorded sub-questions
|
|
spawn new top-level `DiscoveryCandidate` entries via the existing
|
|
Phase B sink — the recursion is reified into the same stream.
|
|
|
|
### Contemplation loop shape
|
|
|
|
```text
|
|
def contemplate(candidate: DiscoveryCandidate, *, max_depth: int = 8) -> DiscoveryCandidate:
|
|
if candidate.contemplation_depth >= max_depth:
|
|
return replace(candidate, recursion_overflow=True) # failsafe, audited
|
|
|
|
decomposition = decompose_question(candidate.proposed_chain)
|
|
if decomposition.terminal:
|
|
# Cannot decompose further — record gap, return undetermined.
|
|
return replace(candidate, sub_questions=(_gap_subquestion(candidate),))
|
|
|
|
sub_results: list[SubQuestion] = []
|
|
for sub in decomposition.sub_questions:
|
|
sub_candidate = _materialise_sub_candidate(sub, parent=candidate)
|
|
# Synchronous probe: vault → pack → corpus → recurse if still ungrounded.
|
|
ev = probe_for_evidence(sub_candidate)
|
|
if ev.grounded:
|
|
sub_results.append(_grounded(sub, ev))
|
|
else:
|
|
recursed = contemplate(sub_candidate, max_depth=max_depth)
|
|
sub_results.append(_summarise(recursed))
|
|
|
|
composed = compose(candidate, tuple(sub_results))
|
|
return composed
|
|
```
|
|
|
|
Every step is a pure function over its inputs. `_materialise_sub_candidate`
|
|
derives a `sub_id` deterministically from `(parent.candidate_id, sub.index)`.
|
|
`probe_for_evidence` calls — in order — vault.recall (matrix-cached
|
|
ADR-0054 path), pack lookup, corpus lookup. The order is canonical;
|
|
the first grounding source wins and is recorded in the evidence
|
|
pointer.
|
|
|
|
### Composition rules
|
|
|
|
After all sub-questions return, `compose` reduces them to a single
|
|
polarity verdict on the parent:
|
|
|
|
- All sub-evidence affirms ⇒ parent polarity `affirms`.
|
|
- All sub-evidence falsifies (or one direct same-pack contradiction
|
|
on the parent itself) ⇒ parent polarity `falsifies`.
|
|
- Mixed evidence with no clear majority of reviewed lines ⇒
|
|
parent polarity `undetermined` and `claim_domain` upgraded one
|
|
tier (factual → relational, relational → evaluative) so the
|
|
later review gate demands more before accepting.
|
|
|
|
Composition is deterministic: no thresholds with floating-point
|
|
math, no scoring weights — every rule is a typed predicate over
|
|
the evidence tuple.
|
|
|
|
### What gets written
|
|
|
|
Enriched candidates emit through the **same** Phase B sink — the
|
|
JSONL line just has more fields populated. No new file, no new
|
|
path. The on-disk record stays append-only.
|
|
|
|
Gap-recorded sub-questions emit as **separate** Phase B candidates
|
|
on the same sink — each gap is its own first-class question the
|
|
system has identified. This is the recursion-into-the-stream
|
|
property.
|
|
|
|
---
|
|
|
|
## Trust boundary
|
|
|
|
- **No corpus mutation.** C1 reads `_pack_index()`,
|
|
`_corpus_index()`, vault, and the most recent `TurnEvent`. It
|
|
writes only to the discovery sink. The active corpus on disk is
|
|
byte-identical before and after a contemplation pass.
|
|
- **No identity / safety / ethics pack mutation.** Ratified packs
|
|
are read-only from this loop.
|
|
- **No clock-time reads.** Trace hashes and candidate ids are
|
|
derived from content, not wall-clock.
|
|
- **No external I/O.** All probes hit in-process indices.
|
|
|
|
---
|
|
|
|
## Non-goals (explicit)
|
|
|
|
- No `TeachingChainProposal` typed object (that is C2).
|
|
- No `core teaching propose` / `core teaching review` CLI (C2).
|
|
- No replay-equivalence eval-lane gating (C2).
|
|
- No corpus append-on-accept (C2).
|
|
- No async or concurrency primitives — per Call 4, deferred to a
|
|
future ADR only if a blocking probe surface emerges.
|
|
- No cross-pack falsification arbitration — per Call 2, deferred.
|
|
- No LLM judgement step, anywhere in the loop.
|
|
|
|
---
|
|
|
|
## Verification (acceptance criteria for the eventual C1 PR)
|
|
|
|
- `contemplate(candidate)` is deterministic: same inputs produce
|
|
identical enriched-candidate JSONL bytes across runs.
|
|
- An empty corpus + empty pack still terminates (every probe
|
|
fails, every sub-question gap-records, the parent returns
|
|
`undetermined` with non-empty `sub_questions`).
|
|
- A factual candidate whose evidence is one reviewed corpus line
|
|
composes to `polarity="affirms"`, `claim_domain="factual"`.
|
|
- A candidate whose direct same-pack contradiction exists composes
|
|
to `polarity="falsifies"`.
|
|
- Mixed-evidence candidates upgrade `claim_domain` by exactly one
|
|
tier and stay `polarity="undetermined"`.
|
|
- Recursion-overflow flips `recursion_overflow=True` and emits the
|
|
telemetry signal — never silently truncates.
|
|
- Cognition eval lane unchanged on dev / public / holdout splits.
|
|
- `versor_condition(F) < 1e-6` invariant preserved (C1 touches no
|
|
algebra path).
|
|
|
|
---
|
|
|
|
## Open questions (deferred, but named)
|
|
|
|
1. **Frame-dependent connective table.** `claim_domain="relational"`
|
|
classification relies on a reviewed list of frame-dependent
|
|
connectives (`orders`, `grounds`, `informs`, …). That list is
|
|
pack data, not code — but who authors v1? Likely a small PR
|
|
alongside the C1 implementation.
|
|
2. **Evaluative classifier without person-axis intent.** Today's
|
|
intent classifier has no person/style axis. Until it does,
|
|
`claim_domain="evaluative"` is operator-assigned only. That is
|
|
the conservative default — the system never silently classifies
|
|
a claim as evaluative.
|
|
3. **Telemetry signal shape for recursion overflow.** Likely a new
|
|
`TurnEvent` flag or a sibling sink to the discovery sink. C2
|
|
may need to consult overflow signals when scoring proposals.
|
|
4. **Sub-question deduplication.** Two parallel candidates may
|
|
produce the same sub-question independently. The current design
|
|
emits both — the receiving sink could dedupe by `sub_id`, but
|
|
that conflicts with the per-trace audit story. Probably leave
|
|
it: duplication IS information about which parent asked.
|
|
|
|
---
|
|
|
|
## Cross-References
|
|
|
|
- [ADR-0021](./ADR-0021-epistemic-status.md) — the
|
|
`EpistemicStatus` substrate this loop reuses. `COHERENT` promotes,
|
|
`FALSIFIED` falsifies, `SPECULATIVE`/`CONTESTED` contest.
|
|
- [ADR-0027](./ADR-0027-identity-packs.md) — ratified pack
|
|
authority. C1 reads packs; never mutates them.
|
|
- [ADR-0038](./ADR-0038-hedge-injection.md) — the hedge surface
|
|
C2 will plumb into for `claim_domain="evaluative"` humility
|
|
enforcement.
|
|
- [ADR-0040](./ADR-0040-structured-logging-sink.md) /
|
|
[ADR-0041](./ADR-0041-fanout-sink-cli-verdicts.md) — sink
|
|
pattern this loop reuses.
|
|
- [ADR-0052](./ADR-0052-teaching-grounded-surface.md) — the
|
|
reviewed teaching corpus this loop reads as evidence.
|
|
- [ADR-0053](./ADR-0053-cognition-lane-closure.md) — the
|
|
`teaching/correction.py` repair path that produces *reviewed*
|
|
evidence after a session correction passes review.
|
|
- [ADR-0054](./ADR-0054-vault-recall-indexing-batching.md) — the
|
|
cached-matrix vault recall the loop's per-probe vault lookup
|
|
uses.
|
|
- [ADR-0055](./ADR-0055-inter-session-memory-discovery-promotion.md)
|
|
— the parent design; this ADR extracts and refines its
|
|
Phase C cognitive half.
|
|
- Future ADR (Phase C2) — `TeachingChainProposal` + review +
|
|
replay-equivalence + corpus append-on-accept.
|
|
|
|
---
|
|
|
|
## Verification of provenance (for future re-derivation)
|
|
|
|
The four calls above were made in conversation on 2026-05-18 after
|
|
Phase B (`07d35c0`) landed. The user surfaced three load-bearing
|
|
distinctions:
|
|
|
|
> "contemplation always starts with a question"
|
|
> "refining by finding truths and/or finding falsities"
|
|
> "needing more evidence or proof before deciding on things as to
|
|
> not be 'judgemental' or better put, to remain humble and think
|
|
> and reason with humility"
|
|
|
|
Each maps directly onto a C1 design element:
|
|
|
|
- "question" → `DiscoveryCandidate` is the posing of the question;
|
|
contemplation is the answering step.
|
|
- "truths and/or falsities" → `polarity ∈ {affirms, falsifies,
|
|
undetermined}` on the chain itself.
|
|
- "humility" → `claim_domain` taxonomy with escalating evidence
|
|
thresholds and mandatory hedge surfaces in evaluative territory.
|
|
|
|
The split of Phase C into C1 + C2, and the choice to land the
|
|
cognitive half before the auto-apply half, was an explicit
|
|
re-decision against my earlier instinct to land C2 first. Reasoning
|
|
recorded in Call 3 above. Future sessions that re-encounter the
|
|
question "should the review surface ship first?" can re-derive
|
|
the no-answer from that record without re-arguing.
|