From 918ec375fc749ac9a3b1c6bef6be2335aac4492b Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 18:55:48 -0400 Subject: [PATCH] refactor(fileitem): migrate FileItem consumers to direct field access (Phase 2) 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). --- src/ai_client.py | 11 +++++++---- src/app_controller.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ai_client.py b/src/ai_client.py index e72bcc8e..efc85193 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -49,7 +49,7 @@ from src.vendor_capabilities import VendorCapabilities, get_capabilities # TODO(Ed): Eliminate these? from src.events import EventEmitter from src.gemini_cli_adapter import GeminiCliAdapter -from src.models import ToolPreset, BiasProfile, Tool +from src.models import FileItem, ToolPreset, BiasProfile, Tool from src.paths import get_credentials_path from src.tool_bias import ToolBiasEngine from src.tool_presets import ToolPresetManager @@ -2562,7 +2562,8 @@ def _send_grok(md_content: str, user_message: str, base_dir: str, if file_items: for fi in file_items: if fi.get("is_image") and fi.get("base64_data"): - user_content = f"[IMAGE: {fi.get('path', 'attachment')}]\n{user_content}" + fi_item = fi if hasattr(fi, 'path') else models.FileItem(path=fi.get('path', 'attachment')) + user_content = f"[IMAGE: {fi_item.path or 'attachment'}]\n{user_content}" if discussion_history and not history: history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"}) else: @@ -2804,7 +2805,8 @@ def _send_qwen(md_content: str, user_message: str, base_dir: str, if file_items: for fi in file_items: if fi.get("is_image") and fi.get("base64_data"): - user_content = f"[IMAGE: {fi.get('path', 'attachment')}]\n{user_content}" + fi_item = fi if hasattr(fi, 'path') else models.FileItem(path=fi.get('path', 'attachment')) + user_content = f"[IMAGE: {fi_item.path or 'attachment'}]\n{user_content}" if discussion_history and not history: history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"}) else: @@ -2895,7 +2897,8 @@ def _send_llama(md_content: str, user_message: str, base_dir: str, if file_items: for fi in file_items: if fi.get("is_image") and fi.get("base64_data"): - user_content = f"[IMAGE: {fi.get('path', 'attachment')}]\n{user_content}" + fi_item = fi if hasattr(fi, 'path') else models.FileItem(path=fi.get('path', 'attachment')) + user_content = f"[IMAGE: {fi_item.path or 'attachment'}]\n{user_content}" if discussion_history and not history: history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"}) else: diff --git a/src/app_controller.py b/src/app_controller.py index 4599391d..8a33c2fc 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -3507,7 +3507,7 @@ class AppController: `self._last_request_errors` for sub-track 4 GUI display.""" try: symbols = parse_symbols(user_msg) - file_paths = [f['path'] for f in file_items] + file_paths = [f.path if hasattr(f, 'path') else f for f in file_items] for symbol in symbols: res = get_symbol_definition(symbol, file_paths) if res: