Six fixes for the c11_python doc sync (chronology row 3):
- C5 (Result notation): Result[str, ErrorInfo] -> Result[str] at
docs/guide_ai_client.md lines 452 + 469; also error_handling.md
line 801 (historical deprecation section).
- C6 (RAGChunk schema): docs/guide_models.md lines 343-349 corrected
to match src/rag_engine.py:19-25 (id, document, path, score, metadata).
- C17 (type_aliases.md table): rewrote alias table to reflect post-2026-06-25
reality (Metadata is @dataclass(frozen=True, slots=True) with 36 fields;
11 per-aggregate dataclasses listed with source locations; removed
stale 'underlying type is dict[str, Any]' claim at line 73 + the
'keep Metadata as dict[str, Any]' claim at line 81).
- C19 (OBLITERATE principle): added 'OBLITERATE Principle' section to
error_handling.md after Migration Playbook; clarified in Hard Rules
that argument types that may be None (caller choice) are NOT banned.
- C2 (audit script name): docs/AGENTS.md references updated to point
to scripts/audit_optional_returns.py (the all-src/ successor to
scripts/audit_optional_in_3_files.py).
Also: docs/reports/CONTRADICTIONS_REPORT_20260627.md — the contradictions
index that drives these fixes. Kept for reference.
C16 + C18 were already addressed in commit 770c2fdb (python.md §10
Documented Exceptions table + §17.10 audit inventory).
Implements the 7th audit script referenced in python.md §17.8. Scans
src/*.py for local imports (§17.9a), _PREFIX aliasing (§17.9b), and
repeated .from_dict() in the same expression (§17.9c, info-only).
Three changes in this commit:
1. scripts/audit_imports.py: AST-based scanner; exits 1 in --strict on
LOCAL_IMPORT or PREFIX_ALIAS. Whitelist-aware via
scripts/audit_imports_whitelist.toml (load with --show-whitelist;
disable with --no-whitelist).
2. scripts/audit_imports_whitelist.toml: 21 files whitelisted with per-file
reason (vendor SDK warmup, hot-reload re-imports, circular-dep avoidance).
Suppresses 187 LOCAL_IMPORT sites; 0 strict violations remain.
3. conductor/code_styleguides/python.md: updated §17.8 (4th audit entry)
and §17.9a (3 documented exceptions + whitelist mechanism).
Tests: tests/test_audit_imports.py (7 tests, all passing).
Implements the 7th audit script referenced in python.md §17.8. Scans
src/*.py for local imports (§17.9a), _PREFIX aliasing (§17.9b), and
repeated .from_dict() in the same expression (§17.9c, info-only).
Three changes in this commit:
1. scripts/audit_imports.py: AST-based scanner; exits 1 in --strict on
LOCAL_IMPORT or PREFIX_ALIAS. Whitelist-aware via
scripts/audit_imports_whitelist.toml (load with --show-whitelist;
disable with --no-whitelist).
2. scripts/audit_imports_whitelist.toml: 21 files whitelisted with per-file
reason (vendor SDK warmup, hot-reload re-imports, circular-dep avoidance).
Suppresses 187 LOCAL_IMPORT sites; 0 strict violations remain.
3. conductor/code_styleguides/python.md: updated §17.8 (4th audit entry)
and §17.9a (3 documented exceptions + whitelist mechanism).
Tests: tests/test_audit_imports.py (7 tests, all passing).
TIER-1 READ AGENTS.md + conductor/workflow.md + conductor/edit_workflow.md
+ conductor/code_styleguides/data_oriented_design.md + conductor/code_styleguides/error_handling.md
+ conductor/code_styleguides/type_aliases.md + conductor/code_styleguides/code_path_audit.md
+ conductor/tracks/module_taxonomy_refactor_20260627/spec.md + conductor/tracks/module_taxonomy_refactor_20260627/plan.md
+ docs/reports/FOLLOWUP_module_taxonomy_refactor_20260627_recoverable.md before this commit.
v2 fixes v1 gaps that gave Tier 2 discretion:
1. THE 4-CRITERIA DECISION RULE (the taxonomy law):
- C1: Cross-system usage (consumed by >= 3 unrelated systems)
- C2: State machine / lifecycle
- C3: Test file already exists
- C4: Substantial size (> 30 lines OR > 5 fields)
- Rule: C1 OR C2 OR C3 -> DEDICATED FILE; ONLY C4 -> MERGE INTO DESTINATION; NONE -> KEEP
2. THE DATA/VIEW/OPS SPLIT (the GUI boundary):
- Data classes go in data files (src/<system>.py)
- View code (ImGui rendering) goes in src/gui_2.py
- Ops (operations on data) go with the data
- Exception: imgui_scopes.py is the EXCEPTION (Python with context managers)
3. ZERO TIER 2 DISCRETION:
- Every move is pre-decided in the spec
- Tier 2 executes, doesn't decide
- v1 had 22 commits because of exploration; v2 has 16 because the work is prescriptive
4. PRESERVED Pydantic PROXIES:
- _create_generate_request, _create_confirm_request, __getattr__ stay in models.py
- They're API-specific; moving them is out of scope for v2
Applied to all 11 classes in models.py:
- DEDICATED: Ticket, Track, WorkerContext, TrackState, TrackMetadata, ThinkingSegment -> src/mma.py (6 classes; C1+C2+C3+C4)
- DEDICATED: FileItem, Preset, ContextPreset, ContextFileEntry, NamedViewPreset -> src/project_files.py (5 classes; C1+C3+C4)
- DEDICATED: ProjectContext + 5 sub + config IO -> src/project.py (1+5+functions; C1+C3+C4)
- MERGE: Tool, ToolPreset -> src/tool_presets.py (C1 NO)
- MERGE: BiasProfile -> src/tool_bias.py (C1 NO)
- MERGE: TextEditorConfig, ExternalEditorConfig -> src/external_editor.py (C1 NO)
- MERGE: Persona -> src/personas.py (C1 NO)
- MERGE: WorkspaceProfile -> src/workspace_manager.py (C1 NO)
- MERGE: MCPServerConfig, MCPConfiguration, VectorStoreConfig, RAGConfig, load_mcp_config -> src/mcp_client.py (C1 YES, coupled to MCP)
- DELETE: AGENT_TOOL_NAMES (redundant with mcp_tool_specs.tool_names())
Net: 65 -> 61 files (possibly 60 if models.py eliminated)
16 atomic commits (down from v1's 22)
14 VCs (added VC13 + VC14: verify the 4-criteria rule and data/view/ops split are documented)
The git stash ban is in place at 3 layers (commit 6240b07b). The timeline-
is-immutable principle is explicit in the agent prompt. The next Tier 2
should not be able to corrupt files the same way.
CRITICAL CORRECTION: the 5 'DAMAGED' tasks in the track report are NOT
data loss. The class definitions (Tool, ToolPreset, BiasProfile,
TextEditorConfig, ExternalEditorConfig, MCPServerConfig,
MCPConfiguration, VectorStoreConfig, RAGConfig, load_mcp_config,
WorkspaceProfile) are STILL in src/models.py with full bodies.
The actual state:
- 11 class definitions in models.py (data INTACT)
- 0 class definitions in destination files (the move was incomplete)
- 1 broken script that Tier 2 ran (the '5 tasks damaged' report)
What the user's anger is about (justified):
- Tier 2 used 'git stash' (now banned at 3 layers in commit 6240b07b)
- Tier 2 made a non-descriptive 'misc' commit
- Tier 2 reported 'DAMAGED' but the data was actually fine
What the user gets:
- Track is RECOVERABLE - just add the 11 classes to their destination files
- New Tier 2 should reset the 5 'damaged' tasks to 'pending' in state.toml
- Phase 1 + Phase 2 of the track are DONE
- The remaining work is mechanical: 5 commits to add class defs to
destination files, then 5 commits to remove them from models.py
Concrete next steps (for new Tier 2):
1. Add Tool + ToolPreset to src/tool_presets.py
2. Add BiasProfile to src/tool_bias.py
3. Add TextEditorConfig + ExternalEditorConfig to src/external_editor.py
4. Add MCP config classes to src/mcp_client.py
5. Add WorkspaceProfile to src/workspace_manager.py
6. (Then) remove from models.py
7. Create src/project.py + src/project_files.py
8. Delete AGENT_TOOL_NAMES
9. Verify
The previous TRACK_ABORTED report is INCORRECT. This report
supersedes it. The data is fine; only the move operation is
incomplete.
ROOT CAUSE: Tier 2 used 'git stash' during the cruft_elimination_20260627
track execution and corrupted the user's in-progress files. The user
explicitly stated: 'if an agent fucks up, their tendency to want to revert
is not correct and instead they must live with the timeline and just do
corrections with a new commit. They can grab artifacts, code, etc, from
old commits but they cannot reset to that.'
This commit adds HARD BANs on git stash* and git clean -fd* at 3 layers
(per the existing 3-layer defense model documented in
conductor/tier2/agents/tier2-autonomous.md):
LAYER 1: AGENTS.md
- Added new HARD BAN: 'git stash* (any form: git stash, git stash pop,
git stash apply, git stash drop, git stash clear) is FORBIDDEN.
Stashing inverts the safety net of the working tree'
LAYER 2: conductor/tier2/opencode.json.fragment (Tier 2 autonomous)
- Added 'git stash*', 'git stash pop*', 'git stash apply*',
'git stash drop*', 'git stash clear*', 'git clean -fd*', 'git clean -fdx*'
to BOTH the top-level permission.bash deny list AND the
agent.tier2-autonomous.permission.bash deny list
- Also added 'git revert*' (was missing from fragment; already banned in prompt)
- These are now HARD DENIED at the OpenCode permission layer; the agent
cannot run them even if it tries
LAYER 3: conductor/tier2/agents/tier2-autonomous.md
- Added 'git stash* (any form)' to the Hard Bans list
- Added 'THE TIMELINE-IS-IMMUTABLE PRINCIPLE' section spelling out
exactly what to do when you fuck up:
- When you make a wrong commit, write a NEW commit that fixes it
- The git history is immutable on this branch
- You CAN grab artifacts from old commits via 'git show <sha>:<path> > <new-path>'
- You CANNOT reset the branch HEAD to an old commit
- 'git revert', 'git reset --hard', 'git reset --soft', 'git stash' are
all attempts to rewrite history and BANNED
- Correct pattern: pause, read the actual file, write a forward
corrective commit with a commit message that explains the fix
This addresses the root cause of the 2026-06-27 cruft_elimination
corruption. Future Tier 2 autonomous runs will be blocked from running
git stash* at 2 layers (OpenCode permission deny + Tier 2 prompt hard
ban list) and reminded at the agent-prompt layer (THE TIMELINE-IS-
IMMUTABLE PRINCIPLE section).
Per spec FR4 + Phase 3.4: Persona dataclass + properties (provider/model/
temperature/top_p/max_output_tokens) + to_dict/from_dict move from
src/models.py into src/personas.py (which already has the PersonaManager
ops layer). Re-export at top of models.py preserves 'from src.models
import Persona'.
Per spec FR3/FR4 + Phase 3.1: the MMA domain dataclasses move to their own module:
- ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata, TrackState, EMPTY_TRACK_STATE
- TrackMetadata is the renamed (was 'Metadata' dataclass in models.py; renamed to avoid
collision with the Metadata type alias = dict[str, Any])
src/models.py:
- Removed class definitions for ThinkingSegment, Ticket, Track, WorkerContext, Metadata, TrackState, EMPTY_TRACK_STATE
- Added backward-compat re-exports so existing 'from src.models import Ticket' continues to work
- Metadata alias kept for the dataclass name (was confusingly shadowing the type alias)
TrackState's metadata field reverts to the original 'default_factory=dict' pattern
(intentionally not auto-constructing TrackMetadata) to preserve the pre-existing
behavior where accessing state.metadata.id on a missing state.toml throws
AttributeError, which project_manager.get_all_tracks catches and falls through
to metadata.json loading. This was a 'bug-on-purpose' that the test
test_get_all_tracks_with_metadata_json relies on.
Verification: 136 tests pass across mma_models, conductor_engine_v2, dag_engine,
ticket_queue, track_state_schema, thinking_gui, manual_block, pipeline_pause,
phase6_engine, parallel_execution, run_worker_lifecycle_abort, spawn_interception,
persona_id, conductor_engine_abort, conductor_tech_lead, execution_engine,
perf_dag, per_ticket_model, metadata_promotion_phase1, thinking_persistence,
progress_viz, gui_progress, mma_ticket_actions, headless_verification,
context_pruner, orchestration_logic, project_manager_tracks,
track_state_persistence.
Per spec FR2 + Phase 2.2 + architecture feedback (data != view):
- VendorMetric (data) -> src/ai_client.py (alongside VendorCapabilities; all vendor data)
- get_vendor_state -> renamed to _get_vendor_state_metrics in src/gui_2.py
(it's a view-helper that builds the metrics for render_vendor_state's table)
- render_vendor_state in gui_2.py now calls _get_vendor_state_metrics directly
Tests:
- tests/test_vendor_state.py: imports get_vendor_state from src.gui_2, VendorMetric from src.ai_client
Per architecture (data != view != ops):
- Data classes (PendingPatch, EMPTY_PATCH, DiffHunk, DiffFile) live in src/patch_modal.py
- PatchModalManager (ops on the data) also stays; it's used only by tests/test_patch_modal.py
(no production src/ code references PatchModalManager; no ImGui rendering of patches uses it)
- src/gui_2.py imports DiffHunk/DiffFile from src.patch_modal (data dependency)
The original spec wanted to merge patch_modal.py into gui_2.py. That would conflate
data (DiffHunk/DiffFile) and ops (PatchModalManager) into the view layer, which
violates the app_controller-owns-state / gui-is-pure-view architecture established
in Phase 1.1 (bg_shader state fix) and Phase 1.3 (command_palette split).
Verification:
- uv run python -c 'from src.patch_modal import PendingPatch, DiffHunk, DiffFile, EMPTY_PATCH, PatchModalManager' OK
- 41 tests pass: test_diff_viewer, test_patch_modal, test_command_palette,
test_commands_no_top_level_command_palette, test_handle_reset_session,
test_app_controller_sigint
Per spec FR1 + Phase 1.4 + architecture feedback (data != view):
- Data classes DiffHunk, DiffFile -> src/patch_modal.py (alongside PendingPatch; all patch-domain data)
- Operations parse_diff/parse_hunk_header/get_line_color/apply_patch_to_file (called by gui_2) -> src/gui_2.py
- GUI is a pure view; data lives elsewhere; no new files per AGENTS.md
Tests: tests/test_diff_viewer.py imports from src.gui_2 (parse_diff/apply_patch_to_file) and src.patch_modal (DiffFile/DiffHunk).
Per spec FR1 + Phase 1.3 + architecture feedback: src/command_palette.py
split by responsibility:
- Command/ScoredCommand/CommandRegistry/fuzzy_match/_close_palette/_execute (data/ops)
-> src/commands.py (which already owns _LazyCommandRegistry pattern)
- render_palette_modal (view/ImGui) -> src/gui_2.py
GUI is a pure view; the registry/data classes are ops; commands.py owns
the registry because commands.py is where @registry.register decorators live.
gui_2.render_palette_modal imports Command from commands.py to type its
parameters.
Also fixes Phase 1.1 (bg_shader) per architecture feedback:
BackgroundShader no longer owns 'enabled' state - the GUI is pure view.
State is now owned by AppController.bg_shader_enabled (read on load from
config, written from gui_2 checkbox via app's __setattr__ delegation).
Tests:
- tests/test_command_palette.py: imports from src.commands (was src.command_palette)
- tests/test_commands_no_top_level_command_palette.py: rewritten for the
new architecture (eager registry in commands.py; render in gui_2; no
circular import between commands.py and gui_2)
Per spec FR1 + Phase 1.2: draw_soft_shadow moved into src/gui_2.py
as a region block; consumer sites changed from shaders.draw_soft_shadow()
to draw_soft_shadow(). Removed the local import workaround at line 7016.
refactor(gui_2): merge bg_shader into gui_2; git rm src/bg_shader.py
Per spec FR1 + Phase 1.1: bg_shader (66 lines) moved into src/gui_2.py
as a region block; consumers updated to use the in-module get_bg().
Local import pattern preserved at app_controller sites (matches existing
circular-dep workaround for gui_2<->app_controller).
User: 'isn't AGENT_TOOL_NAMES a redundant thing thats directly associated
with the mcp_client.py?' - YES, confirmed.
The existing test test_tool_names_subset_of_models_agent_tool_names
literally asserts: tool_names() ⊆ AGENT_TOOL_NAMES. So AGENT_TOOL_NAMES
is just a hardcoded snapshot of mcp_tool_specs.tool_names().
Action: DELETE AGENT_TOOL_NAMES from models.py (not just move it).
Derive at consumer sites: list(mcp_tool_specs.tool_names()).
8 consumer sites to update:
- 3 in src/app_controller.py:2110, 2972, 3273
- 5 in tests/test_arch_boundary_phase2.py:23, 29, 31, 32, 33
The cross-check test becomes either redundant or converts to a
positive assertion (e.g., assert that the derived list has at
least the canonical tool count).
models.py reduces further: from ~60 to ~30 lines after deletion.
This further reduces the models.py footprint. Combined with the
previous audit (move vendor files to ai_client.py, split out mma.py
+ project.py + project_files.py), models.py becomes essentially
empty - just the Pydantic proxy code that may also move to api_hooks.py.
Net effect: models.py could be ELIMINATED entirely (becomes ~0 lines
or just an __init__.py marker). The followup should consider whether
to delete models.py completely.
Tier 2 marked Phase 2 (VC8) as 'spec mismatch' because the spec says
'add ProjectContext with all fields observed in flat_config' but
doesn't enumerate which fields. Tier 2 needs the spec to be specific
before it can resume.
This correction specifies the exact schema based on the actual code:
flat_config returns a NESTED dict with 6 top-level fields:
- project (Meta: name, summary_only, execution_mode)
- output (Output: namespace, output_dir)
- files (Files: base_dir, paths)
- screenshots (Screenshots: base_dir, paths)
- context_presets (opaque dict pass-through)
- discussion (Discussion: roles, history)
The 11 sub-fields are derived from aggregate.run's access patterns
(src/aggregate.py:484-525). output_dir and files.base_dir are REQUIRED
(direct subscript); all others use .get() with defaults.
Recommended design: 6 sub-dataclasses (ProjectMeta, ProjectOutput,
ProjectFiles, ProjectScreenshots, ProjectDiscussion, ProjectContext),
each matching the nested dict shape. ProjectContext has dict-compat
methods (__getitem__ + get) so consumers don't need migration.
Two migration options:
- Option A (incremental): ProjectContext has dict-compat; consumers
unchanged. Flat fix.
- Option B (full): Migrate all 8 consumer sites + 2 test mocks to
use sub-dataclass access. ~40 lines across 10 files.
Acceptance: 5 corrected VC8 criteria. Tier 2 can resume Phase 2 directly.
TIER-1 READ conductor/tracks/cruft_elimination_20260627/spec.md + src/project_manager.py:268 + src/aggregate.py:484-525 + src/type_aliases.py + src/models.py before this commit.
The previous state.toml marked status = 'completed' despite the
track FAILING 4 of 10 acceptance criteria:
- VC1: .get() sites 26 (target < 15)
- VC2: subscript sites 79 (target < 20)
- VC4: effective codepaths not measured
- VC6: 7/11 batched tiers pass (target 10/11)
This commit:
1. Sets state.toml status to 'active' (track is NOT complete)
2. Marks Phase 11 as 'failed' (verification did not pass)
3. Rewrites the completion report to lead with the FAILED status
The 50% reduction in .get() sites (52 -> 26) is meaningful progress
but the spec's quantitative gates were not met. Do not merge this
branch as complete.
In Phase 2 (commit 96f0aa54), I migrated the half-measure pattern
to use 'models.FileItem.from_dict(fi)'. This worked in some scopes
but failed in _send_qwen/_send_grok/_send_llama because ai_client.py
imports 'FileItem' from src.type_aliases (which is a TypeAlias string
forward reference 'models.FileItem', NOT the class). The earlier
import from src.models was shadowed by the type_aliases import
at line 71. Hence 'isinstance(fi, FileItem)' failed with
'isinstance() arg 2 must be a type'.
Fix: add local 'from src.models import FileItem as _FIC' inside
the if-block and use _FIC for isinstance + from_dict.
Discovered by test_qwen_provider.py::test_qwen_vision_vl_model_accepts_image.
Tests: 11/11 pass (test_qwen_provider, test_ai_client_result,
test_ai_client_tool_loop).
In Phase 10 batch 1 (commit 28799766), I migrated the total_cost
sum in render_mma_track_summary using 'MMAUsageStats.from_dict()'
directly instead of the local '_MMA' alias used elsewhere in the
same function. This caused NameError at runtime when the code path
was exercised.
Fix: add 'from src.type_aliases import MMAUsageStats as _MMA'
and use '_MMA.from_dict()' consistently.
Discovered by test_mma_approval_indicators.py::test_no_approval_badge_when_idle
which exercises render_mma_dashboard -> render_mma_track_summary.
Tests: 4/4 pass in test_mma_approval_indicators.py.
Required by Phase 10 migrations which call these from_dict methods.
Without these, CustomSlice.from_dict() and MMAUsageStats.from_dict()
used in gui_2.py would raise AttributeError at runtime.
Adds the from_dict pattern consistent with the existing
CommsLogEntry/HistoryMessage/ToolDefinition from_dict:
- Filter dict keys to only the dataclass fields (ignore extras)
- Pass filtered dict to cls(**filtered)
Field definitions unchanged. No-op behavior for callers that
already have a dataclass instance (they pass through isinstance check).
Tests: 51/51 pass across all related test files.
Phase 10 (batch 2): DiscussionSettings
Before: 1 .get('temperature'/...) site in src/gui_2.py
After: 0
Delta: -1 (plan expected 3 sites; 2 were already migrated by Tier 2)
Migrates the summary line in persona preferred model rendering:
entry.get('temperature', 0.7)
entry.get('top_p', 1.0)
entry.get('max_output_tokens', 0)
to:
ds = DiscussionSettings.from_dict(entry) if isinstance(entry, dict) else ds
ds.temperature, ds.top_p, ds.max_output_tokens
The dataclass defaults match the original .get() defaults exactly
(temperature=0.7, top_p=1.0, max_output_tokens=0), so behavior is preserved.
Phase 9: RAGChunk
Before: 0 .get('document',...) sites
After: 0
Delta: -0 (expected: -3; Tier 2 had already migrated these sites
before this track started; the lines at aggregate.py:3259,
app_controller.py:251,4162 referenced in the plan no longer
exist in the current code)
Verification:
- aggregate.py: no remaining .get('document',...) sites
- app_controller.py: no remaining chunk.get(...) sites
- rag_engine.RAGChunk dataclass + from_dict() method available
- _rag_search_result returns Result[list[Metadata]] (chunks are dicts)
No code changes; the phase is verified complete by Tier 2's earlier
migration. Phase 9 has no remaining .get() sites on the RAGChunk
aggregate, satisfying the per-phase hard guard (delta = 0 because
baseline is already 0).
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).
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 pre-flight
Regenerate the type registry to bring docs into sync with the
current src/type_aliases.py and src/models.py state. Pre-flight
required by Phase 0: 'uv run python scripts/generate_type_registry.py --check'
must exit 0 before per-phase work begins.
Diff: index.md + src_type_aliases.md + type_aliases.md (3 files).
FileItem moved from 'dataclass in src/type_aliases.py' to 'TypeAlias
in src/type_aliases.py' because the canonical FileItem is now
src.models.FileItem (per the previous track's commit b4bd772d which
pointed the alias and removed the duplicate).
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.