feat(build): name the documents the gate refused whole, and close as 1.0.0
Measured 2026-09-20 on an official documentation corpus of 594 sources built with the shipped default gate `guard-trusted-source`: 17 sources were refused OUTRIGHT -- `fail_secure` 3, `quarantine_review` 14 -- and 16 of them were among 197 official documentation pages, the pages on hooks, skills, permissions, errors, env-vars and authentication among them. The summary said only `fail_secure`: 3/594. Three of the four facts a reader needs were missing: the COUNT of documents the gate dropped (the existing `rejected (coded)` line sums gate refusals and extraction failures, two failures with two different remedies), the NAMES, and the way out. Rebuilt with `--gate none`, all 17 went through untouched, so the refusal is the gate and not the readers. `okf build` now prints a `Documents the gate refused WHOLE` section directly under the denominator, carrying all four: the count with its denominator, the names capped at ten with the rest in the bundle's `log.md`, the codes, and `--gate none` for a source you vouch for yourself. The same fact goes to stderr in one line, built from the same field, because `okf build > report.txt` is an ordinary thing to do. `log.md` gains one bullet naming every refused document, uncapped. The exit code deliberately does not move. The build is valid -- every refusal is coded, the conservation identity holds, and the bundle is a true record of what the gate allowed. What was wrong was the silence. A run the gate refused nothing from is byte-identical in both places, which is the known-negative in the new suite: no bundle this repository ships was built with a gate refusal, so this cannot have moved a byte measured here. Also, and measuring nothing new: - README gains `Known limitations` high up -- the gate's refusals and the way out, the absent ceiling on what one run pays for images (a 70 KB PDF with 16 images under the declared limit reached 851 MB peak RSS; RLIMIT_AS is not enforceable on this platform, so the 512 MiB per-link budget is the whole bound), the three gates of this repository that are RED today (retrieval 5/7/8/9, MCP 2, accounting 2/3/6 -- all three re-run on this commit), what the content accounting does not count, and the rough edges nothing is planned for. - The two `pip install` lines under "Install in detail" install `[extract]`. The first screen does; those two did not, so the two recipes produced different installations and the detailed one reports `resolved converter path: unresolved (extractor_extra_missing)`. - Version `1.0.0`, synced across pyproject, `__version__`, `uv.lock`, the four README install lines, the install prose, the current-tag entry and the CHANGELOG, where the two "after the 0.10.1 notes were written, untagged" sections are folded in. It adds no capability over `v0.10.1`; what it adds is that the tool says what it does not do. Suite: 2325 passed, 2 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
16ec574687
commit
404bed26e2
10 changed files with 512 additions and 69 deletions
|
|
@ -76,7 +76,7 @@ from .manifest import (
|
|||
)
|
||||
from .materialize import IngestResult, materialize_bundle
|
||||
|
||||
__version__ = "0.10.1"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
__all__ = [
|
||||
"BlockedFile",
|
||||
|
|
|
|||
|
|
@ -1155,6 +1155,11 @@ def main(argv: list[str] | None = None) -> int:
|
|||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.report.write_text(report.render(), encoding="utf-8", newline="")
|
||||
print(report.render())
|
||||
# Loud where a redirected stdout cannot hide it. `okf build > report.txt`
|
||||
# is an ordinary thing to do, and a document the gate dropped is the one
|
||||
# thing about the run a reader must not learn later, or never.
|
||||
if report.gate_refused:
|
||||
print(f"{CLI_ID}: {report.refusal_banner()}", file=sys.stderr)
|
||||
if report.conservation_failed:
|
||||
print(
|
||||
f"{CLI_ID}: K1b FAILED - {report.identity()}. Unaccounted: "
|
||||
|
|
|
|||
|
|
@ -79,6 +79,16 @@ HARNESS_ID = "okf-corpus-run"
|
|||
LOG_NAME = "log.md"
|
||||
LOG_TITLE = "Corpus run history"
|
||||
|
||||
#: The heading of the summary section that names the documents the gate refused
|
||||
#: WHOLE, in ONE place because the section, the stderr banner and the tests all
|
||||
#: have to mean the same section.
|
||||
REFUSED_HEADING = "## Documents the gate refused WHOLE"
|
||||
|
||||
#: How many refused documents the printed summary names before it stops and
|
||||
#: points at `log.md`. A cap is not a licence to lose the rest: `log.md` carries
|
||||
#: every name, uncapped, and the section says so on the line where it stops.
|
||||
REFUSED_NAME_CAP = 10
|
||||
|
||||
|
||||
#: The permissive stub. It approves EVERYTHING, and it is named rather than
|
||||
#: anonymous because that is the whole lesson of F1: from the day `okf build`
|
||||
|
|
@ -244,6 +254,14 @@ class CorpusReport:
|
|||
carried_files: int = 0
|
||||
#: The content accounting of this run, or None when it was not asked for.
|
||||
accounting: Accounting | None = None
|
||||
#: Every document the GATE refused whole, as (source file, disposition),
|
||||
#: sorted. Its own column because `rejected` sums two different failures
|
||||
#: with two different remedies: a document the extractor could not read is
|
||||
#: a format this library does not handle, while a document the gate refused
|
||||
#: is a screening decision the caller can overrule with `--gate none`.
|
||||
#: Measured 2026-09-20 on a 594-file documentation corpus, where 17 sources
|
||||
#: were refused whole and the summary named neither the count nor a name.
|
||||
gate_refused: tuple[tuple[str, str], ...] = ()
|
||||
|
||||
@property
|
||||
def merged(self) -> int:
|
||||
|
|
@ -273,6 +291,65 @@ class CorpusReport:
|
|||
f"{self.merged} + {self.carried_files} + {self.rejected} = {total}; N = {self.n}"
|
||||
)
|
||||
|
||||
def refusal_section(self) -> list[str]:
|
||||
"""The four facts a reader needs about a document the gate dropped.
|
||||
|
||||
How many (with the denominator), which ones, under which code, and the
|
||||
one command that carries them anyway. Empty when the gate refused
|
||||
nothing, so a clean run's summary is the summary it always was.
|
||||
"""
|
||||
if not self.gate_refused:
|
||||
return []
|
||||
counts: dict[str, int] = {}
|
||||
for _, disposition in self.gate_refused:
|
||||
counts[disposition] = counts.get(disposition, 0) + 1
|
||||
by_code = ", ".join(f"`{code}`: {count}" for code, count in sorted(counts.items()))
|
||||
lines = [
|
||||
REFUSED_HEADING,
|
||||
"",
|
||||
f"The gate `{self.gate}` refused {len(self.gate_refused)} of {self.n} "
|
||||
"document(s) outright: not one element of them reached the bundle. "
|
||||
f"By code: {by_code}.",
|
||||
"",
|
||||
]
|
||||
lines.extend(
|
||||
f"- `{name}` -- `{disposition}`"
|
||||
for name, disposition in self.gate_refused[:REFUSED_NAME_CAP]
|
||||
)
|
||||
remaining = len(self.gate_refused) - REFUSED_NAME_CAP
|
||||
if remaining > 0:
|
||||
lines.append(
|
||||
f"- ... and {remaining} more, every one named in the bundle's `{LOG_NAME}`."
|
||||
)
|
||||
lines += [
|
||||
"",
|
||||
"If you trust these sources yourself -- your own folder, your own "
|
||||
"documents -- build them with `--gate none`. It screens nothing, and "
|
||||
"the bundle records that it screened nothing.",
|
||||
"",
|
||||
]
|
||||
return lines
|
||||
|
||||
def refusal_banner(self) -> str:
|
||||
"""The same fact in one line, for a run whose stdout went to a file.
|
||||
|
||||
Built from `refusal_section`'s own data rather than beside it, so the
|
||||
loud line and the summary cannot come to disagree about a number.
|
||||
"""
|
||||
if not self.gate_refused:
|
||||
return ""
|
||||
shown = ", ".join(
|
||||
f"{name} (`{disposition}`)" for name, disposition in self.gate_refused[:3]
|
||||
)
|
||||
remaining = len(self.gate_refused) - 3
|
||||
if remaining > 0:
|
||||
shown += f", and {remaining} more"
|
||||
return (
|
||||
f"the gate `{self.gate}` refused {len(self.gate_refused)} of {self.n} "
|
||||
f"document(s) WHOLE: {shown}. Trust these sources? build with "
|
||||
f"`--gate none`. Full list in the summary above and in {LOG_NAME}"
|
||||
)
|
||||
|
||||
def render(self) -> str:
|
||||
per_file = self.seconds_total / self.n if self.n else 0.0
|
||||
lines = [
|
||||
|
|
@ -280,6 +357,7 @@ class CorpusReport:
|
|||
"",
|
||||
f"N (denominator, the directory's file count) = {self.n}",
|
||||
"",
|
||||
*self.refusal_section(),
|
||||
"## Three counts, never one",
|
||||
"",
|
||||
"The guard sits between extraction and persist, so a healthy persisted",
|
||||
|
|
@ -364,6 +442,15 @@ class CorpusReport:
|
|||
"Every persisted byte of this bundle passed it.",
|
||||
self._assets_line(),
|
||||
]
|
||||
if self.gate_refused:
|
||||
named = ", ".join(
|
||||
f"`{name}` (`{disposition}`)" for name, disposition in self.gate_refused
|
||||
)
|
||||
lines.append(
|
||||
f"* **Refused whole by the gate**: {len(self.gate_refused)} of {self.n} "
|
||||
f"document(s), so not one element of them is in this bundle: {named}. "
|
||||
"Rebuild with `--gate none` to carry sources you vouch for yourself."
|
||||
)
|
||||
if self.accounting is not None:
|
||||
lines.extend(self.accounting.log_lines())
|
||||
if self.unaccounted:
|
||||
|
|
@ -533,6 +620,7 @@ def measure(
|
|||
assets_found=len(result.assets) + len(result.assets_rejected),
|
||||
carried_files=len(carried),
|
||||
accounting=account_run(corpus, walked, result) if account else None,
|
||||
gate_refused=tuple(sorted((item.source_file, item.disposition) for item in blocked)),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue