Every fixture, test document, tool example and document now uses an invented kitchen-and-baking handbook series, written in this repository. The package's behaviour is unchanged; src/ changes are comments and help text only. - Generated fixtures are regenerated from their generators. Their structural counts are identical before and after: elements, images, rows, cells, headings, bookmarks and the witness inventory's per-document totals. The image-inbox and accounting documents are renamed kapittel-84-*. - tools/okf_accounting_gate.py: the two options that named one real corpus each are replaced by a generic, repeatable --corpus PATH with no default. Row 5 compares the PDF pair alone. Gate verdict unchanged: RED rows 2, 3, 6. - tools/okf_witness.py: the STS JSON reader for one publisher's delivery is removed, along with its three twins and five tests. The mutation harness loses W09. - docs/: 13 dated reports that documented runs on a retired reference corpus are removed, and 40 are neutralized. Dead links are removed, and no new dangling path is introduced. - The synthetic MCP-gate corpus and the residual probe words are neutral. Valgt: keep the `okf quality --fasit` bar value (the measured fraction, one corpus) and rewrite only its provenance, because the verdict stays unchanged and the number names nothing. Term check with the local list: 0 of 411 tracked files, 0 file names, 0 of 27 binary fixtures. Suite after git add: 2457 passed, 1 skipped. The base tree had 2460 passed and 2 skipped; five tests went with the JSON reader and four were added by the term check. ruff, ruff format and mypy --strict src/ are clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
445 lines
19 KiB
Python
445 lines
19 KiB
Python
"""`okf quality` -- the per-file-type verdict, and what it refuses to say.
|
|
|
|
`okf check` is a CONTRACT check: it asks whether a payload carries what a claim
|
|
must rest on. Measured 2026-09-10 by a downstream consumer repository, it
|
|
returned 0 findings and exit 0 on three arms over one corpus whose hit@k ranged
|
|
from 6 of 6 to 0 of 6.
|
|
This module is the other question -- did the cut find anything worth reading --
|
|
and its whole discipline is that it answers it PER FILE TYPE, with the
|
|
denominator printed, and never answers PASS for a type it has no measurement
|
|
for.
|
|
|
|
Each rule below is exercised in both directions on purpose: a rule that has only
|
|
ever been run against the case it fires on has not been measured, it has been
|
|
asserted. The thresholds themselves, and the four evidence corpora they were
|
|
read off, live in `docs/2026-09-12-g37-terskler.md`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from llm_ingestion_okf import quality
|
|
|
|
_FRONTMATTER = """---
|
|
type: reference
|
|
title: {title}
|
|
source_file: {source_file}
|
|
ingested_at: 1970-01-01T00:00:00Z
|
|
---
|
|
|
|
{body}
|
|
"""
|
|
|
|
|
|
def _bundle(root: Path, concepts: list[tuple[str, str, str]]) -> Path:
|
|
"""A flat bundle: one root index, one file per concept.
|
|
|
|
`concepts` is `(stem, source_file, body)`. `source_file` may be the empty
|
|
string, which is the shape a bundle from another producer arrives in.
|
|
"""
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
lines = ["---", "okf_version: 0.2", "bundle_id: quality-fixture", "---", ""]
|
|
for stem, source_file, body in concepts:
|
|
(root / f"{stem}.md").parent.mkdir(parents=True, exist_ok=True)
|
|
(root / f"{stem}.md").write_text(
|
|
_FRONTMATTER.format(title=stem, source_file=source_file, body=body),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
lines.append(f"- [{stem}]({stem}.md)")
|
|
(root / "index.md").write_text("\n".join(lines) + "\n", encoding="utf-8", newline="")
|
|
return root
|
|
|
|
|
|
def _pdf_documents(count: int, *, single: int) -> list[tuple[str, str, str]]:
|
|
"""`count` PDF documents, `single` of them yielding one concept only."""
|
|
concepts: list[tuple[str, str, str]] = []
|
|
for index in range(count):
|
|
source = f"doc-{index}.pdf"
|
|
segments = 1 if index < single else 3
|
|
for segment in range(segments):
|
|
concepts.append((f"doc-{index}-{segment}", source, "Body text enough to count."))
|
|
return concepts
|
|
|
|
|
|
def test_a_type_at_its_measured_reference_passes(tmp_path: Path) -> None:
|
|
"""The known-positive: 8 of 32 is the reference, and 8 of 32 is not worse."""
|
|
root = _bundle(tmp_path / "at-reference", _pdf_documents(32, single=8))
|
|
report = quality.measure_bundle(root)
|
|
row = report.row(".pdf")
|
|
assert row.verdict == "PASS"
|
|
assert (row.structure_null, row.documents) == (8, 32)
|
|
|
|
|
|
def test_a_type_worse_than_its_reference_fails(tmp_path: Path) -> None:
|
|
"""The known-negative for the same rule: 9 of 32 is worse than 8 of 32."""
|
|
root = _bundle(tmp_path / "over-reference", _pdf_documents(32, single=9))
|
|
report = quality.measure_bundle(root)
|
|
assert report.row(".pdf").verdict == "FAIL"
|
|
assert report.exit_code == 1
|
|
|
|
|
|
def test_an_empty_concept_fails_a_type_that_has_no_threshold(tmp_path: Path) -> None:
|
|
"""FAIL is reachable for every type; PASS is not. `.md` has no threshold."""
|
|
root = _bundle(
|
|
tmp_path / "empty-body",
|
|
[("a", "note.md", "Body text."), ("b", "note.md", " \n")],
|
|
)
|
|
report = quality.measure_bundle(root)
|
|
row = report.row(".md")
|
|
assert row.verdict == "FAIL"
|
|
assert (row.empty, row.concepts) == (1, 2)
|
|
|
|
|
|
def test_a_healthy_type_without_a_threshold_is_unmeasured_and_never_pass(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The other direction of the same rule: nothing wrong, and still not PASS."""
|
|
root = _bundle(
|
|
tmp_path / "no-threshold",
|
|
[("a", "note.md", "Body text."), ("b", "note.md", "More body text.")],
|
|
)
|
|
report = quality.measure_bundle(root)
|
|
assert report.row(".md").verdict == "UNMEASURED"
|
|
assert report.exit_code == 3
|
|
|
|
|
|
def test_concepts_without_a_source_file_are_unmeasured(tmp_path: Path) -> None:
|
|
"""The shape three of the four evidence corpora arrive in.
|
|
|
|
Three bundles built by another producer carry `source_file` on 0 of 446,
|
|
0 of 1 133 and 0 of 270 concepts, so a per-file-type gate has no type to
|
|
speak about. Measured 2026-09-12; the gate says so instead of passing them.
|
|
"""
|
|
root = _bundle(tmp_path / "foreign", [("a", "", "Body text."), ("b", "", "More text.")])
|
|
report = quality.measure_bundle(root)
|
|
row = report.row(quality.NO_SOURCE_FILE)
|
|
assert row.verdict == "UNMEASURED"
|
|
assert report.exit_code == 3
|
|
|
|
|
|
def test_a_failing_type_outweighs_a_passing_one(tmp_path: Path) -> None:
|
|
concepts = _pdf_documents(32, single=9)
|
|
for index in range(5): # five documents: the floor a rate needs to be a rate
|
|
concepts.append((f"note-{index}-0", f"note-{index}.docx", "Body text."))
|
|
concepts.append((f"note-{index}-1", f"note-{index}.docx", "More body text."))
|
|
report = quality.measure_bundle(_bundle(tmp_path / "mixed", concepts))
|
|
assert report.row(".docx").verdict == "PASS"
|
|
assert report.row(".pdf").verdict == "FAIL"
|
|
assert report.exit_code == 1
|
|
|
|
|
|
def test_every_row_prints_its_denominator(tmp_path: Path) -> None:
|
|
root = _bundle(tmp_path / "printed", _pdf_documents(32, single=8))
|
|
rendered = quality.measure_bundle(root).render()
|
|
assert "8/32" in rendered
|
|
assert "0/80" in rendered # empty concepts, over the concepts of the type
|
|
assert "documents" in rendered
|
|
|
|
|
|
def test_the_bundle_run_log_is_reported_when_the_bundle_carries_one(tmp_path: Path) -> None:
|
|
"""A rejected document leaves NO row in the bundle: the log is the only trace.
|
|
|
|
On the pinned K2 bundle the log reads `N = 43, merged 39, coded rejections
|
|
4`, and the bundle itself shows 39 documents -- so the gate's own
|
|
denominator is the bundle's, never the corpus's, and it says which one it
|
|
is using.
|
|
"""
|
|
root = _bundle(tmp_path / "logged", _pdf_documents(2, single=0))
|
|
(root / "log.md").write_text(
|
|
"* **Ingested**: /x - N = 3 (the corpus directory's file count, computed at "
|
|
"run time), merged = 2 (2 substantive, 0 degenerate), coded rejections = 1.\n",
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
report = quality.measure_bundle(root)
|
|
assert report.run_log == "N = 3, merged = 2, coded rejections = 1"
|
|
assert "coded rejections = 1" in report.render()
|
|
|
|
|
|
def test_a_bundle_without_a_log_says_so_rather_than_reporting_zero(tmp_path: Path) -> None:
|
|
root = _bundle(tmp_path / "unlogged", _pdf_documents(2, single=0))
|
|
report = quality.measure_bundle(root)
|
|
assert report.run_log is None
|
|
assert "no run log" in report.render()
|
|
|
|
|
|
def test_a_missing_bundle_exits_two(tmp_path: Path, capsys) -> None: # type: ignore[no-untyped-def]
|
|
assert quality.main([str(tmp_path / "nowhere")]) == 2
|
|
|
|
|
|
def test_the_cli_reaches_the_gate_through_okf_quality(tmp_path: Path, capsys) -> None: # type: ignore[no-untyped-def]
|
|
from llm_ingestion_okf import cli
|
|
|
|
root = _bundle(tmp_path / "through-cli", _pdf_documents(32, single=9))
|
|
assert cli.main(["quality", str(root)]) == 1
|
|
assert ".pdf" in capsys.readouterr().out
|
|
|
|
|
|
def test_every_threshold_names_the_corpus_and_the_denominator_it_was_read_off() -> None:
|
|
"""A threshold without an N is the thing this whole gate exists to refuse."""
|
|
assert quality.THRESHOLDS, "no thresholds at all would make every type UNMEASURED"
|
|
for extension, threshold in quality.THRESHOLDS.items():
|
|
assert extension.startswith("."), extension
|
|
assert threshold.documents >= quality.MIN_DOCUMENTS_FOR_A_THRESHOLD
|
|
assert threshold.source, f"{extension} threshold names no source"
|
|
assert threshold.limit_documents == threshold.documents
|
|
|
|
|
|
def test_a_rate_over_too_few_documents_is_not_a_rate(tmp_path: Path) -> None:
|
|
"""FOUND BY RUNNING THE GATE, not by reading it.
|
|
|
|
A downstream consumer's default build of one reference PDF is cut into 2 182
|
|
concepts. Against the 32-document reference its one-concept share is 0 of 1,
|
|
which the first version of this rule read as PASS -- a verdict resting on a
|
|
denominator of one, which is the exact failure `MIN_DOCUMENTS_FOR_A_THRESHOLD`
|
|
exists to refuse. The floor binds the BUNDLE's denominator too, not only the
|
|
threshold's.
|
|
"""
|
|
root = _bundle(tmp_path / "one-document", _pdf_documents(1, single=0))
|
|
assert quality.measure_bundle(root).row(".pdf").verdict == "UNMEASURED"
|
|
|
|
|
|
def test_the_floor_admits_a_bundle_that_reaches_it(tmp_path: Path) -> None:
|
|
"""The other direction: five documents is the floor, and five is enough."""
|
|
root = _bundle(tmp_path / "five-documents", _pdf_documents(5, single=1))
|
|
assert quality.measure_bundle(root).row(".pdf").verdict == "PASS"
|
|
|
|
|
|
def test_an_empty_concept_fails_even_below_the_floor(tmp_path: Path) -> None:
|
|
"""The empty-body bar is per concept, not a rate, so the floor does not gate it."""
|
|
root = _bundle(tmp_path / "one-document-empty", [("a", "only.pdf", " \n")])
|
|
assert quality.measure_bundle(root).row(".pdf").verdict == "FAIL"
|
|
|
|
|
|
def test_the_no_source_file_row_reports_no_document_count(tmp_path: Path) -> None:
|
|
"""A row that is not a file type has no documents either.
|
|
|
|
Every concept without the key shares the same empty `source_file`, so a
|
|
naive grouping reports `documents 1` and `one-concept documents 0/1` for a
|
|
bundle of 446 concepts -- three numbers that look measured and mean nothing.
|
|
"""
|
|
root = _bundle(tmp_path / "foreign-render", [("a", "", "Body."), ("b", "", "More.")])
|
|
rendered = quality.measure_bundle(root).render()
|
|
assert "documents 1" not in rendered
|
|
assert "one-concept documents" not in rendered
|
|
assert "empty 0/2" in rendered
|
|
assert "no source_file on 2 of 2 concepts" in rendered
|
|
|
|
|
|
# --- G37b: `--fasit` and `boundary_share` -------------------------------------
|
|
#
|
|
# The metric that separates the two arms this gate could not tell apart. It is
|
|
# the ONLY one measured that orders them correctly and it needs the publisher's
|
|
# own declared structure, so it arrives as an input rather than as a constant:
|
|
# `okf quality <bundle> --fasit <json>`.
|
|
#
|
|
# Two things are pinned here that were measured before any of it was written
|
|
# (`docs/2026-09-12-g37-terskler.md` SS 7):
|
|
#
|
|
# 1. The fasit's `norm` key is reproduced from its own `title` by stripping all
|
|
# whitespace and lowercasing -- 2 761 of 2 761 rows, so the normalisation is
|
|
# not a guess.
|
|
# 2. That normalisation ALONE reaches 22 of 2 761 on the known-good arm, not
|
|
# 99 %, because okf's default route moves the numbering token STS glues into
|
|
# `<title>` over into the concept id. The second match form -- the
|
|
# (directory, residual title) pair -- is what takes it to 2 759 of 2 761.
|
|
|
|
|
|
def _fasit(path: Path, titles: list[str]) -> Path:
|
|
"""A fasit file in the shipped shape: a list of rows carrying title and norm."""
|
|
path.write_text(
|
|
json.dumps(
|
|
[{"title": title, "norm": quality.normalise_title(title)} for title in titles],
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
return path
|
|
|
|
|
|
def _declaring(titles: list[str]) -> list[tuple[str, str, str]]:
|
|
"""One concept per declared title, matching it literally."""
|
|
return [(f"c{index}", "doc.xml", "Body text.") for index, _ in enumerate(titles)]
|
|
|
|
|
|
def test_the_normalisation_reproduces_the_fasit_key() -> None:
|
|
"""`norm` is whitespace-stripped, lowercased `title` -- not a guess.
|
|
|
|
Measured over the shipped fasit before anything was written: 2 761 of 2 761
|
|
rows reproduce, and the real file pins it below where it exists.
|
|
"""
|
|
assert quality.normalise_title(" 2.1 Hoved Kapitler\n") == "2.1hovedkapitler"
|
|
assert quality.normalise_title("1Bruksområder for Oppskriftsboka") == (
|
|
"1bruksområderforoppskriftsboka"
|
|
)
|
|
|
|
|
|
def test_a_bundle_recovering_every_declared_boundary_passes(tmp_path: Path) -> None:
|
|
"""The known-positive, in miniature: every declared title became a concept."""
|
|
titles = [f"{n} Kapittel {n}" for n in range(1, 7)]
|
|
root = tmp_path / "recovered"
|
|
root.mkdir()
|
|
bundle = _bundle(root, [(quality.normalise_title(t)[:12], "doc.xml", "Body.") for t in titles])
|
|
# The concept titles ARE the declared titles: rewrite the frontmatter.
|
|
for index, title in enumerate(titles):
|
|
stem = quality.normalise_title(title)[:12]
|
|
(bundle / f"{stem}.md").write_text(
|
|
_FRONTMATTER.format(title=title, source_file="doc.xml", body="Body."),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
report = quality.measure_bundle(
|
|
bundle, fasit=quality.load_fasit(_fasit(tmp_path / "f.json", titles))
|
|
)
|
|
assert report.boundaries is not None
|
|
assert (report.boundaries.recovered, report.boundaries.declared) == (6, 6)
|
|
assert report.boundaries.verdict == "PASS"
|
|
assert report.exit_code == 0
|
|
|
|
|
|
def test_a_bundle_recovering_few_declared_boundaries_fails(tmp_path: Path) -> None:
|
|
"""The known-negative for the same rule, and the arm it was built for.
|
|
|
|
`860019-mdb-100` recovers 1 148 of 2 761. Here five of six is already worse
|
|
than the bar of 2 759/2 761 -- the bar is that tight, which is a property of
|
|
a regression bar set at its reference and is said out loud in the document.
|
|
"""
|
|
titles = [f"{n} Kapittel {n}" for n in range(1, 7)]
|
|
root = tmp_path / "lost"
|
|
root.mkdir()
|
|
bundle = _bundle(root, [(f"c{i}", "doc.md", "Body.") for i in range(5)])
|
|
for index, title in enumerate(titles[:5]):
|
|
(bundle / f"c{index}.md").write_text(
|
|
_FRONTMATTER.format(title=title, source_file="doc.md", body="Body."),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
report = quality.measure_bundle(
|
|
bundle, fasit=quality.load_fasit(_fasit(tmp_path / "f.json", titles))
|
|
)
|
|
assert report.boundaries is not None
|
|
assert (report.boundaries.recovered, report.boundaries.declared) == (5, 6)
|
|
assert report.boundaries.verdict == "FAIL"
|
|
assert report.exit_code == 1
|
|
|
|
|
|
def test_the_numbering_token_is_matched_through_the_concept_directory(tmp_path: Path) -> None:
|
|
"""The second match form, and the reason P1's literal reading is not enough.
|
|
|
|
STS glues the numbering into the title (`11.1Fastmerker`); okf's default
|
|
route moves it into the concept id (`11-1/...`) and keeps the residual as
|
|
the title. Measured on the known-good arm: the literal form alone reaches
|
|
22 of 2 761, the pair form 2 737, either 2 759.
|
|
"""
|
|
titles = ["11.1Fastmerker"]
|
|
bundle = _bundle(tmp_path / "paired", [("11-1/p1", "doc.pdf", "Body.")])
|
|
(bundle / "11-1/p1.md").write_text(
|
|
_FRONTMATTER.format(title="Fastmerker", source_file="doc.pdf", body="Body."),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
fasit = quality.load_fasit(_fasit(tmp_path / "f.json", titles))
|
|
report = quality.measure_bundle(bundle, fasit=fasit)
|
|
assert report.boundaries is not None
|
|
assert report.boundaries.literal == 0
|
|
assert report.boundaries.paired == 1
|
|
assert report.boundaries.recovered == 1
|
|
|
|
|
|
def test_without_a_fasit_the_gate_is_byte_for_byte_what_it_was(tmp_path: Path) -> None:
|
|
"""The regression the order asks for: no `--fasit`, no change of any kind."""
|
|
root = _bundle(tmp_path / "unchanged", [("a", "note.md", "Body."), ("b", "note.md", "More.")])
|
|
report = quality.measure_bundle(root)
|
|
assert report.boundaries is None
|
|
assert report.exit_code == 3
|
|
assert "boundary" not in report.render().split("## What this verdict is not")[0].lower()
|
|
|
|
|
|
def test_a_fasit_that_is_not_a_list_exits_two_rather_than_unmeasured(tmp_path: Path) -> None:
|
|
"""An unreadable fasit is a run that did not happen, never a quiet verdict."""
|
|
bad = tmp_path / "object.json"
|
|
bad.write_text('{"title": "x", "norm": "x"}', encoding="utf-8")
|
|
root = _bundle(tmp_path / "b", [("a", "note.md", "Body.")])
|
|
assert quality.main([str(root), "--fasit", str(bad)]) == 2
|
|
|
|
|
|
def test_a_fasit_row_missing_a_key_exits_two(tmp_path: Path) -> None:
|
|
bad = tmp_path / "rows.json"
|
|
bad.write_text('[{"title": "x", "norm": "x"}, {"title": "y"}]', encoding="utf-8")
|
|
root = _bundle(tmp_path / "b", [("a", "note.md", "Body.")])
|
|
assert quality.main([str(root), "--fasit", str(bad)]) == 2
|
|
|
|
|
|
def test_a_fasit_that_is_not_json_exits_two(tmp_path: Path) -> None:
|
|
bad = tmp_path / "broken.json"
|
|
bad.write_text("not json at all", encoding="utf-8")
|
|
root = _bundle(tmp_path / "b", [("a", "note.md", "Body.")])
|
|
assert quality.main([str(root), "--fasit", str(bad)]) == 2
|
|
|
|
|
|
def test_a_missing_fasit_file_exits_two(tmp_path: Path) -> None:
|
|
root = _bundle(tmp_path / "b", [("a", "note.md", "Body.")])
|
|
assert quality.main([str(root), "--fasit", str(tmp_path / "nowhere.json")]) == 2
|
|
|
|
|
|
def test_a_valid_fasit_reaches_the_gate_through_the_cli(tmp_path: Path, capsys) -> None: # type: ignore[no-untyped-def]
|
|
titles = [f"{n} Kapittel {n}" for n in range(1, 7)]
|
|
bundle = _bundle(tmp_path / "cli", [(f"c{i}", "doc.md", "Body.") for i in range(1)])
|
|
(bundle / "c0.md").write_text(
|
|
_FRONTMATTER.format(title=titles[0], source_file="doc.md", body="Body."),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
from llm_ingestion_okf import cli
|
|
|
|
assert (
|
|
cli.main(["quality", str(bundle), "--fasit", str(_fasit(tmp_path / "f.json", titles))]) == 1
|
|
)
|
|
out = capsys.readouterr().out
|
|
assert "boundary_share" in out
|
|
assert "1/6" in out
|
|
|
|
|
|
def test_a_fasit_below_the_floor_is_unmeasured_and_never_pass(tmp_path: Path) -> None:
|
|
"""A share over four declared boundaries is not a rate either."""
|
|
titles = ["1 A", "2 B", "3 C", "4 D"]
|
|
bundle = _bundle(tmp_path / "tiny", [(f"c{i}", "doc.md", "Body.") for i in range(4)])
|
|
for index, title in enumerate(titles):
|
|
(bundle / f"c{index}.md").write_text(
|
|
_FRONTMATTER.format(title=title, source_file="doc.md", body="Body."),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
report = quality.measure_bundle(
|
|
bundle, fasit=quality.load_fasit(_fasit(tmp_path / "f.json", titles))
|
|
)
|
|
assert report.boundaries is not None
|
|
assert report.boundaries.verdict == "UNMEASURED"
|
|
assert report.boundaries.recovered == 4
|
|
|
|
|
|
def test_the_boundary_row_says_its_threshold_rests_on_one_corpus(tmp_path: Path) -> None:
|
|
"""P2, in the printout rather than only in the document."""
|
|
titles = [f"{n} Kapittel {n}" for n in range(1, 7)]
|
|
bundle = _bundle(tmp_path / "caveat", [("c0", "doc.md", "Body.")])
|
|
(bundle / "c0.md").write_text(
|
|
_FRONTMATTER.format(title=titles[0], source_file="doc.md", body="Body."),
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
rendered = quality.measure_bundle(
|
|
bundle, fasit=quality.load_fasit(_fasit(tmp_path / "f.json", titles))
|
|
).render()
|
|
assert "one product" in rendered
|
|
assert "N = 1" in rendered
|
|
|
|
|
|
def test_the_boundary_threshold_names_its_corpus_and_denominator() -> None:
|
|
bar = quality.BOUNDARY_THRESHOLD
|
|
assert bar.limit_declared >= quality.MIN_DECLARED_FOR_A_THRESHOLD
|
|
assert bar.corpora == 1
|
|
assert bar.source
|