Rifqi Hafizuddin Claude Opus 5 commited on
Commit
01c76dd
·
1 Parent(s): 1edf61c

[NOTICKET] fix(knowledge_extraction): close the notation rules against real backend markup

Browse files

Sofhia supplied the same 11 equations as MinerU transcribed them under four
backends -- 44 real strings, committed as a fixture. Running the shipped
normaliser over them reproduced her figures exactly: 11/11 for vlm/medium,
10/11 for hybrid/medium, 8/11 for pipeline, against hybrid/high as reference.
Her independent check and this code agree on both the counts and which
equations differ, which is the point of her having run it separately.

The remaining gaps were in the command inventory, and the inventory is now
evidence rather than guesswork.

\mathsf and \displaystyle occur in this corpus and were missing -- \mathsf sets
the whole of equation 7 under pipeline, including its multiplication sign, and
\displaystyle appears in equation 4. Both are handled now, and the wrapped-x
rule generalises across every presentational wrapper rather than being \mathrm
specific.

Equation 4 needed two more, and one of them was a decision Sofhia deliberately
left open. The array ALIGNMENT SPEC arrives as {l} from one backend and {r}
from another: presentational, dropped. A TRAILING row separator before
\end{array} is an empty row: cosmetic, dropped. A separator BETWEEN rows is
kept, because it is what distinguishes one array from another -- the same
structure-not-notation line the \frac handling already draws.

Also fixed a conflation the fixture exposed: the backslash-space rule was
eating one half of a `\` row separator that happened to be followed by a
space, so a structural marker was being half-deleted by a cosmetic rule. Now
guarded with a lookbehind.

Result: pipeline 8/11 -> 10/11, hybrid/medium 10/11 -> 11/11, vlm/medium
unchanged at 11/11.

The single residual is pipeline equation 1, `Production = MOHH x Qty x PA x UA
x Pty`. The operator is genuinely absent from the source, so it is the marker's
job and not the normaliser's -- reported unverified_operator, which is the
designed behaviour and is asserted by a test.

\mathbf, \mathit, \textrm and \operatorname were my guesses and never occur in
these 44 strings. Kept, because they are the same class of command and cost
nothing, but the comment now says plainly that their presence is not evidence.

5 fixture-driven tests added -- the only tests in this file whose inputs we did
not write ourselves. One of them asserts every backend keeps `frac(` in the PA
formula, so an X21-style silent division loss cannot come back unnoticed.

ruff clean, import main ok, full suite 529 passed 7 skipped (+5).

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

docs/knowledge/KNOWLEDGE_PIPELINE_CALIBRATION.md CHANGED
@@ -198,6 +198,27 @@ rejected, and there is a test for exactly that.
198
  `\mathrm{x}` **is** treated as a multiplication sign, before the wrapper unwrap that would destroy the
199
  distinction. That is grounded: `\mathrm{x}100\%` is the real PA formula's tail in this document.
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  **Which guarantee applied is recorded on the entry** (`latex_verification`, added 2026-08-26). Three
202
  paths leave `formula_latex` populated — located in the markup, no markup to check against, operator
203
  not recoverable — and they are not equally strong. Until this field existed the output could not tell
 
198
  `\mathrm{x}` **is** treated as a multiplication sign, before the wrapper unwrap that would destroy the
199
  distinction. That is grounded: `\mathrm{x}100\%` is the real PA formula's tail in this document.
200
 
201
+ **Measured against real markup, 2026-08-26.** Sofhia supplied the same 11 equations as MinerU
202
+ transcribed them under four backends (44 strings, committed at
203
+ `eval/knowledge/fixtures/latex_backends.json`). Agreement with `hybrid/high`, the production backend:
204
+
205
+ | Backend | Whitespace-only | Notation-aware | After the evidence-led additions |
206
+ |---|---|---|---|
207
+ | `vlm/medium` | 9/11 | **11/11** | 11/11 |
208
+ | `hybrid/medium` | — | 10/11 | **11/11** |
209
+ | `pipeline` | 5/11 | 8/11 | **10/11** |
210
+
211
+ The additions came from the inventory, not from guesswork: `\mathsf` (equation 7) and
212
+ `\displaystyle` (equation 4) occur in this corpus and were missing; the array **alignment spec**
213
+ (`{l}` from one backend, `{r}` from another) and a **trailing row separator** before `\end{array}`
214
+ are presentational. A separator BETWEEN rows stays structural. `\mathbf`, `\mathit`,
215
+ `\textrm` and `\operatorname` were guesses and **never occur** — kept because they are the
216
+ same class and cost nothing, but their presence is not evidence of anything.
217
+
218
+ The single residual, `pipeline` equation 1, is `Production = MOHH x Qty x PA x UA x Pty`: the
219
+ operator is genuinely absent from the source, so it is the marker's job, not the normaliser's. It is
220
+ reported `unverified_operator`, which is the designed behaviour.
221
+
222
  **Which guarantee applied is recorded on the entry** (`latex_verification`, added 2026-08-26). Three
223
  paths leave `formula_latex` populated — located in the markup, no markup to check against, operator
224
  not recoverable — and they are not equally strong. Until this field existed the output could not tell
eval/knowledge/fixtures/latex_backends.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "source_document": "STD_2026_006_MNO_Rev.0.0 - Production Parameter and ECA (9 pages)",
3
+ "note": "The SAME 11 equations as MinerU transcribed them under each backend. Raw Chunk.latex strings, verbatim, no cleanup. hybrid/high is the production backend; pipeline is what every artifact on Sofhia machine currently uses.",
4
+ "schema_version": "0.3.0",
5
+ "backends": {
6
+ "pipeline": [
7
+ "$$\nP r o d u c t i o n = M O H H x Q t y x P A x U A x P t y\n$$",
8
+ "$$\nQ t y = \\frac { I N P R H o u r s } { T o t a l H o u r s }\n$$",
9
+ "$$\nQ t y = \\frac { I N P R H o u r s } { T o t a l H o u r s }\n$$",
10
+ "$$\n\\begin{array} { c } { { Q t y = \\displaystyle \\frac { 3 6 0 } { 7 2 0 } } } \\\\ { { = 0 { , } 5 \\mathrm { { u n i t } } } } \\end{array}\n$$",
11
+ "$$\nQ t y _ { a c t i v i t y } = \\frac { I N P R H o u r s _ { a c t i v i t y } } { T o t a l H o u r s }\n$$",
12
+ "$$\n\\mathrm { P A } = { \\frac { \\mathrm { T o t a l } ~ H o u r s - B r e a k d o w n } { \\mathrm { T o t a l } ~ H o u r s } } \\mathrm { x } 1 0 0 \\%\n$$",
13
+ "$$\n\\mathsf { P A } _ { i j } = \\frac { \\sum _ { i j } \\mathsf { T o t a l } \\ H o u r s _ { i j } - \\sum _ { i j } B r e a k d o w n _ { i j } } { \\sum _ { i j } \\mathsf { T o t a l } \\ H o u r s _ { i j } } \\mathsf { x } \\ 1 0 0 \\%\n$$",
14
+ "$$\nU A = \\frac { W o r k i n g ~ H o u r s } { T o t a l ~ H o u r s - B r e a k d o w n }\n$$",
15
+ "$$\n\\mathrm { U A } _ { i j } = \\frac { \\sum _ { i j } \\mathrm { Q t y } _ { i j } \\mathrm { U A } _ { i j } } { \\sum _ { i j } \\mathrm { Q t y } _ { i j } }\n$$",
16
+ "$$\nP r o d u c t i v i t y = \\frac { P r o d u c t i o n } { E f f e c t i v e W o r k i n g H o u r s }\n$$",
17
+ "$$\n\\mathrm { P T Y } _ { i j } = \\frac { \\sum _ { i j } \\mathrm { Q t y } _ { i j } \\mathrm { P T Y } _ { i j } } { \\sum _ { i j } \\mathrm { Q t y } _ { i j } }\n$$"
18
+ ],
19
+ "vlm/medium": [
20
+ "$$\nP r o d u c t i o n = M O H H \\times Q t y \\times P A \\times U A \\times P t y\n$$",
21
+ "$$\nQ t y = \\frac {I N P R H o u r s}{T o t a l H o u r s}\n$$",
22
+ "$$\nQ t y = \\frac {I N P R H o u r s}{T o t a l H o u r s}\n$$",
23
+ "$$\n\\begin{array}{l} Q t y = \\frac {3 6 0}{7 2 0} \\\\ = 0, 5 \\text {unit} \\\\ \\end{array}\n$$",
24
+ "$$\nQ t y _ {a c t i v i t y} = \\frac {I N P R H o u r s _ {a c t i v i t y}}{T o t a l H o u r s}\n$$",
25
+ "$$\n\\mathrm{PA} = \\frac {\\text {Total Hours} - \\text {Breakdown}}{\\text {Total Hours}} \\times 100\\%\n$$",
26
+ "$$\n\\mathrm{PA} _ {i j} = \\frac {\\sum_ {i j} \\text {Total Hours} _ {i j} - \\sum_ {i j} \\text {Breakdown} _ {i j}}{\\sum_ {i j} \\text {Total Hours} _ {i j}} \\times 100\\%\n$$",
27
+ "$$\nU A = \\frac {\\text {Working Hours}}{\\text {Total Hours} - \\text {Breakdown}}\n$$",
28
+ "$$\n\\mathrm{UA} _ {i j} = \\frac {\\sum_ {i j} \\mathrm{Qty} _ {i j} \\mathrm{UA} _ {i j}}{\\sum_ {i j} \\mathrm{Qty} _ {i j}}\n$$",
29
+ "$$\n\\text {Productivity} = \\frac {\\text {Production}}{\\text {Effective Working Hours}}\n$$",
30
+ "$$\n\\mathrm{PTY} _ {i j} = \\frac {\\sum_ {i j} \\mathrm{Qty} _ {i j} \\mathrm{PTY} _ {i j}}{\\sum_ {i j} \\mathrm{Qty} _ {i j}}\n$$"
31
+ ],
32
+ "hybrid/high": [
33
+ "$$\nP r o d u c t i o n = M O H H \\times Q t y \\times P A \\times U A \\times P t y\n$$",
34
+ "$$\nQ t y = \\frac {I N P R H o u r s}{T o t a l H o u r s}\n$$",
35
+ "$$\nQ t y = \\frac {I N P R H o u r s}{T o t a l H o u r s}\n$$",
36
+ "$$\n\\begin{array}{l} Q t y = \\frac {3 6 0}{7 2 0} \\\\ = 0, 5 \\text {unit} \\\\ \\end{array}\n$$",
37
+ "$$\nQ t y _ {a c t i v i t y} = \\frac {I N P R H o u r s _ {a c t i v i t y}}{T o t a l H o u r s}\n$$",
38
+ "$$\n\\mathrm{PA} = \\frac {\\text {Total Hours} - \\text {Breakdown}}{\\text {Total Hours}} \\times 100\\%\n$$",
39
+ "$$\n\\mathrm{PA} _ {i j} = \\frac {\\sum_ {i j} \\text {Total Hours} _ {i j} - \\sum_ {i j} \\text {Breakdown} _ {i j}}{\\sum_ {i j} \\text {Total Hours} _ {i j}} \\times 100\\%\n$$",
40
+ "$$\nU A = \\frac {\\text {Working Hours}}{\\text {Total Hours} - \\text {Breakdown}}\n$$",
41
+ "$$\n\\mathrm{UA} _ {i j} = \\frac {\\sum_ {i j} \\mathrm{Qty} _ {i j} \\mathrm{UA} _ {i j}}{\\sum_ {i j} \\mathrm{Qty} _ {i j}}\n$$",
42
+ "$$\n\\text {Productivity} = \\frac {\\text {Production}}{\\text {Effective Working Hours}}\n$$",
43
+ "$$\n\\mathrm{PTY} _ {i j} = \\frac {\\sum_ {i j} \\mathrm{Qty} _ {i j} \\mathrm{PTY} _ {i j}}{\\sum_ {i j} \\mathrm{Qty} _ {i j}}\n$$"
44
+ ],
45
+ "hybrid/medium": [
46
+ "$$\nP r o d u c t i o n = M O H H \\times Q t y \\times P A \\times U A \\times P t y\n$$",
47
+ "$$\nQ t y = \\frac {I N P R H o u r s}{T o t a l H o u r s}\n$$",
48
+ "$$\nQ t y = \\frac {I N P R H o u r s}{T o t a l H o u r s}\n$$",
49
+ "$$\n\\begin{array}{r} Q t y = \\frac {3 6 0}{7 2 0} \\\\ = 0, 5 \\text {unit} \\end{array}\n$$",
50
+ "$$\nQ t y _ {a c t i v i t y} = \\frac {I N P R H o u r s _ {a c t i v i t y}}{T o t a l H o u r s}\n$$",
51
+ "$$\n\\mathrm{PA} = \\frac {\\text {Total Hours} - \\text {Breakdown}}{\\text {Total Hours}} \\times 100\\%\n$$",
52
+ "$$\n\\mathrm{PA} _ {i j} = \\frac {\\sum_ {i j} \\text {Total Hours} _ {i j} - \\sum_ {i j} \\text {Breakdown} _ {i j}}{\\sum_ {i j} \\text {Total Hours} _ {i j}} \\times 100 \\%\n$$",
53
+ "$$\nU A = \\frac {\\text {Working Hours}}{\\text {Total Hours} - \\text {Breakdown}}\n$$",
54
+ "$$\n\\mathrm{UA} _ {i j} = \\frac {\\sum_ {i j} \\mathrm{Qty} _ {i j} \\mathrm{UA} _ {i j}}{\\sum_ {i j} \\mathrm{Qty} _ {i j}}\n$$",
55
+ "$$\nP r o d u c t i v i t y = \\frac {P r o d u c t i o n}{E f f e c t i v e W o r k i n g H o u r s}\n$$",
56
+ "$$\n\\mathrm{PTY} _ {i j} = \\frac {\\sum_ {i j} \\mathrm{Qty} _ {i j} \\mathrm{PTY} _ {i j}}{\\sum_ {i j} \\mathrm{Qty} _ {i j}}\n$$"
57
+ ]
58
+ }
59
+ }
src/knowledge_extraction/validate/span_check.py CHANGED
@@ -48,10 +48,38 @@ def span_present(span: str, source: str) -> bool:
48
 
49
  # Commands that change how a token is SET, never what it means. Unwrapped to
50
  # their contents, so `\mathrm{Total}` and `Total` compare equal.
51
- _PRESENTATIONAL = ("mathrm", "mathbf", "mathit", "textrm", "text", "operatorname")
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  # Spacing, in every form LaTeX spells it. All equivalent to nothing here.
54
- _SPACING = re.compile(r"~|\\[,;:!]|\\quad\b|\\qquad\b|\\(?=\s)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
  def _unwrap_command(text: str, name: str) -> str:
@@ -147,13 +175,17 @@ def normalise_latex(text: str) -> str:
147
  # nothing until this has run. Getting this order wrong does not raise; it
148
  # silently skips every rewrite and compares raw markup.
149
  out = re.sub(r"\s+", "", out)
150
- # A roman `x` set on its own is this document's multiplication sign, not a
151
- # variable: `\mathrm{x}100\%` is the real PA formula's tail. Must precede
152
- # the unwrap, which would otherwise destroy the distinction.
153
- out = re.sub(r"\\mathrm\{[xX]\}", r"\\times", out)
 
 
 
 
154
  for name in _PRESENTATIONAL:
155
  out = _unwrap_command(out, name)
156
- out = out.replace("\\cdot", "\\times").replace("\\%", "%")
157
  out = _rewrite_fractions(out)
158
  return out.replace("{", "").replace("}", "")
159
 
 
48
 
49
  # Commands that change how a token is SET, never what it means. Unwrapped to
50
  # their contents, so `\mathrm{Total}` and `Total` compare equal.
51
+ #
52
+ # `mathrm`, `mathsf` and `text` are the three this corpus actually emits, across
53
+ # 44 real strings (11 equations x 4 backends). The rest are kept because they
54
+ # are the same class of command and cost nothing — but they are unobserved here,
55
+ # so do not treat their presence as evidence of anything.
56
+ _PRESENTATIONAL = (
57
+ "mathrm", "mathsf", "text", "mathbf", "mathit", "textrm", "operatorname"
58
+ )
59
+
60
+ # Standalone style commands: no argument, no meaning. `\displaystyle` is the one
61
+ # this corpus emits (pipeline, equation 4).
62
+ _BARE_STYLE = re.compile(r"\\displaystyle\b|\\limits\b|\\!")
63
 
64
  # Spacing, in every form LaTeX spells it. All equivalent to nothing here.
65
+ # The negative lookbehind matters: without it the `\ ` rule eats one half of a
66
+ # `\\` row separator that happens to be followed by a space, conflating a
67
+ # structural marker with a cosmetic one.
68
+ _SPACING = re.compile(r"~|\\[,;:]|\\quad\b|\\qquad\b|(?<!\\)\\(?=\s)")
69
+
70
+ # `\begin{array}{lcr}` — the alignment spec is presentational; the same array
71
+ # arrives as `{l}` from one backend and `{r}` from another.
72
+ _ARRAY_ALIGN = re.compile(r"(\\begin\{array\})\{[lcr|@\s]*\}")
73
+
74
+ # A row separator immediately before `\end{array}` is a trailing empty row.
75
+ # Separators BETWEEN rows are structural and are left alone.
76
+ _TRAILING_ROW = re.compile(r"(?:\\\\)+(?=\\end\{array\})")
77
+
78
+ # A wrapped roman `x` is this corpus's multiplication sign: the PA formula ends
79
+ # `\mathrm{x}100\%` under one backend and `\mathsf{x}100\%` under another.
80
+ _WRAPPED_TIMES = re.compile(
81
+ r"\\(?:" + "|".join(_PRESENTATIONAL) + r")\{[xX]\}"
82
+ )
83
 
84
 
85
  def _unwrap_command(text: str, name: str) -> str:
 
175
  # nothing until this has run. Getting this order wrong does not raise; it
176
  # silently skips every rewrite and compares raw markup.
177
  out = re.sub(r"\s+", "", out)
178
+ # A wrapped roman `x` is this document's multiplication sign, not a
179
+ # variable: the real PA formula ends `\mathrm{x}100\%` under one backend and
180
+ # `\mathsf{x}100\%` under another. Must precede the unwrap, which would
181
+ # otherwise destroy the distinction.
182
+ out = _WRAPPED_TIMES.sub(r"\\times", out)
183
+ out = _BARE_STYLE.sub("", out)
184
+ out = _ARRAY_ALIGN.sub(r"\1", out)
185
+ out = _TRAILING_ROW.sub("", out)
186
  for name in _PRESENTATIONAL:
187
  out = _unwrap_command(out, name)
188
+ out = out.replace("\\cdot", "\\times").replace("\\%", "%").replace("$", "")
189
  out = _rewrite_fractions(out)
190
  return out.replace("{", "").replace("}", "")
191