Commit graph

252 commits

Author SHA1 Message Date
Shay
afdd2ee413 feat(capability): implement ADR-0092 — Reviewer Registry v1
Closes the load-bearing gap blocking every reasoning-capable claim
under ADR-0091: docs/reviewers.yaml was previously `reviewers: []` and
unparsed. Now schema-validated at v1, with a bootstrap shay-j entry
self-sealed via provenance.

- new core.capability.reviewers module: frozen Reviewer/ReviewerRegistry
  dataclasses, strict load_reviewer_registry parser, ReviewerRegistryError
- enforces ADR-0092 schema rules: schema_version==1, no unknown
  top-level keys, no unknown reviewer fields, role∈{primary,domain},
  primary must claim ["*"], domain must NOT claim "*", review_scope
  subset of {pack,proposal,chain,eval}, no duplicate reviewer_ids
- can_review(reviewer_id, domain_id, scope) helper implements
  ADR-0092 rules 2-4 for downstream use by ADR-0093 validator
- docs/reviewers.yaml updated to v1 schema with shay-j bootstrap
- ledger_report() evidence_counts now exposes structured
  reviewer_registry status (valid, schema_version, reviewer_count,
  reviewer_ids, error) alongside the legacy reviewers_present bool
- new evals/reviewer_registry/ lane: 6 cases (2 positive + 4 negative)
  covering empty-registry, wrong-version, domain-wildcard rejection,
  and unknown-field rejection
- runner emits deterministic JSON report; two runs produce byte-identical
  output (sha256 verified)
- 26 unit tests in tests/test_reviewer_registry.py
- capability ledger test extended to assert new reviewer_registry block
- smoke suite green (67/67); lane passes 6/6

The pre-existing test_flag_report_tracks_default_off_flags failure is
unrelated (discourse_planner flag default) and not introduced here.
2026-05-21 18:01:24 -07:00
Shay
327047ce26 feat(contemplation): Phase 5 — articulation-quality miner closes the loop
Final phase of the articulation arc.  Consumes the per-turn
``PlanMetrics`` + ``ContemplationFinding`` streams produced by
Phases 3 + 4 and aggregates across many turns to emit
SPECULATIVE ``PACK_MUTATION_CANDIDATE`` findings that the operator
reviews via the existing proposal-review-ratify chain.

This is the doctrine-aligned answer to the user's question:

  "Should we... realize a way to score whether it should use what
  it produced towards memory confidence for future use?"

Yes — and it stays inside ADR-0080: read-only, SPECULATIVE-only,
deterministic, no parallel learning path, no autonomous memory
mutation.

What it adds
------------

* New module ``chat/articulation_telemetry.py``:
    - ``ArticulationObservation`` frozen dataclass — per-turn
      bundle of (turn_id, anchor_subject, prompt_hash,
      plan_substrate_hash, metrics, findings).
    - ``format_articulation_observation_jsonl(...)`` — deterministic
      sort-keys JSONL line.
    - ``load_articulation_observations(lines)`` — schema-tolerant
      loader; malformed lines drop without aborting.
    - ``ArticulationObservationSink`` protocol — structurally
      identical to ``TurnEventSink`` but distinct named type so
      consumers can subscribe to one stream without the other.

* New module ``core/contemplation/miners/articulation_quality.py``:
    - ``mine_articulation_observations(observations, paths)`` —
      pure deterministic aggregator with three v1 rules.
    - **recurring_predicate_monotony** — when the same
      (subject, predicate) pair is flagged WEAK_SURFACE in
      >= _MIN_RECURRENCE (default 3) observations, propose
      substrate diversification with non-dominant predicates.
    - **recurring_planner_gap** — when the same subject is
      flagged PLANNER_GAP >= _MIN_RECURRENCE times across modes,
      propose substrate expansion.
    - **low_average_predicate_diversity** — when mean
      ``predicate_diversity_ratio`` < 0.5 across >= _MIN_RECURRENCE
      observations on the same anchor subject, propose
      diversification.

* Runtime wiring (``chat/runtime.py``):
    - New ``ChatRuntime.attach_articulation_sink(sink)`` method.
      Mirrors ``attach_telemetry_sink`` pattern.
    - Emission point at the end of
      ``_maybe_apply_discourse_planner``: when contemplation
      enabled + sink attached + plan engaged, builds an
      ``ArticulationObservation`` and emits one JSONL line.
      Sink errors propagate (fail-fast, no swallowing).
    - Per-runtime ``_articulation_turn_counter`` increments on
      every emission; gives downstream consumers a stable
      sequence index.

Tests
-----

* ``tests/test_articulation_quality_miner.py`` (11 tests):
    - Empty / sub-threshold cases yield no findings.
    - Each of the three rules fires at threshold.
    - Recurring_predicate_monotony separates by subject (no
      cross-subject merging).
    - Recurring_planner_gap collects distinct modes into a
      sorted comma-joined string.
    - Determinism — byte-equal finding IDs across two runs.
    - SPECULATIVE doctrine pin.
    - JSONL round-trip preserves observation identity.

* ``tests/test_articulation_quality_e2e.py`` (7 tests):
    - Sink-detached + contemplation-on → no emission.
    - Sink-attached + contemplation-off → no emission.
    - Engaged turn emits exactly one observation line.
    - BRIEF prompt emits nothing (fast-path).
    - **Full loop** — run compound prompt 3x → 3 observations →
      miner emits PACK_MUTATION_CANDIDATE with subject='truth',
      predicate='recurring_predicate_monotony', object='belongs_to'.
    - Full loop is deterministic (byte-equal finding IDs across
      two complete runs).
    - Every full-loop finding is SPECULATIVE.

Doctrine pins
-------------

| Claim                                | Pinned by                                                |
|--------------------------------------|----------------------------------------------------------|
| SPECULATIVE-only                     | test_all_findings_remain_speculative                     |
| Deterministic across runs            | test_miner_is_deterministic_across_runs                  |
| Full-loop determinism (e2e)          | test_full_loop_is_deterministic_byte_equal_finding_ids   |
| No autonomous mutation               | Sink is append-only; miner outputs ContemplationFinding  |
|                                      | objects only; nothing writes to packs/vault/teaching.    |
| Append-only stream                   | Sink protocol has emit(line: str) and nothing else.      |

Live demo (3 identical compound-prompt turns)
---------------------------------------------

Runtime emits 3 observations.  Offline miner aggregates and emits:

  [pack_mutation_candidate] subject='truth'
      predicate='recurring_predicate_monotony' object='belongs_to'
      evidence_refs: 3 observations
      proposed_action: "diversify substrate for 'truth': across 3
        observations the plan repeatedly over-concentrated on
        predicate 'belongs_to'. Candidates: add teaching chains
        rooted on 'truth' with relations OTHER than 'belongs_to'
        (grounds / requires / reveals / contrasts / precedes /
        follows) so the planner's RELATION selector has more
        variety to draw from."
      epistemic_status: speculative

The system observed its own articulation patterns across many
turns, identified the corpus expansion priority, and emitted a
specific reviewable proposal — without mutating anything.  The
operator decides whether to act on it via the existing review
chain.

Verification
------------

  pytest test_articulation_quality_miner.py       11/11 pass
  pytest test_articulation_quality_e2e.py          7/7 pass
  pytest test_plan_metrics*.py                    18/18 pass (Phase 4)
  pytest test_plan_contemplation*.py              17/17 pass (Phase 3)
  pytest test_discourse_planner_*.py              99/99 pass
  pytest test_articulation_demo.py                 all claims supported
  pytest test_narrative_example_intents.py         pass
  core test --suite smoke                         67/67 pass
  core test --suite runtime                       19/19 pass

The articulation arc is complete.  Future work documented in
``docs/sessions/SESSION-2026-05-21-articulation-arc.md`` §8:
connective rotation, generalised pronoun selection, doctrine-gated
plan revision, Phase 2.5 mid-sentence reflection.  None blocking.
2026-05-21 10:55:39 -07:00
Shay
b07fb0413c feat(contemplation): Phase 4 — per-plan articulation telemetry metrics
Quantitative companion to Phase 3 (commit 664e081).  Where Phase 3
emits SPECULATIVE *findings* about plan quality, Phase 4 emits
typed *measurements* — pure-function projection of a
``DiscoursePlan`` into a ``PlanMetrics`` dataclass.

Why this matters
----------------

The discourse planner now produces multi-clause grounded
articulations (Phase 1), the renderer pronominalizes across
consecutive same-subject moves (Phase 2), and the contemplation
pre-flight emits qualitative concerns about plan shape (Phase 3).
What was missing was the *aggregable* layer: per-turn structured
numbers that downstream consumers can stream across many turns
to score quality patterns the per-turn observer cannot see.

Phase 4 lands that layer.  Phase 5 (offline contemplation miner)
becomes possible because there's now structured signal to mine.

What it measures
----------------

  Structure
    * move_count                      — total moves in plan
    * fact_bearing_count              — moves with fact != None
  Move-kind distribution
    * anchor_count / support_count / relation_count
      / transition_count / closure_count
  Diversity
    * unique_predicates               — distinct predicates across
                                        fact-bearing moves
    * unique_subjects                 — distinct subject lemmas
    * unique_sources                  — distinct FactSources
  Topic dynamics
    * topic_shift_count               — consecutive pairs where
                                        subject changed
    * pronominalization_opportunities — consecutive pairs where
                                        subject held (= Phase 2's
                                        anaphora trigger count)
  Derived ratios
    * predicate_diversity_ratio       — unique_predicates /
                                        fact_bearing_count
    * subject_focus_ratio             — pronominalizations /
                                        (pronominalizations +
                                         topic_shifts)

Every field is a deterministic pure function of the plan: same
plan in → byte-equal ``PlanMetrics.as_dict()`` out.  This is the
load-bearing claim that lets Phase 5 aggregate across turns
without "is this the same metric?" ambiguity.

Doctrine alignment
------------------

Per ADR-0080 contemplation discipline:
  * Read-only — metrics are pure projections of the plan; no
    mutation of plan, runtime state, or memory tiers.
  * No autonomous learning — metrics are observations, not
    learned policy.  Promotion to memory still flows through
    the existing proposal-review-ratify chain.
  * Deterministic replay — pinned by test_metrics_are_deterministic_
    and_byte_equal_as_dict plus the runtime-level
    test_metrics_byte_equal_across_runs.

Wiring
------

* New ``ChatRuntime.last_plan_metrics`` property — read-only
  ``PlanMetrics`` from the most recent turn where the planner
  engaged (and ``discourse_contemplation`` was on); ``None``
  otherwise.  Reset between turns alongside ``last_plan_findings``
  via the existing top-of-call reset block.

* Same opt-in flag as Phase 3 (``discourse_contemplation``).
  When True, the runtime computes both findings AND metrics in
  the same block; when False (default), both stay at empty/None.

Demo (config: discourse_contemplation=True)
-------------------------------------------

  "What is knowledge?"          → metrics: None  (BRIEF fast-path)
  "Tell me about memory."       → moves=3 fact_bearing=3
                                  kinds=A:1/S:1/R:1/T:0/C:0
                                  unique_predicates=3 subjects=1
                                  pronominalization_ops=2 shifts=0
                                  predicate_diversity=1.000
                                  subject_focus=1.000
  "What is truth, and why does
   it matter?"                  → moves=7 fact_bearing=6
                                  kinds=A:2/S:2/R:2/T:1/C:0
                                  unique_predicates=4 subjects=1
                                  pronominalization_ops=4 shifts=1
                                  predicate_diversity=0.667  ← Phase 3
                                                                WEAK_SURFACE
                                                                quantified
                                  subject_focus=0.800
                                  + 1 finding (weak_surface)

The compound-prompt numbers are particularly informative:
``predicate_diversity=0.667`` is the algebraic expression of the
Phase 3 ``WEAK_SURFACE`` rule — the rule fires precisely because
6 fact-bearing moves used only 4 distinct predicates.
``subject_focus=0.800`` quantifies that 80% of consecutive pairs
held the same subject — high topic stickiness that Phase 2's
reflective renderer leveraged into 4 ``it`` substitutions.

Tests
-----

* ``tests/test_plan_metrics.py`` — 10 unit tests pinning each
  field, derived ratios, bridge-move handling (``fact=None``
  resets the focus channel), and determinism via ``as_dict()``
  byte-equality.

* ``tests/test_plan_metrics_runtime.py`` — 8 end-to-end tests
  proving the runtime wiring: disabled by default, populated
  when enabled, BRIEF prompts yield None, no cross-turn leak,
  byte-equal across runs, parametrized co-population check
  alongside findings.

Verification
------------

  pytest tests/test_plan_metrics*.py              18/18 pass
  pytest tests/test_plan_contemplation*.py        17/17 pass (Phase 3)
  pytest tests/test_discourse_planner_*.py        99/99 pass
  pytest tests/test_articulation_demo.py          all claims supported
  pytest tests/test_narrative_example_intents.py  pass
  pytest tests/test_runtime_config.py             pass
  cognition eval OFF vs ON                        45/45 surface byte-equal
                                                  45/45 trace_hash byte-equal
                                                  4/4 aggregate metrics
                                                      identical
  core test --suite smoke                         67/67 pass
  core test --suite runtime                       19/19 pass

Phase 5 (logged, not built)
---------------------------

Offline contemplation miner that consumes ``last_plan_findings``
+ ``last_plan_metrics`` streams across many turns and emits
reviewable pack-mutation candidates.  Still SPECULATIVE;
review-gated; never auto-promoted to memory.  Now unblocked by
the structured metric surface Phase 4 lands.
2026-05-21 10:39:39 -07:00
Shay
664e08150c feat(contemplation): Phase 3 — live plan contemplation pre-flight
Wires deterministic, read-only contemplation OVER a completed
``DiscoursePlan`` BEFORE the renderer fires.  This is the
"reasoning at meaningful checkpoints" capability — the system
now inspects the global shape of its own articulation plan and
emits SPECULATIVE findings about quality issues the move-by-move
planner couldn't see locally.

Doctrine alignment (ADR-0080)
-----------------------------

* **Read-only** — never mutates the plan, packs, vault, teaching
  corpus, or runtime state.  Returns findings as a tuple; the
  runtime stores them on a read-only property.
* **SPECULATIVE-only** — every finding is stamped
  ``EpistemicStatus.SPECULATIVE`` by the schema's ``__post_init__``;
  the doctrine pin ``test_findings_always_speculative`` keeps that
  invariant visible.
* **Deterministic replay** — same plan → byte-identical findings
  (same ``substrate_hash``, same ``finding_id``).
* **No parallel learning path** — findings flow to a read-only
  observation surface (``runtime.last_plan_findings``).  Promotion
  to memory still goes through the existing proposal → review →
  ratify chain.  The offline contemplation miner (Phase 5 target)
  is what eventually consumes the findings and emits reviewable
  pack-mutation candidates.

v1 rules (``core/contemplation/plan_preflight.py``)
----------------------------------------------------

* ``PLANNER_GAP`` — non-BRIEF mode produced anchor-only depth.
  Signals the teaching/cross-pack substrate for that lemma is too
  thin for the planner to expand.

* ``WEAK_SURFACE`` — three or more moves share a predicate.
  Signals the rendered surface will read mechanical (e.g. three
  ``belongs_to`` clauses in a row).  Fires on today's compound
  prompt ``"What is truth, and why does it matter?"`` — the
  6-sentence plan uses ``belongs_to`` 3 times.

* ``COVERAGE_GAP`` — every move in a multi-move plan draws from
  a single ``FactSource``.  Signals one-sided substrate (e.g.
  pack-only with no teaching enrichment).

Runtime wiring
--------------

* New ``RuntimeConfig.discourse_contemplation: bool = False`` —
  opt-in for now.  Default off keeps the cognition eval byte-
  identical to Phase 2 (verified 45/45 surface + 45/45 trace_hash).
* New ``ChatRuntime.last_plan_findings`` property — read-only tuple
  of ``ContemplationFinding`` records from the most recent turn.
  Reset to ``()`` at the start of every plan-engagement call so
  findings never leak across turns.
* Contemplation runs AFTER the planner produces a multi-move plan
  and BEFORE the renderer fires; the plan itself is not modified.

Demo (config: discourse_contemplation=True)
-------------------------------------------

  "What is knowledge?"          → planner fast-path; no findings
  "Tell me about memory."       → 3 moves, distinct predicates;
                                  no findings (good!)
  "What is truth, and why does
   it matter?"                  → 6 moves, ``belongs_to`` x 3:
                                  [WEAK_SURFACE] subject='truth'
                                    predicate='predicate_repeats_in_plan'
                                    object='belongs_to'
                                  proposed action: diversify the
                                  relation inventory for 'truth'
                                  (grounds / requires / reveals /
                                  contrasts) so the planner has
                                  more variety to draw from.
  "Explain truth."              → 3 moves, distinct predicates;
                                  no findings

Tests
-----

* ``tests/test_plan_contemplation.py`` — 11 unit tests pinning
  each rule, empty/trivial plans, determinism, and the
  SPECULATIVE-only doctrine.

* ``tests/test_plan_contemplation_runtime.py`` — 6 end-to-end
  tests proving the runtime wiring: disabled by default,
  populated when enabled, reset across turns, deterministic
  across runs, all findings SPECULATIVE.

Verification
------------

  pytest tests/test_plan_contemplation*.py        17/17 pass
  pytest tests/test_discourse_planner_*.py        99/99 pass
  pytest tests/test_articulation_demo.py          all claims supported
  pytest tests/test_narrative_example_intents.py  pass
  pytest tests/test_runtime_config.py             pass
  cognition eval OFF vs ON                        45/45 surface byte-equal
                                                  45/45 trace_hash byte-equal
                                                  4/4 aggregate metrics
                                                      identical
  core test --suite smoke                         67/67 pass
  core test --suite runtime                       19/19 pass

Phases roadmap (logged in commit, not built today)
--------------------------------------------------

* Phase 4 — articulation telemetry enrichment.  Emit per-turn
  metrics (grounding_ratio, anaphora_engagement, plan_completeness,
  novelty, focus_consistency) to the existing telemetry sink so
  the offline miner has structured signal.

* Phase 5 — offline contemplation miner.  Extend
  ``core/contemplation`` with a miner that consumes
  ``last_plan_findings`` streams and emits reviewable
  pack-mutation / teaching-corpus expansion proposals.  Still
  SPECULATIVE; review-gated.
2026-05-21 10:30:22 -07:00
Shay
9dfb505f06 feat(discourse): Phase 2 — reflective rendering pronominalizes focus subject
The Phase 1 multi-clause renderer (commit 63ffd88) produces grounded
content but reads mechanically because the subject lemma repeats in
every clause:

  "Truth is what is true. Furthermore, truth belongs to cognition.truth.
   In turn, truth grounds knowledge. Truth belongs to epistemic.ground.
   Furthermore, truth belongs to logos.core. In turn, truth requires
   evidence."

This is the literal articulation gap that motivated Phase 2 —
"reasoning at meaningful checkpoints during sentence construction
in order to have a stronger idea of what has come prior and is
already done to help better inform the next move."  Between move
``i`` and move ``i+1`` the renderer now reflects on what subject
has just been established (the "focus") and renders the next clause
with a pronoun when the focus carries forward:

  "Truth is what is true. Furthermore, it belongs to cognition.truth.
   In turn, it grounds knowledge. It belongs to epistemic.ground.
   Furthermore, it belongs to logos.core. In turn, it requires
   evidence."

Rules
-----

* Track ``focus_subject`` across moves (the lemma most recently used
  as a fact subject).
* When the next move's ``fact.subject`` is byte-equal to the current
  focus → swap subject token to ``"it"``.
* When the next move's subject differs → preserve the explicit lemma
  AND update focus.  Topic shifts (TRANSITION moves; compound bridge
  TRANSITION) thus reset the pronominalization channel naturally.
* Sentence-initial position (no connective): capitalised ``"It"``.
* Mid-sentence (after connective + comma): lowercase ``"it"``.

Doctrine alignment
------------------

Pure deterministic transformation of the existing plan; no new
content introduced, no LLM, no stochastic sampling.  Same plan in →
same surface out, always.  trace_hash invariance holds because:

  * BRIEF-mode prompts short-circuit the planner before render
    (commit 63ffd88's fast path) and are unaffected.
  * Multi-move plans render to a deterministically-different string
    that compute_trace_hash already folds in via ``surface``.

Wiring
------

* New ``reflective: bool = False`` parameter on ``render_plan``
  (back-compat default — every existing call site and test pinning
  Phase 1 output continues to work).
* ``_clause_for`` gains optional ``prior_focus_subject`` arg used by
  the reflective path; unchanged default behaviour.
* Runtime hook ``chat.runtime._maybe_apply_discourse_planner``
  passes ``reflective=True`` so the default chat path benefits.

Tests
-----

New ``tests/test_discourse_planner_reflective.py``:

* ``test_reflective_replaces_repeated_subject_with_it``
* ``test_reflective_handles_three_consecutive_same_subject_moves``
* ``test_reflective_capitalises_sentence_initial_pronoun``
* ``test_reflective_resets_focus_on_topic_shift``
* ``test_reflective_off_preserves_phase1_output``
* ``test_reflective_default_is_off_for_back_compat``
* ``test_reflective_is_deterministic``
* ``test_reflective_single_move_byte_identical_to_non_reflective``
  (load-bearing — pins that the cognition eval stays byte-equal
  across the Phase 2 flip because every cognition case is single-
  move).

Verification
------------

  pytest tests/test_discourse_planner_*.py        99/99 pass
                                                  (91 existing + 8 new)
  pytest tests/test_articulation_demo.py          all claims supported
  pytest tests/test_narrative_example_intents.py  pass
  pytest tests/test_runtime_config.py             pass
  cognition eval OFF vs ON                        45/45 surface byte-equal
                                                  45/45 trace_hash byte-equal
                                                  4/4 aggregate metrics
                                                      identical
  core test --suite smoke                         67/67 pass
  core test --suite runtime                       19/19 pass

Live demo (default config):

  "What is knowledge?"  → unchanged (BRIEF, fast-path)
  "Tell me about
    memory."            → "Memory is what a person recalls.
                          Furthermore, it belongs to cognition.memory.
                          In turn, it requires recall."
  "What is truth, and
    why does it matter?"→ "Truth is what is true. Furthermore, it
                          belongs to cognition.truth. In turn, it
                          grounds knowledge. It belongs to
                          epistemic.ground. Furthermore, it belongs
                          to logos.core. In turn, it requires
                          evidence."
  "Explain truth."      → "Truth is what is true. Furthermore, it
                          belongs to cognition.truth. In turn, it
                          grounds knowledge."

Out of scope for this commit (future Phase 2 follow-ons):

* Connective rotation ("Furthermore" → "Also" → "In addition"
  to break the repetitive cascade).
* Cross-clause de-duplication (skip moves whose ``new`` lemmas
  were already introduced by an earlier move).
* Generalised pronoun selection beyond ``it`` (requires gender /
  number / animacy signals the pack lexicon doesn't carry today).
2026-05-21 10:16:12 -07:00
Shay
63ffd88595 feat(runtime): default discourse_planner=True + fast-path BRIEF short-circuit
Flips ``RuntimeConfig.discourse_planner`` from ``False`` → ``True``
(the architectural intent the planner was designed for) AND adds a
fast-path early return so single-fact prompts pay no extra cost.

Why the flip
------------

The discourse planner apparatus has been fully wired in the codebase
for some time (``generate.discourse_planner.plan_discourse`` /
``plan_compound_discourse`` / ``render_plan``,
``generate.grounding_accessors.grounding_bundle_for``,
``chat.runtime._maybe_apply_discourse_planner``) but gated off behind
this flag.  Investigation surfaced that:

  * **Cognition eval (45 cases) is byte-identical OFF vs ON** across
    both surface and trace_hash projections — the planner's
    downstream ``len(plan.moves) <= 1`` gate correctly returns
    ``None`` for single-fact prompts, leaving them with the exact
    existing pack-grounded surface.

  * **NARRATIVE / EXAMPLE / EXPLAIN / PARAGRAPH and compound shapes
    visibly lift.**  ``"Tell me about memory."`` goes from a one-
    fragment disclosure to a 3-sentence grounded discourse.
    ``"What is truth, and why does it matter?"`` — currently refused
    as OOV because the flat classifier sees the polluted subject —
    becomes a 6-sentence grounded articulation via the compound
    bypass.

  * **No quality regression on existing benches.**  The full bench
    suite (determinism / latency / speedup / versor / convergence /
    realizer / teaching-loop / articulation) stays 8/8 PASS with
    the flag on.

Why the fast-path
-----------------

Default-on uncovered a perf trap: the gate ran
``grounding_bundle_for(lemma)`` (pack + teaching + cross-pack queries)
AND ``plan_discourse(...)`` on EVERY turn, then discarded the
result when ``len(plan.moves) <= 1``.  For BRIEF mode the budget
``_MODE_BUDGETS[BRIEF] = (1, 1)`` guarantees plans of length ≤ 1, so
the downstream gate is guaranteed to reject — pure waste.  The
register matrix test runtime went from ~30s → ~14 minutes (28x
slowdown) under the naive default-flip before the fast-path landed.

The new short-circuit:

  if mode is BRIEF and not compound.is_compound():
      return None

skips the bundle query + plan run entirely for the common case.
Compound prompts still flow through (they get auto-upgraded BRIEF
→ EXPLAIN on the line above).  Empirical post-fast-path
measurement on a 45-case eval (workers=1):

  OFF: 23.31s  (1.93 turns/sec)
  ON : 17.74s  (2.54 turns/sec)
  slowdown : 0.76x  (flag-ON is actually 24% FASTER — the bundle
                     work the OFF path also touches downstream is
                     short-circuited cleanly when not needed)
  surface byte-equal: True
  trace_hash byte-equal: True

Test updates
------------

* ``test_discourse_planner_render.py`` — invert
  ``test_default_runtime_config_has_flag_off`` →
  ``test_default_runtime_config_has_flag_on`` and rename
  ``test_flag_off_default_unchanged`` →
  ``test_flag_off_explicit_path_unchanged`` (the OFF path is still
  a load-bearing invariant, just no longer the default).

* ``test_narrative_example_intents.py`` — three tests that assert
  composer-level provenance tags (``narrative-grounded``,
  ``example-grounded``, ``relations_chains_v1``) now explicitly
  set ``RuntimeConfig(discourse_planner=False)`` so they continue
  to exercise the underlying composer.  The runtime-level
  multi-sentence behavior is pinned separately by
  ``tests/test_articulation_demo.py``.

Verified
--------

  cognition eval (45 cases)               OFF ≡ ON byte-identical
  pytest tests/test_discourse_planner_*   132/132 pass
  pytest tests/test_articulation_demo.py  all claims supported
  pytest tests/test_narrative_example_intents.py  pass
  pytest tests/test_runtime_config.py     pass
  core test --suite smoke                 67/67 pass
  core test --suite runtime               19/19 pass
  core test --suite packs                  6/6 pass

Live demo (default config):
  "What is knowledge?"          → single sentence (BRIEF, fast-path)
  "Tell me about memory."       → 3 grounded sentences
  "What is truth, and why does
   it matter?"                  → 6 grounded sentences (was: OOV)
  "Explain truth."              → 3 grounded sentences
2026-05-21 10:06:49 -07:00
Shay
c945b9a045 fix(intent): widen CORRECTION to catch fully-spoken `that is/was ...` forms
Follow-on to the word-boundary fix (commit 0dd30b8).  After tightening
``\bno\b`` etc. with word boundaries, an audit surfaced a separate
pre-existing gap in the CORRECTION trigger: the contracted-only
``that'?s\s+(?:not|wrong)`` slot silently dropped every fully-spoken
copula form to UNKNOWN.

Concrete gap (every one previously UNKNOWN):

  "That is not right."        → UNKNOWN
  "That is wrong."            → UNKNOWN
  "That was wrong."           → UNKNOWN
  "That is incorrect."        → UNKNOWN
  "That is false."            → UNKNOWN
  "That was not right."       → UNKNOWN
  "that is mistaken."         → UNKNOWN
  "That was incorrect."       → UNKNOWN

Root cause: the slot ``that'?s\s+(?:not|wrong)`` matches only

    that's  /  thats

— ``'?s`` makes the apostrophe optional but the literal ``s`` is
mandatory.  ``that is`` (full word ``is``) and ``that was`` (full
word ``was``) had no path.  And the predicate alternation only
accepted ``not`` or ``wrong``; ``incorrect``, ``false``, and
``mistaken`` were also missing.

Fix: widen both slots in one pattern revision.

    Before:
      that'?s\s+(?:not|wrong)
    After:
      that(?:'?s|\s+(?:is|was))\s+(?:not|wrong|incorrect|false|mistaken)

The full pattern now reads:

    \b(?:no
       |that(?:'?s|\s+(?:is|was))\s+(?:not|wrong|incorrect|false|mistaken)
       |incorrect
       |actually
       |correction)\b

Boundary discipline holds: the outer ``\b...\b`` still prevents the
predicate alternation from eating into longer words.  Verified:

  "That is correct."          → UNKNOWN (right NOT in predicate set)
  "That is right."            → UNKNOWN (right NOT in predicate set)
  "That is true."             → UNKNOWN (true NOT in predicate set)
  "That works."               → UNKNOWN
  "That is interesting."      → UNKNOWN
  "That is falsifiable."      → UNKNOWN (``false`` + ``i`` is word→word
                                         so ``\b`` after ``false`` fails)
  "That was wrongly accused." → UNKNOWN (same logic for ``wrong``+``ly``)

Tests extended:
  * ``test_correction_canonical_forms_still_route`` — 8 new parametrize
    cases for the fully-spoken copula forms
  * ``test_correction_does_not_eat_no_prefixed_words`` — 9 new
    parametrize cases for the affirmative ``That is/was ...`` shape
    AND the boundary-trap cases ``falsifiable`` / ``wrongly accused``

Verified:
  pytest tests/test_intent_subject_extraction.py         33/33 pass
  full intent + register-diagnostic + proposition graph  77/77 pass
  core test --suite smoke                                67/67 pass
  core test --suite runtime                              19/19 pass
2026-05-21 08:36:33 -07:00
Shay
0dd30b86a7 fix(intent): anchor CORRECTION trigger with word boundaries
While investigating the adjacent RECALL classifier gap, a much
wider intent-classification bug surfaced: every prompt beginning
with a word that *starts with* the letters of any CORRECTION
trigger silently routed to CORRECTION with a mangled subject.

Concrete examples seen during diagnosis:

  "Now remember light."        → CORRECTION  subject="w remember light"
  "Nothing matters."           → CORRECTION  subject="thing matters"
  "Notice the truth."          → CORRECTION  subject="tice the truth"
  "Note that recall fires."    → CORRECTION  subject="te that recall fires"
  "Nominate a candidate."      → CORRECTION  subject="minate a candidate"
  "Norma is here."             → CORRECTION  subject="rma is here"
  "Notwithstanding ..."        → CORRECTION  subject="twithstanding ..."

Root cause: ``generate/intent.py`` ``_RULES`` line ~213 used the
pattern

    (?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)

The alternation has ``no``, ``incorrect``, ``actually``, ``correction``
as bare substrings — no word boundary on either side.  Combined with
``re.match``'s start-of-string anchor, *any* prompt beginning with
``No``-, ``Incorrect``-, ``Actually``-, or ``Correction``-prefixed
text matched as CORRECTION; the regex's match span was then sliced
off the prompt to produce a subject like ``"w remember light"``
(from ``"Now remember light."``).

The same hazard threatens:

  * ``no``         → eats ``Now`` / ``Notice`` / ``Note`` / ``Nothing`` /
                     ``Nominate`` / ``Norma`` / ``Notwithstanding`` / ...
  * ``incorrect``  → would eat ``incorrectly``
  * ``actually``   → would eat ``actualization``
  * ``correction`` → would eat ``corrections``

Fix: add ``\b`` anchors on both sides of the alternation.

    \b(?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)\b

``\b`` is zero-width, so ``re.match``'s start-of-string anchor still
holds; the left ``\b`` is a no-op at position 0.  The right ``\b``
forces the matched token to end on a word boundary — i.e., the next
character must be non-word (whitespace, punctuation, EOL) — so
``\bno\b`` matches ``"No."`` / ``"No way"`` / ``"No, ..."`` but NOT
``"Now"`` / ``"Nothing"`` / etc.

Verified 11/11 previously-misfiring prompts now correctly classify
as UNKNOWN, and 8/8 legitimate CORRECTION pragmas
(``"No."`` / ``"No way."`` / ``"Incorrect."`` / ``"Actually, ..."`` /
``"Correction: ..."`` / ``"That's wrong."`` / ``"No, that's wrong."`` /
``"no, knowledge is wrong."``) still route correctly.

Tests extended with two new parametrized blocks in
``tests/test_intent_subject_extraction.py``:

  * ``test_correction_canonical_forms_still_route`` — 8 cases pinning
    the legitimate CORRECTION patterns
  * ``test_correction_does_not_eat_no_prefixed_words`` — 10 cases
    pinning the boundary fix against regression

Verified:
  pytest tests/test_intent_subject_extraction.py        25/25 pass
  pytest tests/test_intent_proposition_graph.py        + others       60/60 pass
  core test --suite smoke                                            67/67 pass
  core test --suite runtime                                          19/19 pass

Out of scope: ``"That is not right."`` (a real CORRECTION pragma the
regex never caught because ``that'?s\s+`` requires literal ``s`` after
``that``; the colloquial ``that is`` form was always UNKNOWN). Separate
gap, unchanged here.
2026-05-21 08:29:16 -07:00
Shay
7ef4ef4546 fix(intent): widen RECALL trigger to accept `recall alongside remember`
The articulation breadth benchmark surfaced a RECALL intent gap:

  Before (bench output):
    RECALL    UNKNOWN    pack    Pack-resident tokens — pack-grounded
                                 (en_core_cognition_v1): recall ...

The probe prompt ``"Recall truth."`` classified as UNKNOWN and fell
through to the ADR-0086 pack-resident-token surface — a graceful
degradation, not a hard failure, but a real classifier gap.

Root cause: ``generate/intent.py`` ``_RULES`` line 213 only matched
the imperative ``remember``:

    (re.compile(r"remember\s+", re.IGNORECASE), IntentTag.RECALL)

The verb ``recall`` — every bit as natural an imperative — was
missing from the trigger pattern.  ``"Remember truth."`` correctly
routed to RECALL; ``"Recall truth."`` did not.

Fix: widen the alternation to ``(?:remember|recall)\s+``.  One-word
change; ``re.match`` anchoring at the start of the prompt means the
fix only catches the canonical imperative form, leaving downstream
contexts untouched:

  * ``Does memory require recall?``      → VERIFICATION (unchanged;
    earlier rule on the aux-verb pattern fires first)
  * ``What is recall?``                  → DEFINITION   (unchanged;
    ``what\s+is\s+`` fires first)
  * ``Why does recall exist?``           → CAUSE        (unchanged;
    ``why\s+`` fires first)
  * ``I recall.``                        → UNKNOWN      (unchanged;
    no trailing word after ``recall``, ``\s+`` doesn't match)
  * ``Please recall the truth.``         → UNKNOWN      (unchanged
    — symmetric with ``Please remember the truth.`` since rules use
    ``pattern.match`` not ``pattern.search``)

After (bench output):
    RECALL    RECALL    pack    Truth is what is true. pack-grounded
                                (en_core_cognition_v1).

The articulation bench probe now routes correctly and produces a
pack-grounded definition surface — the canonical RECALL output on
a pack-resident lemma.

Tests extended: ``tests/test_intent_subject_extraction.py::
test_recall_strips_articles`` is parametrized with four new
``Recall ...`` cases parallel to the existing ``Remember ...``
cases.  A regression that re-narrows the trigger pattern fails the
gate immediately.

Verified:
  * pytest tests/test_intent_subject_extraction.py            7/7 pass
  * pytest tests/test_register_firing_diagnostic.py           3/3 pass
  * core test --suite smoke                                  67/67 pass
  * core test --suite runtime                                19/19 pass
  * core bench --suite articulation  → RECALL ✓ pack-grounded
2026-05-21 08:26:08 -07:00
Shay
f6f8ee603f
feat(evals): per-intent register-firing diagnostic + CI gate + tests (#103)
Replaces the per-pack-aggregate diagnostic landed at 58ac780 with a
per-intent matrix decomposition authored by Codex on a parallel
worktree. Codex's design directly answers the original motivating
question — "which packs' marker pools don't fire on which intent
shapes" — that the aggregate version flattened.

What Codex's version adds over the prior aggregate version:

  * **Per (pack × intent × prompt) matrix** — cells decompose by
    IntentTag. The C_stance / DEFINITION collapse pattern surfaced
    in the widened tour is now directly visible as
    matrix[register]["DEFINITION"][*].opening_fired == False.

  * **Replayed-variant verification** — every cell records
    decorate_surface()'s opening/closing AND asserts the resulting
    variant_id matches the runtime's emitted register_variant_id
    byte-for-byte. Catches future drift between the replayed
    selection and live selection in a single field
    (variant_id_matches_runtime / all_replayed_variants_match_runtime).

  * **Representative-prompt classification gate** — the companion
    test confirms every prompt in REPRESENTATIVE_PROMPTS actually
    classifies to its declared IntentTag. If intent classification
    drifts, the corpus is invalidated immediately rather than
    silently producing meaningless diagnostic output.

  * **--fail-on-gap CI mode** — exits 1 when any non-empty marker
    bucket never fires across its representative-prompt slice.
    Convertible into a CI gate once the deliberate-silent vs
    accidental-silent distinction is curated.

  * **--register / --intent filters** + **--output PATH** — operator
    ergonomics for targeted debugging and report archival.

  * **3 pytest cases** — corpus integrity, subset-report shape,
    full main()/--output round-trip.

Path: Codex authored at scripts/diagnose_register_firing.py.
Relocated to evals/register_diagnostics/run_firing_diagnostic.py to
match the convention used by evals/register_tour/, anchor_lens_tour/,
orthogonality_tour/, learning_loop/ — measurement artifacts live
under evals/, not scripts/. Test import path adjusted accordingly.

The sys.path bootstrap _REPO_ROOT computation was updated from
.parent.parent to .parents[2] to account for the new path depth.

Verified:
  PYTHONPATH=. pytest tests/test_register_firing_diagnostic.py -v
    → 3 passed in 5.39s
  PYTHONPATH=. python -m evals.register_diagnostics.run_firing_diagnostic \
      --register convivial_v1 --intent DEFINITION --intent CAUSE
    → emits per-cell matrix with variant_id_matches_runtime=True
  PYTHONPATH=. python -m evals.register_diagnostics.run_firing_diagnostic \
      --register expert_v1 --intent DEFINITION --fail-on-gap
    → exit 0 (expert_v1's empty buckets have non_empty_size=0, so
      not a contract gap — that's correct: gap = non-empty bucket
      whose entries never fire)

Co-authored-by: Codex <noreply@openai.com>
2026-05-21 07:05:23 -07:00
Shay
483c66dc5f test(register): widen invariant matrix to all 100 ratified packs
PR #102 ratified 93 drafted register packs, bringing the catalog to
100 fully-sealed packs on disk. This widens
tests/test_cognition_eval_register_matrix.py::_RATIFIED_REGISTERS
from 7 to 100 so every projection-invariant assertion (trace_hash,
intent_correct, terms_captured, surface_contains_pass,
versor_closure, versor_condition, canonical surface, and aggregate
metrics) now runs against every ratified pack.

Verification on PR #102 head: 801 cells passed in 316.76s
  = 100 registers × 8 projections + 1 meta-test
  = full ADR-0072 invariant proven across the entire register axis
    on all 45 cognition cases.

The meta-test test_register_matrix_covers_every_ratified_pack
remains the structural co-evolution guard: any future register pack
ratification must widen both REGISTER_IDS in
scripts/ratify_register_packs.py AND _RATIFIED_REGISTERS here in
the same change, or CI fails fast.
2026-05-21 06:38:22 -07:00
Shay
cad8b39928
feat(packs/register): 93-pack catalog rollout — drafted → ratified (#102)
* feat(packs/register): materialise A_depth drafted registers

Lands 3 drafted depth registers, dominated by disclosure-domain count and structural compression/expansion knobs; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path. Also aligns the smoke contract assertion with the current pack-grounded unknown evidence split.

* feat(packs/register): materialise B_tone drafted registers

Lands 15 drafted tone registers, dominated by bounded affective opening and closing marker palettes; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise C_stance drafted registers

Lands 11 drafted stance registers, dominated by epistemic posture markers plus light deterministic depth clauses; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise D_posture drafted registers

Lands 10 drafted posture registers, dominated by role-shaped marker families for peer, mentor, scholar, practitioner, and related voices; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise E_domain drafted registers

Lands 11 drafted domain registers, dominated by academic, executive, technical, legal, scientific, and philosophical marker families with bounded known-key knobs; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise F_cultural drafted registers

Lands 12 drafted cultural registers, dominated by plainspoken, diplomatic, classic, contemporary, and lyrical marker palettes; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise G_affective drafted registers

Lands 10 drafted affective registers, dominated by cheerful, somber, grave, wry, gentle, and earnest marker families; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise H_functional drafted registers

Lands 10 drafted functional registers, dominated by documentary, instructional, persuasive, clarifying, comparing, and exemplifying marker families; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.

* feat(packs/register): materialise I_composite drafted registers

Lands 11 drafted composite registers, dominated by combined knob and marker families for tutorial, interview, briefing, lecture, memo, story, elegy, epigram, and manifesto voices; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
2026-05-21 06:37:38 -07:00
Shay
66db063f0b test(register): full 7-pack invariant matrix on cognition lane
Adds tests/test_cognition_eval_register_matrix.py — strict superset
of tests/test_register_invariant_grounding.py (which covered only 4
of the 7 ratified register packs).

Parametrizes over all seven ratified register packs
({default_neutral, terse, precise, convivial, pedagogical, formal,
socratic}_v1) and asserts byte-identity against the unregistered
baseline for every per-case projection the cognition eval reports:

  * trace_hash               (ADR-0072 truth-path-isolation)
  * intent_correct           (intent runs upstream of realizer)
  * terms_captured           (scored off canonical surface)
  * surface_contains_pass    (scored off canonical surface)
  * versor_closure           (truth-path field invariant)
  * versor_condition         (exact float, stronger than closure)
  * surface                  (CognitiveTurnResult.surface is the
                              pre-decoration canonical the trace
                              hash consumes; substantive transforms
                              live on turn_log[-1].surface)

Aggregate metrics on EvalReport are pinned identically: total,
intent_correct, terms_captured, terms_expected, surface_grounded,
versor_closures.

Meta-test test_register_matrix_covers_every_ratified_pack enforces
that _RATIFIED_REGISTERS in this file stays in lockstep with
scripts/ratify_register_packs.py::REGISTER_IDS — so the 93 drafted
register packs in packs/register/_catalog.json cannot ratify into
CI without each one passing the full invariant matrix.

Run: 57 cells (8 projections x 7 registers + 1 meta), 27.7s
sequential across 45 cognition cases per register.

Pre-existing smoke failure (test_chat_response_surface_uses_
articulation_plan in tests/test_runtime_config.py) is the ADR-0086
expected-string test on main; unrelated to this change.
2026-05-21 06:24:36 -07:00
Shay
79f1678923 feat: ADR-0086 + ADR-0087 + 100-register catalog — cognition lane closure
Three load-bearing pieces:

1. ADR-0086 — UNKNOWN-intent pack-resident token surface
   New deterministic composer `pack_grounded_unknown_surface` in
   chat/pack_grounding.py.  When intent classification returns UNKNOWN
   but the prompt contains pack-resident lemmas (via cross-pack
   resolver), surface those lemmas with their semantic_domains
   instead of falling to the bare _UNKNOWN_DOMAIN_SURFACE.  Wired
   into chat/runtime.py::_maybe_pack_grounded_surface as the
   last typed-intent branch before the OOV fallback.  Null-lift
   invariant pinned: fully-OOV prompts still emit the universal
   disclosure byte-identically.  Closes four cognition-eval term
   misses: unknown_logos_019 (public), unknown_evidence_042 (dev),
   unknown_spirit_041 + unknown_word_018 (holdout).  Side effect:
   evals/results/phase2_pack_measurements.json refusal_rate drops
   from 0.25 → 0.125 across all three identity packs (no longer
   refusing on these prompts).

2. ADR-0087 — PROCEDURE selector + trailing-clause subject echo
   Two coupled changes in chat/pack_grounding.py:
   (a) Numeric-determiner downrank in _extract_procedure_topic_lemma:
       tokens whose primary semantic_domain starts with
       "quantitative.numeric." are demoted; non-numeric resident
       candidates always win.  So "compare two terms" anchors on
       `compare` not `two`.
   (b) Trailing clause echoes the full normalized subject_text
       rather than just the selected lemma, so OOV head nouns like
       "terms" reach the surface even when only the procedure verb
       is pack-resident.  Closes procedure_compare_011.

3. 100-register catalog
   New packs/register/_catalog.json — canonical machine-readable
   spec for all 100 registers (7 currently-ratified + 93 drafted)
   organized into 9 voice groups (depth/tone/stance/posture/domain/
   cultural/affective/functional/composite).  Each entry is a
   complete production input — realizer_overrides, marker palettes
   (openings/transitions/closings), depth_preference, description,
   author_notes.  All realizer_overrides use only legal keys per
   scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS.
   Companion packs/register/CATALOG.md documents the production
   loop: materialize → widen REGISTER_IDS → ratify → smoke.

Cognition-eval lifts (all three splits):
  public:  term_capture 91.7% → 100.0%  (+8.3pp)
  holdout: term_capture 83.3% → 100.0%  (+16.7pp)
  dev:     term_capture 78.6% → 100.0%  (+21.4pp)
  surface_groundedness: 100% preserved on all splits
  intent_accuracy / versor_closure: 100% preserved on all splits

Tests:
  tests/test_pack_grounded_unknown.py     — 14 tests (composer
    direct + runtime engagement + null-lift invariant)
  tests/test_adr_0087_procedure_selector.py — 12 tests (selector
    numeric downrank + trailing-clause echo + regression guard)
  Existing test suites unaffected — cognition lane 120 passed / 1
  skipped both before and after.  Full lane net −3 failures vs
  pristine main (39 → 36 — none introduced).
2026-05-21 00:08:12 -07:00
Shay
583aae42ef
feat(packs): ADR-0085 content style pass v2 — 3sg + plural agreement (+ closure infra) (#100)
Applies the ADR-0085 v2 brief's 16 fluency rows (Pattern A 3sg agreement on
relative-clause verbs + Pattern B plural after quantifier) plus 7 additional
"what a person {VERB}" rows surfaced in live chat probe (`Knowledge is what
a person know` → `knows`, similar for `memory`/`question`/`word`/`answer`/
`response`/`express`). 23 gloss edits total across 5 packs.

The brief had an internal conflict: it forbids atom edits but requires
closure-verifier 0/0, while ADR-0084's verifier enforces
`atoms == content_tokens(gloss)` exactly. Resolved by:

  1. Extending `scripts/verify_definitional_closure.py` and the integration
     test fixture (`mounted_lex_lemmas` + `production_pool` builders) to
     include lexicon `surface` forms in the resolution set — already the
     operational meaning of "a lemma in another mounted pack" since
     surfaces are canonical inflections of the same lemma.
  2. Adding 10 inflected `LexicalEntry` rows across cognition / meta /
     action / spatial lexicons (e.g. `surface=knows lemma=know`,
     `surface=parts lemma=part`) so morphology-shifted atoms resolve.

Live surface verification (sample 6 prompts):

  before                                          after
  "what a person know from truth and evidence" -> "...knows from..."
  "what a person recall"                       -> "...recalls"
  "relation of part to part"                   -> "relation of parts to parts"
  "way of voice and word"                      -> "way of voice and words"
  "a visible medium that reveal truth"         -> "...reveals truth"
  "what a cause make"                          -> "what a cause makes"

Verification (all gates from brief Phase 4):
  - closure verifier: 0 unresolved / 0 mismatches on all ADR-0084 packs
    (remaining domain-pack red is PR #97 follow-up — addressed by PR #99)
  - ADR-0084 integration test: 30/30
  - cognition eval: byte-identical to baseline
  - packs lane: 6/6
  - smoke lane: 67/67

Files touched: 5 gloss files (cognition / causation / meta / attitude /
spatial), 4 lexicon files (cognition / meta / action / spatial), 5 manifest
checksum refreshes (+ action), 1 verifier code change, 1 integration test
fixture extension, 1 deterministic-pack-entry-id test bump (085→091).
2026-05-20 23:12:28 -07:00
Shay
3d922a1532
Add chain-first capability ledger and domain seeds (#97) 2026-05-20 21:33:24 -07:00
Shay
2a2ef9ce49
perf(salience): vectorize curvature pairwise loop — 57× faster, 42% e2e (#96)
cProfile attribution (2026-05-21) identified
``core.physics.salience.SalienceOperator.compute`` as 64% of total
``ChatRuntime.chat()`` time.  Pre-fix it was a nested Python loop
over ``regions × regions`` with one ``np.linalg.norm`` call per
pair.  For N≈500 mounted-vocab regions per turn that meant ~250k
norm calls per turn, dominating end-to-end latency.

Fix: numpy broadcast for pairwise displacement, distance,
pressure-delta, and contribution.  Same math; same contract.
ULP-level reassociation drift is absorbed by the 12-decimal
precision ``_salience_address`` already used for content
addressing, and by the float32 conversion at the downstream
``SalienceMap.scores_arr`` site, so neither the content_address
nor the top-k ordering changes.

Measurements (region set: N=493, dim=5, seeded):

  vectorized:  11.78 ms/call
  old-loop:   672.30 ms/call
  speedup:    57.1×

End-to-end on 8 cognition-shape prompts:

  pre-fix:  ~970 ms/turn
  post-fix:  565 ms/turn   (-42%)

Validation:

  * 15 new tests in ``tests/test_salience_vectorize_parity.py``:
      - parity with a nested-loop reference to 1e-9 absolute on
        curvature_magnitude, gradient_vector, influence_radius
        across N ∈ {1, 2, 8, 32, 128, 493}
      - content_address byte-identical across N ∈ {1, 8, 32, 128}
      - top-16 ordering matches the reference at N ∈ {32, 128, 493}
      - empty regions returns empty map
      - single region has zero curvature
  * ``core eval cognition`` byte-identical: public 100/100/91.7/100.
  * ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0.

The file's pre-existing docstring promised a Rust path
(``core_rs::physics::salience::compute_curvature``) that does not
yet exist — the numpy vectorization realizes the lift now while
keeping the Rust port a future optimization on stable semantics
(CLAUDE.md: "Rust backend parity only after Python semantics are
locked by tests").
2026-05-20 21:29:42 -07:00
Shay
a36b48b198
feat(runtime): opt-in unified-ingest path (ADR-0090, audit Findings 6+7) (#95)
Closes audit Findings 6 (within-turn recall not batched) and 7
(probe-ingest / commit-ingest dual field) as a single PR — the two
are architecturally entangled and resolve together.

Pre-fix flow in ``ChatRuntime.chat()``:

  1. ``probe_ingest(filtered)`` → ``probe_state.F``
  2. Gate check on ``probe_state.F``
  3. If gate fires: ``commit_ingest`` + stub response
  4. Otherwise: ``commit_ingest`` + drive bias → ``field_state.F``
  5. Walk runs on ``field_state.F``

The gate observes one manifold position; the walk navigates a
slightly different one (drive bias applied between them).  Honest
refusal decisions and walk outputs are made on different fields —
the audit's named coherence gap.

This PR ships a flag-gated unified-ingest path following the
codebase's standard substantive-change pattern (ADR-0046 /
ADR-0062 / ADR-0085 / ADR-0088 / ADR-0089):

``RuntimeConfig.unified_ingest: bool = False`` (default).

When ``True``:

  1. ``commit_ingest(filtered)`` runs first.
  2. Drive bias applied immediately.
  3. Gate observes ``committed.F``.
  4. If gate fires: stub response (turn has already committed —
     intentional semantic change documented in ADR-0090).
  5. Otherwise: walk runs on the same ``committed.F`` the gate
     decided against — no second ``commit_ingest`` call.
  6. ``probe_ingest`` is not called on this path.

When ``False`` (default): historical behavior is preserved
bit-for-bit; ``probe_ingest`` still runs first.

ADR-0090 documents:

  * Phase 1 (this PR): unified-ingest substrate.
  * Phase 2 (separate PR, after Phase 1 validates): batched recall
    — pass the gate's ``direct_hits`` into ``generate()`` as a
    ``prebuilt_first_recall`` so the walk's first step does not
    re-call ``vault.recall()`` on the same field.  Single recall
    call eliminated per turn.
  * Out of scope: ``recall_batch`` for per-step walk recalls
    (each step's query depends on the previous step's field
    state; not batchable without changing walk geometry).

Validation:

  * 5 new tests in ``tests/test_unified_ingest_null_lift.py``:
      - flag defaults to ``False`` on ``DEFAULT_CONFIG``
      - flag-off surface + trace_hash + vault_hits byte-identical
      - flag-on does not call ``probe_ingest`` (verified via spy)
      - flag-on produces well-formed surface + trace_hash
      - flag-off still calls ``probe_ingest`` (historical guard)
  * ``core eval cognition`` byte-identical across all three splits:
    public 100/100/91.7/100, dev 100/100/78.6/100, holdout
    100/100/83.3/100.
  * ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
    ``runtime`` 19/0.

Comb-pass status after this PR:

  * Item 4 (graph topo) ✓ #92
  * Item 5 (realizer node_map) ✓ #91
  * Item 6 (batch recall) ✓ ADR-0090 substrate (this PR); Phase 2
    optimization is queued
  * Item 7 (probe/commit dual ingest) ✓ ADR-0090 (this PR)
  * Item 8 (dead defensiveness sweep) ✓ #91
  * Item 9 (local imports) ✓ #91
  * Item 11 (dead ``_fold_compose_into_surface``) ✓ #91
  * Item 13 (``_serialize_*`` fold) ✓ #91
  * Item 15 (GenerationResult tuple/list) ⊘ false positive
  * Item 16 (subject normalization consistency) ✓ #93
  * Item 17 (redundant ``^`` anchors) ✓ #94
  * Tier 5 minor (``_BE_FORMS`` hoist, walrus, reverse-iter) ✓ #94
2026-05-20 21:00:27 -07:00
Shay
ef7d59287b
rigor(intent): consistent subject normalization across all classifier paths (#93)
Comb pass 2026-05-21 (item 16).

Pre-fix ``classify_intent`` applied ``_normalize_subject`` only to
DEFINITION / CAUSE / VERIFICATION paths.  COMPARISON, FRAME_TRANSFER,
TRANSITIVE_QUERY (non-"means" branch), and BELONG_QUERY returned
bare ``.strip()`` subjects.  A probe like *"Compare the parent and
a child"* would carry the articles ("the parent", "a child") into
the subject slot, breaking downstream pack-resolver lookups that
key on bare lemmas.

Fix: apply ``_normalize_subject(..., IntentTag.DEFINITION)`` at every
classifier return site that was previously bare ``.strip()``.
DEFINITION mode preserves multi-word noun phrases (only strips
leading articles + trailing punctuation + infinitive markers); the
aux-verb stripping that's only meaningful for CAUSE/VERIFICATION
stays scoped to those paths.

Sites fixed (5):

  * COMPARISON subject + secondary_subject
  * FRAME_TRANSFER subject + frame
  * TRANSITIVE_QUERY subject (both the regular and "means" → DEFINITION
    redirect branches now share one normalized binding)
  * BELONG_QUERY subject

Behavior:

  * Eval cases without articles (the entirety of cognition v1) are
    byte-identical: ``"memory"`` and ``"recall"`` survive
    ``_normalize_subject`` unchanged.
  * Multi-word noun phrases survive intact: ``"artificial
    intelligence"`` is preserved (no aux-verb-strip wrongly trimming
    to head-noun).
  * Article-prefixed subjects ("the parent") now strip consistently
    with the DEFINITION path that's done so since ADR-0049.

Validation:

  * 7 new tests in
    ``tests/test_intent_subject_normalization_consistency.py``
    pin the consistency contract across COMPARISON, FRAME_TRANSFER,
    TRANSITIVE_QUERY, BELONG_QUERY, DEFINITION (regression guard
    on the pre-existing path), and CAUSE (regression guard on the
    aux-verb-strip behavior).
  * ``core eval cognition`` byte-identical across all three splits:
    public 100/100/91.7/100, dev 100/100/78.6/100, holdout
    100/100/83.3/100.
  * ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0.
  * ``pytest -k intent`` 229/0.
2026-05-20 20:44:19 -07:00
Shay
548282fadc
perf(graph): PropositionGraph.topo_order — Kahn's O(N+E) instead of O(N×E) (#92)
Comb pass 2026-05-21 (item 4).

Pre-fix the topological-sort implementation in
``PropositionGraph.topo_order`` had two compounding inefficiencies:

  * ``queue.pop(0)`` on a list is O(N) per pop → O(N²) total
  * The inner ``for e in self.edges`` rescanned all edges on every
    iteration → O(N × E) overall

This is invisible on today's 1–2 node production graphs but would
become a real regression the moment compound-intent multi-node
dispatch (ADR-0089 Phase C2) or the grounded realizer's multi-clause
output (ADR-0088 Phase B follow-up) lands.

Fix: standard Kahn's with a precomputed out-edge adjacency map and
a ``deque`` for the work queue.  O(N + E) overall.  Deterministic
output preserved — the queue is seeded with sorted zero-in-degree
nodes (identical to the pre-fix list sort), and direct-successor
order matches edge-iteration order (identical when edges retain
insertion order).

Pinned by 6 new tests in ``tests/test_graph_topo_order_perf.py``:

  * single-node graph (today's production shape) byte-identical to
    pre-fix output
  * empty graph returns empty tuple
  * chain (A→B→C→D) orders root → leaf
  * diamond (A→B, A→C, B→D, C→D) keeps A first, D last, B/C between
  * three disjoint roots emit in sorted order
  * 100-node chain returns correct full order (would have been
    visibly slow under the O(N²) pre-fix algorithm)

Validation:

  * ``core eval cognition`` byte-identical (public 100/100/91.7/100)
  * ``core test --suite cognition`` 120/0/1
  * ``core test --suite smoke`` 67/0

Comb-pass note: item 15 (GenerationResult.tokens typed tuple but
assigned list) was investigated and turned out to be a Pyright
false positive — ``GenerationResult.__post_init__`` already coerces
to tuple via ``object.__setattr__``.  Contract is enforced at
runtime; only Pyright's static analyser misses the coercion site.
No fix needed.
2026-05-20 20:37:21 -07:00
Shay
fd48931838
perf(cognition): hot-path comb pass — 5 mechanical-sympathy fixes (#91)
Bundle of 5 hot-path optimizations + 1 dead-code removal + 1 import
sweep + 1 helper fold, surfaced by a comb pass through the cognitive
spine starting from ``CognitiveTurnPipeline.run()`` and walking
outward through ChatRuntime, intent classification, the graph
planner, the realizer, and the vault.  All eval lanes byte-identical
to MEMORY baseline; null-lift confirmed by ``core eval cognition``
across public / dev / holdout splits.

Hot-path fixes:

  1. ``ChatRuntime._apply_oov_policy`` no longer rescans every
     manifest per OOV token.  Two precomputed booleans on
     ``self`` capture the FAIL_CLOSED-all and PROPOSE_VOCAB-any
     aggregates at construction time.  Manifests are immutable
     post-construction so the cache is safe.  Turns the path from
     O(packs × OOV) to O(OOV).

  2. ``CognitiveTurnPipeline.run`` calls ``classify_compound_intent``
     once and takes its dominant ``compound.primary`` as the seeded
     intent.  Pre-fix the pipeline called both ``classify_intent``
     and ``classify_compound_intent`` on every turn — and
     ``classify_compound_intent`` internally invokes
     ``classify_intent`` on the dominant fragment, so every non-
     compound prompt walked the 15-regex cascade twice.

  3. ``TeachingStore.triples()`` materializes once per turn.
     Pre-fix ``_maybe_transitive_walk`` and ``_maybe_compose_relations``
     each called ``self.teaching_store.triples()`` independently,
     doubling the per-turn O(N) filter+tuple-build cost.  Both
     helpers now accept an optional ``triples`` arg; the pipeline
     computes once and passes through.

  5. ``realize_semantic`` and ``realize_target`` build a
     ``node_id → obj`` map once and look up each step in O(1)
     instead of an O(N) linear scan of ``graph.nodes`` per step.
     The cost was invisible on today's 1-2 node graphs but would
     have become an O(N²) regression on the multi-node graphs
     ADR-0089 Phase C2 plans to introduce.

Dead-code / cleanup:

  - Removed dead ``CognitiveTurnPipeline._fold_compose_into_surface``
    (no callers since PR #76 routed all surface composition
    through ``resolve_surface``).
  - Folded ``_serialize_walk`` + ``_serialize_compose`` (identical
    bodies) into one ``_serialize_operator`` helper.
  - Hoisted ``import json`` and ``RatifiedIntent`` from inside hot
    method bodies to module top (same pattern PR #76 applied to
    ``_is_useful_surface``).
  - Dead-defensiveness sweep on ``ChatResponse`` field reads in
    ``pipeline.run()``: ``getattr(response, "<field>", default)``
    where the field always exists on the dataclass with a default
    is replaced by direct attribute access (6 sites:
    ``realizer_grounded_authority``, ``recalled_words``,
    ``grounding_source``, ``register_canonical_surface``,
    ``pre_decoration_surface``, ``admissibility_trace``,
    ``region_was_unconstrained``).  ``refusal_reason`` retains the
    guarded read because ADR-0024 Phase 2 leaves its
    materialisation site dormant.

Benchmark profiler:

  - ``benchmarks/pipeline_profiler.py`` rebound from
    ``classify_intent`` to ``classify_compound_intent`` (the new
    single-classification site).  All other timing hooks unchanged.

Tests:

  - 4 new tests in ``tests/test_comb_pass_hot_path.py`` pin: OOV
    aggregates exist as bools; compound classifier runs exactly
    once per turn; ``triples()`` materializes exactly once per
    turn; realizer correctly resolves obj slots across an 8-node
    graph.
  - All existing tests pass.  ``core eval cognition`` byte-identical:
    public 100/100/91.7/100, dev 100/100/78.6/100, holdout
    100/100/83.3/100.
  - ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
    ``runtime`` 19/0.
2026-05-20 20:31:56 -07:00
Shay
de3f40b549
feat(cognition): opt-in grounded-realizer authority flag (ADR-0088 Phase B) (#88)
Closes audit Finding 2 (2026-05-20) — Phase B substrate.

Pre-fix ``CognitiveTurnPipeline.run()`` invoked ``realize_semantic``
on the ungrounded ``PropositionGraph``.  Every non-COMPARISON /
non-CORRECTION node was born with ``obj = "<pending>"`` and the
realizer emitted surfaces like ``"X is defined as ..."`` that
``_is_useful_surface`` correctly rejected.  The realizer therefore
never won the surface resolver introduced by PR #76 — it was
structurally present but semantically inert in the hot pipeline
path.

This PR follows the codebase's standard substantive-change pattern
(ADR-0046 ``forward_graph_constraint``, ADR-0062 ``composed_surface``,
ADR-0083 ``transitive_surface``, ADR-0085 ``gloss_aware_cause``):
ship the wiring behind a flag, default ``False``, with a CI-pinned
null-lift invariant.

Changes:

  * ``RuntimeConfig.realizer_grounded_authority: bool = False`` —
    operator-level opt-in.
  * ``ChatResponse.recalled_words: tuple[str, ...] = ()`` —
    alphabetic-filtered walk tokens from the recall step, populated
    on the main path of ``ChatRuntime._chat``.  ``walk_tokens`` is
    now computed unconditionally so non-English packs also surface
    them (English keeps using them for
    ``articulate_with_intent`` as before).
  * ``CognitiveTurnPipeline.run()`` — when the flag is set and the
    response carries any recalled words, calls
    ``ground_graph(graph, response.recalled_words)`` and re-invokes
    ``realize_semantic`` on the grounded graph.  The surface
    resolver (PR #76) then picks the realizer's grounded output
    when it clears ``_is_useful_surface`` and the unknown-domain
    gate did not fire.

Phase A (realizer fluency parity — gloss-aware templates, 3sg verb
agreement, pack-provenance tag) is documented in ADR-0088 §Phase A
and is the prerequisite for enabling this flag in production.  The
known fluency gap (e.g. ``"Light is a visible medium that reveal
truth"`` — subject-verb disagreement leaking from realizer
templates) is the reason the flag ships default-off: operators get
the wiring stable now, the realizer becomes a real authority once
Phase A's fluency upgrade lands.

Verification:

  * 4 new tests in ``tests/test_realizer_grounded_authority_flag.py``:
      - flag defaults to ``False`` on ``DEFAULT_CONFIG``
      - flag-off produces byte-identical surface + trace_hash
        (null-lift invariant)
      - ``recalled_words`` is populated on the main path
      - flag-on runs end-to-end without crashing (surface is
        well-formed regardless of which authority won the resolver)
  * ``core eval cognition`` — public 100/100/91.7/100,
    byte-identical to the MEMORY baseline (default-off).
  * ``core test --suite cognition`` — 120/0/1.
  * ``core test --suite smoke`` — 67/0.
  * ``core test --suite runtime`` — 19/0.
2026-05-20 20:00:58 -07:00
Shay
133a1a3e1c
feat(cognition): compound-intent observability substrate (ADR-0089 Phase C1) (#89)
Closes audit Finding 4 (2026-05-20) — Phase C1.

Pre-fix ``CognitiveTurnPipeline.run()`` called only the single-intent
``classify_intent`` and silently dropped every secondary clause of a
compound prompt like *"What is X and how does it relate to Y?"*.
The graph never saw the second subject, the resolver never saw the
second clause, and the trace recorded only the dominant clause —
with no operator-visible evidence that anything was dropped.

Phase C1 is the **observability substrate** for ADR-0089: the
pipeline now also runs ``classify_compound_intent`` at step 1b and
records every dropped secondary clause on
``CognitiveTurnResult.dropped_compound_clauses``.  The dominant
clause continues to route through the existing single-intent path
exactly as before — surfaces, trace_hashes, and every existing test
remain byte-identical.

Changes:

  * ``CognitiveTurnPipeline.run()`` calls ``classify_compound_intent``
    alongside the existing ``classify_intent`` and computes
    ``dropped_compound_clauses = compound.parts[1:]`` when the
    compound is multi-part.
  * ``CognitiveTurnResult.dropped_compound_clauses:
    tuple[DialogueIntent, ...] = ()`` — empty tuple == single-clause
    turn; len > 0 == operator-visible evidence of dropped secondary
    clauses.

Out of scope (per ADR-0089):

  * Phase C2 (opt-in multi-node graph dispatch + widened trace_hash
    + multi-clause surface) is deliberately scoped to a separate
    PR because it widens ``compute_trace_hash``, the surface
    resolver contract, and ``plan_articulation``.
  * The dominant-clause routing path is unchanged: the audit's
    broken-subject case ("truth, and why does it matter") is *not*
    fixed here — that improvement is Phase C2 scope.

Verification:

  * 4 new tests in ``tests/test_compound_intent_substrate.py``:
      - single-clause prompts record empty
        ``dropped_compound_clauses``
      - AND-joined compound surfaces the secondary clause as a
        DialogueIntent with the right tag (CAUSE for "why does ...")
      - the user-visible surface and trace_hash for a compound prompt
        are byte-identical across two independent runs (no behavior
        change at the truth-path layer)
      - prompts without a recognised connector do not invent a
        secondary clause
  * ``core eval cognition`` — public 100/100/91.7/100, byte-identical
    to the MEMORY baseline.
  * ``core test --suite cognition`` — 120/0/1.
  * ``core test --suite smoke`` — 67/0.
  * ``core test --suite runtime`` — 19/0.
2026-05-20 19:59:38 -07:00
Shay
401ae53328
chore(generate): make stop-tokens caller-overridable via RuntimeConfig (#87)
Closes audit Finding 6 (2026-05-20).

Pre-fix ``_STOP_TOKENS = frozenset({"it", "to", "word"})`` was
hardcoded inside ``generate.stream.generate()`` and inhibited those
three tokens unconditionally across every pack, every language, and
every domain.  If a pack legitimately needed one of them as a content
word — e.g. a philosophy pack where ``"word"`` maps to λόγος, or a
syntax pack where ``"to"`` is a content node — there was no override
path.  The ``_try_index`` guard handled the case where the token was
absent from the pack, but offered nothing for packs that contained
the token and meant it.

Changes:

  * ``generate.stream.generate`` accepts ``stop_tokens: frozenset[str]
    | None = None``.  ``None`` resolves to the historical
    ``_STOP_TOKENS`` constant, preserving byte-identity for every
    pre-Finding-6 caller.
  * ``RuntimeConfig.stop_tokens: tuple[str, ...] | None = None`` —
    operator-level override threaded through ``ChatRuntime`` into
    ``generate()``.
  * Default ``None`` preserves byte-identical behavior for every
    existing pack and every existing test.

Scope notes:

  * This PR delivers the *runtime override* surface.  Manifest-driven
    per-pack overrides (``generation_stop_tokens`` field in the pack
    manifest) are the natural next step but require a pack-schema
    ADR and re-ratification of every affected pack, so the wiring
    lands first and the manifest field follows on a separate ADR.
  * ``agenerate`` was identified as unreachable and is being deleted
    in a sibling PR (Finding 7); its hardcoded ``_STOP_TOKENS``
    reference disappears with it, so it is intentionally not touched
    here.

Verification:

  * 4 new tests in ``tests/test_stop_tokens_override.py``:
      - ``RuntimeConfig.stop_tokens`` defaults to ``None``
      - ``generate()`` signature exposes ``stop_tokens`` with default
        ``None``
      - the historical constant is unchanged
      - an explicit override flows through the runtime end-to-end
  * ``core eval cognition`` — public 100/100/91.7/100, byte-identical
    to the MEMORY baseline.
  * ``core test --suite cognition`` — 120/0/1.
  * ``core test --suite smoke`` — 67/0.
  * ``core test --suite runtime`` — 19/0.
2026-05-20 19:59:33 -07:00
Shay
e41a14f76c
chore(ratifier): calibrate default ratification threshold 0.0 → 0.5 (#86)
Closes audit Finding 3 (2026-05-20).

Pre-fix ``ratify_intent`` defaulted to ``threshold=0.0``, which admits
anything with non-negative ``cga_inner(prompt, anchor)`` — the field
gate (ADR-0022 §TBD-1) was structurally live but semantically
transparent.  RATIFIED was logged on essentially every turn because
the CGA inner product over conformal space is not sign-symmetric.

Measurement (``scripts/calibrate_ratification_threshold.py``):

  * Runs every cognition eval prompt (45 cases = 13 public + 13 dev +
    19 holdout) through a primed ``CognitiveTurnPipeline``.
  * Captures the actual ``cga_inner(prompt, anchor)`` score from the
    pipeline's own ``_ratify_intent`` via a temporary spy on the
    imported ``ratify_intent`` binding.

Observed distribution:

  * 34 RATIFIED:  min=+1.1039  p10=+1.1039  median=+2.6820  max=+5.7508
  * 11 PASSTHROUGH (no vocab-grounded anchor available; score=0.0)
  *  0 DEMOTED at any threshold ≤ 1.10

Threshold = 0.5 chosen as the calibrated default:

  * Well below the empirical floor of 1.10 — every currently-passing
    case stays RATIFIED, byte-identically.
  * Clearly non-trivially positive — random Cl(4,1) inner products
    fluctuate around zero, so 0.5 demands genuine correlation with
    the anchor rather than passive non-negativity.
  * Leaves headroom for the gate to actually demote weakly-aligned
    off-corpus / adversarial prompts to UNKNOWN and route them
    through the honest-refusal surface.

Verification:

  * ``core eval cognition`` — public 100/100/91.7/100, holdout
    100/100/83.3/100, dev 100/100/78.6/100 — byte-identical to
    MEMORY baselines.
  * ``core test --suite cognition`` — 120/0/1
  * ``core test --suite smoke`` — 67/0
  * ``core test --suite runtime`` — 19/0
  * 2 new tests in ``tests/test_ratification_threshold_default.py``
    pin both the constant and the signature default so a future
    change cannot silently regress to ``0.0``.
2026-05-20 19:59:25 -07:00
Shay
4f9e00a6a5
fix(cognition): bound speculative-subject cache + evict on COHERENT promotion (#85)
Closes audit Finding 5 (2026-05-20).

Pre-fix ``CognitiveTurnPipeline._speculative_subjects`` was a bare
``set[str]`` that only grew over a session.  Two correctness gaps:

  * A subject promoted to ``EpistemicStatus.COHERENT`` via the teaching
    review loop kept appearing with the "(speculative, not yet
    reviewed)" marker forever, contaminating reviewed material on
    later probes.
  * Long teaching sessions widened the per-turn substring scan in
    ``_should_mark_speculative`` without bound.

Fix:

  * Back the cache with ``OrderedDict[str, None]`` (LRU) capped at
    ``_MAX_SPECULATIVE_SUBJECTS = 64``.
  * Introduce ``_remember_speculative_subject`` (insert / refresh) and
    ``_forget_speculative_subject`` (evict) helpers; route all
    SPECULATIVE inserts through them.
  * When a proposal lands as ``EpistemicStatus.COHERENT``, evict the
    subject and every long-enough non-stopword token derived from it,
    so the marker stops appearing on reviewed material.

Iteration order in ``_should_mark_speculative`` is unchanged (keys
view); lookups remain O(1).  No surface change for any case the prior
behavior didn't already mishandle, so byte-identical eval surfaces
stay stable (verified locally against ``core eval cognition`` public /
holdout / dev splits — all unchanged from MEMORY baseline).

Tests (7 new, ``tests/test_speculative_subject_lifecycle.py``):

  * storage is an OrderedDict and the cap is 64
  * remember normalizes (lower+strip) and drops empty input
  * remember refreshes LRU position on re-insert
  * cache caps at 64 with insertion-order eviction
  * forget is case-insensitive and removes the entry
  * forget on a missing / empty subject is a no-op
  * ``_should_mark_speculative`` triggers after remember and stops
    triggering after forget

Audit findings referenced:
https://github.com/AssetOverflow/core/pull/76 (Finding 5, "Unbounded
``_speculative_subjects``")
2026-05-20 19:59:21 -07:00
Shay
ff1dcb2594
fix(cognition): declare surface authority resolution (#76)
* fix(cognition): add explicit surface resolution policy

* test(cognition): cover explicit surface resolution policy

* fix(cognition): route pipeline surfaces through resolver

* fix(cognition): address PR #76 review comments

- hoist `_is_useful_surface` import from inside `run()` to module top
- call `_render_walk_surface` / `_render_compose_surface` via the class
  name (both are @staticmethod) for consistency with the existing
  `_fold_*_into_surface` helpers
- drop redundant `realized_surface` truthiness check in
  `resolve_surface` — `realizer_useful` already excludes empty /
  placeholder surfaces via `_is_useful_surface`

Tests: tests/test_surface_resolution.py + tests/test_cognitive_turn_pipeline.py
green (16 passed); cognition suite 120/1s, smoke suite 67/0.
2026-05-20 19:42:10 -07:00
Shay
0cf54a009d
feat(adr-0087): rhetorical-style pack substrate (loader + default_unstyled_v1) (#74)
Substrate-only code-side for ADR-0087 (Rhetorical Style as Selection
Axis). No composer or realizer touches the new pack yet; consumer
integration is the follow-up ADR.

packs/rhetorical_style/ (new)
  - loader.py: RhetoricalStylePack frozen dataclass + load_rhetorical_
    style_pack() with fail-closed mastery-report self-seal verification
  - __init__.py: re-exports (RhetoricalStylePack, RhetoricalStylePack-
    Error, load_rhetorical_style_pack, DEFAULT_RHETORICAL_STYLE_PACK)
  - default_unstyled_v1.json + .mastery_report.json: ratified null-lift
    baseline pack (all three constraint lists empty,
    default_unstyled=true)

scripts/ratify_rhetorical_style_packs.py (new)
  - Mirror of scripts/ratify_anchor_lens_packs.py for the rhetorical-
    style pack family. Computes pack_source_sha256 with mastery_report_
    sha256 blanked, builds self-sealed mastery report, writes both
    files. Idempotent. Uses formation.hashing for canonical JSON +
    self_seal.

Schema gate (ADR-0087 §Verification)
  - Required keys allow-list: pack_id, schema_version, version,
    issued_at, default_unstyled, permitted_frames,
    required_moves_per_claim, forbidden_moves, provenance,
    mastery_report_sha256
  - Unknown keys rejected (strict gate)
  - permitted_frames: allow-list {warrant, concession, hedge,
    definitional_move}
  - required_moves_per_claim / forbidden_moves: allow-list {claim,
    evidence, warrant, concession, hedge, bare_assertion, definitional}
  - default_unstyled=true ⟺ all three lists empty
  - non-default pack must declare at least one constraint (distinguishes
    from null-lift)
  - Duplicates within a list rejected

Ratification gate
  - require_ratified=True by default
  - CORE_ALLOW_UNRATIFIED_RHETORICAL_STYLE=1 env-var bypass for dev
  - Companion mastery report SHA must match pack's declared sha
  - verify_seal(report) must pass (self-seal integrity)
  - Sister to packs.safety.SafetyPackError pattern

core/config.py
  - Added RuntimeConfig.rhetorical_style_id: str | None = None
  - No runtime code reads it yet — that's the consumer ADR's job
  - Field declared so the interface is stable when consumer lands

Tests (tests/test_adr_0087_rhetorical_style_substrate.py — 20)
  - Default pack loads, is_null_lift, mastery-report self-seal verified,
    discovery lists it as ratified
  - Schema gate: missing key, unknown key, unknown frame, unknown move,
    duplicate frame, default_unstyled-with-constraints,
    non-default-with-zero-constraints, pack_id mismatch, path traversal
  - Ratification gate: unratified pack rejected by default, env-var
    bypass, companion report missing, companion sha mismatch
  - RuntimeConfig field: default None, accepts string, independent of
    other axes

Lanes
  smoke 67/0, cognition 120/0/1, packs 6/0. core eval cognition
  byte-identical 100/91.7/100/100.

Null-lift consumer test deferred
  ADR-0087 §Required tests lists rhetorical_style_null_lift as a
  required invariant. Today it would be trivially true because no
  composer reads the field. The invariant becomes meaningful when
  the consumer ADR wires the field through the dispatch — at that
  point the null-lift test goes into the consumer PR alongside the
  three-axis orthogonality test.

Scope per ADR-0087 §Scope limits
  - No consumer code (composer/realizer changes deferred)
  - No genre packs (en_academic_v1, etc. are content efforts after
    consumer lands)
  - No prompt-routing (operator-set only)
2026-05-20 16:19:36 -07:00
Shay
4b9404a88e
feat(adr-0085): gloss-aware CAUSE composer — explanation frame from glosses (#70)
The original "Why does light exist?" complaint that motivated ADR-0084
was specifically about CAUSE-intent surfaces. ADR-0084 (substrate) +
PR #65 (content) already moved DEFINITION/RECALL to gloss-grounded
surfaces ("Light is visible medium that reveal truth."). But CAUSE
still dispatched through the chain-walk path:

  Before: light — teaching-grounded (cognition_chains_v1):
            cognition.illumination; logos.core.
            light reveals truth (cognition.truth).
            No session evidence yet.

  After:  Light exists as visible medium that reveal truth.
          pack-grounded (en_core_cognition_v1).

The chain-walk is structurally correct but the wrong SHAPE for a why-
question — it's a graph traversal, not an explanation. ADR-0085 fixes
the shape using the same gloss material that DEFINITION/RECALL already
consume, with no new content authoring.

Additive composer
  chat/pack_grounding.py:gloss_aware_cause_surface()
  - Resolves gloss via lexicon-residency-checked resolve_gloss().
  - Frames POS-aware:
      NOUN -> "{Lemma} exists as {gloss}."
      VERB -> "To {lemma} is to {gloss}."
      ADJ  -> "To be {lemma} is to {gloss}."
      *    -> falls back to _frame_gloss (predicate-identity).
  - Threads anchor lens via the existing helper (ADR-0073c parity).
  - Returns None when no gloss exists — runtime falls through to the
    existing chain-walk path. Additive: no CAUSE case loses its surface.

Runtime dispatch
  chat/runtime.py — IntentTag.CAUSE tries gloss path FIRST under the
  flag; falls through to teaching_grounded_surface* on None.
  Unconditional fallback — never silent.

Opt-in flag
  core/config.py — RuntimeConfig.gloss_aware_cause: bool = False
  Default off preserves pre-ADR-0085 chain-walk surfaces byte-
  identically (null-drop invariant, CI-pinned).

Prompt-diversity classifier update
  evals/prompt_diversity/runner.py — _CAUSE_MARKERS widened with the
  explanation-frame markers ("exists as", "is to", "to be", "is for",
  "purpose of") plus bare-form predicates ("reveal" alongside
  "reveals"). Neither composer path is penalised on shape_fit just on
  inflection grounds.

v1/public lift (flag OFF vs ON, 26 cases)
  intent_accuracy        : 65.4% -> 65.4%   ( — )
  versor_closure_rate    : 100.0% -> 100.0% ( — )
  response_shape_fit     : 57.7% -> 57.7%   ( — , both frames recognized)
  audit_in_surface_rate  : 42.3% -> 42.3%   ( — , envelope ADR's job)
  gloss_quote_rate       : 11.5% -> 23.1%   (+11.5pp, structural lift)

Tests (15)
  - 5 pure composer (NOUN/VERB frame, unknown/empty None, no chain-
    walk artifacts in surface)
  - 5 runtime dispatch (flag-off chain-walk, flag-on gloss, parametrized
    across glossed subjects, VERIFICATION unchanged under flag, no-
    gloss fallback engages)
  - 5 cognition lane invariance (aggregate metrics byte-identical
    under both flag states; surfaces deliberately shift on the 2 CAUSE
    cases with glossed subjects — the structural-change-vs-metric-
    invariance both-sides invariant)

Lanes
  smoke 67/0, cognition 120/0/1 skipped, packs 6/0, teaching 17/0,
  runtime 19/0. core eval cognition byte-identical 100/91.7/100/100
  under both flag states.

Scope limits (per ADR §Scope limits)
  - CAUSE only; VERIFICATION still chain-walks (different shape).
  - English pilot only; Greek/Hebrew packs not opted into definitional
    layer yet (ADR-0084 scope limit).
  - Single-lemma subjects; compound/anaphoric fall through.
  - Opt-in until cognition holdout confirms the lift transfers off-
    fixture. Future PR flips default on.

Out of scope
  - Surface-vs-envelope cleanup ("pack-grounded (...)" still leaks).
  - Predicate licensing (ADR-0086).
  - Content style pass (bare lemma forms in glosses — separate brief).
2026-05-20 15:55:08 -07:00
Shay
6b0d723987
fix(evals): prompt_diversity gloss-quote heuristic — 4-token window → substring (#69)
The v1 gloss-quote detector used a 4-token contiguous window of
≥4-char tokens.  That heuristic was too strict for the actual ADR-0084
brief gloss style, which is deliberately short and primitive-only:

  light    "visible medium that reveal truth"   5 tokens ≥4 chars
  parent   "person with a child"                3 tokens ≥4 chars   ← can't window
  recall   "get memory from before"             3 tokens ≥4 chars   ← can't window
  wisdom   "good use of knowledge"              2 tokens ≥4 chars   ← can't window

Result: post-PR #65 baseline showed gloss_quote_rate=0.0% even though
the pack-grounded composer was visibly emitting glosses verbatim:

  surface: "Parent is person with a child. pack-grounded (en_core_relations_v1)."
  gloss:   "person with a child"
  window:  could not even form

Replace with substring match against the gloss text.  The composer
emits the gloss verbatim (no paraphrasing — that's the no-LLM
discipline), so substring is exact, high-confidence, and trivially
correct:

  gloss_quoted ⟺ gloss.lower().strip() in surface.lower()

Re-baselined v1/public (26 cases):
  gloss_quote_rate: 7.7% (false-positive 4-token window noise)
                  → 0.0% (post-#65, broken metric)
                  → 11.5% (this PR, real signal)

The other four metrics unchanged.  3/26 cases (DEFINITION on
``evidence``/``recall``/``parent``) are detected as gloss-quoted now,
which matches reality — the pack-grounded composer at
chat/pack_grounding.py:398 has been gloss-aware all along; it just
had no glosses to quote pre-#65.

Why this is just a heuristic refinement, not a contract change:

The contract.md still says v1 has NO pass thresholds beyond
versor_closure_rate==1.00.  The lane's job is to establish baseline
distribution.  The heuristic was *measuring the wrong thing* — fixing
the measurement is a contract clarification, not a contract change.

Tests added (TestGlossQuote, 4 cases):
  - short brief-style gloss detected via substring
  - chain-walk surface for same lemma NOT counted as gloss-quoted
  - unknown term returns False
  - empty terms returns False

Updated the function docstring with the post-#65 context so future
readers understand why v1's contract predicted 0% but reality is ~12%.
2026-05-20 15:43:01 -07:00
Shay
1938aaa674
test(adr-0084): integration test pins substrate gate against ratified content (#68)
After PR #64 (substrate) and PR #65 (content) both landed on main, this
test is the promised follow-up that exercises the substrate-callable
verify_definitional_closure against the real ratified content rather
than fixture packs. It pins three contracts:

  1. Substrate-vs-content handshake. The standalone
     scripts/verify_definitional_closure.py is the agent's dev-loop
     tool; this test is the gate-callable equivalent the ratification
     pipeline can invoke. Both must agree on what passes — divergence
     is a contract bug.

  2. Content drift catcher. Any future content edit that adds an
     unresolved token / non-mounted dependency / silent staging leak
     fails this test before the edit lands on main.

  3. Staging exclusion. en_minimal_v1 is staging per the ADR-0084
     pack-content brief and must not be load-bearing for the closure
     rule. Test-pinned via a production-pool subtest.

Substrate fix: allow empty definitional_atoms

The substrate's strict parser previously rejected empty
definitional_atoms. That stance was wrong: per the ADR-0084 pack-
content brief, the per-entry atom list excludes articles, prepositions,
and copulas. A gloss whose every content word is a function word
(e.g. en_core_temporal_v1/prior → "before") has zero content atoms by
construction. The closure rule passes vacuously when atoms is empty
— there is nothing to close. The gloss-vs-atoms mismatch check in
the standalone verifier is the second-layer gate that distinguishes
by-construction emptiness (legitimate) from by-omission emptiness
(laziness). Substrate parser shouldn't double-gate the same concern.

The corresponding substrate test flipped from
test_empty_definitional_atoms_rejected to
test_empty_definitional_atoms_accepted, with comment explaining the
reasoning.

Primitives expansion: can + action

Two content entries (en_core_cognition_v1/person → "who can know and
do" and en_core_meta_v1/intend → "decide before an action") leaned on
'can' and 'action' as atom references. Today those lemmas resolve
ONLY via en_minimal_v1/lexicon.jsonl — the staging pack. That's a
production-vs-staging leak: production content should not be load-
bearing on staging.

Two clean alternatives:
  (a) rewrite the two glosses to avoid 'can' and 'action'
  (b) promote 'can' and 'action' to primitives

Chose (b): both lemmas are genuinely terminal-feeling (can is a basic
capability modal; action is an irreducible "what is done"); the
content reads more naturally with them present than with paraphrased
substitutes; and the floor was always going to need both eventually.
The cost is two primitives.jsonl rows + checksum + count bump.

Verification:
  scripts/verify_definitional_closure.py            exit 0
  tests/test_adr_0084_integration_closure.py        30/30 pass
  tests/test_adr_0084_definitional_substrate.py     39/39 pass
  core test --suite smoke -q                        67/67
  core test --suite packs -q                         6/6
  core eval cognition                               byte-identical
                                                    (100/91.7/100/100)

Two-layer gate now in place:
  - standalone verifier (dev loop, gloss/atom mismatch check)
  - substrate verifier (ratification gate, parametrized over every
    opted-in pack, staging-exclusion test, primitives floor coverage)
2026-05-20 15:35:37 -07:00
Shay
48282eef8d
feat(adr-0084): definitional layer — proposal + substrate (schema/loader/closure) (#64)
* docs(adr-0084): propose definitional layer + prompt-diversity suite

Three companion artifacts proposing the next substantive design step
after ADR-0083:

1. ADR-0084 (Proposed) — Definitional Layer for Lexicon Packs
   Optional `definition` block on pack entries: gloss,
   definitional_atoms, predicates_invited, definition_version,
   provenance.  Pack-level opt-in.  Closure rule: every word in a
   gloss must resolve to a same-pack lemma, another mounted pack's
   lemma, or a primitive in a new `packs/primitives/` pack.
   NO composer change in this ADR (sequenced for ADR-0085) —
   ratify substrate before any consumer depends on it.

2. evals/prompt_diversity/ (Proposed) — companion eval lane
   ~50 cases across question-shape × sophistication × domain,
   measuring three new metrics: response_shape_fit,
   audit_in_surface_rate (quantifies the trust-boundary leak into
   user surfaces), gloss_quote_rate (zero today; rises with future
   gloss-aware composer).  No v1 pass thresholds — the lane
   establishes a baseline distribution so future work has
   something to move.  26 seed cases authored covering all 21
   categories.

3. docs/handoff/ADR-0084-pack-content-brief.md — paste-ready brief
   for a cheaper/faster dev agent to produce the pack content in
   parallel.  Self-contained, 5 sequenced phases (primitives pack
   → extend 9 existing glosses → add to relations/anchors → write
   closure verifier → run safety lanes), explicit don't-touch list
   (no composer / runtime / algebra / Greek+Hebrew packs / schema
   parser), no-LLM-glosses discipline, per-phase acceptance.

Discovery while drafting: 9 packs already carry glosses.jsonl
under language_packs/data/ with a flat schema (78 entries in
en_core_cognition_v1 alone).  The brief reflects that — most
work is extending existing entries, not authoring from scratch.

Strategic context: ADR-0083 raised the *depth* ceiling on chain
composition; ADR-0084 raises the *fidelity* ceiling.  The φ
separation probe (memory: phi-separation-falsified) established
that semantic capability lives in chain composition, not in φ
geometry, so deepening the composer's substrate is the natural
next step.  ADR-0084 → 0085 (gloss-aware composer) → 0086
(predicate licensing at ratification) is the planned sequence.

* feat(adr-0084): substrate — schema parser, primitives loader, closure verifier

Substrate-only code-side for ADR-0084 (Definitional Layer for Lexicon Packs).
No composer touches the new fields yet; consumer integration is ADR-0085.

Schema (additive, default preserves byte-identity)
  - LanguagePackManifest.definitional_layer: bool = False
  - compiler loader propagates the flag from manifest.json

language_packs/definitions.py (new)
  - GlossEntry dataclass: lemma, gloss, pos, definitional_atoms,
    predicates_invited, definition_version, provenance_ids
  - parse_gloss_entry(payload, *, strict) — strict mode enforces ADR-0084
    §Schema validation row-by-row: required keys, typed lists, no
    unknown keys, positive definition_version; lax mode preserves the
    legacy two-field shape for back-compat
  - load_pack_glosses(pack_id, *, strict) with cache + clear hook
  - verify_definitional_closure(pack_id, *, mounted_pack_lemmas,
    primitive_lemmas, strict) returning tuple[ClosureViolation, ...];
    case-insensitive resolution; cycles permitted per ADR

packs/primitives/loader.py (new)
  - Sister loader to packs/safety/ and packs/identity/
  - PrimitivesPack frozen dataclass with .lemmas frozenset
  - Gates: checksum match, kind=='primitives', definitional_layer:true,
    never_auto_mutable:true, pack_id matches dir, primitive_count
    cross-check, duplicate-lemma rejection, path-traversal rejection,
    strict per-entry schema with allow-list
  - DEFAULT_PRIMITIVES_PACK = 'en_semantic_primitives_v1'

tests/test_adr_0084_definitional_substrate.py
  - 38 tests covering strict parser (each required key rejection, unknown
    key rejection, empty predicates_invited allowed, empty
    definitional_atoms rejected, invalid definition_version), lax
    parser back-compat, load_pack_glosses (missing/strict raise/lax
    skip/malformed JSON), closure verifier (same-pack/primitive/mounted/
    unresolved/case-insensitive), primitives loader (every gate), and
    a back-compat check that every shipped pack still ratifies with
    definitional_layer=False

Lanes: smoke 67/0, cognition 120/0/1, teaching 17/0, runtime 19/0,
packs 6/0. Cognition eval byte-identical 100/91.7/100/100.

When the content PR lands (primitives.jsonl + extended glosses.jsonl
under ADR-0084-pack-content-brief.md), the gate catches any closure-rule
violation without further code change.

* feat(evals): prompt_diversity lane runner — measurement instrument for ADR-0084+

Implements the runner against the existing contract.md + 26-case v1
public split.  Lane auto-discovered by evals.framework via the standard
contract + runner convention.

Runner (evals/prompt_diversity/runner.py)
  - run_lane(cases, *, config, workers) -> LaneReport
  - 5 metrics: intent_accuracy, versor_closure_rate (carried over from
    cognition), plus the three new lane-specific metrics —
    response_shape_fit, audit_in_surface_rate, gloss_quote_rate
  - breakdown dict groups by (question_shape, sophistication, domain)
    per contract §How to read the output
  - mirrors evals.cognition.runner's parallel worker pattern

Per-shape classifier (deliberately substring/regex-simple at v1)
  - predicate_identity, explanation, sequence, two_subject_contrast,
    narrative, honest_disclosure
  - Unknown shape => neutral pass (don't penalise new categories)

Audit-leak detector
  - trust-boundary preamble markers (teaching-grounded (, pack-grounded
    (, No session evidence yet.)
  - dotted semantic-domain tag regex (cognition.illumination, etc.)

Gloss-quote detector
  - resolves expected_terms via chat.pack_resolver.resolve_gloss
  - 4-token contiguous-window match against surface (high-confidence
    "gloss actually quoted", not "shared one common word")

Tests (tests/test_prompt_diversity_runner.py — 23)
  - shape classifier parametrized over the six expected_shape values
  - audit-leak detector parametrized over preamble + tag + clean cases
  - end-to-end on v1 public:
      * versor_closure_rate == 1.0 (only v1 pass threshold per contract)
      * every metric in [0, 1]
      * breakdown groups present with the four per-cell metrics
      * diversity gate: >=5 question shapes, >=3 domains
        (defends against future regressions that collapse the suite
         back to a cognition-shaped fixture)

v1/public baseline (26 cases)
  intent_accuracy      : 65.4%   (contract predicted 70-85%)
  versor_closure_rate  : 100.0%  (only v1 pass threshold)  PASS
  response_shape_fit   : 53.8%   (contract predicted low)
  audit_in_surface_rate: 42.3%   (contract predicted ~100%)
  gloss_quote_rate     :  7.7%   (contract predicted 0%)

Three baseline surprises worth noting in the report (NOT failures —
the v1 lane is explicitly there to establish the distribution):

  - audit_in_surface_rate at 42% (not 100%) means the chain-walk leak
    fires on ~11/26; the other 15 are honest-disclosure cases that
    emit no audit envelope.  Sharpens the future surface-vs-envelope
    ADR's actual target: grounded surfaces specifically.
  - response_shape_fit at 54% (not "low") — classifier likely has
    false positives on the ", which " cause-marker.  Worth tightening
    once we have an ADR-0085 baseline to compare against.
  - intent_accuracy at 65% (below predicted 70-85%) — classifier dips
    harder on adversarial/cross-pack than expected.  Real gap.

All five smoke/cognition/teaching/runtime/packs lanes still green;
core eval cognition byte-identical 100/91.7/100/100.

* feat(packs): ADR-0084 pack content (primitives + extend glosses + closure verifier) (#65)

* feat(packs): ADR-0084 pack content

* feat(packs): repair ADR-0084 definitional content

* test(adr-0084): adjust substrate manifest tests for post-#65 content reality

PR #65 flipped definitional_layer:true on 13 English packs (9 core +
4 relations + collapse-anchors).  The substrate's previous test
test_existing_packs_unchanged asserted that en_core_cognition_v1 +
en_core_relations_v1 still had definitional_layer:False — which was
the right pre-content invariant but is wrong post-content.

Replace it with two complementary tests that hold against real content:

  - test_non_opted_packs_default_false:
      pins that packs that DIDN'T flip the flag (en_minimal_v1,
      he_core_cognition_v1, grc_logos_cognition_v1) still surface
      definitional_layer=False through the loader.  Defends against
      a future change accidentally flipping the flag on a non-opted
      pack.

  - test_opted_packs_carry_flag:
      pins that packs that DID flip the flag (en_core_cognition_v1,
      en_core_relations_v1) surface definitional_layer=True through
      the loader.  Proves the substrate's manifest-field propagation
      works against real ratified content, not just fixture packs.

Net: +1 test, same intent (substrate ratifies the manifest field
correctly), now with real-content coverage on both sides of the gate.

All 62 ADR-0084 substrate + prompt-diversity tests pass.
2026-05-20 15:25:25 -07:00
Shay
4d26e1503b
fix(tests): make frontier_compare viewer test resilient to copy refreshes (#67)
test_frontier_compare_report_viewer_exists was failing on main against
the current report_viewer.html because two verbatim substring checks
no longer matched the viewer's UI copy:

  - "Drop report JSON"  →  viewer now says "Drop JSON report" (order swapped)
  - "No network calls"  →  viewer now says "no network calls" (lowercase)

Both copy refreshes were behavior-preserving — drop-zone affordance
and network-free trust boundary are both intact in the viewer. The
test was coupling to verbatim phrasing rather than to the load-bearing
affordances.

Switch to case-insensitive substring checks that pin what actually
matters:
  - "frontier compare" — viewer identity
  - "drop" AND "json" together — drop-zone affordance, order-independent
  - "no network calls" — trust boundary (case-insensitive)
  - fetch(/XMLHttpRequest still hard-banned (case-sensitive — these
    are JS API surface, not human-readable copy)

Pre-existing failure flagged in PR #66's body as out-of-scope cleanup;
this is that cleanup.
2026-05-20 15:13:38 -07:00
Copilot
dedf05565d
feat(frontier): add replay variability suite and token-cost telemetry (#66)
Agent-Logs-Url: https://github.com/AssetOverflow/core/sessions/f88b48fa-0c2a-4f9d-a42b-d275596e43b8

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AssetOverflow <109810776+AssetOverflow@users.noreply.github.com>
2026-05-20 15:04:34 -07:00
Shay
9e6fa4be75
feat(adr-0083): transitive (multi-hop) teaching-grounded surface (#63)
Strict superset of ADR-0062's depth-1 composer.  `max_depth` is the
number of follow-up hops appended beyond the initial chain:

  max_depth=0  → byte-identical to single-chain surface
  max_depth=1  → byte-identical to ADR-0062 composed
  max_depth=2  → byte-identical to ADR-0062 when no second hop
                 survives, strict superset when one does

The composer surfaces content the realizer was silently dropping
from chains already ratified in `cognition_chains_v1`.  Example
live lift on `"Why does light exist?"`:

  composed: "light reveals truth, which grounds knowledge."
  transitive(2): "...which grounds knowledge, which requires evidence."

Cycle-safe at every depth via a single visited-set; single-corpus
traversal in v1 (cross-corpus transitive deferred to a follow-up
ADR alongside ADR-0064's cross-pack model).

Both flags default False — every existing surface is preserved
byte-identically.  When both `composed_surface` and
`transitive_surface` are True, transitive wins.

Implementation:
- `core/config.py`: `transitive_surface: bool = False`,
  `transitive_max_depth: int = 2`.
- `chat/teaching_grounding.py`: `_resolve_followup` shared helper
  refactored out of the depth-1 composer (no behavioural change),
  plus new `teaching_grounded_surface_transitive(subject,
  intent_tag, *, max_depth)`.
- `chat/runtime.py`: dispatch order — transitive > composed > single.

Verification:
- tests/test_transitive_surface.py: 16 new tests covering pure-fn
  contract, visited-set cycle guard at every depth, runtime
  integration, and the cognition-lane null-drop invariant at
  `max_depth=2` (public + holdout splits).
- tests/test_composed_surface.py: 11/11 pass after the helper
  refactor (ADR-0062 behaviour preserved).
- `core test --suite smoke`: 67 pass.
- `core test --suite cognition`: 120 pass, 1 skipped.
- `core test --suite teaching`: 17 pass.
- `core eval cognition`: 100 / 91.7 / 100 / 100 (byte-identical).
2026-05-20 14:11:40 -07:00
Shay
9459f815b0
feat(evals): wire ADR-0082 providers into frontier_compare runner (#61)
#58 shipped providers.py + model_registry.py for cross-provider
benchmarking but never connected them to runner.py — the adapters
sat unused.  This PR wires them through with a clear lane split.

Why a new suite instead of refactoring existing ones
-----------------------------------------------------
The three existing suites (determinism / truth_lock / axis_orthogonality)
pull CORE-only telemetry: trace_hash, versor_condition, register_id,
register_variant_id, anchor_lens_id, register_canonical_surface.
None of those fields can come from OpenAI / Anthropic / Ollama.

Forcing those suites cross-provider would silently produce reports
where the cross-provider rows have empty telemetry — a worse failure
mode than not running them at all.  So the routing is explicit:

  CORE-only suites          → --provider must be 'core'
  Cross-provider suites     → any provider; CORE is one adapter among many

Operator asks for the wrong combo → loud error with the right alternative.

New module: evals/frontier_compare/cross_provider.py
-----------------------------------------------------
- ProviderObservation dataclass — provider-agnostic observation shape
  (prompt, surface, provider, model, elapsed_ms, error fields).  No
  CORE-internal telemetry expected.
- run_prompt_battery(adapter, *, cfg) → SuiteReport reusing existing
  CaseResult / SuiteReport shapes so the report viewer renders both
  lanes without schema branching.
- _PROMPT_BATTERY: 7 fixed cases spanning definition / cause /
  verification / comparison / procedure / unknown intent shapes.
  Stable case_ids so future re-runs against the same provider produce
  diffable JSON.
- Per-case 'passed' is loose by design (non-empty surface, no
  exception).  Cross-provider quality is for human review — not for
  the runner to silently score.

Updated CLI: evals/frontier_compare/__main__.py
-----------------------------------------------
- --provider {core, openai, anthropic, ollama}    (default: core)
- --model <id>                                     (validated via require_model_card)
- --env-file <path>                                (default: ./.env)
- Auto-persist non-CORE runs to
  evals/frontier_compare/results/<provider>_<model>_<utc>.json
  even when --report is omitted.  API calls are rate-limited / paid;
  losing the artifact is costly.
- Existing CORE-native behavior unchanged when --provider not set.

Results directory: evals/frontier_compare/results/
--------------------------------------------------
Created with .gitkeep — matches the convention used by other lanes
(evals/long_context_cost/results/, evals/koine_greek_fluency/results/,
etc.).  Distinct from reports/ which .gitignore excludes for
transient debug output.

Tests: tests/test_frontier_compare_cross_provider.py (9 cases)
--------------------------------------------------------------
- prompt_battery runs with CORE adapter (no API needed)
- adapter exceptions recorded as failed observations, never propagated
- empty surfaces flagged distinctly from adapter errors
- CLI default runs CORE-native (no breaking change)
- CLI prompt_battery with --provider core routes through cross-provider path
- CLI rejects CORE-only suite + non-CORE provider with operator-helpful error
- --help surfaces both suite families
- unregistered model is rejected before any benchmark cycles burn
- ProviderObservation.succeeded handles error / empty / whitespace cases

Live evidence
-------------
$ core test --suite smoke -q
67 passed in 26.55s   (no regression)

$ python -m evals.frontier_compare --provider core --suite prompt_battery --json
model=core-native mode=core suite=prompt_battery passed=True score=1.000
  [definition_truth              ] PASS  Truth is a claim or state grounded by evidence...
  [definition_knowledge          ] PASS  Knowledge is justified understanding grounded...
  [cause_understanding           ] PASS  understanding — teaching-grounded (cognition_chains_v1)...
  [verification_evidence         ] PASS  evidence — teaching-grounded (cognition_chains_v1)...
  [comparison_knowledge_wisdom   ] PASS  knowledge contrasts with wisdom...
  [procedure_recall              ] PASS  To recall means to retrieve a stored state from memory...
  [unknown_term                  ] PASS  I haven't learned 'xylomorphic' yet...

$ python -m evals.frontier_compare --provider openai --suite determinism
error: suite 'determinism' is CORE-only; pass --suite prompt_battery
(the cross-provider suite) when --provider='openai'.

.gitignore: adds frontier_wave1.json (stray report file repeatedly
written by ad-hoc test invocations).
2026-05-20 13:22:37 -07:00
Shay
83c18e4641
fix(cli, tests): wire core contemplation + restore INV-02 allowlist (#60)
Two follow-up fixes from end-of-session verification of recent merges:

1. core/cli.py — wire `core contemplation` subcommand
   PR #55 + #58 added the contemplation CLI at python -m core.contemplation
   but never registered it under the `core` umbrella command, so
   `core --help` didn't show it.  Adds a subparser mirroring the existing
   pattern (chat/test/check/.../doctor) that delegates to the existing
   core.contemplation.__main__:main() — no duplication of arg parsing.

   Surface preserved verbatim: reports (positional, 1+), --lane
   {frontier_compare, contradiction_detection}, --pack-id, --note,
   --report, --sink-root.

2. tests/test_architectural_invariants.py — restore INV-02 allowlist
   PR #57's evals/lab/phi_separation_probe.py imports normalize_to_versor
   for construction-time experimental rotor + embedding work, which
   triggered INV-02's AST-scan failure (the test enforces that
   normalize_to_versor is only called from a small allowed file set).

   evals/lab/ is research-only, never imported by runtime — adding the
   probe to allowed_files doesn't weaken the runtime invariant the
   test enforces.

Verification
------------
$ core test --suite smoke -q
67 passed in 26.63s   (was 66 passed / 1 failed before)

$ core contemplation --help
... shows the new subcommand surface

$ core contemplation evals/contradiction_detection/results/v1_public_*.json \
    --lane contradiction_detection \
    --sink-root /tmp/sink \
    --report /tmp/run.json
... 4 SPECULATIVE findings; sink writes to /tmp/sink/2026/2026-05.jsonl
2026-05-20 13:10:29 -07:00
Shay
1573064349
refactor(contemplation): converge to shared discovery-sink plumbing (#58)
Connects ADR-0080's read-only contemplation loop to the existing
teaching-pipeline plumbing without forcing a type collapse.  The
SPECULATIVE-only invariant from #55 is preserved verbatim; what
changes is *where the findings flow*.

What was wrong with the prior shape
-----------------------------------
PR #55 shipped a parallel core/contemplation/ package whose findings
were written as one JSON blob per CLI invocation, with no consumer.
The SPECULATIVE-only invariant protected a write path that didn't
exist.  My closed PR #56 (second miner) would have entrenched the
duplication.

What this PR changes
--------------------
1. Schema (core/contemplation/schema.py)
   - Adds a BOUNDARY note documenting why EvidencePointer (teaching)
     and ContemplationEvidenceRef (core) intentionally stay separate:
     EvidencePointer.source is constrained to {corpus, pack,
     vault_coherent} — pointers into reviewed in-process memory the
     runtime trusts.  ContemplationEvidenceRef points to external
     report files that have NOT been reviewed.  Converging them would
     either widen the runtime-grounding enum (losing the "reviewed
     memory only" guarantee) or force benchmark reports to masquerade
     as vault_coherent.  Both are worse than keeping them separate.
   - Adds format_contemplation_finding_jsonl(finding) — the canonical
     JSONL formatter mirroring teaching.discovery.format_candidate_jsonl.

2. Runner (core/contemplation/runner.py)
   - Both runners gain an optional sink: DiscoveryCandidateSink | None
     parameter.  When supplied, each finding is emitted as one
     canonical JSONL line via the SHARED protocol — same protocol
     that backs DiscoveryBufferSink and DiscoveryMonthlyFileSink.
   - Sink path is additive: the ContemplationRun blob is byte-identical
     whether or not a sink is supplied (pinned by test).
   - No sink supplied → existing in-memory behavior preserved exactly.

3. CLI (core/contemplation/__main__.py)
   - Adds --lane {frontier_compare, contradiction_detection} flag.
     Default unchanged.
   - Adds --sink-root <path> flag.  When set, instantiates a
     DiscoveryMonthlyFileSink and findings land at
     <root>/<YYYY>/<YYYY-MM>.jsonl — the SAME layout discovery
     candidates use, so operators can grep one stream.

4. Miner (core/contemplation/miners/contradiction_detection.py)
   - Restored from closed PR #56 under the unified pipeline.
   - Failure-mode split preserved (missed_contradiction /
     false_contradiction_flag) with asymmetric repair actions.

What this PR does NOT do
------------------------
- Does NOT unify ContemplationFinding with DiscoveryCandidate.
  DiscoveryCandidate.trigger is Literal[would_have_grounded,
  successful_comparison, hedge_acknowledged, oov_resolved_via_decomp]
  — all turn-loop flavored.  None describe "I parsed a benchmark
  report."  Forcing a 5th trigger that no turn-loop extractor
  produces would pollute the turn-loop type for the schema's sake.
- Does NOT extend teaching/gaps.py.  Gap aggregates DiscoveryCandidate
  cells by (subject, intent) — domain nouns.  ContemplationFinding
  subjects are namespaced ("contradiction_detection/CON-PUB-002").
  Different operator views.  A sibling aggregator can come later
  when an operator actually asks for it.

Why this is the right unification point
---------------------------------------
The honest convergence is at the *sink* (so all SPECULATIVE evidence
lives in one rooted append-only stream), not the *aggregator* (which
appropriately produces typed views per evidence family).  The boundary
doctrine from #55 is preserved; it now connects to existing plumbing
instead of writing JSON to disk with no consumer.

Tests (tests/test_contemplation_pipeline_convergence.py, 10 cases)
------------------------------------------------------------------
- DiscoveryBufferSink satisfies DiscoveryCandidateSink (shared protocol)
- frontier runner emits findings to shared sink
- contradiction runner emits findings to shared sink
- sink is optional — no-op when absent
- emission is canonical JSONL (sorted keys, no newline, deterministic)
- DiscoveryMonthlyFileSink persists findings at <root>/<YYYY>/<YYYY-MM>.jsonl
- sink emission does not alter the ContemplationRun blob (additive)
- contradiction miner predicate split + repair-action asymmetry
- config_hash differs between lanes (replay can distinguish)
- BOUNDARY doc is present in schema.py (regression guard)
- ContemplationEvidenceRef field invariants
- format_contemplation_finding_jsonl is deterministic + canonical

All 18 tests pass (5 original ADR-0080 + 13 new convergence).

Live evidence
-------------
$ uv run python -m core.contemplation \
    evals/contradiction_detection/results/v1_public_*.json \
    --lane contradiction_detection \
    --sink-root /tmp/sink_demo

  /tmp/sink_demo/2026/2026-05.jsonl  ← same layout as discovery candidates

  predicate=missed_contradiction         subject=contradiction_detection/CON-PUB-002
  predicate=missed_contradiction         subject=contradiction_detection/CON-PUB-004
  predicate=false_contradiction_flag     subject=contradiction_detection/CON-PUB-005
  predicate=false_contradiction_flag     subject=contradiction_detection/CON-PUB-006
2026-05-20 12:32:53 -07:00
Shay
06bbac86e1
feat(contemplation): ADR-0080 read-only speculative loop (#55)
* docs(adr): ADR-0080 contemplation loop boundary

* feat(contemplation): add read-only contemplation package

* feat(contemplation): add immutable speculative finding schema

* feat(contemplation): add deterministic substrate snapshot

* feat(contemplation): add frontier report miner package

* feat(contemplation): mine frontier compare failures as speculative findings

* feat(contemplation): add read-only contemplation runner

* feat(contemplation): add read-only contemplation CLI

* test(contemplation): prove read-only speculative loop invariants
2026-05-20 11:40:12 -07:00
Shay
6761fc0974 feat(realizer): C1.5 — articulation legality at the realizer boundary
Adds a typed legality check that catches a narrow class of incoherent
finite-predicate surfaces before they ship.  Scope is deliberately
narrow:

  - generate/articulation_legality.py:
    - SlotKind enum {VERB, NON_VERB, UNKNOWN}
    - ArticulationLegality enum {LEGAL, ILLEGAL_NON_VERB_FINITE_PREDICATE}
    - classify_predicate_slot_kind() — token allowlists for known verbs
      and known non-verb nouns
    - validate_finite_predicate_legality() — fails on negated +
      NON_VERB; fail-open on UNKNOWN to preserve canary behavior

  - generate/templates.py:
    - _inflect_predicate: copular-aware negation
      ("is X" -> "is not X" instead of the default "does not be X")
    - render_step: invokes the legality validator; returns
      "I cannot realize that proposition coherently yet." when an
      illegal shape is detected

The check is upstream of register / anchor-lens transforms (presentation
+ substantive axes both downstream of the realizer); no interaction
with R6 / ADR-0073 layering.

Tests pin:
  - NON_VERB + negated -> ILLEGAL_NON_VERB_FINITE_PREDICATE
  - UNKNOWN + negated -> LEGAL (fail-open preserved)
  - render_step returns the disclosure string when illegal detected
  - render_step still produces the fall-through surface on UNKNOWN

Validation:
  - Cognition eval byte-identical (100/100/91.7/100)
  - 370 realizer / lens / register / pack / lane tests pass
  - anchor-lens-tour + register-tour both green
2026-05-20 11:11:28 -07:00
Shay
6387872051 feat(packs): en_collapse_anchors_v1 — activate chesed/shalom/tzedek lenses on EN input
ADR-0073c shipped he_chesed_v1, he_shalom_v1, he_tzedek_v1 with lossy
EN-collapse alignment edges (he-021 → en-collapse-love @ 0.63, etc.)
but the synthetic en-collapse-* targets didn't exist in any mounted
lexicon.  Result: the three lenses ratified but stayed dormant — the
runtime OOV gate fired on "What is love?" / "What is peace?" /
"What is justice?" before the lens engagement path got a chance.

This commit adds a minimal pack whose lexicon carries exactly those
three synthetic anchors:

  en-collapse-love     lemma="love"     domain=collapse_anchor.love
  en-collapse-peace    lemma="peace"    domain=collapse_anchor.peace
  en-collapse-justice  lemma="justice"  domain=collapse_anchor.justice

Mounted last in DEFAULT_RESOLVABLE_PACK_IDS — cognition / relations
packs win first-match on any future collision.  No real content pack
currently carries these lemmas (grep-confirmed) so the mount adds no
collision risk.

The pack-grounded surface for "What is love?" advertises its nature
honestly via the pack id (en_collapse_anchors_v1) and the domain
string (collapse_anchor.love) — the surface is intentionally minimal;
the substantive content arrives via the lens annotation
[lens(he_chesed_v1):covenant-love] / [lens(he_shalom_v1):wholeness-peace] /
[lens(he_tzedek_v1):right-order].

chat/pack_grounding.py:_en_lemma_to_entry_id() now reads both
en_core_cognition_v1 and en_collapse_anchors_v1, with cognition
winning on lemma collision.

New test file tests/test_en_collapse_anchors_v1_pack.py pins:
  - each anchor lemma resolves to its synthetic entry_id
  - collapse pack mounted last (precedence guarantee)
  - each of the three lenses engages on its target English prompt
  - baseline surface (no lens) still advertises anchor nature

Validation:
  - Cognition eval byte-identical (100/100/91.7/100)
  - 160 lens/pack/resolver tests pass + 8 new
  - anchor-lens-tour green
  - register-tour green
2026-05-20 10:58:07 -07:00
Shay
3065ad9e19
feat(packs): expansion round 2 — ethics ×3, anchor-lens ×3, relations-v3, register ×2 (#48)
* feat(packs): ethics ×3, anchor-lens ×3, relations-v3, register ×2

Group 1 — Ethics domain packs (ADR-0044 sibling)
  legal_ethics_v1: 6 commitments covering no-legal-advice, no-outcome-prediction,
    jurisdiction-disclosure, privilege-disclosure, conflict-disclosure, refer-to-counsel
  engineering_ethics_v1: 6 commitments covering safety-primacy, standard-disclosure,
    no-sign-off, uncertainty-surface, public-welfare-priority, refer-to-pe
  research_ethics_v1: 6 commitments covering no-fabrication, no-plagiarism,
    irb-disclosure, conflict-of-interest-disclosure, data-integrity, reproducibility-hedge
  ratify_ethics_pack.py: PACK_IDS extended with all three new ids

Group 2 — Anchor lens packs (grc cognition atoms, ADR-0073c)
  grc_sophia_v1: atom logos.sophia.wisdom via grc-core-cog-008 (cross_lang.logos.sophia
    edge weight 0.88); cognitive mode wisdom-practical
  grc_epignosis_v1: atom logos.epignosis.experiential via grc-core-cog-007 (weight 0.78,
    en_collapse edge documented); cognitive mode experiential-knowledge
  grc_episteme_v1: atom logos.episteme.systematic via grc-core-cog-021 (weight 0.72,
    en_collapse edge documented); cognitive mode systematic-knowledge
  ratify_anchor_lens_packs.py: LENS_IDS extended with all three new ids

Group 3 — en_core_relations_v3 (social + part-whole extension of v2 kinship)
  7 new lemmas: colleague, mentor, neighbor, component, member, instance, peer
  manifest.json: new pack with checksum placeholder (operator must recompute after
    ratify run — same pattern as other packs)

Group 4 — Register packs formal_v1 + socratic_v1
  formal_v1: standard depth, drop_provenance_tag=true + drop_articles=true;
    no markers; ratifies under known_key_overrides_invariant_grounding
  socratic_v1: pedagogical depth, append_semantic_domain_clause=true; markers scaffold
    question-and-response rhythm (openings×4, transitions×3, closings×4)
  ratify_register_packs.py: REGISTER_IDS extended with formal_v1, socratic_v1

* fix(anchor_lens): loader v1/v2 dual-schema compat — resolves blocker 1 of #48

Refactor AnchorLens to use v2 schema fields and normalize legacy fields. Update validation and loading functions for improved clarity and functionality.

* fix(ratify): restore default_unanchored_v1 + full LENS_IDS (17) — resolves blocker 2 of #48

Added new lens IDs for the he substrate and updated the order of lens IDs.

* chore(packs): migrate 8 legacy anchor-lens packs to v2 schema [1/8 default_unanchored_v1]

Updated the default unanchored lens JSON structure with new fields and modified descriptions.

* chore(packs): migrate grc_logos_v1 to v2 schema [2/8]

Updated the description and added new fields for cognitive mode, atom, and source entry ID.

* chore(packs): migrate grc_aletheia_v1 to v2 schema [3/8]

Updated the description and added new fields related to cognitive mode and atom.

* chore(packs): migrate grc_zoe_v1 to v2 schema [4/8]

Updated the description and added new fields for cognitive mode, atom, and source entry ID.

* chore(packs): migrate grc_arche_v1 to v2 schema [5/8]

Updated the description and added new fields for cognitive mode, atom, and source entry ID.

* chore(packs): migrate he_logos_v1 to v2 schema [6/8]

Updated the Hebrew-substrate anchor lens JSON structure with new fields and modified descriptions.

* chore(packs): migrate he_dabar_v1 to v2 schema [7/8]

Updated the description and added new fields for cognitive mode and source entry.

* chore(packs): migrate he_chayyim_v1 to v2 schema [8/8] — resolves blocker 3 of #48

Updated the description and added new fields for cognitive mode and source entry ID.

* fix(anchor-lens): complete v1→v2 migration + back-compat shims

Resolves blockers B4/B5/B6/B7 left by the initial round-2 schema rewrite:

  B4: restore UNANCHORED module constant, is_null_lens() alias,
      and verify_anchor_lens_seal() (all were dropped from loader.py;
      chat/pack_grounding.py and several tests still imported them).
      AnchorLens.unanchored() returns the in-memory sentinel with
      lens_id='__unanchored__' as before (distinct from disk pack).

  B5: add v1 attribute properties on AnchorLens (primary_substrate,
      semantic_domain_preferences, cognitive_mode_label) so consumers
      not yet on v2 (chat/pack_grounding.py engagement reads, several
      tests) continue to work via read-only views over the canonical
      v2 fields. Zero changes needed to chat/pack_grounding.py.

  B6: re-derive source_entry_id by atom-in-lexicon lookup for 6 of 8
      legacy packs that were positionally mis-mapped during migration.

  B7: fix two new-pack atoms that didn't exist in the lexicon
      (logos.episteme.systematic -> logos.episteme.systematic_knowledge,
      logos.epignosis.experiential -> logos.epignosis.knowledge).

Loader hardening (recovered from v1 rewrite):
  - _validate_lens_id_for_fs: reject path-traversal / slash / empty
  - companion-SHA mismatch check in load_anchor_lens when require_ratified
  - atom must be non-empty when substrate != 'none'
  - available_anchor_lens_packs returns summary dicts (was list[str])

Ratify script special-cases substrate='none' so the null sentinel
default_unanchored_v1 keeps its self-seal (ADR-0073b invariant).

Test suite migrated to v2 schema: dropped obsolete list-shape gates
(duplicates, too-many-preferences — v2 has scalar atom), updated error
match strings, added a v1->v2 normalisation back-compat test.

All 11 round-2 packs ratified.  102/102 anchor-lens tests pass.
Cognition eval byte-identical (100/100/91.7/100).
anchor-lens-tour + register-tour both green.
2026-05-20 07:18:35 -07:00
Shay
e64ec578eb
feat(evals): frontier comparison benchmark wave 1 (#52)
* feat(evals): add frontier comparison benchmark wave one scaffold

* feat(evals): add frontier comparison runner package

* feat(evals): implement frontier comparison wave one suites

* feat(evals): add frontier comparison CLI entrypoint

* feat(evals): add static frontier benchmark report viewer

* test(evals): cover frontier comparison wave one benchmarks

* fix(evals): record runtime observation failures instead of aborting suites

* docs(evals): document frontier comparison recording UI
2026-05-20 06:27:32 -07:00
Shay
8e96728009 feat(telemetry): ADR-0078 Phase 1 — composer/graph atom equivalence (observational)
Wires observational telemetry on the composer-vs-graph atom-set
relationship.  Phase 1 is strictly observational: no enforcement,
no surface mutation, no grounding-source change, no trace-hash impact.

New telemetry fields on TurnEvent + ChatResponse:
  composer_graph_atom_status         ∈ {equivalent, divergent,
                                         graph_unconstrained,
                                         composer_no_atoms,
                                         not_applicable, ""}
  composer_atom_set_hash             SHA-256 over sorted unique atoms
  graph_atom_set_hash                SHA-256 over sorted unique atoms
  composer_graph_atom_overlap_count  int

Composer atoms come from existing pack candidate metadata
(pack_semantic_domains channel through _maybe_pack_grounded_surface).
Graph atoms come from build_graph_from_input + resolve_lemma on
node.subject/predicate/obj — no prose parsing.  When a grounded
composer path lacks explicit atom provenance, status is
'composer_no_atoms'.

New pure helper:
  chat/atom_equivalence.py — normalize_atoms, hash_atoms,
  atoms_for_graph_nodes, compare_atom_sets

Tests (tests/test_composer_graph_atom_equivalence.py):
  - Pack DEFINITION path produces observable equivalence
  - Divergent atom sets produce distinct hashes
  - Register invariance: atom hashes + status identical across
    {neutral, terse, convivial}; trace_hash also constant (R5 axis)
  - Anchor lens engaged case still ASCII-only on surface
  - No prose-parsing helper symbols introduced in runtime.py
    (extract_candidate_surface_lemmas, surface_lemma,
    parse_surface_atoms) — enforces Phase 1 boundary

Performance note: build_graph_from_input now runs on every warm
English turn (previously only when forward_graph_constraint=True).
Phase 1 accepts this cost to make the telemetry universally
available; Phase 2+ can introduce a feature flag if needed.

Validation:
  - Cognition eval byte-identical: 100/100/91.7/100
  - Full lane: 2864 passed, 3 skipped, 0 failed (+5 over baseline)
  - Targeted lane: 72 passed in tests/test_{graph_constraint,
    pack_grounding,register_tour_demo,anchor_lens_tour_demo,
    orthogonality_tour_demo,realizer_guard_holdout,
    composer_graph_atom_equivalence}.py
2026-05-20 06:14:25 -07:00
Shay
5a78b0e37b feat(register): ADR-0077 — substantive register knobs + layering boundary (R6)
R5 (ADR-0072) shipped the register *machinery*; ADR-0074's orthogonality
tour proved the axis was decoratively orthogonal to anchor-lens but
inspection of the cognition-eval surfaces revealed two structural gaps:

* On pack-grounded DEFINITION/RECALL/COMPARISON composers, the only
  realizer override any register consumed was `disclosure_domain_count`
  — which only fires on the no-gloss disclosure path.  Under terse_v1,
  every gloss-DEFINITION cell was byte-identical to default_neutral_v1.
* The register-tour's `surfaces_vary_at_least_once` gate could be
  satisfied by convivial's decorative wrapper alone, masking that
  regression in CI.

R6 closes both:

Layering separation (the load-bearing fix):
* New TurnEvent/ChatResponse field `register_canonical_surface` carries
  the composer output BEFORE any register transformation.  The pipeline
  hashes this field for `trace_hash`, preserving R5's invariant that
  per-prompt trace_hash is CONSTANT across registers even while
  substantive transforms produce visibly different surfaces.

Substantive transforms (`chat/register_substantive.py`):
* terse_v1 gains 3 bool knobs: `drop_provenance_tag`, `compress_gloss`,
  `drop_articles` — all pure regex transforms on the canonical surface.
* convivial_v1 gains `append_semantic_domain_clause` — appends a single
  bounded "Related: <atom>." clause using the lemma's pack atoms.
* default_neutral_v1 leaves overrides empty; substantive transform is
  byte-identical no-op (preserves `byte_identity_null_lift`).
* C1 (ADR-0075) safety preserved: drop_articles refuses to drop
  articles following `not` (avoids R3 violations); no knob combination
  trips R2/R3.

Strengthened tour gate (`evals/register_tour/run_tour.py`):
* Replaces `surfaces_vary_at_least_once` with two falsifiable claims:
  - `terse_substantively_differs_from_neutral_on_pack_grounded_definition`
  - `convivial_substantively_differs_from_neutral_on_pack_grounded_definition`
  Both restrict to DEFINITION+pack-grounded cells and require
  difference beyond whitespace/punctuation.
* New claim `register_canonical_surfaces_identical` directly proves
  the layering separation.
* Preserves R5's `all_grounding_sources_identical` +
  `all_trace_hashes_identical`.

Pack ratification:
* Loader widened to accept `bool` for closed-set R6 keys
  (drop_provenance_tag / compress_gloss / drop_articles /
  append_semantic_domain_clause).
* `_KNOWN_OVERRIDE_KEYS` ratify gate extended with same.
* terse_v1 + convivial_v1 reratified with new knobs; companion
  mastery reports re-sealed.  default_neutral_v1 unchanged.

Invariants pinned:
* `invariant_register_canonical_surface_constant_across_registers` (new)
* `invariant_terse_substantively_distinct_from_neutral` (new)
* `invariant_convivial_substantively_distinct_from_neutral` (new)
* `invariant_realizer_no_illegal_articulation` (C1, preserved)
* `invariant_realizer_guard_byte_identity_on_currently_passing_cases`
  (C1, preserved)

Verification:
* `core eval cognition`: 100.0% / 91.7% / 100.0% / 100.0% — byte-
  identical under default_neutral_v1.
* `core demo register-tour`: all 5 claims green, exit 0.
* `core demo anchor-lens-tour`: green (no anchor-lens code touched).
* `core demo orthogonality-tour`: green (5/5 claims).
* Full lane: 2858 passed, 1 pre-existing failure
  (test_all_preamble_explains_combined_run, carried forward
  unchanged from main).  56 new R6 tests across three files.
2026-05-19 23:39:11 -07:00
Shay
d7499c80b3
feat(intent): normalize confirmation-tag propositions (#45) 2026-05-19 22:55:28 -07:00
Shay
7cc2888ed2 feat(coherence): ADR-0075 — realizer slot-type guard (C1)
C1 coherence floor: a deterministic verifier that runs on every
candidate surface produced by the truth path, before assignment to
ChatResponse.surface.  Rejects illegal articulations and routes them
to a bounded disclosure string — admission control with a
deterministic fallback, not normalization.

Active rules (R1 deferred during ratification — see ADR):
  R2_aux_neg_requires_verb     — "<aux> not <wrong-POS>"  rejected
  R3_be_neg_requires_predicate — "<be>  not <verb>"       rejected

Fail-open on unknown POS, fail-closed on explicit wrong POS.
Cognition eval byte-identical (100/91.7/100/100).

Original bug class — "Light reveals truth, right?" → "Right does not
thought." — now routes to "I do not have a reviewed articulation for
that yet." with grounding_source=none, walk_surface preserving the
rejected candidate, and telemetry carrying R2_aux_neg_requires_verb.

Files:
  generate/realizer_guard.py            NEW — pure verifier
  chat/runtime.py                       hook on stub + main paths
  chat/telemetry.py                     serialize guard fields
  core/physics/identity.py              TurnEvent +2 fields
  evals/realizer_guard/run_holdout.py   NEW — 6-prompt cluster
  tests/test_realizer_guard_*.py        NEW — 46 tests (unit/seam/holdout)
  docs/decisions/ADR-0075-*.md          NEW — ratified

Invariants pinned:
  invariant_realizer_no_illegal_articulation
  invariant_realizer_guard_byte_identity_on_currently_passing_cases

Lanes (excluding 1 pre-existing TestDemoPreambles failure unrelated
to C1, already present at 4426f38):
  smoke 67/67  cognition 120/120(+1s)  teaching 17/17
  packs 6/6   runtime 19/19   algebra 132/132   full 2792/2793
2026-05-19 22:35:09 -07:00
Shay
4426f387d1 feat(demo): ADR-0074 — orthogonality tour (anchor-lens × register)
A single demo that walks the full 3 × 3 × 2 matrix (register × lens
× prompts, 18 cells) and pins five claims simultaneously, packaging
both single-axis invariants into one composition gate.

The single-axis tours assert opposite invariants:

  register-tour    : per (lens, prompt), trace_hash CONSTANT across
                     registers (R5 / ADR-0072).
  anchor-lens-tour : per (register, prompt), engaged lens diverges
                     in trace_hash from the unanchored baseline
                     (L1.4 / ADR-0073d).

Orthogonality-tour packages both claims simultaneously across the
full matrix, plus three surface-level claims that pin the markers
operators actually see.

Composed claims (all five must hold)

  A) inner_register_invariant_within_lens
     For each (lens, prompt) cell, the three register runs share an
     identical trace_hash.  (R5 register-tour, applied 6 times:
     3 lenses × 2 prompts.)

  B) outer_lens_distinctness_within_register
     For each (register, prompt) cell where any non-unanchored lens
     engages, that engaged lens's trace_hash differs from the
     unanchored baseline at the same (register, prompt).
     (L1.4 anchor-lens-tour, applied 6 times: 3 registers × 2 prompts.)

  C) surface_carries_register_marker_under_convivial
     Every convivial cell with a non-empty surface has a non-empty
     register_variant_id.

  D) surface_carries_lens_annotation_when_engaged
     Every engaged cell carries [lens(<id>):<mode>] in surface AND
     a non-empty anchor_lens_mode_label.

  E) no_substrate_glyph_leak_across_grid
     No cell's surface contains Greek/Hebrew/Syriac/Arabic glyphs.
     (ADR-0073c gate re-asserted across the full matrix.)

CLI wiring

  core demo orthogonality-tour            human-readable grid + claims
  core demo orthogonality-tour --json     structured report

Exit code 0 iff all five claims hold.

Files

  evals/orthogonality_tour/__init__.py             NEW
  evals/orthogonality_tour/run_tour.py             NEW
  core/cli.py                                       EDIT
    - cmd_demo handler wires orthogonality-tour
    - demo choices + EPILOG examples updated
  tests/test_orthogonality_tour_demo.py             NEW (9 tests)
  docs/decisions/ADR-0074-orthogonality-tour.md     NEW

Sanity check baked into tests
  test_engaged_cells_appear_for_both_non_trivial_lenses pins that
  grc_logos_v1 engages on knowledge in all 3 registers (3 cells)
  and he_logos_v1 engages on truth in all 3 registers (3 cells).
  Prevents the lift claims being vacuously satisfied by a future
  engagement regression.

Lane evidence

  - 9 new orthogonality-tour tests pass.
  - core demo register-tour      → all_claims_supported: True
  - core demo anchor-lens-tour   → all_claims_supported: True
  - core demo orthogonality-tour → all_claims_supported: True
  - python -m core.cli eval cognition → byte-identical 100/100/91.7/100.
  - Full lane: 2745 passed / 4 skipped / 1 pre-existing failure
    (+9 over L1.4's 2736; the one failure remains
    test_all_preamble_explains_combined_run, unrelated).

No runtime / composer / loader / pack / schema changes.  Pure demo
consumer of existing telemetry contracts.
2026-05-19 20:33:33 -07:00
Shay
1feec74b1c feat(anchor_lens): ADR-0073d — L1.4 telemetry, CLI flag, tour demo
L1.4 closes the anchor-lens inside-out arc (L1.1→L1.4 mirroring
R1→R5).  Substantive axis is now operator-observable,
operator-driven, and demo-falsifiable — exactly what R5 did for
the register subsystem.

Telemetry extension
  - TurnEvent + ChatResponse gain anchor_lens_id +
    anchor_lens_mode_label (both default "" → pre-L1.4
    byte-identical).
  - serialize_turn_event surfaces both fields in every JSONL line.
  - Mode-label extracted via _ANCHOR_LENS_ANNOTATION_RE from the
    PRE-decoration surface (so register decoration cannot interfere
    with anchor-lens telemetry).  Composer remains the sole source
    of truth for engagement; the runtime helper is read-only.

Operator surface
  - core chat --anchor-lens <id> CLI flag threads into
    RuntimeConfig.anchor_lens_id.
  - Invalid id → AnchorLensError caught at cmd_chat and surfaced
    as _die("invalid --anchor-lens pack id: ...", code=2) before
    the REPL launches.
  - Composes with --register (both flags wire through
    _runtime_config_from_args).

Narrative demo
  - evals/anchor_lens_tour/run_tour.py walks 2 prompts × 3
    ratified lenses ({default_unanchored_v1, grc_logos_v1,
    he_logos_v1}).  Asserts four claims:
      * lens_ids_recorded_per_turn
      * trace_hashes_distinct_across_lenses (OPPOSITE of
        register-tour's identical-hash claim)
      * surface_propositions_distinct_across_lenses
      * no_substrate_glyph_leak (block-scoped Greek/Hebrew/
        Syriac/Arabic; stylistic punct allowed)
  - Exit code 0 iff all four hold.
  - Bundled into `core demo` choices + EPILOG.

Tests (30 new)
  - tests/test_anchor_lens_telemetry.py (16) — TurnEvent shape,
    serializer keys, runtime emits per lens / per engagement
    state, ChatResponse mirrors event, mode-label extractor unit.
  - tests/test_anchor_lens_cli.py (9) — _runtime_config_from_args
    threading, invalid id fail-fast, parser flag wiring, parser
    composes with --register.
  - tests/test_anchor_lens_tour_demo.py (9) — four seam claims
    pinned individually + all_claims_supported + per-cell
    anchor_lens_id + unanchored cells empty mode + engaged cells
    carry mode label.

Lane evidence
  - 30 new L1.4 tests pass.
  - core demo anchor-lens-tour --json → all_claims_supported: True.
  - core demo register-tour --json    → all_claims_supported: True.
    Both tours pass simultaneously — orthogonality CI-pinned.
  - python -m core.cli eval cognition → public 100/100/91.7/100
    byte-identical (lens=None / default_unanchored_v1).
  - Full lane: 2736 passed / 4 skipped / 1 pre-existing failure
    (+30 over L1.3's 2706; the one failure remains
    test_all_preamble_explains_combined_run, unrelated).

Live demo (canonical proof)
  P1: 'What is knowledge?'
    default_unanchored_v1  trace=17c9aabe…  mode=(none)
    grc_logos_v1           trace=0198ad4c…  mode=systematic
    he_logos_v1            trace=17c9aabe…  mode=(none)
  P2: 'What is truth?'
    default_unanchored_v1  trace=2557f3e8…  mode=(none)
    grc_logos_v1           trace=2557f3e8…  mode=(none)
    he_logos_v1            trace=ec8d84aa…  mode=covenant-verity

  Engagement is substrate-scoped: grc never touches truth, he
  never touches knowledge.  Trace hashes diverge exactly where the
  lens engages.

Trust boundaries
  - --anchor-lens flag does not bypass ratification; loader still
    enforces companion mastery report self-seal + ratify-time
    substrate-atom existence check (ADR-0073b/c).
  - Mode-label extraction is read-only regex parse; can't forge
    annotations the composer didn't emit.
  - Telemetry stays redact-safe — both fields are identifiers /
    mode labels, not content.  include_content=False emits them
    unconditionally.
  - No new mutation surface; pack files unchanged.

Closes the anchor-lens inside-out arc
  L1.1  content prerequisite                  ✓ (ADR-0073a)
  L1.2  class + loader + unanchored sentinel  ✓ (ADR-0073b)
  L1.3  first lenses + composer wiring        ✓ (ADR-0073c)
  L1.4  telemetry + CLI + tour demo           ✓ (this commit)

  Mirrors the R1→R5 register cadence exactly.  Both axes are now
  operator-observable, CI-falsifiable, audit-traceable, and
  composable via the orthogonality claim pinned in both tours.
2026-05-19 20:21:41 -07:00
Shay
b35bec6465 feat(anchor_lens): ADR-0073c — L1.3 first lenses + composer wiring
L1.3 of the anchor-lens inside-out rollout — first substantive
surface lift on the substantive axis.  Two ratified non-trivial
lenses engage on cognition-pack lemmas via the alignment graph,
appending [lens(<id>):<mode>] annotations to the existing
pack-grounded surface.

Two ratified lenses

  grc_logos_v1 (Greek substrate)
    primary_substrate         : "grc"
    semantic_domain_preferences: ["logos.episteme.systematic_knowledge"]
    cognitive_mode_label       : "systematic"
    Engages on en "knowledge" via grc-core-cog-021 (ἐπιστήμη) →
    en-core-cog-007 alignment edge.

  he_logos_v1 (Hebrew substrate)
    primary_substrate         : "he"
    semantic_domain_preferences: ["logos.aletheia.verity"]
    cognitive_mode_label       : "covenant-verity"
    Engages on en "truth" via he-core-cog-002 (אמת) →
    en-core-cog-002 alignment edge.

  Both ratified under method anchor_lens_lifts_proposition.

Engagement rule (single)

  1. Resolve en_lemma → entry_id (cognition pack).
  2. For each substrate pack matching lens.primary_substrate, load
     alignment.jsonl; find edges where target_id == entry_id.
  3. For each such substrate lemma, if any atom in its
     semantic_domains ∈ lens.semantic_domain_preferences → engage.
  4. No match → None (no annotation; byte-identical surface).

The pivot is shared semantic_domain atoms surfaced via the
alignment graph — exactly the language-neutral commitment from
ADR-0073.  Engagement never touches non-English surface text;
entry_ids and atom strings only.

Surface lift

  no-lens : "Knowledge is X. pack-grounded (en_core_cognition_v1)."
  lens-on : "Knowledge is X. pack-grounded (en_core_cognition_v1) [lens(grc_logos_v1):systematic]."

  Annotation between existing provenance and trailing period.
  Both metadata fields are ASCII-bounded ≤64 chars at the loader
  level, so the annotation can never carry non-ASCII.

Scope deliberately narrow

  L1.3 wiring restricted to pack_grounded_surface /
  build_pack_surface_candidate (DEFINITION/RECALL only).  Other
  composers (COMPARISON / CORRECTION / PROCEDURE / NARRATIVE /
  EXAMPLE / CAUSE / VERIFICATION) accept the anchor_lens kwarg via
  forward-compat default UNANCHORED but do not yet consume it.
  L1.3b or later broadens to those intent shapes.

Ratify gate widening

  Non-null lenses must:
    - have primary_substrate ∈ {grc, he, en}
    - have a non-empty cognitive_mode_label
    - every preferred atom must exist in at least one lemma of the
      named substrate (trust boundary: operators cannot ship a lens
      pointing at atoms not on disk).
  Method: anchor_lens_lifts_proposition.  Null lenses still ratify
  under byte_identity_null_lift (L1.2 method).

Seam allow-list widening

  Truth-path modules (cognition / trace / pipeline / intent /
  propagation / vault / algebra) still refused.  Composer-side
  imports from chat/pack_grounding.py now permitted — the same way
  ADR-0069's R2 widened the register seam.

New invariants pinned (3)

  tests/test_anchor_lens_engagement_unit.py (14 tests) — resolver
  returns mode label only on intended substrate × en lemma pair;
  case-insensitive; engagement None under null lens; synthetic
  lens with unmatched atom returns None; annotation is pure ASCII.

  tests/test_anchor_lens_lifts_proposition.py (17 tests) — grc
  engages on knowledge only, he engages on truth only,
  cross-lens isolation, three-way distinctness, replay determinism
  per (lens × prompt), register-tour seam holds within each lens
  scope (orthogonality CI-pinned, parametrized over 4 lens
  choices).

  tests/test_anchor_lens_no_glyph_leak.py (5 tests) — hard
  block-scoped gate: Greek (U+0370..03FF, U+1F00..1FFF), Hebrew
  (U+0590..05FF), Syriac, Arabic.  Stylistic punctuation
  (em-dash etc.) explicitly allowed; em-dash predates L1.3 by a
  wide margin and is not a substrate-leak risk.  Tested per-lens
  across every cognition case + direct lens-metadata ASCII check.

Lane evidence

  74 anchor-lens tests pass (37 from L1.2 + 37 new).
  python -m core.cli eval cognition → public 100/100/91.7/100
  byte-identical (lens=None / default_unanchored_v1).
  core demo register-tour --json → all_claims_supported: True
  (R5 seam still holds; L1.3 doesn't perturb presentation axis).
  Full lane: 2706 passed / 4 skipped / 1 pre-existing failure
  (+37 over L1.2's 2669; the one failure remains
  test_all_preamble_explains_combined_run, unrelated).

Files

  packs/anchor_lens/grc_logos_v1.json                        NEW
  packs/anchor_lens/grc_logos_v1.mastery_report.json         NEW
  packs/anchor_lens/he_logos_v1.json                         NEW
  packs/anchor_lens/he_logos_v1.mastery_report.json          NEW

  scripts/ratify_anchor_lens_packs.py                        EDIT
    LENS_IDS adds grc_logos_v1 / he_logos_v1; gate widened.

  chat/pack_grounding.py                                     EDIT
    _resolve_anchor_lens_mode, _maybe_append_anchor_lens_annotation,
    _substrate_lexicon_by_entry_id, _en_lemma_to_entry_id.
    build_pack_surface_candidate + pack_grounded_surface gain
    anchor_lens kwarg (default UNANCHORED).

  chat/runtime.py                                            EDIT
    Thread self.anchor_lens into pack_grounded_surface() call.

  tests/test_anchor_lens_pack_seam.py                        EDIT
    Doc-comment updated for L1.3 allow-list.

  tests/test_anchor_lens_*                                   NEW (3 files)

  docs/decisions/ADR-0073c-anchor-lens-composer-wiring.md    NEW
2026-05-19 20:06:02 -07:00