# Data Model: Knowledge Source Deletion Guardrails No new persisted entities or backend schema changes. This feature only adds client-side derived values computed from existing `orchestrationApi` types (`src/services/orchestrationApi.ts`). ## Existing entities reused (unchanged) - **`ApiDocument`** (`orchestrationApi.ts:60`-ish): `{ id, filename, file_type, file_size, status, created_at, ... }` — a document knowledge source. - **`DatabaseClient`**: `{ id, name, db_type, status, ... }` — a database knowledge source. - **`DataBindItem`** (`orchestrationApi.ts:150`): `{ id, name, group_type: "document" | "database", type }` — a reference from an `Analysis` to a bound knowledge source. - **`Analysis`** (`orchestrationApi.ts:157`): `{ id, analysis_title, objective, business_questions, status: "active" | "inactive" | string, data_bind: DataBindItem[], data_bind_version, ... }`. `status` is **not** modified by this feature (see Clarifications in spec.md). ## New client-side derived values (not persisted, not sent to the backend) ### `DeletionImpactSummary` Computed at delete-intent time in `KnowledgeManagement.tsx`, before the confirmation dialog opens. | Field | Type | Description | |---|---|---| | `sourceIds` | `string[]` | The knowledge source id(s) about to be deleted (one for single delete, many for "clear all"). | | `affectedAnalyses` | `{ id: string; analysis_title: string }[]` | Analyses whose `data_bind` contains at least one of `sourceIds`, deduplicated by analysis id. Derived by fetching `listAnalyses()` and filtering `analysis.data_bind.some(item => sourceIds.includes(item.id))`. | **Lifecycle**: Computed fresh each time a delete action is initiated; discarded when the dialog closes (confirm or cancel). Never persisted. ### Analysis usability flag (`isUnusable`) **Reused, not newly computed**: `AnalysisShell.tsx` already maintains `staleSources: DataBindItem[]` (state at `AnalysisShell.tsx:80`, computed in the `useEffect` at `AnalysisShell.tsx:117-138`) by cross-referencing `activeAnalysis.data_bind` against live `getDocuments`/`getDatabaseClients` results — this is exactly the "unusable" derivation this feature needs, built for feature 002's stale-sources banner. This feature derives from it rather than re-implementing it: ``` isActiveAnalysisUnusable: boolean = staleSources.length > 0 ``` - Any single missing bound source is sufficient to mark the whole analysis unusable (per spec Assumptions / Acceptance Scenario US3.6) — already true of `staleSources`'s existing filter logic. - This is a pure, stateless derivation — not stored on the `Analysis` object returned by the API, and never written back via `updateAnalysis`/`UpdateAnalysisPayload`. - Recomputed on-demand (page load / analysis selection), not via a live subscription (per research.md Unknown 3 and the plan's Constraints) — matching `staleSources`'s existing recompute trigger (the `activeAnalysis`-keyed effect). **State transitions**: `isActiveAnalysisUnusable` has no explicit transition function in this feature — it is a pure function of current data. It can only become `true` when a bound source is deleted (User Story 1 → 3). Per clarification, this feature has **no** transition back to `false`: once `staleSources.length > 0`, the update-bound-data action (including the pre-existing "Update binding" control that would normally clear `staleSources`) is itself disabled by this feature (FR-011/FR-014), so there is no in-app path to restore usability. ## Validation rule: analysis name uniqueness Enforced client-side in `NewAnalysisDialog.tsx` before calling `createAnalysis()`: ``` isDuplicateName(candidate: string, existing: Analysis[]): boolean = existing.some(a => a.analysis_title.trim().toLowerCase() === candidate.trim().toLowerCase()) ``` - Requires the dialog to have access to the current analysis list (already loaded by the parent `AnalysisShell.tsx` as `analyses` state) — passed down as a new prop, or fetched via `listAnalyses()` if the dialog is opened without it in scope. - This is a client-side guard only; per FR-007/FR-008 no backend contract change is assumed to exist, so this check does not guarantee uniqueness under concurrent creation from two sessions (acceptable per spec — no server-side uniqueness constraint was requested).