mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
953fd7d5f8
* Spike: typed parameter contracts and JSON Schemas for solid.box and solid.extrude The schema RFC's spike (plan/schema-spike.md). solid.box and solid.extrude get typed contracts with generated JSON Schemas (2020-12), checked in front of the unchanged handlers by a per-session level: off (the default, which changes nothing), warn (the call runs and gains param_warnings) or strict (the call is refused and the design left alone). Also engine.describe and the MCP tool describe_command, `solidcraft-cli mcp --param-check off|warn|strict`, a FeatureResult type for feature replies, and an env-var audit recorder (SOLIDCRAFT_PARAM_AUDIT) used to check the contracts against every test. Local spike branch; not for upstream as is (see the results report). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Parameter contracts: put the audit recorder behind a feature; drop two ignored test keys The audit recorder is now on only in a build with the engine's `param-audit` feature and `SOLIDCRAFT_PARAM_AUDIT` set. Its code is still compiled, linted and tested without the feature. `scripts/param_audit.py` summarises a test run's audit logs, and the schema module's docs give the steps for converting a further command. Two tests passed keys their command ignores, which the audit found: `position` on `solid.box` (`hole_face_tests.rs`) and `symmetric` on `solid.extrude` (`face_tests.rs`). Both are removed, so the tests build what they always did. Comments no longer call the contracts a spike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
89 lines
3.6 KiB
Python
89 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Summarise a parameter-contract audit: the calls that break a command's contract, by issue.
|
|
|
|
Usage: param_audit.py AUDIT_DIR [OUT_JSON]
|
|
|
|
AUDIT_DIR is the directory `SOLIDCRAFT_PARAM_AUDIT` named for a test run built with the engine's
|
|
`param-audit` feature (see crates/engine/src/schema/record.rs). Each process writes one
|
|
audit-<pid>.jsonl, repeating a signature's line as its count grows, so this keeps the highest
|
|
count per signature in each file and adds the files up.
|
|
|
|
The report has three parts:
|
|
SUCCEEDS TODAY calls that work now and that strict would refuse: a form the contract is
|
|
missing (fix the contract) or a mistake the handler ignores (fix the caller)
|
|
ALREADY FAILS calls the handler rejects anyway
|
|
REFUSED calls strict refused (tests that run with strict on)
|
|
|
|
OUT_JSON, when given, receives every signature with its summed count.
|
|
"""
|
|
import json
|
|
import sys
|
|
from collections import Counter, defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
def load(audit_dir):
|
|
signatures = {}
|
|
malformed = []
|
|
files = sorted(audit_dir.glob("audit-*.jsonl"))
|
|
for f in files:
|
|
per_file = {}
|
|
for n, line in enumerate(f.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
r = json.loads(line)
|
|
key = json.dumps([r["command"], r["issues"], r["outcome"], r["source"]])
|
|
except (json.JSONDecodeError, KeyError) as e:
|
|
malformed.append(f"{f.name}:{n}: {e}")
|
|
continue
|
|
if key not in per_file or r["count"] > per_file[key]["count"]:
|
|
per_file[key] = r
|
|
for key, r in per_file.items():
|
|
if key in signatures:
|
|
signatures[key] = {**signatures[key], "count": signatures[key]["count"] + r["count"]}
|
|
else:
|
|
signatures[key] = dict(r)
|
|
return files, signatures, malformed
|
|
|
|
|
|
def by_issue(records):
|
|
counts = Counter()
|
|
examples = defaultdict(list)
|
|
for r in records:
|
|
for issue in r["issues"]:
|
|
counts[(r["command"], issue)] += r["count"]
|
|
if len(examples[(r["command"], issue)]) < 3:
|
|
examples[(r["command"], issue)].append(f"{r['source']}: {r['sample_params'][:300]}")
|
|
return counts, examples
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) not in (2, 3):
|
|
sys.exit(__doc__.split("\n\n")[1])
|
|
files, signatures, malformed = load(Path(sys.argv[1]))
|
|
records = list(signatures.values())
|
|
parts = [
|
|
("SUCCEEDS TODAY, STRICT WOULD REFUSE", [r for r in records if r["outcome"] == "ok"], True),
|
|
("ALREADY FAILS TODAY", [r for r in records if r["outcome"] not in ("ok", "refused")], False),
|
|
("REFUSED BY STRICT", [r for r in records if r["outcome"] == "refused"], False),
|
|
]
|
|
print(f"files: {len(files)} signatures: {len(signatures)} malformed lines: {len(malformed)}")
|
|
for m in malformed[:10]:
|
|
print(" malformed:", m)
|
|
for label, part, show_examples in parts:
|
|
counts, examples = by_issue(part)
|
|
print(f"\n== {label}: {len(part)} signatures, at least {sum(r['count'] for r in part)} calls")
|
|
for (command, issue), n in counts.most_common():
|
|
print(f" {n:>7} {command:14} {issue}")
|
|
if show_examples:
|
|
for e in examples[(command, issue)]:
|
|
print(f" e.g. {e}")
|
|
if len(sys.argv) == 3:
|
|
out = {"signatures": records, "malformed": malformed}
|
|
Path(sys.argv[2]).write_text(json.dumps(out, indent=1), encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|