Files changed: - CHANGES.md - EVALS.md - VERSION - instructions/wiki-lint/SKILL.md - instructions/wiki-manage/SKILL.md - tools/CONTRACT.md - tools/chemenu/blocks.py - tools/chemenu/commands/lint.py - tools/chemenu/evals/scorecard.py - tools/chemenu/lint_core.py - tools/chemenu/tests/test_blocks.py - tools/chemenu/tests/test_evals.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_pipeline_l0.py - types/type-spec.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
"""L0: the pipeline against a wiki the tools built themselves.
|
|
|
|
Every other test in this suite asks whether one command does its job. This one
|
|
asks whether the commands compose - whether a wiki assembled the documented way
|
|
comes out structurally clean, and whether the catalog is a fixed point rather
|
|
than something that keeps changing every time it is rebuilt.
|
|
|
|
That is the hard-oracle floor the eval ladder in EVALS.md rests on: before a
|
|
score can say anything about how an agent worked, running the tools correctly has
|
|
to be known to produce a clean tree.
|
|
|
|
The wiki here is built through the CLI and starts empty, deliberately on both
|
|
counts. `test_lint.py` already checks lint against the hand-written `kb_dir`
|
|
fixture, and a hand-written tree cannot show that `new`, `xref`, `index` and
|
|
`lint` agree with each other - only a tree the tools produced can.
|
|
"""
|
|
import re
|
|
|
|
import pytest
|
|
import chemenu.config as config
|
|
from typer.testing import CliRunner
|
|
|
|
from chemenu.commands.index_build import plan_index
|
|
from chemenu.commands.lint import HARD_ERROR_KEYS, has_hard_errors, run_lint
|
|
|
|
runner = CliRunner()
|
|
|
|
COLLECTIONS = ("entities", "concepts", "sources", "comparisons")
|
|
|
|
# The body `new` scaffolds carries placeholder wikilinks - `[[Entity 1]]` and
|
|
# friends - for the author to replace. Until they are replaced they point at
|
|
# nothing, which is a broken link and is meant to be: the scaffold is not a
|
|
# finished page. Stripping them stands in for writing the page.
|
|
PLACEHOLDER_LINK = re.compile(r"\[\[(?:Entity|Related Concept|Concept|Source)[^\]]*\]\]")
|
|
|
|
|
|
@pytest.fixture
|
|
def empty_kb(tmp_path, monkeypatch):
|
|
"""A kb/ with the collection layout and no pages at all."""
|
|
kb = tmp_path / "kb"
|
|
for collection in COLLECTIONS:
|
|
(kb / collection).mkdir(parents=True)
|
|
(kb / collection / "COLLECTION.md").write_text(
|
|
f"---\nprofile: {collection}\n"
|
|
f"required_by_stack: {'true' if collection == 'sources' else 'false'}\n"
|
|
"outbound:\n any: [implements, uses, see-also]\n---\n\n"
|
|
f"# kb/{collection}/ - Collection Contract\n",
|
|
encoding="utf-8",
|
|
)
|
|
raw = tmp_path / "raw"
|
|
raw.mkdir()
|
|
|
|
monkeypatch.setattr(config, "KB_DIR", kb)
|
|
monkeypatch.setattr(config, "INDEX_FILE", kb / "index.md")
|
|
monkeypatch.setattr(config, "RAW_DIR", raw)
|
|
return kb
|
|
|
|
|
|
def invoke(args):
|
|
from chemenu.cli import app
|
|
|
|
result = runner.invoke(app, args)
|
|
assert result.exit_code == 0, f"{' '.join(args)} failed:\n{result.output}"
|
|
return result
|
|
|
|
|
|
def finish_page(path):
|
|
"""Replace the scaffold's placeholder links, as an author would."""
|
|
body = path.read_text(encoding="utf-8")
|
|
path.write_text(PLACEHOLDER_LINK.sub("", body), encoding="utf-8")
|
|
|
|
|
|
def build_wiki(kb):
|
|
"""The documented sequence: create, write, link, rebuild the catalog."""
|
|
invoke(["new", "entity", "--name", "Pipeline Host",
|
|
"--set", "entity_type=system",
|
|
"--set", "summary=A host created by the pipeline test"])
|
|
invoke(["new", "concept", "--name", "Pipeline Concept",
|
|
"--set", "concept_type=workflow",
|
|
"--set", "summary=A concept created by the pipeline test"])
|
|
for page in kb.rglob("Pipeline *.md"):
|
|
finish_page(page)
|
|
invoke(["xref", "add", "--a", "Pipeline Host", "--b", "Pipeline Concept",
|
|
"--rel", "implements"])
|
|
invoke(["index", "rebuild"])
|
|
|
|
|
|
def test_a_scaffolded_page_is_not_yet_a_finished_one(empty_kb):
|
|
"""`new` writes placeholder links, so lint reports them until the page is
|
|
written. Recorded here because it is easy to mistake for a defect: a freshly
|
|
scaffolded page does not lint clean, and should not."""
|
|
invoke(["new", "concept", "--name", "Scaffold Only",
|
|
"--set", "concept_type=pattern", "--set", "summary=Untouched scaffold"])
|
|
|
|
report = run_lint(empty_kb)
|
|
assert report["broken_links"], \
|
|
"the scaffold no longer carries placeholder links - update this test"
|
|
assert {f["page"] for f in report["broken_links"]} == {"Scaffold Only"}
|
|
assert [f["page"] for f in report["unfilled_sections"]] == ["Scaffold Only"], \
|
|
"the scaffold no longer marks its placeholders with TODO - see types/type-spec.md"
|
|
|
|
|
|
def test_a_wiki_built_by_the_tools_lints_clean(empty_kb):
|
|
build_wiki(empty_kb)
|
|
|
|
report = run_lint(empty_kb)
|
|
found = {key: report[key] for key in HARD_ERROR_KEYS if report.get(key)}
|
|
assert not has_hard_errors(report), f"hard errors after a clean build: {found}"
|
|
|
|
|
|
def test_an_edge_points_one_way_and_the_far_end_stops_being_an_orphan(empty_kb):
|
|
"""`xref add` declares one direction, and that is what the orphan check now
|
|
measures: reachability.
|
|
|
|
It used to assert that *neither* page was an orphan, which only held because
|
|
`xref add` wrote a mirror edge on the target. With authored directional
|
|
edges the source of the only edge in a two-page wiki genuinely has nothing
|
|
pointing at it - so the check reporting it is the check working, not a
|
|
regression. A real corpus answers this by having entry points that other
|
|
pages point at."""
|
|
build_wiki(empty_kb)
|
|
|
|
report = run_lint(empty_kb)
|
|
assert "Pipeline Concept" not in report["orphan_pages"]
|
|
assert report["orphan_pages"] == ["Pipeline Host"]
|
|
assert report["broken_links"] == []
|
|
assert report["dangling_frontmatter_refs"] == []
|
|
|
|
|
|
def test_the_catalog_covers_what_was_created(empty_kb):
|
|
build_wiki(empty_kb)
|
|
|
|
report = run_lint(empty_kb)
|
|
assert report["missing_from_index"] == []
|
|
assert report["dangling_index_entries"] == []
|
|
|
|
|
|
def test_rebuilding_the_catalog_changes_nothing(empty_kb):
|
|
"""A rebuild has to be a fixed point.
|
|
|
|
`test_index_build.py` checks that the *planner* is stable - two calls return
|
|
the same plan. This is the stronger property: after the plan has been written
|
|
to disk, planning again over the changed tree produces the same files with
|
|
the same contents, and what is on disk matches. A catalog that drifted on
|
|
every rebuild would make lint's index-drift check fire on a tree nobody
|
|
touched.
|
|
"""
|
|
build_wiki(empty_kb)
|
|
after_first = plan_index(empty_kb)
|
|
|
|
invoke(["index", "rebuild"])
|
|
after_second = plan_index(empty_kb)
|
|
assert after_second == after_first
|
|
|
|
on_disk = {path: path.read_text(encoding="utf-8") for path in after_second}
|
|
assert on_disk == after_second, "the written catalog differs from the plan"
|