コンテンツにスキップ

Evidence-backed causal graph: implementation contract

このコンテンツはまだ日本語訳がありません。

Status: candidate extraction, human model review, and a limited observational estimator are implemented. The system does not yet establish a production causal effect.

  1. A SourceArtifact is an immutable, scoped version of text or an observational dataset. It records a digest, lineage, event and ingestion times, ACL, and retention deadline.
  2. A CausalClaim is a proposed directed relation with lag, context, and modality. An exact UTF-8 EvidenceSpan supports or opposes it. Extraction creates candidates only.
  3. A CausalVariable fixes a measured variable’s name, definition, unit, type, and version. A CausalModel pins variable versions and claim IDs into a directed acyclic graph for a population and time window.
  4. Claim and model reviews are separate human actions with append-only review records. New opposing evidence, source revocation, or expired support can return an approved model to needs_review.
  5. Six assumption reviews cover data availability, temporal order, positivity, exchangeability, consistency, and identifiability. Each review records the reviewer, rationale, verdict, and version. Missing, failed, or unknown verdicts keep the estimator preflight at Unknown.
  6. An ObservationalEstimate records a scoped dataset version, model, estimator code digest, adjustment method, estimate, interval, and diagnostics. Its status remains observational_adjusted_unvalidated.

The extraction worker must use a provider authorized for the source ACL. The structured response names cause and effect, direction and lag, modality, speaker, and exact byte offsets. The parser checks all proposed spans against the original source before any candidates are ingested; the entire response is then committed in one transaction, including alias resolution, so a later failure rolls back earlier proposals. The reviewer can inspect the original within the same scope, accept or reject claims, and review a DAG. Speculated or negated claims cannot become asserted model edges. A cycle cannot be stored as a model.

causal_extraction_runner::extract_candidates_with_provider is the provider-agnostic invocation seam. Its caller supplies a trusted route authorizer; denial happens before scoped source text is read or sent to a model. The request disables tools and asks for JSON candidates only. It rejects incomplete responses and non-text output, then re-reads the source through ingest_extraction_response so a source revoked during the provider call cannot create a candidate. The run returns source, prompt, and response digests plus provider/model identity for audit without retaining the provider prompt or response in that run object. The admin-only /api/causal/extract route and curation page use an opt-in config.toml [causal_extraction] policy with exact tenant/ACL, provider, and model matching; it is disabled by default and requires a provider API key in the standard environment variable. The scripted provider tests the boundary offline. Real extraction accuracy is not yet measured.

duduclaw causal-eval --dataset fixtures/causal-extraction-synthetic.json requires the causal-extraction-eval-v2 schema. Every case carries a nonempty, human-curated source_family_id in addition to its source_lineage_id; old v1 files and missing family labels fail explicitly; callers must review source provenance, add each family label, and change the dataset version to v2. The evaluator unions declared family IDs, repeated lineage IDs, and byte-identical source text, then rejects any resulting family that crosses the fixed training/held-out time cutoff. The label is an asserted upstream provenance group: the evaluator cannot discover an unmarked paraphrase or validate the human label. The synthetic fixture includes a differently worded arrival claim in the same declared held-out family and a similar but separately labeled training rule. For held-out scores, full-claim and reviewed-edge counts deduplicate by family and claim/edge identity; exact-span and span-grounded direction, stance, modality, lag, and negation scores remain per case. A full-claim family true positive still needs an exact grounded match in at least one case; ungrounded proposals cannot earn true positives, while an ungrounded duplicate of a grounded family claim/edge does not add a second predicted item. The report exposes both held-out case and family counts. A field true positive requires a grounded exact-span match; its denominators include unmatched predictions and labels. Each per-case span metric uses maximum one-to-one matching, so response order does not alter shared-span scores. Legacy matched-span counts remain diagnostics. A case may include reviewed_edges: a complete decision for every distinct gold source claim edge (cause, effect, lag, modality), each with accepted: true or false. If one held-out case in a family is reviewed, every case in that family must be reviewed; contradictory decisions for the same edge across family cases fail validation. Predicted edge keys count once per family; ungrounded-only keys are false positives, and rejected_edges_proposed includes rejected keys even when their span is invalid. Unlabeled families do not enter this metric. These labels measure agreement with claim curation, not identified intervention effects. Synthetic labels check evaluator mechanics only; real annotated held-out sources and independent reviewers are still needed.

The admin-only POST /api/causal/eval/extraction and the Causal Curation evaluation tab expose that same v2 scorer without invoking a provider or persisting an uploaded dataset. bundled_synthetic scores the checked-in fixture and explicitly labels its sources unbound. scoped_dataset accepts a v2 dataset plus a case_artifacts map with exactly one distinct artifact ID for every case; the gateway checks each active artifact in the requested exact tenant/ACL against its lineage, occurrence time, and full source bytes, retains source delivery leases through scoring, then rechecks active metadata and digest before returning aggregate metrics. The request is admin-only, capped at 2 MiB and 128 cases; the response includes no source text and distinguishes source provenance from caller-supplied family labels. A report records a point-in-time local evaluation, not continuing source validity. Manual family labels, response/provider identity, gold labels, and any real-model quality remain unverified. The page clears old reports when scope changes.

duduclaw causal-eval-compare --dataset fixtures/causal-extraction-compare-synthetic.json requires the causal-extraction-compare-v2 schema with the same manual family label on every case. It uses one shared time-split case, label, and family set with a complete response matrix for at least two named models. It rejects missing or duplicate model responses and requires exactly one held-out case per computed source family, including declared paraphrases, so duplicate cases cannot inflate paired wins. It runs the same evaluator for each model and counts paired held-out families where either model has fewer full-claim or reviewed-edge errors (false positives + false negatives). The fixture demonstrates that a response can improve source-claim extraction while proposing more rejected graph edges. Model IDs, source-family labels, and responses are supplied by the dataset and are not independently attested; paired counts are descriptive, with no significance or real-model accuracy claim.

Repeated extractions of the same semantic proposition from the same tenant, ACL, and source lineage reuse one claim. A new lineage with byte-identical active source content and the same proposition semantics also reuses that claim, while retaining its own lineage mapping and exact evidence span. Repeated extraction of the same span is idempotent. Curation can read a lineage summary that counts active supporting and opposing spans separately from conservative source families. Versions with one lineage ID and byte-identical artifacts with different lineage IDs form one connected family, counted once per stance; a family containing both stances is flagged. Revoked artifacts contribute no stance, but their retained opaque digest can still connect copies for conservative counting, so invalidation cannot inflate the independent-source count. This prevents an exact copy relabeled as another lineage from inflating corroboration, while preserving every span for inspection. Different text copied or paraphrased across claimed lineages can still be miscounted until an upstream connector supplies authoritative provenance. Legacy claims created before this mapping are not automatically merged, because their reviews and model links are append-only audit records.

An instance administrator can map an exact extracted variable name to a registered canonical variable name within one tenant and ACL. The mapping is reviewed and versioned; fuzzy matching, alias chains, and collisions with registered variable names are rejected. Future candidate extraction resolves through the current mapping in the same transaction as evidence insertion, while the original names, mapping review IDs, and exact source excerpt remain available. A changed or revoked mapping demotes dependent claims and scrubs effects from linked models. Acceptance rechecks every alias dependency against the current mapping; a stale claim cannot simply be reaccepted. A new extraction under a new mapping version creates a new candidate instead of silently rewriting a historical claim.

Source text and evidence writes verify the stored artifact digest. Adding a manual span holds an immediate SQLite transaction from the active-source check through the span insert, so a concurrent source deletion cannot leave a newly inserted excerpt after its scrub step.

The gateway exposes a first curation surface over the causal tables in <home>/memory.db. All routes require a valid dashboard bearer JWT, an allowed Origin, an account whose password change is complete, and the instance-admin role. The current JWT has no tenant claim, so tenant_id and acl are exact store selectors for an instance administrator; this API must not be widened to tenant-scoped users without binding those selectors to authenticated tenant identity.

  • GET /api/causal/claims?tenant_id=...&acl=...&review_state=...&limit=... lists up to 100 claims in the exact scope.
  • GET /api/causal/claim?tenant_id=...&acl=...&id=... returns a claim, its effective state, exact evidence spans with revoked excerpts hidden, independent-lineage counts, and revision links.
  • GET /api/causal/source?tenant_id=...&acl=...&id=...&offset=...&limit=... returns a verified UTF-8 source chunk, at most 64 KiB, with byte and next offsets. Revoked or expired source text is unavailable.
  • POST /api/causal/source/import-memory accepts {agent_id, memory_id} from an instance administrator. It imports only a live, non-quarantined memory owned by that exact agent into tenant_id=agent_id, acl=agent-private; the caller cannot choose a wider ACL. The artifact version binds the content digest and a memory lifecycle revision. SQLite triggers installed alongside the causal tables erase the copied wording and evidence excerpts, scrub the context_json of every linked claim and the rationale of every linked negative-control review, demote accepted claims, and clear dependent effect values when the original memory changes, is quarantined, or is deleted. A row still bound to an active CCR delivery lease is left untouched by that scrub and retried on the next causal-store open once the lease is released, rather than failing the memory write. A later valid import uses a new version. The memory’s valid_until also becomes the artifact retention deadline. A source with no deadline — an open-ended memory, any Wiki page — reports retention_at: null rather than a sentinel timestamp, because the stored sentinel is larger than a JavaScript number can hold exactly and a client comparing it would read a wrong date.
  • POST /api/causal/source/import-wiki accepts {agent_id, page_path} from an instance administrator and imports only that agent’s non-quarantined Wiki page body into the same agent-private scope. The source version is the raw Markdown digest, including frontmatter. A causal-store instance rechecks active imported pages against the live file and Wiki trust state when it opens, and at most once every 30 seconds thereafter; a changed, deleted, low-trust, or do_not_inject page has its copied wording and excerpts scrubbed, its linked claims’ context_json and negative-control review rationale cleared, accepted claims demoted, and dependent effects cleared before any subsequent causal read is served. Each HTTP request builds its own store, so every request still rechecks before it answers; the interval only stops a single request that reads a hundred claims from re-reading every imported page a hundred times. Disclosure is not throttled: a held CCR delivery lease compares the live file itself on every still_valid() call, with no store open in between, so a page rewritten mid-delivery invalidates that delivery immediately. A page still bound to an active CCR delivery lease defers that scrub to the next causal-store open once the lease is released, rather than failing the surrounding read or write. Opening the live Wiki trust store can contend with the per-directory Wiki delivery fence; that contention is now reported as a distinct transient “busy” condition answered with HTTP 503, instead of being reported as invalid causal evidence input with HTTP 400. The filesystem bridge rejects path traversal and symlinked Wiki roots outside the agent’s home.
  • POST /api/causal/source/clear-revocation-fence accepts {tenant_id, acl, artifact_id} in a 16 KiB body and clears a revocation fence an abandoned revoke left behind, returning the fenced cleared_version. Starting a revocation hides the source from readers immediately and that is deliberate; this route does not reverse a revocation that took effect. It returns conflict when the artifact is already invalidated or erased, when a tombstone notice is still queued for CCR, or while any delivery lease for that version is recorded — in each case something downstream has already acted on the revocation. Every call, allowed or refused, writes a causal_clear_revocation_fence audit row naming the administrator. The same operation is available to an operator at the terminal as duduclaw causal-clear-revocation-fence --db <path> --tenant <id> --acl <scope> --artifact <id>.

A delivery lease is a database row plus an OS lock file under <memory.db>.ccr-leases/. The lock file is created before the row is committed, so a process killed in between leaves a file no row points at. The causal-store maintenance pass lists that directory and deletes only files that have no lease row and whose exclusive lock it can take; a file a live acquirer still holds is never assumed dead, and a failure to reap never fails the store open.

  • POST /api/causal/source/import-shared-wiki accepts {page_path} from an instance administrator and imports only a live shared Wiki page into tenant_id=workspace, acl=shared-wiki. The namespace’s .scope.toml mode and synced_from value are bound into the artifact version alongside the raw page digest. A page or namespace-policy change invalidates the old causal copy before subsequent reads; a malformed policy fails closed. The current shared-Wiki namespace mode governs writing, while the existing shared-Wiki read surface is cross-agent. This import uses that same read scope and never reclassifies an agent-private page as shared.
  • POST /api/causal/claim/review accepts {tenant_id, acl, id, expected_state, accept} in a 16 KiB body. The reviewer ID comes from the JWT. A single immediate transaction checks the expected state and active support before appending the human review; a stale state returns conflict.
  • POST /api/causal/claim/revise accepts {tenant_id, acl, id, expected_state, cause_variable, effect_variable, lag_min_seconds, lag_max_seconds, modality, context, evidence_ids, note} in a 16 KiB body. The selected evidence IDs must belong to the old claim, include support, and still match active source bytes and digests. The transaction creates a new candidate with fresh evidence IDs, appends a reviewer-linked revision record, permanently supersedes the old claim, and clears effect values in models using it. The new candidate needs a separate human review; old review decisions and the source-linked audit remain readable.
  • GET /api/causal/models?tenant_id=...&acl=...&limit=... lists up to 100 model summaries with their effective states.
  • GET /api/causal/model?tenant_id=...&acl=...&id=... returns a model’s pinned variable versions, claim edges, effective state, and active opposing-evidence count and digest. Model creation already rejects directed cycles.
  • POST /api/causal/model/review accepts {tenant_id, acl, id, expected_state, expected_opposition_digest, approve, acknowledge_conflicts}. Use review.model.review_state as the expected stored state; review.effective_state separately reports whether an earlier approval remains viable. Within one immediate transaction the server checks both expected values, active supporting evidence, and whether active opposition requires acknowledgement. A new opposing span after the reviewer loads the model causes conflict, so it cannot be silently acknowledged by a stale approval.
  • GET /api/causal/model/assumptions?tenant_id=...&acl=...&model_id=... returns the six-kind current-review projection and readiness reasons. POST /api/causal/model/assumptions accepts {tenant_id, acl, model_id, kind, verdict, rationale, expected_review_id}. Set expected_review_id to null for the first judgment of that kind or the current ID for a revision. The reviewer comes from the admin JWT, stale submissions return conflict, and every accepted judgment scrubs prior numeric estimates for the model.
  • POST /api/causal/negative-control/protocol accepts {tenant_id, acl, external_id, version, lineage_id, content, occurred_at, retention_at} and creates an immutable exact-scope negative_control_protocol artifact. The caller must identify the source version and retention deadline; synthetic prose alone does not establish an exclusion restriction.
  • POST /api/causal/negative-control/review accepts {tenant_id, acl, model_id, variable_id, protocol_artifact_id, verdict, rationale, expected_review_id} where verdict is pass, fail, or unknown. Set expected_review_id to null for the first review or the current review ID for a revision; stale submissions return conflict. The reviewer ID comes only from the admin JWT. Each review is appended and replaces the earlier effective verdict for that model and control variable; affected numeric estimates are scrubbed.
  • GET /api/causal/negative-control/review?tenant_id=...&acl=...&model_id=...&variable_id=... returns the newest review and its current readiness. Administrators use its ID as expected_review_id when revising a review; readiness can fall back to unknown after the protocol or graph changes.
  • POST /api/causal/effect/estimate accepts {tenant_id, acl, model_id, external_id, version, lineage_id, occurred_at, retention_at, dataset}. The dataset object follows ObservedDataset; its optional negative_control names the exact variable and review IDs, and every unit then supplies negative_control_outcome and negative_control_recorded_at. The route stores an immutable scoped causal_dataset artifact and returns its ID plus {state: "estimated", estimate} or {state: "unknown", reasons}. GET /api/causal/effect?tenant_id=...&acl=...&id=... rechecks a persisted estimate and returns the same result shape. The dataset body is limited to 2 MiB. A dataset with failed estimator gates remains an auditable artifact; a corrected version needs a new version ID.
  • GET /api/causal/aliases?tenant_id=...&acl=...&limit=... lists up to 100 current reviewed mappings.
  • POST /api/causal/alias accepts {tenant_id, acl, alias, canonical_name, expected_review_id}. Set expected_review_id to null for a new alias or to the current review ID for a revision. The reviewer ID comes from the JWT. Stale submissions return conflict.
  • POST /api/causal/alias/revoke accepts {tenant_id, acl, alias, expected_review_id} and invalidates dependent interpretations under the same transaction.

The admin-only /app/system/causal page consumes these endpoints for exact-scope source inspection, claim review and revision, alias curation, and model edge review. It shows each edge’s direction, lag, modality, and source-backed claim detail; a client-side cycle check blocks approval if the returned graph contains a directed cycle. The server independently rejects cyclic model creation and rechecks model approval. A selected model also exposes the six assumption reviews, a negative-control panel, protocol source inspection, compare-and-set verdicts, ObservedDataset JSON submission, and Estimated or Unknown results. The page labels observational intervals as uncalibrated. An administrator can trigger source-grounded candidate extraction only when the deployment has explicitly enabled its provider, model, and exact scope. Tenant-bound user authorization remains open.

For an isolated synthetic walkthrough, run duduclaw causal-demo --db <path-to-memory.db> --tenant synthetic-support --acl demo-private with an explicit database path. The command is idempotent for its fixed source versions, creates two exact-span candidate claims and a draft DAG, and never approves a claim/model or writes an effect estimate. To view it in the dashboard, the path must be the gateway’s own <home>/memory.db; the scope is entered explicitly on /app/system/causal. The sources describe simulator rules, not observations from real support operations.

For the binary-treatment estimator walkthrough, run duduclaw causal-observational-demo --db <home>/memory.db --tenant synthetic-support --acl demo-private --output <base.json>. It seeds four source-grounded candidate DAG edges, a draft model with extra_staffing as a binary treatment, a synthetic audit_count exclusion protocol, and an 80-unit ObservedDataset JSON file. The deterministic generator makes queue cohort affect treatment propensity, wait hours, and audit count; extra staffing lowers wait hours by exactly two in both cohorts and periods while leaving audit count unchanged. In /app/system/causal, review claims, the model, all six assumptions, and the negative-control protocol. After obtaining a passing review ID, rerun the command with --review-id <id> --output <with-control.json> to produce a complete negative-control dataset. Add --evaluation-manifest <evaluation.json> to store separate 40-unit pre/post-cutoff synthetic cohort estimates with disjoint source lineages and emit a replayable effect evaluation manifest and report. Submit the combined JSON through the selected model’s effect panel to inspect its within-dataset temporal diagnostic. Reused output paths are accepted only if bytes are identical. These are synthetic engineering fixtures; the deterministic demo can produce a zero-width interval with one held-out case, which exercises report wiring but says nothing about useful sharpness or real-data calibration. The CLI never grants a human review.

The current estimator accepts a binary treatment, a count or continuous outcome, and predeclared joint confounder strata. Each dataset now names the exact model variable versions used for adjustment. Each unit records those variables’ values and when they were measured; the values must precede treatment assignment and match a one-to-one joint-stratum label. An empty adjustment set cannot be paired with multiple hidden strata.

Before estimation, a graph check rejects treatment descendants and verifies d-separation between treatment and outcome in the DAG with treatment’s outgoing edges removed. It uses ancestral moralization for this classical backdoor check. Failure returns Unknown even if every assumption review says pass. A passing graph check remains conditional on the reviewed DAG containing all relevant causes; it cannot detect omitted or unmeasured confounding. This is narrower than generalized adjustment identification.

The admin model detail API and curation page also show a read-only parent-adjustment candidate after the model is approved. It collects the direct parents of treatment in the reviewed DAG and runs that set through the same backdoor check; a draft or stale model shows unknown. The displayed names are guidance for constructing a dataset, not an automatic edit to adjustment_variable_ids. Each value must still be available and measured before assignment, and a graphically admissible set cannot establish that the DAG includes latent causes.

Under the reviewed exchangeability and graphically admissible backdoor assumptions, the estimator computes the standardized contrast:

sum_w P(W=w) × (mean(Y | A=1,W=w) - mean(Y | A=0,W=w)).

It requires exact scoped dataset access, unique units, type-valid outcomes, assignment before outcome within the model window, and at least two treated and two control units per stratum. Continuous outcomes must be finite; count outcomes, pre-treatment values of that same outcome, and count-typed negative-control outcomes must be nonnegative exact integers within the f64 safe-integer range. Invalid measurements return Unknown. It returns Unknown when these checks fail. Its normal-approximation interval is conditional on recorded strata and is not calibrated. Leave-one-stratum-out sign changes are reported as a sensitivity diagnostic. A deterministic leave-one-unit-out check also reports how many deletions remain eligible, how many would violate the minimum group size, the largest absolute estimate shift, and whether any eligible deletion reverses the sign. When none are eligible, the shift and sign result are absent. This diagnoses sensitivity to individual observed units, not unmeasured confounding. The estimator also runs 128 deterministic within-stratum treatment-label shuffles and reports how many placebo contrasts are at least as large in absolute value as the observed contrast, plus the placebo median and maximum. This is an association robustness diagnostic; it is not a randomization-test p-value for observational assignment, and it cannot detect unmeasured confounding.

A dataset may additionally supply pre_treatment_outcome and pre_treatment_outcome_recorded_at for every unit. Partial coverage, nonfinite values, or timestamps outside the model window or after assignment return Unknown. When complete, the estimator applies the same strata and treatment groups to this earlier outcome and reports a standardized pre-treatment contrast with a descriptive normal interval. An interval excluding zero sets imbalance_flag, signaling possible selection or confounding before treatment; an interval containing zero does not establish exchangeability. The estimate remains observational_adjusted_unvalidated either way. Successful arithmetic does not validate interference, treatment consistency, or the completeness of the DAG. Older estimates without the current method and diagnostic version return Unknown until recomputed.

An optional post-assignment negative-control outcome requires a separate count or continuous variable in the approved model, no directed path from treatment to that variable in the reviewed graph, and a human Pass review tied to an active, exact-scope negative_control_protocol source and its content digest. A newer review supersedes the old one. The dataset pins the variable and review IDs and supplies a finite value and timestamp after assignment but within the model window for every unit. Missing, unreviewed, stale, or late observations return Unknown. The estimator reports the same stratum-standardized treated/control contrast and descriptive normal interval for this control. A nonzero interval flags possible residual bias or a violated exclusion assumption; a zero-containing interval does not prove that either assumption holds. Review replacement, source invalidation, erasure, or expiry clears numeric results that used that control. Reads recheck the current review, protocol and graph. The diagnostic does not correct the primary effect estimate or promote its identification state.

A dataset may also declare evaluation_cutoff before examining outcomes. Units with treatment assignment before the cutoff form training; later assignments form holdout. Each period must contain at least two treated and two controls in every original joint stratum, or estimation returns Unknown. Both period contrasts use the full dataset’s stratum weights so their absolute gap and sign change describe conditional-effect drift without silently switching population weights. This is a temporal stability diagnostic, not a calibrated interval, a causal refutation by itself, or a substitute for a separately held-out real cohort. The source digest is persisted with the estimate and rechecked on read; changing both source content and the artifact’s own digest invalidates an old estimate.

The source artifact and any effect derived from it are scoped. On source invalidation, deletion, or retention expiry, stored estimate values and diagnostics are cleared, along with the context_json of every linked claim and the rationale of every linked negative-control review — the same scrub erase_artifact performs, applied consistently on every path that removes a source rather than only on explicit erasure. Reads also recheck the model, assumptions, graph adjustment, source content digest, estimator code version, source visibility, and retention boundary. An assumption re-review, including pass followed by a new pass rationale, clears dependent numeric estimates. Reads independently compare the current six assumption-review IDs with the IDs bound into the estimate identity, so a stale effect cannot reappear if a row was not scrubbed.

For offline synthetic evaluation, duduclaw causal-effect-eval --db <path> --tenant <id> --acl <scope> --dataset <path> accepts a causal-effect-eval-v1 JSON manifest with truth_basis: "synthetic_dgp", a model ID, cutoff timestamp, and cases containing unique IDs, persisted estimate IDs, and known synthetic average effects. It obtains each estimate’s source timestamp, lineage, and content digest from the scoped store; both lineage IDs and content digests must be unique within each split and disjoint across training and held-out periods. A relabeled exact cohort copy therefore cannot increase the held-out case count, even if the original source has since been revoked and only its opaque digest remains. The evaluator scores only post-cutoff cases. It verifies active source bytes against their digest and checks every unit’s assignment/outcome time: training outcomes must finish by the cutoff, held-out assignments must start after it, and every outcome must precede its source export. A late export of old observations cannot qualify as held-out data. It reports unknown estimates separately from scored estimates, plus mean error, absolute error, RMSE, observed normal-interval coverage, and unit-deletion, temporal, pre-treatment, and negative-control diagnostic-flag counts. The interval_calibration field adds the nominal 95% target from the estimator’s 1.96-standard-error interval, scored/covered/below/above counts, mean width, and a descriptive 95% interval score: width plus 40 times any truth-to-boundary miss distance. It also gives a Wilson 95% band for observed coverage. Fewer than 20 scored held-out cases return insufficient_held_out_cases; otherwise a Wilson upper bound below 0.95 returns undercoverage_signal, and all other cases return inconclusive. An inconclusive result does not certify calibration. Unknown estimates are excluded from numeric interval scores, and invalid or overflowing bounds fail closed. The Wilson band descriptively treats cohorts as independent even though distinct source identities do not prove independence. A case may also supply expected_diagnostics with optional Boolean labels for those four checks. For each labeled check, the report separates true/false positives and negatives from unavailable diagnostics, gives sensitivity and specificity conditional on availability, and gives a positive detection rate that conservatively counts unavailable positive labels as missed. The synthetic observational demo labels its held-out unit-deletion, pre-treatment, and negative-control checks as expected stable; it does not label an absent temporal check. Persisted integration fixtures evaluate independent held-out cohorts with a known early negative/late positive treatment reversal, a pre-treatment imbalance, and one influential baseline shock. They verify these diagnostics as true positives, record the shock-induced effect error against its known synthetic treatment effect, and check that revoking a different held-out source moves its labels to unavailable while the remaining cohorts stay scored. The observational demo test also changes a held-out negative-control outcome after review to deliberately violate the synthetic exclusion rule; it verifies true-negative detection before the change and true-positive detection afterward. These tests validate warning mechanics, not the real-world exclusion assumption. The command cannot verify that a declared truth or diagnostic label came from the stated data-generating process or that lineage labels are authoritative; its output cannot establish real-data calibration.

For an active cohort export, the evaluator first rejects blank or duplicate unit IDs, then hashes the sorted multiset of complete measured rows with the local unit ID omitted. This conservatively rejects a second export whose lineage, unit IDs, row order, or JSON field order changed while its measurements and timestamps stayed identical. Two genuinely distinct cohorts with identical measured row multisets are also treated as nonindependent. If the original export has been erased, only its retained raw content digest is available; changed measurements or timestamps, partial cohort overlap, and real upstream cohort identity still need authoritative checks.

Work remaining before causal claims can inform operations

Section titled “Work remaining before causal claims can inform operations”
  • Cover remaining source-system migrations and cross-store revocation into decision snapshots and CCR handles; assess tenant-bound authorization before widening the admin API.
  • Connect lineage IDs to authoritative source systems and add a fuller contradiction-review workflow.
  • Extend the classical backdoor check to handle latent-variable assumptions and cases requiring generalized adjustment or other identification methods; compare decisions with established implementations.
  • Validate negative-control exclusion and shared-confounding assumptions against external evidence; add broader data-perturbation refutations, uncertainty calibration, externally held-out time-split evaluation, and comparison with established estimators. The optional reviewed negative control, same-outcome pre-treatment placebo, unit-deletion check, and within-dataset temporal holdout are initial diagnostics.
  • Keep any operational action behind existing human approval and policy checks.

duduclaw-gateway/src/causal_audit_ingest.rs (2026-09-29) is the first source on this line that is not an operator upload or a synthetic fixture. When a goal task settles as rejected or needs_human, two things already exist on disk: the deterministic fault rule token from fault_attribution and that round’s own tool_calls.jsonl rows. The module writes those exact bytes as a source artifact and files one candidate claim against them:

  • cause — the rule token that fired, refined to tool_error:<tool> when an audit row names a failing tool, and left as the bare token when none does. A tool name is never invented.
  • effect — task_rejected:<goal_kind> or task_escalated:<goal_kind>.
  • evidence — a byte span into the artifact pointing at the exact audit line, which replays to the original bytes.

This is deterministic transcription, not causal inference. Zero LLM calls, zero egress. What it records is “these bytes were written in the same round”, which is a timestamped co-occurrence. Becoming evidence still requires a person to accept the claim on /app/system/causal; nothing downstream may read an unreviewed candidate. The claim is born candidate and the review-state machine is unchanged.

Boundaries:

  • config.toml [causal] audit_ingest (default true, because the operation is local-only and costs nothing).
  • 50 claims per agent per UTC day, and deduplication on (agent, cause, effect, UTC day), both enforced inside one with_file_lock read-modify-write so two concurrent settles cannot each decide they are the first. A task failing the same way six times in a morning files one claim.
  • Scope is tenant_id = "local", acl = "audit", artifact kind duduclaw_tool_audit; the curation page labels that scope as audit-derived rather than synthetic.
  • Every failure path fails toward not writing: no audit rows, an unreadable store, a refused proposal all log at debug and return.

Consumer: the AEE causal_support dimension

Section titled “Consumer: the AEE causal_support dimension”

gvu/verifier_measure.rs’s MeasureVector gained causal_support: Option<f64> — the fraction of a candidate playbook entry’s signals_match tokens that a human-accepted audit-derived claim supports. Matching is exact and case-insensitive after trimming (a tool_error:<tool> cause also matches a signal naming the bare tool, since that is one fact written two ways); substring matching would make every token “supported”.

None means not measured — no accepted claim exists for that agent yet — and is deliberately different from a measured 0.0. The dimension is telemetry only: it is absent from the commit gate’s dimensions() map, so it can neither veto nor promote a candidate. A correlation transcribed from an audit log and blessed by one reviewer is not a quality measurement, and letting it move the commit gate would be the same “screened ⇒ promoted” slide this spec forbids elsewhere.

The one place it does change behaviour is opt-in: config.toml [evolution] require_causal_evidence (default false). With it on, a playbook Add that no accepted claim supports is committed carrying the existing shadow-rule tag — stored and scored by the v1.54 held-out gate, never injected — instead of entering the injection pool. The entry is kept, not rejected: “not yet evidenced” is a distinct state from “wrong”. With the key off, the write path is byte-identical to before.

The full work breakdown is in the TODO list. The backdoor standardization form and graph criterion follow DoWhy’s official estimation guide and its adjustment criterion example.