From 1ed42a59766a96390caee9bc6eed358b6e8793f3 Mon Sep 17 00:00:00 2001
From: Kaspar Bumke
Date: Wed, 8 Jul 2026 17:15:44 +0100
Subject: [PATCH 1/7] DOC: Add RFC 0006: Notebook Typescript API
---
rfc/0006.md | 2962 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 2962 insertions(+)
create mode 100644 rfc/0006.md
diff --git a/rfc/0006.md b/rfc/0006.md
new file mode 100644
index 000000000..f110fe690
--- /dev/null
+++ b/rfc/0006.md
@@ -0,0 +1,2962 @@
+---
+title: |
+ `0006` Notebook Typescript API
+author: Kaspar Bumke
+date: 2026-07-08
+toc: true
+toc-depth: 3
+---
+
+::: {.callout-note}
+
+### RFC status
+
+This RFC is tracked in
+[#1334](https://github.com/ToposInstitute/CatColab/issues/1334).
+:::
+
+## Summary & Motivation
+
+Catlog and its WASM interface expose a rich and flexible API surface for formal
+modeling. The full flexibility of this is available to our frontend via
+Typescript, with types largely generated from Rust, but the ergonomics and
+type-safety of this API are sub-par.
+
+Colocation of the Typescript handling `catlog-wasm` and our
+`catcolab-document-types` with the frontend has led to entanglement of UI
+framework concerns with the modeling API.
+
+We want to improve this API surface with the aim of helping three main use-cases:
+
+1. Our use in the CatColab frontend
+2. Other programmatic use from Javascript and Typescript
+3. Users instructing LLMs
+
+We will focus on notebook documents, since that is the only document currently
+in use, but this API should be expandable to other types of documents in the
+future.
+
+## Technical approach
+
+We will replace our current `catcolab-document-methods` package with a package
+called `catcolab-documents` that makes use of `catcolab-document-types` and
+wraps notebook creation and editing in a more ergonomic interface.
+
+We will also add a package called `catcolab-logics` that defines the logics we
+currently make available to users on the frontend.
+
+In the future we may publish these packages on public registries such as NPM
+and JSR.
+
+### Creating notebooks
+
+`catcolab-documents` provides a default `binder` API instance and defines the
+`RichText` and `Instantiation` cells.
+
+
+
+```ts
+import { binder, RichText, Instantiation } from "catcolab-documents";
+import { Aspect, SimpleOlog, Type } from "catcolab-logics/simple-olog";
+```
+
+All documents are created through a binder instance. The default binder uses a
+plain in-memory store for documents and provides no reactivity on its document
+view. Creating custom binder instances is covered in [Use with SolidJS and
+Automerge](#use-with-solidjs-and-automerge).
+
+
+
+```ts
+const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+```
+
+### Editing notebooks
+
+All cells are added with a single `add` method. The first argument selects
+the kind of cell: `RichText` for prose, or an object/morphism type from the
+logic for formal cells in the case of editing models.
+
+
+
+```ts
+const intro = notebook.add(RichText, { content: "We define a simple olog." });
+```
+
+We can create objects and morphisms in the notebook.
+
+
+
+```ts
+const source = notebook.add(Type, {
+ name: "A",
+});
+
+const target = notebook.add(Type, {
+ name: "B",
+});
+
+const arrow = notebook.add(Aspect, {
+ name: "has",
+ from: source,
+ to: target,
+});
+```
+
+We can update any item.
+
+
+
+```ts
+notebook.update({ name: "A simple Olog example" });
+
+intro.update({
+ content: "We define a simple olog with two objects and one arrow.",
+});
+
+source.update({
+ name: "Source",
+});
+
+arrow.update({
+ name: "has as",
+ from: source,
+ to: target,
+});
+```
+
+We can also do partial updates.
+
+```ts
+arrow.update({
+ name: "has as example",
+});
+```
+
+### Instantiation
+
+Instantiation is just another cell type provided by `catcolab-documents`. In our
+initial implementation we only support instantiation of notebooks already defined
+in the local binder.
+
+```ts
+const anotherOlog = await binder.createNotebook(SimpleOlog, { name: "Another Olog" });
+const thing = anotherOlog.add(Type, { name: "Thing" });
+
+const instantiation = notebook.add(Instantiation, {
+ name: "ImportedOlog",
+ model: anotherOlog,
+ // maps ImportedOlog.Thing <- B
+ specializations: [{ object: thing, as: target }],
+});
+```
+
+### Iterating through cells
+
+A `cells` method is provided to iterate through all cells of the notebook.
+
+```ts
+import { CellKind } from "catcolab-documents";
+
+for (const cell of notebook.cells()) {
+ switch (cell.kind) {
+ case CellKind.RichText:
+ console.log("text:", cell.content);
+ break;
+ case CellKind.Object:
+ console.log("object:", cell.name, "type:", cell.type.obType.content);
+ break;
+ case CellKind.Morphism:
+ console.log("morphism:", cell.name, "type tag:", cell.type.morType.tag);
+ break;
+ }
+}
+```
+
+```
+text: We define a simple olog with two objects and one arrow.
+object: Source type: Object
+object: B type: Object
+morphism: has as type tag: Hom
+```
+
+Additionally we provide a `cellsOf` method where we can filter by cell type
+(and by `Shape`, see [Generic shapes of notebooks](#generic-shapes-of-notebooks)).
+
+
+
+
+
+```ts
+import { Attr, AttrType, Entity, Mapping, SimpleSchema } from "catcolab-logics/simple-schema";
+import { binder } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
+
+const person = notebook.add(Entity, { name: "Person" });
+const company = notebook.add(Entity, { name: "Company" });
+
+const mapping = notebook.add(Mapping, { name: "employer", from: person, to: company });
+```
+
+```ts
+const entities = notebook.cellsOf(Entity);
+const mappings = notebook.cellsOf(Mapping);
+
+console.log("entities:", entities.map((cell) => cell.name).join(", "));
+console.log("mappings:", mappings.map((cell) => cell.name).join(", "));
+```
+
+```
+entities: Person, Company
+mappings: employer
+```
+
+`cells` and `cellsOf` do not recurse into instantiations.
+
+
+
+Code example
+
+
+
+
+
+```ts
+import { Entity, Mapping, SimpleSchema } from "catcolab-logics/simple-schema";
+import { binder } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
+
+const person = notebook.add(Entity, { name: "Person" });
+const company = notebook.add(Entity, { name: "Company" });
+
+const mapping = notebook.add(Mapping, { name: "employer", from: person, to: company });
+```
+
+
+
+```ts
+import { Instantiation } from "catcolab-documents";
+
+const anotherSchema = await binder.createNotebook(SimpleSchema, { name: "Another schema" });
+const enterprise = anotherSchema.add(Entity, { name: "Enterprise" });
+const building = anotherSchema.add(Entity, { name: "Building" });
+const owner = anotherSchema.add(Mapping, { name: "owner", from: enterprise, to: building });
+
+const instantiation = notebook.add(Instantiation, {
+ name: "ImportedSchema",
+ model: anotherSchema,
+ specializations: [{ object: enterprise, as: company }],
+});
+```
+
+```ts
+const instantiations = notebook.cellsOf(Instantiation);
+const entities = notebook.cellsOf(Entity);
+const mappings = notebook.cellsOf(Mapping);
+
+console.log("instantiations:", instantiations.map((cell) => cell.name).join(", "));
+console.log("entities:", entities.map((cell) => cell.name).join(", "));
+console.log("mappings:", mappings.map((cell) => cell.name).join(", "));
+```
+
+```
+instantiations: ImportedSchema
+entities: Person, Company
+mappings: employer
+```
+
+
+
+### Further details on notebook editing
+
+#### Duplicating cells
+
+We can duplicate cells. Copies keep the same logical shape but receive
+fresh identities, and their handles can be updated independently.
+
+
+
+ Code example
+
+
+
+
+```ts
+import { SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { binder, RichText } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+
+const a = notebook.add(Type, { name: "A" });
+const b = a.duplicate();
+b.update({
+ name: `B (copy of ${a.name})`,
+});
+
+console.log("a:", a.name);
+console.log("b:", b.name);
+```
+
+```
+a: A
+b: B (copy of A)
+```
+
+
+
+#### Re-ordering cells
+
+Every cell handle can move itself within the notebook. Moves locate the cell
+by its id at the moment the change applies, so they remain valid even if the
+notebook was edited after the handle was obtained.
+
+
+
+ Code examples
+
+
+
+
+
+```ts
+import { SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { binder, RichText } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+
+const a = notebook.add(Type, { name: "A" });
+const b = notebook.add(Type, { name: "B" });
+const c = notebook.add(Type, { name: "C" });
+
+function names() {
+ return notebook
+ .cellsOf(Type)
+ .map((cell) => cell.name)
+ .join(", ");
+}
+```
+
+`moveUp` and `moveDown` shift a cell one position; `moveTo` moves it to an
+index, interpreted after the cell is removed from its current position.
+
+```ts
+c.moveUp();
+console.log(names());
+
+a.moveDown();
+console.log(names());
+
+b.moveTo(0);
+console.log(names());
+```
+
+```
+A, C, B
+C, A, B
+B, C, A
+```
+
+Impossible moves are silent no-ops and out-of-range targets clamp to the ends
+of the notebook.
+
+```ts
+a.moveUp();
+c.moveDown();
+console.log(names());
+
+b.moveTo(99);
+console.log(names());
+```
+
+```
+A, B, C
+A, C, B
+```
+
+
+
+#### Deleting cells
+
+Every cell handle can remove itself from the notebook with `delete`. Like the
+reorder methods, delete locates the cell by its id when the change applies, so
+it stays valid even if the notebook was edited after the handle was obtained.
+
+
+
+ Code examples
+
+Deleting a cell removes it from the notebook's order and contents.
+
+```ts
+console.log(names());
+b.delete();
+console.log(names());
+```
+
+```
+A, B, C
+A, C
+```
+
+Rich-text cells can be deleted in the same way.
+
+```ts
+const note = notebook.add(RichText, { content: "A note." });
+console.log(notebook.cells().length);
+note.delete();
+console.log(notebook.cells().length);
+```
+
+```
+4
+3
+```
+
+After deletion, reading fields off the stale handle returns `undefined`.
+
+```ts
+b.delete();
+console.log(b.name);
+```
+
+```
+undefined
+```
+
+Deleting an already-deleted cell is a silent no-op.
+
+```ts
+b.delete();
+b.delete();
+console.log(names());
+```
+
+```
+A, C
+```
+
+
+
+#### Getting a cell by id
+
+We can retrieve a cell by its id, filtering by the type of cell we are looking
+for.
+
+
+
+ Code example
+
+```ts
+const retrievedA = notebook.get(Type, a.id);
+if (retrievedA.tag === "Ok") {
+ console.log("retrieved:", retrievedA.content.name);
+}
+```
+
+```
+retrieved: A
+```
+
+
+
+#### Subscribing to changes.
+
+We can subscribe to notebook changes with `onChange`. This triggers on _any_
+change to the notebook. No arguments are passed to the callback.
+
+
+
+ Code example
+
+```ts
+const unsubscribe = notebook.onChange(() => {
+ console.log("changed");
+});
+```
+
+
+
+### A `Result` type
+
+In cases where where functions take in unknown and user generated inputs we want a
+standardized way to return the result on success and diagnostics on failure.
+
+Our `Result` type is a tagged union structurally identical to the `JsResult`
+type in our Rust bindings (`catlog-wasm`). Results coming out of the Rust layer
+can therefore pass through untouched.
+
+When a failure is about validation of unknown or user generated input, the
+`Err` content is an array of issues following the [Standard
+Schema](https://standardschema.dev) definition. This allows for flexibility in
+validation libraries and implementation while sticking to a standard format,
+and lets us design and make use of general purpose libraries that follow this
+standard.
+
+```ts
+/** The result of a fallible operation. */
+type Result> =
+ | { readonly tag: "Ok"; readonly content: T }
+ | { readonly tag: "Err"; readonly content: E };
+
+/** The issue interface of the failure output. */
+interface Issue {
+ /** The error message of the issue. */
+ readonly message: string;
+ /** The path of the issue, if any. */
+ readonly path?: ReadonlyArray | undefined;
+}
+
+/** The path segment interface of the issue. */
+interface PathSegment {
+ /** The key representing a path segment. */
+ readonly key: PropertyKey;
+}
+```
+
+Since it is general enough, in the case of e.g. validation failure of a model,
+we can use the path field to point at specific object or morphism. If we have
+the need we can extend this in the future e.g. adding a `uuid` field to the
+`PathSegment` interface, without breaking compatibility with the standard.
+
+### Model validation
+
+#### Validate method
+
+`validate` is an asynchronous method that returns a `Result`: an `Ok` carrying
+the elaborated model as `content`, or an `Err` carrying an array of issues.
+
+
+
+
+
+```ts
+import type { DblModel } from "catlog-wasm";
+import type { Result } from "catcolab-documents";
+import { Aspect, SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { binder } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+
+const source = notebook.add(Type, { name: "A" });
+const target = notebook.add(Type, { name: "B" });
+notebook.add(Aspect, { name: "has", from: source, to: target });
+```
+
+A well-formed notebook validates to an `Ok` carrying the model as `content`.
+
+```ts
+const result: Result = await notebook.validate();
+console.log("valid:", result.tag === "Ok");
+```
+
+```
+valid: true
+```
+
+The validated model is available on the result and can be queried.
+
+```ts
+const result = await notebook.validate();
+if (result.tag === "Ok") {
+ console.log("objects:", result.content.obGenerators().length);
+ console.log("morphisms:", result.content.morGenerators().length);
+}
+```
+
+```
+objects: 2
+morphisms: 1
+```
+
+#### Subscribing to validation changes
+
+You can subscribe to validation changes with `onValidate` and get notified when
+any of the formal content of the notebook affected the validation result.
+
+```ts
+const unsubscribe = notebook.onValidate((result: Result) => {
+ console.log("valid:", result.tag === "Ok");
+});
+```
+
+#### Validation result
+
+An ill-formed notebook results in an `Err` carrying issues.
+
+
+
+```ts
+import { SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { binder, Instantiation } from "catcolab-documents";
+
+const first = await binder.createNotebook(SimpleOlog, { name: "First" });
+const second = await binder.createNotebook(SimpleOlog, { name: "Second" });
+
+first.add(Type, { name: "A" });
+second.add(Type, { name: "B" });
+
+// A cycle: `first` instantiates `second`, which instantiates `first`.
+first.add(Instantiation, { name: "ImportedSecond", model: second });
+second.add(Instantiation, { name: "ImportedFirst", model: first });
+
+const result = await first.validate();
+console.log("valid:", result.tag === "Ok");
+if (result.tag === "Err") {
+ console.log("issues:", result.content.map((issue) => issue.message).join("; "));
+}
+```
+
+```
+valid: false
+issues: Instantiation cycle detected: "First" → "Second" → "First". A notebook cannot instantiate itself, directly or indirectly. To fix, remove one of the instantiations in this chain.
+```
+
+### Serialization
+
+
+
+
+
+```ts
+import { PetriNet } from "catcolab-logics/petri-net";
+import { binder } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(PetriNet, { name: "Example Petri-net" });
+```
+
+We can dump a notebook.
+
+
+
+```ts
+const petriNetData = notebook.dump();
+```
+
+And load it.
+
+```ts
+const loaded = await binder.loadNotebook(PetriNet, petriNetData);
+console.log("tag:", loaded.tag);
+if (loaded.tag === "Ok") {
+ console.log("loaded:", loaded.content.name);
+}
+```
+
+```
+tag: Ok
+loaded: Example Petri-net
+```
+
+Trying to load a document with the wrong shape yields an `Err` result whose
+issues describe the mismatch.
+
+```ts
+import { SimpleOlog } from "catcolab-logics/simple-olog";
+
+const wrong = await binder.loadNotebook(SimpleOlog, petriNetData);
+if (wrong.tag === "Err") {
+ console.log("issues:", wrong.content.map((issue) => issue.message).join("; "));
+}
+```
+
+```
+issues: Cannot load document with theory "petri-net" using a shape with theory "simple-olog".
+```
+
+We can also load a notebook from a handle.
+
+```ts
+const loaded = await binder.loadNotebookFromHandle(PetriNet, notebook.handle);
+```
+
+### Migrating between logics
+
+
+
+
+
+```ts
+import { Aspect, SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { Entity, Mapping, SimpleSchema } from "catcolab-logics/simple-schema";
+import { binder } from "catcolab-documents";
+
+const olog = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+
+const a = olog.add(Type, { name: "A" });
+const b = olog.add(Type, { name: "B" });
+olog.add(Aspect, { name: "has", from: a, to: b });
+```
+
+`migrateTo` returns a `Result`.
+
+```ts
+const migration = await olog.migrateTo(SimpleSchema);
+console.log("tag:", migration.tag);
+if (migration.tag === "Ok") {
+ const schema = migration.content;
+
+ // The original document was rewritten in place, not copied.
+ console.log("same document:", schema.document === olog.document);
+ console.log("theory:", schema.document.theory);
+ console.log(
+ "entities:",
+ schema
+ .cellsOf(Entity)
+ .map((cell) => cell.name)
+ .join(", "),
+ );
+ console.log(
+ "mappings:",
+ schema
+ .cellsOf(Mapping)
+ .map((cell) => cell.name)
+ .join(", "),
+ );
+ console.log("valid:", (await schema.validate()).tag === "Ok");
+}
+```
+
+```
+tag: Ok
+same document: true
+theory: simple-schema
+entities: A, B
+mappings: has
+valid: true
+```
+
+### Diagram notebooks
+
+Diagrams are another type of notebook and have a very similar editing interface.
+
+#### Diagram creation
+
+
+
+ `model` definition
+
+
+
+
+
+```ts
+import { Aspect, SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { binder, RichText } from "catcolab-documents";
+
+const model = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+
+const A = model.add(Type, { name: "A" });
+const B = model.add(Type, { name: "B" });
+const has = model.add(Aspect, { name: "has", from: A, to: B });
+```
+
+
+
+
+
+```ts
+const diagram = await binder.createNotebook(SimpleOlog.Diagram, {
+ name: "Olog diagram",
+ in: model,
+});
+```
+
+```ts
+console.log("name:", diagram.name);
+console.log("theory:", diagram.theory);
+```
+
+```
+name: Olog diagram
+theory: simple-olog
+```
+
+#### Adding cells over the model
+
+
+
+```ts
+diagram.add(RichText, { content: "We picture two instances of the olog." });
+
+const x = diagram.add(SimpleOlog.Diagram.Individual, { name: "x", over: A });
+const y = diagram.add(SimpleOlog.Diagram.Individual, { name: "y", over: B });
+```
+
+```ts
+console.log("over:", x.over.name);
+console.log("type:", x.type.obType.content);
+```
+
+```
+over: A
+type: Object
+```
+
+
+
+```ts
+const f = diagram.add(SimpleOlog.Diagram.Aspect, { from: x, to: y, over: has });
+```
+
+```ts
+console.log("over:", f.over.name);
+console.log("from:", f.from.name);
+console.log("to:", f.to.name);
+```
+
+```
+over: has
+from: x
+to: y
+```
+
+### Analysis notebooks
+
+Analyses are yet another type of notebook, again with a similar editing interface.
+Common analysis cell types will be packaged by us into `catcolab-analyses`.
+
+
+
+ `model` definition
+
+
+
+
+
+```ts
+import { SimpleOlog, Type } from "catcolab-logics/simple-olog";
+import { binder, RichText } from "catcolab-documents";
+
+const model = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
+
+const source = model.add(Type, { name: "A" });
+const target = model.add(Type, { name: "B" });
+```
+
+
+
+
+
+```ts
+const analysis = await binder.createNotebook(SimpleOlog.Analysis, {
+ name: "Olog analysis",
+ of: model,
+});
+```
+
+```ts
+console.log("name:", analysis.name);
+console.log("type:", analysis.analysisType);
+```
+
+```
+name: Olog analysis
+type: model
+```
+
+
+
+```ts
+import { Visualization } from "catcolab-analyses";
+analysis.add(RichText, { content: "We visualize the olog." });
+
+const viz = analysis.add(Visualization);
+```
+
+```ts
+console.log("analysis id:", viz.type.id);
+console.log("layout:", viz.params.layout);
+```
+
+```
+analysis id: diagram
+layout: graphviz-directed
+```
+
+```ts
+viz.update({ direction: "horizontal" });
+
+console.log("layout:", viz.params.layout);
+console.log("direction:", viz.params.direction);
+```
+
+```
+layout: graphviz-directed
+direction: horizontal
+```
+
+#### Running an analysis
+
+Running an analysis is an asynchronous function that returns a `Result`: an
+`Ok` carrying the analysis output as `content`, or an `Err` carrying an array
+of issues.
+
+```ts
+const result = await viz.run();
+if (result.tag === "Ok") {
+ const { svg } = result.content;
+ console.log("is svg:", svg.includes("
}>
+ {(m) => (
+
+
+
+ {(cell) => }
+
+
+
+ )}
+
+
+ );
+}
+```
+
+```tsx
+import { Attr, AttrType, Entity, SimpleSchema } from "catcolab-logics/simple-schema";
+async function until(predicate: () => boolean) {
+ while (!predicate()) {
+ await new Promise((resolve) => setTimeout(resolve));
+ }
+}
+
+const notebook = await solidBinder.createNotebook(SimpleSchema, { name: "Company schema" });
+const person = notebook.add(Entity, { name: "Person" });
+const string = notebook.add(AttrType, { name: "String" });
+notebook.add(AttrType, { name: "Integer" });
+notebook.add(AttrType, { name: "Boolean" });
+const name = notebook.add(Attr, { name: "name", from: person, to: string });
+
+const [text, setText] = createSignal("");
+const container = document.createElement("div");
+document.body.appendChild(container);
+const dispose = render(() => , container);
+
+await until(() => testCurrentModel() !== undefined);
+console.log(
+ "all:",
+ [...container.querySelectorAll(".completion-list li")].map((li) => li.textContent).join(", "),
+);
+console.log("codomain:", codomainLabel(testCurrentModel(), name));
+
+setText("in");
+console.log(
+ "filtered:",
+ [...container.querySelectorAll(".completion-list li")].map((li) => li.textContent).join(", "),
+);
+
+const model = testCurrentModel();
+if (!model) {
+ throw new Error("Expected a valid model.");
+}
+const integer = filteredCompletions(model, MyAttrType.obType, "in").find(
+ (id) => label(model, id) === "Integer",
+)!;
+selectCodomain(notebook, name, integer);
+await until(() => codomainLabel(testCurrentModel(), name) === "Integer");
+console.log("codomain:", codomainLabel(testCurrentModel(), name));
+
+dispose();
+container.remove();
+```
+
+```
+all: String, Integer, Boolean
+codomain: String
+filtered: String, Integer
+codomain: Integer
+```
+
+
+
+### Rich text handling
+
+Rich text is a special case because our editor components will need to bypass
+our notebook interface to work with ProseMirror and the Automerge plugin for
+ProseMirror.
+
+We need to define a `getRichTextRef` method on our store for this to work. This
+will add a `editorRef` field to the RichText cell that our editor component can
+make use of.
+
+
+
+Code example
+
+
+
+
+
+```tsx
+import * as Automerge from "@automerge/automerge";
+import { type Doc, getBackend, getObjectId, type Patch } from "@automerge/automerge";
+import {
+ type DocHandle,
+ type DocHandleChangePayload,
+ type Prop,
+ Repo,
+} from "@automerge/automerge-repo";
+import { makeDocumentProjection } from "@automerge/automerge-repo-solid-primitives";
+import { basicSchemaAdapter, init } from "@automerge/prosemirror";
+import {
+ createBinder,
+ defineShape,
+ type DocumentStore,
+ RichText,
+ type RichTextCell,
+} from "catcolab-documents";
+import { SimpleOlog } from "catcolab-logics/simple-olog";
+import { EditorState } from "prosemirror-state";
+import { EditorView } from "prosemirror-view";
+import { createEffect, createSignal, onCleanup } from "solid-js";
+import { For, render } from "solid-js/web";
+import { expect } from "vitest";
+
+import type { Document } from "catcolab-document-types";
+
+type StoreHandle = {
+ readonly docHandle: DocHandle;
+ readonly docView: Document;
+};
+
+function makeAutomergeRichTextStore(): DocumentStore {
+ const repo = new Repo();
+ return {
+ createHandle: async (initialDoc) => {
+ const docHandle = repo.create(initialDoc as Document);
+ return { docHandle, docView: makeDocumentProjection(docHandle) };
+ },
+ getDocumentView: (handle) => handle.docView,
+ changeDocument: (handle, fn) => handle.docHandle.change(fn),
+ subscribe: (handle, callback) => {
+ handle.docHandle.on("change", callback);
+ return () => handle.docHandle.off("change", callback);
+ },
+ copyValue: (handle, value) => {
+ const objId = getObjectId(value as object);
+ return getBackend(handle.docHandle.doc()).materialize(objId!) as typeof value;
+ },
+ getLinkForHandle: () => undefined,
+ getHandle: async () => undefined,
+ // This is needed for the RichTextCellEditor to bypass the notebook interface.
+ getRichTextRef: (handle, cellId) => ({
+ docHandle: handle.docHandle,
+ path: ["notebook", "cellContents", cellId, "content"],
+ }),
+ };
+}
+
+function isPathPrefixOf(candidate: Prop[], target: readonly Prop[]) {
+ return (
+ candidate.length <= target.length &&
+ candidate.every((part, index) => part === target[index])
+ );
+}
+
+function hasStructuralReplacement(patches: Patch[], textPath: readonly Prop[]) {
+ return patches.some(
+ (patch) =>
+ (patch.action === "put" || patch.action === "del") &&
+ isPathPrefixOf(patch.path, textPath),
+ );
+}
+
+function RichTextCellEditor(props: { cell: RichTextCell; onView: (view: EditorView) => void }) {
+ let editorRoot!: HTMLDivElement;
+ const [reinitTrigger, setReinitTrigger] = createSignal(0);
+
+ createEffect(() => {
+ void reinitTrigger();
+
+ // The editorRef is exposed on the cell.
+ const ref = props.cell.editorRef;
+ if (!ref) {
+ throw new Error("RichTextCellEditor: cell has no editorRef");
+ }
+
+ const docHandle = ref.docHandle as DocHandle;
+ const path = [...ref.path];
+ const { schema, pmDoc, plugin } = init(docHandle, path, {
+ schemaAdapter: basicSchemaAdapter,
+ });
+ const state = EditorState.create({ schema, plugins: [plugin], doc: pmDoc });
+ const view: EditorView = new EditorView(editorRoot, {
+ state,
+ dispatchTransaction: (tx) => {
+ if (!view.isDestroyed) {
+ view.updateState(view.state.apply(tx));
+ }
+ },
+ });
+
+ const onRemoteChange = (payload: DocHandleChangePayload) => {
+ if (hasStructuralReplacement(payload.patches, path)) {
+ setReinitTrigger((n) => n + 1);
+ }
+ };
+
+ docHandle.on("change", onRemoteChange);
+ props.onView(view);
+ onCleanup(() => {
+ docHandle.off("change", onRemoteChange);
+ view.destroy();
+ });
+ });
+
+ return ;
+}
+```
+
+```tsx
+const InformalShape = defineShape({ informal: [RichText] });
+
+const store = makeAutomergeRichTextStore();
+const frontendBinder = createBinder(store);
+const notebook = await frontendBinder.createNotebook(SimpleOlog, { name: "Notes" });
+const note = notebook.add(RichText, { content: "" });
+
+let view: EditorView | undefined;
+const container = document.createElement("div");
+document.body.appendChild(container);
+const dispose = render(
+ () => (
+
+ {(cell) => (view = nextView)} />}
+
+ ),
+ container,
+);
+
+view!.dispatch(view!.state.tr.insertText("Hello from ProseMirror"));
+expect(note.content).toBe("Hello from ProseMirror");
+
+const ref = note.editorRef!;
+const docHandle = ref.docHandle as DocHandle;
+docHandle.change((doc) => {
+ Automerge.splice(doc as Doc, [...ref.path], 0, 5, "Howdy");
+});
+expect(view!.state.doc.textContent).toBe("Howdy from ProseMirror");
+
+const staleView = view;
+note.update({ content: "Replaced programmatically" });
+expect(view).not.toBe(staleView);
+expect(staleView?.isDestroyed).toBe(true);
+expect(view!.state.doc.textContent).toBe("Replaced programmatically");
+
+dispose();
+container.remove();
+```
+
+
+
+### Future extensions
+
+#### Path equations
+
+As this RFC is written, path equations are being added to the `simple-olog` and
+`simple-schema` theories in the frontend. `PathEquation` is another cell type,
+with a `CellKind.PathEquation` discriminant, and a shape opts into it via
+`supportsEquations: true`.
+
+
+
+
+
+```ts
+import { Entity, Mapping, SimpleSchema } from "catcolab-logics/simple-schema";
+import { binder, PathEquation } from "catcolab-documents";
+
+const notebook = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
+
+const person = notebook.add(Entity, { name: "Person" });
+const department = notebook.add(Entity, { name: "Department" });
+const company = notebook.add(Entity, { name: "Company" });
+
+const worksIn = notebook.add(Mapping, { name: "works in", from: person, to: department });
+const partOf = notebook.add(Mapping, { name: "part of", from: department, to: company });
+const employer = notebook.add(Mapping, { name: "employer", from: person, to: company });
+
+// An employee's employer is the company their department is part of.
+const equation = notebook.add(PathEquation, {
+ name: "employment",
+ lhs: [worksIn, partOf],
+ rhs: [employer],
+});
+```
+
+
+
+Code verification
+
+```ts
+console.log("equation:", equation.name);
+console.log("lhs:", equation.lhs.map((step) => step.name).join(" ; "));
+console.log("rhs:", equation.rhs.map((step) => step.name).join(" ; "));
+console.log("equations:", notebook.cellsOf(PathEquation).length);
+
+const result = await notebook.validate();
+console.log("valid:", result.tag === "Ok");
+```
+
+```
+equation: employment
+lhs: works in ; part of
+rhs: employer
+equations: 1
+valid: true
+```
+
+
+
+#### Instances
+
+Non-diagram instances are being added to CatColab. We can envision an API for
+instances that mirrors our Notebook API.
+
+
+
+
+
+```ts
+import { binder } from "catcolab-documents";
+import { SimpleSchema, Mapping, Attr, Entity, AttrType } from "catcolab-logics/simple-schema";
+
+const schema = await binder.createNotebook(SimpleSchema, { name: "Company schema" });
+const person = schema.add(Entity, { name: "Person" });
+const company = schema.add(Entity, { name: "Company" });
+const str = schema.add(AttrType, { name: "String" });
+
+const employer = schema.add(Mapping, { name: "employer", from: person, to: company });
+const name = schema.add(Attr, { name: "name", from: person, to: str });
+```
+
+An instance can have a very similar `add` method.
+
+
+
+```ts
+const instance = await binder.createInstance(schema, { name: "Company instance" });
+const acme = instance.add(company, {});
+instance.add(person, { name: "Alice", employer: acme });
+instance.add(person, { name: "Bob", employer: acme });
+```
+
+A "table" could be retrieved via a `rowsOf` that mirrors `cellsOf` though,
+unlike cells it may be better to keep a separate `values` field on rows, to
+keep them in their own namespace.
+
+```ts
+for (const row of instance.rowsOf(person)) {
+ console.log(row.values["name"]);
+}
+```
+
+```
+Alice
+Bob
+```
+
+Our API could in principle allow us to define logic shapes as notebooks of
+`SimpleSchema`. To illustrate this here is the `SimpleSchema` logic defined as
+a notebook of `SimpleSchema` by aliasing the real `SimpleSchema` to
+`SuperSchema` first.
+
+
+
+
+
+```ts
+import {
+ Attr as SuperAttr,
+ AttrType as SuperAttrType,
+ Entity as SuperEntity,
+ Mapping as SuperMapping,
+ SimpleSchema as SuperSchema,
+} from "catcolab-logics/simple-schema";
+import { binder } from "catcolab-documents";
+const SimpleSchema = await binder.createNotebook(SuperSchema, { name: "simple-schema" });
+
+const Entity = SimpleSchema.add(SuperEntity, { name: "Entity" });
+const AttrType = SimpleSchema.add(SuperEntity, { name: "AttrType" });
+const String = SimpleSchema.add(SuperAttrType, { name: "String" });
+
+const Mapping = SimpleSchema.add(SuperEntity, { name: "Mapping" });
+SimpleSchema.add(SuperMapping, { name: "from", from: Mapping, to: Entity });
+SimpleSchema.add(SuperMapping, { name: "to", from: Mapping, to: Entity });
+SimpleSchema.add(SuperAttr, { name: "name", from: Mapping, to: String });
+
+const Attr = SimpleSchema.add(SuperEntity, { name: "Attr" });
+SimpleSchema.add(SuperMapping, { name: "from", from: Attr, to: Entity });
+SimpleSchema.add(SuperMapping, { name: "to", from: Attr, to: AttrType });
+SimpleSchema.add(SuperAttr, { name: "name", from: Entity, to: String });
+```
+
+Schemas can can then be defined as instances of the `SimpleSchema` notebook as
+follows.
+
+```ts
+const schema = await binder.createInstance(SimpleSchema, { name: "Company schema" });
+
+const person = schema.add(Entity, { name: "Person" });
+const company = schema.add(Entity, { name: "Company" });
+const str = schema.add(AttrType, { name: "String" });
+
+const employer = schema.add(Mapping, { name: "employer", from: person, to: company });
+const name = schema.add(Attr, { name: "name", from: person, to: str });
+```
+
+To use this flexibility for defining logics as notebooks in practice a lot of
+details would still need to be worked out of course.
+
+### Rollout
+
+Our implementation should loosely follow these steps.
+
+1. **Test suite for model notebooks**
+
+ We will implement the tests first. This very document forms part of a spec
+ we can test against. We should write some more tests covering further
+ feature details and edge cases. If possible we should implement property
+ based tests that show our new API will not cause document corruption.
+
+2. **Package for model notebooks**
+
+ A package that meets the spec outline in the test suite. We will not
+ implement diagram or analysis notebooks initially.
+
+3. **CatColab script execution**
+
+ Our initial feature will be a way, possibly through a secret "developer"
+ interface, a user can write Javascript that makes use of this API to create
+ and edit model notebooks.
+
+4. **LLM integration in CatColab**
+
+ Our LLM integration should make use of the CatColab scripting capability to
+ build out a way for users to use LLMs to write scripts.
+
+5. **Test suite & implement analyses and diagrams**
+
+ We will spec and implement a package for analyses and add diagram capability
+ to the relevant models. These will become available to the script execution
+ and LLM integration.
+
+6. **New features use the API & port existing features**
+
+ Once we have proven the stability and usefuleness of the new API we should
+ dictate that any new features need to be made using the new API. We will
+ also begin the process of porting existing features to the new API.
From eb0382adec09a0a6179324f28353680aa89af4a2 Mon Sep 17 00:00:00 2001
From: Kaspar Bumke
Date: Wed, 8 Jul 2026 19:07:16 +0100
Subject: [PATCH 2/7] DOC: Clarify `onValidate` initial trigger
---
rfc/0006.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/rfc/0006.md b/rfc/0006.md
index f110fe690..b8c5489cf 100644
--- a/rfc/0006.md
+++ b/rfc/0006.md
@@ -575,6 +575,10 @@ morphisms: 1
You can subscribe to validation changes with `onValidate` and get notified when
any of the formal content of the notebook affected the validation result.
+`onValidate` will trigger a validation if the notebook has not been validated
+yet and should always call the callback at least once with the current
+validation.
+
```ts
const unsubscribe = notebook.onValidate((result: Result) => {
console.log("valid:", result.tag === "Ok");
From 2c1bf0de786bc43404df3683afb71d300b7c6cee Mon Sep 17 00:00:00 2001
From: Kaspar Bumke
Date: Thu, 9 Jul 2026 13:16:37 +0100
Subject: [PATCH 3/7] DOC: Tabular instances
---
rfc/0006.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/rfc/0006.md b/rfc/0006.md
index b8c5489cf..9fea1bf30 100644
--- a/rfc/0006.md
+++ b/rfc/0006.md
@@ -2826,9 +2826,9 @@ valid: true
-#### Instances
+#### Tabular instances
-Non-diagram instances are being added to CatColab. We can envision an API for
+Tabular instances are being added to CatColab. We can envision an API for
instances that mirrors our Notebook API.
@@ -2923,8 +2923,8 @@ const employer = schema.add(Mapping, { name: "employer", from: person, to: compa
const name = schema.add(Attr, { name: "name", from: person, to: str });
```
-To use this flexibility for defining logics as notebooks in practice a lot of
-details would still need to be worked out of course.
+To use this flexibility for defining logics through notebooks in practice a lot
+of details would still need to be worked out of course.
### Rollout
From cc1618cbd32b9aa57c39c237a96c1d651763754e Mon Sep 17 00:00:00 2001
From: Kaspar Bumke
Date: Thu, 9 Jul 2026 13:44:29 +0100
Subject: [PATCH 4/7] DOC: Rename `cell.name` to `cell.label`
---
rfc/0006.md | 352 ++++++++++++++++++++++++++--------------------------
1 file changed, 176 insertions(+), 176 deletions(-)
diff --git a/rfc/0006.md b/rfc/0006.md
index 9fea1bf30..6bebe3761 100644
--- a/rfc/0006.md
+++ b/rfc/0006.md
@@ -89,15 +89,15 @@ We can create objects and morphisms in the notebook.
```ts
const source = notebook.add(Type, {
- name: "A",
+ label: "A",
});
const target = notebook.add(Type, {
- name: "B",
+ label: "B",
});
const arrow = notebook.add(Aspect, {
- name: "has",
+ label: "has",
from: source,
to: target,
});
@@ -115,11 +115,11 @@ intro.update({
});
source.update({
- name: "Source",
+ label: "Source",
});
arrow.update({
- name: "has as",
+ label: "has as",
from: source,
to: target,
});
@@ -129,7 +129,7 @@ We can also do partial updates.
```ts
arrow.update({
- name: "has as example",
+ label: "has as example",
});
```
@@ -141,10 +141,10 @@ in the local binder.
```ts
const anotherOlog = await binder.createNotebook(SimpleOlog, { name: "Another Olog" });
-const thing = anotherOlog.add(Type, { name: "Thing" });
+const thing = anotherOlog.add(Type, { label: "Thing" });
const instantiation = notebook.add(Instantiation, {
- name: "ImportedOlog",
+ label: "ImportedOlog",
model: anotherOlog,
// maps ImportedOlog.Thing <- B
specializations: [{ object: thing, as: target }],
@@ -164,10 +164,10 @@ for (const cell of notebook.cells()) {
console.log("text:", cell.content);
break;
case CellKind.Object:
- console.log("object:", cell.name, "type:", cell.type.obType.content);
+ console.log("object:", cell.label, "type:", cell.type.obType.content);
break;
case CellKind.Morphism:
- console.log("morphism:", cell.name, "type tag:", cell.type.morType.tag);
+ console.log("morphism:", cell.label, "type tag:", cell.type.morType.tag);
break;
}
}
@@ -193,18 +193,18 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
-const person = notebook.add(Entity, { name: "Person" });
-const company = notebook.add(Entity, { name: "Company" });
+const person = notebook.add(Entity, { label: "Person" });
+const company = notebook.add(Entity, { label: "Company" });
-const mapping = notebook.add(Mapping, { name: "employer", from: person, to: company });
+const mapping = notebook.add(Mapping, { label: "employer", from: person, to: company });
```
```ts
const entities = notebook.cellsOf(Entity);
const mappings = notebook.cellsOf(Mapping);
-console.log("entities:", entities.map((cell) => cell.name).join(", "));
-console.log("mappings:", mappings.map((cell) => cell.name).join(", "));
+console.log("entities:", entities.map((cell) => cell.label).join(", "));
+console.log("mappings:", mappings.map((cell) => cell.label).join(", "));
```
```
@@ -228,10 +228,10 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
-const person = notebook.add(Entity, { name: "Person" });
-const company = notebook.add(Entity, { name: "Company" });
+const person = notebook.add(Entity, { label: "Person" });
+const company = notebook.add(Entity, { label: "Company" });
-const mapping = notebook.add(Mapping, { name: "employer", from: person, to: company });
+const mapping = notebook.add(Mapping, { label: "employer", from: person, to: company });
```
@@ -240,12 +240,12 @@ const mapping = notebook.add(Mapping, { name: "employer", from: person, to: comp
import { Instantiation } from "catcolab-documents";
const anotherSchema = await binder.createNotebook(SimpleSchema, { name: "Another schema" });
-const enterprise = anotherSchema.add(Entity, { name: "Enterprise" });
-const building = anotherSchema.add(Entity, { name: "Building" });
-const owner = anotherSchema.add(Mapping, { name: "owner", from: enterprise, to: building });
+const enterprise = anotherSchema.add(Entity, { label: "Enterprise" });
+const building = anotherSchema.add(Entity, { label: "Building" });
+const owner = anotherSchema.add(Mapping, { label: "owner", from: enterprise, to: building });
const instantiation = notebook.add(Instantiation, {
- name: "ImportedSchema",
+ label: "ImportedSchema",
model: anotherSchema,
specializations: [{ object: enterprise, as: company }],
});
@@ -256,9 +256,9 @@ const instantiations = notebook.cellsOf(Instantiation);
const entities = notebook.cellsOf(Entity);
const mappings = notebook.cellsOf(Mapping);
-console.log("instantiations:", instantiations.map((cell) => cell.name).join(", "));
-console.log("entities:", entities.map((cell) => cell.name).join(", "));
-console.log("mappings:", mappings.map((cell) => cell.name).join(", "));
+console.log("instantiations:", instantiations.map((cell) => cell.label).join(", "));
+console.log("entities:", entities.map((cell) => cell.label).join(", "));
+console.log("mappings:", mappings.map((cell) => cell.label).join(", "));
```
```
@@ -289,14 +289,14 @@ import { binder, RichText } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const a = notebook.add(Type, { name: "A" });
+const a = notebook.add(Type, { label: "A" });
const b = a.duplicate();
b.update({
- name: `B (copy of ${a.name})`,
+ label: `B (copy of ${a.label})`,
});
-console.log("a:", a.name);
-console.log("b:", b.name);
+console.log("a:", a.label);
+console.log("b:", b.label);
```
```
@@ -326,14 +326,14 @@ import { binder, RichText } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const a = notebook.add(Type, { name: "A" });
-const b = notebook.add(Type, { name: "B" });
-const c = notebook.add(Type, { name: "C" });
+const a = notebook.add(Type, { label: "A" });
+const b = notebook.add(Type, { label: "B" });
+const c = notebook.add(Type, { label: "C" });
function names() {
return notebook
.cellsOf(Type)
- .map((cell) => cell.name)
+ .map((cell) => cell.label)
.join(", ");
}
```
@@ -418,7 +418,7 @@ After deletion, reading fields off the stale handle returns `undefined`.
```ts
b.delete();
-console.log(b.name);
+console.log(b.label);
```
```
@@ -451,7 +451,7 @@ for.
```ts
const retrievedA = notebook.get(Type, a.id);
if (retrievedA.tag === "Ok") {
- console.log("retrieved:", retrievedA.content.name);
+ console.log("retrieved:", retrievedA.content.label);
}
```
@@ -539,9 +539,9 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const source = notebook.add(Type, { name: "A" });
-const target = notebook.add(Type, { name: "B" });
-notebook.add(Aspect, { name: "has", from: source, to: target });
+const source = notebook.add(Type, { label: "A" });
+const target = notebook.add(Type, { label: "B" });
+notebook.add(Aspect, { label: "has", from: source, to: target });
```
A well-formed notebook validates to an `Ok` carrying the model as `content`.
@@ -598,12 +598,12 @@ import { binder, Instantiation } from "catcolab-documents";
const first = await binder.createNotebook(SimpleOlog, { name: "First" });
const second = await binder.createNotebook(SimpleOlog, { name: "Second" });
-first.add(Type, { name: "A" });
-second.add(Type, { name: "B" });
+first.add(Type, { label: "A" });
+second.add(Type, { label: "B" });
// A cycle: `first` instantiates `second`, which instantiates `first`.
-first.add(Instantiation, { name: "ImportedSecond", model: second });
-second.add(Instantiation, { name: "ImportedFirst", model: first });
+first.add(Instantiation, { label: "ImportedSecond", model: second });
+second.add(Instantiation, { label: "ImportedFirst", model: first });
const result = await first.validate();
console.log("valid:", result.tag === "Ok");
@@ -688,9 +688,9 @@ import { binder } from "catcolab-documents";
const olog = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const a = olog.add(Type, { name: "A" });
-const b = olog.add(Type, { name: "B" });
-olog.add(Aspect, { name: "has", from: a, to: b });
+const a = olog.add(Type, { label: "A" });
+const b = olog.add(Type, { label: "B" });
+olog.add(Aspect, { label: "has", from: a, to: b });
```
`migrateTo` returns a `Result`.
@@ -708,14 +708,14 @@ if (migration.tag === "Ok") {
"entities:",
schema
.cellsOf(Entity)
- .map((cell) => cell.name)
+ .map((cell) => cell.label)
.join(", "),
);
console.log(
"mappings:",
schema
.cellsOf(Mapping)
- .map((cell) => cell.name)
+ .map((cell) => cell.label)
.join(", "),
);
console.log("valid:", (await schema.validate()).tag === "Ok");
@@ -751,9 +751,9 @@ import { binder, RichText } from "catcolab-documents";
const model = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const A = model.add(Type, { name: "A" });
-const B = model.add(Type, { name: "B" });
-const has = model.add(Aspect, { name: "has", from: A, to: B });
+const A = model.add(Type, { label: "A" });
+const B = model.add(Type, { label: "B" });
+const has = model.add(Aspect, { label: "has", from: A, to: B });
```
@@ -784,12 +784,12 @@ theory: simple-olog
```ts
diagram.add(RichText, { content: "We picture two instances of the olog." });
-const x = diagram.add(SimpleOlog.Diagram.Individual, { name: "x", over: A });
-const y = diagram.add(SimpleOlog.Diagram.Individual, { name: "y", over: B });
+const x = diagram.add(SimpleOlog.Diagram.Individual, { label: "x", over: A });
+const y = diagram.add(SimpleOlog.Diagram.Individual, { label: "y", over: B });
```
```ts
-console.log("over:", x.over.name);
+console.log("over:", x.over.label);
console.log("type:", x.type.obType.content);
```
@@ -805,9 +805,9 @@ const f = diagram.add(SimpleOlog.Diagram.Aspect, { from: x, to: y, over: has });
```
```ts
-console.log("over:", f.over.name);
-console.log("from:", f.from.name);
-console.log("to:", f.to.name);
+console.log("over:", f.over.label);
+console.log("from:", f.from.label);
+console.log("to:", f.to.label);
```
```
@@ -835,8 +835,8 @@ import { binder, RichText } from "catcolab-documents";
const model = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const source = model.add(Type, { name: "A" });
-const target = model.add(Type, { name: "B" });
+const source = model.add(Type, { label: "A" });
+const target = model.add(Type, { label: "B" });
```
@@ -930,10 +930,10 @@ import { binder } from "catcolab-documents";
const petriNet = await binder.createNotebook(PetriNet, { name: "SIR" });
-const susceptible = petriNet.add(Place, { name: "S" });
-const infected = petriNet.add(Place, { name: "I" });
+const susceptible = petriNet.add(Place, { label: "S" });
+const infected = petriNet.add(Place, { label: "I" });
-petriNet.add(Transition, { name: "infection", from: [susceptible, infected], to: [infected] });
+petriNet.add(Transition, { label: "infection", from: [susceptible, infected], to: [infected] });
const analysis = await binder.createNotebook(PetriNet.Analysis, {
name: "Petri net analysis",
@@ -1015,9 +1015,9 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const source = notebook.add(Type, { name: "A" });
-const target = notebook.add(Type, { name: "B" });
-const arrow = notebook.add(Aspect, { name: "has", from: source, to: target });
+const source = notebook.add(Type, { label: "A" });
+const target = notebook.add(Type, { label: "B" });
+const arrow = notebook.add(Aspect, { label: "has", from: source, to: target });
```
Invalid shapes should be type errors.
@@ -1027,11 +1027,11 @@ Invalid shapes should be type errors.
arrow.update({ from: [source] });
// @ts-expect-error Arrays are not valid endpoints in a simple olog.
-notebook.add(Aspect, { name: "bad", from: [source, target], to: target });
+notebook.add(Aspect, { label: "bad", from: [source, target], to: target });
// @ts-expect-error Missing required fields.
notebook.add(Aspect, {});
// null fields are allowed.
-notebook.add(Aspect, { name: null, from: null, to: null });
+notebook.add(Aspect, { label: null, from: null, to: null });
```
#### Further type errors
@@ -1050,11 +1050,11 @@ import { binder } from "catcolab-documents";
const schema = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
-const str = schema.add(AttrType, { name: "String" });
+const str = schema.add(AttrType, { label: "String" });
// @ts-expect-error A mapping's endpoints must be entities, not attribute types.
schema.add(Mapping, {
- name: "bad",
+ label: "bad",
from: str,
to: str,
});
@@ -1072,21 +1072,21 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(PetriNet, { name: "Example Petri-net" });
-const a = notebook.add(Place, { name: "A" });
+const a = notebook.add(Place, { label: "A" });
-const b = notebook.add(Place, { name: "B" });
+const b = notebook.add(Place, { label: "B" });
-const c = notebook.add(Place, { name: "C" });
+const c = notebook.add(Place, { label: "C" });
notebook.add(Transition, {
- name: "t1",
+ label: "t1",
from: [a, b],
to: [c],
});
// @ts-expect-error Petri net transitions require arrays of places.
notebook.add(Transition, {
- name: "bad",
+ label: "bad",
from: a,
to: [c],
});
@@ -1111,9 +1111,9 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(SimpleOlog, { name: "An Olog" });
-const source = notebook.add(Type, { name: "A" });
-const target = notebook.add(Type, { name: "B" });
-const arrow = notebook.add(Aspect, { name: "has", from: source, to: target });
+const source = notebook.add(Type, { label: "A" });
+const target = notebook.add(Type, { label: "B" });
+const arrow = notebook.add(Aspect, { label: "has", from: source, to: target });
```
Invalid shapes throw at runtime, with a Standard Schema compatible `issues` array:
@@ -1130,7 +1130,7 @@ try {
try {
// Arrays are not valid endpoints in a simple olog.
- notebook.add(Aspect, { name: "bad", from: [source, target], to: target });
+ notebook.add(Aspect, { label: "bad", from: [source, target], to: target });
} catch (error) {
console.log("add:", error.issues?.map((issue) => issue.message).join("; "));
}
@@ -1143,13 +1143,13 @@ try {
}
// null fields are allowed.
-notebook.add(Aspect, { name: null, from: null, to: null });
+notebook.add(Aspect, { label: null, from: null, to: null });
```
```
update: `from` must be an object cell or null (was an array)
add: `from` must be an object cell or null (was an array)
-missing: `from` must be an object cell or null (was missing); `name` must be a string or null (was missing); `to` must be an object cell or null (was missing)
+missing: `from` must be an object cell or null (was missing); `label` must be a string or null (was missing); `to` must be an object cell or null (was missing)
```
#### Further runtime errors
@@ -1172,14 +1172,14 @@ import { binder } from "catcolab-documents";
const schema = await binder.createNotebook(SimpleSchema, { name: "Example schema" });
-const str = schema.add(AttrType, { name: "String" });
+const str = schema.add(AttrType, { label: "String" });
```
```ts
try {
- schema.add(Mapping, { name: "bad", from: str, to: str });
+ schema.add(Mapping, { label: "bad", from: str, to: str });
} catch (error) {
console.log("mapping:", error.issues?.map((issue) => issue.message).join("; "));
}
@@ -1203,8 +1203,8 @@ import { binder } from "catcolab-documents";
const notebook = await binder.createNotebook(PetriNet, { name: "Example Petri-net" });
-const a = notebook.add(Place, { name: "A" });
-const c = notebook.add(Place, { name: "C" });
+const a = notebook.add(Place, { label: "A" });
+const c = notebook.add(Place, { label: "C" });
```
@@ -1212,7 +1212,7 @@ const c = notebook.add(Place, { name: "C" });
```ts
try {
// Petri net transitions require arrays of places.
- notebook.add(Transition, { name: "bad", from: a, to: [c] });
+ notebook.add(Transition, { label: "bad", from: a, to: [c] });
} catch (error) {
console.log("transition:", error.issues?.map((issue) => issue.message).join("; "));
}
@@ -1457,11 +1457,11 @@ const backendBinder = createBinder(backendStore);
```ts
const imported = await backendBinder.createNotebook(SimpleOlog, { name: "Imported" });
-imported.add(Type, { name: "Thing" });
+imported.add(Type, { label: "Thing" });
const notebook = await backendBinder.createNotebook(SimpleOlog, { name: "Main" });
-notebook.add(Type, { name: "A" });
-notebook.add(Instantiation, { name: "ImportedOlog", model: imported });
+notebook.add(Type, { label: "A" });
+notebook.add(Instantiation, { label: "ImportedOlog", model: imported });
const result = await notebook.validate();
console.log(result.tag);
@@ -1639,8 +1639,8 @@ import { SimpleOlog } from "catcolab-logics/simple-olog";
import { PetriNet } from "catcolab-logics/petri-net";
function addObjects(notebook: Notebook) {
- notebook.add(BasicObj, { name: "A" });
- notebook.add(BasicObj, { name: "B" });
+ notebook.add(BasicObj, { label: "A" });
+ notebook.add(BasicObj, { label: "B" });
}
```
@@ -1693,20 +1693,20 @@ const OnlyEntityObj = defineShape({
function addBasicAndEntityObjects(notebook: Notebook) {
// @ts-expect-error We can't add a BasicObj without narrowing the notebook
// type because EntityWithMor does not support BasicObj.
- notebook.add(BasicObj, { name: "A" });
+ notebook.add(BasicObj, { label: "A" });
// This is ok because we narrowed the notebook type.
if (notebook.supports(BasicObj)) {
- notebook.add(BasicObj, { name: "B" });
+ notebook.add(BasicObj, { label: "B" });
}
// @ts-expect-error We can't add a BasicObj without narrowing the notebook
// type because OnlyBasicObj does not suppor EntityObj.
- notebook.add(EntityObj, { name: "C" });
+ notebook.add(EntityObj, { label: "C" });
// This is ok because we narrowed the notebook type.
if (notebook.supports(EntityObj)) {
- notebook.add(EntityObj, { name: "D" });
+ notebook.add(EntityObj, { label: "D" });
}
}
```
@@ -1725,22 +1725,22 @@ const EntityWithMor = defineShape({
function addBasicEntityAndMor(notebook: Notebook) {
// @ts-expect-error We can't add a BasicObj without narrowing the notebook
// type because EntityWithMor does not support BasicObj.
- notebook.add(BasicObj, { name: "A" });
+ notebook.add(BasicObj, { label: "A" });
// This is ok because we narrowed the notebook type.
if (notebook.supports(OnlyBasicObj)) {
- notebook.add(BasicObj, { name: "B" });
+ notebook.add(BasicObj, { label: "B" });
}
// @ts-expect-error We can't add a BasicObj without narrowing the notebook
// type because OnlyBasicObj does not support EntityObj.
- notebook.add(EntityObj, { name: "C" });
+ notebook.add(EntityObj, { label: "C" });
// This is ok because we narrowed the notebook type to supporting both
// `EntiyObj` and `Morphism`.
if (notebook.supports(EntityWithMor)) {
- const d = notebook.add(EntityObj, { name: "D" });
- notebook.add(Morphism, { name: "f", from: d, to: d });
+ const d = notebook.add(EntityObj, { label: "D" });
+ notebook.add(Morphism, { label: "f", from: d, to: d });
}
}
```
@@ -1763,14 +1763,14 @@ const EntityShape = defineShape({
const schema = await binder.createNotebook(SimpleSchema, { name: "Example" });
-const a = schema.add(Entity, { name: "A" });
-const b = schema.add(AttrType, { name: "B" });
-const c = schema.add(Entity, { name: "C" });
-schema.add(Attr, { name: "f", from: a, to: b });
-schema.add(Mapping, { name: "g", from: a, to: c });
+const a = schema.add(Entity, { label: "A" });
+const b = schema.add(AttrType, { label: "B" });
+const c = schema.add(Entity, { label: "C" });
+schema.add(Attr, { label: "f", from: a, to: b });
+schema.add(Mapping, { label: "g", from: a, to: c });
for (const cell of schema.cellsOf(EntityShape)) {
- console.log(cell.name);
+ console.log(cell.label);
}
```
@@ -1884,21 +1884,21 @@ type NotebookOfLists = Notebook<
>;
function addListMorphism(notebook: NotebookOfLists) {
- const a = notebook.add(BasicObj, { name: "A" });
- const b = notebook.add(BasicObj, { name: "B" });
- const c = notebook.add(BasicObj, { name: "C" });
+ const a = notebook.add(BasicObj, { label: "A" });
+ const b = notebook.add(BasicObj, { label: "B" });
+ const c = notebook.add(BasicObj, { label: "C" });
if (notebook.supports(ListMor)) {
- notebook.add(ListMor, { name: "L", from: [a, b], to: [c] });
+ notebook.add(ListMor, { label: "L", from: [a, b], to: [c] });
} else if (notebook.supports(SymmetricListMor)) {
console.log("Adding SymmetricListMor!");
- notebook.add(SymmetricListMor, { name: "L", from: [a, b], to: [c] });
+ notebook.add(SymmetricListMor, { label: "L", from: [a, b], to: [c] });
} else if (notebook.supports(CocartesianListMor)) {
- notebook.add(CocartesianListMor, { name: "L", from: [a, b], to: [c] });
+ notebook.add(CocartesianListMor, { label: "L", from: [a, b], to: [c] });
} else if (notebook.supports(CartesianListMor)) {
- notebook.add(CartesianListMor, { name: "L", from: [a, b], to: [c] });
+ notebook.add(CartesianListMor, { label: "L", from: [a, b], to: [c] });
} else if (notebook.supports(AdditiveListMor)) {
- notebook.add(AdditiveListMor, { name: "L", from: [a, b], to: [c] });
+ notebook.add(AdditiveListMor, { label: "L", from: [a, b], to: [c] });
} else {
// If the code type checked this should be unreachable.
throw new Error("Did not find any supported List morphism in the notebook.");
@@ -1908,12 +1908,12 @@ function addListMorphism(notebook: NotebookOfLists) {
```ts
function badAddListMorphism(notebook: NotebookOfLists) {
- const a = notebook.add(BasicObj, { name: "A" });
- const b = notebook.add(BasicObj, { name: "B" });
- const c = notebook.add(BasicObj, { name: "C" });
+ const a = notebook.add(BasicObj, { label: "A" });
+ const b = notebook.add(BasicObj, { label: "B" });
+ const c = notebook.add(BasicObj, { label: "C" });
//@ts-expect-error Not all variants support adding a `ListMor`. You need to narrow the type using the `supports` method.
- notebook.add(ListMor, { name: "L", from: [a, b], to: [c] });
+ notebook.add(ListMor, { label: "L", from: [a, b], to: [c] });
}
```
@@ -2011,13 +2011,13 @@ const MultiObjectListShape = defineShape({
});
function badAddListMorphism2(notebook: Notebook) {
- const a = notebook.add(BasicObj, { name: "A" });
- const b = notebook.add(BasicObj, { name: "B" });
- const e = notebook.add(EntityObj, { name: "E" });
+ const a = notebook.add(BasicObj, { label: "A" });
+ const b = notebook.add(BasicObj, { label: "B" });
+ const e = notebook.add(EntityObj, { label: "E" });
- notebook.add(ListMor, { name: "L1", from: [a, b], to: [b] });
+ notebook.add(ListMor, { label: "L1", from: [a, b], to: [b] });
//@ts-expect-error We can't use an EntityObj with a ListMor
- notebook.add(ListMor, { name: "L2", from: [a, b], to: [e] });
+ notebook.add(ListMor, { label: "L2", from: [a, b], to: [e] });
}
```
@@ -2050,11 +2050,11 @@ const EntityObj = entityObType;
function goodAddObject(notebook: NotebookOfListsWithEntity) {
if (notebook.supports(BasicObj)) {
- notebook.add(BasicObj, { name: "A" });
+ notebook.add(BasicObj, { label: "A" });
}
if (notebook.supports(EntityObj)) {
- notebook.add(EntityObj, { name: "E" });
+ notebook.add(EntityObj, { label: "E" });
}
}
@@ -2064,33 +2064,33 @@ const BothObjectsShape = defineShape({
function goodAddObject2(notebook: NotebookOfListsWithEntity) {
if (notebook.supports(BothObjectsShape)) {
- notebook.add(BasicObj, { name: "A" });
- notebook.add(EntityObj, { name: "E" });
+ notebook.add(BasicObj, { label: "A" });
+ notebook.add(EntityObj, { label: "E" });
}
}
type JustEntityObjectListShape = Notebook;
function goodAddObject3(notebook: JustEntityObjectListShape) {
- notebook.add(EntityObj, { name: "E" });
+ notebook.add(EntityObj, { label: "E" });
}
function badAddObject(notebook: NotebookOfListsWithEntity) {
//@ts-expect-error We can't add a BasicObj without narrowing the notebook type because EntityObjectListShape does not support BasicObj.
- notebook.add(BasicObj, { name: "A" });
+ notebook.add(BasicObj, { label: "A" });
//@ts-expect-error We can't add a EntityObj without narrowing the notebook type because not all notebooks support EntityObj.
- notebook.add(EntityObj, { name: "E" });
+ notebook.add(EntityObj, { label: "E" });
}
function badAddObject2(notebook: Notebook) {
- const a = notebook.add(BasicObj, { name: "A" });
- const b = notebook.add(BasicObj, { name: "B" });
+ const a = notebook.add(BasicObj, { label: "A" });
+ const b = notebook.add(BasicObj, { label: "B" });
//@ts-expect-error BothObjectsShape can never support CocartesianListMor.
if (notebook.supports(CocartesianListMor)) {
//@ts-expect-error BothObjectsShape does not support CocartesianListMor.
- notebook.add(CocartesianListMor, { name: "L", from: [a, b], to: [b] });
+ notebook.add(CocartesianListMor, { label: "L", from: [a, b], to: [b] });
}
}
```
@@ -2184,7 +2184,7 @@ const GenericShape = defineShape({
});
function ObListEditor(props: { objects: BasicObCell[] }) {
- return [{props.objects.map((place) => place.name).join(", ")}];
+ return [{props.objects.map((place) => place.label).join(", ")}];
}
function MorphismCellEditor(props: {
@@ -2207,7 +2207,7 @@ function MorphismCellEditor(props: {
Transition: ->
- {props.morphism.name}
+ {props.morphism.label}
@@ -2225,7 +2225,7 @@ function ModelCellEditor(props: {
if (cell.kind === CellKind.Object) {
return (