Commit graph

13 commits

Author SHA1 Message Date
Shay
c0d6493219 fix(ask): canonicalize question/proposal path collision check
Compare resolved canonical Path values, not raw string spellings, when
detecting a question_path / proposal_path collision. A relative
proposal_path and an absolute question_path that name the same file now
fail closed with reason "path_collision" instead of resolving.

Adds a regression test for the absolute-vs-relative same-file collision
and keeps the existing exact-string collision test. The new test is
non-vacuous: it fails under the prior raw-string comparison.

Scope: ask_handle.py seam + its test only. No runtime.py, CLAIMS,
metrics, telemetry, schema, or lane-pin changes.
2026-06-10 12:44:46 -07:00
Shay
41664ce60d feat(ask): add carried-handle acquisition seam for served ASK
The missing-boundary doc (#682) established that chat/runtime.py has no
lawful in-turn provider for a QUESTION_NEEDED ContemplationResult: the
only producer is the off-serving contemplation pass, and scanning
teaching/questions/ would be new, unspecified acquisition logic. This
slice builds the recommended unblocking seam instead of the forbidden
runtime shortcut: carried-handle acquisition.

core/epistemic_disclosure/ask_handle.py:
- AskArtifactHandle — an explicit, caller-carried reference to one
  already-produced Q1-D DeliveredQuestion artifact. Provenance is the
  producer's own content address (sha256 of blocking_reason:slot_name:
  text, also the artifact filename stem) — the only identity field the
  Q1-D artifact carries today. No turn/case provenance exists yet; that
  boundary is documented, not invented.
- resolve_served_ask_handle — gate-first (zero filesystem access while
  ask_serving_enabled is dark), structural handle validation, single
  explicit-path artifact read (no glob/iterdir/scan), and content-hash
  re-derivation so a stale/replaced/tampered artifact fails closed.
  Emits a ResolvedAskCandidate duck-typed to what evaluate_served_ask
  reads (terminal/question_path/proposal_path) — no question prose is
  ever constructed or carried.
- acquire_served_ask_from_handle — composes resolution into the
  existing acquire_served_ask_candidate / evaluate_served_ask stack.
  All Q1-D artifact policy (status/review/served/answer-binding/text/
  slot/collision) stays delegated; the seam owns identity only.

Not changed: chat/runtime.py, chat(...) signature, ChatResponse,
TurnEvent, telemetry schema, Q1B_ASK_CARVE_OUT, proposal_allowed,
VERIFIED serving, CLAIMS/metrics/lane pins. The seam imports neither
generate.contemplation (pass_manager) nor core.epistemic_questions
render/delivery — enforced by AST tests.

Tests (tests/test_ask_handle.py, 18 cases): gate-dark no-I/O proof,
fallback preservation, valid resolution served end-to-end through the
existing stack, proposal-artifact / missing / malformed / invalid-Q1-D
/ path-collision / stale-hash / non-addressed-filename / structurally-
invalid-handle fail-closed paths, no-discovery-without-exact-handle,
and AST guards for scan primitives, forbidden imports, and prose.

Verification: focused ASK+governance suites 396 passed; architectural
invariants 56 passed; smoke-equivalent suite 90 passed; lane SHAs 8/9
content-match (the 9th is the documented local public_demo wall-clock
flake, 48.3s vs the 30s ADR-0099 budget — no content drift, no re-pin).
2026-06-10 10:53:54 -07:00
Shay
4403a21044 feat(ask): export served ASK acquisition seam 2026-06-09 12:59:15 -07:00
Shay
16979f48d0 feat(ask): add served ASK acquisition seam 2026-06-09 12:58:31 -07:00
Shay
4308519ca9
feat(ask): add served ASK artifact adapter
Adds the Stage 2 served ASK artifact adapter and adapter-only tests.

- Validates Q1-D DeliveredQuestion artifacts before ASK eligibility
- Uses ask_serving_enabled as a default-dark gate
- Uses the disclosure bus disposition mapping
- Fails closed to fallback surface/disposition on invalid artifacts
- Does not wire chat/runtime, telemetry, TurnEvent, ChatResponse, CLAIMS, or metrics
- Does not retire Q1B_ASK_CARVE_OUT or flip proposal_allowed
2026-06-09 12:49:52 -07:00
Shay
45835987b9
feat(verified): add default-dark VERIFIED serving gate helper
Adds a default-dark verified_serving_enabled helper with focused tests. The helper is unwired: no runtime integration, no verify.py wiring, no eval producer imports, no served VERIFIED behavior, and no CLAIMS/metric movement.
2026-06-09 10:06:36 -07:00
Shay
f88e03e53a feat(epistemic): Q1-D — off-serving ASK delivery (QUESTION_NEEDED tenant)
The fourth and final Q1 rung: route the Q1-C rendered question onto the
contemplation bus as the QUESTION_NEEDED tenant (sibling of PROPOSAL_EMITTED),
off-serving. Consumes render_question VERBATIM — Q1-D constructs no prose, so the
Q1-C grounded-rendering wrong=0 guard is never bypassed by a second surface.

- generate/contemplation/findings.py: add Terminal.QUESTION_NEEDED.
- core/epistemic_disclosure/limitation.py: ask_question's home terminal is now
  QUESTION_NEEDED, making terminal_for_action total (all six actions map onto
  shipped terminals — the consolidation proof is now complete, not 'five of six').
- core/epistemic_questions/delivery.py: DeliveredQuestion (proposal-only artifact,
  cannot wrap an unrenderable question / cannot be served / answer_binding reserved
  None — illegal states unrepresentable), DeliveryOutcome, deliver_ask, the
  teaching/questions sink emitter, and AnswerBinding (reserved for Q2).

Rulings honored:
- D1: off-serving only — no served surface, no ask_serving_enabled, no chat wiring.
- D2: an unrenderable ASK falls back to the family's standing disposition
  (PROPOSAL_EMITTED if it still proposes, else NO_PROGRESS) — never a contentless
  QUESTION_NEEDED. Enforced twice: in deliver_ask and structurally in __post_init__.
- D3: Q1B_ASK_CARVE_OUT untouched — registry keeps proposal_allowed=True; both
  signals coexist off-serving so no operational signal is lost.
- D4: sink under teaching/questions/ (sibling of teaching/proposals/).
- D5: strictly single-slot — multi-slot is unrenderable, takes the D2 fallback.

Tests: 16 delivery tests (happy path verbatim, both D2 fallback branches,
structural guards, idempotent sink, off-serving AST) + updated the two limitation
consolidation tests for the now-total terminal map. smoke 90/0, affected suites
128/0.
2026-06-08 19:10:54 -07:00
Shay
710743c38f
feat(verified): P1-C — split bound_slots_digest into separate obligation
Adds bound_slots_digest as a distinct VerificationProof obligation before any serving wiring. Keeps the VERIFIED lane off-serving while separating derivation, stated-slot binding, and back-substitution.
2026-06-08 17:17:31 -07:00
Shay
f90d84b4f7 feat(epistemic-disclosure): Q1-B — typed ASK residue + missing_* reclassification (carve-out)
Reclassify missing_total_count / missing_weighted_total in the limitation layer
from (capability_gap, emit_proposal) to (missing_information, ask_question) —
the user CAN state the total, so these are missing data, not capability gaps.
Add typed ASK residue (MissingSlot, grounded_terms) on LimitationAssessment;
defaults to empty so existing P0-1 callers continue to work.

Q1B_ASK_CARVE_OUT: the shipped failure_family REGISTRY still flags both as
proposal_allowed=True so existing consumers (proposal pile, contemplation
pass_manager) keep emitting proposal artifacts until Q1-C/Q1-D wires ASK
delivery. Flipping the registry flag before ASK is served would create a
no-proposal/no-question dead zone — a capability-regression window with no
compensating intake. The carve-out is named, enumerated, and explicit; once
ASK delivery is live the REGISTRY flag flips and the carve-out retires.

Wrong=0 invariant on residue (scoping §2): MissingSlot carries family-typed
structural identifiers only (snake_case); grounded_terms is verbatim text from
ComprehensionAttempt.evidence SourceSpanLinks — never fabricated from family
or refusal_reason. Today classify_* leaves evidence empty for refusals, so
grounded_terms is empty everywhere; the wiring is ready for the residue
upgrade to readers later.

Off-serving: limitation.py imports no generate.derivation / core.reliability_gate
(AST-guarded by test_limitation_module_is_off_serving). No renderer, no bus
delivery — those are Q1-C/Q1-D.

Tests:
  tests/test_limitation_assessment.py — 39 passing
  + test_proposal_allowed_iff_emit_proposal_with_q1b_ask_carveout (amended INV-A)
  + test_missing_{total,weighted}_count_limitation_is_ask_oriented
  + test_q1b_ask_carveout_is_explicit_and_enumerated
  + test_registry_proposal_allowed_preserved_until_ask_delivery
  + test_no_signal_loss_before_question_bus_is_serving (live pass_manager check)
  + test_missing_{total,weighted}_count_has_typed_missing_slot_residue
  + test_grounded_terms_populated_only_from_evidence_spans
  + test_residue_never_contains_ungrounded_terms
  + test_residue_empty_for_non_ask_attempts
  + test_missing_slots_default_empty_for_non_ask_families
  + test_missing_slot_keys_are_subset_of_ask_families
  + test_limitation_module_is_off_serving (AST guard)

Suites:
  tests/test_epistemic_*.py, test_disclosure_claim.py, test_served_disposition.py,
  test_verified_contract.py, test_failure_family.py, test_failure_proposal.py,
  test_r3_router_contemplation.py, test_proposal_review_*.py,
  test_idle_proposal_review.py — 135 passing
  core test --suite smoke -q — 90 passing
2026-06-08 16:51:53 -07:00
Shay
ec7c680cfc feat(epistemic-disclosure): P1-A — the VERIFIED contract + meaningful-fail tests
Defines what it MEANS for a result to earn DisclosureClaim.VERIFIED /
EpistemicState.VERIFIED — the soundness≠correctness gate the whole VERIFIED lane
rests on. Contract ONLY: no producer, no reader, no solver, no serving, no
verify.py, no verified_serving_enabled, no claim emission in any serving path.

core/epistemic_disclosure/verified_contract.py:
- VerificationObligation — the declarative contract (4 load-bearing flags);
  VERIFICATION_OBLIGATION is fully strict.
- VerificationProof — the replayable proof SHAPE P1-B+ will fill (two reads kept
  separate: distinct lineages for independence, matching digests for convergence).
- evaluate_verification(proof, *, limitation, obligation) -> VerificationResult —
  refuse-preferring: VERIFIED only if EVERY obligation survives.
- disclosure_for_verification(result) — the ONLY sanctioned route to
  (EpistemicState.VERIFIED, DisclosureClaim.VERIFIED); anything else -> UNDETERMINED/NONE.

The mechanism that makes the lane safe: VERIFIED requires TWO INDEPENDENT reads
(distinct lineages) that CONVERGE on one canonical structure. A faithful solve of a
WRONG read is caught because the independent read disagrees (back-substitution alone
cannot catch a read error); "same answer twice" is rejected as not independent.
Neither gold-agreement nor no-refusal nor a second solver over one read can verify.

15 tests, all non-vacuous. THE central test rejects a faithful solve of a wrong read;
one proves the wrong-read obligation is load-bearing (relax it -> poison slips the
check); the full chain composes proof -> contract -> (VERIFIED, VERIFIED) -> P0-3
DISCLOSE, and the poison degrades to COMMIT. Off-serving; smoke 90/0.
2026-06-08 16:20:19 -07:00
Shay
178e3967ed feat(epistemic-disclosure): P0-3 — ServedDisposition mapping scaffold
The third axis of the bus: choose_served_disposition composes
EpistemicState × LimitationAssessment × DisclosureClaim → ServedDisposition.
Pure mapping — no rendering, no bus behaviour, no verify.py, no govern_response
mutation. Nothing consumes it yet.

core/epistemic_disclosure/disposition.py:
- ServedDisposition(str, Enum): commit | disclose | ask | propose | report |
  explain | refuse | step_aside.
- choose_served_disposition(*, epistemic_state, limitation, disclosure_claim=NONE):
  a blocking limitation dominates (its resolution_action -> ask/propose/report/
  explain/refuse/step_aside; scope_boundary -> EXPLAIN, NOT a hard REFUSE);
  otherwise the disclosure claim + state pick the serve mode.

The Phase-1 VERIFIED guard (load-bearing): a DisclosureClaim.VERIFIED discloses
ONLY when EpistemicState.VERIFIED backs it; an unbacked verified claim degrades to
a plain COMMIT — never served as verified before the producer exists.

30 tests: the load-bearing rules via REAL P0-1 assessments; the VERIFIED guard
parametrized across all 14 non-VERIFIED states; limitation-dominates-claim;
answer-action fall-through; totality over both axes; off-serving. Smoke 90/0.
2026-06-08 16:00:15 -07:00
Shay
8811917280 feat(epistemic-disclosure): P0-2 — the DisclosureClaim axis
A served response carries two ORTHOGONAL governance properties: ReachLevel (how
far past grounded fact it reaches) and DisclosureClaim (the epistemic claim it
makes about its own truth status). "verified" is NOT a reach level — it is a
proof-state claim. Keeping the axes separate is the locked Stage-2 decision (no
ReachLevel.VERIFIED; a proven answer never inherits an [approximate] surface).

core/epistemic_disclosure/disclosure_claim.py:
- DisclosureClaim(str, Enum): none | verified | approximate | proposal_only.
  Default = none. str-valued for stable serialization (EpistemicState convention).
- Discipline "no claim without a producer": every member has a real (APPROXIMATE
  = cognition Step E; PROPOSAL_ONLY = teaching/proposals) or imminent (VERIFIED =
  Phase 1 target) emitter. PROVEN and ESTIMATED are intentionally ABSENT — nothing
  emits them; ESTIMATED is a future split of APPROXIMATE, added only when a real
  estimator producer exists (docs-note only).

Also folds the P0-1 review nit: _KIND_TO_STATE / _ACTION_TO_TERMINAL tightened
from dict[str, ...] to their Literal key types.

9 tests, non-vacuous: default is NONE; verified is not a ReachLevel (structural);
claim axis disjoint from ResolutionAction; PROVEN and ESTIMATED absent; exactly the
approved four; str-enum serialization; module imports nothing (off-serving).

No bus behaviour, no ServedDisposition mapping (that is P0-3). Off-serving; smoke
green (90 passed) locally.
2026-06-08 15:48:20 -07:00
Shay
159b6830e4 feat(epistemic-disclosure): P0-1 — limitation pass as a consolidating view
The intake gate of the Epistemic Disclosure spine: classify WHAT KIND of
limitation blocks resolution before choosing a served disposition. Asking is
one resolution among six; mis-classifying breaks intake (refuse-when-should-ask
loses the unlocking datum; ask-when-should-propose wastes the channel).

New off-serving package core/epistemic_disclosure/:
- LimitationKind (8) / ResolutionAction (6) / LimitationAssessment
- the mapping DERIVED from the shipped failure_family REGISTRY + contemplation
  Terminal set — a consolidating VIEW, not a fourth taxonomy. The only genuinely
  new action is ask_question (the one action with no shipped terminal); every
  other action maps onto an existing Terminal (terminal_for_action proves it).
- assess_from_family / assess_from_attempt; conservative refuse default
  (an unclassified reason NEVER becomes an answerable question — wrong=0-safe).

The missing_total_count / missing_weighted_total reclassification
(capability_gap -> missing_information) is DEFERRED to Q1-B with its own tests,
not made here; PENDING_Q1B_RECLASSIFICATION + a tripwire test document it.

27 tests, all non-vacuous (each fails under a single mis-keyed entry): totality
over REGISTRY, flag-consistency invariants (proposal<->propose; contradiction
verdict reports; foreign text steps aside), the wrong=0 invariant (hard_boundary
never asks), load-bearing specific mappings, the consolidation proof, and the
attempt-level paths.

Off-serving: imports no generate.derivation / core.reliability_gate (AST-checked);
cannot move the sealed GSM8K metric. Nothing consumes resolution_action yet.
Smoke green (90 passed) locally.
2026-06-08 15:29:46 -07:00