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
18 KiB
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 0–4 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_preflightnode betweenquestionandretrieval_planstages. - 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_rootso 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_versionchanges. - 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, beforet_start). - Wire into
runner.ask()symmetrically. - The node hash contributes to
run_dag_rootautomatically 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 optionalpreflight_hashparameter inserts apreflightstage at position 1 (betweenquestionandretrieval). Quote-mode shape becomes 8 stages; pointer-mode CTI shape becomes 10 stages. Backward-compatible: whenpreflight_hashis None, shapes stay 7/9 exactly so legacyrun_dag_rootvalues re-validate.arborist/qa/query.py+arborist/qa/runner.py— both build the preflight payload fromquestion_state, the quantifier dict, and a 10-fieldpolicy_statecapturing 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 fromrun_dag_blob. Out of scope here. - Bench harness preflight-hash field: bench rows could
surface
preflight_hash(12-char prefix likecache_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
- A — reject-path DAG. Build a minimal
question → preflight → final_labelDAG for the reject-broad early-return path. Reject rows currently have norun_dag_root/run_dag_blob; this gives them one with the preflight node and a final-label that carriesBROAD_QUANTIFIER_REJECTED. Audit replay parity with normal runs. - B — restructure payload. Move from flat 3-key to nested
5-clause:
Plus a top-levelclassifier — 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)node_version: "preflight-node-v1"for legacy disambiguation (C). - 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.) inanswer_contract/prompt_contractsince those are per-run-state-of-the-world, not policy. - E — reminder_template_id. When reminder fires, record which
template (
broad-quantifier-bounded-v1/broad-quantifier-unbounded-v1). - F — keep
preflightname because the node carries BOTH #000008 quantifier classifier AND #000010 metacognition QuestionState.quantifier_preflightwould be misleading. Thenode_versionfield disambiguates schema. - 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
questionandretrieval— ✓ - Backward-compat (None preflight_hash → 7/9-stage shape) — ✓
governance_policy_hashinvalidates cache rows on policy flip — ✓ (already in #000008 / #000010)- No verifier / retrieval / schema changes — ✓
- Symmetric wiring in
query()andrunner.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— pinsPREFLIGHT_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.