Rifqi Hafizuddin Claude Fable 5 commited on
Commit
1c207df
·
1 Parent(s): ec7d3d4

[NOTICKET] docs(knowledge): diagnose the rule-branch regression — three causes, one real

Browse files

The full run scored the rule branch 1/15 against the prototype's 4/15. Diagnosed
to three separate causes; only one is an actual quality loss. Written up in
KNOWLEDGE_PIPELINE_CALIBRATION.md §12, tracked as X19-X22.

(a) TWO OF THE THREE LOSSES ARE A SCORING ARTIFACT. The gold set's
statement_contains strings were written against PyMuPDF-era notation, which ran
subscripts together (QtyA, QtyP). Rendered LaTeX writes them with an underscore
(Qty_A). The scorer normalises punctuation but "_" is a WORD character, so
"qty_a" never contains "qtya" and the match fails on notation alone. Collapsing
"_" takes the score 1/15 -> 3/15. GAINLOSS_QTY_ZERO_PLAN and
QTY_ACTUAL_ZERO_PLAN_ALT are correct extractions scored as misses.

The true comparison is 4/15 -> 3/15: a loss of ONE rule, not three.

(b) THE REAL LOSS: rendered formulas now compete with prose. 4 of 8 v2 rule
statements are bare equations; in the prototype 0 of 7 were. Before rendering,
formulas reached the model as mangled character-spaced LaTeX and did not read as
statements; rendered cleanly they read like prescriptive rules, and rule.txt
never excludes them. The lost rule shows it exactly: chunk ::0015 contains BOTH
the units sentence (gold GAINLOSS_UNITS) and a Gain/Loss equation, was a cue
candidate, and the model returned the equation.

This is pure loss, because the FORMULA BRANCH ALREADY CAPTURES those
equations — "Qty = (I NPRHour s) / (T otalHour s)" appears in both branches
of the same run. The rule branch is duplicating the formula branch and
dropping the operational prose it exists for.

(c) A RENDERING BUG, owner Sofhia: render_latex eats the leading character of
each character-spaced run. "Q t y" -> "ty", "P r o d u c t i o n" ->
"roduction". Three of 31 chunks affected on the reference document, and it
carries into formula_latex. Corrupted formula text in Chunk.text also means a
span quoting it cannot be verified. Three-line reproduction in §12.

Nothing fixed here on purpose. The scorer and gold set are the frozen measuring
instrument (X22), rule.txt is a prompt change that owes an eval run per §7B
(X20), and render.py is the parsing half (X21).

Verification: docs only; ruff clean; import main OK; 497 passed, 7 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

KNOWLEDGE_PIPELINE_CALIBRATION.md CHANGED
@@ -305,3 +305,76 @@ blank lines and closing the block on two consecutive blanks.
305
  **The lesson generalises:** the two halves agree on the *contract*, and this still slipped through —
306
  a formatting choice that is completely reasonable on one side silently broke a regex on the other.
307
  Whitespace is part of the interface whether or not the schema says so.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  **The lesson generalises:** the two halves agree on the *contract*, and this still slipped through —
306
  a formatting choice that is completely reasonable on one side silently broke a regex on the other.
307
  Whitespace is part of the interface whether or not the schema says so.
308
+
309
+ ---
310
+
311
+ ## 12. Rule-branch regression — diagnosed (2026-08-24)
312
+
313
+ The full run scored the rule branch at **1/15**, against the prototype's 4/15. Diagnosed to three
314
+ separate causes, only one of which is an actual quality loss.
315
+
316
+ ### Cause 1 — two of the three losses are a scoring artifact, not a regression
317
+
318
+ The gold set's `statement_contains` strings were written against the PyMuPDF-era notation, which ran
319
+ subscripts together: `QtyA`, `QtyP`. The rendered LaTeX writes them with an underscore: `Qty_A`,
320
+ `Qty_P`. The scorer normalises punctuation but **`_` is a word character**, so `qty_a` never contains
321
+ `qtya` and the match fails on notation alone.
322
+
323
+ | Scoring | Rules matched |
324
+ |---|---|
325
+ | As committed | **1/15** |
326
+ | Collapsing `_` to nothing | **3/15** |
327
+
328
+ `GAINLOSS_QTY_ZERO_PLAN` and `QTY_ACTUAL_ZERO_PLAN_ALT` are both correct extractions scored as
329
+ misses. **The true comparison is 4/15 → 3/15, a loss of one rule.** Fixing this means changing either
330
+ the scorer or the gold set — both are the frozen measuring instrument, so it is Rifqi's call, not a
331
+ silent edit.
332
+
333
+ ### Cause 2 — the real loss: rendered formulas now compete with prose
334
+
335
+ **4 of 8 v2 rule statements are bare equations. In the prototype, 0 of 7 were.**
336
+
337
+ | Run | Formula statements | Prose statements |
338
+ |---|---|---|
339
+ | v2 | 4 | 4 |
340
+ | Prototype | 0 | 7 |
341
+
342
+ Before rendering, formulas reached the model as mangled character-spaced LaTeX and did not read as
343
+ statements. Rendered to clean prose they read exactly like prescriptive rules, and the model takes
344
+ them. `rule.txt` never excludes them — it defines a rule as *"a statement that constrains or directs
345
+ behaviour"*, which a formula arguably is.
346
+
347
+ The lost rule shows the mechanism exactly: chunk `::0015` contains **both** the units sentence
348
+ (*"Gain/Loss menggunakan satuan BCM atau ton…"* — gold `GAINLOSS_UNITS`) and a Gain/Loss equation.
349
+ The chunk **was** a cue candidate, so the model saw it, and returned the equation.
350
+
351
+ This is pure loss, because **the formula branch already captures those equations** — `Qty = (I
352
+ NPRHour s) / (T otalHour s)` appears in *both* branches of the same run. The rule branch is spending
353
+ calls duplicating the formula branch and dropping the operational prose it exists for.
354
+
355
+ Fix direction: `rule.txt` should exclude bare equations explicitly ("an equation is the formula
356
+ branch's job; a rule is prose that constrains behaviour"). That is a prompt change and therefore owes
357
+ an eval run per CLAUDE.md §7B — **not** to be done on vibes.
358
+
359
+ ### Cause 3 — a rendering bug: `render_latex` eats the first character
360
+
361
+ Reproducible in three lines against `src.knowledge_parsing.render`:
362
+
363
+ ```python
364
+ render_latex(r"$$ Q t y = \frac {I N P R H o u r s}{T o t a l H o u r s} $$")
365
+ # → 'ty = (I NPRHour s) / (T otalHour s)' expected: 'Qty = (INPRHours) / (TotalHours)'
366
+
367
+ render_latex(r"$$ P r o d u c t i o n = M O H H \times Q t y $$")
368
+ # → 'roduction = MOHH × Qty' expected: 'Production = MOHH × Qty'
369
+ ```
370
+
371
+ The character-spacing collapse consumes the **leading letter** of each spaced run, and re-splits the
372
+ remainder in the wrong place. Present in **3 of 31 chunks** of the reference document (`I NPRHour`,
373
+ `T otalHour`, `P roductio`, `E ffectiveWorkingHour`) and visible in the committed formula entries.
374
+
375
+ Consequences: formula text in `Chunk.text` is corrupted, so a span quoting it is unverifiable; terms
376
+ that occur *only* inside a formula lose their first letter and can be missed by the term filter; and
377
+ `formula_latex` values carry the damage into the Skill Registry.
378
+
379
+ **Owner: Sofhia** — `src/knowledge_parsing/render.py`. Not fixed here: it is the parsing half, and
380
+ the reproduction above is enough to act on.
KNOWLEDGE_PIPELINE_TODO.md CHANGED
@@ -167,7 +167,10 @@ envelope shape proposed in §3 — so S1 can settle either way without touching
167
  | **X11** | Verbatim-span validation | ✅ **1.00** | ✅ | `validate/span_check.py`. Whitespace-only normalisation; unlocatable provenance rejects every guarded field; never repairs. **Extended:** `full_name`/`source_wording` are checked against the source themselves, so a silent normalisation is caught rather than stored |
168
  | **X12** | Null-definition escalation | 🔎 | ✅ | `validate/escalate.py` + the glossary loop. Still **unexercised on this document** — most clusters have too little evidence to escalate to, which is a property of a 9-page corpus, not a defect |
169
  | **X18** | Full-document run through both halves | Rifqi | ✅ | **Done 2026-08-24** → `eval/knowledge/results/v2_full_document_2026-08-24_093051.json`. Parsing module built the artifact (31 chunks), extraction ran all four branches: 77 calls, 80% cache hit. **E3 = 0.90, PASS** — the prototype's failing experiment (0.75 vs a 0.80 line) now clears it, on a 10-entry scoreable base |
170
- | **X19** | Rule-branch recall regression | Rifqi | ⛔ | **Found 2026-08-24: 1/15, down from the prototype's 4/15.** The cue filter proposed 8 candidate passages vs 9, so the loss is downstream of candidate generation — not diagnosed. The rule branch also runs on nano, the tier its failure mode is least detectable on |
 
 
 
171
  | **X13** | Conflict detection | 🔎 | ✅ | `validate/conflict.py`, token overlap (explainable to the reviewer, unlike embeddings); never picks a winner. Unit-tested both ways; still **0 conflicts on real data** — one consistent standard gives it nothing to find. Works only because clustering puts all evidence in one call |
172
  | **X14** | Diff vs. active glossary version | 🔎 | ✅ | `diff/glossary_diff.py`. The baseline is passed in explicitly (`--active-glossary`) rather than read from wherever the last run wrote, so the duplicate and conflicting paths are reachable — all three verified by test |
173
  | **X15** | Frequency-sorted review queue | ✅ | ✅ | `queue/review_queue.py`. Each row carries page, section and the verbatim span so review is quote-vs-page. Gained a review reason for **wording discrepancies**, which is how the literal-wording decision reaches the expert |
 
167
  | **X11** | Verbatim-span validation | ✅ **1.00** | ✅ | `validate/span_check.py`. Whitespace-only normalisation; unlocatable provenance rejects every guarded field; never repairs. **Extended:** `full_name`/`source_wording` are checked against the source themselves, so a silent normalisation is caught rather than stored |
168
  | **X12** | Null-definition escalation | 🔎 | ✅ | `validate/escalate.py` + the glossary loop. Still **unexercised on this document** — most clusters have too little evidence to escalate to, which is a property of a 9-page corpus, not a defect |
169
  | **X18** | Full-document run through both halves | Rifqi | ✅ | **Done 2026-08-24** → `eval/knowledge/results/v2_full_document_2026-08-24_093051.json`. Parsing module built the artifact (31 chunks), extraction ran all four branches: 77 calls, 80% cache hit. **E3 = 0.90, PASS** — the prototype's failing experiment (0.75 vs a 0.80 line) now clears it, on a 10-entry scoreable base |
170
+ | **X19** | Rule-branch recall regression | Rifqi | 🔎 | **Diagnosed 2026-08-24 — calibration §12.** Three causes: (a) 2 of 3 losses are a *scoring artifact* (gold written `QtyA`, rendered output `Qty_A`; `_` is a word char so the substring match fails) — true comparison is 4/15 → 3/15; (b) the one real loss is rendered formulas competing with prose — 4 of 8 v2 rule statements are bare equations vs 0 of 7 in the prototype, and the formula branch already captures them; (c) a `render_latex` bug (X21) |
171
+ | **X20** | Exclude bare equations in `rule.txt` | Rifqi | ⬜ | The fix for X19(b). A prompt change → owes an eval run per §7B; do not ship on vibes. The formula branch already covers equations, so this is duplicated spend as well as a miss |
172
+ | **X21** | `render_latex` eats the leading character | Sofhia | ⛔ | **Found 2026-08-24.** `Q t y` → `ty`, `P r o d u c t i o n` → `roduction`. 3 of 31 chunks affected on the reference document; corrupts formula text in `Chunk.text` (so spans quoting it are unverifiable) and carries into `formula_latex`. Three-line reproduction in calibration §12 |
173
+ | **X22** | Gold/scorer notation decision | Rifqi | ⬜ | Whether to normalise `_` in the scorer or rewrite the gold's `statement_contains`. Either edits the frozen measuring instrument, so it needs a deliberate call rather than a silent fix |
174
  | **X13** | Conflict detection | 🔎 | ✅ | `validate/conflict.py`, token overlap (explainable to the reviewer, unlike embeddings); never picks a winner. Unit-tested both ways; still **0 conflicts on real data** — one consistent standard gives it nothing to find. Works only because clustering puts all evidence in one call |
175
  | **X14** | Diff vs. active glossary version | 🔎 | ✅ | `diff/glossary_diff.py`. The baseline is passed in explicitly (`--active-glossary`) rather than read from wherever the last run wrote, so the duplicate and conflicting paths are reachable — all three verified by test |
176
  | **X15** | Frequency-sorted review queue | ✅ | ✅ | `queue/review_queue.py`. Each row carries page, section and the verbatim span so review is quote-vs-page. Gained a review reason for **wording discrepancies**, which is how the literal-wording decision reaches the expert |