Files
autocad-mcp-pro/scripts/check_doc_numbers.py
T
Umut CANandClaude Opus 5 8d9c444b76 bench: matrix v4 adds pid_roundtrip; correctness suite grows to 29 with three P&ID checks
tasks_v4.py extends v3 with one task the reader can fail on its own: the
example spec is drawn through pid_from_spec's code and read back by the
graph builder, which never sees the spec. The runner, the chart and the
doc-number gate now default to v4; --matrix v3 / v2 reproduce the earlier
sets, and tests/test_benchmark_v3.py labels its full-matrix run "v3"
explicitly now that the default has moved.

The plan's adapter check expected the two unnumbered lines to come back
with no line number; draw_line builds one from the default number_format
(bare sequence when size/service/spec/insulation are empty), so the task
asserts the two authored numbers on the two process lines and sequence-
built XDATA numbers on the other two instead.

The published A/B report still covers 26 checks; Task 21 re-runs it
against v1.5.1, which is what clears the remaining doc-number drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-16 13:06:52 +03:00

215 lines
7.0 KiB
Python

"""Hold the numbers in the documents equal to the numbers in the tree.
Every figure this checks has been wrong at least once, and the expensive one was
wrong in three files at the same time: `benchmarks/README.md` and `CLAUDE.md`
described the A/B suite as 24 checks at 87.5% while the suite held 26 and the
published report said 80.8%. Nothing caught it because each file was edited on
its own and nobody re-read the other two.
The rule is the one the release snapshot already follows: a number a human
types is a number that rots, so derive it and fail when the prose disagrees.
python scripts/check_doc_numbers.py # report and exit non-zero on drift
python scripts/check_doc_numbers.py --list # show every derived value
`tests/test_release_consistency.py` runs this as a gate.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _read(relative: str) -> str:
return (ROOT / relative).read_text(encoding="utf-8")
def _json(relative: str) -> dict:
return json.loads(_read(relative))
def _collected_tests() -> int:
"""How many tests pytest actually collects.
On the same README line as tools/resources/prompts, and the only number on
it that nothing derived -- so it drifted by 95 while its neighbours stayed
exact. Collection only; running the suite here would make the gate cost a
minute.
"""
env = dict(os.environ, AUTOCAD_MCP_BACKEND="ezdxf")
result = subprocess.run(
[sys.executable, "-m", "pytest", "--collect-only", "-q"],
cwd=ROOT,
capture_output=True,
text=True,
env=env,
timeout=300,
)
match = re.search(r"(\d+) tests collected", result.stdout)
if not match:
raise SystemExit(
"check_doc_numbers: could not read a collected-test count from pytest.\n"
+ result.stdout[-2000:]
)
return int(match.group(1))
def derived() -> dict[str, object]:
"""Every number a document is allowed to quote, computed from the tree."""
sys.path.insert(0, str(ROOT))
inventory = _json("docs/tool-inventory.json")["totals"]
ab = _json("benchmarks/results/published/ab-v1.5.0-vs-v1.5.1.json")["summary"]
perf = {
w["workload_id"]: w["wall_ms"]
for w in _json("benchmarks/results/published/perf-ezdxf.json")["workloads"]
}
from benchmarks import correctness_suite
from benchmarks.tasks_v4 import TASKS_V4
return {
"collected_tests": _collected_tests(),
"tools": inventory["tools"],
"resources": inventory["resources"],
"prompts": inventory["prompts"],
"groups": inventory["groups"],
"ab_total": ab["total"],
"ab_old_pass": ab["old_pass"],
"ab_old_pct": ab["old_pct"],
"ab_fixed": ab["fixed"],
"ab_regressed": ab["regressed"],
"suite_checks": len(correctness_suite.CHECKS),
"v4_tasks": len(TASKS_V4),
"perf_ms": perf,
}
#: (relative path, human label, regex over the file, callable -> expected value)
#:
#: Each pattern captures the number as group 1. A pattern that matches nothing is
#: itself a failure: the sentence it was written for has moved, and a silent
#: no-op gate is worse than no gate.
CHECKS: list[tuple[str, str, str, str]] = [
("README.md", "release snapshot tools", r"release snapshot:\*\*\s*(\d+) tools", "tools"),
(
"README.md",
"release snapshot resources",
r"release snapshot:.*?(\d+) resources",
"resources",
),
(
"README.md",
"release snapshot prompts",
r"release snapshot:.*?(\d+) prompt templates",
"prompts",
),
("README.md", "A/B suite size", r"(\d+) deterministic headless checks", "ab_total"),
("README.md", "A/B baseline pass", r"v1\.5\.0 \*\(baseline\)\* \| (\d+) / \d+", "ab_old_pass"),
("README.md", "A/B suite denominator", r"v1\.5\.0 \*\(baseline\)\* \| \d+ / (\d+)", "ab_total"),
("benchmarks/README.md", "A/B suite size", r"(\d+) checks, ezdxf backend", "ab_total"),
(
"benchmarks/README.md",
"A/B baseline pass",
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| (\d+) / \d+",
"ab_old_pass",
),
(
"benchmarks/README.md",
"A/B suite denominator",
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| \d+ / (\d+)",
"ab_total",
),
(
"README.md",
"release snapshot collected tests",
r"release snapshot:.*?(\d+) collected tests",
"collected_tests",
),
(
"README.md",
"A/B baseline pass rate",
r"v1\.5\.0 \*\(baseline\)\* \| \d+ / \d+ \| ([\d.]+) %",
"ab_old_pct",
),
(
"README.md",
"A/B fixed count",
r"v1\.5\.1\*\* \*\(this release\)\* \| \*\*\d+ / \d+\*\* \| \*\*\d+ %\*\* \| (\d+)",
"ab_fixed",
),
(
"benchmarks/README.md",
"A/B baseline pass rate",
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| \d+ / \d+ \| ([\d.]+) %",
"ab_old_pct",
),
(
"benchmarks/README.md",
"A/B fixed count",
r"\*\*v1\.5\.1\*\* \(this release\) \| \d+ / \d+ \| \d+ % \| (\d+)",
"ab_fixed",
),
("CLAUDE.md", "A/B suite size", r"correctness_suite\.py` \((\d+) checks\)", "ab_total"),
("CLAUDE.md", "v4 task count", r"the current set \((\d+) tasks", "v4_tasks"),
]
def run(verbose: bool = False) -> int:
values = derived()
if values["suite_checks"] != values["ab_total"]:
print(
f"DRIFT correctness_suite.py has {values['suite_checks']} checks but the published "
f"A/B report was run over {values['ab_total']} -- regenerate the report:\n"
f" python benchmarks/compare_versions.py v1.5.0 "
f"--json benchmarks/results/published/ab-v1.5.0-vs-v1.5.1.json"
)
return 1
if verbose:
for key, value in values.items():
print(f" {key:20s} {value}")
print()
failures = 0
for relative, label, pattern, key in CHECKS:
text = _read(relative)
match = re.search(pattern, text, re.DOTALL)
expected = values[key]
if match is None:
print(
f"DRIFT {relative}: the sentence carrying '{label}' no longer matches its pattern"
)
failures += 1
continue
found = type(expected)(match.group(1))
if found != expected:
print(f"DRIFT {relative}: {label} says {found}, the tree says {expected}")
failures += 1
elif verbose:
print(f" ok {relative:24s} {label:24s} {found}")
if failures:
print(f"\n{failures} document number(s) disagree with the tree.")
return 1
print(f"ok: {len(CHECKS)} document numbers match the tree")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--list", action="store_true", help="Print every derived value")
return run(verbose=parser.parse_args().list)
if __name__ == "__main__":
raise SystemExit(main())