Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ It mixes two formats:
The system prompt at `system-prompt.txt` explains how to read both
formats.

The digest may also end with a `# Prior Signal Ledger (advisory)`
section listing the previous iteration's named signals. When present,
preserve still-supported prior signals and record drops in
`dropped_signals` with `origin: "prior_baseline"` per the "Prior Signal
Ledger" rules in `system-prompt.txt`. Absence of the ledger is
ambiguous on its own (first-iteration vs. a rerun whose Stage 0 was
called without `--prior`); the orchestrator (`run-napkin-math-pipeline`)
disambiguates it via the Stage 1 preflight before invoking this skill.
When invoked standalone, treat absent-ledger as first-iteration only
when the caller has confirmed there is no prior accepted baseline for
the slug on disk; otherwise stop and ask.

Output schema and hard limits are identical to `extract-parameters-from-full`, so the
two skills can be compared head-to-head on the same plan.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ Wraps the quantitative-triage system prompt at `system-prompt.txt` (next to this

Not for: full report summarisation, narrative analysis, code generation. The system prompt explicitly forbids those.

The input may also end with a `# Prior Signal Ledger (advisory)` section
listing the previous iteration's named signals. When present, preserve
still-supported prior signals and record drops in `dropped_signals` with
`origin: "prior_baseline"` per the "Prior Signal Ledger" rules in
`system-prompt.txt`. Absence of the ledger is ambiguous on its own
(first-iteration vs. a rerun whose Stage 0 was called without
`--prior`); the orchestrator (`run-napkin-math-pipeline`) disambiguates
it via the Stage 1 preflight before invoking this skill. When invoked
standalone, treat absent-ledger as first-iteration only when the caller
has confirmed there is no prior accepted baseline for the slug on disk;
otherwise stop and ask.

## Workflow

1. **Get the report path.** If the user did not provide one, ask. Do not guess.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ paths and a one-line task; do not paste file contents into the prompt.

| Stage | How to dispatch |
|---|---|
| 0. Digest | `Bash` → `prepare_extract_input.py --planexe-dir <PlanExe-web/...> --output-dir <target>` |
| 0. Digest | `Bash` → `prepare_extract_input.py --planexe-dir <PlanExe-web/...> --output-dir <target> [--prior <prior-version>/<plan-slug>/parameters.json]` (see "Prior-baseline context for reruns" below) |
| 1. Parameters | `Agent` with the sibling skill name `extract-parameters-from-digest`; prompt: "Read `<target>/extract_parameters_input.md` per `system-prompt.txt`, write the result to `<target>/parameters.json`." |
| 2. Validation | `Bash` → `validate_parameters.py --parameters … --output …` |
| 3. Bounds | `Agent` with `generate-bounds`; prompt: "Read `<target>/parameters.json` per the generate-bounds rules, write `<target>/bounds.json`." |
Expand Down Expand Up @@ -95,6 +95,93 @@ to preserve. If the user wants a stage re-run, they will delete its
output file first; absence of the file is the signal to run, presence
is the signal to leave alone.

## Prior-baseline context for reruns

When a pipeline rerun produces a new version v(N) for a plan slug that
already has at least one earlier version on disk, Stage 0 MUST pass
`--prior <prior-version>/<plan-slug>/parameters.json` to
`prepare_extract_input.py`. The script appends a `# Prior Signal Ledger`
section to `extract_parameters_input.md` listing the prior iteration's
named signals (entry ids and output_names with their section,
formula_hint, and depends_on), and stamps a `Prior source:
\`<path>\`` line so the Stage 1 preflight (below) can confirm the
digest was built against the intended accepted baseline rather than an
arbitrary or probe prior. The extract LLM uses the ledger to decide
which prior signals to preserve and which to record in
`dropped_signals` with `origin: "prior_baseline"`. Without the ledger,
the LLM cannot emit prior-baseline-origin drops, and the
source-preservation audit cannot classify v(N-1) → v(N) signal loss.

**Selecting the prior.** The prior is an *accepted baseline* the user
names — typically the immediately-preceding integer-numbered version
(for a rerun at v59, the prior is `v58/<plan-slug>/parameters.json`;
for a comparison against v49, the prior is
`v49/<plan-slug>/parameters.json`). Letter-suffixed dirs (`v52a`,
`v53b`, `v57a_<topic>`, …) are experimental probes, not accepted
baselines; never auto-select one. If the user has not named the
baseline, ask — do not derive it from mtime or "most-recent earlier
version on disk."

**When to omit `--prior`.** First-iteration extractions only — the plan
slug has no earlier `parameters.json` on disk under any accepted
baseline. The script's `build_prior_signal_ledger` returns a
"first-iteration baseline" stub when handed an empty prior, which is
not the same as omitting `--prior` entirely; pass `--prior` only when a
real prior exists.

**Stage 1 preflight (mandatory).** Before invoking the extract skill,
the orchestrator MUST verify that the prior-baseline context is wired
correctly — regardless of whether Stage 0 just ran or the digest is
being resumed from a previous run:

1. Establish whether a prior accepted baseline exists for this slug
under `output/`. The user names it (or confirms a suggested
immediately-preceding integer version); if no accepted prior
exists, this is a first-iteration extraction.
2. If a prior exists, run **two** preflight checks on the digest.
Presence is necessary but not sufficient — a ledger built from the
wrong prior (e.g. a probe dir) still passes the presence check, so
also verify the source stamp:

```sh
# (a) Ledger present?
grep -q '# Prior Signal Ledger' $D/extract_parameters_input.md

# (b) Ledger built from the named accepted baseline?
# The ledger is stamped with `Prior source:
# output/<baseline>/<slug>/parameters.json` (relative to
# experiments/napkin_math/) when prepare_extract_input.py was
# given a prior under that tree, or with an absolute path
# otherwise. Match the baseline+slug suffix:
grep -q "Prior source:.*$BASELINE/$SLUG/parameters.json" \
$D/extract_parameters_input.md
```

- Both exit 0 → ledger present and built from the intended baseline
→ proceed to Stage 1.
- (a) non-zero (ledger absent) **or** (b) non-zero (ledger built
from a different prior) → STOP. Two acceptable resolutions
(offer both to the user, do not pick silently):
- **Regenerate Stage 0 with `--prior`.** Delete
`extract_parameters_input.md` (and the four `compress_*.md`
files if a fresh compression is also desired) and re-run Stage 0
with `--prior <baseline>/<plan-slug>/parameters.json`. The
pinned-digest cardinal rule does not apply here: the digest is
being repaired, not re-rolled for convenience.
- **Record an explicit waiver.** Write a one-line file
`$D/.no_prior_waiver` naming the reason the rerun is proceeding
without the prior ledger (e.g. "v(N-1) parameters.json was
lost; rebuilding without comparability"). The waiver file is
the *only* path that lets Stage 1 proceed with absent ledger
when a prior exists.

This closes the v58 failure mode: a digest produced without
`--prior` cannot enter Stage 1 unnoticed.

3. If no prior exists for the slug (first-iteration extraction),
record that explicitly: a missing `# Prior Signal Ledger` is
expected. No waiver file needed.

## The two cardinal rules

**1. Never copy a pipeline artifact from a different output directory
Expand Down Expand Up @@ -145,6 +232,12 @@ Two scenarios:
**(a) Fresh start from a PlanExe-web report.** User points at
`/Users/neoneye/git/PlanExe-web/<date>_<slug>/` and a target version.
Create `output/<version>/<slug>/` if it doesn't exist. Run stage 0 first.
If a prior accepted baseline exists for this slug under `output/`, ask
the user to name (or confirm) the accepted baseline — typically the
immediately-preceding integer-numbered version — and pass it as
`--prior` to Stage 0 per "Prior-baseline context for reruns". Never
auto-select a letter-suffixed dir (`v52a`, `v53b`, `v57a_<topic>`, …)
as the prior; those are experimental probes.

**(b) Resume from a partially populated output directory.** User points
at `output/<version>/<slug>/`. List the dir, classify which stages are
Expand Down Expand Up @@ -191,9 +284,16 @@ Stage 0 — digest preparation (creates the 8 compress files + the digest):
```sh
$PY $NM/prepare_extract_input.py \
--planexe-dir /Users/neoneye/git/PlanExe-web/<date>_<slug> \
--output-dir $NM/output/<version>/<plan-slug>
--output-dir $NM/output/<version>/<plan-slug> \
--prior $NM/output/<prior-version>/<plan-slug>/parameters.json
```

Omit `--prior` only on first-iteration extractions for a plan slug that
has no earlier `parameters.json` on disk. Otherwise the rerun produces a
digest without the `# Prior Signal Ledger` and the extract LLM cannot
record prior-baseline-origin drops. See "Prior-baseline context for
reruns" above.

Stage 2 — validation:

```sh
Expand Down Expand Up @@ -277,8 +377,8 @@ presence only — never by sibling-directory comparison.

| Present | First missing | Action |
|---|---|---|
| nothing | digest | If user gave a PlanExe-web dir, run Stage 0. If they only gave the output dir, ask for the source dir. |
| 8 compress files + digest | `parameters.json` | Run Stage 1. |
| nothing | digest | If user gave a PlanExe-web dir, run Stage 0. If they only gave the output dir, ask for the source dir. When a prior version exists for this slug under `output/`, pass `--prior` per "Prior-baseline context for reruns". |
| 8 compress files + digest | `parameters.json` | Run the Stage 1 preflight from "Prior-baseline context for reruns" (ledger present AND `Prior source:` matches the named baseline), then Stage 1. If either check fails, do not proceed — repair the digest or record an explicit waiver. |
| + `parameters.json` | `validation.json` | Run Stage 2. If validation reports errors, fix the parameters and re-validate before continuing. |
| + `validation.json` (valid) | `bounds.json` | Run Stage 3. |
| + `bounds.json` | `calculations.py` | Run Stage 4. |
Expand Down
69 changes: 59 additions & 10 deletions experiments/napkin_math/prepare_extract_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
Defaults to a sibling ``output/<planexe-dir-name>/`` directory next to this
script. Override with ``--output-dir`` or ``--llm``.

When rerunning the pipeline at version v(N) for a plan slug that already
has an earlier version on disk, pass ``--prior <prior parameters.json>``
to append a compact ``# Prior Signal Ledger`` to the combined digest.
That ledger is the wiring point for the source-preservation audit and
the extract LLM's ``dropped_signals`` rules; without it, a rerun cannot
emit prior-baseline-origin drops. See
``.claude/skills/run-napkin-math-pipeline/SKILL.md`` for orchestration.

PROMPT> python experiments/napkin_math/prepare_extract_input.py \\
--planexe-dir /Users/neoneye/git/PlanExe-web/20260215_nuuk_clay_workshop
"""
Expand Down Expand Up @@ -156,7 +164,24 @@ def run_compress(planexe_dir: Path, output_dir: Path, llm: str | None) -> None:
"""


def build_prior_signal_ledger(prior_params: dict) -> str:
def render_prior_source(prior_path: Path) -> str:
"""Render the prior path stably across worktrees. When the path is
under ``NAPKIN_MATH_DIR``, return the suffix (e.g.
``output/v58/<slug>/parameters.json``); otherwise return the
absolute path. The result is what gets stamped into the ledger and
what the orchestrator's Stage 1 preflight greps for to confirm the
digest was generated against the intended accepted baseline.
"""
resolved = prior_path.resolve()
try:
return str(resolved.relative_to(NAPKIN_MATH_DIR))
except ValueError:
return str(resolved)


def build_prior_signal_ledger(
prior_params: dict, prior_path: Path | None = None,
) -> str:
"""Build a compact markdown ledger listing the prior baseline's
named signals — entry ids and output_names across the five sections
that carry them. Intentionally narrow: no source_text, no labels,
Expand All @@ -168,6 +193,12 @@ def build_prior_signal_ledger(prior_params: dict) -> str:
structural relationships survive. Signals are deduplicated: a name
that appears as both an id and an output_name is listed once with
kind = ``id`` (the more authoritative reading).

When ``prior_path`` is supplied, a ``Prior source: `<path>``` line
is stamped between the header and the signal list. This lets the
orchestrator's Stage 1 preflight verify the ledger was built
against the named accepted baseline, not against an arbitrary or
probe prior.
"""
seen: dict[str, dict[str, Any]] = {}
for section in SECTIONS_WITH_IDS_FOR_LEDGER:
Expand Down Expand Up @@ -196,7 +227,11 @@ def build_prior_signal_ledger(prior_params: dict) -> str:
"formula_hint": entry.get("formula_hint"),
"depends_on": entry.get("depends_on") or [],
})
lines: list[str] = [PRIOR_LEDGER_HEADER.rstrip(), "", "## Signals", ""]
lines: list[str] = [PRIOR_LEDGER_HEADER.rstrip(), ""]
if prior_path is not None:
lines.append(f"Prior source: `{render_prior_source(prior_path)}`")
lines.append("")
lines.extend(["## Signals", ""])
for name in sorted(seen):
meta = seen[name]
lines.append(f"- `{name}` [{meta['section']}/{meta['kind']}]")
Expand All @@ -213,7 +248,9 @@ def build_prior_signal_ledger(prior_params: dict) -> str:


def build_combined_digest(
planexe_dir: Path, output_dir: Path, prior_params: dict | None = None,
planexe_dir: Path, output_dir: Path,
prior_params: dict | None = None,
prior_path: Path | None = None,
) -> Path:
"""Concatenate the 137-recommended extraction bundle, in 137's order, with
a legend at the top. Compressed sections come from ``output_dir/compress_*.md``;
Expand All @@ -228,6 +265,11 @@ def build_combined_digest(
intentionally narrow — names, sections, formula_hints and depends_on
only — so it acts as a preservation budget rather than a phrasing
target.

When ``prior_path`` is also supplied, the ledger is stamped with a
``Prior source: `<path>``` line so the orchestrator's Stage 1
preflight can verify the digest was built against the intended
accepted baseline.
"""
parts: list[str] = [LEGEND.rstrip(), "", "---", ""]
found_any = False
Expand Down Expand Up @@ -258,7 +300,9 @@ def build_combined_digest(
f"that the raw section files exist under {planexe_dir}."
)
if prior_params is not None:
parts.append(build_prior_signal_ledger(prior_params).rstrip())
parts.append(
build_prior_signal_ledger(prior_params, prior_path=prior_path).rstrip()
)
parts.append("")
combined = output_dir / "extract_parameters_input.md"
combined.write_text("\n".join(parts).rstrip() + "\n", encoding="utf-8")
Expand Down Expand Up @@ -320,18 +364,23 @@ def main() -> None:
print(f"LLM : {args.llm or '(run_compress_full default)'}\n")

prior_params: dict | None = None
prior_path: Path | None = None
if args.prior is not None:
prior_path: Path = args.prior.resolve()
if not prior_path.is_file():
raise SystemExit(f"--prior not found or not a file: {prior_path}")
resolved_prior: Path = args.prior.resolve()
if not resolved_prior.is_file():
raise SystemExit(f"--prior not found or not a file: {resolved_prior}")
try:
prior_params = json.loads(prior_path.read_text(encoding="utf-8"))
prior_params = json.loads(resolved_prior.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise SystemExit(f"--prior is not valid JSON: {exc}") from exc
print(f"Prior : {prior_path}\n")
print(f"Prior : {resolved_prior}\n")
prior_path = resolved_prior

run_compress(planexe_dir, output_dir, args.llm)
combined = build_combined_digest(planexe_dir, output_dir, prior_params=prior_params)
combined = build_combined_digest(
planexe_dir, output_dir,
prior_params=prior_params, prior_path=prior_path,
)

print(f"\nWrote combined digest: {combined}")
print("Feed this file to the extract-parameters-from-digest skill.")
Expand Down
Loading