Skip to content

Commit 7656f27

Browse files
authored
Merge pull request #704 from PlanExeOrg/feat/napkin-math-prepare-extract-input
feat(napkin-math): add compressed-digest path for extract-parameters
2 parents bdbf140 + 68611f8 commit 7656f27

8 files changed

Lines changed: 1720 additions & 29 deletions

File tree

‎docs/proposals/137-section_filtering_for_parameter_extraction.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,3 +801,42 @@ Self Audit
801801
The main principle:
802802

803803
> Feed the extractor sections that contain assumptions, gates, risks, denominators, and missing evidence. Drop sections that mainly contain persuasion, UI, documentation planning, or duplicate narrative.
804+
805+
---
806+
807+
## Reference implementation
808+
809+
A first reference implementation of this section-filtering recommendation lives in `experiments/napkin_math/prepare_extract_input.py`. It assembles `extract_parameters_input.md` in 137's order, with the Strategic Decisions slot replaced by **Selected Scenario** per [proposal 139](139-compress-for-monte-carlo.md) to keep rejected alternatives out of the parameter extractor.
810+
811+
The bundle is mixed-format:
812+
813+
- The four sections 137 marks "Keep or compress" (Selected Scenario, Review Plan, Premortem, Expert Criticism) are compressed via `compress_report_section`. Each bullet carries an inline epistemic tag of the form `[<source_status> | e=N r=N | quote: verified|unverified]`.
814+
- The four sections marked plain "Keep" (Executive Summary, Project Plan, Assumptions, Data Collection) are passed through raw from the PlanExe sample. They are already short and primarily numeric; further compression risks dropping modelling primitives.
815+
816+
The companion skill `experiments/napkin_math/.claude/skills/extract-parameters-from-digest/` consumes the assembled file and produces a JSON parameter set with the same schema as the `extract-parameters` skill that reads the full PlanExe HTML. The two skills are head-to-head comparable on the same source plan.
817+
818+
---
819+
820+
## Lessons from the first reference implementation
821+
822+
A handful of failure modes recurred during head-to-head testing against the full-HTML pipeline. Recording them so the next implementation does not re-discover them.
823+
824+
### Budget and revenue are not the same denominator
825+
826+
When the report says "25% from rentals" or "40% Courses, 35% Memberships, 25% Drop-ins", that is a share of **revenue**, not of budget. An extractor that builds `rental_revenue = budget * rental_share` silently conflates spend capacity with sales target, and every downstream coverage and utilization ratio derived from that formula is distorted. The remedy is to surface `year1_revenue_target_dkk` (or the period equivalent) as a missing input whenever a revenue-mix share is captured without an explicit revenue target in the source. The same trap exists for cost-share, margin-share, contribution-share, and channel-share percentages.
827+
828+
### Cross-section duplication is the consumer's job to resolve
829+
830+
The four compressed sections routinely surface the same primitive ("minimum viable rental rate", "off-peak hourly price", "speculative high hourly rate") under different phrasings, because each compress call sees a different multi-file Luigi blob and is asked to be locally complete. Resist the urge to deduplicate inside the compressor — losing redundancy loses per-section provenance. The downstream extractor is where canonicalisation belongs: pick one stable snake_case id, merge near-duplicates, prefer the framing closest to a modelling primitive (rate, count, fraction, amount-per-period).
831+
832+
### Denominator-pairing is cheap and load-bearing
833+
834+
Every rate, share, conversion rate, hourly price, FTE count, and failure-duration magnitude in the digest needs a paired denominator or scaling input to be turned into an executable Monte Carlo formula. The compressor surfaces those denominators as `[missing]` items in the `missing_data_to_estimate` bucket — revenue target for a revenue-mix share, billable hours for an hourly rate, attendee count for a conversion rate, per-head cost for a headcount, per-period revenue exposure for a downtime duration. Without this rule the extractor invents the denominator, which makes the resulting distribution arbitrary.
835+
836+
### Strategic-Decisions content still arrives, just not as a standalone section
837+
838+
Substituting Selected Scenario for Strategic Decisions (per 139) does not lose the rejected-alternative content the planner considered. That content still flows into the compressor through the multi-file Luigi blobs that feed Review Plan, Premortem, and Expert Criticism — the compressor sees it as context but does not promote rejected alternatives into the numeric_values or gates of any section. The substitution is about provenance discipline, not information loss.
839+
840+
### Raw "Keep" sections are not free
841+
842+
The four raw sections inflate the digest by ~30 KB on the Nuuk sample (the compressed digest alone is ~21 KB; the full bundle is ~56 KB). That is the cost of preserving Executive Summary, Project Plan, Assumptions, and Data Collection at full fidelity. The savings claim ("instead of a 100 KB+ HTML report") still holds, but the extractor's prompt needs to make clear that raw sections carry no inline tags and require general triage — the format split is real, not cosmetic.

‎docs/proposals/139-compress-for-monte-carlo.md‎

Lines changed: 150 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -204,22 +204,29 @@ output directories use `COMPRESS_FULL_SAMPLE_DIR` and
204204

205205
## Status
206206

207-
The current bundle (`selected_scenario` + `review_plan` + `premortem` +
208-
`expert_criticism`) is graded usable for serious pipeline testing.
209-
Recent external review (ChatGPT) put all four files at **B+** overall:
207+
The compressor is now consumed by a first downstream reader: the
208+
`extract-parameters-from-digest` skill in
209+
`experiments/napkin_math/.claude/skills/`, driven by
210+
`experiments/napkin_math/prepare_extract_input.py` which assembles the
211+
137-recommended bundle (the four compressed sections + four raw
212+
"Keep" sections). External review of the v26 head-to-head run rated
213+
the bundle as the best version produced so far and accepted the
214+
resulting `parameters.json` after one prompt-side fix (budget vs
215+
revenue denominator, recorded below).
216+
217+
Per-section quality after the v26 iteration:
210218

211219
- Selected scenario: no rejected-alternative leakage; staffing-model
212-
description is unambiguous; missing-data items are primitive
220+
description unambiguous; missing-data items are primitives
213221
(electricity DKK/kWh, monthly shipping cost, FTE salary burden, etc.).
214222
- Review plan: clean executable gates with deadlines and numeric
215223
thresholds; consistent `if/then` form; baseline anchors clearly
216224
separated from `stress_test` shocks.
217225
- Premortem: stress magnitudes consistently tagged `stress_test`
218-
(reliably 6/6 on the risks bucket in repeated runs after the
219-
three-iteration robustness fix to the source_status definitions);
220-
baseline plan facts kept separate.
221-
- Expert criticism: useful quantified estimates with the right epistemic
222-
tags; mixed-language fragments eliminated.
226+
(reliably 6/6 on the risks bucket after the three-iteration
227+
source_status robustness fix); baseline plan facts kept separate.
228+
- Expert criticism: useful quantified estimates with the right
229+
epistemic tags; mixed-language fragments eliminated.
223230

224231
Per-section runtime is ~12–20 s for the six chained LLM calls. Each
225232
`numeric_values` call typically produces 6–8 candidates, of which 5–8
@@ -274,14 +281,34 @@ Danish/English hybrid sentences that earlier attempts produced are gone.
274281
threshold into `missing_data_to_estimate`.
275282
- Add a currency-consistency rule keyed off the source's dominant
276283
currency to catch the "dollar amount in a DKK plan" pattern.
277-
- Extend the scoring/tagging pattern to additional Luigi outputs
278-
(`data_collection`, `consolidate_assumptions`) if downstream extraction
279-
needs them as inputs.
280-
- Consider a dedup / merge step for risks and assumptions when a section
281-
paraphrases the same shock multiple times.
282284
- Optionally: a separate `expert_estimate` source_status for
283285
critic-proposed quantified impacts, distinguishing them from
284286
premortem-style `stress_test` numbers.
287+
- Investigate whether the section-by-section over-production rate
288+
(currently 8 candidates per bucket, top-6 kept) should adapt to the
289+
section's role — premortem might justify more risk candidates,
290+
selected_scenario fewer.
291+
- Evaluate a deterministic validator + cheap-model repair loop on
292+
`parameters.json` (the downstream consumer's output) rather than
293+
growing the extractor prompt with more invariant rules. The
294+
compressor's job is mostly done; the next wins are in the
295+
extract→validate→repair stage.
296+
297+
### Done (since first writing this proposal)
298+
299+
- Denominator-pairing rule added to the `missing_data_to_estimate`
300+
bucket prompt so every rate, share, conversion rate, FTE count, and
301+
failure-duration magnitude in earlier buckets gets its matching
302+
denominator surfaced as a `[missing]` item.
303+
- Number-word digit annotation in `line_english` (the LLM writes
304+
"four (4) part-time instructors") so the numeric-density scoring
305+
bonus picks up source phrases that state quantities as words.
306+
- Numeric-density bonus added to the composite ranking score
307+
(language- and domain-neutral, unlike the rejected
308+
`PROTECTED_MODELLING_TERMS` experiment).
309+
- Cross-section canonicalisation moved out of the compressor and into
310+
the downstream `extract-parameters-from-digest` skill prompt, where
311+
it belongs.
285312

286313
## Insights from iterating with reviewer feedback
287314

@@ -417,19 +444,125 @@ correct content or non-existent. A two-minute substring check or content
417444
inspection before making a prompt change saved chasing ghosts. Treat
418445
external feedback as a candidate signal, not a directive.
419446

447+
### Scoring code must stay language- and domain-neutral
448+
449+
A v24-era iteration added a `PROTECTED_MODELLING_TERMS` boost to the
450+
ranker: lines containing words like "budget", "contingency", "revenue",
451+
"labor", "rate", "break-even", "hourly" were given a score bonus on the
452+
theory that those concepts are modelling-relevant. The list was
453+
English-only and commercial-finance-biased. PlanExe inputs are
454+
multilingual — prompts arrive in Danish, French, German, Greenlandic —
455+
and domain-spanning: house renovations, hobby projects, public-health
456+
interventions, civic plans. A protected-term list silently downranks
457+
Danish phrasing and non-commercial plans without any signal in the
458+
output that the bias exists. The current ranker keeps only
459+
language- and domain-neutral signals: numeric token density (digits are
460+
digits in any language) plus the code-verified quote bonus. If lexical
461+
cues are useful, they belong in the prompt, where the LLM can apply
462+
them across languages — not in substring-match code.
463+
464+
### Annotate number-words in the prompt, not in code
465+
466+
A source phrase like "four part-time instructors" carries the same
467+
modelling signal as "4 part-time instructors", but a digit-only ranker
468+
misses the first form. The wrong fix is a multilingual number-word
469+
table in the ranker — that reintroduces the language-bias problem
470+
above. The right fix is a prompt rule: the compression LLM annotates
471+
count-words inline with their digit form (`four (4) part-time
472+
instructors`) in `line_english`, while keeping the native phrasing in
473+
`line_original`. The numeric-density bonus then picks up the digit
474+
form naturally, and the LLM handles the multilingual recognition
475+
where it has the training to do so. Vague quantifiers ("several", "a
476+
handful", "some") are explicitly excluded from the rule — the prompt
477+
forbids inventing digits for them.
478+
479+
### Pair every rate-like value with its missing denominator
480+
481+
For Monte Carlo, a rate is useful only when paired with the scaling
482+
input it operates on. The compressor over-produces on purpose: when
483+
earlier buckets capture a rate, share, per-unit price, conversion
484+
rate, FTE count, or failure-duration magnitude, the
485+
`missing_data_to_estimate` bucket emits the matching denominator —
486+
revenue target for a revenue-mix share, billable hours for an hourly
487+
rate, attendee count for a conversion rate, per-head cost for a
488+
headcount, per-period revenue exposure for a downtime duration. This
489+
rule produced one of the largest single improvements to downstream
490+
extraction quality after the multi-call architecture itself, because
491+
it stops the extractor from inventing arbitrary denominators.
492+
493+
### Canonicalisation belongs to the consumer, not the compressor
494+
495+
Each compressed section is fed a different multi-file Luigi blob and
496+
asked to be locally complete. As a result the four sections routinely
497+
surface the same primitive ("minimum viable rental rate", "off-peak
498+
hourly price", "speculative high hourly rate") under different
499+
phrasings. The redundancy is a feature: it gives the downstream
500+
consumer enough hooks to canonicalise correctly. Pushing
501+
canonicalisation upstream into the compressor would couple it to
502+
extractor schema decisions and lose the per-section provenance trail.
503+
The downstream `extract-parameters-from-digest` skill prompt carries
504+
the canonicalisation rule with concrete merge patterns ("collapse all
505+
hourly-rate phrasings to one `*_hourly_rate_dkk` id, drop the rest").
506+
507+
### Fix prompts, not outputs
508+
509+
When external review (ChatGPT and similar) flags a flaw in a generated
510+
artifact — a compressed digest, a `parameters.json` — the fix belongs
511+
in the prompt or code that produced it, not in the artifact itself.
512+
Hand-patching the output is a local one-shot fix; the underlying
513+
generator still has the bug, so the next run reproduces it. The cost
514+
of a real prompt fix is one edit plus a re-run; the cost of repeated
515+
hand-patches is unbounded. This applies equally to bucket-prompt
516+
issues in `compress_report_section.py` and to schema/canonicalisation
517+
issues in the downstream extract-skill prompt.
518+
519+
### Budget and revenue are not the same denominator
520+
521+
A v26-era extract bug computed `rental_revenue = budget * rental_share`
522+
because the prompt did not draw the distinction explicitly. The plan's
523+
25% rental share is a fraction of Year 1 *revenue*, not of Year 1
524+
budget — those are different quantities in any commercial plan.
525+
Without an explicit rule, the LLM substituted budget for revenue
526+
because budget was the only absolute amount available in the digest.
527+
The fix lives downstream in
528+
`extract-parameters-from-digest/system-prompt.txt` (the "Budget vs
529+
revenue denominator rule"), but the compressor should be aware: when
530+
a section quotes a revenue-mix percentage *and* a budget total, the
531+
compressor should not let the language collapse the two — keep
532+
budget and revenue framings distinct in `line_english` so downstream
533+
canonicalisation has clean signal to work from.
534+
420535
## Implementation
421536

422537
The compressor lives in `worker_plan/worker_plan_internal/parameter_extraction/`:
423538

424539
- `compress_report_section.py` — schemas, system and per-bucket prompts,
425540
six-call chained orchestration, code-side annotation and forced-status
426-
override, markdown render
541+
override, markdown render. The composite ranking score combines
542+
`source_evidence * modelling_relevance`, a code-verified quote bonus,
543+
and a language-neutral numeric-density bonus. The
544+
`missing_data_to_estimate` bucket prompt carries the denominator-pairing
545+
rule. The shared `line_english` prompt carries the number-word
546+
digit-annotation rule.
427547
- `run_compress_full.py` — driver that builds each job's multi-file blob
428548
in the same shape the corresponding Luigi task uses, then runs all four
429549
compress jobs
430550
- `tests/test_compress_report_section.py` — schema shape, section-type
431-
normalisation, markdown render, forced-status override, and
432-
English/original-language split
551+
normalisation, markdown render, forced-status override,
552+
English/original-language split, language-neutral numeric-density
553+
bonus, and composite-score head-to-head ordering
554+
555+
The first downstream consumer lives in
556+
`experiments/napkin_math/`:
557+
558+
- `prepare_extract_input.py` — assembles the 137-recommended 8-section
559+
bundle (four compressed + four raw), with Strategic Decisions
560+
replaced by Selected Scenario per this proposal
561+
- `.claude/skills/extract-parameters-from-digest/` — sibling of
562+
`extract-parameters` that reads the assembled bundle and emits a
563+
parameter-extraction JSON; system prompt carries the
564+
cross-section canonicalisation rule and the budget-vs-revenue
565+
denominator rule
433566

434567
Invoke as:
435568

0 commit comments

Comments
 (0)