Files
zeekay 82120531a1 fix(debrand): read the TypeScript import graph before dropping a product
drop_closure exists to refuse a drop that is really a refactor: if code we
keep still imports a product, deleting it is a ModuleNotFoundError waiting
for the next boot. It only ever read Python, so half the dependency graph was
invisible to it and five products whose TypeScript the core still imports were
dropped anyway — 21 files left importing 25 specifiers that resolve to
nothing, including lib/api.ts, which 760 files import, and seven lazy scene
imports in productScenes.tsx.

Django booted past it and the bundle did not, which is why the Python-only
rule looked like it was working.

The closure now reads both module systems through one table of
(extensions, pattern) pairs, so a third one is a row rather than a branch. The
TypeScript pattern is anchored on the import keyword rather than on the path,
because jest's testPathIgnorePatterns and oxlint's ignore list quote a product
path precisely because it is NOT built; matching those would hold desktop, the
4,797-file product the drop is mostly for.

integrations, wizard, platform_features, visual_review and ai_gateway come
back with their Django apps and workspace entries, since the closure now
spares them. desktop, field_notes, ingestion and support stay dropped.
2026-08-01 22:17:05 -07:00

85 lines
2.0 KiB
Python

"""
Contract types for wizard.
Frozen dataclasses that define what this product exposes.
No Django imports. Used by facade as inputs/outputs.
"""
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
from .enums import RunPhase, TaskStatus
STALE_AFTER = timedelta(minutes=10)
@dataclass(frozen=True)
class WizardTaskDTO:
id: str
title: str
status: TaskStatus
@dataclass(frozen=True)
class WizardSessionUserDTO:
id: int
first_name: str
email: str
@dataclass(frozen=True)
class WizardSessionDTO:
session_id: str
team_id: int
workflow_id: str
skill_id: str
started_at: datetime
run_phase: RunPhase
tasks: tuple[WizardTaskDTO, ...]
event_plan: dict[str, Any] | None
error: dict[str, Any] | None
pending_input: dict[str, Any] | None
handoff_text: str | None
created_by: WizardSessionUserDTO | None
created_at: datetime
updated_at: datetime
is_stale: bool
class WizardSessionOwnershipError(Exception):
"""Raised when an upsert would overwrite a session owned by a different user."""
@dataclass(frozen=True)
class UpsertWizardSessionRequest:
"""What the wizard CLI POSTs. team_id is derived from the URL, not the body."""
session_id: str
workflow_id: str
skill_id: str
started_at: datetime
run_phase: RunPhase
tasks: tuple[WizardTaskDTO, ...]
event_plan: dict[str, Any] | None = None
error: dict[str, Any] | None = None
pending_input: dict[str, Any] | None = None
handoff_text: str | None = None
@dataclass(frozen=True)
class UpsertWizardSessionInput:
team_id: int
session_id: str
workflow_id: str
skill_id: str
started_at: datetime
run_phase: RunPhase
tasks: tuple[WizardTaskDTO, ...]
event_plan: dict[str, Any] | None
error: dict[str, Any] | None
pending_input: dict[str, Any] | None
handoff_text: str | None = None
# Set on create only, never overwritten on later pushes for the same run.
created_by_id: int | None = None