core/core/config.py
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

250 lines
13 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class RuntimeConfig:
# ADR-0063 — ``en_core_relations_v1`` (kinship starter pack) joins the
# default mount once the cross-pack surface resolver lands. Pack
# composers in :mod:`chat.pack_grounding` now consult
# :mod:`chat.pack_resolver`, so kinship lemmas ground deterministically
# without a separate composer module.
input_packs: tuple[str, ...] = (
"en_minimal_v1",
"en_core_cognition_v1",
"en_core_meta_v1",
"en_core_attitude_v1",
"en_core_temporal_v1",
"en_core_action_v1",
"en_core_quantitative_v1",
"en_core_spatial_v1",
"en_core_causation_v1",
"en_core_polarity_v1",
"en_core_relations_v1",
"en_core_relations_v2",
"he_logos_micro_v1",
"grc_logos_micro_v1",
)
output_language: str = "en"
frame_pack: str = "en"
max_tokens: int = 32
allow_cross_language_recall: bool = True
allow_cross_language_generation: bool = False
vault_reproject_interval: int = 20
use_salience: bool = True
salience_top_k: int = 16
inhibition_threshold: float = 0.3
inner_loop_admissibility: bool = False
admissibility_threshold: float = 0.0
# ADR-0026 / Phase 3 — margin-based admissibility. ``mode``
# selects between ADR-0024's per-candidate threshold check and
# the ranked-with-margin check. Default "threshold" preserves
# ADR-0024 acceptance evidence; opt-in "margin" replaces the
# static-threshold gate with a scale-invariant margin.
admissibility_mode: str = "threshold"
admissibility_margin: float = 0.4
# ADR-0027 — Identity pack id loaded at runtime startup. Empty string
# resolves to ``DEFAULT_IDENTITY_PACK``. CLI override on chat:
# ``core chat --identity <pack_id>``. See docs/identity_packs.md.
identity_pack: str = ""
# ADR-0033 — Ethics pack id loaded at runtime startup. Empty string
# resolves to ``DEFAULT_ETHICS_PACK``. See docs/ethics_packs.md.
ethics_pack: str = ""
# ADR-0046 / ADR-0047 — forward graph constraint. When True, the
# PropositionGraph built from the classified intent + articulation
# plan is converted into an AdmissibilityRegion BEFORE generate()
# runs (Pillar 1→2→3 coupling closes on the live path). Default
# False preserves existing behavior during the transition window —
# ADR-0024's honest-refusal exhaustion is the correct response when
# the constraint geometry and the walk candidate pool do not
# intersect, but operators must opt in to observing that behavior
# on their workloads. Enable to live the forward constraint;
# disable to retain the pre-ADR-0046 unconstrained walk.
forward_graph_constraint: bool = False
# ADR-0062 — composed teaching-grounded surface. When enabled,
# the teaching-grounded composer extends a single-chain surface
# with a follow-up chain whose subject equals the initial chain's
# object — producing surfaces like "light reveals truth, which
# grounds knowledge" instead of just "light reveals truth".
# Default False preserves all pre-ADR-0062 behaviour. Cycle-safe
# (won't follow if the next subject has been visited), bounded
# depth (max one follow-up chain in v1).
composed_surface: bool = False
# ADR-0083 — transitive (multi-hop) teaching-grounded surface.
# Strict superset of ADR-0062's depth-1 composer: iterates the
# per-hop follow-up resolution under a visited-set guard, so the
# surface can extend beyond a single follow-up chain.
# ``transitive_max_depth`` is the maximum number of follow-up hops
# to append beyond the initial chain. At ``max_depth=0``
# byte-identical to the single-chain surface; at ``max_depth=1``
# byte-identical to ADR-0062's composed surface; at
# ``max_depth=2`` byte-identical to ADR-0062 when no second hop
# exists, strict superset when one does. When True, this
# supersedes ``composed_surface``. Cycle-safe across every depth
# (visited-set covers ADR-0062's 1-step cycle guard). Single-
# corpus traversal in v1; cross-corpus transitive is deferred to
# a follow-up ADR.
transitive_surface: bool = False
transitive_max_depth: int = 2
# ADR-0085 — gloss-aware CAUSE surface. When True, IntentTag.CAUSE
# consults the subject lemma's gloss first and emits an explanation-
# shaped sentence drawn from the gloss text, falling through to
# the chain-walk ``teaching_grounded_surface*`` only when no gloss
# exists for the lemma. Default False preserves the pre-ADR-0085
# chain-walk surface byte-identically (null-drop invariant).
gloss_aware_cause: bool = False
# ADR-0087 — rhetorical-style selection axis (substrate phase).
# ``None`` resolves to ``DEFAULT_RHETORICAL_STYLE_PACK`` per the
# mounting discipline, which is the null-lift baseline. No
# composer or realizer reads this field yet — that wiring is the
# consumer ADR's job. The field is declared here so the runtime
# interface is stable when the consumer lands.
rhetorical_style_id: str | None = None
# ADR-0066 / P3.2 — opt-in thread anaphora. When enabled, the
# runtime prepends a deterministic backreference to a recent
# grounded turn when the current turn's subject lemma matches
# one in the bounded session-thread context. Engages only on
# pack/teaching-tier turns (both prior and current); weaker
# tiers do not anchor. Default False preserves every pre-P3.2
# surface byte-identically.
thread_anaphora: bool = False
# Discourse planner (step 5 of the discourse-planner sequencing).
# When True, the runtime builds a deterministic DiscoursePlan via
# ``generate.discourse_planner.plan_discourse`` from a
# ``GroundingBundle`` assembled by ``generate.grounding_accessors``
# and renders it as multi-clause output. Mode selection comes from
# ``generate.intent.classify_response_mode``; BRIEF mode is
# byte-identical to today's single-sentence pack-grounded surface
# because the runtime hook (``_maybe_apply_discourse_planner``)
# returns ``None`` when the rendered plan has <= 1 move — single-
# fact prompts get exactly the same surface and trace_hash as
# the planner-off path.
#
# Default flipped to True 2026-05-21: cognition eval (45 cases)
# was verified byte-identical across both projections (surface
# AND trace_hash) flag-OFF vs flag-ON, so single-fact prompts are
# not perturbed. The flag's value shows up on NARRATIVE / EXAMPLE
# / PARAGRAPH / EXPLAIN modes and compound prompts that the flat
# classifier currently misclassifies as OOV — those turns become
# multi-clause grounded articulations rather than single-fragment
# disclosures or OOV refusals.
discourse_planner: bool = True
# Phase 3 — plan-level contemplation pre-flight. When True, after
# the discourse planner produces a plan (and before the renderer
# fires) the runtime runs ``core.contemplation.plan_preflight.
# contemplate_plan`` over the plan and stores any SPECULATIVE
# findings on the runtime for downstream consumption. Per
# ADR-0080: contemplation is read-only and SPECULATIVE-only — no
# plan mutation, no autonomous memory promotion. Findings flow
# into the offline contemplation miner (Phase 5) for review-gated
# pack-mutation candidates. Default False keeps the contemplation
# opt-in until the operator wires the downstream sink.
discourse_contemplation: bool = False
# ADR-0068 / ADR-0069 — register pack id loaded at runtime startup.
# ``None`` resolves to ``RegisterPack.unregistered()`` (the in-memory
# null-register sentinel; structurally identical to
# ``default_neutral_v1``). At R2 the register is loaded, stored, and
# threaded through every realizer call site, but no composer consumes
# it — the three byte-identity invariants (None ≡ default_neutral_v1
# ≡ pre-R2 output) are CI-pinned by ``test_register_null_lift.py``.
# R3 widens composers to dispatch on ``register.realizer_overrides``.
register_pack_id: str | None = None
# ADR-0073b — anchor-lens pack id loaded at runtime startup.
# ``None`` resolves to ``AnchorLens.unanchored()`` (the in-memory
# null-lens sentinel; structurally identical to
# ``default_unanchored_v1``). At L1.2 the lens is loaded and
# stored on the runtime, but no composer consumes it — the
# ``anchor_lens_byte_identity_null_lift`` invariant (None ≡
# default_unanchored_v1) is CI-pinned by
# ``test_anchor_lens_null_lift.py``. L1.3 will widen composers
# to dispatch on ``anchor_lens.semantic_domain_preferences``.
anchor_lens_id: str | None = None
# Finding 6 (audit 2026-05-20) — generation stop tokens.
#
# ``None`` resolves to ``generate.stream._STOP_TOKENS`` (the
# historical ``frozenset({"it", "to", "word"})``) so every
# pre-Finding-6 caller preserves byte-identity. Operators that
# mount a pack where one of the historical stop tokens carries
# meaningful content (e.g. a philosophy pack where ``word`` maps
# to λόγος, a syntax pack where ``to`` is a content node) can
# override the set here to free them. Manifest-driven
# per-pack override (``generation_stop_tokens`` field in the pack
# manifest) is the natural next step; that requires a pack-
# schema ADR and re-ratification so the wiring lands first.
stop_tokens: tuple[str, ...] | None = None
# ADR-0088 Phase B (audit Finding 2, 2026-05-20) — realizer becomes
# a real surface authority by grounding the proposition graph from
# the recall step's walk tokens before invoking
# ``realize_semantic``. Default ``False`` preserves byte-identity
# for every existing pack and test — the pipeline's
# ``realize_semantic`` call continues to fire on the ungrounded
# graph, which produces ``<pending>`` / ``...`` surfaces that the
# ``_is_useful_surface`` gate rejects, leaving the runtime path's
# ADR-0085-polished pack-grounded surface as the user-visible
# answer (same as today).
#
# When True the pipeline reorders ``realize_semantic`` to run
# AFTER ``runtime.chat``, calls ``ground_graph(graph,
# response.recalled_words)`` to fill the ``<pending>`` slots, then
# 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.
#
# NOTE — Phase A (realizer fluency parity: gloss-aware templates,
# 3sg verb agreement, pack-provenance tag) 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) is documented in ADR-0088 §Phase A.
# The wiring lands first so the runtime contract is stable when
# Phase A's realizer fluency upgrade ships.
realizer_grounded_authority: bool = False
# ADR-0090 (audit Findings 6 + 7, 2026-05-21) — unified-ingest path.
# Default ``False`` preserves bit-identical behavior with the
# historical probe-then-commit path:
#
# 1. ``chat()`` calls ``probe_ingest(filtered)`` first.
# 2. The gate observes ``probe_state.F``.
# 3. If the gate fires, ``commit_ingest`` runs and a stub
# response returns. If not, ``commit_ingest`` runs and
# drive bias is applied, producing a different field
# from the one the gate saw.
#
# The probe/commit distinction means the gate decides on one
# manifold position and the walk navigates a slightly different
# one — a coherence gap the second-opinion audit named.
#
# When ``True``:
#
# 1. ``chat()`` calls ``commit_ingest(filtered)`` first.
# 2. Drive bias is applied immediately.
# 3. The gate observes ``committed.F`` (the same field the walk
# will navigate on the non-stub path).
# 4. ``probe_ingest`` is skipped entirely on this path.
#
# **Semantic change when True:** stub-path turns commit before
# the stub response is generated. Under ``False`` stub turns do
# not commit at the time of the gate check (today's behavior).
# Operators opt into the cleaner coherence by flipping the flag;
# the unified path is not the default until validated against a
# live workload.
unified_ingest: bool = False
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
DEFAULT_ETHICS_PACK: str = "default_general_ethics_v1"
DEFAULT_REGISTER_PACK: str = "default_neutral_v1"
DEFAULT_ANCHOR_LENS: str = "default_unanchored_v1"
DEFAULT_CONFIG = RuntimeConfig()