[NOTICKET] fix(knowledge_extraction): close the notation rules against real backend markup
Browse filesSofhia 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>
|
@@ -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
|
|
@@ -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 |
+
}
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
# Spacing, in every form LaTeX spells it. All equivalent to nothing here.
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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`
|
| 151 |
-
# variable: `\mathrm{x}100\%`
|
| 152 |
-
# the unwrap, which would
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|