harryagasi
[NOTICKET] feat(knowledge,analysis): add deletion confirmation, duplicate-name guard, and unusable-analysis lockout
3baad7e
|
Raw
History Blame Contribute Delete
4.32 kB
# 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).