Private
Public Access
0
0

feat(video_analysis): ocr_frames.py with TDD (4 tests, winsdk + tesseract backends)

This commit is contained in:
2026-06-21 15:35:41 -04:00
parent 9ccdedeeb3
commit ed0d198afe
5 changed files with 1112 additions and 0 deletions
@@ -0,0 +1,198 @@
{
"track_id": "any_type_componentization_20260621",
"name": "Any-Type Componentization (Promote dict[str, Any] to dataclass(frozen=True))",
"initialized": "2026-06-21",
"owner": "tier2-tech-lead",
"priority": "medium",
"status": "active",
"type": "refactor + ai-readability + type-safety",
"scope": {
"new_files": [
"src/mcp_tool_specs.py",
"src/openai_schemas.py",
"src/provider_state.py",
"scripts/audit_dataclass_coverage.py",
"scripts/audit_dataclass_coverage.baseline.json",
"tests/test_audit_dataclass_coverage.py",
"tests/test_mcp_tool_specs.py",
"tests/test_openai_schemas.py",
"tests/test_provider_state.py",
"docs/type_registry/src_mcp_tool_specs.md",
"docs/type_registry/src_openai_schemas.md",
"docs/type_registry/src_provider_state.md",
"docs/reports/TRACK_COMPLETION_any_type_componentization_20260621.md"
],
"modified_files": [
"src/type_aliases.py",
"src/mcp_client.py",
"src/openai_compatible.py",
"src/ai_client.py",
"src/log_registry.py",
"src/session_logger.py",
"src/log_pruner.py",
"src/gui_2.py",
"src/api_hooks.py",
"src/api_hook_client.py",
"conductor/code_styleguides/type_aliases.md",
"docs/type_registry/src_ai_client.md",
"docs/type_registry/src_openai_compatible.md",
"docs/type_registry/src_mcp_client.md",
"docs/type_registry/src_api_hooks.md",
"docs/type_registry/src_log_registry.md"
],
"deleted_files": []
},
"blocked_by": [
"data_structure_strengthening_20260606"
],
"blocks": [
"any_type_componentization_phase2_2026MMDD",
"openai_tools_dataclass_bridge_2026MMDD"
],
"estimated_phases": 7,
"spec": "spec.md",
"plan": "plan.md (to be authored by writing-plans skill after spec approval)",
"priority_order": "A (5 fat-struct conversions + audit gate) > B (JsonValue + styleguide §12) > C (registry updates) > D (cross-phase coupling follow-up)",
"input_artifact": {
"report": "docs/reports/ANY_TYPE_AUDIT_20260621.md",
"date": "2026-06-21",
"findings_total": 300,
"candidates_identified": 5,
"candidates_sites": 89
},
"reference_pattern": {
"file": "src/vendor_capabilities.py",
"lines": "64-76",
"template": "@dataclass(frozen=True) + module-level _REGISTRY dict + factory function"
},
"candidates": {
"p1_mcp_tool_specs": {
"file": "src/mcp_client.py",
"current": "MCP_TOOL_SPECS: list[dict[str, Any]] (45 tools)",
"target_module": "src/mcp_tool_specs.py (new)",
"sites": 8,
"value": "HIGH"
},
"p1_openai_schemas": {
"file": "src/openai_compatible.py",
"current": "NormalizedResponse + OpenAICompatibleRequest with list[dict[str, Any]] fields",
"target_module": "src/openai_schemas.py (new)",
"sites": 17,
"value": "HIGH"
},
"p2_provider_state": {
"file": "src/ai_client.py",
"current": "7× _<provider>_history + 7× _<provider>_history_lock module globals",
"target_module": "src/provider_state.py (new)",
"sites": 41,
"value": "HIGH"
},
"p2_log_registry_session": {
"file": "src/log_registry.py",
"current": "self.data: dict[str, dict[str, Any]]",
"target_module": "src/log_registry.py (inline)",
"sites": 7,
"value": "MEDIUM"
},
"p3_api_hooks_websocket": {
"file": "src/api_hooks.py",
"current": "def broadcast(channel, payload: dict[str, Any]) + _serialize_for_api",
"target_module": "src/api_hooks.py (inline)",
"sites": 16,
"value": "LOW"
}
},
"audit_ci_gate": {
"script": "scripts/audit_dataclass_coverage.py",
"modes": {
"default": "informational (exit 0)",
"--json": "machine-readable report",
"--strict": "CI gate (exit 1 if current > baseline)",
"--baseline": "path to baseline file (default: scripts/audit_dataclass_coverage.baseline.json)"
},
"baseline_after_track": "211 (300 Any sites - 89 promoted = 211 remaining)"
},
"phases": {
"phase_0": {
"name": "Shared scaffolding",
"scope": "JsonValue TypeAlias + dataclass-coverage audit + styleguide §12",
"estimated_commits": 3,
"files": ["src/type_aliases.py", "scripts/audit_dataclass_coverage.py", "conductor/code_styleguides/type_aliases.md"]
},
"phase_1": {
"name": "mcp_tool_specs (P1)",
"scope": "src/mcp_tool_specs.py new; src/mcp_client.py refactor 8 sites",
"estimated_commits": 10,
"files": ["src/mcp_tool_specs.py", "src/mcp_client.py", "src/ai_client.py"]
},
"phase_2": {
"name": "openai_schemas (P1)",
"scope": "src/openai_schemas.py new; 17 sites in src/openai_compatible.py + src/ai_client.py",
"estimated_commits": 10,
"files": ["src/openai_schemas.py", "src/openai_compatible.py", "src/ai_client.py"]
},
"phase_3": {
"name": "provider_state (P2)",
"scope": "src/provider_state.py new; 41 sites in src/ai_client.py",
"estimated_commits": 15,
"files": ["src/provider_state.py", "src/ai_client.py"]
},
"phase_4": {
"name": "log_registry Session (P2)",
"scope": "7 sites in src/log_registry.py + 3 consumer files",
"estimated_commits": 5,
"files": ["src/log_registry.py", "src/session_logger.py", "src/log_pruner.py", "src/gui_2.py"]
},
"phase_5": {
"name": "api_hooks WebSocketMessage (P3)",
"scope": "16 sites in src/api_hooks.py",
"estimated_commits": 5,
"files": ["src/api_hooks.py"]
},
"phase_6": {
"name": "Verify + archive",
"scope": "Full audit + 11-tier regression + docs + archive move",
"estimated_commits": 2,
"files": ["docs/reports/TRACK_COMPLETION_*", "conductor/tracks.md"]
}
},
"total_estimated_commits": 50,
"ai_performance_analysis": {
"win": "Closed-shape types vs open dicts. The AI now sees `.tool_calls[0].function.name` (field access; type-checked) instead of `tool_calls[0]['function']['name']` (3 nested dict-key lookups; untyped). Static analysis can verify field existence.",
"cost": "Migration overhead (~50 commits). New dataclass vocabulary for the AI to learn (similar to the 10 TypeAliases from data_structure_strengthening). Cross-phase coupling deferred (Phase 2's tools field stays as list[dict[str, Any]] for now).",
"caveat": "Frozen dataclasses are slightly slower to construct than dict literals (~microseconds). For hot paths (per-provider history append), this is negligible. The JSON wire format (`JsonValue`) is type-level only; runtime serialization is unchanged.",
"honest_assessment": "Net win. The 5 candidates are the highest-value fat-struct sites identified by the audit. Promoting them to frozen dataclasses + registries adds type safety, IDE autocomplete, and dispatch verification. The remaining 211 Any sites are intentional flexibility (Patterns 3/4/5) and stay as Any."
},
"architectural_invariant": "Frozen dataclasses are the canonical pattern for closed-shape data in this codebase. TypeAlias remains the canonical pattern for open-shape data. The decision tree lives in conductor/code_styleguides/type_aliases.md §12 (added in Phase 0).",
"threading_constraint": "Phase 3 (provider_state) consolidates 7 locks into a single _PROVIDER_HISTORIES dict. Each ProviderHistory instance owns its own lock (via default_factory=threading.Lock). The lock semantics are unchanged from the current per-provider locks.",
"verification_criteria": [
"src/mcp_tool_specs.py exists with ToolParameter + ToolSpec + registry",
"src/openai_schemas.py exists with ToolCall + ChatMessage + UsageStats",
"src/provider_state.py exists with ProviderHistory + _PROVIDER_HISTORIES dict",
"src/log_registry.py has Session + SessionMetadata dataclasses",
"src/api_hooks.py has WebSocketMessage + JsonValue TypeAlias usage",
"src/type_aliases.py extended with JsonPrimitive + JsonValue",
"scripts/audit_dataclass_coverage.py exists with --strict mode",
"scripts/audit_dataclass_coverage.baseline.json committed",
"conductor/code_styleguides/type_aliases.md has §12 When to Promote section",
"6 new test files exist with 48+ tests (Phase 0 audit: 6, Phase 1: 8, Phase 2: 10, Phase 3: 10, Phase 4: 8, Phase 5: 6)",
"All existing tests pass (no regressions in 11-tier batched run)",
"audit_weak_types.py --strict exits 0",
"audit_dataclass_coverage.py --strict exits 0",
"generate_type_registry.py --check exits 0 (5 new .md files appear)",
"docs/reports/TRACK_COMPLETION_any_type_componentization_20260621.md written",
"Track archived; conductor/tracks.md updated"
],
"sequencing_note": "Per user direction 2026-06-21: this track is NOT blocked by code_path_audit_20260607. The two tracks are orthogonal (semantic clarity vs runtime cost). Both can run in parallel.",
"links": {
"input_report": "docs/reports/ANY_TYPE_AUDIT_20260621.md",
"parent_track": "conductor/tracks/data_structure_strengthening_20260606/",
"reference_pattern": "src/vendor_capabilities.py",
"audit_template": "scripts/audit_weak_types.py",
"type_alias_module": "src/type_aliases.py",
"code_styleguide": "conductor/code_styleguides/type_aliases.md",
"error_handling_styleguide": "conductor/code_styleguides/error_handling.md",
"testing_guide": "docs/guide_testing.md",
"parallel_track": "conductor/tracks/code_path_audit_20260607/"
}
}
@@ -0,0 +1,633 @@
# Track: Any-Type Componentization (Promote `dict[str, Any]` to `dataclass(frozen=True)`)
**Status:** Active (spec approved 2026-06-21)
**Initialized:** 2026-06-21
**Owner:** Tier 2 Tech Lead
**Priority:** Medium (developer + AI-readability; not a regression blocker)
---
## 1. Overview
The `data_structure_strengthening_20260606` track established the `TypeAlias` convention: 10 aliases + 1 `NamedTuple` in `src/type_aliases.py`, replacing 416 of 528 weak-type sites (79% reduction) across 6 high-traffic files. The aliases are **renames** — they point to the same underlying `dict[str, Any]` / `list[dict[str, Any]]` shapes. The alias names document intent; they do not add type safety.
A follow-on audit (`docs/reports/ANY_TYPE_AUDIT_20260621.md`, committed 2026-06-21) identified **5 fat-struct candidates** that warrant promotion to `dataclass(frozen=True)` definitions, following the `src/vendor_capabilities.py` pattern (`frozen=True` dataclass + module-level registry + factory function). This track is the implementation of the audit's recommendations.
**The 5 candidates (89 of the 300 `Any` usages, ~30%):**
| Rank | Target | Sites | Value |
|---|---|---:|---|
| P1 | `src/mcp_client.py: MCP_TOOL_SPECS` (45 tools) | 8 | HIGH — 180 implicit fields become explicit |
| P1 | `src/openai_compatible.py: NormalizedResponse + OpenAICompatibleRequest` | 17 | HIGH — well-documented OpenAI schema |
| P2 | `src/ai_client.py: 7× ProviderHistory + 7 locks` | 41 | HIGH — 14 module globals → 1 dict |
| P2 | `src/log_registry.py: Session metadata` | 7 | MEDIUM — 2 levels of structural anonymity |
| P3 | `src/api_hooks.py: WebSocketMessage + JsonValue` | 16 | LOW — generic serialization |
**The audit's 5-pattern taxonomy (`ANY_TYPE_AUDIT_20260621.md` §2.2):** only Pattern 1 (JSON-shaped payloads) and Pattern 2 (per-provider message lists) are componentization candidates. Patterns 3 (SDK holders), 4 (`__getattr__`), 5 (generic serialization) stay as `Any` — see §10.
**Scope is deliberately bounded.** The track promotes the 5 fat-struct candidates to `dataclass(frozen=True)`. It does NOT migrate all 300 `Any` usages; it does NOT convert `TypeAlias` definitions to `TypedDict`; it does NOT introduce Pydantic. The audit's recommended boundary is honored.
**Sequencing (revised 2026-06-21 per user direction).** The audit's §5.2 originally proposed gating this track behind `code_path_audit_20260607`. **This gate is removed.** The two tracks are orthogonal:
- `code_path_audit` measures RUNTIME cost per call (CPU/memory)
- `any_type_componentization` measures SEMANTIC clarity (AI-readability)
Neither depends on the other. The code_path_audit's report can retroactively flag which any-type candidates it found in hot paths as a side benefit. Both tracks can run in parallel.
## 2. Goals (Priority Order)
| Priority | Goal | Rationale |
|---|---|---|
| **A (primary)** | Convert the 5 fat-struct candidates (89 sites) to `dataclass(frozen=True)` definitions following `src/vendor_capabilities.py` template | The audit identified these as the high-value subset; aliases alone don't add type safety |
| **A (primary)** | New `scripts/audit_dataclass_coverage.py` with `--strict` mode | The CI gate that prevents regression of dataclass promotion work |
| **B (architectural)** | New `JsonValue` recursive `TypeAlias` (in `src/type_aliases.py`) for the JSON wire format | Phase 5 (api_hooks) needs it; reusable for future JSON-boundary tracks |
| **B (architectural)** | New styleguide §12 "When to Promote `TypeAlias` to `dataclass`" section | Captures the rule that future contributors can apply without re-deriving |
| **C (documentation)** | Update `docs/type_registry/` registry entries for the 3 new modules + modified files | The type-registry generator picks them up automatically; `--check` mode validates |
| **D (forward-looking)** | Note the cross-phase coupling opportunity (Phase 2's `OpenAICompatibleRequest.tools` could consume Phase 1's `ToolSpec`) as a follow-up track — NOT in this track | Cross-phase coupling is a future concern; this track ships each phase independently |
### 2.1 Non-Goals (this track)
- **NOT** converting all 300 `Any` usages. Only the 5 fat-struct candidates.
- **NOT** converting SDK client holders (Pattern 3). They stay as `Any` — heterogeneous SDK types.
- **NOT** changing the `__getattr__` dynamic-dispatch pattern (Pattern 4). It stays as `Any` — intentional.
- **NOT** typing the generic serialization functions (Pattern 5). They stay as `Any` — input-driven.
- **NOT** converting `dict[str, Any]` to `TypedDict` (per `data_structure_strengthening_20260606` §10, deferred to a separate decision).
- **NOT** introducing Pydantic (would be a much larger architectural decision).
- **NOT** changing function signatures at the runtime level (dataclasses are serialization-compatible via `from_dict()`/`to_dict()` helpers).
- **NOT** waiting for `code_path_audit_20260607` (per the §1 sequencing revision).
## 3. Architecture
### 3.1 The Reference Pattern: `src/vendor_capabilities.py`
`src/vendor_capabilities.py` is the **canonical "module-level abstraction layer"** (76 lines):
```python
@dataclass(frozen=True)
class VendorCapabilities:
vendor: str
model: str
vision: bool = False
tool_calling: bool = True
caching: bool = False
# ... 22 named fields total
_REGISTRY: dict[tuple[str, str], VendorCapabilities] = {}
def register(cap: VendorCapabilities) -> None: ...
def get_capabilities(vendor: str, model: str) -> VendorCapabilities: ...
```
**Properties that make this pattern successful:**
| Property | Why it matters |
|---|---|
| `frozen=True` | Immutable; thread-safe; no accidental mutation |
| Named fields | Every field is addressable by name (no `dict['vision']` lookups) |
| Module-level registry | O(1) lookup; no instantiation overhead |
| Wildcard `*` model | Fallback for unregistered models |
| Flat (no nesting) | Single cache-line access for most queries |
| Registration pattern | Extensible without modifying existing code |
All 5 fat-struct candidates follow this template.
### 3.2 The Conversion API: `from_dict` / `to_dict`
For each new dataclass, the convention is:
```python
@classmethod
def from_dict(cls, data: Metadata) -> Result[Self, ErrorInfo]:
"""Parse a dict into the dataclass. Returns Result for graceful failure."""
def to_dict(self) -> Metadata:
"""Serialize the dataclass back to a dict (for logging, JSON wire)."""
```
The `Result[Self, ErrorInfo]` return type follows the data-oriented convention from `data_oriented_error_handling_20260606` (see `conductor/code_styleguides/error_handling.md`). Conversion failures (missing required field, type mismatch, malformed JSON) return `ErrorInfo` instead of raising.
### 3.3 The `JsonValue` Recursive Type
Phase 5 (`api_hooks.py`) needs a type for arbitrary JSON-shaped data. Python 3.12+ has `type` statement; earlier versions need a `TypeAlias`:
```python
# src/type_aliases.py (extension)
JsonPrimitive: TypeAlias = str | int | float | bool | None
JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"]
```
This makes `_serialize_for_api(obj: Any) -> JsonValue` and `broadcast(message: WebSocketMessage)` (with `payload: JsonValue`) explicit.
### 3.4 Module Layout
```
src/
type_aliases.py # MODIFIED: add JsonPrimitive + JsonValue TypeAliases
vendor_capabilities.py # UNCHANGED: the reference pattern (no edits)
mcp_tool_specs.py # NEW: ToolParameter + ToolSpec dataclasses + registry
openai_schemas.py # NEW: ToolCall + ToolCallFunction + ChatMessage + UsageStats
provider_state.py # NEW: ProviderHistory dataclass + _PROVIDER_HISTORIES dict
mcp_client.py # MODIFIED: MCP_TOOL_SPECS -> list[ToolSpec]; update dispatch
openai_compatible.py # MODIFIED: NormalizedResponse + OpenAICompatibleRequest use ChatMessage/UsageStats/ToolSpec
ai_client.py # MODIFIED: replace 14 globals with _PROVIDER_HISTORIES dict; update _send_grok/_send_minimax/_send_llama
log_registry.py # MODIFIED: add Session + SessionMetadata dataclasses
session_logger.py # MODIFIED: use Session dataclass
log_pruner.py # MODIFIED: use Session dataclass
gui_2.py # MODIFIED: Log Management panel uses Session
api_hooks.py # MODIFIED: add WebSocketMessage dataclass; _serialize_for_api -> JsonValue
scripts/
audit_dataclass_coverage.py # NEW: counts anonymous dict[str, Any] per module; --strict mode
audit_dataclass_coverage.baseline.json # NEW: baseline count post-track
audit_weak_types.py # UNCHANGED (still gates the alias convention)
generate_type_registry.py # UNCHANGED (registry generator; auto-includes new modules)
conductor/
code_styleguides/
type_aliases.md # MODIFIED: add §12 "When to Promote TypeAlias to dataclass"
tests/
test_mcp_tool_specs.py # NEW
test_openai_schemas.py # NEW
test_provider_state.py # NEW
test_log_registry_dataclasses.py # NEW (or extend existing)
test_api_hooks_dataclasses.py # NEW (or extend existing)
test_audit_dataclass_coverage.py # NEW
(existing test files): # MODIFIED: update call sites; existing tests should pass unchanged
docs/
type_registry/ # AUTO-GENERATED: new modules appear automatically
mcp_tool_specs.md # NEW (generated)
openai_schemas.md # NEW (generated)
provider_state.md # NEW (generated)
api_hooks.md # NEW (generated; replaces existing 16-Any-flavored entry)
log_registry.md # NEW (generated)
src_ai_client.md # MODIFIED (generated; ProviderHistory changes shape)
src_openai_compatible.md # MODIFIED (generated; NormalizedResponse changes shape)
src_mcp_client.md # MODIFIED (generated; MCP_TOOL_SPECS changes shape)
docs/reports/
TRACK_COMPLETION_any_type_componentization_20260621.md # NEW (end-of-track)
```
### 3.5 Coexistence with the Type-Alias Convention
The new dataclasses **complement** the `TypeAlias` convention (not replace it):
- **`TypeAlias`** = rename a shape that's still a dict at runtime (cheap; 0 structural cost)
- **`dataclass(frozen=True)`** = give the shape fields + methods + invariants (expensive; changes runtime type)
The decision tree (now in styleguide §12):
```
Is the shape open-ended (extra keys allowed, no invariants)? ──► TypeAlias (Metadata)
Is the shape a closed set of named fields with specific types? ──► dataclass(frozen=True)
Is the shape a JSON wire format (recursive)? ──► JsonValue (TypeAlias)
```
The 5 fat-struct candidates are closed sets of named fields. The 112 remaining `dict[str, Any]` sites in the audit's 27 lower-impact files are mostly open-ended (provider payloads, config dicts) and stay as `TypeAlias` (or even raw `dict[str, Any]`) until a future track identifies them as closed-shape candidates.
## 4. Per-Phase Plan
### Phase 0: Shared scaffolding (1 task; ~3 commits)
- **WHERE:** `src/type_aliases.py`, `scripts/audit_dataclass_coverage.py`, `conductor/code_styleguides/type_aliases.md`
- **WHAT:** Add `JsonPrimitive` + `JsonValue` TypeAliases; new audit script that counts anonymous `dict[str, Any]` per module with `--strict` mode (CI gate); styleguide §12
- **HOW:** Use the existing `audit_weak_types.py` script as the template for the new audit; follow `audit_weak_types.py:130-160` for the `--strict` mode pattern
- **SAFETY:** No behavior change; type aliases + new audit script are additive
- **TESTS:** `tests/test_audit_dataclass_coverage.py` (6+ tests; mirror `tests/test_audit_weak_types.py`)
- **VERIFICATION:** `uv run python scripts/audit_dataclass_coverage.py --strict` exits 0 (baseline == current)
- **COMMIT:** `feat(scaffold): JsonValue TypeAlias + dataclass-coverage audit + styleguide §12`
### Phase 1: `src/mcp_tool_specs.py` (P1, 8 sites)
**Current state** (`src/mcp_client.py:1944-2747`):
```python
MCP_TOOL_SPECS: list[dict[str, Any]] = [
{ "name": "py_remove_def", "description": "...", "parameters": {...} },
# ... 44 more dicts of identical shape
]
TOOL_NAMES: set[str] = {t['name'] for t in MCP_TOOL_SPECS} # line 2747
```
**Refactor target:**
```python
# src/mcp_tool_specs.py (NEW; ~120 lines)
@dataclass(frozen=True)
class ToolParameter:
name: str
type: str # "string" | "integer" | "boolean" | "object" | "array"
description: str
required: bool = False
enum: Optional[list[str]] = None
@dataclass(frozen=True)
class ToolSpec:
name: str
description: str
parameters: tuple[ToolParameter, ...]
category: str = "file"
_REGISTRY: dict[str, ToolSpec] = {}
def register(spec: ToolSpec) -> None: ...
def get_tool_spec(name: str) -> ToolSpec: ...
def get_tool_schemas() -> list[ToolSpec]: ...
def tool_names() -> set[str]: ...
```
**Call sites to update:**
- `src/mcp_client.py:1944` `native_names = {t['name'] for t in MCP_TOOL_SPECS}``mcp_tool_specs.tool_names()`
- `src/mcp_client.py:1958` `res = list(MCP_TOOL_SPECS)``res = mcp_tool_specs.get_tool_schemas()`
- `src/mcp_client.py:1972` `MCP_TOOL_SPECS: list[dict[str, Any]] = [...]` → moved to `mcp_tool_specs.py:_REGISTRY`
- `src/mcp_client.py:2747` `TOOL_NAMES: set[str] = {t['name'] for t in MCP_TOOL_SPECS}``mcp_tool_specs.tool_names()`
- `src/ai_client.py:560,582,1012` `mcp_client.TOOL_NAMES``mcp_tool_specs.tool_names()` (3 sites)
- `src/app_controller.py:2103,2962,3263` `models.AGENT_TOOL_NAMES` (cross-check; not directly `TOOL_NAMES`)
**Compatibility shim:** keep `mcp_client.MCP_TOOL_SPECS` and `mcp_client.TOOL_NAMES` as thin re-exports for the duration of this phase, then remove in a follow-up commit if no external test breaks. Alternative: deprecate immediately and fix the 3 callers.
**Tests:** `tests/test_mcp_tool_specs.py` (8+ tests)
- Verify all 45 tools are registered
- Verify `get_tool_spec("py_remove_def")` returns correct spec
- Verify `tool_names()` matches expected set
- Verify `from_dict()` returns `Result` for valid + invalid inputs
- Verify `TOOL_NAMES` is a subset of `models.AGENT_TOOL_NAMES` (cross-module invariant)
### Phase 2: `src/openai_schemas.py` (P1, 17 sites)
**Current state** (`src/openai_compatible.py:10-30`):
```python
@dataclass(frozen=True)
class NormalizedResponse:
text: str
tool_calls: list[dict[str, Any]] # FAT: JSON tool call shape
usage_input_tokens: int
usage_output_tokens: int
usage_cache_read_tokens: int
usage_cache_creation_tokens: int
raw_response: Any # FAT: SDK-specific response (Pattern 3, stay)
@dataclass
class OpenAICompatibleRequest:
messages: list[dict[str, Any]] # FAT: message shape
model: str
...
tools: Optional[list[dict[str, Any]]] = None # FAT: tool schema (cross-phase: Phase 1)
extra_body: Optional[dict[str, Any]] = None # FAT: arbitrary params
```
**Refactor target:**
```python
# src/openai_schemas.py (NEW; ~150 lines)
@dataclass(frozen=True)
class ToolCall:
id: str
type: str = "function"
function: "ToolCallFunction"
@dataclass(frozen=True)
class ToolCallFunction:
name: str
arguments: str # JSON string
@dataclass(frozen=True)
class ChatMessage:
role: str # "system" | "user" | "assistant" | "tool"
content: str
tool_calls: Optional[tuple[ToolCall, ...]] = None
tool_call_id: Optional[str] = None
name: Optional[str] = None
@dataclass(frozen=True)
class UsageStats:
input_tokens: int
output_tokens: int
cache_read_tokens: int = 0
cache_creation_tokens: int = 0
# NormalizedResponse becomes:
@dataclass(frozen=True)
class NormalizedResponse:
text: str
tool_calls: tuple[ToolCall, ...]
usage: UsageStats # was 4 separate fields
raw_response: Any # Unavoidable: SDK-specific
# OpenAICompatibleRequest becomes:
@dataclass
class OpenAICompatibleRequest:
messages: list[ChatMessage]
model: str
temperature: float = 0.0
top_p: float = 1.0
max_tokens: int = 8192
tools: Optional[list[dict[str, Any]]] = None # Cross-phase: Phase 1's ToolSpec (deferred)
tool_choice: str = "auto"
stream: bool = False
stream_callback: Optional[Callable[[str], None]] = None
extra_body: Optional[dict[str, Any]] = None
```
**Cross-phase coupling (deferred):** `OpenAICompatibleRequest.tools: Optional[list[ToolSpec]]` would reuse Phase 1's `ToolSpec`. This is a follow-up track concern; Phase 2 ships with `list[dict[str, Any]]` for that field with a `# TODO(future-track): migrate to list[ToolSpec]` note.
**Call sites to update:**
- `src/openai_compatible.py` itself (~5 internal functions consuming `NormalizedResponse`)
- `src/ai_client.py` `_send_grok()`, `_send_minimax()`, `_send_llama()` (~3 functions; they construct `NormalizedResponse` and `OpenAICompatibleRequest`)
- `src/api_hook_client.py` (the API hook payloads may serialize these; cross-check)
**Tests:** `tests/test_openai_schemas.py` (10+ tests)
- Verify `ChatMessage.from_dict()` round-trip for all 4 roles
- Verify `UsageStats` field access
- Verify `ToolCall.function.arguments` JSON parsing
- Verify `Result[Self, ErrorInfo]` error cases (missing required field, malformed JSON)
- Verify `NormalizedResponse.raw_response` is still `Any` (Pattern 3)
### Phase 3: `src/provider_state.py` (P2, 41 sites)
**Current state** (`src/ai_client.py:111-133`):
```python
_anthropic_history: list[Metadata] = []
_anthropic_history_lock: threading.Lock = threading.Lock()
_deepseek_history: list[Metadata] = []
_deepseek_history_lock: threading.Lock = threading.Lock()
# ... 7 providers × 2 vars = 14 module globals
```
Plus the SDK client holders (Pattern 3, stay):
```python
_gemini_chat: Any = None
_deepseek_client: Any = None
# ... 7 SDK clients stay as-is
```
**Refactor target:**
```python
# src/provider_state.py (NEW; ~80 lines)
@dataclass
class ProviderHistory:
messages: list[Metadata] = field(default_factory=list)
lock: threading.Lock = field(default_factory=threading.Lock)
def append(self, message: Metadata) -> None: ...
def get_all(self) -> list[Metadata]: ...
def replace_all(self, messages: list[Metadata]) -> None: ...
def clear(self) -> None: ...
_PROVIDER_HISTORIES: dict[str, ProviderHistory] = {
"anthropic": ProviderHistory(),
"deepseek": ProviderHistory(),
"minimax": ProviderHistory(),
"qwen": ProviderHistory(),
"grok": ProviderHistory(),
"llama": ProviderHistory(),
}
def get_history(provider: str) -> ProviderHistory:
return _PROVIDER_HISTORIES[provider]
```
**Call sites to update** (`src/ai_client.py`):
- Lines 463-466: `global _anthropic_history` declarations (4 declarations across `cleanup()` and similar) → removed
- Lines 483-499: 7 `with _<provider>_history_lock:` blocks in `cleanup()``get_history("<provider>").clear()`
- Lines 1447, 1457-1460, 1469, 1471, 1475, 1489, 1503, 1506, 1582: ~20 `_anthropic_history` references → `get_history("anthropic").messages` and `.append()`
- Lines 2201-2202, 2221-2222, 2353, 2360, 2418-2420: ~10 `_deepseek_history` references → `get_history("deepseek")`
- Lines 2575-2588, 2605: ~10 `_grok_history` references → `get_history("grok")`
- Lines 2659-2685: ~10 `_minimax_history` references → `get_history("minimax")`
- Lines 2812-2823: ~8 `_qwen_history` references → `get_history("qwen")`
- Lines 2901-2925: ~8 `_llama_history` references → `get_history("llama")`
- The `_repair_<provider>_history()` and `_trim_<provider>_history()` helpers (lines 1353, 1381, 2138, 2462, 2482) take `history: list[Metadata]` parameters — they stay as-is; call sites pass `get_history("<provider>").messages`
**Tests:** `tests/test_provider_state.py` (10+ tests)
- Verify `ProviderHistory.append()` is thread-safe (lock semantics)
- Verify `ProviderHistory.clear()` resets the list atomically
- Verify `get_history("anthropic")` returns the same instance across calls (singleton)
- Verify `replace_all()` swaps the list under lock
- Verify `cleanup()` clears all 6 histories
- Verify SDK client holders (`_gemini_chat`, etc.) are NOT touched (Pattern 3 preserved)
**Risk:** This phase has the largest ripple. The 41 sites include 14 module globals (renames are mechanical) + ~27 call-site updates. The audit may undercount if helper functions in `ai_client.py` reference these globals beyond the listed lines. **Mitigation:** Phase 3 has its own audit baseline snapshot before starting; any new finds get added to the phase's task list.
### Phase 4: `src/log_registry.py: Session` (P2, 7 sites)
**Current state** (`src/log_registry.py:58`):
```python
self.data: dict[str, dict[str, Any]] = {} # session_id -> session content
```
The outer key is `session_id: str`. The inner dict has implicit fields: `path`, `start_time`, `whitelisted`, `metadata`.
**Refactor target** (inline in `src/log_registry.py`):
```python
@dataclass(frozen=True)
class SessionMetadata:
message_count: int = 0
errors: int = 0
size_kb: int = 0
whitelisted: bool = False
reason: str = ''
timestamp: Optional[str] = None
@dataclass(frozen=True)
class Session:
session_id: str
path: str
start_time: str # ISO format
whitelisted: bool = False
metadata: Optional[SessionMetadata] = None
@dataclass
class LogRegistry:
registry_path: str
data: dict[str, Session] = field(default_factory=dict) # typed!
```
**Call sites to update:**
- `src/log_registry.py` `get_old_non_whitelisted_sessions()` and 6 other internal methods
- `src/session_logger.py` `open_session()`, `close_session()`
- `src/log_pruner.py` `prune_old_logs()`
- `src/gui_2.py` Log Management panel (find via `grep "log_registry"` or "session_log")
**Tests:** `tests/test_log_registry_dataclasses.py` (or extend existing)
- Verify `Session.from_dict()` round-trip
- Verify `Session.metadata` is `Optional[SessionMetadata]`
- Verify `LogRegistry.data: dict[str, Session]` (no longer `dict[str, dict[str, Any]]`)
- Verify `prune_old_logs()` works on the new schema
### Phase 5: `src/api_hooks.py: WebSocketMessage + JsonValue` (P3, 16 sites)
**Current state** (`src/api_hooks.py:48-145`):
```python
def _get_app_attr(app: Any, name: str, default: Any = None) -> Any: ...
def _set_app_attr(app: Any, name: str, value: Any) -> None: ...
def _serialize_for_api(obj: Any) -> Any: ...
def broadcast(self, channel: str, payload: dict[str, Any]) -> None: ...
```
The `_get_app_attr` / `_set_app_attr` are Pattern 4 (stay as `Any`).
The `_serialize_for_api` and `broadcast` are the JSON wire format.
**Refactor target** (inline in `src/api_hooks.py`):
```python
from src.type_aliases import JsonValue
@dataclass(frozen=True)
class WebSocketMessage:
channel: str
payload: JsonValue
def _serialize_for_api(obj: Any) -> JsonValue: ...
def broadcast(self, message: WebSocketMessage) -> None: ...
```
**Call sites to update:** `broadcast()` callers (~5-10 sites across `src/app_controller.py`, `src/gui_2.py`)
**Tests:** extend `tests/test_api_hooks.py`
- Verify `WebSocketMessage` is `frozen=True` (cannot mutate)
- Verify `JsonValue` round-trip via `_serialize_for_api`
- Verify `_get_app_attr` / `_set_app_attr` signatures are unchanged (Pattern 4 preserved)
### Phase 6: Verification + docs + archive
- Run full audit: `audit_weak_types.py --strict` exits 0; `audit_dataclass_coverage.py --strict` exits 0
- Run full regression suite: 11-tier batched (per `test_sandbox_hardening_20260619` convention)
- Regenerate `docs/type_registry/` via `scripts/generate_type_registry.py`
- Verify `--check` mode passes
- Write end-of-track report at `docs/reports/TRACK_COMPLETION_any_type_componentization_20260621.md`
- Move `conductor/tracks/any_type_componentization_20260621/``conductor/tracks/archive/`
- Update `conductor/tracks.md`
## 5. The Audit Script as a Permanent CI Gate
The new `scripts/audit_dataclass_coverage.py` mirrors `audit_weak_types.py`'s design:
**Modes:**
- Default: informational (exits 0; prints report)
- `--json`: machine-readable
- `--strict`: CI gate (exits 1 if current anonymous `dict[str, Any]` count > baseline)
- `--baseline`: path to baseline file (default: `scripts/audit_dataclass_coverage.baseline.json`)
**What it counts:** sites where the structural anonymity persists (the 89 this track targets). Aliases that point to `dict[str, Any]` (e.g., `Metadata`, `CommsLogEntry`) are NOT counted; the audit counts actual `dict[str, Any]` / `list[dict[...]]` annotations and the remaining `Any` usages outside the 5 candidates.
**Baseline:** committed at `scripts/audit_dataclass_coverage.baseline.json` post-Phase-6. Expected: 211 `Any` sites remain (300 - 89 = 211). The audit's 5-pattern taxonomy justifies the boundary.
## 6. Configuration
No new dependencies. No new environment variables. No new config files.
The new dataclasses use stdlib `dataclasses.dataclass(frozen=True)` (Python 3.11+).
## 7. Testing Strategy
| Test File | Purpose | Coverage Target |
|---|---|---|
| `tests/test_audit_dataclass_coverage.py` | Verify the audit script's patterns + `--strict` mode + baseline | 90% |
| `tests/test_mcp_tool_specs.py` | Verify 45 tools registered + dispatch + cross-module invariants | 100% |
| `tests/test_openai_schemas.py` | Verify ChatMessage/UsageStats/ToolCall round-trips + Result[T] errors | 100% |
| `tests/test_provider_state.py` | Verify ProviderHistory thread safety + cleanup + singleton semantics | 100% |
| `tests/test_log_registry_dataclasses.py` | Verify Session dataclass + LogRegistry typed | 100% |
| `tests/test_api_hooks.py` (extended) | Verify WebSocketMessage + JsonValue round-trip | 100% |
| `tests/test_ai_client.py` (existing) | No regressions after 41-site Phase 3 refactor | 100% (regression) |
| `tests/test_mcp_client.py` (existing) | No regressions after Phase 1 dispatch refactor | 100% (regression) |
| `tests/test_openai_compatible.py` (existing) | No regressions after Phase 2 refactor | 100% (regression) |
| `tests/test_log_registry.py` (existing) | No regressions after Phase 4 | 100% (regression) |
| `tests/test_api_hooks.py` (existing) | No regressions after Phase 5 | 100% (regression) |
**Mocking strategy:** Per the project's structural testing contract (`docs/guide_testing.md`), Tier 3 workers do NOT use `unittest.mock.patch` for core infrastructure. The new tests use the real dataclasses with synthetic `Metadata` inputs.
**Audit baseline check:** Post-Phase-6, `audit_dataclass_coverage.py` should report ≤ baseline count. The dataclass-coverage baseline is expected to be 211 (300 `Any` minus the 89 candidates promoted in this track).
## 8. Migration / Rollout
| Phase | What | Risk | Commits |
|---|---|---|---|
| **0 — Scaffolding** | Add `JsonValue`, new audit, styleguide §12 | Low (additive only) | ~3 |
| **1 — `mcp_tool_specs`** | P1 (8 sites) | Medium (45 tools × ~4 params) | ~10 |
| **2 — `openai_schemas`** | P1 (17 sites) | Medium (cross-module: ai_client consumers) | ~10 |
| **3 — `provider_state`** | P2 (41 sites) | **Medium-High** (14 globals + ~27 call sites) | ~15 |
| **4 — `log_registry` Session** | P2 (7 sites) | Low (self-contained file) | ~5 |
| **5 — `api_hooks` WebSocketMessage** | P3 (16 sites) | Low (Pattern 5 preserved) | ~5 |
| **6 — Verify + archive** | Audit + tests + docs | Low | ~2 |
| **Total** | | | **~50 atomic commits** |
Each phase has its own checkpoint commit and git note (per `conductor/workflow.md` Task Workflow §9-10).
## 9. Risks & Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Phase 3 (`provider_state`) has more call sites than the audit identified. | Medium | Medium | Snapshot an audit baseline before Phase 3; any new finds get added to the phase's task list. Worst case: Phase 3 grows to ~20 commits (still tractable). |
| Phase 1 (`mcp_tool_specs`) dispatch map (`_dispatch_table`) has dead-code that the typed refactor surfaces. | Medium | Low | The dataclass + registry pattern naturally surfaces dead code. Add a "dead code removal" task to Phase 1 if discovered. |
| The `JsonValue` recursive type fails to type-check in Python 3.11. | Low | Low | Use `TypeAlias` with forward-reference (`"JsonValue"`) in `list` and `dict`; tested in Phase 0. |
| A consumer of `mcp_client.TOOL_NAMES` lives outside `src/` (e.g., `tests/`, `conductor/`) and breaks. | Medium | Low | Compatibility shim (re-export) for 1 commit; remove in follow-up. |
| `frozen=True` dataclasses break code that mutates dict fields. | Medium | Medium | Audit each candidate for mutation patterns before phase; convert mutators to `replace()` (returns new instance) per `dataclasses.replace()`. |
| The new audit script's `--strict` mode is too strict (rejects valid uses). | Low | Medium | Set baseline conservatively (post-Phase-6 actual count); tighten only after 1 week of clean CI. |
| Cross-phase coupling (Phase 2's `tools: list[ToolSpec]`) creates merge conflict with Phase 1. | Low | Low | Explicitly deferred; Phase 2 ships with `list[dict[str, Any]]` + TODO comment. |
| The 5 candidates leave 211 `Any` sites untouched; users expect more. | Low | Low | Document in §10 explicitly; the audit's 5-pattern taxonomy justifies the boundary. |
## 10. Out of Scope (Explicit)
- **The remaining 211 `Any` usages** (300 - 89 = 211). The audit's 5-pattern taxonomy identifies these as Patterns 3/4/5 (SDK holders, dynamic dispatch, generic serialization) — they stay as `Any` because they're intentionally flexible. A future track may identify additional fat-struct candidates; this track does not.
- **TypedDict migration** of any alias. Per `data_structure_strengthening_20260606` §10, deferred.
- **Pydantic models.** Not requested; would be a much larger architectural decision.
- **The `JsonValue` recursive type as a runtime validator** (e.g., `jsonschema` validation). The TypeAlias is a type hint, not a runtime guard.
- **Conversion of the `TypeAlias` definitions themselves to `dataclass` (e.g., making `Metadata: TypeAlias = dict[str, Any]` a `class Metadata(dict)`).** The aliases document intent; converting them is a separate decision.
- **Cross-phase coupling** between Phase 1 and Phase 2 (Phase 2's `OpenAICompatibleRequest.tools: list[ToolSpec]`). Deferred to a follow-up track.
- **Wait for `code_path_audit_20260607` to ship.** Per the §1 sequencing revision, the two tracks are orthogonal.
- **Modifying the audit scripts** (`audit_weak_types.py`, `audit_dataclass_coverage.py`) beyond the new `--strict` mode in Phase 0. Future extensions are separate tracks.
## 11. Decisions Made During Spec Authoring
The following design choices were resolved during spec drafting (formerly "Open Questions"):
1. **`ToolSpec.parameters: tuple[ToolParameter, ...]` (RESOLVED)** — Tuple wins. Immutable matches `frozen=True` philosophy; serialization uses explicit `to_dict()` helper. `list[ToolParameter]` would force runtime conversion at every JSON boundary.
2. **`ProviderHistory.clear()` reuses the lock (RESOLVED)** — The lock protects the list, not the lock instance. `default_factory=threading.Lock` in the dataclass field ensures every `ProviderHistory` gets its own lock on construction; `clear()` does NOT reset the lock.
3. **`Session.metadata: Optional[SessionMetadata] = None` (RESOLVED)** — `Optional` with default None wins. Matches existing call patterns in `session_logger.py` where sessions may exist without metadata populated yet.
4. **`JsonValue` lives in `src/type_aliases.py` (RESOLVED)** — Existing file is the canonical location for TypeAliases. New file would split the convention across 2 modules.
5. **No compatibility shim in Phase 1 (RESOLVED)** — Phase 1's 3 call sites in `ai_client.py` are updated immediately. The shim would add a commit of pure re-exports that gets removed in the next commit anyway.
## 12. See Also
### 12.1 Project References
- `docs/reports/ANY_TYPE_AUDIT_20260621.md` — the audit that drove this track (the input artifact)
- `conductor/tracks/data_structure_strengthening_20260606/` — the parent track (the 10 TypeAliases + 1 NamedTuple; this track builds on it)
- `src/vendor_capabilities.py` — the reference pattern (`frozen=True` dataclass + module-level registry + factory)
- `src/type_aliases.py` — the TypeAlias module (extended in Phase 0 with `JsonValue`)
- `scripts/audit_weak_types.py` — the audit script template (`scripts/audit_dataclass_coverage.py` mirrors its design)
- `conductor/code_styleguides/type_aliases.md` — the canonical styleguide (Phase 0 adds §12)
- `conductor/code_styleguides/error_handling.md` — the `Result[T]` convention (used by `from_dict()`)
- `docs/guide_testing.md` — the test infrastructure (live_gui fixture, structural testing contract)
- `docs/reports/TRACK_COMPLETION_data_structure_strengthening_20260606.md` — the parent track's end-of-track report
- `conductor/tracks/code_path_audit_20260607/` — the parallel runtime-cost track (NOT a blocker)
### 12.2 External References
- **Python `dataclasses.dataclass(frozen=True)`** — the canonical pattern for immutable named records (PEP 681 for `dataclass_transform`; Python 3.11+ stdlib).
- **Mike Acton's data-oriented design** — the "data is the API" framing that motivates named fields over dict access.
- **Casey Muratori on module layer boundaries** — the convention that each module owns its data and exposes a clear interface.
- **Ryan Fleury's "errors are just cases"** — the `Result[T]` convention adopted by this track for `from_dict()` return types.
### 12.3 Follow-up Track (planned; NOT in this track)
- **`any_type_componentization_phase2_2026MMDD`** (placeholder): the 211 remaining `Any` sites not in the 5 candidates. Identified by the audit's Pattern 3/4/5 analysis; may yield additional fat-struct candidates as future tracks touch those code areas.
- **`openai_tools_dataclass_bridge_2026MMDD`** (placeholder): the cross-phase coupling opportunity (Phase 2's `OpenAICompatibleRequest.tools: list[ToolSpec]`).
- **`type_registry_ci_20260606`** (planned in `data_structure_strengthening_20260606` §12.1): wires `generate_type_registry.py --check` into CI. This track ships the new modules; the CI gate is a separate concern.
## 13. Verification Criteria (Definition of Done)
- [ ] `src/mcp_tool_specs.py` exists with `ToolParameter` + `ToolSpec` + registry
- [ ] `src/openai_schemas.py` exists with `ToolCall` + `ChatMessage` + `UsageStats`
- [ ] `src/provider_state.py` exists with `ProviderHistory` + `_PROVIDER_HISTORIES` dict
- [ ] `src/log_registry.py` has `Session` + `SessionMetadata` dataclasses
- [ ] `src/api_hooks.py` has `WebSocketMessage` + `JsonValue` TypeAlias usage
- [ ] `src/type_aliases.py` extended with `JsonPrimitive` + `JsonValue`
- [ ] `scripts/audit_dataclass_coverage.py` exists with `--strict` mode
- [ ] `scripts/audit_dataclass_coverage.baseline.json` committed
- [ ] `conductor/code_styleguides/type_aliases.md` has §12 "When to Promote" section
- [ ] 6 new test files exist with 48+ tests (Phase 0 audit: 6, Phase 1: 8, Phase 2: 10, Phase 3: 10, Phase 4: 8, Phase 5: 6)
- [ ] All existing tests pass (no regressions in 11-tier batched run)
- [ ] `audit_weak_types.py --strict` exits 0
- [ ] `audit_dataclass_coverage.py --strict` exits 0
- [ ] `generate_type_registry.py --check` exits 0 (5 new .md files appear)
- [ ] `docs/reports/TRACK_COMPLETION_any_type_componentization_20260621.md` written
- [ ] Track archived; `conductor/tracks.md` updated
@@ -0,0 +1,129 @@
# Track state for any_type_componentization_20260621
# Updated by Tier 2 Tech Lead as tasks complete
[meta]
track_id = "any_type_componentization_20260621"
name = "Any-Type Componentization (Promote dict[str, Any] to dataclass(frozen=True))"
status = "active"
current_phase = 0
last_updated = "2026-06-21"
[blocked_by]
data_structure_strengthening_20260606 = "pending_merge"
[blocks]
any_type_componentization_phase2_2026MMDD = "planned"
openai_tools_dataclass_bridge_2026MMDD = "planned"
[phases]
phase_0 = { status = "pending", checkpointsha = "", name = "Shared scaffolding (JsonValue + audit + styleguide)" }
phase_1 = { status = "pending", checkpointsha = "", name = "mcp_tool_specs (P1, 8 sites)" }
phase_2 = { status = "pending", checkpointsha = "", name = "openai_schemas (P1, 17 sites)" }
phase_3 = { status = "pending", checkpointsha = "", name = "provider_state (P2, 41 sites)" }
phase_4 = { status = "pending", checkpointsha = "", name = "log_registry Session (P2, 7 sites)" }
phase_5 = { status = "pending", checkpointsha = "", name = "api_hooks WebSocketMessage (P3, 16 sites)" }
phase_6 = { status = "pending", checkpointsha = "", name = "Verify + docs + archive" }
[tasks]
# Phase 0: Shared scaffolding
t0_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_audit_dataclass_coverage.py (mirror tests/test_audit_weak_types.py structure; verify regex patterns + Finding dataclass + --strict mode)" }
t0_2 = { status = "pending", commit_sha = "", description = "Green: implement scripts/audit_dataclass_coverage.py (informational + --json + --strict + --baseline modes)" }
t0_3 = { status = "pending", commit_sha = "", description = "Extend src/type_aliases.py with JsonPrimitive + JsonValue TypeAliases" }
t0_4 = { status = "pending", commit_sha = "", description = "Add §12 'When to Promote TypeAlias to dataclass' to conductor/code_styleguides/type_aliases.md" }
t0_5 = { status = "pending", commit_sha = "", description = "Phase 0 checkpoint commit + git note" }
# Phase 1: mcp_tool_specs (P1)
t1_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_mcp_tool_specs.py (verify 45 tools registered; get_tool_spec dispatch; TOOL_NAMES cross-module invariant)" }
t1_2 = { status = "pending", commit_sha = "", description = "Green: create src/mcp_tool_specs.py with ToolParameter + ToolSpec dataclasses + module-level _REGISTRY" }
t1_3 = { status = "pending", commit_sha = "", description = "Migrate MCP_TOOL_SPECS dict literals to ToolSpec instances in src/mcp_tool_specs.py:_REGISTRY" }
t1_4 = { status = "pending", commit_sha = "", description = "Update src/mcp_client.py call sites (lines 1944, 1958, 2747) to use mcp_tool_specs.tool_names() / get_tool_schemas()" }
t1_5 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py:560,582,1012 (3 sites using mcp_client.TOOL_NAMES -> mcp_tool_specs.tool_names())" }
t1_6 = { status = "pending", commit_sha = "", description = "Verify cross-module invariant: TOOL_NAMES is a subset of models.AGENT_TOOL_NAMES" }
t1_7 = { status = "pending", commit_sha = "", description = "Run regression suite on tests/test_mcp_client.py + tests/test_ai_client.py" }
t1_8 = { status = "pending", commit_sha = "", description = "Phase 1 checkpoint commit + git note" }
# Phase 2: openai_schemas (P1)
t2_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_openai_schemas.py (ChatMessage.from_dict round-trip for 4 roles; UsageStats field access; ToolCall.function.arguments JSON parse; Result[T] error cases)" }
t2_2 = { status = "pending", commit_sha = "", description = "Green: create src/openai_schemas.py with ToolCall + ToolCallFunction + ChatMessage + UsageStats dataclasses" }
t2_3 = { status = "pending", commit_sha = "", description = "Refactor src/openai_compatible.py:NormalizedResponse (4 usage fields -> UsageStats; tool_calls -> tuple[ToolCall, ...])" }
t2_4 = { status = "pending", commit_sha = "", description = "Refactor src/openai_compatible.py:OpenAICompatibleRequest (messages -> list[ChatMessage])" }
t2_5 = { status = "pending", commit_sha = "", description = "Update src/openai_compatible.py internal consumers (~5 functions constructing/parsing NormalizedResponse)" }
t2_6 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_grok + _send_minimax + _send_llama (3 functions constructing OpenAICompatibleRequest)" }
t2_7 = { status = "pending", commit_sha = "", description = "Cross-check src/api_hook_client.py for NormalizedResponse/OpenAICompatibleRequest consumers" }
t2_8 = { status = "pending", commit_sha = "", description = "Run regression suite on tests/test_openai_compatible.py + tests/test_ai_client.py" }
t2_9 = { status = "pending", commit_sha = "", description = "Phase 2 checkpoint commit + git note" }
# Phase 3: provider_state (P2)
t3_1 = { status = "pending", commit_sha = "", description = "Audit baseline snapshot: count _<provider>_history + _<provider>_history_lock references in src/ai_client.py" }
t3_2 = { status = "pending", commit_sha = "", description = "Red: tests/test_provider_state.py (ProviderHistory.append thread-safety; clear atomicity; get_history singleton; cleanup clears all 6)" }
t3_3 = { status = "pending", commit_sha = "", description = "Green: create src/provider_state.py with ProviderHistory dataclass + _PROVIDER_HISTORIES dict" }
t3_4 = { status = "pending", commit_sha = "", description = "Remove 7 module globals + 7 lock declarations from src/ai_client.py:111-133" }
t3_5 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py:463-466 (cleanup() global declarations removed)" }
t3_6 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py:483-499 (cleanup() 7 lock blocks -> get_history(p).clear())" }
t3_7 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_anthropic (~20 sites at lines 1447, 1457-1460, 1469, 1471, 1475, 1489, 1503, 1506, 1582)" }
t3_8 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_deepseek (~10 sites at lines 2201-2202, 2221-2222, 2353, 2360, 2418-2420)" }
t3_9 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_grok (~10 sites at lines 2575-2588, 2605)" }
t3_10 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_minimax (~10 sites at lines 2659-2685)" }
t3_11 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_qwen (~8 sites at lines 2812-2823)" }
t3_12 = { status = "pending", commit_sha = "", description = "Update src/ai_client.py _send_llama (~8 sites at lines 2901-2925)" }
t3_13 = { status = "pending", commit_sha = "", description = "Verify SDK client holders (_gemini_chat, etc.) NOT touched (Pattern 3 preserved)" }
t3_14 = { status = "pending", commit_sha = "", description = "Run regression suite on tests/test_ai_client*.py (8 files; 27 tests)" }
t3_15 = { status = "pending", commit_sha = "", description = "Phase 3 checkpoint commit + git note" }
# Phase 4: log_registry Session (P2)
t4_1 = { status = "pending", commit_sha = "", description = "Red: extend tests/test_log_registry.py (Session.from_dict round-trip; Session.metadata Optional; LogRegistry.data typed)" }
t4_2 = { status = "pending", commit_sha = "", description = "Green: add Session + SessionMetadata dataclasses inline in src/log_registry.py" }
t4_3 = { status = "pending", commit_sha = "", description = "Refactor LogRegistry.data: dict[str, dict[str, Any]] -> dict[str, Session]" }
t4_4 = { status = "pending", commit_sha = "", description = "Update src/session_logger.py (open_session, close_session)" }
t4_5 = { status = "pending", commit_sha = "", description = "Update src/log_pruner.py (prune_old_logs)" }
t4_6 = { status = "pending", commit_sha = "", description = "Update src/gui_2.py Log Management panel" }
t4_7 = { status = "pending", commit_sha = "", description = "Run regression suite on tests/test_log_registry.py + tests/test_session_logger.py + tests/test_log_pruner.py" }
t4_8 = { status = "pending", commit_sha = "", description = "Phase 4 checkpoint commit + git note" }
# Phase 5: api_hooks WebSocketMessage (P3)
t5_1 = { status = "pending", commit_sha = "", description = "Red: extend tests/test_api_hooks.py (WebSocketMessage frozen=True; JsonValue round-trip via _serialize_for_api; Pattern 4 preserved)" }
t5_2 = { status = "pending", commit_sha = "", description = "Green: add WebSocketMessage dataclass inline in src/api_hooks.py" }
t5_3 = { status = "pending", commit_sha = "", description = "Update broadcast() signature: (channel, payload: dict[str, Any]) -> (message: WebSocketMessage)" }
t5_4 = { status = "pending", commit_sha = "", description = "Update _serialize_for_api return type: Any -> JsonValue" }
t5_5 = { status = "pending", commit_sha = "", description = "Update broadcast() callers (~5-10 sites across src/app_controller.py, src/gui_2.py)" }
t5_6 = { status = "pending", commit_sha = "", description = "Verify Pattern 4 preserved: _get_app_attr, _set_app_attr signatures unchanged" }
t5_7 = { status = "pending", commit_sha = "", description = "Run regression suite on tests/test_api_hooks.py + tests/test_app_controller.py" }
t5_8 = { status = "pending", commit_sha = "", description = "Phase 5 checkpoint commit + git note" }
# Phase 6: Verify + docs + archive
t6_1 = { status = "pending", commit_sha = "", description = "Run scripts/audit_weak_types.py --strict (exit 0)" }
t6_2 = { status = "pending", commit_sha = "", description = "Run scripts/audit_dataclass_coverage.py --strict (exit 0; generate baseline)" }
t6_3 = { status = "pending", commit_sha = "", description = "Run scripts/generate_type_registry.py (auto-include new modules) + --check (exit 0)" }
t6_4 = { status = "pending", commit_sha = "", description = "Run 11-tier batched regression suite (per test_sandbox_hardening_20260619 convention)" }
t6_5 = { status = "pending", commit_sha = "", description = "Write docs/reports/TRACK_COMPLETION_any_type_componentization_20260621.md" }
t6_6 = { status = "pending", commit_sha = "", description = "git mv conductor/tracks/any_type_componentization_20260621 conductor/tracks/archive/" }
t6_7 = { status = "pending", commit_sha = "", description = "Update conductor/tracks.md (move entry to Recently Completed)" }
t6_8 = { status = "pending", commit_sha = "", description = "Final state.toml update + Phase 6 checkpoint commit + git note" }
[verification]
phase_0_jsonvalue_complete = false
phase_0_audit_script_complete = false
phase_0_styleguide_complete = false
phase_1_mcp_tool_specs_complete = false
phase_2_openai_schemas_complete = false
phase_3_provider_state_complete = false
phase_4_log_registry_complete = false
phase_5_api_hooks_complete = false
phase_6_track_archived = false
full_11_tier_regression_passes = false
audit_weak_types_strict_passes = false
audit_dataclass_coverage_strict_passes = false
type_registry_check_passes = false
[candidate_progression]
# Filled as phases complete
p1_mcp_tool_specs_sites = 8
p1_openai_schemas_sites = 17
p2_provider_state_sites = 41
p2_log_registry_sites = 7
p3_api_hooks_sites = 16
total_candidate_sites = 89
[files_modified_or_created]
new = ["src/mcp_tool_specs.py", "src/openai_schemas.py", "src/provider_state.py", "scripts/audit_dataclass_coverage.py", "scripts/audit_dataclass_coverage.baseline.json"]
modified = ["src/type_aliases.py", "src/mcp_client.py", "src/openai_compatible.py", "src/ai_client.py", "src/log_registry.py", "src/session_logger.py", "src/log_pruner.py", "src/gui_2.py", "src/api_hooks.py", "conductor/code_styleguides/type_aliases.md"]
[input_artifact]
report = "docs/reports/ANY_TYPE_AUDIT_20260621.md"
findings_count = 300
candidates_count = 5
candidate_sites = 89
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from scripts.video_analysis.error_types import ErrorInfo, make_error
@dataclass
class _Ok:
value: Any
def is_ok(self) -> bool:
return True
def is_err(self) -> bool:
return False
@dataclass
class _Err:
err: ErrorInfo
def is_ok(self) -> bool:
return False
def is_err(self) -> bool:
return True
def ok(value: Any) -> _Ok:
return _Ok(value)
def err(error: ErrorInfo) -> _Err:
return _Err(error)
def list_frame_files(frames_dir: Path) -> list[Path]:
return sorted(p for p in frames_dir.glob("frame_*.jpg"))
def _ocr_single_image(image_path: Path, backend: str) -> tuple[str, float]:
if backend == "winsdk":
return _ocr_winsdk(image_path)
if backend == "tesseract":
return _ocr_tesseract(image_path)
raise ValueError(f"Unknown OCR backend: {backend}")
def _ocr_winsdk(image_path: Path) -> tuple[str, float]:
from winsdk.windows.storage import StorageFile
from winsdk.windows.graphics.imaging import BitmapDecoder
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.globalization import Language
async def _run() -> str:
file = await StorageFile.get_file_from_path_async(str(image_path.resolve()))
stream = await file.open_read_async()
decoder = await BitmapDecoder.create_async(stream)
bitmap = await decoder.get_software_bitmap_async()
engine = OcrEngine.try_create_from_language(Language("en-US"))
if not engine:
return ""
result = await engine.recognize_async(bitmap)
return "\n".join(line.text for line in result.lines)
text = asyncio.run(_run())
return text, 0.9 if text else 0.0
def _ocr_tesseract(image_path: Path) -> tuple[str, float]:
import pytesseract
from PIL import Image
img = Image.open(image_path)
text = pytesseract.image_to_string(img)
return text, 0.85 if text.strip() else 0.0
def format_ocr_markdown(frames: list[tuple[str, str, str]]) -> str:
lines = ["# OCR Results", ""]
for filename, text, _timestamp in frames:
lines.append(f"## {filename}")
lines.append("")
lines.append("```")
lines.append(text or "(no text extracted)")
lines.append("```")
lines.append("")
return "\n".join(lines)
def ocr_frames(frames_dir: Path, output: Path, backend: str = "winsdk") -> _Ok | _Err:
if not frames_dir.exists():
return err(make_error("FramesDirNotFound", "ocr_frames", str(frames_dir)))
frames = list_frame_files(frames_dir)
if not frames:
return err(make_error("NoFramesFound", "ocr_frames", str(frames_dir)))
now = datetime.now(timezone.utc).isoformat()
results: list[tuple[str, str, str]] = []
for frame_path in frames:
try:
text, confidence = _ocr_single_image(frame_path, backend)
except Exception as e:
return err(make_error("OcrError", "ocr_frames", f"{frame_path}: {e}"))
results.append((frame_path.name, text, now))
output.write_text(format_ocr_markdown(results), encoding="utf-8")
return ok({"frames_ocrd": len(results), "output": str(output), "backend": backend})
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from scripts.video_analysis.ocr_frames import (
format_ocr_markdown,
list_frame_files,
ocr_frames,
)
def test_list_frame_files_returns_sorted(tmp_path: Path) -> None:
(tmp_path / "frame_00001.jpg").write_bytes(b"x")
(tmp_path / "frame_00002.jpg").write_bytes(b"x")
(tmp_path / "frame_00010.jpg").write_bytes(b"x")
(tmp_path / "ignored.txt").write_bytes(b"x")
files = list_frame_files(tmp_path)
assert len(files) == 3
assert files[0].name == "frame_00001.jpg"
assert files[2].name == "frame_00010.jpg"
def test_format_ocr_markdown_empty() -> None:
out = format_ocr_markdown([])
assert "# OCR Results" in out
def test_format_ocr_markdown_with_frames() -> None:
frames = [("frame_00001.jpg", "Hello world", "2026-06-21T00:00:00Z")]
out = format_ocr_markdown(frames)
assert "frame_00001.jpg" in out
assert "Hello world" in out
def test_ocr_frames_calls_backend(tmp_path: Path) -> None:
(tmp_path / "frame_00001.jpg").write_bytes(b"fake-jpg-bytes")
with patch("scripts.video_analysis.ocr_frames._ocr_single_image") as mock_ocr:
mock_ocr.return_value = ("extracted text", 0.95)
result = ocr_frames(tmp_path, tmp_path / "ocr.md", backend="tesseract")
assert result.is_ok()
assert (tmp_path / "ocr.md").exists()