|
| 1 | +--- |
| 2 | +title: "Source-Preservation Audit for the Napkin Math Pipeline" |
| 3 | +date: 2026-05-21 |
| 4 | +status: Proposal |
| 5 | +author: PlanExe Team |
| 6 | +--- |
| 7 | + |
| 8 | +# Source-Preservation Audit for the Napkin Math Pipeline |
| 9 | + |
| 10 | +**Author:** PlanExe Team |
| 11 | +**Date:** 2026-05-21 |
| 12 | +**Status:** Proposal |
| 13 | +**Tags:** `napkin-math`, `validation`, `audit`, `parameters`, `llm-output` |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Pitch |
| 18 | + |
| 19 | +Add a deterministic source-preservation audit between parameter extraction and validation so load-bearing source signals cannot silently disappear. Every threshold-like source claim or prior-baseline signal must either be carried forward into the current `parameters.json` or be recorded in `dropped_signals` with a mechanically checkable structural reason. |
| 20 | + |
| 21 | +## Problem |
| 22 | + |
| 23 | +The current extraction-stage discipline verifies that every declared `key_value` connects to a downstream calculation. It does not verify that every signal stated in the source or carried forward from the prior baseline survives into the current output. Silent drops pass the audit because the absent variable was never declared in the first place. |
| 24 | + |
| 25 | +Two failure modes motivated this proposal: |
| 26 | + |
| 27 | +1. **Source-stated threshold absent from output** — the source names a floor, cap, target, deadline, or pass/fail condition, but the extractor does not surface it in `key_values`, `missing_values_to_estimate`, a formula input, or `unmodelled_gates`. |
| 28 | +2. **Prior-baseline signal absent from current output** — a variable, calculation, or unmodelled gate present in a prior artifact disappears without an explicit replacement or rationale. |
| 29 | + |
| 30 | +Both modes are invisible to the existing no-dead-end-variable audit and to reporting that only describes what the current artifact contains. |
| 31 | + |
| 32 | +## Feasibility |
| 33 | + |
| 34 | +The audit is feasible as an advisory deterministic script because `parameters.json` already has stable IDs, labels, formulas, dependencies, and source anchors. The source-side scan is less precise: it must infer threshold-like claims from compressed digest text, so it will produce false positives and miss unusual phrasing. That is acceptable for an advisory first phase, but strict CI gating should wait until the matching fields and false-positive rate are proven on the full napkin_math corpus. |
| 35 | + |
| 36 | +The implementation should avoid adding plan-specific literals to prompts. Regression plans are probes only; tests should use synthetic fixtures for unit coverage and optionally run real corpus probes as non-normative integration checks. |
| 37 | + |
| 38 | +## Proposal |
| 39 | + |
| 40 | +Build two orthogonal audit forks that share one `dropped_signals` schema. |
| 41 | + |
| 42 | +### Fork A: Source Digest To Current Artifact |
| 43 | + |
| 44 | +Reads `extract_parameters_input.md` or the equivalent raw report input. It identifies threshold-like claims by structural pattern: numeric value plus language such as minimum, maximum, floor, ceiling, cap, target, deadline, must be at least, must not exceed, falls below, exceeds, or equivalent pass/fail phrasing. |
| 45 | + |
| 46 | +For each detected source claim, the audit computes a deterministic `source_claim_id`: |
| 47 | + |
| 48 | +```text |
| 49 | +source_claim_id = "claim_" + sha1(normalized_source_anchor + "\n" + normalized_claim_text)[:12] |
| 50 | +``` |
| 51 | + |
| 52 | +A source claim is considered preserved when at least one of these is true: |
| 53 | + |
| 54 | +1. A current artifact entry declares that claim in `source_claim_ids`. |
| 55 | +2. A current artifact entry has sufficient deterministic text overlap with the claim, using source anchor, numeric token, comparison token, and noun-token overlap. |
| 56 | +3. A `dropped_signals` entry records the same `source_claim_id` with an allowed structural reason. |
| 57 | + |
| 58 | +The explicit `source_claim_ids` field is the preferred long-term mechanism. Text overlap is a compatibility fallback for older outputs and should be reported as lower-confidence. |
| 59 | + |
| 60 | +### Fork B: Prior Baseline To Current Artifact |
| 61 | + |
| 62 | +Reads a prior `parameters.json` and the current `parameters.json`. It computes the prior signal set from every `id` and `output_name` across: |
| 63 | + |
| 64 | +- `key_values` |
| 65 | +- `missing_values_to_estimate` |
| 66 | +- `recommended_first_calculations` |
| 67 | +- `derived_questions` |
| 68 | +- `unmodelled_gates` |
| 69 | + |
| 70 | +A prior signal is considered preserved when at least one of these is true: |
| 71 | + |
| 72 | +1. The same `id` or `output_name` appears in the current artifact. |
| 73 | +2. A current formula depends on the prior signal name and the producer still exists under a compatible output name. |
| 74 | +3. A `dropped_signals` entry records the prior signal with `reason: "replaced_by"` or `reason: "redundant_with"` and points to an existing current ID. |
| 75 | +4. A `dropped_signals` entry records another allowed structural reason. |
| 76 | + |
| 77 | +Fork A protects against omissions that the prior baseline also missed. Fork B protects against regressions relative to a known earlier artifact. |
| 78 | + |
| 79 | +## Schema |
| 80 | + |
| 81 | +The extract-stage schemas gain two optional additions. |
| 82 | + |
| 83 | +First, any emitted entry may carry source-claim references: |
| 84 | + |
| 85 | +```jsonc |
| 86 | +"source_claim_ids": ["claim_ab12cd34ef56"] |
| 87 | +``` |
| 88 | + |
| 89 | +Second, the top-level artifact may include `dropped_signals`: |
| 90 | + |
| 91 | +```jsonc |
| 92 | +"dropped_signals": [ |
| 93 | + { |
| 94 | + "id": "prior_or_claim_id", |
| 95 | + "origin": "source_digest", |
| 96 | + "source_claim_id": "claim_ab12cd34ef56", |
| 97 | + "source_anchor": "review_plan", |
| 98 | + "expected_section": "key_values", |
| 99 | + "dropped_from": null, |
| 100 | + "reason": "replaced_by", |
| 101 | + "replacement_id": "current_signal_id", |
| 102 | + "redundant_with_id": null, |
| 103 | + "cap_kind": null, |
| 104 | + "rationale": "Equivalent threshold is represented by a clearer current margin input." |
| 105 | + } |
| 106 | +] |
| 107 | +``` |
| 108 | + |
| 109 | +Field semantics: |
| 110 | + |
| 111 | +- `id` is the prior signal ID for Fork B, or the `source_claim_id` for Fork A. |
| 112 | +- `origin` is one of `source_digest` or `prior_baseline`. |
| 113 | +- `source_claim_id` is required when `origin == "source_digest"`. |
| 114 | +- `source_anchor` names the source section when known; otherwise use `prior_baseline`. |
| 115 | +- `expected_section` is the section where the signal would normally land. |
| 116 | +- `dropped_from` is required only when `origin == "prior_baseline"` and names the prior section. |
| 117 | +- `reason` is one of `replaced_by`, `cap_pressure`, `out_of_scope`, `moved_to_unmodelled_gate`, or `redundant_with`. |
| 118 | +- `replacement_id` is required for `replaced_by` and must reference an existing current ID or output name. |
| 119 | +- `redundant_with_id` is required for `redundant_with` and must reference an existing current ID or output name. |
| 120 | +- `cap_kind` is required for `cap_pressure` and must name the capped array. |
| 121 | +- `rationale` is a one-sentence structural justification, capped at 25 words. |
| 122 | + |
| 123 | +Hard limit: at most 8 `dropped_signals`. If more than 8 signals must be dropped, the audit should surface an overflow finding instead of encouraging a long confession list. |
| 124 | + |
| 125 | +## Validation Rules |
| 126 | + |
| 127 | +The audit validates `dropped_signals` before trusting it: |
| 128 | + |
| 129 | +1. `reason` must be in the closed enum. |
| 130 | +2. `replacement_id` and `redundant_with_id` must reference existing current IDs or output names. |
| 131 | +3. `cap_pressure` must name a capped array in `cap_kind`, and that array must actually be at its cap in the current artifact. |
| 132 | +4. `moved_to_unmodelled_gate` must reference an existing `unmodelled_gates` entry through `replacement_id`. |
| 133 | +5. `source_claim_id` values must match the deterministic `claim_<12 hex>` shape. |
| 134 | +6. `rationale` must be non-empty, plan-neutral, and at most 25 words. |
| 135 | + |
| 136 | +Malformed `dropped_signals` entries are audit failures. They should not be accepted as explanations. |
| 137 | + |
| 138 | +## Prompt Rule |
| 139 | + |
| 140 | +The extract prompts should gain a corpus-agnostic source-preservation rule: |
| 141 | + |
| 142 | +```text |
| 143 | +Source preservation rule: |
| 144 | +
|
| 145 | +Every threshold-like source claim must either appear in the current |
| 146 | +artifact, be represented by a declared source_claim_id on a current |
| 147 | +entry, or be recorded in dropped_signals with a structural reason. |
| 148 | +Silent omission is not allowed. |
| 149 | +
|
| 150 | +When running an evaluation iteration with a prior baseline, every |
| 151 | +prior-baseline signal must either carry forward, be replaced by a |
| 152 | +current signal named in dropped_signals, or be recorded with another |
| 153 | +allowed structural reason. |
| 154 | +
|
| 155 | +Do not use dropped_signals to excuse weak extraction. Each entry must |
| 156 | +name a defensible structural reason and point to the current signal |
| 157 | +when the signal was replaced, made redundant, or moved to unmodelled |
| 158 | +gates. |
| 159 | +``` |
| 160 | + |
| 161 | +The prompt must not mention corpus plan names, literal values, expected output IDs, or domain-specific probe details. |
| 162 | + |
| 163 | +## Audit Script |
| 164 | + |
| 165 | +Add `experiments/napkin_math/audit_source_preservation.py`. No LLM call. |
| 166 | + |
| 167 | +Inputs: |
| 168 | + |
| 169 | +- `--digest` — path to `extract_parameters_input.md` |
| 170 | +- `--parameters` — path to the current `parameters.json` |
| 171 | +- `--prior` — optional path to the prior baseline `parameters.json` |
| 172 | +- `--report-json` — optional output path for a machine-readable report |
| 173 | +- `--strict` — exit non-zero on unjustified drops |
| 174 | + |
| 175 | +Behaviour: |
| 176 | + |
| 177 | +1. Parse and validate the current artifact shape. |
| 178 | +2. Scan the digest for threshold-like claims and compute `source_claim_id` values. |
| 179 | +3. Build the current signal index from IDs, output names, labels, source text, formulas, dependencies, unmodelled gates, and `source_claim_ids`. |
| 180 | +4. Run Fork A preservation checks. |
| 181 | +5. If `--prior` is present, build the prior signal set and run Fork B checks. |
| 182 | +6. Validate every `dropped_signals` explanation. |
| 183 | +7. Emit a human-readable report and, when requested, a JSON report. |
| 184 | + |
| 185 | +Exit code is 0 when clean, 1 when strict mode finds unjustified drops, and 2 for malformed input JSON. |
| 186 | + |
| 187 | +## Integration |
| 188 | + |
| 189 | +The audit runs after `extract-parameters-from-digest` or `extract-parameters-from-full` and before `validate-parameters`. |
| 190 | + |
| 191 | +Initial integration should be advisory: |
| 192 | + |
| 193 | +1. Manual invocation during prompt-development work. |
| 194 | +2. Optional step documented in `run-napkin-math-pipeline`. |
| 195 | +3. Later orchestrator integration that writes `audit_source_preservation.json` next to `parameters.json`. |
| 196 | +4. Strict mode only after false positives are measured and reduced across the corpus. |
| 197 | + |
| 198 | +The existing `validate_parameters.py` should stay focused on internal structural consistency. Source preservation is a separate audit because it needs external artifacts: the digest and optional prior baseline. |
| 199 | + |
| 200 | +## What This Proposal Does Not Do |
| 201 | + |
| 202 | +- It does not audit raw-source to compressed-digest preservation. Compression intentionally drops content, so that needs a separate design. |
| 203 | +- It does not solve compress-LLM run-to-run variance. |
| 204 | +- It does not make source preservation a CI gate in the first implementation. |
| 205 | +- It does not retro-edit existing gitignored outputs. |
| 206 | +- It does not require plan-specific prompt text or probe-specific rules. |
| 207 | + |
| 208 | +## Implementation Phases |
| 209 | + |
| 210 | +1. **Schema and docs** — document `source_claim_ids` and `dropped_signals` in both extract prompts and the napkin_math README. |
| 211 | +2. **Advisory script** — implement the audit with synthetic unit fixtures for Fork A, Fork B, and malformed `dropped_signals`. |
| 212 | +3. **Corpus probe run** — run against a subset of existing outputs and record false positives, false negatives, and useful catches. |
| 213 | +4. **Pipeline note** — document manual invocation in the orchestrator skill without changing orchestration behaviour. |
| 214 | +5. **Orchestrator integration** — write `audit_source_preservation.json` as a normal intermediate artifact. |
| 215 | +6. **Strict policy decision** — decide whether any subset of findings should become blocking. |
| 216 | + |
| 217 | +## Success Metrics |
| 218 | + |
| 219 | +- Synthetic tests catch an omitted source threshold, a dropped prior missing value, a dropped prior derived question, and a malformed replacement reference. |
| 220 | +- On corpus probes, every reported finding is classified as true positive, false positive, or accepted tradeoff. |
| 221 | +- The audit catches at least one silent drop that the current no-dead-end-variable audit misses. |
| 222 | +- False positives are low enough that reviewers can inspect them during prompt work without ignoring the report. |
| 223 | +- No corpus literals are introduced into extract prompts. |
| 224 | + |
| 225 | +## Risks |
| 226 | + |
| 227 | +- **False positives from regex scanning** — mitigate with advisory rollout, source anchors, numeric-token checks, and `source_claim_ids`. |
| 228 | +- **LLM overuses `dropped_signals`** — mitigate with hard caps, validation rules, and strict replacement references. |
| 229 | +- **Prior baseline was wrong or incomplete** — mitigate by treating Fork B as regression evidence, not ground truth. |
| 230 | +- **Schema bloat** — keep fields optional and local to napkin_math artifacts until the audit proves useful. |
| 231 | +- **Multilingual blind spots** — start with English structural patterns and add multilingual phrase tables only after measuring misses. |
| 232 | + |
| 233 | +## Acceptance |
| 234 | + |
| 235 | +- [ ] Proposal follows `docs/proposals/AGENTS.md` formatting rules. |
| 236 | +- [ ] `source_claim_ids` and `dropped_signals` are documented in both extract system prompts. |
| 237 | +- [ ] `experiments/napkin_math/README.md` documents the new optional fields and audit workflow. |
| 238 | +- [ ] `audit_source_preservation.py` lands under `experiments/napkin_math/`. |
| 239 | +- [ ] Unit tests cover source-claim detection, prior-baseline diffing, malformed `dropped_signals`, cap-pressure validation, and replacement-ID validation. |
| 240 | +- [ ] A synthetic regression fixture demonstrates a previously silent drop is caught. |
| 241 | +- [ ] A corpus probe report is produced without adding corpus literals to prompts. |
| 242 | + |
| 243 | +## Open Questions |
| 244 | + |
| 245 | +- Should `source_claim_ids` be required on new extract outputs once the field is introduced, or remain optional until the matching fallback has enough data? |
| 246 | +- Should strict mode ever apply to Fork A regex findings, or only to Fork B and explicit `source_claim_ids`? |
| 247 | +- Should `dropped_signals` be capped at 8 or 5? |
| 248 | +- Should the JSON report be consumed by Self-Improve as a prompt-optimization signal? |
0 commit comments