Files
autocad-mcp-pro/scripts/check_doc_numbers.py
T
Umut CANandClaude Opus 5.5 0abbe55041 bench: matrix v6 adds arch_roundtrip; four architectural correctness checks; the live arch smoke
The evidence for track F, in the shape tracks A, E and B + G used.

- benchmarks/tasks_v6.py: 19 tasks, DEFAULT_MATRIX = "v6"; v5 to v2 stay
  addressable. arch_roundtrip draws the example plan with
  arch_plan_from_spec and reads it back without the spec: rooms_detect must
  find each labelled room within 0.1 % of its label and of the net floor
  computed by hand (4825 x 5750 and 3825 x 5750), the schedules must list
  D1, D2, W1, W2, and the plan is gated on a zero-issue critique and a
  finalize score of at least 90. A test removes one room label and the gate
  fails naming arch_room_unlabelled.
- Four correctness checks, every number worked out by hand in the docstring:
  L / T / X junction corners and wall areas, a 4 x 5 m room labelled 20.00 m2
  rather than its 21.84 m2 axis area, a door interrupting both faces, and a
  plan of plain lines read as 20 and 15 m2 at confidence 0.6. The published
  A/B report is re-run over 43 checks: 26 / 43 -> 43 / 43, zero regressed.
- scripts/smoke_arch_com.py runs the whole plan once on a live seat and
  closes every document it opens; the CHANGELOG records what it returned.
- The token suite measures a TOOL_PACKS=core,arch row; check_doc_numbers
  derives the task count from the v6 matrix; CLAUDE.md, both READMEs and
  the spec status line say what shipped.
- fix(com): the first live smoke exited 1 - the room reader found 0 wall
  segments. Measured on AutoCAD 2026 (2026-09-23): with a makepy cache,
  ModelSpace.Item(i) returns the gen_py IAcadEntity wrapper
  (type(item).__name__ == "IAcadEntity") and item.StartPoint on an
  AcDbLine raises AttributeError, so every entity_list row lost its
  type-specific properties (28 wall LINEs, 0 with start/end). _entity_info
  now re-dispatches exactly that wrapper through _com_unnarrow; after the
  fix 38 of 53 rows carry start/end and the smoke exits 0 with both rooms
  at 27 743 750 / 21 993 750 mm2 and 0 critique issues. Re-dispatch
  measured at ~1.7 ms per entity (dynamic vs gencache alike).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 23:35:46 +03:00

233 lines
7.5 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]
#: The published A/B report the README and benchmarks/README quote. Bump this,
#: the two row labels below and the prose together, or the gate reports drift.
AB_REPORT = "benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json"
AB_BASELINE_LABEL = r"v1\.5\.1"
AB_CURRENT_LABEL = r"v1\.6\.0-dev"
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(AB_REPORT)["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_v6 import TASKS_V6
return {
"collected_tests": _collected_tests(),
"tools": inventory["tools"],
"resources": inventory["resources"],
"prompts": inventory["prompts"],
"groups": inventory["groups"],
"ab_baseline_ref": ab["baseline_ref"],
"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),
"matrix_tasks": len(TASKS_V6),
"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",
AB_BASELINE_LABEL + r" \*\(baseline\)\* \| (\d+) / \d+",
"ab_old_pass",
),
(
"README.md",
"A/B suite denominator",
AB_BASELINE_LABEL + r" \*\(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"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| (\d+) / \d+",
"ab_old_pass",
),
(
"benchmarks/README.md",
"A/B suite denominator",
r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(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",
AB_BASELINE_LABEL + r" \*\(baseline\)\* \| \d+ / \d+ \| ([\d.]+) %",
"ab_old_pct",
),
(
"README.md",
"A/B fixed count",
AB_CURRENT_LABEL
+ r"\*\* \*\(this branch\)\* \| \*\*\d+ / \d+\*\* \| \*\*\d+ %\*\* \| (\d+)",
"ab_fixed",
),
(
"benchmarks/README.md",
"A/B baseline pass rate",
r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| \d+ / \d+ \| ([\d.]+) %",
"ab_old_pct",
),
(
"benchmarks/README.md",
"A/B fixed count",
r"\*\*" + AB_CURRENT_LABEL + r"\*\* \(this branch\) \| \d+ / \d+ \| \d+ % \| (\d+)",
"ab_fixed",
),
("CLAUDE.md", "A/B suite size", r"correctness_suite\.py` \((\d+) checks\)", "ab_total"),
("CLAUDE.md", "matrix task count", r"the current set \((\d+) tasks", "matrix_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 {values['ab_baseline_ref']} "
f"--json {AB_REPORT}"
)
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())