// SQLite-backed dev persistence (M5.9). The model is stored as a JSON snapshot // per project + a changelog of applied ops. This is the smallest shape that // gives us "refresh persists state" without committing to the full // event-sourced architecture in docs/sync.md (that lands later). // // Switch `provider` to "postgresql" + set DATABASE_URL=postgres://… to move // to a real DB later — schema is portable. generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = "file:./dev.db" } model Project { id String @id name String scope String tagline String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt snapshots ModelSnapshot[] 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 { id String @id @default(cuid()) projectId String version Int json String // serialized SysMLModel createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([projectId, version]) @@index([projectId, version]) } model ChangelogEntry { id String @id @default(cuid()) projectId String version Int // resulting model version after these ops landed ops String // JSON-encoded ModelOp[] reason String? // optional human/Socrates-supplied rationale createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@index([projectId, version]) } model SocratesThread { id String @id @default(cuid()) projectId String /// Model element this thread is anchored to, if any (block id, req id, …). anchorElementId String? status String @default("open") // open | archived | resolved title String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) messages SocratesMessage[] @@index([projectId]) } model SocratesMessage { id String @id @default(cuid()) threadId String role String // "user" | "assistant" /// JSON: { text, options?: [{n,label,sub?}] } content String /// Provider + model that produced this turn (assistant turns only). provider String? model String? inputTokens Int? outputTokens Int? createdAt DateTime @default(now()) thread SocratesThread @relation(fields: [threadId], references: [id], onDelete: Cascade) @@index([threadId, createdAt]) } /// Background-detected finding (assumption / risk / inconsistency). /// Findings are scoped to the model version that produced them; on every /// detection run we delete the project's previous open findings and write /// the new set so we don't accumulate stale ones across model edits. model Finding { id String @id @default(cuid()) projectId String /// "assumption" | "risk" | "inconsistency" kind String text String /// JSON-encoded string[] of element ids this finding references. linkedElementIds String confidence Float /// Risks only: "low" | "medium" | "high" severity String? /// Inconsistencies only: optional structural-rule code (S1, M2, T1, …) validationCode String? /// 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: /// `:`; for cross-validation: `:`. /// Null on legacy rows. suggestionKey String? /// Model version this finding was detected against. modelVersion Int /// Provider + model that emitted the finding. provider String? llmModel String? createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) research ResearchFinding[] @@index([projectId, status]) @@index([projectId, kind]) @@index([projectId, suggestionKey]) } /// Web research results from validating a finding via Tavily / similar. /// Deferred to a follow-up commit; schema is here so we don't have to /// migrate again later. model ResearchFinding { id String @id @default(cuid()) findingId String query String url String title String? snippet String? /// "supports" | "contradicts" | "neutral" stance String @default("neutral") createdAt DateTime @default(now()) finding Finding @relation(fields: [findingId], references: [id], onDelete: Cascade) @@index([findingId]) }