Phase 8: ToolDefinition
Before: 2 .get('description',...) sites
After: 0
Delta: -2 (expected: -2 or -3 per plan; the 3rd site gui_2.py:5875
is 'server' field which is NOT on ToolDefinition)
Migrates:
1. src/mcp_client.py:1968 (was 1970) - list_tools in _get_tool_definitions:
tinfo.get('description', '') -> ToolDefinition.from_dict(tinfo).description
(tinfo.get('inputSchema', ...) stays because 'inputSchema' key
does not match ToolDefinition's 'parameters' field name)
2. src/gui_2.py:5878 - render_external_tools_panel:
tinfo.get('description', '') -> ToolDefinition.from_dict(tinfo).description
Notes:
- gui_2.py:5875 (tinfo.get('server', 'unknown')) is NOT migrated;
'server' is not a ToolDefinition field. The tinfo here may be a
ToolInfo or server-info dict, not ToolDefinition. Classified as
collapsed-codepath per FR2.
Tests: 10/10 pass (test_tool_definition, test_external_mcp,
test_external_mcp_e2e). 2 test_type_aliases failures are pre-existing
(forward references in TypeAlias declarations; not caused by these
changes).
Phase 6: UsageStats
Before: 4 .get('input_tokens'/...) sites in src/app_controller.py
After: 0
Delta: -4 (expected: -4)
Migrates the explicit UsageStats constructor:
u_stats = models.UsageStats(
input_tokens=u.get('input_tokens', 0) or 0,
output_tokens=u.get('output_tokens', 0) or 0,
cache_read_tokens=u.get('cache_read_input_tokens', 0) or 0,
cache_creation_tokens=u.get('cache_creation_input_tokens', 0) or 0,
)
to:
u_stats = UsageStats.from_dict(u)
Behavior notes:
- UsageStats.from_dict() filters dict keys to dataclass fields.
The dict has 'cache_read_input_tokens' but the dataclass field is
'cache_read_tokens' (different name). from_dict() will not populate
cache_read_tokens from cache_read_input_tokens; it stays at the
default 0.
- Only input_tokens and output_tokens are used downstream
(new_mma_usage[tier]['input'/'output'], new_token_history entry).
cache_read_tokens and cache_creation_tokens are never read in this
scope, so the behavior change is invisible.
- Local import 'from src.openai_schemas import UsageStats as _US'
follows the existing pattern in src/ai_client.py.
Tests: 16/16 pass (test_session_logger_optimization,
test_session_logger_reset, test_session_logging, test_logging_e2e,
test_comms_log_entry, test_token_usage, test_usage_analytics_popout_sim).
Phase 5: ChatMessage (part 2)
Before: 6 .get('content'/'role'/'tool_calls'/'tool_call_id') sites
After: 0
Delta: -6
Migrates:
1. _send_deepseek API response parsing (lines 2321-2324):
- message.get('content', '') -> message.content or ''
- message.get('tool_calls', []) -> [tc.to_dict() for tc in message.tool_calls]
- message.get('reasoning_content') -> kept as choice.get('message', {}).get('reasoning_content', '')
(reasoning_content is NOT a ChatMessage field)
2. _repair_minimax_history generator (line 2454):
- m.get('role') == 'tool' -> _CM.from_dict(m).role == 'tool'
- m.get('tool_call_id') -> _CM.from_dict(m).tool_call_id
Used inline conversion because the generator iterates over a
dict list and reads 2 fields. Inline conversion avoids an
intermediate list comprehension.
openai_schemas.py:
- ChatMessage.from_dict() now provides defaults for required fields
('role' -> 'assistant', 'content' -> '') when the input dict is
missing them. This handles the case where DeepSeek's API returns
an empty {} for 'message' (e.g., finish_reason='length' with no
content). Without this default, ChatMessage.__init__() raises
TypeError.
Tests: 46/46 pass (test_ai_client_result, test_ai_client_tool_loop,
test_deepseek_provider, test_openai_schemas, test_minimax_provider).
Phase 5: ChatMessage (part 1)
Before: 6 .get('role'/'content'/'tool_calls'/'tool_call_id') sites in _send_deepseek
After: 0
Delta: -6
Migrates _send_deepseek's history transformation loop from
dict-style access to ChatMessage direct field access:
msg = _ChatMessage.from_dict(msg_raw)
msg.role (was msg.get('role'))
msg.content (was msg.get('content'))
msg.tool_calls (was msg.get('tool_calls') / msg['tool_calls'])
msg.tool_call_id (was msg.get('tool_call_id'))
The api_msg dict (output for the DeepSeek API) is constructed via
direct field access. The tool_calls list is converted to dicts via
tc.to_dict() (preserves the existing API payload format).
Notes:
- msg_raw.get('reasoning_content') is preserved as-is because
reasoning_content is NOT a ChatMessage field.
- Local import 'from src.openai_schemas import ChatMessage as _ChatMessage'
follows the existing pattern in this file (lazy imports inside functions).
Tests: 36/36 pass (test_ai_client_result, test_ai_client_tool_loop,
test_deepseek_provider, test_openai_schemas).
Infrastructure change required by Phase 5/6/7 of the
type_alias_unfuck_20260626 track. The plan's migration pattern
(var = Aggregate.from_dict(var)) requires from_dict on the
target dataclasses. None existed for the openai_schemas
classes, so this commit adds them.
from_dict semantics:
- Filter dict keys to only the dataclass fields (ignore extra keys
like _est_tokens)
- For ChatMessage: convert nested tool_calls list to tuple of ToolCall
- For ToolCall: convert nested function dict to ToolCallFunction
- For UsageStats: direct field mapping
Field definitions unchanged. Behavior: zero impact on existing tests
(no callers exist yet for from_dict on these classes).
Tests: syntax check OK; manual instantiation confirms from_dict works.
Phase 3: CommsLogEntry
Before: 3 .get('source_tier',...) sites + 1 half-measure in src/gui_2.py
After: 0
Delta: -4 (expected: -5 per plan; the 5th site was app_controller.py:1930
which returns None for missing source_tier and cannot be migrated
without breaking test_append_tool_log_dict_keys)
Migrates the following CommsLogEntry-related sites in src/gui_2.py:
1. gui_2.py:1810 - cache filter source_tier (.get('source_tier', ''))
2. gui_2.py:1818 - cache filter source_tier (.get('source_tier', ''))
3. gui_2.py:5104 - render_comms_log_panel source_tier (.get('source_tier', 'main'))
4. gui_2.py:5106 - render_comms_log_panel ts (.get('ts', '00:00:00'))
5. gui_2.py:5107 - render_comms_log_panel direction (.get('direction', '??'))
6. gui_2.py:5110 - render_comms_log_panel model (.get('model', '?'))
7. gui_2.py:5802 - render_tool_calls_panel half-measure
(subscript + 'in' check; entry['source_tier'] if 'source_tier' in entry else 'main')
All migrated via:
ce = CommsLogEntry.from_dict(entry)
ce.<field> # direct attribute access
The dataclass default for source_tier is 'main', which preserves the
fallback behavior for sites that had 'main' as the default. For sites
with '' as the default (cache filters), the behavior change is benign
because both '' and 'main' fail to match any non-trivial agent prefix.
Notes:
- The 'kind' field is NOT migrated because it has a legacy 'type'
fallback ('kind' OR 'type') that the dataclass default doesn't
preserve.
- 'provider' and 'payload' are NOT on CommsLogEntry; they remain
as entry.get(...) calls.
- src/app_controller.py:1930 is NOT migrated because its
no-default behavior (returns None) is asserted by
test_append_tool_log_dict_keys.
Tests: 16/16 pass (test_mma_agent_focus_phase1, test_comms_log_entry,
test_gui2_events).
src/type_aliases.py had two exact anti-patterns the user flagged:
1. Line 91: 'ToolCall: TypeAlias = Metadata' -- the dict alias the user
called out as 'the exact bad pattern'. Now points to the canonical
@dataclass(frozen=True, slots=True) class ToolCall in openai_schemas.py.
2. Lines 53-69: duplicate FileItem dataclass with 8 fields (path, content,
view_mode, summary, skeleton, annotations, tags) that conflicted with
the canonical models.FileItem (10 fields: path, auto_aggregate,
force_full, view_mode, selected, ast_signatures, ast_definitions,
ast_mask, custom_slices, injected_at). Two FileItem types was the
'FileItem is duplicated in TWO places' blocker. Duplicate removed;
FileItem now aliases models.FileItem.
state.toml updated to honest state: status='active', current_phase=0,
phases 2-10 marked 'not_done', 3 of 5 blockers fixed in this commit,
2 blockers (RAG return type, tool builders dicts) remain open with
followup tracks planned.
The 5 files that import ToolCall from src.type_aliases
(aggregate/ai_client/api_hook_client/app_controller/models) only use it
as a type annotation -- no constructor calls, no .from_dict() calls.
Safe to fix the alias.
TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md,
conductor/tier2/githooks/forbidden-files.txt,
conductor/tracks/tier2_leak_prevention_20260620/spec.md,
conductor/code_styleguides/data_oriented_design.md,
conductor/code_styleguides/error_handling.md,
conductor/code_styleguides/type_aliases.md before Phase 4.
Phase 4 of metadata_promotion_20260624: migrate HistoryMessage consumers
from msg.get(key, default) to direct field access.
Per-site resolutions (documented per Hard Rule #11):
1. src/synthesis_formatter.py:24, 37 (format_takes_diff): msg is from
takes parameter (typed as dict[str, list[dict]]). Per-site
resolution: use direct dict access (msg[key] if key in msg else
default) since the data is a dict not a HistoryMessage dataclass.
Migration pattern:
old: msg.get(key, default)
new: msg[key] if key in msg else default
2. src/gui_2.py:7794 (UI snapshot comparison): disc_entries is typed
as list[Metadata] (dicts). The last entry is accessed for content
comparison. Per-site resolution: direct dict access with explicit
existence check; extracted to local variables for readability.
Note: HistoryMessage is imported in several files (provider_state.py
uses it for the messages field) but the consumer sites that use .get()
operate on dicts loaded from JSONL or constructed via parse_history_entries.
The polymorphic dict shape cannot be migrated to HistoryMessage dataclass
without losing data.
TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md,
conductor/tier2/githooks/forbidden-files.txt,
conductor/tracks/tier2_leak_prevention_20260620/spec.md,
conductor/code_styleguides/data_oriented_design.md,
conductor/code_styleguides/error_handling.md,
conductor/code_styleguides/type_aliases.md before Phase 3.
Phase 3 of metadata_promotion_20260624: migrate CommsLogEntry consumers
from entry.get(key, default) to direct field access.
Per-site resolutions (documented per Hard Rule #11):
1. src/app_controller.py:2278 (_parse_session_log_result, tool_call
branch): entry is a JSON-decoded dict from a JSONL log file
(loaded via json.loads). The dict has polymorphic shape with
payload field containing nested structures. Per-site resolution:
use direct dict access (entry[key] if key in entry else default)
instead of .get() since the data is a dict not a CommsLogEntry
dataclass. Migration pattern:
old: entry.get(key, default)
new: entry[key] if key in entry else default
2. src/app_controller.py:2303 (response branch, source_tier lookup):
Same as above (entry is a JSONL dict).
3. src/app_controller.py:2311 (response branch, model lookup):
Same as above.
4. src/gui_2.py:5803 (render_tool_calls_panel): entry is from
app._tool_log_cache (typed as list[dict[str, Any]]), populated
from app.prior_tool_calls (typed as list[Metadata]). Per-site
resolution: direct dict access.
Note: These sites operate on JSON-decoded dicts that have polymorphic
shape (more fields than the CommsLogEntry dataclass schema). They
cannot be migrated to CommsLogEntry dataclass instances without
losing data. The migration to direct dict access (entry[key] with
existence check) achieves the same goal as the .get() pattern with
zero branches at the access site.
TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md,
conductor/tier2/githooks/forbidden-files.txt,
conductor/tracks/tier2_leak_prevention_20260620/spec.md,
conductor/code_styleguides/data_oriented_design.md,
conductor/code_styleguides/error_handling.md,
conductor/code_styleguides/type_aliases.md before Phase 2.
Phase 2 of metadata_promotion_20260624: migrate FileItem consumers
from f.get(key, default) / f[key] to direct field access.
Per-site resolutions (documented per Hard Rule #11):
1. src/ai_client.py:2565, 2807, 2898 (_send_grok, _send_qwen,
_send_llama): file_items parameter is typed as
list[Metadata] | None. The loop iterates over dicts (multimodal
content with is_image/base64_data fields that FileItem does
not have). Per-site resolution: construct FileItem(path=...) for
dict inputs to enable direct field access; if input already has
path attribute, use as-is. Migration pattern:
old: fi.get('path', 'attachment')
new: (fi if hasattr(fi, 'path') else FileItem(path=fi.get('path', 'attachment'))).path or 'attachment'
Added FileItem to src/models import in src/ai_client.py:52.
2. src/app_controller.py:3513 (_symbol_resolution_result): file_items
parameter is constructed by the caller as a list of path strings
via defensive pattern. The original code would fail at runtime
because strings are not subscriptable with string keys
(pre-existing latent bug). Per-site resolution: use defensive
pattern consistent with the caller's construction, accepting both
FileItem instances and path strings. Migration pattern:
old: [f[key] for f in file_items]
new: [f.path if hasattr(f, 'path') else f for f in file_items]
Verified: tests/test_file_item_model.py + tests/test_aggregate_flags.py
pass (5 passed, 1 skipped; no regressions).
TIER-2 READ AGENTS.md conductor/workflow.md conductor/edit_workflow.md conductor/tier2/githooks/forbidden-files.txt conductor/tracks/tier2_leak_prevention_20260620/spec.md conductor/code_styleguides/data_oriented_design.md conductor/code_styleguides/error_handling.md conductor/code_styleguides/type_aliases.md before Phase 0 Tasks 0.1, 0.2, 0.4.
Phase 0 of metadata_promotion_20260624. 11 NEW per-aggregate dataclasses added to src/type_aliases.py (CommsLogEntry, HistoryMessage, FileItem, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo) + RAGChunk added to src/rag_engine.py. Metadata: TypeAlias = dict[str, Any] preserved unchanged as the catch-all for collapsed codepaths. Each dataclass has paired to_dict()/from_dict() methods.
11 regression-guard test files created with 5-7 tests each (~70 tests total). All tests PASS.
The existing tests/test_type_aliases.py was updated to reflect the NEW design (CommsLogEntry etc. are now classes, not aliases to Metadata).
Conventions: 1-space indentation, CRLF preserved, no comments.
Phase 7 of code_path_audit_phase_3_provider_state_20260624.
Per-provider history is now accessed via provider_state.get_history()
at call sites; the 12 module-level _X_history/_X_history_lock aliases
are no longer referenced anywhere in production code (helper function
DEFINITIONS that take history as a parameter are unaffected).
TIER-2 READ conductor/code_styleguides/error_handling.md before Phase 2 (deepseek migration; RLock re-entrance critical).
Phase 2 of code_path_audit_phase_3_provider_state_20260624. 11 sites in _send_deepseek (lines 2186-2414) migrated from _deepseek_history/_deepseek_history_lock to local capture history = provider_state.get_history('deepseek'). The RLock re-entrance is critical here — this was the deadlock-prone site that prompted cc7993e5. The local capture pattern uses one acquisition per function instead of one per call site, minimizing lock acquisitions while preserving the same RLock instance that _deepseek_history_lock aliased to.
4 with-blocks migrated (lines 2195, 2215, 2347, 2412). 6 _deepseek_history alias references migrated to history (lines 2196, 2197, 2201, 2216, 2354, 2414).
Verified: 30 tests pass across test_provider_state_migration (14) + test_deepseek_provider (7) + 5 ai_client test files. The test_lock_acquisition_no_deadlock regression test verifies RLock re-entrance works correctly inside the with history.lock: blocks.
Conventions: 1-space indentation, CRLF preserved, no comments added.
TIER-2 READ conductor/code_styleguides/error_handling.md before Phase 1 (anthropic migration).
Phase 1 of code_path_audit_phase_3_provider_state_20260624. 13 call sites in _send_anthropic (lines 1430-1575) migrated from the module-level _anthropic_history alias to a local capture history = provider_state.get_history('anthropic'). The local capture pattern is used (instead of repeated provider_state.get_history() calls) to minimize lock acquisitions and improve readability.
The migration preserves behavior: ProviderHistory is the same singleton that _anthropic_history aliased to, so the migration is a pure refactor. The lock acquisition pattern is unchanged (this function does not acquire _anthropic_history_lock; thread-safety comes from _send_anthropic being called per-thread).
Verified: 37 tests pass across test_provider_state_migration.py + 6 ai_client test files.
Conventions: 1-space indentation, CRLF preserved, no comments added.
TIER-3 READ AGENTS.md + conductor/workflow.md + conductor/code_styleguides/error_handling.md + the 4 source files + 3 test files before this commit.
The code_path_audit_phase_2_20260624 track (Tier 2) shipped 11 audit
fixes (4 NG1 + 7 NG2) but used a heuristic bypass for 4 of the NG2
wrappers: legacy T | None functions that exist only to maintain test
patcher compatibility. Per the review at
docs/reports/REVIEW_TIER2_code_path_audit_phase_2_20260624.md Finding 8,
this track eliminates the legacy wrappers properly.
11 wrappers eliminated (8 main + 3 _legacy_compat inner):
- src/ai_client.py: get_current_tier (1 src + 1 test consumer)
- src/ai_client.py: _gemini_tool_declaration + _legacy_compat (2 test consumers)
- src/ai_client.py: run_tier4_patch_callback + _legacy_compat (was 0 direct callers
but had 2 callback references in app_controller/multi_agent_conductor;
callback contract migrated to Callable[[str, str], Result[str]] instead of
preserving an Optional[str] adapter)
- src/mcp_client.py: _get_symbol_node + _legacy_compat (8 in-file consumers)
- src/mcp_client.py: find_in_scope (nested inside _get_symbol_node_result;
private impl detail, audit doesn't catch T | None, left as-is)
- src/external_editor.py: launch_diff (1 src + 3 test + 1 live_gui test consumer)
- src/external_editor.py: launch_editor (no consumers; deleted)
- src/session_logger.py: log_tool_output (2 src + 3 test consumers)
- src/project_manager.py: parse_ts (no consumers; deleted)
For each consumer: replace legacy_fn(args) with legacy_fn_result(args).data.
For T | None checks: replace if x is None: with if not result.ok: or
if not result.ok or not isinstance(result.data, ...) (depending on pattern).
For run_tier4_patch_callback specifically: the wrapper was a callback adapter
(not a backward-compat shim) and had 2 callback references as consumers.
Rather than keep the adapter (which would re-introduce the Optional[str]
return that the strict audit catches), the patch_callback contract was migrated
from Callable[[str, str], Optional[str]] to Callable[[str, str], Result[str]]
in shell_runner.py + app_controller.py + 9 _send_<vendor>_result signatures
in ai_client.py. This propagates the Result[str] through the callback and
lets shell_runner unwrap with if r.ok and r.data instead of if patch_text.
Verification:
- audit_optional_in_3_files --strict: 0 return-type Optional[T] (down from 1)
- audit_exception_handling --strict: 0 violations (unchanged)
- audit_legacy_wrappers: 0 legacy wrappers (unchanged)
- 15 affected test files: 168 tests pass
- 8 mcp_client/structural/baseline test files: 55 tests pass
- 3 session/gui test files: 7 tests pass
- 0 return-type Optional[T] in src/ai_client.py (was 1: run_tier4_patch_callback)
The 7 code_path_audit*.py files (2604 lines total) are pure static
analysis tools. They do AST traversal of src/, no intrusive profiling,
no runtime markers. They were inlaid with src/ but only import:
- src.result_types (the Result[T] convention type)
- each other (the 6 siblings)
After the move:
- src/ is now pure application code; line-count audit metrics are clean
- scripts/code_path_audit/ is a new namespace-isolated subdir per
AGENTS.md 'scripts are namespace-isolated by directory' rule
TIER-3 READ AGENTS.md + conductor/workflow.md + conductor/edit_workflow.md
+ conductor/code_styleguides/code_path_audit.md + the 7 files before
this commit.
Changes:
- 7 files moved: src/code_path_audit*.py -> scripts/code_path_audit/
- 7 files updated: internal imports rom src.code_path_audit_X ->
rom code_path_audit_X (siblings in same subdir)
- 7 files updated: add sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'src'))
to find src.result_types when run standalone
- 5 test files updated: rom src.code_path_audit -> rom code_path_audit
+ sys.path setup to find the new subdir
- 6 throwaway scripts in scripts/tier2/artifacts/ updated: import path
+ sys.path setup (parents[3] / 'src' + parents[3] / 'scripts' / 'code_path_audit')
- 2 styleguide/spec references updated: conductor/code_styleguides/code_path_audit.md
+ conductor/tracks/code_path_audit_20260607/spec_v2.md
- 1 meta-audit docstring updated: scripts/audit_code_path_audit_coverage.py
- 1 type registry entry deleted: docs/type_registry/src_code_path_audit.md
(the type is no longer in src/)
- 1 type registry index updated: docs/type_registry/index.md (22 files, was 23)
Verification:
- 7/7 audit gates pass --strict (weak_types 102<=112, type_registry 22 files,
main_thread_imports OK, no_models_config_io OK, code_path_audit_coverage 0
violations, exception_handling 0 violations, optional_in_3_files 0 violations)
- 6/6 test files pass: test_code_path_audit, test_code_path_audit_integration,
test_code_path_audit_phase78, test_code_path_audit_phase89,
test_code_path_audit_ssdl_behavioral, test_metadata_nil_sentinel
- src/ line count: 29997 lines (down from 32621 = -2624 lines)
- scripts/code_path_audit/ line count: 2620 lines
3 Result helper methods (_deserialize_active_track_result, _serialize_tool_calls_result, _parse_token_history_first_ts_result) were nested inside cb_load_prior_log as inner defs. The inner 'return' at the except block (line 2370) made the rest of the function body (lines 2377-2392) unreachable past the nested defs' scope.
User fix: moved the 3 helpers to class level so they're reachable from other class methods (_refresh_from_project, _load_beads, etc.). Kept _resolve_log_ref and _read_ref_file_result as nested defs inside cb_load_prior_log because they're only used there.
File: -69 lines (the 60-line def cb_load_prior_log block from its original position), +64 lines (the 3 helpers + cb_load_prior_log re-added in the correct order).
Verified: ast.parse OK; from src import app_controller OK; AppController.cb_load_prior_log is reachable.
TIER-3 READ AGENTS.md + conductor/code_styleguides/error_handling.md + src/provider_state.py + src/ai_client.py:2148-2220 before provider-state-rlock-fix.
Tier 2's 25a22057 commit re-bound the 14 module globals in src/ai_client.py as
aliases to provider_state.get_history(...) instances. The ProviderHistory dunder
methods (__bool__, __len__, __iter__, __getitem__) all use \with self.lock:\.
The dunders are non-reentrant: \ hreading.Lock\ blocks if the lock is already
held. The call site in src/ai_client.py:2210-2217 acquires the lock via
\with _deepseek_history_lock:\ (alias to ProviderHistory.lock), then calls
_rerepair_deepseek_history(_deepseek_history) which does \history[-1]\
(acquires the lock again -> DEADLOCK). This caused
tests/test_deepseek_provider.py::test_deepseek_completion_logic to hang
with a 30s timeout.
Fix: change \ hreading.Lock\ to \ hreading.RLock\ in ProviderHistory.
The dunders can now be safely called while the lock is already held.
Also removed:
- Duplicate @dataclass decorator on ProviderHistory (line 25-26)
- Duplicate _PROVIDER_HISTORIES dict declaration (lines 64-71 and 74-81)
Acceptance: test_deepseek_provider (7/7) + test_provider_state + test_ai_client_result + test_ai_client_tool_loop all pass.
OpenAI ChatMessage content can be either a string (simple text) or a list
of content parts (multimodal: text + image_url, etc.). Updated the type
annotation to match the actual API. No behavioral change; this is a
type-hint-only widening so callers can pass multimodal content via
ChatMessage instead of dicts.
Required by tests/test_openai_compatible.py::test_vision_multimodal_message
which was passing raw dicts to OpenAICompatibleRequest (wrong - the field
is typed list[ChatMessage]). With this widening, that test can now use
ChatMessage(role='user', content=[...multimodal parts]) without losing
type fidelity.
12 tests fail with:
TypeError: NormalizedResponse.__init__() got an unexpected keyword argument 'usage_input_tokens'
The @dataclass(frozen=True) auto-generated __init__ requires `usage: UsageStats`,
but 12 tests + 1 production site (src/ai_client.py:908) call it with the OLD
flat-kwarg API (usage_input_tokens=..., usage_output_tokens=..., etc.).
Change @dataclass(frozen=True) -> @dataclass(frozen=True, init=False) and add
a custom __init__ that accepts BOTH signatures:
- New: usage: UsageStats (used by current production code)
- Legacy: usage_input_tokens, usage_output_tokens, usage_cache_read_tokens,
usage_cache_creation_tokens (used by tests + 1 ai_client site)
If usage is None and any legacy flat kwarg is non-None, build a UsageStats
from the legacy kwargs. Otherwise use the provided usage. All field
assignments use object.__setattr__ because frozen=True locks __setattr__.
Verification:
- Legacy kwargs work: NormalizedResponse(text="hi", tool_calls=(), usage_input_tokens=10, usage_output_tokens=5, raw_response=None) sets usage.input_tokens=10
- New kwargs work: NormalizedResponse(text="hi", tool_calls=(), usage=UsageStats(1, 2)) sets usage directly
- 12 affected tests now pass (was 12 failed, 3 passed; now 15 passed)
compute_result_coverage() was implemented during the 14-phase plan but is
never called: synthesize_aggregate_profile() (now at ~line 1075) inlines
its own ResultCoverage construction via the actual AST analysis at
~line 1135-1145. The function has a latent bug at line 754 (was):
result_producers = total_producers
which hardcodes result_producers to 100% of total_producers regardless of
input — making the function return meaningless numbers.
Tests deleted in lockstep:
- tests/test_code_path_audit_phase78.py: test_compute_result_coverage_no_producers
- tests/test_code_path_audit_phase78.py: test_compute_result_coverage_full
The 'compute_result_coverage' import was also removed from the test file's
import block.
Verification:
- grep -c 'compute_result_coverage' src/code_path_audit.py = 0
- grep -c 'compute_result_coverage' tests/ = 0
- 125 of 125 remaining tests pass (was 127; -2 tests deleted)
The v2 postfix DSL parser (DSL_WORD_ARITY_V2, _atom, to_dsl_v2, parse_dsl_v2)
was implemented during the 14-phase DSL plan but never reached production:
run_audit() (line ~1217 after this change) only writes .md files (AUDIT_REPORT.md
plus per-aggregate markdowns via to_markdown/to_tree), never .dsl files. The DSL
parser carried latent arity bugs (DSL_WORD_ARITY_V2 declared 5 for 'result-coverage'
but writer emits 4; 4 for 'type-alias-coverage' but writer emits 3) which would
have caused silent parse failures.
Also removed the now-unused 'import re' statement (was only used by parse_dsl_v2).
The 'from datetime import date as date_mod' is retained (still used at line ~1259,
1275, 1291 in the markdown renderer).
Tests deleted in lockstep:
- tests/test_code_path_audit_phase78.py: test_dsl_word_arity_v2_14_new_words
- tests/test_code_path_audit_phase89.py: test_to_dsl_v2_includes_aggregate_kind_section,
test_parse_dsl_v2_round_trip_aggregate_kind, test_parse_dsl_v2_malformed
Verification:
- grep -c 'to_dsl_v2|parse_dsl_v2|DSL_WORD_ARITY_V2' src/code_path_audit.py = 0
- 127 of 127 remaining tests pass (was 131; -4 tests deleted)
The import statement appeared twice in quick succession (lines 655 and 658).
Both were identical and contributed nothing. Removed one. No functional change.
Verification:
- grep -c '^import json' src/code_path_audit.py = 1
- uv run python -c 'from src import code_path_audit' returns OK
- 124 tests in tests/test_code_path_audit*.py pass
Resolves audit_weak_types.py --strict regression (117 vs baseline 112 -> 104).
The regression was in src/openai_schemas.py (10 sites) and src/mcp_tool_specs.py
(4 sites), both files added after the 2026-06-21 baseline. JsonValue is the
canonical JSON-serializable data TypeAlias from src/type_aliases.py:22 and is a
structural superset of dict[str, Any], so consumers expecting the legacy shape
are unaffected. All 30 existing tests in tests/test_openai_schemas.py and
tests/test_mcp_tool_specs.py continue to pass.
Spec WHERE for t1.1 referenced code_path_audit*.py files but those modules
report 0 weak type findings per the audit (they use dict[str, int],
dict[str, dict], etc., not dict[str, Any]); see plan.md investigation note.
MVP pipeline simplification:
- render_rollups() now produces ONLY summary.md + AUDIT_REPORT.md
- run_audit() now produces only per-aggregate .md (no .dsl/.tree)
- New src/code_path_audit_gen.py generates the single coherent report
Stale artifacts moved to _stale/ subdirectory (preserved for history):
- 13 per-aggregate .dsl files (redundant with .md)
- 13 per-aggregate .tree files (redundant with .md)
- 9 old top-level rollups (cross_audit_summary, decomposition_matrix,
candidates, field_usage, call_graph, hot_paths, dead_fields,
ssdl_analysis, organization_deductions - all superseded by sections
inlined in AUDIT_REPORT.md)
- _stale/README.md explains what happened
Meta-audit updated to check .md files (14 required H2 sections per
aggregate) instead of .dsl files. 0 violations on 10 real profiles.
Tests: 131 passing. New MVP report: 5000+ lines.
Three real bugs fixed:
1. FunctionRef always used line=0. Now passes node.lineno from AST.
2. P3_pass results were discarded with bare pass. Now stored in
ProducerConsumerGraph.field_accesses.
3. Field-access detector only saw entry['key']; missed entry.get('key')
which is the dominant pattern in this codebase. Now handles both.
Plus _extract_type_name() helper handles Optional[T], dict[str, T],
list[T], Result[T], Union[T, ...], and T | None (PEP 604) so P1/P2
catch more annotation patterns.
Real numbers (Metadata aggregate):
- producers: 77 -> 117
- consumers: 35 -> 66
- field-access sites: 130 -> 173
- line numbers: all real (line 1281, 1746, etc.)
AUDIT_REPORT.md grew 2009 -> 3140 lines with real evidence.
Total audit output: 5176 lines / 50 files (was 2415 / 49).
All 131 tests still passing.