Skip to content

Commit 33a6780

Browse files
Address Will's feedback
1 parent ff45f6b commit 33a6780

4 files changed

Lines changed: 144 additions & 98 deletions

File tree

scripts/gen_tool_types_manifest.py

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,52 +8,27 @@
88

99
from __future__ import annotations
1010

11-
import json
12-
import re
11+
import sys
1312
from pathlib import Path
1413

1514
_REPO_ROOT = Path(__file__).resolve().parents[1]
16-
_MANIFEST_PATH = _REPO_ROOT / "static" / "tool_types.json"
15+
if str(_REPO_ROOT) not in sys.path:
16+
sys.path.insert(0, str(_REPO_ROOT))
1717

18+
from utils.tool_types_manifest_io import ( # noqa: E402
19+
load_known_tool_types_from_dispatch,
20+
serialize_tool_types_manifest,
21+
)
1822

19-
def _load_known_tool_types(repo_root: Path) -> frozenset[str]:
20-
path = repo_root / "utils" / "tool_dispatch.py"
21-
text = path.read_text(encoding="utf-8")
22-
marker = "_FILE_ACTIVITY_HANDLERS: dict"
23-
start = text.find(marker)
24-
if start == -1:
25-
msg = f"could not find {marker} in {path}"
26-
raise ValueError(msg)
27-
brace_start = text.find("{", start)
28-
if brace_start == -1:
29-
msg = f"could not find opening brace for {marker} in {path}"
30-
raise ValueError(msg)
31-
depth = 0
32-
i = brace_start
33-
while i < len(text):
34-
ch = text[i]
35-
if ch == "{":
36-
depth += 1
37-
elif ch == "}":
38-
depth -= 1
39-
if depth == 0:
40-
body = text[brace_start + 1 : i]
41-
keys = re.findall(r'"([^"]+)":', body)
42-
if not keys:
43-
msg = f"no tool names found in {marker} in {path}"
44-
raise ValueError(msg)
45-
return frozenset(keys)
46-
i += 1
47-
msg = f"unbalanced braces in {marker} in {path}"
48-
raise ValueError(msg)
23+
_MANIFEST_PATH = _REPO_ROOT / "static" / "tool_types.json"
4924

5025

5126
def write_tool_types_manifest(path: Path | None = None, *, repo_root: Path | None = None) -> int:
5227
root = (repo_root or _REPO_ROOT).resolve()
5328
dest = path or root / "static" / "tool_types.json"
54-
known = _load_known_tool_types(root)
55-
payload = {"tool_types": sorted(known)}
56-
dest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
29+
dispatch_path = root / "utils" / "tool_dispatch.py"
30+
known = load_known_tool_types_from_dispatch(dispatch_path)
31+
dest.write_text(serialize_tool_types_manifest(known), encoding="utf-8")
5732
return len(known)
5833

5934

scripts/scaffold_tool_type.py

Lines changed: 36 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
js_render_result_name,
3535
js_render_use_name,
3636
)
37+
from utils.tool_types_manifest_io import ( # noqa: E402
38+
parse_file_activity_handler_names,
39+
serialize_tool_types_manifest,
40+
)
3741

3842
_FILE_ACTIVITY_HANDLER = {
3943
"none": "None",
@@ -125,7 +129,7 @@ def emit(self, record: ToolTypeRecord) -> list[Path]:
125129

126130
def _guard_not_present(self, record: ToolTypeRecord) -> None:
127131
dispatch_text = self.paths.tool_dispatch.read_text(encoding="utf-8")
128-
known = _parse_handlers_from_text(dispatch_text)
132+
known = parse_file_activity_handler_names(dispatch_text)
129133
if record.name in known:
130134
msg = f"tool type {record.name!r} already present in _FILE_ACTIVITY_HANDLERS"
131135
raise ValueError(msg)
@@ -439,9 +443,8 @@ def _plan_manifest(self, plan: _EmitPlan) -> None:
439443
if self.paths.tool_dispatch not in plan.pending:
440444
msg = "internal error: tool_dispatch was not staged before manifest"
441445
raise ValueError(msg)
442-
known = _parse_handlers_from_text(plan.pending[self.paths.tool_dispatch])
443-
payload = {"tool_types": sorted(known)}
444-
plan.stage(self.paths.tool_types_manifest, json.dumps(payload, indent=2) + "\n")
446+
known = parse_file_activity_handler_names(plan.pending[self.paths.tool_dispatch])
447+
plan.stage(self.paths.tool_types_manifest, serialize_tool_types_manifest(known))
445448

446449
def _plan_record(self, record: ToolTypeRecord, plan: _EmitPlan) -> None:
447450
path = self.paths.records_dir / f"{record.snake_name}.json"
@@ -485,36 +488,6 @@ def _append_to_block(text: str, block_start: str, entry: str) -> str:
485488
return text[:close_pos] + "\n" + entry + text[close_pos:]
486489

487490

488-
def _parse_handlers_from_text(text: str) -> frozenset[str]:
489-
marker = "_FILE_ACTIVITY_HANDLERS: dict"
490-
start = text.find(marker)
491-
if start == -1:
492-
msg = f"could not find {marker} in staged tool_dispatch.py"
493-
raise ValueError(msg)
494-
brace_start = text.find("{", start)
495-
if brace_start == -1:
496-
msg = "could not find opening brace for _FILE_ACTIVITY_HANDLERS"
497-
raise ValueError(msg)
498-
depth = 0
499-
i = brace_start
500-
while i < len(text):
501-
ch = text[i]
502-
if ch == "{":
503-
depth += 1
504-
elif ch == "}":
505-
depth -= 1
506-
if depth == 0:
507-
body = text[brace_start + 1 : i]
508-
keys = re.findall(r'"([^"]+)":', body)
509-
if not keys:
510-
msg = "no tool names found in staged _FILE_ACTIVITY_HANDLERS"
511-
raise ValueError(msg)
512-
return frozenset(keys)
513-
i += 1
514-
msg = "unbalanced braces in staged _FILE_ACTIVITY_HANDLERS"
515-
raise ValueError(msg)
516-
517-
518491
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
519492
parser = argparse.ArgumentParser(description=__doc__)
520493
parser.add_argument(
@@ -571,39 +544,42 @@ def _resolve_record(args: argparse.Namespace) -> ToolTypeRecord:
571544

572545

573546
def main(argv: list[str] | None = None) -> int:
574-
args = _parse_args(argv)
575-
record = _resolve_record(args)
576-
root = args.root.resolve()
577-
578-
if args.write_record_only:
579-
out = args.record or root / "tool_types" / f"{record.snake_name}.json"
580-
if args.dry_run:
581-
print(f"Would write record to {out}")
547+
try:
548+
args = _parse_args(argv)
549+
record = _resolve_record(args)
550+
root = args.root.resolve()
551+
552+
if args.write_record_only:
553+
out = args.record or root / "tool_types" / f"{record.snake_name}.json"
554+
if args.dry_run:
555+
print(f"Would write record to {out}")
556+
return 0
557+
record.save(out)
558+
print(f"Wrote {out}")
582559
return 0
583-
record.save(out)
584-
print(f"Wrote {out}")
585-
return 0
586560

587-
emitter = ScaffoldEmitter(root, dry_run=args.dry_run)
588-
try:
561+
emitter = ScaffoldEmitter(root, dry_run=args.dry_run)
589562
written = emitter.emit(record)
563+
564+
if args.dry_run:
565+
print(f"Dry run complete for {record.name} ({len(written)} artifacts planned)")
566+
else:
567+
print(f"Scaffolded {record.name}: {len(written)} files updated")
568+
print("Next: complete TODO stubs, then run:")
569+
print(
570+
" pytest tests/test_scaffold_tool_type.py tests/test_tool_dispatch_sync.py "
571+
"tests/test_tool_dispatch_ordering.py tests/test_tool_dispatch_adversarial.py "
572+
"tests/test_jsonl_parser.py tests/test_real_session_fixtures.py -q"
573+
)
574+
print(" npm test")
575+
return 0
576+
except json.JSONDecodeError as exc:
577+
print(f"scaffold_tool_type: invalid JSON: {exc}", file=sys.stderr)
578+
return 1
590579
except ValueError as exc:
591580
print(f"scaffold_tool_type: {exc}", file=sys.stderr)
592581
return 1
593582

594-
if args.dry_run:
595-
print(f"Dry run complete for {record.name} ({len(written)} artifacts planned)")
596-
else:
597-
print(f"Scaffolded {record.name}: {len(written)} files updated")
598-
print("Next: complete TODO stubs, then run:")
599-
print(
600-
" pytest tests/test_scaffold_tool_type.py tests/test_tool_dispatch_sync.py "
601-
"tests/test_tool_dispatch_ordering.py tests/test_tool_dispatch_adversarial.py "
602-
"tests/test_jsonl_parser.py tests/test_real_session_fixtures.py -q"
603-
)
604-
print(" npm test")
605-
return 0
606-
607583

608584
if __name__ == "__main__":
609585
raise SystemExit(main())

tests/test_scaffold_tool_type.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,32 @@ def test_overlap_invariant_resolves_explicit_guards() -> None:
9595
{
9696
"before_dispatch_id": "plan",
9797
"after_dispatch_id": "file_write",
98-
"before_guard": "is_plan_tool_result",
99-
"after_guard": "is_file_write_tool_result",
98+
"before_guard": "is_custom_plan_guard",
99+
"after_guard": "is_custom_write_guard",
100+
"reason": "test overlap",
101+
"fixture_id": "overlap_tool_fixture",
102+
"overlap_blob": {"plan": [], "filePath": "x", "content": "y"},
103+
}
104+
],
105+
},
106+
}
107+
)
108+
assert record.result is not None
109+
inv = record.result.overlap_invariants[0]
110+
assert inv.resolved_before_guard() == "is_custom_plan_guard"
111+
assert inv.resolved_after_guard() == "is_custom_write_guard"
112+
113+
114+
def test_overlap_invariant_falls_back_to_dispatch_id_guards() -> None:
115+
record = ToolTypeRecord.from_mapping(
116+
{
117+
"name": "OverlapTool",
118+
"result": {
119+
"dispatch_id": "overlap_tool",
120+
"overlap_invariants": [
121+
{
122+
"before_dispatch_id": "plan",
123+
"after_dispatch_id": "file_write",
100124
"reason": "test overlap",
101125
"fixture_id": "overlap_tool_fixture",
102126
"overlap_blob": {"plan": [], "filePath": "x", "content": "y"},
@@ -232,6 +256,26 @@ def test_dry_run_main_exits_zero(capsys: pytest.CaptureFixture[str]) -> None:
232256
assert "would write" in out
233257

234258

259+
def test_main_rejects_invalid_cli_name(capsys: pytest.CaptureFixture[str]) -> None:
260+
code = main(["--name", "NotSnake", "--dry-run"])
261+
assert code == 1
262+
err = capsys.readouterr().err
263+
assert "scaffold_tool_type:" in err
264+
assert "invalid snake_case name" in err
265+
266+
267+
def test_main_rejects_invalid_record_json(
268+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
269+
) -> None:
270+
bad_record = tmp_path / "bad.json"
271+
bad_record.write_text("{not json", encoding="utf-8")
272+
code = main(["--record", str(bad_record), "--dry-run"])
273+
assert code == 1
274+
err = capsys.readouterr().err
275+
assert "scaffold_tool_type:" in err
276+
assert "invalid JSON" in err
277+
278+
235279
def test_scaffold_rejects_duplicate_name(tmp_path: Path) -> None:
236280
repo = _mirror_repo(tmp_path)
237281
record = ToolTypeRecord.from_cli_name("example_tool")

utils/tool_types_manifest_io.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Parse and serialize ``static/tool_types.json`` from ``tool_dispatch.py`` source."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import re
7+
from pathlib import Path
8+
9+
_FILE_ACTIVITY_HANDLERS_MARKER = "_FILE_ACTIVITY_HANDLERS: dict"
10+
11+
12+
def parse_file_activity_handler_names(
13+
text: str, *, source: str = "tool_dispatch.py"
14+
) -> frozenset[str]:
15+
"""Return tool names listed in a ``_FILE_ACTIVITY_HANDLERS`` dict literal."""
16+
start = text.find(_FILE_ACTIVITY_HANDLERS_MARKER)
17+
if start == -1:
18+
msg = f"could not find {_FILE_ACTIVITY_HANDLERS_MARKER} in {source}"
19+
raise ValueError(msg)
20+
brace_start = text.find("{", start)
21+
if brace_start == -1:
22+
msg = f"could not find opening brace for {_FILE_ACTIVITY_HANDLERS_MARKER} in {source}"
23+
raise ValueError(msg)
24+
depth = 0
25+
i = brace_start
26+
while i < len(text):
27+
ch = text[i]
28+
if ch == "{":
29+
depth += 1
30+
elif ch == "}":
31+
depth -= 1
32+
if depth == 0:
33+
body = text[brace_start + 1 : i]
34+
keys = re.findall(r'"([^"]+)":', body)
35+
if not keys:
36+
msg = f"no tool names found in {_FILE_ACTIVITY_HANDLERS_MARKER} in {source}"
37+
raise ValueError(msg)
38+
return frozenset(keys)
39+
i += 1
40+
msg = f"unbalanced braces in {_FILE_ACTIVITY_HANDLERS_MARKER} in {source}"
41+
raise ValueError(msg)
42+
43+
44+
def load_known_tool_types_from_dispatch(path: Path) -> frozenset[str]:
45+
text = path.read_text(encoding="utf-8")
46+
return parse_file_activity_handler_names(text, source=str(path))
47+
48+
49+
def serialize_tool_types_manifest(known: frozenset[str]) -> str:
50+
payload = {"tool_types": sorted(known)}
51+
return json.dumps(payload, indent=2) + "\n"

0 commit comments

Comments
 (0)