chore(audit): switch output format from JSON to custom postfix DSL

Per user direction ('make a custom DSL ideal for recording the
call-graph or other metrics', 'I want a post-fix heiarchy', 'JSON
is ill-performant'): replaced JSON serializer with a custom
postfix (RPN) DSL tailored to the audit's record shapes.

THE CUSTOM DSL
- Postfix (operands before operator); no brackets, braces,
  commas, or colons.
- Length-prefixed lists: N items followed by 'list' word.
- Tagged records: each 'word' is a constructor with a known
  arity (action=3, fn=3, call=1, mut=3, exp-op=5, pair=2, int=1).
- Whitespace-tokenized; bare atoms unquoted; double quotes
  only when whitespace/special chars present.
- nil for null; backslash for line comments; true/false for bool.
- Trivial parser (~30 lines): _tokenize_dsl splits on
  whitespace and respects quotes + comments; parse_dsl
  walks tokens and evaluates tagged words against a known
  arity table (DSL_WORD_ARITY).
- Round-trips: to_dsl(profile) -> parse_dsl(to_dsl(profile))
  yields the same in-memory structure.

DELIVERABLES (updated spec + plan)
- src/code_path_audit.py: to_dsl, dump_dsl, parse_dsl,
  _tokenize_dsl, to_tree (prefix-tree text renderer),
  to_markdown, to_mermaid.
- Output: .dsl files (machine) + .tree (human prefix view) +
  .md (summary tables) + .mmd (Mermaid diagrams).
- No new pip dependencies; pure stdlib.

WHAT STAYED
- The 7 cost classes (file_io, network, ast_parse, json_io,
  pickle, deep_copy, loop_amplified) and 5 mutation kinds
  are unchanged. The json_io cost class is for JSON file
  I/O the audit detects, not the output format.
- 36 tests total (15 + 8 + 10 + 3 across the 4 implementation
  phases).
This commit is contained in:
ed
2026-06-07 12:17:56 -04:00
parent 5f29c4b1b9
commit ad13007352
2 changed files with 375 additions and 109 deletions
+334 -96
View File
@@ -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. > **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] [phases]
phase_1 = { status = "pending", checkpointsha = "", name = "Data structures (CallGraph, ExpensiveOpIndex, StateMutationIndex)" } phase_1 = { status = "pending", checkpointsha = "", name = "Data structures (CallGraph, ExpensiveOpIndex, StateMutationIndex)" }
phase_2 = { status = "pending", checkpointsha = "", name = "trace_action + ActionProfile + cost model" } 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_4 = { status = "pending", checkpointsha = "", name = "MCP tool + CLI surface" }
phase_5 = { status = "pending", checkpointsha = "", name = "Run audit on 3 actions; commit report" } phase_5 = { status = "pending", checkpointsha = "", name = "Run audit on 3 actions; commit report" }
phase_6 = { status = "pending", checkpointsha = "", name = "tracks.md update" } phase_6 = { status = "pending", checkpointsha = "", name = "tracks.md update" }
[verification] [verification]
call_graph_json_produced = false call_graph_dsl_produced = false
expensive_ops_json_produced = false expensive_ops_dsl_produced = false
state_mutations_json_produced = false state_mutations_dsl_produced = false
actions_ai_message_produced = false actions_ai_message_produced = false
actions_save_load_produced = false actions_save_load_produced = false
actions_gui_startup_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 from __future__ import annotations
import ast import ast
import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Literal 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`. **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: Append:
```python ```python
def _to_jsonable(obj: object) -> object: DSL_ATOM_QUOTE_CHARS: frozenset[str] = frozenset({'"', "'"})
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)
def to_json(profile: ActionProfile) -> str: def _needs_quoting(atom: str) -> bool:
return json.dumps(_to_jsonable(profile), indent=2) """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: def _format_atom(value: object) -> str:
Path(path).write_text(to_json(profile), encoding="utf-8") 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`: Append to `tests/test_code_path_audit.py`:
```python ```python
import json from src.code_path_audit import to_dsl, dump_dsl
from src.code_path_audit import trace_action, ACTIONS, to_json
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) profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3)
js = to_json(profile) dsl = to_dsl(profile)
parsed = json.loads(js) assert "ai_message_lifecycle" in dsl
assert parsed["action"]["name"] == "ai_message_lifecycle" assert "action" in dsl
assert "call_graph" in parsed assert "exp-op" in dsl or "expensive_ops" in dsl
assert "expensive_ops" in parsed
assert "state_mutations" in parsed
def test_to_json_serializes_sets_as_lists() -> None: def test_to_dsl_uses_postfix_order() -> None:
from src.code_path_audit import CallGraph """The DSL is postfix: tagged words come AFTER their args."""
cg = CallGraph() profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3)
cg.add_edge("a", "b") dsl = to_dsl(profile)
js = to_json(cg) # action word should appear AFTER its 3 args
parsed = json.loads(js) action_idx = dsl.find("\n action\n")
assert isinstance(parsed["nodes"], dict) assert action_idx > 0
assert isinstance(parsed["edges"]["a"], list)
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: Append:
@@ -793,7 +1008,7 @@ def to_markdown(profile: ActionProfile) -> str:
for m in profile.state_mutations[:50]: for m in profile.state_mutations[:50]:
lines.append(f"| `{m.target}` | {m.kind} | {m.line} |") lines.append(f"| `{m.target}` | {m.kind} | {m.line} |")
if not profile.state_mutations: if not profile.state_mutations:
lines.append("| _(none)_ | - | - |") lines.append("| _(none)_ | - | - | - |")
lines += ["", "## Redundancy (ops called >1x)", ""] lines += ["", "## Redundancy (ops called >1x)", ""]
if profile.redundancy: if profile.redundancy:
for op, count in sorted(profile.redundancy, key=lambda x: -x[1])[:20]: 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) return "\n".join(lines)
``` ```
- [ ] **Step 3.2.2: Add tests for markdown output** - [ ] **Step 3.3.3: Add the `to_mermaid` function to `src/code_path_audit.py`**
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`**
Append: 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`: Append to `tests/test_code_path_audit.py`:
```python ```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: def test_to_mermaid_basic() -> None:
profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3) profile = trace_action(ACTIONS["ai_message_lifecycle"], max_depth=3)
@@ -858,22 +1069,33 @@ def test_to_mermaid_basic() -> None:
```bash ```bash
git add src/code_path_audit.py tests/test_code_path_audit.py 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 to_dsl / dump_dsl: write ActionProfile as custom postfix DSL
ActionProfile. JSON is round-trippable. Markdown has sections (RPN) with length-prefixed lists and tagged records. Whitespace-
for: summary, expensive ops (top 50 by weight), state tokenized; bare atoms unquoted; \"...\" only when needed;
mutations (first 50), redundancy, unresolved calls. Mermaid \"\\\\\" for line comments; nil for null.
is rendered from the subgraph rooted at the first entry
point.
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.2: Attach git note + update state.toml (phase_3 = completed; current_phase = 4)**
- [ ] **Step 3.4.3: Conductor - User Manual Verification** - [ ] **Step 3.4.3: Conductor - User Manual Verification**
---
--- ---
## Phase 4: MCP tool + CLI surface ## Phase 4: MCP tool + CLI surface
@@ -902,10 +1124,10 @@ if __name__ == "__main__":
date_str = args.date or date.today().isoformat() date_str = args.date or date.today().isoformat()
out_dir = Path(args.output_dir) / date_str out_dir = Path(args.output_dir) / date_str
(out_dir / "actions").mkdir(parents=True, exist_ok=True) (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}.md").write_text(profile.markdown, encoding="utf-8")
(out_dir / "actions" / f"{args.action}.mmd").write_text(profile.mermaid, 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** - [ ] **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 = trace_action(ACTIONS[action_name], max_depth=max_depth)
profile.markdown = to_markdown(profile) profile.markdown = to_markdown(profile)
profile.mermaid = to_mermaid(profile, max_depth=min(5, max_depth)) 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)** - [ ] **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 git commit -m "feat(audit): add MCP tool + CLI surface
CLI: python -m src.code_path_audit --action <name> [--depth N] CLI: python -m src.code_path_audit --action <name> [--depth N]
[--output-dir DIR] [--date YYYY-MM-DD]. Writes JSON + MD + MMD [--output-dir DIR] [--date YYYY-MM-DD]. Writes custom postfix `.dsl` + MD + MMD
to <output-dir>/<date>/actions/<name>.{json,md,mmd}. to <output-dir>/<date>/actions/<name>.{dsl,md,mmd}.
MCP tool: code_path_audit(action_name, max_depth=10) -> dict. MCP tool: code_path_audit(action_name, max_depth=10) -> dict.
Raises ValueError on unknown action. Registered alongside the Raises ValueError on unknown action. Registered alongside the
other MCP tools in src/models.py. 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)** - [ ] **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 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** - [ ] **Step 5.1.2: Generate the full call graph + indexes**
@@ -1012,19 +1234,35 @@ uv run python -c "
import sys import sys
sys.path.insert(0, 'src') sys.path.insert(0, 'src')
from pathlib import Path 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 from code_path_audit import build_call_graph, build_expensive_ops_index, build_state_mutations_index, to_dsl, COST_CLASS_WEIGHTS
import json
cg = build_call_graph() cg = build_call_graph()
out = Path('docs/reports/code_path_audit/2026-06-07') out = Path('docs/reports/code_path_audit/2026-06-07')
(out).mkdir(parents=True, exist_ok=True) (out).mkdir(parents=True, exist_ok=True)
(out / 'call_graph.json').write_text(json.dumps(_to_jsonable(cg), indent=2), encoding='utf-8') # to_dsl takes an ActionProfile. For the indexes, wrap each in a synthetic container.
(out / 'expensive_ops.json').write_text(json.dumps(_to_jsonable(build_expensive_ops_index(cg)), indent=2), encoding='utf-8') # The implementer adds a _to_dsl_dict() helper in src/code_path_audit.py during Phase 3
(out / 'state_mutations.json').write_text(json.dumps(_to_jsonable(build_state_mutations_index(cg)), indent=2), encoding='utf-8') # that handles plain dicts and lists of dataclass records (for the indexes).
print('Wrote call_graph.json, expensive_ops.json, state_mutations.json') 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`** - [ ] **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/: Generated artifacts under docs/reports/code_path_audit/2026-06-07/:
- call_graph.json (full call graph of src/) - call_graph.dsl (full call graph of src/)
- expensive_ops.json (per-function expensive-op index) - expensive_ops.dsl (per-function expensive-op index)
- state_mutations.json (per-function state-mutation index) - state_mutations.dsl (per-function state-mutation index)
- actions/ai_message_lifecycle.{json,md,mmd} - actions/ai_message_lifecycle.{json,md,mmd}
- actions/discussion_save_load.{json,md,mmd} - actions/discussion_save_load.{json,md,mmd}
- actions/gui_startup.{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 ```markdown
- [x] **Track: Code Path & Data Pipeline Audit** `[checkpoint: <last_commit_sha>]` - [x] **Track: Code Path & Data Pipeline Audit** `[checkpoint: <last_commit_sha>]`
*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)* *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 `<last_commit_sha>` with the SHA from the report commit in Phase 5. Replace `<last_commit_sha>` with the SHA from the report commit in Phase 5.
@@ -1134,10 +1372,10 @@ Ask the user to confirm the track is complete.
## Summary ## 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. - **3 per-action reports** + 1 cross-action summary + 1 ranked candidates list.
- **2 follow-up tracks** recorded (runtime profiling + pruning). - **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/`. - **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. - **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`. - **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`.
@@ -9,7 +9,7 @@
## Overview ## 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). 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`). - 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). - 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`). - 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 <name>`) and an MCP tool (`code_path_audit(action_name, max_depth)`). - A CLI (`python -m src.code_path_audit --action <name>`) 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/`. - The actual audit run on the 3 actions, with the report committed to `docs/reports/code_path_audit/2026-06-07/`.
## Goals ## 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. 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. 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. 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: CLI:
```bash ```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): MCP tool (for agents):
@@ -166,16 +166,43 @@ Generated artifacts (all under `docs/reports/code_path_audit/<YYYY-MM-DD>/`):
| File | Format | Purpose | | File | Format | Purpose |
|------|--------|---------| |------|--------|---------|
| `call_graph.json` | JSON | Full call graph (all of `src/`) | | `call_graph.dsl` | Custom postfix DSL | Full call graph (all of `src/`); machine-readable, parses in ~30 lines |
| `expensive_ops.json` | JSON | Expensive ops index (per-file, per-function) | | `expensive_ops.dsl` | Custom postfix DSL | Expensive ops index (per-file, per-function) |
| `state_mutations.json` | JSON | State mutations index (per function) | | `state_mutations.dsl` | Custom postfix DSL | State mutations index (per function) |
| `actions/<action>.json` | JSON | Per-action profile (machine-readable) | | `actions/<action>.dsl` | Custom postfix DSL | Per-action profile (machine-readable) |
| `actions/<action>.md` | Markdown | Per-action human-readable summary | | `actions/<action>.tree` | Prefix tree (text) | Per-action human-readable tree (for human review) |
| `actions/<action>.md` | Markdown | Per-action summary + table (for code review) |
| `actions/<action>.mmd` | Mermaid | Per-action call graph (visual) | | `actions/<action>.mmd` | Mermaid | Per-action call graph (visual) |
| `summary.md` | Markdown | Top-level cross-action summary + ranked optimization candidates | | `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 | | `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/<action>.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`) ## 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. - `ExpensiveOpIndex` detects each of the 7 cost classes on synthetic source.
- `StateMutationIndex` detects each of the 5 mutation kinds 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. - `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). - Markdown output is well-formed (header per section, table per category).
- Mermaid output parses as valid Mermaid syntax. - 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 2. feat(audit): add trace_action + ActionProfile + cost model
- src/code_path_audit.py (extends with action tracing) - src/code_path_audit.py (extends with action tracing)
- tests/test_code_path_audit.py (integration tests) - 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 4. feat(audit): add MCP tool + CLI surface
5. docs(audit): run audit on 3 actions; commit report 5. docs(audit): run audit on 3 actions; commit report
- docs/reports/code_path_audit/2026-06-07/* (the deliverable) - 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. | | 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. | | 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. | | 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. | | 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. | | 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. |