Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions lib/loop.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ import { units, ESTIMATE } from "./meter.mjs";
* A picture the host drew, as the `diagram` event and the note carry it. diagramOf checks the
* source, each node's three keys, each link and each row of everyType; the shape, the title, the
* count left out, a node's type and the count of edges are the host's, held by the diagram tool's
* output schema. A lifecycle's `transitions` are checked too, since a state is no node a link joins,
* output schema. A picture of the schemas' `unit` and each row of its `units` are checked as well. A lifecycle's `transitions` are checked too, since a state is no node a link joins,
* and a scenario's `steps` and `extensions`, since a step is a box and no node.
* @typedef {{ aggregate: string; from: string | null; to: string; command: string | null }} Transition
* @typedef {{ step: string; actor: string; action: string }} Step
* @typedef {{ step: string; condition: string; handling: string }} Extension
* @typedef {{ shape: string; title: string | null; mermaid: string; nodes: { node: string; id: string; title: string; type?: string }[]; omitted: number; links: { from: string; to: string; label: string }[]; everyType?: { via: string; to: string; multiplicity: string }[]; core?: string | null; transitions?: Transition[]; steps?: Step[]; extensions?: Extension[]; actor?: string; edges?: number }} Diagram
* @typedef {{ shape: string; title: string | null; mermaid: string; nodes: { node: string; id: string; title: string; type?: string }[]; omitted: number; links: { from: string; to: string; label: string }[]; everyType?: { via: string; to: string; multiplicity: string }[]; core?: string | null; unit?: string; units?: { unit: string; types: number }[]; transitions?: Transition[]; steps?: Step[]; extensions?: Extension[]; actor?: string; edges?: number }} Diagram
*/
// get_entity answers the entity under `entity`, fetch answers it flat with `title`, and
// find_evidence answers the skill the evidence is for under `skill`. Each of the three is one
Expand Down Expand Up @@ -165,6 +165,10 @@ export function diagramOf(name, r) {
const links = Array.isArray(d.links) ? d.links.filter((/** @type {any} */ l) => l && kept.has(l.from) && kept.has(l.to) && typeof l.label === "string") : [];
const everyType = Array.isArray(d.everyType) ? d.everyType.filter((/** @type {any} */ x) => x && typeof x.via === "string" && typeof x.to === "string" && typeof x.multiplicity === "string") : [];
const core = typeof d.model?.core === "string" ? d.model.core : null;
// The unit of the meta-model a picture of the schemas draws, where it draws one, and every
// unit there is with how many types it declares.
const unit = typeof d.unit === "string" && d.unit ? d.unit : null;
const units = Array.isArray(d.units) ? d.units.filter((/** @type {any} */ u) => u && typeof u.unit === "string" && typeof u.types === "number") : [];
/** @param {unknown} v */
const blankOr = (v) => v == null || typeof v === "string";
const transitions = Array.isArray(d.transitions) ? d.transitions.filter((/** @type {any} */ t) => t && typeof t.aggregate === "string" && typeof t.to === "string" && blankOr(t.from) && blankOr(t.command)) : [];
Expand All @@ -173,7 +177,7 @@ export function diagramOf(name, r) {
const steps = /** @type {Step[]} */ (Array.isArray(d.steps) ? d.steps.filter((/** @type {unknown} */ x) => allStrings(["step", "actor", "action"], x)) : []);
const extensions = /** @type {Extension[]} */ (Array.isArray(d.extensions) ? d.extensions.filter((/** @type {unknown} */ x) => allStrings(["step", "condition", "handling"], x)) : []);
const actor = typeof d.actor === "string" ? d.actor : "";
return { shape: d.shape, title: d.title ?? null, mermaid: d.mermaid, nodes, omitted: d.omitted ?? 0, links, ...(d.shape === "schema" ? { everyType, core } : {}), ...(d.shape === "lifecycle" ? { transitions } : {}), ...(d.shape === "scenario" ? { steps, extensions } : {}), ...(d.shape === "usecase" ? { actor } : {}) };
return { shape: d.shape, title: d.title ?? null, mermaid: d.mermaid, nodes, omitted: d.omitted ?? 0, links, ...(d.shape === "schema" ? { everyType, core, ...(unit ? { unit } : {}), units } : {}), ...(d.shape === "lifecycle" ? { transitions } : {}), ...(d.shape === "scenario" ? { steps, extensions } : {}), ...(d.shape === "usecase" ? { actor } : {}) };
}

// What a flow's and a lifecycle's note ask of each relation: its says, said in words.
Expand Down Expand Up @@ -308,8 +312,8 @@ export const diagramNote = (d) => {
// A picture of the schemas: the types each node is, the core they are declared in, and what
// every other type declares, said once, since the picture draws no arrow for it.
...(d.shape === "schema" ? {
schemas: `Each node is a type the schemas of core ${d.core ?? "(version not named)"} declare, and each relation a reference its schema declares, labeled with the field and how many one entity may hold; nested-in joins an owned type to its owner. Name the core version once. The picture is the answer: write two or three sentences, naming a few of the types and relations that carry its structure, never a list of every relation. In German core stays core, never Kern, and a schema deklariert, never erklärt.${d.everyType?.length ? " everyType lists what every other type also declares, which the picture leaves out: say it once, in a sentence of its own." : ""}`,
everyType: d.everyType ?? [],
schemas: `Each node is a type the schemas of core ${d.core ?? "(version not named)"} declare, and each relation a reference its schema declares, labeled with the field and how many one entity may hold; nested-in joins an owned type to its owner. Name the core version once. The picture is the answer: write two or three sentences, naming a few of the types and relations that carry its structure, never a list of every relation. In German core stays core, never Kern, and a schema deklariert, never erklärt.${d.everyType?.length ? " everyType lists what every other type also declares, which the picture leaves out: say it once, in a sentence of its own." : ""}${d.unit ? ` This picture draws one unit of the meta-model, ${d.unit}, ${d.unit === "core" ? "core alone" : "the pack's own types with the core types they declare to"}, because the meta-model is drawn a unit at a time; units lists every unit with how many types it declares. Say which unit this is, and name the other units as what can be shown next, a pack by its name and the word pack; where the visitor asked for the meta-model as a whole, say in one sentence that it is drawn a unit at a time.` : ""}`,
everyType: d.everyType ?? [], ...(d.unit ? { unit: d.unit, units: d.units ?? [] } : {}),
} : {}),
// A picture of a context map: each relation comes with the sentence that states it, so the model
// repeats that sentence and never decodes a direction.
Expand Down Expand Up @@ -501,7 +505,7 @@ export async function answer({ host, model, meter, questionCap = DEFAULT_QUESTIO
}
if (names.length) emit("names", { names });
if (verdict) heard(found, given);
if (picture) { const { links, everyType, core, transitions, steps, extensions, actor, ...event } = picture; emit("diagram", event); }
if (picture) { const { links, everyType, core, unit, units, transitions, steps, extensions, actor, ...event } = picture; emit("diagram", event); }
results.push({ type: "tool_result", tool_use_id: call.id, content: given, is_error: r.isError });
}
// The naming note follows the tool answers, so the model reads it just before it writes.
Expand Down
2 changes: 1 addition & 1 deletion lib/prompt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const KIND_RULE = "A question about the questions this model answers, whi
// that hold the same, as a context is asked for as four; a system that holds nothing gets the
// landscape alone and one sentence, since what is missing is rows. Which system masters a concept
// is a question the pages answer in a line, and a picture of one system cannot.
export const DIAGRAM_RULE = "A visitor who asks to see how the concepts relate, how a process runs, or how something connects is shown a picture: call diagram, with shape concepts, process, neighborhood, context, aggregate, flow, lifecycle, organization, system, holds, usecase, scenario or requirements; one who asks to see the meta-model, the schemas or the types the model is written in, or what it is based on, is shown shape schema, with type where they name one, not shape concepts; the meta-model is always this model's schemas, so a question about what it is gets that answer. One who asks to see a bounded context, or the diagrams of one, is shown shape context, then aggregate, then flow, then lifecycle, all with the context's id, and the answer names what each drew; where aggregate refuses as empty, the map stands alone and the answer says only that the context holds no aggregate; otherwise, where flow or lifecycle refuses as empty, the answer says in one sentence that the model does not describe that flow or lifecycle yet. One who asks for the sequence or the lifecycle of a context or an aggregate is shown that shape alone, flow for the sequence, with its id. One who asks how the company is organized or for an org chart is shown shape organization, with a group's id where they name one, then calls get_entity on every group it drew, and the answer names each group's lead, its members and its open positions as those pages give them, never as read off the picture or its lines; where its omitted is above zero, the answer says in one sentence that the groups outside the line are drawn when one is named. One who asks who leads a group or who works in one is answered from that group's page through get_entity, in words, and shown no picture unless they ask to see one. One who asks to see a system, or the diagram of one, what it connects to, what runs on it or what it runs on, or what data it holds, is shown shape system and then shape holds, both with the system's id, never shape neighborhood, since a system's connections are its landscape, and a name that is both a surface's and a system's is read as the system's, the surface named in one sentence; the answer names what each drew; where holds refuses as empty, the landscape stands alone and the answer says in one sentence that the model names no data the system holds. One who asks which system masters a concept is answered in words from the pages through get_entity, and shown no picture unless they ask to see one. One who asks to see a use case, or the diagram of one, who wants what from it or what it is held to, is shown shape usecase and then shape requirements, both with the use case's id, and the answer names what each drew; where requirements refuses as empty, the use case stands alone and the answer says in one sentence that the model names no requirement it is held to. One who asks how a use case runs, its steps, or what happens when something goes wrong, is shown shape scenario alone, with its id. One who asks what a feature or a product is held to is shown shape requirements with that id, and one who asks about a requirement by its name is shown shape requirements with the requirement's id. Then write a sentence or two naming what it drew by title, with no table of it, stating only the relations the tool listed, or for an organization what get_entity gave for its groups, and never the picture itself, in Mermaid or any other form, because the widget draws it under the answer. Where diagram refuses for any other reason, answer in words and a table as you would without it.";
export const DIAGRAM_RULE = "A visitor who asks to see how the concepts relate, how a process runs, or how something connects is shown a picture: call diagram, with shape concepts, process, neighborhood, context, aggregate, flow, lifecycle, organization, system, holds, usecase, scenario or requirements; one who asks to see the meta-model, the schemas or the types the model is written in, or what it is based on, is shown shape schema, with type where they name one, not shape concepts; one who names core or a pack, the landscape pack, the requirements pack, is shown shape schema with unit where the tool takes one, the pack's name alone, and never with type; the meta-model is always this model's schemas, so a question about what it is gets that answer. One who asks to see a bounded context, or the diagrams of one, is shown shape context, then aggregate, then flow, then lifecycle, all with the context's id, and the answer names what each drew; where aggregate refuses as empty, the map stands alone and the answer says only that the context holds no aggregate; otherwise, where flow or lifecycle refuses as empty, the answer says in one sentence that the model does not describe that flow or lifecycle yet. One who asks for the sequence or the lifecycle of a context or an aggregate is shown that shape alone, flow for the sequence, with its id. One who asks how the company is organized or for an org chart is shown shape organization, with a group's id where they name one, then calls get_entity on every group it drew, and the answer names each group's lead, its members and its open positions as those pages give them, never as read off the picture or its lines; where its omitted is above zero, the answer says in one sentence that the groups outside the line are drawn when one is named. One who asks who leads a group or who works in one is answered from that group's page through get_entity, in words, and shown no picture unless they ask to see one. One who asks to see a system, or the diagram of one, what it connects to, what runs on it or what it runs on, or what data it holds, is shown shape system and then shape holds, both with the system's id, never shape neighborhood, since a system's connections are its landscape, and a name that is both a surface's and a system's is read as the system's, the surface named in one sentence; the answer names what each drew; where holds refuses as empty, the landscape stands alone and the answer says in one sentence that the model names no data the system holds. One who asks which system masters a concept is answered in words from the pages through get_entity, and shown no picture unless they ask to see one. One who asks to see a use case, or the diagram of one, who wants what from it or what it is held to, is shown shape usecase and then shape requirements, both with the use case's id, and the answer names what each drew; where requirements refuses as empty, the use case stands alone and the answer says in one sentence that the model names no requirement it is held to. One who asks how a use case runs, its steps, or what happens when something goes wrong, is shown shape scenario alone, with its id. One who asks what a feature or a product is held to is shown shape requirements with that id, and one who asks about a requirement by its name is shown shape requirements with the requirement's id. Then write a sentence or two naming what it drew by title, with no table of it, stating only the relations the tool listed, or for an organization what get_entity gave for its groups, and never the picture itself, in Mermaid or any other form, because the widget draws it under the answer. Where diagram refuses for any other reason, answer in words and a table as you would without it.";

// The types as one sentence, so the model knows what kinds of thing the model holds before it
// reaches for a tool: `feature (5)`, and an owned type with its owner, `phase (12, owned by
Expand Down
34 changes: 34 additions & 0 deletions test/loop.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from "node:fs";
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { answer, namesIn, namesPastTheCap, NAME_CAP, foundNothing, diagramOf, diagramNote } from "../lib/loop.mjs";
Expand Down Expand Up @@ -587,6 +588,39 @@ test("a picture of the schemas keeps what every other type declares and the core
assert.equal("everyType" in diagramOf("diagram", { isError: false, data: { ...data, shape: "concepts" } }), false, "only the schemas carry it");
});

// A meta-model of more types than one picture holds is drawn a unit at a time, core or one pack,
// and the host says which unit it drew and which there are. The note carries both, so the answer
// says what is shown and what can be shown next, and the widget needs neither.
test("a picture of one unit of the schemas says which unit it is and names the others, in the note alone", () => {
const nodes = [{ node: "n0", id: "core/phase", title: "phase", type: "schema" }, { node: "n1", id: "core/process", title: "process", type: "schema" }];
const data = {
shape: "schema", title: "core", mermaid: "classDiagram", nodes, edges: 1, omitted: 3, links: [{ from: "n0", to: "n1", label: "nested-in" }], everyType: [], model: { core: "0.69.0" },
unit: "core", units: [{ unit: "core", types: 37 }, { unit: "landscape", types: 5 }, { unit: 7 }, { unit: "software" }],
};
const picture = diagramOf("diagram", { isError: false, data });
assert.equal(picture.unit, "core");
assert.deepEqual(picture.units, [{ unit: "core", types: 37 }, { unit: "landscape", types: 5 }]);
const note = JSON.parse(diagramNote({ ...picture, edges: 1 }));
assert.equal(note.unit, "core");
assert.deepEqual(note.units, [{ unit: "core", types: 37 }, { unit: "landscape", types: 5 }]);
assert.match(note.schemas, /draws one unit of the meta-model/);
assert.match(note.schemas, /shown next/);
// A picture of the whole, or of one type, names no unit and its note says nothing of one.
const whole = diagramOf("diagram", { isError: false, data: { ...data, title: null, unit: undefined } });
assert.equal("unit" in whole, false);
const wholeNote = JSON.parse(diagramNote({ ...whole, edges: 1 }));
assert.equal("unit" in wholeNote, false);
assert.doesNotMatch(wholeNote.schemas, /one unit of the meta-model/);
assert.equal("units" in diagramOf("diagram", { isError: false, data: { ...data, shape: "concepts" } }), false, "only the schemas carry it");
// The event the widget gets is the picture and nothing the note alone needs.
assert.match(fs.readFileSync(new URL("../lib/loop.mjs", import.meta.url), "utf8"), /const \{ links, everyType, core, unit, units, transitions, steps, extensions, actor, \.\.\.event \} = picture; emit\("diagram", event\);/);
});

test("the diagram rule sends core or a pack to the schema shape by unit, where the tool takes one", () => {
assert.match(DIAGRAM_RULE, /one who names core or a pack[^;]*is shown shape schema with unit where the tool takes one/);
assert.match(DIAGRAM_RULE, /never with type/);
});

test("a type in a picture of the schemas is no name the answer links", () => {
const data = { shape: "schema", nodes: [{ node: "n0", id: "core/seat", title: "seat", type: "schema" }, { node: "n1", id: "concepts/claim", title: "Claim", type: "concept" }] };
assert.deepEqual(namesIn(data), [{ id: "concepts/claim", title: "Claim" }]);
Expand Down
5 changes: 5 additions & 0 deletions types/lib/loop.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export type Diagram = {
multiplicity: string;
}[];
core?: string | null;
unit?: string;
units?: {
unit: string;
types: number;
}[];
transitions?: Transition[];
steps?: Step[];
extensions?: Extension[];
Expand Down
Loading
Loading