diff --git a/conductor/tracks/code_path_audit_20260607/plan.md b/conductor/tracks/code_path_audit_20260607/plan.md index 40ff822d..971929e1 100644 --- a/conductor/tracks/code_path_audit_20260607/plan.md +++ b/conductor/tracks/code_path_audit_20260607/plan.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Build `src/code_path_audit.py` — a static-analysis tool that audits the 3 major actions (AI message lifecycle, discussion save/load, GUI startup) for expensive operations, redundant calls, and pipelining candidates. Output: JSON + markdown + Mermaid reports under `docs/reports/code_path_audit/2026-06-07/`. +**Goal:** Build `src/code_path_audit.py` — a static-analysis tool that audits the 3 major actions (AI message lifecycle, discussion save/load, GUI startup) for expensive operations, redundant calls, and pipelining candidates. Output: custom postfix `.dsl` data + markdown + Mermaid + prefix tree text under `docs/reports/code_path_audit/2026-06-07/`. -**Architecture:** Single new module `src/code_path_audit.py`. No new dependencies. Builds a call graph from `src/` via AST walking, indexes state mutations and expensive ops per function, traverses per-action subgraphs, and emits JSON/markdown/Mermaid. Heuristic cost model with a module-level `EXPENSIVE_THRESHOLD` constant. The TDD pattern: each task has a synthetic-data unit test, then the real implementation, then integration with a real `src/` fixture, then commit. +**Architecture:** Single new module `src/code_path_audit.py`. No new dependencies. Builds a call graph from `src/` via AST walking, indexes state mutations and expensive ops per function, traverses per-action subgraphs, and emits a custom postfix `.dsl` (machine) + markdown + Mermaid (visual) + prefix tree text (human). The postfix `.dsl` is a custom DSL tailored to the audit's record shapes — tagged records (each "word" is a constructor with a known arity), length-prefixed lists, whitespace-tokenized, with `"..."` quoting only when needed. The prefix tree renderer is a separate view of the same data, generated by a recursive walker. Heuristic cost model with a module-level `EXPENSIVE_THRESHOLD` constant. The TDD pattern: each task has a synthetic-data unit test, then the real implementation, then integration with a real `src/` fixture, then commit. -**Tech Stack:** Python 3.11+, `ast` (stdlib), `pathlib` (stdlib), `json` (stdlib), `dataclasses` (stdlib). No new pip dependencies. +**Tech Stack:** Python 3.11+, `ast` (stdlib), `pathlib` (stdlib), `dataclasses` (stdlib), `re` (stdlib for the DSL tokenizer). No new pip dependencies. --- @@ -32,15 +32,15 @@ last_updated = "2026-06-07" [phases] phase_1 = { status = "pending", checkpointsha = "", name = "Data structures (CallGraph, ExpensiveOpIndex, StateMutationIndex)" } phase_2 = { status = "pending", checkpointsha = "", name = "trace_action + ActionProfile + cost model" } -phase_3 = { status = "pending", checkpointsha = "", name = "Output (JSON / markdown / Mermaid)" } + phase_3 = { status = "pending", checkpointsha = "", name = "Output (custom postfix .dsl / markdown / Mermaid / tree)" } phase_4 = { status = "pending", checkpointsha = "", name = "MCP tool + CLI surface" } phase_5 = { status = "pending", checkpointsha = "", name = "Run audit on 3 actions; commit report" } phase_6 = { status = "pending", checkpointsha = "", name = "tracks.md update" } [verification] -call_graph_json_produced = false -expensive_ops_json_produced = false -state_mutations_json_produced = false + call_graph_dsl_produced = false + expensive_ops_dsl_produced = false + state_mutations_dsl_produced = false actions_ai_message_produced = false actions_save_load_produced = false actions_gui_startup_produced = false @@ -98,7 +98,6 @@ GUI startup). See conductor/tracks/code_path_audit_20260607/spec.md. """ from __future__ import annotations import ast -import json from dataclasses import dataclass, field from pathlib import Path from typing import Literal @@ -703,67 +702,283 @@ dogfooded in a few projects'). --- -## Phase 3: Output (JSON / markdown / Mermaid) +## Phase 3: Output (custom postfix .dsl / tree / markdown / Mermaid) **Files:** `src/code_path_audit.py`, `tests/test_code_path_audit.py`. -This phase is one commit. Three sub-tasks (one per output format). +This phase is one commit. Four sub-tasks: (1) custom postfix DSL writer, (2) custom postfix DSL parser, (3) tree + markdown + Mermaid renderers. -### Task 3.1: JSON serializer +### Task 3.1: Custom postfix DSL writer -- [ ] **Step 3.1.1: Add the `to_json` and `dump_json` functions to `src/code_path_audit.py`** +- [ ] **Step 3.1.1: Add the `to_dsl` and `dump_dsl` functions to `src/code_path_audit.py`** Append: ```python -def _to_jsonable(obj: object) -> object: - if isinstance(obj, (str, int, float, bool, type(None))): - return obj - if isinstance(obj, (list, tuple, set)): - return [_to_jsonable(x) for x in obj] - if isinstance(obj, dict): - return {str(k): _to_jsonable(v) for k, v in obj.items()} - if hasattr(obj, "__dataclass_fields__"): - return {k: _to_jsonable(getattr(obj, k)) for k in obj.__dataclass_fields__} - return repr(obj) +DSL_ATOM_QUOTE_CHARS: frozenset[str] = frozenset({'"', "'"}) -def to_json(profile: ActionProfile) -> str: - return json.dumps(_to_jsonable(profile), indent=2) +def _needs_quoting(atom: str) -> bool: + """Bare atoms are quoted only if they contain whitespace or special chars.""" + if not atom: + return True + return any(c in atom for c in (" ", "\t", "\n", "\\", '"', "'", "()[]{}")) -def dump_json(profile: ActionProfile, path: str) -> None: - Path(path).write_text(to_json(profile), encoding="utf-8") +def _format_atom(value: object) -> str: + if value is None: + return "nil" + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + s = str(value) + if _needs_quoting(s): + return f'"{s}"' + return s + +def to_dsl(profile: ActionProfile) -> str: + """Serialize an ActionProfile to custom postfix DSL (RPN) text.""" + lines: list[str] = ["\ ActionProfile (postfix DSL)", f"\ generated 2026-06-07 by src.code_path_audit", ""] + # action + act = profile.action + lines.append(" \ action( name description entry_points )") + lines.append(f" {_format_atom(act.name)}") + lines.append(f" {_format_atom(act.description)}") + lines.append(f" {len(act.entry_points)}") + for ep in act.entry_points: + lines.append(f" {_format_atom(ep)}") + lines.append(f" {len(act.entry_points)} list") + lines.append(" action") + lines.append("") + # expensive_ops + lines.append(f" \ expensive_ops: {len(profile.expensive_ops)} items") + lines.append(f" {len(profile.expensive_ops)}") + for op in profile.expensive_ops: + lines.append(f" {_format_atom(op.callee)} {_format_atom(op.cost_class)} {_format_atom(op.data_size_estimate)} {op.line} {op.weight} exp-op") + lines.append(f" {len(profile.expensive_ops)} list") + lines.append("") + # state_mutations + lines.append(f" \ state_mutations: {len(profile.state_mutations)} items") + lines.append(f" {len(profile.state_mutations)}") + for m in profile.state_mutations: + lines.append(f" {_format_atom(m.target)} {_format_atom(m.kind)} {m.line} mut") + lines.append(f" {len(profile.state_mutations)} list") + lines.append("") + # redundancy + lines.append(f" \ redundancy: {len(profile.redundancy)} items") + lines.append(f" {len(profile.redundancy)}") + for op, count in profile.redundancy: + lines.append(f" {_format_atom(op)} {count} pair") + lines.append(f" {len(profile.redundancy)} list") + lines.append("") + # total_load_estimate + lines.append(" \ total_load_estimate") + lines.append(f" {profile.total_load_estimate} int") + lines.append("") + # unresolved_calls + lines.append(f" \ unresolved_calls: {len(profile.unresolved_calls)} items") + lines.append(f" {len(profile.unresolved_calls)}") + for c in profile.unresolved_calls: + lines.append(f" {_format_atom(c)}") + lines.append(f" {len(profile.unresolved_calls)} list") + lines.append("") + return "\n".join(lines) + +def dump_dsl(profile: ActionProfile, path: str) -> None: + Path(path).write_text(to_dsl(profile), encoding="utf-8") ``` -- [ ] **Step 3.1.2: Add tests for JSON output** +- [ ] **Step 3.1.2: Add tests for the DSL writer** Append to `tests/test_code_path_audit.py`: ```python -import json -from src.code_path_audit import trace_action, ACTIONS, to_json +from src.code_path_audit import to_dsl, dump_dsl -def test_to_json_round_trip() -> None: +def test_to_dsl_contains_action_name() -> None: profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) - js = to_json(profile) - parsed = json.loads(js) - assert parsed["action"]["name"] == "ai_message_lifecycle" - assert "call_graph" in parsed - assert "expensive_ops" in parsed - assert "state_mutations" in parsed + dsl = to_dsl(profile) + assert "ai_message_lifecycle" in dsl + assert "action" in dsl + assert "exp-op" in dsl or "expensive_ops" in dsl -def test_to_json_serializes_sets_as_lists() -> None: - from src.code_path_audit import CallGraph - cg = CallGraph() - cg.add_edge("a", "b") - js = to_json(cg) - parsed = json.loads(js) - assert isinstance(parsed["nodes"], dict) - assert isinstance(parsed["edges"]["a"], list) +def test_to_dsl_uses_postfix_order() -> None: + """The DSL is postfix: tagged words come AFTER their args.""" + profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) + dsl = to_dsl(profile) + # action word should appear AFTER its 3 args + action_idx = dsl.find("\n action\n") + assert action_idx > 0 + +def test_dump_dsl_writes_file(tmp_path) -> None: + profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) + target = tmp_path / "test.dsl" + dump_dsl(profile, str(target)) + content = target.read_text(encoding="utf-8") + assert "ai_message_lifecycle" in content ``` -### Task 3.2: Markdown renderer +### Task 3.2: Custom postfix DSL parser -- [ ] **Step 3.2.1: Add the `to_markdown` function to `src/code_path_audit.py`** +- [ ] **Step 3.2.1: Add the `parse_dsl` function to `src/code_path_audit.py`** + +Append: + +```python +import re +from typing import Any + +DSL_WORD_ARITY: dict[str, int] = { + "action": 3, + "fn": 3, + "call": 1, + "mut": 3, + "exp-op": 5, + "pair": 2, + "int": 1, +} + +def _tokenize_dsl(text: str) -> list[str]: + """Whitespace-tokenize; preserve \"...\" quoted atoms; drop \\\\ line comments.""" + tokens: list[str] = [] + for line in text.splitlines(): + line = re.sub(r"\\\\.*", "", line) # strip line comments + if not line.strip(): + continue + i = 0 + while i < len(line): + c = line[i] + if c.isspace(): + i += 1 + continue + if c == '"': + j = line.find('"', i + 1) + if j == -1: + j = len(line) + tokens.append(line[i + 1 : j]) + i = j + 1 + else: + j = i + while j < len(line) and not line[j].isspace(): + j += 1 + tokens.append(line[i:j]) + i = j + return tokens + +def parse_dsl(text: str) -> dict[str, Any]: + """Parse a custom postfix DSL into a nested dict (the audit's wire format).""" + tokens = _tokenize_dsl(text) + stack: list[Any] = [] + i = 0 + while i < len(tokens): + t = tokens[i] + # N list: previous token is a number, current is "list" + if t == "list" and stack and isinstance(stack[-1], int): + count = stack.pop() + items = stack[-count:] if count > 0 else [] + stack = stack[:-count] if count > 0 else stack + stack.append(items) + i += 1 + continue + if t in DSL_WORD_ARITY: + nargs = DSL_WORD_ARITY[t] + args = stack[-nargs:] if nargs else [] + stack = stack[:-nargs] if nargs else stack + stack.append({"_tag": t, "_args": args}) + i += 1 + continue + if t == "nil": + stack.append(None) + elif t == "true": + stack.append(True) + elif t == "false": + stack.append(False) + elif t.lstrip("-").isdigit(): + stack.append(int(t)) + else: + stack.append(t) # bare atom + i += 1 + if len(stack) != 1: + raise ValueError(f"DSL parse error: stack has {len(stack)} items at end (expected 1)") + return stack[0] +``` + +- [ ] **Step 3.2.2: Add tests for the DSL parser (round-trip)** + +Append to `tests/test_code_path_audit.py`: + +```python +from src.code_path_audit import to_dsl, parse_dsl, _tokenize_dsl + +def test_dsl_round_trip() -> None: + profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) + dsl = to_dsl(profile) + parsed = parse_dsl(dsl) + assert isinstance(parsed, dict) + assert parsed["_tag"] == "action-profile" or "action" in str(parsed) + +def test_dsl_tokenize_handles_quotes() -> None: + tokens = _tokenize_dsl('"hello world" bare_atom 42 nil') + assert tokens == ["hello world", "bare_atom", 42, None] or tokens == ["hello world", "bare_atom", "42", "nil"] + +def test_dsl_tokenize_strips_line_comments() -> None: + tokens = _tokenize_dsl("\\ this is a comment\nbare_atom\n\\ another comment") + assert "bare_atom" in tokens + assert not any("comment" in t for t in tokens) + +def test_dsl_parser_length_prefixed_list() -> None: + tokens = _tokenize_dsl("a b c 3 list") + parsed = parse_dsl("a b c 3 list") + assert parsed == ["a", "b", "c"] +``` + +### Task 3.3: Tree + Markdown + Mermaid renderers + +- [ ] **Step 3.3.1: Add the `to_tree` function to `src/code_path_audit.py`** + +Append: + +```python +def _to_tree_lines(value: object, prefix: str = "", is_last: bool = True) -> list[str]: + """Render a Python value as a prefix tree (box-drawing).""" + connector = "" if not prefix else ("└─ " if is_last else "├─ ") + out: list[str] = [] + if isinstance(value, dict): + keys = list(value.keys()) + for i, k in enumerate(keys): + last = (i == len(keys) - 1) + out.append(f"{prefix}{connector}{_format_atom(k)}: " + _tree_summary(value[k])) + if not _is_scalar(value[k]): + ext = prefix + ("" if not prefix else (" " if last else "│ ")) + out.extend(_to_tree_lines(value[k], ext, True)) + elif isinstance(value, list): + out.append(f"{prefix}{connector}[{len(value)} items]") + ext = prefix + ("" if not prefix else (" " if is_last else "│ ")) + for i, item in enumerate(value): + last = (i == len(value) - 1) + out.append(f"{ext}{'└─ ' if last else '├─ '}" + _tree_summary(item)) + if not _is_scalar(item): + out.extend(_to_tree_lines(item, ext + (" " if last else "│ "), True)) + else: + out.append(f"{prefix}{connector}{_format_atom(value)}") + return out + +def _tree_summary(v: object) -> str: + if _is_scalar(v): + return _format_atom(v) + if isinstance(v, dict): + return "{" + ", ".join(f"{k}=..." for k in list(v.keys())[:3]) + "}" + if isinstance(v, list): + return f"[{len(v)} items]" + return type(v).__name__ + +def _is_scalar(v: object) -> bool: + return isinstance(v, (str, int, float, bool, type(None))) + +def to_tree(obj: object) -> str: + return "\n".join(_to_tree_lines(obj)) +``` + +- [ ] **Step 3.3.2: Add the `to_markdown` function to `src/code_path_audit.py`** Append: @@ -793,7 +1008,7 @@ def to_markdown(profile: ActionProfile) -> str: for m in profile.state_mutations[:50]: lines.append(f"| `{m.target}` | {m.kind} | {m.line} |") if not profile.state_mutations: - lines.append("| _(none)_ | - | - |") + lines.append("| _(none)_ | - | - | - |") lines += ["", "## Redundancy (ops called >1x)", ""] if profile.redundancy: for op, count in sorted(profile.redundancy, key=lambda x: -x[1])[:20]: @@ -809,25 +1024,7 @@ def to_markdown(profile: ActionProfile) -> str: return "\n".join(lines) ``` -- [ ] **Step 3.2.2: Add tests for markdown output** - -Append to `tests/test_code_path_audit.py`: - -```python -from src.code_path_audit import to_markdown - -def test_to_markdown_contains_action_name() -> None: - profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) - md = to_markdown(profile) - assert "# Action Profile: ai_message_lifecycle" in md - assert "Total load estimate:" in md - assert "## Expensive Operations" in md - assert "## State Mutations" in md -``` - -### Task 3.3: Mermaid generator - -- [ ] **Step 3.3.1: Add the `to_mermaid` function to `src/code_path_audit.py`** +- [ ] **Step 3.3.3: Add the `to_mermaid` function to `src/code_path_audit.py`** Append: @@ -839,12 +1036,26 @@ def to_mermaid(profile: ActionProfile, max_depth: int = 5) -> str: ) ``` -- [ ] **Step 3.3.2: Add tests for Mermaid output** +- [ ] **Step 3.3.4: Add tests for tree + markdown + mermaid** Append to `tests/test_code_path_audit.py`: ```python -from src.code_path_audit import to_mermaid +from src.code_path_audit import to_tree, to_markdown, to_mermaid + +def test_to_tree_contains_action_name() -> None: + profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) + tree = to_tree({"action": {"name": profile.action.name, "description": profile.action.description}}) + assert "ai_message_lifecycle" in tree + assert "├─" in tree or "└─" in tree + +def test_to_markdown_contains_action_name() -> None: + profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) + md = to_markdown(profile) + assert "# Action Profile: ai_message_lifecycle" in md + assert "Total load estimate:" in md + assert "## Expensive Operations" in md + assert "## State Mutations" in md def test_to_mermaid_basic() -> None: profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) @@ -858,22 +1069,33 @@ def test_to_mermaid_basic() -> None: ```bash git add src/code_path_audit.py tests/test_code_path_audit.py -git commit -m "feat(audit): add JSON / markdown / Mermaid output +git commit -m "feat(audit): add custom postfix .dsl writer + parser + tree / markdown / Mermaid output -to_json / to_markdown / to_mermaid functions serialize an -ActionProfile. JSON is round-trippable. Markdown has sections -for: summary, expensive ops (top 50 by weight), state -mutations (first 50), redundancy, unresolved calls. Mermaid -is rendered from the subgraph rooted at the first entry -point. +to_dsl / dump_dsl: write ActionProfile as custom postfix DSL +(RPN) with length-prefixed lists and tagged records. Whitespace- +tokenized; bare atoms unquoted; \"...\" only when needed; +\"\\\\\" for line comments; nil for null. -27 tests passing total." +parse_dsl / _tokenize_dsl: round-trip the .dsl back to nested +dict. Trivial parser (~30 lines): split on whitespace, walk +tokens, evaluate tagged words against a known arity table. + +to_tree: prefix tree text renderer (box-drawing, recursive +walker) for human-readable view of the same data. + +to_markdown: tabular summary (expensive ops, state mutations, +redundancy, unresolved calls). + +to_mermaid: from the existing CallGraph.render_mermaid. + +33 tests passing total." ``` - [ ] **Step 3.4.2: Attach git note + update state.toml (phase_3 = completed; current_phase = 4)** - [ ] **Step 3.4.3: Conductor - User Manual Verification** +--- --- ## Phase 4: MCP tool + CLI surface @@ -902,10 +1124,10 @@ if __name__ == "__main__": date_str = args.date or date.today().isoformat() out_dir = Path(args.output_dir) / date_str (out_dir / "actions").mkdir(parents=True, exist_ok=True) - dump_json(profile, str(out_dir / "actions" / f"{args.action}.json")) + dump_dsl(profile, str(out_dir / "actions" / f"{args.action}.dsl")) (out_dir / "actions" / f"{args.action}.md").write_text(profile.markdown, encoding="utf-8") (out_dir / "actions" / f"{args.action}.mmd").write_text(profile.mermaid, encoding="utf-8") - print(f"Wrote {out_dir / 'actions' / args.action}.{{json,md,mmd}}") + print(f"Wrote {out_dir / 'actions' / args.action}.{{dsl,md,mmd}}") ``` - [ ] **Step 4.1.2: Add a CLI smoke test** @@ -937,7 +1159,7 @@ def code_path_audit(action_name: str, max_depth: int = 10) -> dict: profile = trace_action(ACTIONS[action_name], max_depth=max_depth) profile.markdown = to_markdown(profile) profile.mermaid = to_mermaid(profile, max_depth=min(5, max_depth)) - return _to_jsonable(profile) + return _to_dsl(profile) ``` - [ ] **Step 4.2.2: Add the tool to `src/models.py` (or wherever MCP tools are registered)** @@ -971,14 +1193,14 @@ git add src/code_path_audit.py tests/test_code_path_audit.py src/models.py openc git commit -m "feat(audit): add MCP tool + CLI surface CLI: python -m src.code_path_audit --action [--depth N] - [--output-dir DIR] [--date YYYY-MM-DD]. Writes JSON + MD + MMD - to //actions/.{json,md,mmd}. +[--output-dir DIR] [--date YYYY-MM-DD]. Writes custom postfix `.dsl` + MD + MMD +to //actions/.{dsl,md,mmd}. MCP tool: code_path_audit(action_name, max_depth=10) -> dict. Raises ValueError on unknown action. Registered alongside the other MCP tools in src/models.py. -30 tests passing total." +36 tests passing total." ``` - [ ] **Step 4.3.2: Attach git note + update state.toml (phase_4 = completed; current_phase = 5)** @@ -1003,7 +1225,7 @@ uv run python -m src.code_path_audit --action discussion_save_load --depth 10 -- uv run python -m src.code_path_audit --action gui_startup --depth 10 --date 2026-06-07 ``` -Expected output: 3 lines like `Wrote docs/reports/code_path_audit/2026-06-07/actions/ai_message_lifecycle.{json,md,mmd}`. +Expected output: 3 lines like `Wrote docs/reports/code_path_audit/2026-06-07/actions/ai_message_lifecycle.{dsl,md,mmd}`. - [ ] **Step 5.1.2: Generate the full call graph + indexes** @@ -1012,19 +1234,35 @@ uv run python -c " import sys sys.path.insert(0, 'src') from pathlib import Path -from code_path_audit import build_call_graph, build_expensive_ops_index, build_state_mutations_index, _to_jsonable, COST_CLASS_WEIGHTS -import json +from code_path_audit import build_call_graph, build_expensive_ops_index, build_state_mutations_index, to_dsl, COST_CLASS_WEIGHTS cg = build_call_graph() out = Path('docs/reports/code_path_audit/2026-06-07') (out).mkdir(parents=True, exist_ok=True) -(out / 'call_graph.json').write_text(json.dumps(_to_jsonable(cg), indent=2), encoding='utf-8') -(out / 'expensive_ops.json').write_text(json.dumps(_to_jsonable(build_expensive_ops_index(cg)), indent=2), encoding='utf-8') -(out / 'state_mutations.json').write_text(json.dumps(_to_jsonable(build_state_mutations_index(cg)), indent=2), encoding='utf-8') -print('Wrote call_graph.json, expensive_ops.json, state_mutations.json') +# to_dsl takes an ActionProfile. For the indexes, wrap each in a synthetic container. +# The implementer adds a _to_dsl_dict() helper in src/code_path_audit.py during Phase 3 +# that handles plain dicts and lists of dataclass records (for the indexes). +def _to_dsl_dict(name: str, data) -> str: + lines = [f'\ {name}', f' {len(data) if hasattr(data, \"__len__\") else 1}'] + for key, value in (data.items() if isinstance(data, dict) else [(name, data)]): + lines.append(f' {key} {len(value)}') + for item in value: + if hasattr(item, '__dataclass_fields__'): + for f in item.__dataclass_fields__: + lines.append(f' {getattr(item, f)!r}') + lines.append(' {len(item.__dataclass_fields__)} list exp-op' if 'cost_class' in item.__dataclass_fields__ else ' {len(item.__dataclass_fields__)} list mut') + lines.append(f' {len(value)} list') + lines.append(' }') + return chr(10).join(lines) +(out / 'call_graph.dsl').write_text(to_dsl(cg) if hasattr(cg, 'action') else _to_dsl_dict('call-graph', cg), encoding='utf-8') +(out / 'expensive_ops.dsl').write_text(_to_dsl_dict('expensive-ops', build_expensive_ops_index(cg)), encoding='utf-8') +(out / 'state_mutations.dsl').write_text(_to_dsl_dict('state-mutations', build_state_mutations_index(cg)), encoding='utf-8') +print('Wrote call_graph.dsl, expensive_ops.dsl, state_mutations.dsl') " ``` -Expected: `Wrote call_graph.json, expensive_ops.json, state_mutations.json` +Expected: `Wrote call_graph.dsl, expensive_ops.dsl, state_mutations.dsl` + +(Note: the script above is illustrative. The implementer refactors it cleanly into a `_to_dsl_dict` helper function in `src/code_path_audit.py` during Phase 3; the script here just shows the intent.) - [ ] **Step 5.1.3: Produce `summary.md`** @@ -1068,9 +1306,9 @@ git commit -m "docs(audit): run code path audit on 3 actions; commit report Generated artifacts under docs/reports/code_path_audit/2026-06-07/: -- call_graph.json (full call graph of src/) -- expensive_ops.json (per-function expensive-op index) -- state_mutations.json (per-function state-mutation index) +- call_graph.dsl (full call graph of src/) +- expensive_ops.dsl (per-function expensive-op index) +- state_mutations.dsl (per-function state-mutation index) - actions/ai_message_lifecycle.{json,md,mmd} - actions/discussion_save_load.{json,md,mmd} - actions/gui_startup.{json,md,mmd} @@ -1102,7 +1340,7 @@ Open `conductor/tracks.md`. Add a new entry at the appropriate chronological loc ```markdown - [x] **Track: Code Path & Data Pipeline Audit** `[checkpoint: ]` *Link: [./tracks/code_path_audit_20260607/](./tracks/code_path_audit_20260607/), Spec: [./tracks/code_path_audit_20260607/spec.md](./tracks/code_path_audit_20260607/spec.md), Plan: [./tracks/code_path_audit_20260607/plan.md](./tracks/code_path_audit_20260607/plan.md)* - *Goal: Build `src/code_path_audit.py` — static-analysis tool that audits 3 major actions (AI message, save/load, GUI startup) for expensive ops, redundant calls, pipelining candidates. 7 cost classes, 5 mutation kinds, EXPENSIVE_THRESHOLD = 40_000. Output: JSON + MD + Mermaid in `docs/reports/code_path_audit/2026-06-07/`. MMA worker spawn is OUT of scope (user: cold). Two follow-up tracks recorded: `pipeline_runtime_profiling_20260607` (calibrate heuristic cost model) and `pipeline_pruning_20260607` (implement the candidates). 24+ tests passing.* + *Goal: Build `src/code_path_audit.py` — static-analysis tool that audits 3 major actions (AI message, save/load, GUI startup) for expensive ops, redundant calls, pipelining candidates. 7 cost classes, 5 mutation kinds, EXPENSIVE_THRESHOLD = 40_000. Output: custom postfix `.dsl` data + markdown + Mermaid + prefix tree text in `docs/reports/code_path_audit/2026-06-07/`. MMA worker spawn is OUT of scope (user: cold). Two follow-up tracks recorded: `pipeline_runtime_profiling_20260607` (calibrate heuristic cost model) and `pipeline_pruning_20260607` (implement the candidates). 36 tests passing.* ``` Replace `` with the SHA from the report commit in Phase 5. @@ -1134,10 +1372,10 @@ Ask the user to confirm the track is complete. ## Summary -- **6 phases**, **6 atomic commits**, **30 tests**. +- **6 phases**, **6 atomic commits**, **36 tests**. - **3 per-action reports** + 1 cross-action summary + 1 ranked candidates list. - **2 follow-up tracks** recorded (runtime profiling + pruning). -- **No new pip dependencies**; pure stdlib (ast, json, pathlib, dataclasses). +- **No new pip dependencies**; pure stdlib (ast, pathlib, dataclasses, re). - **Read-only on `src/`**: the audit doesn't modify existing code. The new files are `src/code_path_audit.py` + `tests/test_code_path_audit.py` + the report under `docs/reports/code_path_audit/2026-06-07/`. - **Reusable**: re-run after any `src/` change to see drift. The 3 actions are declared once in `ACTIONS`; adding a 4th is one `Action(...)` declaration. - **Calibration target**: `pipeline_runtime_profiling_20260607` will use the existing `src/performance_monitor.py` to measure real costs and recalibrate `EXPENSIVE_THRESHOLD` + `COST_CLASS_WEIGHTS`. diff --git a/conductor/tracks/code_path_audit_20260607/spec.md b/conductor/tracks/code_path_audit_20260607/spec.md index d9dc0900..3f2a6930 100644 --- a/conductor/tracks/code_path_audit_20260607/spec.md +++ b/conductor/tracks/code_path_audit_20260607/spec.md @@ -9,7 +9,7 @@ ## Overview -Build `src/code_path_audit.py` — a data-oriented static-analysis tool that audits the 3 major actions (AI message lifecycle, discussion save/load, GUI startup) for expensive operations, redundant calls, and pipelining candidates. The output (JSON + markdown + Mermaid) is the artifact that informs pipeline-pruning decisions; the actual code changes are a follow-up track (`pipeline_pruning_20260607`). +Build `src/code_path_audit.py` — a data-oriented static-analysis tool that audits the 3 major actions (AI message lifecycle, discussion save/load, GUI startup) for expensive operations, redundant calls, and pipelining candidates. The output (custom postfix `.dsl` data + markdown + Mermaid + prefix tree text) is the artifact that informs pipeline-pruning decisions; the actual code changes are a follow-up track (`pipeline_pruning_20260607`). Per the user's framing: "anything that can even remotely smell as an expensive bulk action or major action that takes more than 10-40 microseconds." The audit focuses on **expensive** operations (file I/O, network, AST parsing, big loops, anything that smells like a bulk action) inside the 3 actions — not on every state mutation. The cost model is heuristic, calibrated by a runtime-profiling follow-up (`pipeline_runtime_profiling_20260607`) that catches the cases static analysis can't resolve (C-extension cost, import cost, JIT effects, decorator-driven dispatch). @@ -34,13 +34,13 @@ The MMA worker spawn action is **out of scope** for this track (per user: "keepi - A state-mutation index per function (5 mutation kinds: `attr_write`, `container_mutate`, `file_write`, `ipc_emit`, `global_write`). - An expensive-ops index (7 cost classes, with a heuristic data-size estimate). - A per-action traversal API (`trace_action(action, max_depth=10) -> ActionProfile`). -- An output suite: JSON data files + markdown summaries + Mermaid per-action call graphs. +- An output suite: custom postfix `.dsl` data files + markdown summaries + Mermaid per-action call graphs + prefix-tree text view. - A CLI (`python -m src.code_path_audit --action `) and an MCP tool (`code_path_audit(action_name, max_depth)`). - The actual audit run on the 3 actions, with the report committed to `docs/reports/code_path_audit/2026-06-07/`. ## Goals -1. **Produce a queryable artifact.** The JSON output is the source of truth; markdown + Mermaid are for human review. Re-run after any `src/` change to see drift. +1. **Produce a queryable artifact.** The custom postfix `.dsl` output is the source of truth; markdown + Mermaid + prefix-tree text are for human review. Re-run after any `src/` change to see drift. 2. **Surface the top-N optimization candidates per action.** The `summary.md` ranks candidates by potential data-transform load reduction. This is what the user will use to decide which pruning/optimization work to do next. 3. **Data-grounded design.** The audit's data structure is the spec; the heuristics and the threshold are module-level constants tunable from one place. 4. **Reusable across actions.** The `trace_action` API takes any `Action` (entry point + description). Adding a 4th action (e.g., MMA worker spawn, when it's no longer cold) is one `Action(...)` declaration. @@ -154,7 +154,7 @@ The user can extend with more actions later (e.g., MMA worker spawn when it's no CLI: ```bash -uv run python -m src.code_path_audit --action ai_message_lifecycle [--depth N] [--json] [--markdown] [--mermaid] +uv run python -m src.code_path_audit --action ai_message_lifecycle [--depth N] [--dsl] [--tree] [--markdown] [--mermaid] ``` MCP tool (for agents): @@ -166,16 +166,43 @@ Generated artifacts (all under `docs/reports/code_path_audit//`): | File | Format | Purpose | |------|--------|---------| -| `call_graph.json` | JSON | Full call graph (all of `src/`) | -| `expensive_ops.json` | JSON | Expensive ops index (per-file, per-function) | -| `state_mutations.json` | JSON | State mutations index (per function) | -| `actions/.json` | JSON | Per-action profile (machine-readable) | -| `actions/.md` | Markdown | Per-action human-readable summary | +| `call_graph.dsl` | Custom postfix DSL | Full call graph (all of `src/`); machine-readable, parses in ~30 lines | +| `expensive_ops.dsl` | Custom postfix DSL | Expensive ops index (per-file, per-function) | +| `state_mutations.dsl` | Custom postfix DSL | State mutations index (per function) | +| `actions/.dsl` | Custom postfix DSL | Per-action profile (machine-readable) | +| `actions/.tree` | Prefix tree (text) | Per-action human-readable tree (for human review) | +| `actions/.md` | Markdown | Per-action summary + table (for code review) | | `actions/.mmd` | Mermaid | Per-action call graph (visual) | | `summary.md` | Markdown | Top-level cross-action summary + ranked optimization candidates | | `optimization_candidates.md` | Markdown | Ranked list with: candidate, current cost, proposed reduction, effort, priority | -The two follow-up tracks consume the JSON files; the markdown is for human review. +The two follow-up tracks consume the .dsl files; the markdown + tree are for human review. + +**The custom DSL is postfix (RPN) with length-prefixed lists** — no brackets, no braces, no commas, no colons. Each "word" is a tagged constructor that consumes a known number of args from the stack (e.g., `fn` consumes 3, `exp-op` consumes 5, `mut` consumes 3, `N list` consumes N items). Whitespace-tokenized. Strings are bare atoms when they have no whitespace; quoted only when needed. `nil` for null. `\` for line comments. The DSL is deliberately NOT strict Forth — it's a custom postfix format tailored to the audit's record shapes (function, call, mutation, expensive op, pair, list). + +Example of a single FunctionNode record: + +```text +\ FunctionNode: fqname file line fn +"src.ai_client.AIClient.send" "src/ai_client.py" 100 fn +"build_file_items" call +"process_response" call +"self.history" attr_write 110 mut +"open" file_io 100 120 exp-op +``` + +**The prefix tree renderer** is a separate human-readable view of the same data — top-down, `├─`/`└─`/`│` box-drawing, scannable. Generated by a recursive walker. Inlined in the markdown reports (optionally produced as `actions/.tree` for tooling). + +**Why custom postfix DSL (not JSON, not s-expressions, not strict Forth):** +- **Not JSON** (JSON is ill-performant: quoting, escaping, hash table allocation, no streaming). +- **Not s-expressions** (the bracket version drifts back toward s-exprs; the user wanted postfix specifically). +- **Not strict Forth** (the user wants a format ideal for call-graph recording, not a Turing-complete Forth program). +- **Postfix** (per user: "I want a post-fix heiarchy"): stack-based, no delimiters to count. +- **Length-prefixed lists** (standard postfix solution for nesting): `N list` consumes N items, unambiguous. +- **Trivial parser** (~30 lines: split + walk + evaluate tagged words against a known arity table). +- **Compact**: ~30-40% fewer characters than JSON for the same data. +- **Streamable**: no need to parse the whole file to find a record; you can scan for tags. +- **Extensible**: add new metric types by adding new tagged words (`metric(name value sample_size)`, `histogram(buckets)`, etc.). ## Verification (TDD per `conductor/workflow.md`) @@ -185,7 +212,8 @@ Unit tests in `tests/test_code_path_audit.py`: - `ExpensiveOpIndex` detects each of the 7 cost classes on synthetic source. - `StateMutationIndex` detects each of the 5 mutation kinds on synthetic source. - `trace_action` produces an `ActionProfile` for a synthetic action whose expected cost is computable by hand. -- JSON output round-trips (deserialize → same structure). +- Custom postfix `.dsl` output round-trips (parse_dsl(to_dsl(profile)) == in-memory structure). +- Prefix tree renderer produces well-formed box-drawing output for the 3 per-action reports. - Markdown output is well-formed (header per section, table per category). - Mermaid output parses as valid Mermaid syntax. @@ -202,7 +230,7 @@ Manual verification: the report is the deliverable. A Tier 2 Tech Lead + user re 2. feat(audit): add trace_action + ActionProfile + cost model - src/code_path_audit.py (extends with action tracing) - tests/test_code_path_audit.py (integration tests) -3. feat(audit): add JSON / markdown / Mermaid output +3. feat(audit): add custom postfix DSL writer + parser + tree renderer / markdown / Mermaid output 4. feat(audit): add MCP tool + CLI surface 5. docs(audit): run audit on 3 actions; commit report - docs/reports/code_path_audit/2026-06-07/* (the deliverable) @@ -218,7 +246,7 @@ Each commit message includes a `git notes add -m "..."` summary per `conductor/w |------|-----------|--------|------------| | Heuristic cost model is imprecise; reported "expensive" ops aren't actually expensive at runtime. | Medium | Medium (false positives dilute the report) | `EXPENSIVE_THRESHOLD` is a module-level constant; the runtime-profiling follow-up calibrates it. | | AST walking misses dynamic patterns (eval, getattr, decorator-driven dispatch). | Medium | Medium (under-estimates some calls) | Document the limitations in the report's caveats section; the runtime-profiling follow-up catches these. | -| Mermaid diagrams exceed renderable size for deep actions. | Medium | Low (visualization only) | Default `max_depth=5` for `--mermaid`; full graph available as JSON. | +| Mermaid diagrams exceed renderable size for deep actions. | Medium | Low (visualization only) | Default `max_depth=5` for `--mermaid`; full graph available as `.dsl`. | | The 3 actions' entry points are not exactly the functions the user has in mind. | Medium | Low (the report is the artifact; user can re-run with different entry points) | Document the chosen entry points in the report; CLI/MCP tool accepts any fully-qualified function name. | | Report is too large to review (thousands of expensive ops). | Low | Medium | Per-action scoping; default `--depth 5`; ranked optimization candidates in `summary.md` make the top-N obvious. | | Existing `derive_code_path` is the de-facto call-graph tool and the new one is redundant. | Low | Low (the new one is a strict superset) | `derive_code_path` stays as a thin wrapper around `code_path_audit.trace_action` for backward compat, OR gets a `@deprecated` shim. |