The v1.6 roadmap in five tracks: 154 -> 247 registered tools in 28 groups. Evidence re-measured for the release: - correctness A/B against v1.5.1: 26/48 -> 48/48, 22 fixed, 0 regressed (ab-v1.5.1-vs-v1.6.0.json, replacing the -dev report); - the reference adapter on matrix v7: 21/21 (autocad-mcp-pro.json, which was the v3 run of 1.5); the published-report test follows it to v7; - the 100-point rubric re-scored per category, every withheld point named: 93 (the competitor rows are their July snapshots, not re-reviewed); - the perf lane as medians of five alternating runs, beside a v1.4.0 comparison on one interpreter (perf-v1.4.0-vs-v1.6.0.json). Two roadmap criteria are not green, and the changelog, the README and the roadmap say so: criterion 5 is withdrawn (track D, Plant 3D, dropped) and criterion 7 is not met (creation 1.25x / 1.38x v1.4.0's time, still 1.17x / 1.20x with the timeout off). Carried to 1.7. README rewritten for 1.6: what is new, the mechanical and architectural sheets, the rubric and task-matrix charts, the measured limits (including the four feature tables that ship empty), and the 1.7 roadmap. server.json describes the 1.6 surface and declares TOOL_PACKS. CLAUDE.md corrects the ISO 286 letter set and the empty feature tables. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
233 lines
7.5 KiB
Python
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.json"
|
|
AB_BASELINE_LABEL = r"v1\.5\.1"
|
|
AB_CURRENT_LABEL = r"v1\.6\.0"
|
|
|
|
|
|
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_v7 import TASKS_V7
|
|
|
|
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_V7),
|
|
"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 release\)\* \| \*\*\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 release\) \| \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())
|