Pivot to text-first column-stack workspace + merge-with-review across AI artifacts

Workspace
- Pivot from "set of open panes" to a Finder-style miller column stack:
  TopBar / LeftSidebar / [section → entity → entity ...] / pinned text editor.
  openPanesStore is now an ordered Column[] with pushFrom / closeFrom /
  setStack; only one top-level section is rooted at a time.
- New entity column panes: Term, Block, Association, Constraint,
  Requirement, Finding. Click-through navigation truncates deeper
  columns automatically.
- LeftSidebar surfaces a pending-count chip per section (single-glance
  navigation cue) and spins its analyze ↻ via SVG Spinner whenever the
  LLM is working — including server-initiated runs caught by the runs
  poll, not just user-triggered ones.

Analyze pipeline + persistence
- Unified `concepts` pass (taxonomy + glossary in one LLM call) replaces
  the two-pass setup. Server still accepts ?section=taxonomy|glossary
  and normalizes them for back-compat.
- model / requirements / detection (assumptions, risks, inconsistencies)
  + cross-layer validation rules (X1–X4: stale term link, unlinked
  formalism, undefined linked term, prose-only term).
- Persistence: NarrativeDocument, ModelSnapshot, ChangelogEntry,
  TaxonomyTerm, RequirementEntry, Finding, AnalysisRun. Re-runs MERGE
  instead of replace: gentle update on existing items, suggested on new,
  deprecated on missing — same idiom for every artifact kind. User pins
  preserve "kept" decisions across re-analyses.
- Migrations: pivot_text_first, add_requirement_linked_term,
  term_review_state, review_state_for_reqs_and_findings,
  add_term_definition_pinned.

Concept ↔ ontology integration
- linkedTermId on Block / Association / Constraint / Requirement.
  PromoteToolbar lets the user formalize a concept inline: + Block /
  + Association / + Constraint / + Requirement, all routed through
  applyOps so undo/redo and SSE work for free.
- decideElement op for in-canvas keep/discard on review-pending model
  elements.

User-authored definitions
- TermColumn definition is click-to-edit. Save (Cmd-Enter / blur),
  Cancel (Esc), Reset to AI suggestion when pinned.
- definitionPinned flag on TaxonomyTerm: future Analyze runs leave the
  user's text alone. setTermDefinition repo function + POST
  /api/projects/[id]/terms/[termId]/definition endpoint.
- mergeTaxonomySuggestion + applyGlossaryDefinitions both pin-aware.

UX/UI
- StatusChip: single component for all state idioms (suggested,
  deprecated, accepted, dismissed, resolved, severity, validation code,
  confidence, warn). Replaces 5+ ad-hoc badge classes.
- PaneControls (PaneViewTabs + PaneFilterChip): separates view-mode
  toggles from filter chips so toggling Pending no longer flips you off
  the current view.
- PaneEmpty: unified empty-state with title + hint + action.
- PaneDrawer: collapsible groups for Pending / Discarded review; cards
  group as Kept (top) → Pending (bottom drawer) → Discarded (Findings
  only, hidden when empty). Restore action recovers dismissed/resolved
  findings.
- ConceptCard unifies Tree and A–Z views in Concepts; only Tree parents
  carry the chevron (no empty placeholder offset).
- Type + spacing tokens (--text-xs..xl, --space-1..6, --lh-tight/ui/
  prose, --radius-*) replace every ad-hoc value.
- Buttons standardized to body sans 500 (was a mishmash of mono / display).
- Card shells unified across Concepts / Requirements / Findings.

Cleanup
- Removed: LeftRail, FindingsPanel, IssuesPanel, SocratesDock,
  ProposalCard, SlashMenu, SlashExtension, slashSuggestion,
  CanvasHeader, TaxonomyPane, GlossaryPane, TermDetail (popover; now
  TermColumn).
- Section ids in openPanesStore: dropped taxonomy/glossary, added
  concepts. localStorage migration runs on hydrate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 00:12:06 +02:00
parent 4e725c0b2b
commit b55425cc68
88 changed files with 10855 additions and 1768 deletions

View File

@@ -0,0 +1,182 @@
-- CreateTable
CREATE TABLE "Project" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"tagline" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "NarrativeDocument" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"doc" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "NarrativeDocument_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "TaxonomyTerm" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"parentId" TEXT,
"label" TEXT NOT NULL,
"definition" TEXT,
"synonyms" TEXT NOT NULL DEFAULT '[]',
"linkedBlockId" TEXT,
"modelVersion" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TaxonomyTerm_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TaxonomyTerm_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "TaxonomyTerm" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "RequirementEntry" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"tag" TEXT NOT NULL,
"text" TEXT NOT NULL,
"tracedToIds" TEXT NOT NULL DEFAULT '[]',
"unsupported" BOOLEAN NOT NULL DEFAULT false,
"modelVersion" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RequirementEntry_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AnalysisRun" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"section" TEXT NOT NULL,
"status" TEXT NOT NULL,
"startedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"finishedAt" DATETIME,
"modelVersion" INTEGER NOT NULL,
"inputTokens" INTEGER,
"outputTokens" INTEGER,
"errorMessage" TEXT,
CONSTRAINT "AnalysisRun_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ModelSnapshot" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"json" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ModelSnapshot_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ChangelogEntry" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"ops" TEXT NOT NULL,
"reason" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ChangelogEntry_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "SocratesThread" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"anchorElementId" TEXT,
"status" TEXT NOT NULL DEFAULT 'open',
"title" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "SocratesThread_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "SocratesMessage" (
"id" TEXT NOT NULL PRIMARY KEY,
"threadId" TEXT NOT NULL,
"role" TEXT NOT NULL,
"content" TEXT NOT NULL,
"provider" TEXT,
"model" TEXT,
"inputTokens" INTEGER,
"outputTokens" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SocratesMessage_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "SocratesThread" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Finding" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"text" TEXT NOT NULL,
"linkedElementIds" TEXT NOT NULL,
"confidence" REAL NOT NULL,
"severity" TEXT,
"validationCode" TEXT,
"status" TEXT NOT NULL DEFAULT 'open',
"modelVersion" INTEGER NOT NULL,
"provider" TEXT,
"llmModel" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Finding_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ResearchFinding" (
"id" TEXT NOT NULL PRIMARY KEY,
"findingId" TEXT NOT NULL,
"query" TEXT NOT NULL,
"url" TEXT NOT NULL,
"title" TEXT,
"snippet" TEXT,
"stance" TEXT NOT NULL DEFAULT 'neutral',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ResearchFinding_findingId_fkey" FOREIGN KEY ("findingId") REFERENCES "Finding" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "NarrativeDocument_projectId_key" ON "NarrativeDocument"("projectId");
-- CreateIndex
CREATE INDEX "TaxonomyTerm_projectId_idx" ON "TaxonomyTerm"("projectId");
-- CreateIndex
CREATE INDEX "TaxonomyTerm_projectId_label_idx" ON "TaxonomyTerm"("projectId", "label");
-- CreateIndex
CREATE INDEX "RequirementEntry_projectId_idx" ON "RequirementEntry"("projectId");
-- CreateIndex
CREATE INDEX "AnalysisRun_projectId_section_idx" ON "AnalysisRun"("projectId", "section");
-- CreateIndex
CREATE INDEX "AnalysisRun_projectId_startedAt_idx" ON "AnalysisRun"("projectId", "startedAt");
-- CreateIndex
CREATE INDEX "ModelSnapshot_projectId_version_idx" ON "ModelSnapshot"("projectId", "version");
-- CreateIndex
CREATE UNIQUE INDEX "ModelSnapshot_projectId_version_key" ON "ModelSnapshot"("projectId", "version");
-- CreateIndex
CREATE INDEX "ChangelogEntry_projectId_version_idx" ON "ChangelogEntry"("projectId", "version");
-- CreateIndex
CREATE INDEX "SocratesThread_projectId_idx" ON "SocratesThread"("projectId");
-- CreateIndex
CREATE INDEX "SocratesMessage_threadId_createdAt_idx" ON "SocratesMessage"("threadId", "createdAt");
-- CreateIndex
CREATE INDEX "Finding_projectId_status_idx" ON "Finding"("projectId", "status");
-- CreateIndex
CREATE INDEX "Finding_projectId_kind_idx" ON "Finding"("projectId", "kind");
-- CreateIndex
CREATE INDEX "ResearchFinding_findingId_idx" ON "ResearchFinding"("findingId");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "RequirementEntry" ADD COLUMN "linkedTermId" TEXT;

View File

@@ -0,0 +1,26 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_TaxonomyTerm" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"parentId" TEXT,
"label" TEXT NOT NULL,
"definition" TEXT,
"synonyms" TEXT NOT NULL DEFAULT '[]',
"linkedBlockId" TEXT,
"modelVersion" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'accepted',
"pinned" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TaxonomyTerm_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TaxonomyTerm_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "TaxonomyTerm" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_TaxonomyTerm" ("createdAt", "definition", "id", "label", "linkedBlockId", "modelVersion", "parentId", "projectId", "synonyms") SELECT "createdAt", "definition", "id", "label", "linkedBlockId", "modelVersion", "parentId", "projectId", "synonyms" FROM "TaxonomyTerm";
DROP TABLE "TaxonomyTerm";
ALTER TABLE "new_TaxonomyTerm" RENAME TO "TaxonomyTerm";
CREATE INDEX "TaxonomyTerm_projectId_idx" ON "TaxonomyTerm"("projectId");
CREATE INDEX "TaxonomyTerm_projectId_label_idx" ON "TaxonomyTerm"("projectId", "label");
CREATE INDEX "TaxonomyTerm_projectId_status_idx" ON "TaxonomyTerm"("projectId", "status");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,48 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Finding" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"text" TEXT NOT NULL,
"linkedElementIds" TEXT NOT NULL,
"confidence" REAL NOT NULL,
"severity" TEXT,
"validationCode" TEXT,
"status" TEXT NOT NULL DEFAULT 'suggested',
"pinned" BOOLEAN NOT NULL DEFAULT false,
"suggestionKey" TEXT,
"modelVersion" INTEGER NOT NULL,
"provider" TEXT,
"llmModel" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Finding_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Finding" ("confidence", "createdAt", "id", "kind", "linkedElementIds", "llmModel", "modelVersion", "projectId", "provider", "severity", "status", "text", "validationCode") SELECT "confidence", "createdAt", "id", "kind", "linkedElementIds", "llmModel", "modelVersion", "projectId", "provider", "severity", "status", "text", "validationCode" FROM "Finding";
DROP TABLE "Finding";
ALTER TABLE "new_Finding" RENAME TO "Finding";
CREATE INDEX "Finding_projectId_status_idx" ON "Finding"("projectId", "status");
CREATE INDEX "Finding_projectId_kind_idx" ON "Finding"("projectId", "kind");
CREATE INDEX "Finding_projectId_suggestionKey_idx" ON "Finding"("projectId", "suggestionKey");
CREATE TABLE "new_RequirementEntry" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"tag" TEXT NOT NULL,
"text" TEXT NOT NULL,
"tracedToIds" TEXT NOT NULL DEFAULT '[]',
"unsupported" BOOLEAN NOT NULL DEFAULT false,
"modelVersion" INTEGER NOT NULL,
"linkedTermId" TEXT,
"status" TEXT NOT NULL DEFAULT 'accepted',
"pinned" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RequirementEntry_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_RequirementEntry" ("createdAt", "id", "linkedTermId", "modelVersion", "projectId", "tag", "text", "tracedToIds", "unsupported") SELECT "createdAt", "id", "linkedTermId", "modelVersion", "projectId", "tag", "text", "tracedToIds", "unsupported" FROM "RequirementEntry";
DROP TABLE "RequirementEntry";
ALTER TABLE "new_RequirementEntry" RENAME TO "RequirementEntry";
CREATE INDEX "RequirementEntry_projectId_idx" ON "RequirementEntry"("projectId");
CREATE INDEX "RequirementEntry_projectId_status_idx" ON "RequirementEntry"("projectId", "status");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,27 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_TaxonomyTerm" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"parentId" TEXT,
"label" TEXT NOT NULL,
"definition" TEXT,
"synonyms" TEXT NOT NULL DEFAULT '[]',
"linkedBlockId" TEXT,
"modelVersion" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'accepted',
"pinned" BOOLEAN NOT NULL DEFAULT false,
"definitionPinned" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TaxonomyTerm_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TaxonomyTerm_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "TaxonomyTerm" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_TaxonomyTerm" ("createdAt", "definition", "id", "label", "linkedBlockId", "modelVersion", "parentId", "pinned", "projectId", "status", "synonyms") SELECT "createdAt", "definition", "id", "label", "linkedBlockId", "modelVersion", "parentId", "pinned", "projectId", "status", "synonyms" FROM "TaxonomyTerm";
DROP TABLE "TaxonomyTerm";
ALTER TABLE "new_TaxonomyTerm" RENAME TO "TaxonomyTerm";
CREATE INDEX "TaxonomyTerm_projectId_idx" ON "TaxonomyTerm"("projectId");
CREATE INDEX "TaxonomyTerm_projectId_label_idx" ON "TaxonomyTerm"("projectId", "label");
CREATE INDEX "TaxonomyTerm_projectId_status_idx" ON "TaxonomyTerm"("projectId", "status");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@@ -27,6 +27,112 @@ model Project {
changes ChangelogEntry[]
threads SocratesThread[]
findings Finding[]
document NarrativeDocument?
taxonomy TaxonomyTerm[]
reqs RequirementEntry[]
runs AnalysisRun[]
}
/// Canonical prose. After the pivot, the narrative is the source of truth;
/// taxonomy / glossary / model / findings are projections produced by the
/// Analyze pipeline.
model NarrativeDocument {
id String @id @default(cuid())
projectId String @unique
doc String // ProseMirror JSON
version Int @default(1)
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
}
/// Taxonomy term (concept / entity) extracted by the analyze-taxonomy pass.
/// Optionally linked to a SysML block, which surfaces the "linked" indicator
/// in the sidebar and powers the click-to-jump action.
model TaxonomyTerm {
id String @id @default(cuid())
projectId String
parentId String?
label String
definition String?
/// JSON-encoded string[] of synonym surface forms.
synonyms String @default("[]")
/// SysML block id this term is currently linked to (if any).
linkedBlockId String?
/// Model version when this term was last refreshed.
modelVersion Int
/// Review state. `accepted` (default — confirmed term, surfaces normally),
/// `suggested` (analyzer added in last run, awaits user keep/discard),
/// `deprecated` (analyzer didn't see in last run, awaits user keep/discard).
status String @default("accepted")
/// User explicitly chose to keep this term despite the analyzer not seeing
/// it. Future merges leave it alone instead of re-marking it deprecated.
pinned Boolean @default(false)
/// User authored or edited the definition by hand. When true, future
/// Analyze runs will NOT replace the definition — the LLM's emit is
/// dropped on the floor. Reset by writing an empty string back through
/// setTermDefinition (which clears the pin).
definitionPinned Boolean @default(false)
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
parent TaxonomyTerm? @relation("TaxonomyTree", fields: [parentId], references: [id], onDelete: SetNull)
children TaxonomyTerm[] @relation("TaxonomyTree")
@@index([projectId])
@@index([projectId, label])
@@index([projectId, status])
}
/// Requirement extracted from prose. Traceability is a JSON list of block ids
/// that the analyzer believes fulfill the requirement.
model RequirementEntry {
id String @id @default(cuid())
projectId String
tag String
text String
/// JSON-encoded string[] of block ids.
tracedToIds String @default("[]")
unsupported Boolean @default(false)
modelVersion Int
/// Term this requirement is conceptually about (links REQ → concept).
/// Null when the requirement isn't anchored to a specific term.
linkedTermId String?
/// Review state. Same triple as TaxonomyTerm — `accepted` (default,
/// surfaces normally), `suggested` (analyzer added in last run), or
/// `deprecated` (analyzer didn't see in last run). Pending statuses
/// await user keep/discard.
status String @default("accepted")
/// User explicitly preserved → don't re-deprecate next merge.
pinned Boolean @default(false)
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId])
@@index([projectId, status])
}
/// One row per Analyze invocation per section. Powers last-run timestamps
/// and progress indicators in the sidebar.
model AnalysisRun {
id String @id @default(cuid())
projectId String
/// "taxonomy" | "glossary" | "model" | "requirements"
/// | "assumptions" | "risks" | "inconsistencies" | "all"
section String
/// "running" | "succeeded" | "failed"
status String
startedAt DateTime @default(now())
finishedAt DateTime?
modelVersion Int
inputTokens Int?
outputTokens Int?
errorMessage String?
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId, section])
@@index([projectId, startedAt])
}
model ModelSnapshot {
@@ -106,8 +212,23 @@ model Finding {
severity String?
/// Inconsistencies only: optional structural-rule code (S1, M2, T1, …)
validationCode String?
/// Lifecycle: "open" | "dismissed" | "resolved"
status String @default("open")
/// Lifecycle (review-aware):
/// "suggested" — new from latest run, awaits user keep/discard
/// "accepted" — user has explicitly kept (or carried forward); the
/// normal "open" surfaces in the pane
/// "deprecated"— was accepted, analyzer didn't see in latest run
/// "dismissed" — user said never (don't resurface)
/// "resolved" — user marked done
/// Pre-existing rows from before merge-with-review used "open"; we
/// continue to accept that value as a synonym for "accepted" for one
/// release.
status String @default("suggested")
/// User explicitly preserved → don't re-deprecate next merge.
pinned Boolean @default(false)
/// Stable identity key for cross-run matching. For LLM findings:
/// `<kind>:<normalized-text>`; for cross-validation: `<validationCode>:<elementId>`.
/// Null on legacy rows.
suggestionKey String?
/// Model version this finding was detected against.
modelVersion Int
/// Provider + model that emitted the finding.
@@ -120,6 +241,7 @@ model Finding {
@@index([projectId, status])
@@index([projectId, kind])
@@index([projectId, suggestionKey])
}
/// Web research results from validating a finding via Tavily / similar.