From 241993adb30019e885102739de6eee06b6ee8e8f Mon Sep 17 00:00:00 2001 From: Tim Hosgood Date: Tue, 30 Jun 2026 19:49:29 +0100 Subject: [PATCH 1/4] WIP: document-types/instance.rs First suggestion of instances document type WIP: fix CI WIP: Lots of frontend faff WIP: Whatever is needed to pass tests null UUID WIP: Remove fake tabular instance editor from frontend WIP: Remove f32 so we can derive Eq WIP: restore f32, remove Eq, remove instance_judgment WIP: fix frontend type narrowing confused by instance small change to comment wording ENH: Backend test FIX: DocumentType FIX: .clone() FIX: this??? FIX: fix? FIX: FIX?! FIX: Half of Evan's comments FIX: Add rust lints to document-types FIX: Some comments FIX: Yet more comments FIX: Remove linting (for now) --- packages/backend/tests/user_state_tests.rs | 115 +++++++++++++----- packages/document-methods/src/index.ts | 2 + packages/document-methods/src/instance.ts | 17 +++ packages/document-types/src/v0/api.rs | 3 + packages/document-types/src/v2/cell.rs | 9 +- packages/document-types/src/v2/document.rs | 58 ++++++++- packages/document-types/src/v2/instance.rs | 43 +++++++ packages/document-types/src/v2/mod.rs | 5 + packages/frontend/src/model/model_library.ts | 2 +- .../src/page/document_breadcrumbs.tsx | 2 + packages/frontend/src/page/document_menu.tsx | 13 +- packages/frontend/src/page/document_page.tsx | 2 + .../ui-components/src/document_type_icon.tsx | 6 +- 13 files changed, 239 insertions(+), 38 deletions(-) create mode 100644 packages/document-methods/src/instance.ts create mode 100644 packages/document-types/src/v2/instance.rs diff --git a/packages/backend/tests/user_state_tests.rs b/packages/backend/tests/user_state_tests.rs index dba49e949..5d2260777 100644 --- a/packages/backend/tests/user_state_tests.rs +++ b/packages/backend/tests/user_state_tests.rs @@ -50,7 +50,7 @@ mod integration_tests { } /// Creates document content for a child document (diagram) that links to a parent ref. - fn create_child_document_content(name: &str, parent_ref_id: Uuid) -> serde_json::Value { + fn create_child_diagram_document_content(name: &str, parent_ref_id: Uuid) -> serde_json::Value { json!({ "version": "1", "type": "diagram", @@ -68,6 +68,25 @@ mod integration_tests { }) } + /// Creates document content for a child document (instance) that links to a parent ref. + fn create_child_instance_document_content( + name: &str, + parent_ref_id: Uuid, + ) -> serde_json::Value { + json!({ + "version": "2", + "type": "instance", + "name": name, + "instanceOf": { + "_id": parent_ref_id.to_string(), + "_version": null, + "_server": "test", + "type": "instance-of" + }, + "tables": [] + }) + } + // ----------------------------------------------------------------------- // Document creation // ----------------------------------------------------------------------- @@ -411,14 +430,25 @@ mod integration_tests { assert_eq!(doc.theory.as_deref(), Some("causal-loop")); // Diagram (no theory) - let diagram_content = create_child_document_content("Test Diagram", ref_id); - let diagram_id = - document::new_ref(ctx, diagram_content).await.expect("Failed to create diagram"); + let diagram_content = create_child_diagram_document_content("Test Diagram", ref_id); + let diagram_id = document::new_ref(ctx.clone(), diagram_content) + .await + .expect("Failed to create diagram"); let s = read_user_state_from_samod(&state, &user_id).await.unwrap(); assert_eq!(s.documents[&diagram_id.to_string()].type_name, DocumentType::Diagram); assert_eq!(s.documents[&diagram_id.to_string()].theory, None); + // Instance (no theory) + let instance_content = create_child_instance_document_content("Test Instance", ref_id); + let instance_id = document::new_ref(ctx, instance_content) + .await + .expect("Failed to create instance"); + + let s = read_user_state_from_samod(&state, &user_id).await.unwrap(); + assert_eq!(s.documents[&instance_id.to_string()].type_name, DocumentType::Instance); + assert_eq!(s.documents[&instance_id.to_string()].theory, None); + // Update theory and propagate user state let updated = create_model_document_content("Theory Test", "petri-net"); let fake_heads: Vec> = vec![vec![0u8; 32]]; @@ -689,27 +719,42 @@ mod integration_tests { let parent_id = document::new_ref(ctx.clone(), create_test_document_content("Parent Doc")) .await .unwrap(); - let child_id = - document::new_ref(ctx, create_child_document_content("Child Doc", parent_id)) - .await - .unwrap(); + let child_diagram_id = document::new_ref( + ctx.clone(), + create_child_diagram_document_content("Child Diagram", parent_id), + ) + .await + .unwrap(); + let child_instance_id = document::new_ref( + ctx, + create_child_instance_document_content("Child Instance", parent_id), + ) + .await + .unwrap(); get_or_create_user_state_doc(&state, &user_id).await.unwrap(); let us = read_user_state_from_samod(&state, &user_id).await.unwrap(); - assert_eq!(us.documents.len(), 2); + assert_eq!(us.documents.len(), 3); let parent = &us.documents[&parent_id.to_string()]; - let child = &us.documents[&child_id.to_string()]; + let child_diagram = &us.documents[&child_diagram_id.to_string()]; + let child_instance = &us.documents[&child_instance_id.to_string()]; assert!(parent.depends_on.is_empty()); - assert_eq!(child.depends_on.len(), 1); - assert_eq!(child.depends_on[0].ref_id, parent_id); - assert_eq!(child.depends_on[0].relation_type, "diagram-in"); - - assert_eq!(parent.used_by.len(), 1); - assert_eq!(parent.used_by[0].ref_id, child_id); - assert!(child.used_by.is_empty()); + assert_eq!(child_diagram.depends_on.len(), 1); + assert_eq!(child_diagram.depends_on[0].ref_id, parent_id); + assert_eq!(child_diagram.depends_on[0].relation_type, "diagram-in"); + assert_eq!(child_instance.depends_on.len(), 1); + assert_eq!(child_instance.depends_on[0].ref_id, parent_id); + assert_eq!(child_instance.depends_on[0].relation_type, "instance-of"); + + assert_eq!(parent.used_by.len(), 2); + let used_by_ref_ids: Vec<_> = parent.used_by.iter().map(|doc| doc.ref_id).collect(); + assert!(used_by_ref_ids.contains(&child_diagram_id)); + assert!(used_by_ref_ids.contains(&child_instance_id)); + assert!(child_diagram.used_by.is_empty()); + assert!(child_instance.used_by.is_empty()); Ok(()) } @@ -736,23 +781,37 @@ mod integration_tests { let parent_id = document::new_ref(ctx.clone(), create_test_document_content("Parent Doc")) .await .unwrap(); - let child_id = - document::new_ref(ctx, create_child_document_content("Child Doc", parent_id)) - .await - .unwrap(); + let child_diagram_id = document::new_ref( + ctx.clone(), + create_child_diagram_document_content("Child Diagram", parent_id), + ) + .await + .unwrap(); + let child_instance_id = document::new_ref( + ctx, + create_child_instance_document_content("Child Instance", parent_id), + ) + .await + .unwrap(); let us = read_user_state_from_samod(&state, &user_id).await.unwrap(); - assert_eq!(us.documents.len(), 2); + assert_eq!(us.documents.len(), 3); let parent = &us.documents[&parent_id.to_string()]; - let child = &us.documents[&child_id.to_string()]; + let child_diagram = &us.documents[&child_diagram_id.to_string()]; + let child_instance = &us.documents[&child_instance_id.to_string()]; assert!(parent.depends_on.is_empty()); - assert_eq!(child.depends_on.len(), 1); - assert_eq!(child.depends_on[0].ref_id, parent_id); - assert_eq!(parent.used_by.len(), 1); - assert_eq!(parent.used_by[0].ref_id, child_id); - assert!(child.used_by.is_empty()); + assert_eq!(child_diagram.depends_on.len(), 1); + assert_eq!(child_diagram.depends_on[0].ref_id, parent_id); + assert_eq!(child_instance.depends_on.len(), 1); + assert_eq!(child_instance.depends_on[0].ref_id, parent_id); + assert_eq!(parent.used_by.len(), 2); + let used_by_ref_ids: Vec<_> = parent.used_by.iter().map(|doc| doc.ref_id).collect(); + assert!(used_by_ref_ids.contains(&child_diagram_id)); + assert!(used_by_ref_ids.contains(&child_instance_id)); + assert!(child_diagram.used_by.is_empty()); + assert!(child_instance.used_by.is_empty()); Ok(()) } diff --git a/packages/document-methods/src/index.ts b/packages/document-methods/src/index.ts index 5cd7ee7fd..4a1e7e8f9 100644 --- a/packages/document-methods/src/index.ts +++ b/packages/document-methods/src/index.ts @@ -1,9 +1,11 @@ export type { DiagramDocument } from "./diagram"; +export type { InstanceDocument } from "./instance"; export type { LLMConversationDocument } from "./llm_conversation"; export type { ModelDocument } from "./model"; export type { FormalCell, RichTextCell } from "./notebook"; export * as Diagram from "./diagram"; export * as LLMConversation from "./llm_conversation"; +export * as Instance from "./instance"; export * as Model from "./model"; export * as Nb from "./notebook"; diff --git a/packages/document-methods/src/instance.ts b/packages/document-methods/src/instance.ts new file mode 100644 index 000000000..6053a766c --- /dev/null +++ b/packages/document-methods/src/instance.ts @@ -0,0 +1,17 @@ +import type { Document, StableRef } from "catcolab-document-types"; +import { currentVersion } from "catcolab-document-types"; + +/** A document defining a instance in a model. */ +export type InstanceDocument = Document & { type: "instance" }; + +/** Create an empty instance of a model. */ +export const newInstanceDocument = (modelRef: StableRef): InstanceDocument => ({ + name: "", + type: "instance", + instanceOf: { + ...modelRef, + type: "instance-of", + }, + tables: [], + version: currentVersion(), +}); diff --git a/packages/document-types/src/v0/api.rs b/packages/document-types/src/v0/api.rs index d4a2984eb..b7785347f 100644 --- a/packages/document-types/src/v0/api.rs +++ b/packages/document-types/src/v0/api.rs @@ -57,6 +57,9 @@ pub enum LinkType { #[serde(rename = "diagram-in")] DiagramIn, + #[serde(rename = "instance-of")] + InstanceOf, + #[serde(rename = "llmconversation-of")] ConversationOf, diff --git a/packages/document-types/src/v2/cell.rs b/packages/document-types/src/v2/cell.rs index 6a5096f4e..5cd799dd8 100644 --- a/packages/document-types/src/v2/cell.rs +++ b/packages/document-types/src/v2/cell.rs @@ -13,12 +13,19 @@ use crate::v1; #[serde(tag = "tag")] #[tsify(into_wasm_abi, from_wasm_abi)] pub enum NotebookCell { + /// A rich-text cell. #[serde(rename = "rich-text")] RichText { id: Uuid, content: RichTextContent }, #[serde(rename = "formal")] - Formal { id: Uuid, content: T }, + Formal { + /// The ID of the cell. + id: Uuid, + /// The formal content of the cell. + content: T, + }, } +/// Short-hand declaration for readability. #[declare] pub type Cell = NotebookCell; diff --git a/packages/document-types/src/v2/document.rs b/packages/document-types/src/v2/document.rs index 83364584a..960a447af 100644 --- a/packages/document-types/src/v2/document.rs +++ b/packages/document-types/src/v2/document.rs @@ -1,6 +1,7 @@ +use crate::current::instance::Table; use crate::v0::AnalysisType; use crate::v1; -pub use crate::v1::DocumentType; +use std::{collections::HashMap, str::FromStr}; use super::analysis::Analysis; use super::api::Link; @@ -9,6 +10,7 @@ use super::notebook::Notebook; use serde::{Deserialize, Serialize}; use tsify::Tsify; +use uuid::Uuid; /// This is the content of a model document. For legacy reasons, we reserve /// the name "ModelDocument" for `Document & { type: "model" }`. @@ -37,6 +39,16 @@ pub struct DiagramDocumentContent { pub version: String, } +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct InstanceDocumentContent { + pub name: String, + #[serde(rename = "instanceOf")] + pub instance_of: Link, + pub tables: HashMap, + pub version: String, +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct AnalysisDocumentContent { @@ -49,7 +61,7 @@ pub struct AnalysisDocumentContent { pub version: String, } -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)] +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] #[serde(tag = "type")] #[tsify(into_wasm_abi, from_wasm_abi)] pub enum Document { @@ -59,10 +71,52 @@ pub enum Document { Diagram(DiagramDocumentContent), #[serde(rename = "analysis")] Analysis(AnalysisDocumentContent), + #[serde(rename = "instance")] + Instance(InstanceDocumentContent), #[serde(rename = "llmconversation")] LLMConversation(LLMConversationDocumentContent), } +/// The type/kind of a document, without any associated content. +#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, Tsify)] +#[cfg_attr( + feature = "backend", + derive(autosurgeon::Reconcile, autosurgeon::Hydrate, ts_rs::TS) +)] +#[cfg_attr( + feature = "backend", + ts(export_to = "user_state.ts", rename_all = "lowercase") +)] +#[serde(rename_all = "lowercase")] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum DocumentType { + #[cfg_attr(feature = "backend", autosurgeon(rename = "model"))] + Model, + #[cfg_attr(feature = "backend", autosurgeon(rename = "diagram"))] + Diagram, + #[cfg_attr(feature = "backend", autosurgeon(rename = "instance"))] + Instance, + #[cfg_attr(feature = "backend", autosurgeon(rename = "analysis"))] + Analysis, + #[cfg_attr(feature = "backend", autosurgeon(rename = "llmconversation"))] + LLMConversation, +} + +impl FromStr for DocumentType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "model" => Ok(DocumentType::Model), + "diagram" => Ok(DocumentType::Diagram), + "instance" => Ok(DocumentType::Instance), + "analysis" => Ok(DocumentType::Analysis), + "llmconversation" => Ok(DocumentType::LLMConversation), + other => Err(format!("unknown document type: {other}")), + } + } +} + impl Document { pub fn migrate_from_v1(old: v1::Document) -> Self { match old { diff --git a/packages/document-types/src/v2/instance.rs b/packages/document-types/src/v2/instance.rs new file mode 100644 index 000000000..376cc47ec --- /dev/null +++ b/packages/document-types/src/v2/instance.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tsify::Tsify; +use uuid::Uuid; + +/// The value of a single "cell" (i.e. field) in a table row. If the column corresponds to an +/// attribute morphism then we provide the value of the type; if the column corresponds to a +/// mapping morphism then we provide the uuid of the row. +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +pub enum FieldValue { + /// Base type: the empty type. + Null, + /// Base type: boolean. + Bool(bool), + /// Base type: integer. + Int(i32), + /// Base type: float. + Float(f32), + /// Base type: string. + String(String), + /// Mapping type: the uuid of another row. + RowRef(Uuid), +} + +/// A single row of a table. +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +pub struct TableRow { + /// The row "number". + pub id: Uuid, + /// The content of the row, given as a map from column IDs to values. + pub fields: HashMap, +} + +/// A single table, corresponding to a single entity. +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +pub struct Table { + /// The uuid of the entity to which this table corresponds. + pub id: Uuid, + /// The rows of the table. + pub rows: HashMap, + /// The order of the rows of the table. + pub row_order: Vec, +} diff --git a/packages/document-types/src/v2/mod.rs b/packages/document-types/src/v2/mod.rs index 11ff4cf00..689bce177 100644 --- a/packages/document-types/src/v2/mod.rs +++ b/packages/document-types/src/v2/mod.rs @@ -2,9 +2,14 @@ use crate::v1; pub use v1::{analysis, api, diagram_judgment, model, model_judgment, path, theory}; +/// Cells in a notebook. pub mod cell; +/// Model documents, containing a notebook along with metadata. pub mod document; +/// Tabular instances of models. +pub mod instance; pub mod llm_conversation; +/// Notebooks for models and diagrams. pub mod notebook; pub mod rich_text; diff --git a/packages/frontend/src/model/model_library.ts b/packages/frontend/src/model/model_library.ts index 79e0337f7..0208cce5f 100644 --- a/packages/frontend/src/model/model_library.ts +++ b/packages/frontend/src/model/model_library.ts @@ -319,7 +319,7 @@ function isPatchToFormalContent(doc: Document, patch: Patch): boolean { // Ignore changes to top-level data like document name. return false; } - if (path[0] === "notebook" && path[1] === "cellContents" && path[2]) { + if (doc.type === "model" && path[0] === "notebook" && path[1] === "cellContents" && path[2]) { // Ignores changes to cells without formal content. const cell = doc.notebook.cellContents[path[2]]; if (cell?.tag !== "formal") { diff --git a/packages/frontend/src/page/document_breadcrumbs.tsx b/packages/frontend/src/page/document_breadcrumbs.tsx index f98db6591..d6c08f08b 100644 --- a/packages/frontend/src/page/document_breadcrumbs.tsx +++ b/packages/frontend/src/page/document_breadcrumbs.tsx @@ -39,6 +39,8 @@ export function getParentRefId(document: Document): string | null { return null; case "diagram": return document.diagramIn._id; + case "instance": + return document.instanceOf._id; case "analysis": return document.analysisOf._id; case "llmconversation": diff --git a/packages/frontend/src/page/document_menu.tsx b/packages/frontend/src/page/document_menu.tsx index d62dded74..7b828c796 100644 --- a/packages/frontend/src/page/document_menu.tsx +++ b/packages/frontend/src/page/document_menu.tsx @@ -60,10 +60,8 @@ export function DocumentMenu(props: { const onNewAnalysis = async () => { const docRefId = props.docRef.refId; const docType = props.liveDoc.doc.type; - invariant( - docType === "model" || docType === "diagram", - "Analysis can only be created on a model or diagram", - ); + invariant(docType !== "analysis", "Analysis cannot be created on other analysis"); + invariant(docType !== "instance", "Analysis cannot yet be created on an instance"); const newRef = await createAnalysis(api, docType, api.makeUnversionedRef(docRefId)); handleDocCreated("analysis", newRef); @@ -117,7 +115,12 @@ export function DocumentMenu(props: { - + onNewAnalysis()}> {`New analysis of this ${docType()}`} diff --git a/packages/frontend/src/page/document_page.tsx b/packages/frontend/src/page/document_page.tsx index bbf82338f..7aa7730ce 100644 --- a/packages/frontend/src/page/document_page.tsx +++ b/packages/frontend/src/page/document_page.tsx @@ -571,6 +571,8 @@ async function getLiveDocument( const { liveAnalysis, docRef } = await getLiveAnalysis(refId, api, models); return { liveDoc: liveAnalysis, docRef }; } + case "instance": + throw new Error("Instance documents are not supported by the frontend"); case "llmconversation": throw new Error("LLM conversation pages are not implemented"); default: diff --git a/packages/ui-components/src/document_type_icon.tsx b/packages/ui-components/src/document_type_icon.tsx index e8e7c3070..faf53e51d 100644 --- a/packages/ui-components/src/document_type_icon.tsx +++ b/packages/ui-components/src/document_type_icon.tsx @@ -2,11 +2,12 @@ import ChartSpline from "lucide-solid/icons/chart-spline"; import File from "lucide-solid/icons/file"; import FileX from "lucide-solid/icons/file-x"; import Network from "lucide-solid/icons/network"; +import Table from "lucide-solid/icons/table"; import { Match, Switch } from "solid-js"; import { ModelFileIcon } from "./model_file_icon"; -export type DocumentType = "model" | "diagram" | "analysis" | "llmconversation"; +export type DocumentType = "model" | "diagram" | "analysis" | "instance" | "llmconversation"; export function DocumentTypeIcon(props: { documentType: DocumentType; @@ -30,6 +31,9 @@ export function DocumentTypeIcon(props: { + + + ); } From a9cc8dbcc4512fd1b26269c94d793b0f92dc215c Mon Sep 17 00:00:00 2001 From: Tim Hosgood Date: Fri, 31 Jul 2026 13:19:37 +0100 Subject: [PATCH 2/4] FIX: type mismatch lint --- packages/backend/tests/user_state_tests.rs | 2 +- packages/document-methods/src/instance.ts | 2 +- packages/document-types/src/v2/document.rs | 2 +- packages/frontend/src/page/document_menu.tsx | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/backend/tests/user_state_tests.rs b/packages/backend/tests/user_state_tests.rs index 5d2260777..223c13292 100644 --- a/packages/backend/tests/user_state_tests.rs +++ b/packages/backend/tests/user_state_tests.rs @@ -83,7 +83,7 @@ mod integration_tests { "_server": "test", "type": "instance-of" }, - "tables": [] + "tables": {} }) } diff --git a/packages/document-methods/src/instance.ts b/packages/document-methods/src/instance.ts index 6053a766c..172f84406 100644 --- a/packages/document-methods/src/instance.ts +++ b/packages/document-methods/src/instance.ts @@ -12,6 +12,6 @@ export const newInstanceDocument = (modelRef: StableRef): InstanceDocument => ({ ...modelRef, type: "instance-of", }, - tables: [], + tables: {}, version: currentVersion(), }); diff --git a/packages/document-types/src/v2/document.rs b/packages/document-types/src/v2/document.rs index 960a447af..843e45970 100644 --- a/packages/document-types/src/v2/document.rs +++ b/packages/document-types/src/v2/document.rs @@ -40,7 +40,7 @@ pub struct DiagramDocumentContent { } #[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[tsify(into_wasm_abi, from_wasm_abi, hashmap_as_object)] pub struct InstanceDocumentContent { pub name: String, #[serde(rename = "instanceOf")] diff --git a/packages/frontend/src/page/document_menu.tsx b/packages/frontend/src/page/document_menu.tsx index 7b828c796..f2d84f805 100644 --- a/packages/frontend/src/page/document_menu.tsx +++ b/packages/frontend/src/page/document_menu.tsx @@ -62,6 +62,10 @@ export function DocumentMenu(props: { const docType = props.liveDoc.doc.type; invariant(docType !== "analysis", "Analysis cannot be created on other analysis"); invariant(docType !== "instance", "Analysis cannot yet be created on an instance"); + invariant( + docType !== "llmconversation", + "Analysis cannot be created on an LLM conversation", + ); const newRef = await createAnalysis(api, docType, api.makeUnversionedRef(docRefId)); handleDocCreated("analysis", newRef); From fd8f753ed5e868cde4ab314d85678e9ca3e81efe Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Fri, 31 Jul 2026 11:35:03 -0700 Subject: [PATCH 3/4] CLEANUP: Remove redundant check on document type. This was already fixed in a slightly different way by #1353. --- packages/frontend/src/model/model_library.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/model/model_library.ts b/packages/frontend/src/model/model_library.ts index 0208cce5f..79e0337f7 100644 --- a/packages/frontend/src/model/model_library.ts +++ b/packages/frontend/src/model/model_library.ts @@ -319,7 +319,7 @@ function isPatchToFormalContent(doc: Document, patch: Patch): boolean { // Ignore changes to top-level data like document name. return false; } - if (doc.type === "model" && path[0] === "notebook" && path[1] === "cellContents" && path[2]) { + if (path[0] === "notebook" && path[1] === "cellContents" && path[2]) { // Ignores changes to cells without formal content. const cell = doc.notebook.cellContents[path[2]]; if (cell?.tag !== "formal") { From 80dcc73d80f51b4a7bb4419f77e35cfaf2d80536 Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Fri, 31 Jul 2026 11:50:31 -0700 Subject: [PATCH 4/4] CLEANUP: Simplify document type check when creating new analysis. --- packages/frontend/src/page/document_menu.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/frontend/src/page/document_menu.tsx b/packages/frontend/src/page/document_menu.tsx index f2d84f805..90b5d6f61 100644 --- a/packages/frontend/src/page/document_menu.tsx +++ b/packages/frontend/src/page/document_menu.tsx @@ -60,11 +60,9 @@ export function DocumentMenu(props: { const onNewAnalysis = async () => { const docRefId = props.docRef.refId; const docType = props.liveDoc.doc.type; - invariant(docType !== "analysis", "Analysis cannot be created on other analysis"); - invariant(docType !== "instance", "Analysis cannot yet be created on an instance"); invariant( - docType !== "llmconversation", - "Analysis cannot be created on an LLM conversation", + docType === "model" || docType === "diagram", + () => `Cannot create analysis of ${docType} document`, ); const newRef = await createAnalysis(api, docType, api.makeUnversionedRef(docRefId)); @@ -119,12 +117,7 @@ export function DocumentMenu(props: { - + onNewAnalysis()}> {`New analysis of this ${docType()}`}