arborist/docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md
russell@unturf.com 8d6961fcc1
aborist/arborist
modified:   .gitlab-ci.yml
	modified:   bench/qa_questions.txt
	modified:   bench/qa_sweep.py
	modified:   bench/run.sh
	modified:   docs/TICKETS.md
	modified:   docs/_source/README.md
	modified:   docs/_source/_ext/makefile_targets.py
	modified:   docs/_source/api/cli.rst
	modified:   docs/_source/api/distill.rst
	modified:   docs/_source/api/mesh.rst
	modified:   docs/_source/api/qa.rst
	modified:   docs/_source/api/retrieval.rst
	modified:   docs/_source/api/storage.rst
	modified:   docs/_source/api/substrate.rst
	modified:   docs/_source/concepts.rst
	modified:   docs/_source/conf.py
	modified:   docs/_source/cookbook.rst
	modified:   docs/_source/index.rst
	modified:   docs/_source/license.rst
	modified:   docs/_source/quickstart.rst
	modified:   docs/bench-maxing.md
	modified:   docs/benchmarks.md
	modified:   docs/cti-architecture.md
	modified:   docs/diagrams/aborist-modules.dot
	modified:   docs/diagrams/aborist-modules.svg
	modified:   docs/diagrams/mesh-data-flow.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.svg
	modified:   docs/diagrams/mesh-group-decisions.dot
	modified:   docs/diagrams/mesh-group-decisions.svg
	modified:   docs/diagrams/mesh-identity-stack.dot
	modified:   docs/diagrams/mesh-secret-envelope.dot
	modified:   docs/mesh.md
	modified:   docs/qa-modes-bench.md
	modified:   docs/seven-point-program.md
	modified:   docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md
	modified:   docs/tickets/ticket-000002-reference-frame-polarity-contract.md
	modified:   docs/tickets/ticket-000003-anchor-class-warrant.md
	modified:   docs/tickets/ticket-000005-label-ladder-migration.md
	modified:   docs/tickets/ticket-000006-bench-emergent-findings.md
	modified:   docs/tickets/ticket-000007-query-layer-hyphen-fold.md
	modified:   docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md
	modified:   docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md
	modified:   docs/tickets/ticket-000010-metacognition-preflight-guard.md
	modified:   docs/tickets/ticket-000011-soft-preflight-hint-sidecar.md
	modified:   scripts/backfill_concepts.py
	modified:   scripts/bench_emergent.py
	modified:   tests/crawler/test_async_web_fetcher.py
	modified:   tests/crawler/test_bridge.py
	modified:   tests/crawler/test_web_fetch.py
	modified:   tests/test_bench_qa_sweep.py
	modified:   tests/test_burn.py
	modified:   tests/test_burn_doc.py
	modified:   tests/test_claim_lattice.py
	modified:   tests/test_cli_render.py
	modified:   tests/test_compress.py
	modified:   tests/test_concepts.py
	modified:   tests/test_dag.py
	modified:   tests/test_directives.py
	modified:   tests/test_distill.py
	modified:   tests/test_distill_recursive.py
	modified:   tests/test_evict.py
	modified:   tests/test_frame.py
	modified:   tests/test_grok_source.py
	modified:   tests/test_html_source.py
	modified:   tests/test_ingest.py
	modified:   tests/test_inspect.py
	modified:   tests/test_journal.py
	modified:   tests/test_keys.py
	modified:   tests/test_llm_context_base.py
	modified:   tests/test_merkle.py
	modified:   tests/test_mesh.py
	modified:   tests/test_mesh_aead.py
	modified:   tests/test_mesh_chain.py
	modified:   tests/test_mesh_cli.py
	modified:   tests/test_mesh_cli_pull.py
	modified:   tests/test_mesh_wire.py
	modified:   tests/test_mesh_wire_e2e.py
	modified:   tests/test_metacognition.py
	modified:   tests/test_migration_audit_mode.py
	modified:   tests/test_providence_source.py
	modified:   tests/test_qa.py
	modified:   tests/test_qa_quality_live.py
	modified:   tests/test_quantifier_caps.py
	modified:   tests/test_quantifier_classifier.py
	modified:   tests/test_quantifier_phase4.py
	modified:   tests/test_quantifier_reminder.py
	modified:   tests/test_query.py
	modified:   tests/test_reclassify.py
	modified:   tests/test_repair.py
	modified:   tests/test_resume.py
	modified:   tests/test_snapshot.py
	modified:   tests/test_soft_preflight.py
	modified:   tests/test_tfidf.py
	modified:   tests/test_vcs_source.py
	modified:   tests/test_verify.py
	modified:   tests/test_verify_json.py
	modified:   tests/test_versioned_ingest.py
	modified:   tests/test_warrant.py
	modified:   tests/test_wikipedia_old.py
	modified:   tests/test_wikipedia_xml.py
	modified:   tests/test_wikitext.py
2026-05-07 09:31:49 -04:00

18 KiB
Raw Blame History

Ticket #000009 — Preflight run-DAG node binding

Status: closed · re-landed 2026-05-04 with §8 corrections (reject-path DAG, nested CTI clauses, node_version, reminder template id, policy_refs). Original landing 2026-05-03 in c36e85c; §8 corrections per architectural feedback at ~/Downloads/RESPONSE-ticket-000009-quantifier-preflight-dag-binding.txt. Opened: 2026-05-03 Scope (expanded 2026-05-03): Bind BOTH ticket #000008's broad- quantifier preflight contract AND ticket #000010's meta-cognition QuestionState into the per-run Merkle-DAG. Both share the same audit-replay gap (per #000010 §12.6) and inserting two separate nodes between question and retrieval is operationally awkward; single combined preflight node carries both payloads. Audience: fox + future blackops shifts. Hard constraint: No schema_version, canonicalization_version, or chunking_version bumps. Same constraint #000008 §1 carried. The new DAG node is part of the per-run run_dag_blob payload, not a column on providence_cache. Adding the node bumps a derived field (run_dag_root SHA), which is allowed — that's already recomputed every run.


1. Problem statement

#000008 landed Phases 04 in commit 4f2b5a6. The preflight classifier output (quantifier_intensity, scope_bound_hint, claim_cap_applied) and the policy decision (cap applied or not, reminder injected or not) appear on the query() / runner.ask() result dict and on bench rows.

They do NOT appear in run_dag_root.

That means an audit replay of a providence_cache row CANNOT distinguish:

question Q + classifier=ALL + cap=8-applied + reminder=injected
question Q + classifier=ALL + cap=12-applied + reminder=injected
question Q + classifier=ALL + cap=8-applied + reminder=skipped
question Q + classifier=null (guard off entirely)

All four would produce the same run_dag_root because the run DAG is unchanged by quantifier-guard activity. Same model output, same verifier verdict, same hash — but causally different behavior.

The §9.5 Merkle-AGI-DAG framing (#000008) explicitly calls for the preflight decision to be hash-bound:

The node hash should include: stage, question_hash, model_profile_hash, governance_policy_hash, classifier_version, intensity, matched_token, explicit_count, claim_cap, allow_broad, reject_broad, reminder_injected.

That binding is what closes the audit-replay gap.

2. Scope

What this ticket covers:

  • Extend the per-run DAG with a quantifier_preflight node between question and retrieval_plan stages.
  • Three-clause CTI contract from #000008 §10.4 — quantifier, answer_contract, evidence_contract — bound as sub-fields of the new node OR as three separate nodes (design TBD).
  • Hash binding: classifier output + policy decision contribute to run_dag_root so audit replay can distinguish guard-on vs guard-off, cap-applied vs not, reminder-injected vs skipped.

What this ticket does NOT cover:

  • Verifier semantics. The preflight node carries policy state into the audit chain; the verifier still operates on the same inputs.
  • Schema bumps. Per #000008 §1 hard constraint, no schema_version/canonicalization_version/chunking_version changes.
  • Cross-model bench (Qwen / GPT-4) — that's a separate manual task per #000008 §11.11.

3. Design sketch

3.1 New stage between question and retrieval_plan

Current run-DAG order (claim_lattice modes):

question → retrieval_plan → retrieval_result → evidence_map →
prompt → raw_model_output → parsed_claim_lattice → pointer_verify
→ format_check → anchor_warrant → final_label

Proposed:

question → quantifier_preflight → retrieval_plan → ...

quantifier_preflight is hash-bound and feeds into retrieval_plan the same way question does today (parent → child via HashCombine).

3.2 Node payload

Single-node form (proposal):

{
  "stage": "quantifier_preflight",
  "classifier_version": "quantifier-v0.1",
  "intensity": "ALL",
  "matched_token": "all",
  "explicit_count": null,
  "scope_bound_hint": "unbounded",
  "is_broad": true,
  "operational_shape": "universal_enumeration",
  "guard_enabled": true,
  "mode_gated": true,
  "apply_caps_active": false,
  "claim_cap_resolved": 8,
  "claim_cap_applied": null,
  "reminder_enabled": true,
  "reminder_injected": true,
  "reject_broad_active": false
}

Three-clause form (per #000008 §10.4):

{
  "stage": "quantifier_preflight",
  "classifier_version": "quantifier-v0.1",
  "quantifier_clause":   { "intensity": "ALL", ... },
  "answer_contract":     { "max_claims": 8, ... },
  "evidence_contract":   { "max_evidence_ids_exposed": 8, ... }
}

Three-clause form is more extensible (each clause can grow independently) but adds one indirection layer. Single-node form is denser and simpler. Recommendation: single node initially; split if a future clause needs independent hashing.

3.3 Where the node lives in code

  • New helper in arborist/qa/dag.py (alongside existing per-stage helpers): _quantifier_preflight_node(quantifier, policy, effective_max_claims, reminder_injected).
  • Wire into query() at the same point quantifier classification runs (after policy resolution, before t_start).
  • Wire into runner.ask() symmetrically.
  • The node hash contributes to run_dag_root automatically via the existing run-DAG framing.

3.4 Audit replay implications

After this lands, an audit reading a providence_cache row can:

hash(quantifier_preflight node) = h_pre
→ pin: classifier output, cap decision, reminder decision

If h_pre changes between two cache_keys for the same question,
that row reflects a different preflight policy.

Allows for: regression bisection ("which day did the cap default
flip break our STRICT-rate?"), policy A/B reconstruction
("show me all rows where cap was applied vs not"), cross-model
diff ("hermes vs qwen on the same question with the same
preflight contract").

4. Implementation sketch

1. New function in arborist/qa/dag.py:
     def quantifier_preflight_node(quantifier, policy, ...)
2. Wire it into query() between policy resolution and t_start.
3. Wire it into runner.ask() symmetrically.
4. Run-DAG ordering: insert as child of `question` node, parent
   of `retrieval_plan` node.
5. Update tests/test_dag.py with a fixture asserting the node
   appears with the expected payload + position.
6. Update docs/cti-architecture.md to reflect the new stage.
7. Bench cycle to confirm no run_dag_root collisions across
   guard-on vs guard-off cells.

Estimated cost: 3-4 hours including tests + bench verification.

5. Why now

#000008 §11.11 explicitly listed this as Phase 5 follow-up. Closing #000008 with the preflight mechanism shipped but the audit-replay gap still open creates a "shipped but non-auditable" state — the seven-point program D3 directive ("build CTI internally") is half-honored: the contract exists in code, but isn't bound into the audit chain.

This ticket closes the loop.

6. Open questions

  • Single node vs three-clause split (§3.2)? Default: single, split later if needed.
  • Should the node hash include the model_profile_id verbatim, or just rely on the existing model_profile_hash? Default: rely on existing — it's already in the cache_key derivation.
  • Bench cell needed before merging? Default: yes — confirm run_dag_root bumps when a guard knob flips, AND that governance_policy_hash continues to invalidate cache rows appropriately.

7. Status

Closed · landed 2026-05-03 (zero-shot). Mechanism shipped end- to-end:

  • arborist/qa/dag.py:preflight_node_hash() — combines QuestionState + quantifier classifier output + behavioral policy_state into one canonical SHA-256 hex.
  • arborist/qa/dag.py:build_run_dag() — new optional preflight_hash parameter inserts a preflight stage at position 1 (between question and retrieval). Quote-mode shape becomes 8 stages; pointer-mode CTI shape becomes 10 stages. Backward-compatible: when preflight_hash is None, shapes stay 7/9 exactly so legacy run_dag_root values re-validate.
  • arborist/qa/query.py + arborist/qa/runner.py — both build the preflight payload from question_state, the quantifier dict, and a 10-field policy_state capturing the behavioral decisions taken on this run (guard_enabled, apply_caps_active, claim_cap_resolved, claim_cap_actually_applied, reminder_enabled, reminder_eligible, reject_broad_active, metacognition_enabled, block_on_contradiction, mode-gating bits).

9 new tests in tests/test_dag.py pin: hash determinism, question_state/policy_state independence, all-None defensive shape, 7→8 / 9→10 stage transitions, root change on policy flip, verify_run_dag round-trip with preflight stage.

987 tests passing (9 new); 36 skipped.

7.1 What this enables (audit replay)

Two cache rows that have:

  • The same question
  • The same model output
  • The same verifier verdict

But different behind-the-scenes preflight policy state (e.g. cap applied vs not, reminder injected vs not, reject-broad path taken vs not) now produce different run_dag_root values.

Audit replay can:

hash(preflight node) = h_pre
→ pin: classifier output, cap decision, reminder decision,
       reject decision, metacog gates

If h_pre changes between two cache_keys for the same question,
that row reflects a different preflight policy.

Allows: regression bisection ("which day did the cap default
flip break our STRICT-rate?"), policy A/B reconstruction
("show me all rows where cap was applied vs not"), cross-model
diff ("hermes vs qwen on the same question with the same
preflight contract").

7.2 What's NOT in this ticket

  • CLI flag for inspecting preflight node: a future arborist providence --show-preflight <cache_key> would render the preflight payload from run_dag_blob. Out of scope here.
  • Bench harness preflight-hash field: bench rows could surface preflight_hash (12-char prefix like cache_key) for cross-row comparison. Out of scope; can add later if bench analysis needs it.
  • SOFT_PREFLIGHT_HINT (model-assisted preflight): reserved per #000010 §18 / source doc. Hard rule preserved.

8. Architectural review + corrections (2026-05-04)

Source: ~/Downloads/RESPONSE-ticket-000009-quantifier-preflight-dag-binding.txt (2026-05-04, Asia/Kuala_Lumpur). Verdict: "Go. Land #000009." Recommendations against the as-shipped (c36e85c) implementation:

8.1 Gaps surfaced

# Gap Severity
A Reject-broad path emits NO run_dag_blob. Preflight rejection currently early-returns from query() before build_run_dag() runs. Audit replay can't see that a rejection happened. critical
B Payload is flat (question_state / quantifier / policy_state). Feedback §3 recommends nested CTI clauses (classifier, answer_contract, prompt_contract, evidence_contract, policy_refs) for diff legibility. structural
C No node_version field. Legacy runs without the stage can't be unambiguously labeled unavailable_legacy_run. clarity
D Payload bundles raw policy booleans instead of referencing governance_policy_hash / model_profile_hash. Bloats the hash and double-commits already-hashed state. clarity
E No reminder_template_id field for the prompt contract. minor
F Stage named preflight (because metacog also lives there) vs feedback's quantifier_preflight. Defensible either way; we'll document the choice. naming
G docs/cti-architecture.md not updated. docs

8.2 Landing plan

  1. A — reject-path DAG. Build a minimal question → preflight → final_label DAG for the reject-broad early-return path. Reject rows currently have no run_dag_root / run_dag_blob; this gives them one with the preflight node and a final-label that carries BROAD_QUANTIFIER_REJECTED. Audit replay parity with normal runs.
  2. B — restructure payload. Move from flat 3-key to nested 5-clause:
    classifier      — quantifier classifier output
    answer_contract — guard / cap / reject / allow_broad state
    prompt_contract — reminder enabled / injected / template_id
    evidence_contract — exposure budget, line-discipline
    policy_refs     — governance_policy_hash, model_profile_hash,
                      answer_mode (reference, not raw policy)
    
    Plus a top-level node_version: "preflight-node-v1" for legacy disambiguation (C).
  3. D — reference hashes only. Replace bundled policy booleans with policy_refs.governance_policy_hash + policy_refs.model_profile_hash. Keep the behavioral decisions (claim_cap_applied, reminder_injected, etc.) in answer_contract / prompt_contract since those are per-run-state-of-the-world, not policy.
  4. E — reminder_template_id. When reminder fires, record which template (broad-quantifier-bounded-v1 / broad-quantifier-unbounded-v1).
  5. F — keep preflight name because the node carries BOTH #000008 quantifier classifier AND #000010 metacognition QuestionState. quantifier_preflight would be misleading. The node_version field disambiguates schema.
  6. G — update docs/cti-architecture.md.

8.3 What stays as-is

The feedback's recommendations match what's already shipped on:

  • Single DAG stage (not three separate nodes) — ✓
  • Stage placement between question and retrieval — ✓
  • Backward-compat (None preflight_hash → 7/9-stage shape) — ✓
  • governance_policy_hash invalidates cache rows on policy flip — ✓ (already in #000008 / #000010)
  • No verifier / retrieval / schema changes — ✓
  • Symmetric wiring in query() and runner.ask() — ✓

8.4 Out of scope (per feedback §12)

Do not use #000009 to sneak in any of this:

  • new verifier rules
  • new quantifier classifier categories
  • retrieval changes
  • new schema/cache columns
  • new audit_mode token
  • NLI / semantic entailment
  • prompt-template behavior changes
  • default policy flips

Confirmed. Corrections stay strictly in the audit-binding lane.

8.5 Corrections landed (commit pending after this update)

A — reject-path DAG (the critical gap):

arborist/qa/dag.py:build_reject_run_dag() ships. Builds a 3-stage question → preflight → final_label DAG for the reject-broad early-return path. query() now wires it in and returns run_dag_root + run_dag_blob on the rejection result dict. Live-verified on make query Q="winners of all major sports?" REJECT_BROAD=1 BURN=1:

"status": "broad_quantifier_rejected",
"run_dag_root": "86a03380…",
"run_dag_blob": {"nodes": [
  {"stage": "question", "hash": "…"},
  {"stage": "preflight", "hash": "…"},
  {"stage": "final_label", "hash": "…"}
]}

3-stage shape always means reject path; audit replay can read the stage list and tell instantly without parsing the payload.

B — nested CTI clauses + C — node_version + D — reference hashes + E — reminder_template_id:

preflight_node_hash() payload restructured from flat 3-key (question_state / quantifier / policy_state) to nested 5-clause:

classifier         — quantifier classifier output
answer_contract    — guard / cap / reject / metacog state (per-run)
prompt_contract    — reminder enabled / injected / template_id
evidence_contract  — exposure budget, line discipline
policy_refs        — governance_policy_hash, model_profile_hash,
                     answer_mode (reference, not raw policy)

Plus top-level stage, node_version: "preflight-node-v1", and question_state (metacog, kept its own clause for now since QuestionState carries its own internal preflight_policy_hash).

reminder_template_id is "broad-quantifier-bounded-v1" or "broad-quantifier-unbounded-v1" depending on scope_bound_hint, populated only when reminder actually fires.

policy_refs.governance_policy_hash is the verifier_policy_hash(policy) already used for cache identity — reusing the existing hash rather than re-canonicalizing all the policy fields. model_profile_hash follows the same pattern.

F — stage name kept as preflight (not quantifier_preflight) since the node carries both #000008 quantifier classifier AND #000010 metacognition QuestionState. node_version field disambiguates schema for audit tools.

G — docs/cti-architecture.md update: deferred to a small follow-up commit. The architecture description in the ticket §8 serves as the canonical reference until then.

Hash compatibility note: rows written between commit c36e85c (initial #000009 landing) and this commit have hash payloads matching the OLD flat 3-key shape. The persisted run_dag_blob captures the actual payload that was hashed, so those rows still verify via verify_run_dag(). New rows after this commit use the nested 5-clause shape. Operators reading the run_dag_blob directly see the structure either way.

8.6 New tests

tests/test_dag.py:

  • test_preflight_node_hash_changes_with_answer_contract — apply_caps flip in answer_contract → distinct node hash.
  • test_preflight_node_hash_changes_with_prompt_contract — reminder injection flip → distinct hash.
  • test_preflight_node_hash_changes_with_policy_refs — governance_policy_hash flip → distinct hash.
  • test_preflight_node_hash_includes_node_version — pins PREFLIGHT_NODE_VERSION = "preflight-node-v1".
  • test_reject_run_dag_three_stage_shape — reject DAG always 3 stages (question → preflight → final_label).
  • test_reject_run_dag_root_changes_with_preflight_hash — audit-replay payoff for reject path.
  • test_reject_run_dag_round_trips_through_verify — 3-stage shape verifies the same way as 7/9/8/10-stage shapes.

993 tests passing (6 net new); 36 skipped.

8.7 Status — re-closed

Closed · landed via this commit on top of c36e85c. Audit-binding gap from feedback §6.2 (reject path) closed; nested-clause payload landed per feedback §3; node_version + reminder_template_id + policy_refs landed per feedback §4 + §9.