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("A<")); + console.log("has B:", svg.includes(">B<")); +} +``` + +``` +is svg: true +has A: true +has B: true +``` + +#### Mass-action dynamics example + +
+ + Code example + + + + + +```ts +import { MassActionDynamics } from "catcolab-analyses"; +import { PetriNet, Place, Transition } from "catcolab-logics/petri-net"; +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" }); + +petriNet.add(Transition, { name: "infection", from: [susceptible, infected], to: [infected] }); + +const analysis = await binder.createNotebook(PetriNet.Analysis, { + name: "Petri net analysis", + of: petriNet, +}); + +const sim = analysis.add(MassActionDynamics); +``` + +```ts +console.log("analysis id:", sim.type.id); +console.log("duration:", sim.params.duration); +``` + +``` +analysis id: mass-action +duration: 10 +``` + + + +```ts +sim.update({ duration: 3, initialValues: { [susceptible.id]: 1 } }); +``` + +```ts +console.log("duration:", sim.params.duration); +console.log("S initial:", sim.params.initialValues[susceptible.id]); +``` + +``` +duration: 3 +S initial: 1 +``` + +```ts +const result = await sim.run(); +if (result.tag === "Ok") { + const { solution, latexEquations } = result.content; + if (solution.tag === "Ok") { + console.log("has times:", solution.content.time.length > 0); + console.log("states:", [...solution.content.states.keys()].length); + } + console.log("equations:", latexEquations.length > 0); +} +``` + +``` +has times: true +states: 2 +equations: true +``` + +
+ +### Type errors & runtime errors + +We make use of Typescript type checking to have confidence that our use of the +Notebook API makes sense and is free of misspellings or other issues +that would throw errors at runtime. + +Since the API can also be used from Javascript directly without type checking +we want runtime validation that inputs match our expected schema. + +At runtime invalid inputs: + +- Should _throw_ an exception, ideally carrying a Standard Schema compatible `issues` array, _if_ it is something Typescript would have caught +- Should return a `Result` type otherwise + +#### Type errors + + + + + +```ts +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" }); +const arrow = notebook.add(Aspect, { name: "has", from: source, to: target }); +``` + +Invalid shapes should be type errors. + +```ts +// @ts-expect-error Arrays are not valid endpoints in a simple olog. +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 }); +// @ts-expect-error Missing required fields. +notebook.add(Aspect, {}); +// null fields are allowed. +notebook.add(Aspect, { name: null, from: null, to: null }); +``` + +#### Further type errors + +
+ +Further type errors + +A mapping's endpoints must be entities, not attribute types: + + + +```ts +import { AttrType, Mapping, SimpleSchema } from "catcolab-logics/simple-schema"; +import { binder } from "catcolab-documents"; + +const schema = await binder.createNotebook(SimpleSchema, { name: "Example schema" }); + +const str = schema.add(AttrType, { name: "String" }); + +// @ts-expect-error A mapping's endpoints must be entities, not attribute types. +schema.add(Mapping, { + name: "bad", + from: str, + to: str, +}); +``` + +But adapt to the underlying logic: + + + + + +```ts +import { PetriNet, Place, Transition } from "catcolab-logics/petri-net"; +import { binder } from "catcolab-documents"; + +const notebook = await binder.createNotebook(PetriNet, { name: "Example Petri-net" }); + +const a = notebook.add(Place, { name: "A" }); + +const b = notebook.add(Place, { name: "B" }); + +const c = notebook.add(Place, { name: "C" }); + +notebook.add(Transition, { + name: "t1", + from: [a, b], + to: [c], +}); + +// @ts-expect-error Petri net transitions require arrays of places. +notebook.add(Transition, { + name: "bad", + from: a, + to: [c], +}); +``` + +
+ +#### Runtime errors + +The same invalid inputs that Typescript catches are also rejected at runtime. +Each one throws an error carrying a Standard Schema compatible `issues` array. + + + + + + + +```ts +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" }); +const arrow = notebook.add(Aspect, { name: "has", from: source, to: target }); +``` + +Invalid shapes throw at runtime, with a Standard Schema compatible `issues` array: + + + +```ts +try { + // Arrays are not valid endpoints in a simple olog. + arrow.update({ from: [source] }); +} catch (error) { + console.log("update:", error.issues?.map((issue) => issue.message).join("; ")); +} + +try { + // Arrays are not valid endpoints in a simple olog. + notebook.add(Aspect, { name: "bad", from: [source, target], to: target }); +} catch (error) { + console.log("add:", error.issues?.map((issue) => issue.message).join("; ")); +} + +try { + // Missing required fields. + notebook.add(Aspect, {}); +} catch (error) { + console.log("missing:", error.issues?.map((issue) => issue.message).join("; ")); +} + +// null fields are allowed. +notebook.add(Aspect, { name: 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) +``` + +#### Further runtime errors + +
+ +Further runtime errors + +A mapping's endpoints must be entities, not attribute types: + + + + + + + +```ts +import { AttrType, Mapping, SimpleSchema } from "catcolab-logics/simple-schema"; +import { binder } from "catcolab-documents"; + +const schema = await binder.createNotebook(SimpleSchema, { name: "Example schema" }); + +const str = schema.add(AttrType, { name: "String" }); +``` + + + +```ts +try { + schema.add(Mapping, { name: "bad", from: str, to: str }); +} catch (error) { + console.log("mapping:", error.issues?.map((issue) => issue.message).join("; ")); +} +``` + +``` +mapping: `from` must be an object cell of type Entity (was an object cell of type AttrType); `to` must be an object cell of type Entity (was an object cell of type AttrType) +``` + +Validation also adapts to the underlying logic: + + + + + + + +```ts +import { PetriNet, Place, Transition } from "catcolab-logics/petri-net"; +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" }); +``` + + + +```ts +try { + // Petri net transitions require arrays of places. + notebook.add(Transition, { name: "bad", from: a, to: [c] }); +} catch (error) { + console.log("transition:", error.issues?.map((issue) => issue.message).join("; ")); +} +``` + +``` +transition: `from` must be an array or null (was an object cell of type Object) +``` + +
+ +### Use with SolidJS and Automerge + +The `Binder` abstraction exsits to keep the API consistent while being able to +plug in custom backends. We offer the `createBinder` method and the +`DocumentStore` type to be able to do so. + + + +```ts +import type { Document, Link } from "catcolab-document-types"; +interface DocumentStore { + // An async function to create a document handle from inital data + createHandle(initialDoc: Document): Promise; + // An async function to get a document handle from a `Link` + getHandle(link: Link): Promise; + // Apply modifications to a handle. + changeDocument(handle: Handle, fn: (doc: Document) => void): void; + // Subscribe change callbacks to our store for `onChange`. Returns a + // function to unsubscribe. + subscribe(handle: Handle, callback: () => void): () => void; + // Copy values (with any proxies removed) + copyValue(handle: Handle, value: T): T; + // Get the link for a handle + linkForHandle(handle: Handle): Omit | undefined; + // Get a document view from a handle + getDocumentView(handle: Handle): Readonly; +} +``` + +#### SolidJS binder + +An example of a simple SolidJS binder: + + + + + +```ts +import { createEffect, createRoot } from "solid-js"; +import { createStore, reconcile, type SetStoreFunction, unwrap } from "solid-js/store"; +import { binder, createBinder, type DocumentStore } from "catcolab-documents"; +import { type Document } from "catcolab-document-types"; + +type SolidStoreHandle = { + draftDoc: Document; + docView: Document; + setDocView: SetStoreFunction; + listeners: Set<() => void>; +}; + +const solidStore: DocumentStore = { + async createHandle(initialDoc) { + const draftDoc = structuredClone(initialDoc as Document); + const [docView, setDocView] = createStore(initialDoc as Document); + return { draftDoc, docView, setDocView, listeners: new Set() }; + }, + changeDocument: (handle, fn) => { + fn(handle.draftDoc); + handle.setDocView(reconcile(structuredClone(handle.draftDoc), { key: "id" })); + for (const listener of Array.from(handle.listeners)) { + listener(); + } + }, + subscribe: (handle, callback) => { + handle.listeners.add(callback); + return () => { + handle.listeners.delete(callback); + }; + }, + copyValue: (_handle, value) => structuredClone(unwrap(value)), + getDocumentView: (handle) => handle.docView, + // Link resolution omitted for brevity. + getLinkForHandle: () => undefined, + getHandle: async () => undefined, +}; + +const solidBinder = createBinder(solidStore); +``` + +Which can be used just as the default binder. + +```ts +import { SimpleOlog } from "catcolab-logics/simple-olog"; +const notebook = await solidBinder.createNotebook(SimpleOlog, { name: "An Olog" }); +``` + +#### Automerge binder + +A simple Automerge binder: + + + + + +```ts +import { type Doc, getBackend, getObjectId } from "@automerge/automerge"; +import { type DocHandle, Repo } from "@automerge/automerge-repo"; +import { createBinder, type DocumentStore } from "catcolab-documents"; +import { type Document } from "catcolab-document-types"; + +const repo = new Repo(); + +const automergeStore: DocumentStore> = { + createHandle: async (initialDoc) => { + return repo.create(initialDoc as Document); + }, + changeDocument: (handle, fn) => handle.change(fn), + subscribe: (handle, callback) => { + const onChange = () => callback(); + handle.on("change", onChange); + return () => handle.off("change", onChange); + }, + copyValue: (handle, value) => { + const doc = handle.doc(); + const objId = getObjectId(value as object); + return getBackend(doc).materialize(objId!) as typeof value; + }, + getDocumentView: (handle) => handle.doc(), + // Link resolution omitted for brevity. + getLinkForHandle: () => undefined, + getHandle: async () => undefined, +}; + +const automergeBinder = createBinder(automergeStore); +``` + +```ts +import { SimpleOlog } from "catcolab-logics/simple-olog"; +const notebook = await automergeBinder.createNotebook(SimpleOlog, { name: "An Olog" }); +``` + +#### An Automerge and SolidJS binder for our backend + +Here is an approximation of how we can define a binder to work with our backend +and frontend using Automerge, SolidJS and our RPC client (mocked here as +`FakeBackend`). + + + + + +```ts +import { getBackend, getObjectId } from "@automerge/automerge"; +import { type DocHandle, type DocumentId } from "@automerge/automerge-repo"; +import { makeDocumentProjection } from "@automerge/automerge-repo-solid-primitives"; +import { createBinder, type DocumentStore, Instantiation } from "catcolab-documents"; +import { SimpleOlog, Type } from "catcolab-logics/simple-olog"; + +import type { Document } from "catcolab-document-types"; +import { FakeBackend } from "document-methods/test/fake_backend"; + +const backend = new FakeBackend(); +const repo = backend.repo; + +type StoreHandle = { + docHandle: DocHandle; + docView: Document; +}; + +const makeHandle = (docHandle: DocHandle): StoreHandle => ({ + docHandle, + docView: makeDocumentProjection(docHandle), +}); + +const refByDocId = new Map(); +const handleByRefId = new Map(); + +const backendStore: DocumentStore = { + createHandle: async (initialDoc: Document) => { + const created = await backend.new_ref(initialDoc); + if (created.tag !== "Ok") { + throw new Error(created.message); + } + const refId = created.content; + + const fetched = await backend.get_doc(refId); + if (fetched.tag !== "Ok") { + throw new Error(fetched.message); + } + const docHandle = await repo.find(fetched.content.docId as DocumentId); + const handle = makeHandle(docHandle); + + refByDocId.set(docHandle.documentId, refId); + handleByRefId.set(refId, handle); + return handle; + }, + 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 doc = handle.docHandle.doc(); + const objId = getObjectId(value as object); + return getBackend(doc).materialize(objId!) as typeof value; + }, + getLinkForHandle: (handle) => { + const refId = refByDocId.get(handle.docHandle.documentId); + if (!refId) { + return undefined; + } + return { _id: refId, _version: null, _server: backend.serverHost }; + }, + getHandle: async (link) => { + const refId = link._id; + const cached = handleByRefId.get(refId); + if (cached) { + return cached; + } + + const result = await backend.get_doc(refId); + if (result.tag !== "Ok") { + return undefined; + } + + const docHandle = await repo.find(result.content.docId as DocumentId); + const handle = makeHandle(docHandle); + handleByRefId.set(refId, handle); + refByDocId.set(docHandle.documentId, refId); + return handle; + }, +}; + +const backendBinder = createBinder(backendStore); +``` + +
+ +Code verification + +```ts +const imported = await backendBinder.createNotebook(SimpleOlog, { name: "Imported" }); +imported.add(Type, { name: "Thing" }); + +const notebook = await backendBinder.createNotebook(SimpleOlog, { name: "Main" }); +notebook.add(Type, { name: "A" }); +notebook.add(Instantiation, { name: "ImportedOlog", model: imported }); + +const result = await notebook.validate(); +console.log(result.tag); +``` + +``` +Ok +``` + +
+ +### Defining notebook shapes + +#### A logic + +We provide some utilities for defining a logic: `defineMorphism`, +`defineObject` and `defineShape`. Here is what the definition of the `PetriNet` +logic looks like: + + + +```ts +import { MassActionDynamics, Visualization } from "catcolab-analyses"; +import { defineMorphism, defineObject, defineShape } from "catcolab-documents"; + +export const Place = defineObject({ tag: "Basic", content: "Object" }); +export const Transition = defineMorphism( + { tag: "Hom", content: Place.obType }, + { + domain: { apply: { tag: "Basic", content: "tensor" }, modality: "SymmetricList" }, + codomain: { apply: { tag: "Basic", content: "tensor" }, modality: "SymmetricList" }, + }, +); + +export const PetriNet = defineShape({ + theory: "petri-net", + getCoreTheory: async () => { + const { ThSymMonoidalCategory } = await import("catlog-wasm"); + return new ThSymMonoidalCategory().theory(); + }, + objects: [Place], + morphisms: [Transition], + modelAnalyses: [Visualization, MassActionDynamics /*, ... */], +}); +``` + +To illustrate how migrations are defined here is the definition of `SimpleSchema`. + + + + + +```ts +import { defineMorphism, defineObject, defineShape, RichText } from "catcolab-documents"; + +export const Entity = defineObject({ tag: "Basic", content: "Entity" }); +export const AttrType = defineObject({ tag: "Basic", content: "AttrType" }); + +export const Mapping = defineMorphism({ tag: "Hom", content: Entity.obType }); +export const Attr = defineMorphism( + { tag: "Basic", content: "Attr" }, + { domain: Entity.obType, codomain: AttrType.obType }, +); + +export const SimpleSchema = defineShape({ + theory: "simple-schema", + getCoreTheory: async () => { + const { ThSchema } = await import("catlog-wasm"); + return new ThSchema().theory(); + }, + objects: [Entity, AttrType], + morphisms: [Mapping, Attr], + supportsInstances: true, + informal: [RichText], + analyses: [ + /* ... */ + ], + migrations: [ + { + target: "simple-olog", + migrate: async (model, targetTheory) => { + const { ThSchema } = await import("catlog-wasm"); + return ThSchema.toCategory(model, targetTheory); + }, + }, + ], +}); +``` + +`supportsIntances` is a boolean flag that indicates whether the notebook +supports diagrams. When included the `.Diagram` is generated for the logic. See +[Diagram notebooks](#diagram-notebooks) for using the `.Diagram` shape. + +#### Analysis definitions + +For analysis cell definitions we can use the `defineAnalysis` helper. Here is +an example for `MassActionDynamics`. + + + + + +```ts +import { defineAnalysis } from "catcolab-documents"; +import type { + MassActionProblemData, + ODEResultWithEquations, + ThSymMonoidalCategory, + DblModel, +} from "catlog-wasm"; + +let thSymMonoidalCategory: Promise | undefined; + +function getCachedTheory(): Promise { + if (thSymMonoidalCategory === undefined) { + thSymMonoidalCategory = import("catlog-wasm").then( + ({ ThSymMonoidalCategory }) => new ThSymMonoidalCategory(), + ); + } + return thSymMonoidalCategory; +} + +export const MassActionDynamics = defineAnalysis({ + id: "mass-action", + getInitialParams: (): MassActionProblemData => ({ + massConservationType: { type: "Balanced" }, + rates: {}, + transitionProductionRates: {}, + transitionConsumptionRates: {}, + placeProductionRates: {}, + placeConsumptionRates: {}, + initialValues: {}, + duration: 10, + }), + run: async ( + model: DblModel, + params: MassActionProblemData, + ): Promise => { + const th = await getCachedTheory(); + return th.massAction(model, params); + }, +}); +``` + +#### Generic shapes of notebooks + +Using `defineShape` we don't have to define a complete notebook. We can define +shapes that are subsets of a notebook and/or span across different notebooks. + +Here is an example of a shape that only deals with basic objects: + + + + + +```ts +import { defineObject, defineShape } from "catcolab-documents"; + +const BasicObj = defineObject({ tag: "Basic", content: "Object" }); + +const OnlyBasicObj = defineShape({ + objects: [BasicObj], +}); +``` + +Because `OnlyBasicObj` is a subset of both the olog and petri-net logics (they +share the basic `Object`), we can write a generic function against it that +works with notebooks from either logic. + + + +```ts +import { binder, type Notebook } from "catcolab-documents"; +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" }); +} +``` + +Both an olog and a petri-net notebook support `BasicObj`, so `addObjects` adds +to them: + +```ts +const olog = await binder.createNotebook(SimpleOlog, { name: "olog" }); +const petriNet = await binder.createNotebook(PetriNet, { name: "petri-net" }); + +addObjects(olog); +addObjects(petriNet); +``` + +A schema notebook has no basic `Object` so we get a type error. + +```ts +import { SimpleSchema } from "catcolab-logics/simple-schema"; + +const schema = await binder.createNotebook(SimpleSchema, { name: "schema" }); + +// @ts-expect-error A SimpleSchema notebook lacks the basic `Object` that OnlyBasicObj requires. +addObjects(schema); +``` + +We can pass a union of shapes to `Notebook` but we need to use `supports` to +narrow the type to confirm it supports adding cells of a particular shape. + + + + + +```ts +import { defineObject, defineShape, defineMorphism } from "catcolab-documents"; +import type { Notebook } from "catcolab-documents"; + +const BasicObj = defineObject({ tag: "Basic", content: "Object" }); + +const OnlyBasicObj = defineShape({ + objects: [BasicObj], +}); + +const EntityObj = defineObject({ tag: "Basic", content: "Entity" }); +const OnlyEntityObj = defineShape({ + objects: [EntityObj], +}); +``` + +```ts +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" }); + + // This is ok because we narrowed the notebook type. + if (notebook.supports(BasicObj)) { + notebook.add(BasicObj, { name: "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" }); + + // This is ok because we narrowed the notebook type. + if (notebook.supports(EntityObj)) { + notebook.add(EntityObj, { name: "D" }); + } +} +``` + +`supports` can also take a shape as an argument. + +```ts +export const Morphism = defineMorphism({ + tag: "Hom", + content: EntityObj.obType, +}); +const EntityWithMor = defineShape({ + objects: [EntityObj], + morphisms: [Morphism], +}); +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" }); + + // This is ok because we narrowed the notebook type. + if (notebook.supports(OnlyBasicObj)) { + notebook.add(BasicObj, { name: "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" }); + + // 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 }); + } +} +``` + + + + + +`cellsOf` takes `Shape` as an alternative to a cell definition as an argument +to filter by. `cellsOf` narrows the cell shape type in a similar way to `supports`. + +```ts +import { SimpleSchema, Entity, AttrType, Mapping, Attr } from "catcolab-logics/simple-schema"; +import { binder, defineShape } from "catcolab-documents"; + +const EntityShape = defineShape({ + objects: [Entity], + morphisms: [Mapping], +}); + +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 }); + +for (const cell of schema.cellsOf(EntityShape)) { + console.log(cell.name); +} +``` + +``` +A +C +g +``` + +#### Further shape examples + +##### Implementing a list shape + +Here is a complete example that accepts morphisms with lists as domain and +codomain of all the variants we currently have defined. + +
+ +Code example + + + + + +```ts +import { defineMorphism, defineObject, defineShape, type Notebook } from "catcolab-documents"; + +const BasicObj = defineObject({ tag: "Basic", content: "Object" }); + +const tensor = { tag: "Basic", content: "tensor" } as const; + +const ListMor = defineMorphism( + { tag: "Hom", content: BasicObj.obType }, + { + domain: { apply: tensor, modality: "List" }, + codomain: { apply: tensor, modality: "List" }, + }, +); + +const ListShape = defineShape({ + objects: [BasicObj], + morphisms: [ListMor], +}); + +const SymmetricListMor = defineMorphism( + { tag: "Hom", content: BasicObj.obType }, + { + domain: { apply: tensor, modality: "SymmetricList" }, + codomain: { apply: tensor, modality: "SymmetricList" }, + }, +); +const SymmetricListShape = defineShape({ + objects: [BasicObj], + morphisms: [SymmetricListMor], +}); + +const CocartesianListMor = defineMorphism( + { tag: "Hom", content: BasicObj.obType }, + { + domain: { apply: tensor, modality: "CocartesianList" }, + codomain: { apply: tensor, modality: "CocartesianList" }, + }, +); + +const CocartesianListShape = defineShape({ + objects: [BasicObj], + morphisms: [CocartesianListMor], +}); + +const CartesianListMor = defineMorphism( + { tag: "Hom", content: BasicObj.obType }, + { + domain: { apply: tensor, modality: "CartesianList" }, + codomain: { apply: tensor, modality: "CartesianList" }, + }, +); + +const CartesianListShape = defineShape({ + objects: [BasicObj], + morphisms: [CartesianListMor], +}); + +const AdditiveListMor = defineMorphism( + { tag: "Hom", content: BasicObj.obType }, + { + domain: { apply: tensor, modality: "AdditiveList" }, + codomain: { apply: tensor, modality: "AdditiveList" }, + }, +); + +const AdditiveListShape = defineShape({ + objects: [BasicObj], + morphisms: [AdditiveListMor], +}); +``` + + + +`addListMorphism` works on any notebook that supports any of the morphisms our +list shapes support. When implementing a generic consumer like this we need to +narrow down what object and morphism types the notebook actually supports by +using `notebook.supports`. + +```ts +type NotebookOfLists = Notebook< + | typeof ListShape + | typeof SymmetricListShape + | typeof CocartesianListShape + | typeof CartesianListShape + | typeof AdditiveListShape +>; + +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" }); + + if (notebook.supports(ListMor)) { + notebook.add(ListMor, { name: "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] }); + } else if (notebook.supports(CocartesianListMor)) { + notebook.add(CocartesianListMor, { name: "L", from: [a, b], to: [c] }); + } else if (notebook.supports(CartesianListMor)) { + notebook.add(CartesianListMor, { name: "L", from: [a, b], to: [c] }); + } else if (notebook.supports(AdditiveListMor)) { + notebook.add(AdditiveListMor, { name: "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."); + } +} +``` + +```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" }); + + //@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] }); +} +``` + +
+ +##### A structurally compatible notebook is accepted and the appropriate morphism is added + +
+ +Code example + + + +```ts +import { binder } from "catcolab-documents"; +import { PetriNet } from "catcolab-logics/petri-net"; + +const petriNet = await binder.createNotebook(PetriNet, { name: "example" }); + +addListMorphism(petriNet); +``` + +``` +Adding SymmetricListMor! +``` + +```ts +const entityObType = defineObject({ tag: "Basic", content: "Entity" }); +const EntityObjectShape = defineShape({ + theory: "entity-objects", + objects: [entityObType, BasicObj], + morphisms: [ListMor], +}); + +const entityObjects = await binder.createNotebook(EntityObjectShape, { name: "example" }); + +addListMorphism(entityObjects); +``` + +
+ +##### A structurally incompatible notebook should be rejected + +
+ +Code examples + +```ts +import { SimpleOlog } from "catcolab-logics/simple-olog"; + +const simpleOlog = await binder.createNotebook(SimpleOlog, { name: "example" }); + +// @ts-expect-error A SimpleOlog notebook lacks the list-valued morphisms ListShape requires. +addListMorphism(simpleOlog); +``` + +```ts +const JustObjectShape = defineShape({ + theory: "just-objects", + objects: [BasicObj], +}); + +const justObjects = await binder.createNotebook(JustObjectShape, { name: "example" }); + +// @ts-expect-error We have no morphisms in `JustObjectShape`. +addListMorphism(justObjects); +``` + +```ts +const JustMorphismShape = defineShape({ + theory: "just-morphisms", + morphisms: [ListMor], +}); + +const justMorphisms = await binder.createNotebook(JustMorphismShape, { name: "example" }); + +// @ts-expect-error We have no objects in `JustMorphismShape`. +addListMorphism(justMorphisms); +``` + +```ts +const EntityObj = defineObject({ tag: "Basic", content: "Entity" }); + +const EntityListMor = defineMorphism( + { tag: "Hom", content: EntityObj.obType }, + { + domain: { apply: tensor, modality: "List" }, + codomain: { apply: tensor, modality: "List" }, + }, +); + +const MultiObjectListShape = defineShape({ + objects: [BasicObj, EntityObj], + morphisms: [ListMor, EntityListMor], +}); + +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" }); + + notebook.add(ListMor, { name: "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] }); +} +``` + +```ts +const entityObType = defineObject({ tag: "Basic", content: "Entity" }); + +const entityListMorType = defineMorphism( + { tag: "Hom", content: entityObType.obType }, + { + domain: { apply: tensor, modality: "List" }, + codomain: { apply: tensor, modality: "List" }, + }, +); + +const EntityObjectListShape = defineShape({ + objects: [entityObType], + morphisms: [entityListMorType], +}); + +type NotebookOfListsWithEntity = Notebook< + | typeof ListShape + | typeof SymmetricListShape + | typeof CocartesianListShape + | typeof CartesianListShape + | typeof AdditiveListShape + | typeof EntityObjectListShape +>; + +const EntityObj = entityObType; + +function goodAddObject(notebook: NotebookOfListsWithEntity) { + if (notebook.supports(BasicObj)) { + notebook.add(BasicObj, { name: "A" }); + } + + if (notebook.supports(EntityObj)) { + notebook.add(EntityObj, { name: "E" }); + } +} + +const BothObjectsShape = defineShape({ + objects: [BasicObj, entityObType], +}); + +function goodAddObject2(notebook: NotebookOfListsWithEntity) { + if (notebook.supports(BothObjectsShape)) { + notebook.add(BasicObj, { name: "A" }); + notebook.add(EntityObj, { name: "E" }); + } +} + +type JustEntityObjectListShape = Notebook; + +function goodAddObject3(notebook: JustEntityObjectListShape) { + notebook.add(EntityObj, { name: "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" }); + + //@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" }); +} + +function badAddObject2(notebook: Notebook) { + const a = notebook.add(BasicObj, { name: "A" }); + const b = notebook.add(BasicObj, { name: "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] }); + } +} +``` + +
+ +#### Use with SolidJS + +##### Shape consumer + +We can define the shape our components consume. + +
+ +Code example + + + + + +```tsx +import { + type NotebookCell, + CellKind, + createBinder, + defineMorphism, + defineObject, + type DocumentStore, + type Notebook, + defineShape, + RichText, +} from "catcolab-documents"; +import { PetriNet, Place, Transition } from "catcolab-logics/petri-net"; +import { For } from "solid-js"; +import { createStore, reconcile, type SetStoreFunction, unwrap } from "solid-js/store"; +import { render } from "solid-js/web"; + +import type { Document } from "catcolab-document-types"; + +type SolidStoreHandle = { + draftDoc: Document; + docView: Document; + setDocView: SetStoreFunction; + listeners: Set<() => void>; +}; + +const solidStore: DocumentStore = { + async createHandle(initialDoc) { + const draftDoc = structuredClone(initialDoc as Document); + const [docView, setDocView] = createStore(initialDoc as Document); + return { draftDoc, docView, setDocView, listeners: new Set() }; + }, + getDocumentView: (handle) => handle.docView, + changeDocument: (handle, fn) => { + fn(handle.draftDoc); + handle.setDocView(reconcile(structuredClone(handle.draftDoc), { key: "id" })); + for (const listener of Array.from(handle.listeners)) { + listener(); + } + }, + subscribe: (handle, callback) => { + handle.listeners.add(callback); + return () => { + handle.listeners.delete(callback); + }; + }, + copyValue: (_handle, value) => structuredClone(unwrap(value)), + // Link resolution omitted for brevity. + getLinkForHandle: () => undefined, + getHandle: async () => undefined, +}; + +const solidBinder = createBinder(solidStore); + +const basicObject = defineObject({ tag: "Basic", content: "Object" }); +const symmetricListMorphism = defineMorphism( + { tag: "Hom", content: basicObject.obType }, + { + domain: { apply: { tag: "Basic", content: "tensor" }, modality: "SymmetricList" }, + codomain: { apply: { tag: "Basic", content: "tensor" }, modality: "SymmetricList" }, + }, +); + +type BasicObCell = NotebookCell; +type SymmetricListCell = NotebookCell; + +const GenericShape = defineShape({ + objects: [basicObject], + morphisms: [symmetricListMorphism], + informal: [RichText], +}); + +function ObListEditor(props: { objects: BasicObCell[] }) { + return [{props.objects.map((place) => place.name).join(", ")}]; +} + +function MorphismCellEditor(props: { + notebook: Notebook; + morphism: SymmetricListCell; +}) { + // Contrived test example: adding an arbitrary but valid input place + const runTestMutation = () => { + const referenced = new Set( + [...props.morphism.from, ...props.morphism.to].map((ob) => ob.id), + ); + const input = props.notebook.cellsOf(basicObject).find((ob) => !referenced.has(ob.id)); + if (input) { + props.morphism.update({ from: [...props.morphism.from, input] }); + } + }; + return ( +
  • + + Transition: + -> + + {props.morphism.name} + +
  • + ); +} + +function ModelCellEditor(props: { + notebook: Notebook; + cell: NotebookCell; +}) { + const cell = props.cell; + if (cell.kind === CellKind.Morphism) { + return ; + } + if (cell.kind === CellKind.Object) { + return ( +
  • + Place: {cell.name} +
  • + ); + } + if (cell.kind === CellKind.RichText) { + return ( +
  • + Text: {cell.content} +
  • + ); + } + return null; +} + +function ModelNotebookEditor(props: { notebook: Notebook }) { + return ( +
    +

    {props.notebook.name}

    +
      + + {(cell) => } + +
    +
    + ); +} +``` + +```tsx +const notebook = await solidBinder.createNotebook(PetriNet, { name: "Petri net" }); +const a = notebook.add(Place, { name: "A" }); +notebook.add(Place, { name: "B" }); +const c = notebook.add(Place, { name: "C" }); +notebook.add(Transition, { name: "fires", from: [a], to: [c] }); + +const container = document.createElement("div"); +document.body.appendChild(container); + +const dispose = render(() => , container); + +console.log(container.innerHTML); + +const appendButton = container.querySelector( + '[aria-label="run test mutation"]', +)!; +appendButton.click(); +console.log(container.innerHTML); + +dispose(); +``` + +``` +

    Petri net

    • Place: A
    • Place: B
    • Place: C
    • Transition: [A] -> [C]fires
    +

    Petri net

    • Place: A
    • Place: B
    • Place: C
    • Transition: [A, B] -> [C]fires
    +``` + +
    + +##### SolidJS example with validation & completions + +Wiring in the validated model through the components for completions looks +something like this, using `onValidate` to feed a signal with validation +results. Future work could be to bring the validated model API closer to our +notebook API. + +
    + + Code example + + + + + +```tsx +import { + type NotebookCell, + CellKind, + createBinder, + defineMorphism, + defineObject, + defineShape, + type DocumentStore, + type ModelValidationResult, + type Notebook, + RichText, + type ValidatableNotebook, +} from "catcolab-documents"; +import { + createContext, + createSignal, + For, + onCleanup, + Show, + useContext, + type Accessor, +} from "solid-js"; +import { createStore, reconcile, type SetStoreFunction, unwrap } from "solid-js/store"; +import { render } from "solid-js/web"; + +import type { Document } from "catcolab-document-types"; +import type { DblModel, ObType, QualifiedName } from "catlog-wasm"; +import { selfResolving } from "document-methods/test/self_resolving"; + +type SolidStoreHandle = { + draftDoc: Document; + docView: Document; + setDocView: SetStoreFunction; + listeners: Set<() => void>; +}; + +const solidStore: DocumentStore = { + async createHandle(initialDoc) { + const draftDoc = structuredClone(initialDoc as Document); + const [docView, setDocView] = createStore(initialDoc as Document); + return { draftDoc, docView, setDocView, listeners: new Set() }; + }, + getDocumentView: (handle) => handle.docView, + changeDocument: (handle, fn) => { + fn(handle.draftDoc); + handle.setDocView(reconcile(structuredClone(handle.draftDoc), { key: "id" })); + for (const listener of Array.from(handle.listeners)) { + listener(); + } + }, + subscribe: (handle, callback) => { + handle.listeners.add(callback); + return () => { + handle.listeners.delete(callback); + }; + }, + copyValue: (_handle, value) => structuredClone(unwrap(value)), + ...selfResolving(), +}; + +const solidBinder = createBinder(solidStore); +``` + + + +```tsx +const MyEntity = defineObject({ tag: "Basic", content: "Entity" }); +const MyAttrType = defineObject({ tag: "Basic", content: "AttrType" }); +const MyAttr = defineMorphism( + { tag: "Basic", content: "Attr" }, + { domain: MyEntity.obType, codomain: MyAttrType.obType }, +); + +const MyShape = defineShape({ + objects: [MyEntity, MyAttrType], + morphisms: [MyAttr], + informal: [RichText], +}); + +type MyNotebook = Notebook; +``` + + + +```tsx +function label(model: DblModel, id: QualifiedName): string { + return model.obGeneratorLabel(id)?.join(".") ?? "?"; +} + +function filteredCompletions(model: DblModel, obType: ObType, text: string): QualifiedName[] { + const needle = text.toLowerCase(); + return model + .obGeneratorsWithType(obType) + .filter((id) => label(model, id).toLowerCase().includes(needle)); +} + +function codomainLabel(model: DblModel | undefined, morphism: NotebookCell): string { + if (!model) { + return "?"; + } + const cod = model?.morPresentation(morphism.id)?.cod; + return cod?.tag === "Basic" ? label(model, cod.content) : "?"; +} +``` + + + +```tsx +type MyContextValue = { + notebook: MyNotebook; + model: Accessor; +}; + +const MyContext = createContext(); + +function useMyContext() { + const context = useContext(MyContext); + if (!context) { + throw new Error("Schema editor context is missing."); + } + return context; +} + +function selectCodomain( + notebook: MyNotebook, + attrCell: NotebookCell, + id: QualifiedName, +) { + const result = notebook.get(MyAttrType, id); + if (result.tag === "Ok") { + attrCell.update({ to: result.content }); + } +} + +function CompletionPicker(props: { + obType: ObType; + selected: string; + text: Accessor; + onSelect: (id: QualifiedName) => void; +}) { + const context = useMyContext(); + return ( + + {props.selected} +
      + + {(id) => ( +
    • props.onSelect(id)}>{label(context.model(), id)}
    • + )} +
      +
    +
    + ); +} + +function MyAttrCellEditor(props: { + attrCell: NotebookCell; + text: Accessor; +}) { + const context = useMyContext(); + return ( +
  • + Attr: {props.attrCell.name}: {props.attrCell.from.name} ->{" "} + selectCodomain(context.notebook, props.attrCell, id)} + /> +
  • + ); +} + +function MyCellEditor(props: { cell: NotebookCell; text: Accessor }) { + if (props.cell.kind === CellKind.Morphism) { + return ; + } + if (props.cell.kind === CellKind.Object) { + const kind = props.cell.type === MyAttrType ? "MyAttrType" : "MyEntity"; + return ( +
  • + {kind}: {props.cell.name} +
  • + ); + } + if (props.cell.kind === CellKind.RichText) { + return
  • Text: {props.cell.content}
  • ; + } + return null; +} + +// Globals for testing +let testCurrentModel!: () => DblModel | undefined; + +function MyNotebookEditor(props: { + notebook: MyNotebook & ValidatableNotebook; + text: Accessor; +}) { + // `onValidate` delivers an initial result and then re-validates whenever + // anything the validation depends on changes, notifying only when the + // result actually changed. + const [validation, setValidation] = createSignal(); + const unsubscribe = props.notebook.onValidate(setValidation); + onCleanup(unsubscribe); + + const model = () => { + const result = validation(); + return result?.tag === "Ok" ? result.content : undefined; + }; + + testCurrentModel = model; + + return ( +
    +

    {props.notebook.name}

    + Validating...

    }> + {(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}