feat(propose,cli): the coverage gap had one cause, and round 7's own decomposition did not reproduce
Round 7 named two open items: a table-block candidate displacing a declared
heading (26 041 characters between entries, `md` at 3 of 4 declared headings)
and 17 590 characters after the last entry, never examined. Measured on
`a364ef4`, the first premise does not reproduce and the second is not a
separate mechanism.
`md` recovers 4 of 4, not 3 of 4. D1 (`--keep-table-heading`) is the repair
for exactly the mechanism round 7 § 5 describes, and round 7 moved it into
the default in the same commit -- so § 5 is a pre-move measurement presented
as the post-move state. That is round 7's own trap, stated in its own report:
a number about a rule is a measurement of a configuration. Nothing held the
cell, which is why it could be wrong and stay wrong.
The remaining gap has ONE cause. Every rule closes a span against the NEXT
MARK; three steps then remove a mark after its neighbour's `end` was fixed
against it. The orphan check leaks 18 527 characters over 15 of 39 documents,
`fold_units` clause 1 leaks 7 514 between entries, and the same clause on the
last run leaks all 17 590 tail characters -- with `unit_fold=False` the corpus
tail gap is 0. Round 6 already established the principle (filter at admission,
let the mark above carry the text) and it was never applied post-filter.
`--close-span-gaps` states it once, after the fold, where every removal has
happened: a span runs to the next SURVIVOR, the last to the end of the text.
It adds no boundary. Measured: 43 631 characters (2.51 %) -> 0, entries
429 = 429, K2 concepts 436 = 436, the operator's folder 52 md = 52.
All four acceptance conditions hold at once. The 12-position reference is
label-identical -- 11 of 12 under |F|[3]=12 and 10 of 12 under |F|[3]=11,
both readings reported, not one position moved. hit@8 holds [1,1,1,1,1,-] on
the round 8 bundle (436), the round 7 default (436) and Arm B (629), and the
known-negative reproduces on the new bytes: `--no-tie-shared-rank` gives
[2,1,1,1,1,-].
The pin is rebuilt against the default that actually ships
(`K2-bundle-default-20260911`, 832 files, digest 8c93e5e3...); two independent
builds of it differ in nothing, `log.md` included.
Two measurement failures are recorded rather than hidden. The corpus identity
check first used `xargs` without `-print0`, every filename split on its
spaces, `shasum` read nothing, and both sides agreed on a digest of the
failure; a control against the digest of empty input separated them. And the
smoke build cannot prove the flag arrives -- the operator's folder has a
coverage gap of zero already, so `diff -rq` is 0 either way; the plumbing has
its own test on a document that has the defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a364ef4c7d
commit
956714594d
8 changed files with 592 additions and 22 deletions
|
|
@ -163,6 +163,13 @@ DEFAULT_DROP_WRAPPED_OUTLINE = True
|
|||
#: `docx`, 10 of 12) AND hit@8 holds rank 1 on every row it held, measured end
|
||||
#: to end on a K2 bundle built with it. Opt-out `--no-first-span-from-zero`.
|
||||
DEFAULT_FIRST_SPAN_FROM_ZERO = True
|
||||
#: Round 8. The remaining coverage gap round 7 named and left open -- 43 631
|
||||
#: characters, 2.51 % of the corpus over 8 of 32 documents with a plan -- has
|
||||
#: ONE cause: three steps remove a candidate AFTER its neighbour's span was
|
||||
#: already closed against it, so the removed mark's text is in no segment. The
|
||||
#: rule closes each span against the next SURVIVOR. It adds no boundary, so the
|
||||
#: concept count is unchanged and the cost to a consumer is a re-run.
|
||||
DEFAULT_CLOSE_SPAN_GAPS = True
|
||||
|
||||
#: Round 3's two spreadsheet rules (D1 and D3), held back through rounds 5 and
|
||||
#: 6 by a RETRIEVAL regression rather than by the reference: they take the
|
||||
|
|
@ -228,6 +235,7 @@ def _propose_plans(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
pdf_headings: bool = False,
|
||||
pdf_headings_reserve: bool = False,
|
||||
ocr: bool = False,
|
||||
|
|
@ -263,6 +271,7 @@ def _propose_plans(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
pdf_headings=pdf_headings,
|
||||
pdf_headings_reserve=pdf_headings_reserve,
|
||||
ocr=ocr,
|
||||
|
|
@ -297,6 +306,7 @@ def build(
|
|||
drop_wrapped_outline: bool = DEFAULT_DROP_WRAPPED_OUTLINE,
|
||||
outline_gate: bool = DEFAULT_OUTLINE_GATE,
|
||||
first_span_from_zero: bool = DEFAULT_FIRST_SPAN_FROM_ZERO,
|
||||
close_span_gaps: bool = DEFAULT_CLOSE_SPAN_GAPS,
|
||||
pdf_headings: bool = DEFAULT_PDF_HEADINGS,
|
||||
pdf_headings_reserve: bool = DEFAULT_PDF_HEADINGS_RESERVE,
|
||||
ocr: bool = DEFAULT_OCR,
|
||||
|
|
@ -366,6 +376,7 @@ def build(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
pdf_headings=pdf_headings,
|
||||
pdf_headings_reserve=pdf_headings_reserve,
|
||||
ocr=ocr,
|
||||
|
|
@ -649,6 +660,30 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|||
"pre-2026-09-10 first span byte for byte"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--close-span-gaps",
|
||||
action="store_true",
|
||||
default=DEFAULT_CLOSE_SPAN_GAPS,
|
||||
help=(
|
||||
"Close a concept's span against the next SURVIVING concept, and "
|
||||
"the last one against the end of the text, so a candidate removed "
|
||||
"after its neighbour's span was fixed does not take its text out "
|
||||
"of the plan. Measured over the 39-document corpus: it closes the "
|
||||
"43 631 characters -- 2.51 %% of the corpus over 8 of the 32 "
|
||||
"documents with a plan -- that round 7 left, both the 26 041 "
|
||||
"between entries and the 17 590 after the last. It adds no "
|
||||
"boundary and moves only spans' ends. ON since 2026-09-11"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--no-close-span-gaps",
|
||||
action="store_false",
|
||||
dest="close_span_gaps",
|
||||
help=(
|
||||
"The repair's explicit opt-out, for a consumer who needs the "
|
||||
"pre-2026-09-11 spans byte for byte"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--pdf-headings",
|
||||
choices=("none", "font", "font-reserve"),
|
||||
|
|
@ -721,6 +756,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
drop_wrapped_outline=args.drop_wrapped_outline,
|
||||
outline_gate=args.outline_gate,
|
||||
first_span_from_zero=args.first_span_from_zero,
|
||||
close_span_gaps=args.close_span_gaps,
|
||||
pdf_headings=args.pdf_headings == "font",
|
||||
pdf_headings_reserve=args.pdf_headings == "font-reserve",
|
||||
ocr=args.ocr,
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ def find_candidates(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
) -> list[Candidate]:
|
||||
"""Every boundary the mechanical rules propose, in document order.
|
||||
|
||||
|
|
@ -549,6 +550,16 @@ def find_candidates(
|
|||
It is its own flag and not part of an arm because the orphan check is
|
||||
reached by every file type, and moving it is a decision about all of them.
|
||||
|
||||
`close_span_gaps` is OFF at False, where a mark removed after its
|
||||
neighbour's span was closed takes that text out of the plan entirely. On,
|
||||
a span runs to the next SURVIVING candidate's start and the last one runs
|
||||
to the end of the text. It adds no boundary and removes none -- only `end`
|
||||
moves -- so a plan's entry count is identical either way. Three steps
|
||||
remove marks late and all three leak: the orphan check (18 527 characters
|
||||
over 15 of 39 documents), `fold_units` clause 1 between entries (7 514),
|
||||
and the same clause on the LAST run (all 17 590 tail characters; with
|
||||
`unit_fold=False` the corpus tail gap is 0).
|
||||
|
||||
`first_span_from_zero` is OFF at False, where the text above the first
|
||||
concept belongs to no segment. On, the first surviving concept starts at 0.
|
||||
It adds no boundary and removes none; only the first span's `start` moves.
|
||||
|
|
@ -813,6 +824,19 @@ def find_candidates(
|
|||
# opens at 0: the fold can discard a contents run, and moving the start
|
||||
# before it would put the preamble on an entry the plan never carries.
|
||||
resolved = [replace(resolved[0], start=0), *resolved[1:]]
|
||||
if close_span_gaps and resolved:
|
||||
# LAST, and after the fold for the same reason: every step that can
|
||||
# REMOVE a mark has already run, so this is the one place where the
|
||||
# surviving list is final. Each span is closed against the next
|
||||
# SURVIVOR rather than against the next mark, which is what the
|
||||
# removing steps could not do -- they fixed a neighbour's `end`
|
||||
# against a candidate that had not been judged yet.
|
||||
resolved = [
|
||||
replace(candidate, end=max(candidate.end, next_start))
|
||||
for candidate, next_start in zip(
|
||||
resolved, [c.start for c in resolved[1:]] + [end_of_text], strict=True
|
||||
)
|
||||
]
|
||||
return resolved
|
||||
|
||||
|
||||
|
|
@ -1146,6 +1170,7 @@ def build_plan(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""The artifact. Every entry PROPOSED, the plan itself never adjudicated."""
|
||||
taken: set[str] = set()
|
||||
|
|
@ -1161,6 +1186,7 @@ def build_plan(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
)
|
||||
for candidate in subdivide(text, candidates, max_segment_chars):
|
||||
entries.append(
|
||||
|
|
@ -1236,6 +1262,7 @@ def run(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
pdf_headings: bool = False,
|
||||
pdf_headings_reserve: bool = False,
|
||||
ocr: bool = False,
|
||||
|
|
@ -1310,6 +1337,7 @@ def run(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
)
|
||||
# Nothing to propose is an OUTCOME, and it is not an artifact. An empty
|
||||
# plan cannot be replayed -- `process_inbox` refuses one, because a plan
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue