@jigs.design/server 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @jigs.design/server
2
2
 
3
- `createJigsServer` turns a host into one fetch handler. That handler streams checked programs, runs queries and mutations, and serves the sandbox frame. You give it a `run` function per procedure, a `context` for each request, and a `model` function that calls your LLM.
3
+ `createJigsServer` turns a host into one fetch handler. That handler streams checked programs, runs queries and mutations, and serves the sandbox frame. You give it a `run` function per procedure, and a `context` for each request; `.withModel` adds a `model` function that calls your LLM. Without it, the server serves calls, pages, the push stream and a session that opens a document with no ask, and nothing that asks a model.
4
4
 
5
5
  Import it from server code only.
6
6
 
@@ -21,8 +21,9 @@ export const jigs = createJigsServer({
21
21
  }
22
22
  },
23
23
  },
24
- model: aiSdkModel("anthropic/claude-sonnet-5.5"),
25
24
  instructions: "You are Acme's order assistant. Today is 2026-10-18.",
25
+ }).withModel({
26
+ model: aiSdkModel("anthropic/claude-sonnet-5.5"),
26
27
  });
27
28
 
28
29
  // Mount it as GET, POST and OPTIONS on one route, e.g. /api/jigs.
@@ -31,26 +32,31 @@ export const handler = jigs.handler;
31
32
 
32
33
  The server renders the prompt from the host, calls the model, checks each streamed prefix and compiles it to a program. `instructions`, such as who the assistant is and today's date, reach every prompt: document-mode sessions, chat and repairs. When the checker finds errors, it runs repair rounds. A call carries its procedure's `kind`, and the server refuses one that does not match the host. A follow-up turn may answer with SEARCH/REPLACE edits instead of a whole document.
33
34
 
34
- Specs need not come from the model. `jigs.spec(name)` returns a spec your product ships in `specs`, compiled once at startup. `jigs.compile(source)` checks and compiles one a user saved.
35
+ UIs need not come from the model. `await jigs.page(name, props)` returns a page your product ships as a jig on the host, compiled at startup or, for new props, at its first call; `await jigs.page(id, undefined, { ctx })` also answers a page a user saved, which `resolvePage` finds for that `ctx`. The handler serves both as `POST { type: "page" }`, gated by `context` and `pagePolicy`, and compiled pages go through an in-process cache bounded by size. A session can open either as its document, `{ name, props }`.
36
+
37
+ With an `events` option, a `GET` that accepts `text/event-stream` opens a push stream, on which your server names the queries and pages to refetch.
35
38
 
36
39
  ## Exports
37
40
 
38
- | Export | What it is |
39
- | -------------------------- | ----------------------------------------------------------------------------------- |
40
- | `createJigsServer` | The server: `handler`, `spec`, `compile`, `chat`, and the functions `handler` wraps |
41
- | `defineRun` | Types `run` functions outside a server, to share them between two servers |
42
- | `JigsError` | Throw from `context` or `run` with a code the browser can read |
43
- | `JigsCompileError` | What a strict `jigs.compile` rejects with, carrying the `diagnostics` |
44
- | `chatBlockText` | Writes a chat block back as a fenced block, for the model's history |
45
- | `@jigs.design/server/node` | `nodeHandler`, for Node's `http` and Express |
41
+ | Export | What it is |
42
+ | -------------------------- | ------------------------------------------------------------------------------ |
43
+ | `createJigsServer` | The server: `handler`, `page`, `compile`, `withModel` for `session` and `chat` |
44
+ | `defineRun` | Types `run` functions outside a server, to share them between two servers |
45
+ | `JigsError` | Throw from `context` or `run` with a code the browser can read |
46
+ | `JigsCompileError` | What a strict `jigs.compile` rejects with, carrying the `diagnostics` |
47
+ | `chatBlockText` | Writes a chat block back as a fenced block, for the model's history |
48
+ | `@jigs.design/server/node` | `nodeHandler`, for Node's `http` and Express |
46
49
 
47
- It re-exports the shared types from `@jigs.design/runtime`.
50
+ It re-exports the shared types from `@jigs.design/protocol`.
48
51
 
49
52
  ## Source
50
53
 
51
- - `src/server.ts`: `createJigsServer` and the request routing.
54
+ - `src/core.ts`: `createJigsServer`, the model-free core: calls, pages, compiling, the frame and prompts.
55
+ - `src/compile-cache.ts`: the process's compile cache, bounded by the size of its programs.
56
+ - `src/handler.ts`: the fetch handler: refusals, body parsing, routing and the event stream.
57
+ - `src/model-server.ts`: `withModel`: `session`, `chat` and the handler that runs turns.
52
58
  - `src/session.ts`: one turn: prompt, model, checking, repair and edits.
53
- - `src/compile.ts`: compiling a document against the host's catalog and recipes.
59
+ - `src/compile.ts`: compiling a document against the host's catalog and tags, a page against every define jig, and the checks on a page's props.
54
60
  - `src/inputs.ts`, `src/literal.ts`: literal procedure inputs checked against their schemas.
55
61
  - `src/chat.ts`: chat mode, where a Markdown reply carries fenced `jigs` blocks.
56
62
  - `src/invalidation.ts`: which queries a mutation refreshes.
package/dist/chat.d.ts CHANGED
@@ -2,11 +2,11 @@ import "./design.js";
2
2
  import { ChatBlockSource } from "./types.js";
3
3
  import "./session.js";
4
4
  import "@jigs.design/compiler";
5
- import { ChatBlock, Diagnostic, Program } from "@jigs.design/runtime";
5
+ import { ChatBlock, Diagnostic, PageRef, Program } from "@jigs.design/protocol";
6
6
  //#region src/chat.d.ts
7
7
  /**
8
8
  * A block as the model wrote it, for the model's history: a fenced `jigs`
9
- * block with its id, and its source without the pragma line.
9
+ * block with its name as the fence id, and its source without the pragma line.
10
10
  */
11
11
  export declare function chatBlockText(block: ChatBlockSource): string;
12
12
  //#endregion
package/dist/chat.js CHANGED
@@ -1,5 +1,5 @@
1
- import { MARKDOWN_ELEMENTS } from "./compile.js";
2
1
  import { belowPrefix, pragmaLine, splitPragma, withPragma } from "./pragma.js";
2
+ import { MARKDOWN_ELEMENTS } from "./compile.js";
3
3
  import { reorderDeclarations } from "./rewrite.js";
4
4
  import { applyChecked, editResult, runSession } from "./session.js";
5
5
  import { applyEdits, isEditReply, parseEdits } from "@jigs.design/compiler";
@@ -87,11 +87,11 @@ const AUTO_ID = /^jigs-(\d+)$/;
87
87
  const VALID_ID = /^[a-z][a-z0-9-]*$/;
88
88
  /**
89
89
  * A block as the model wrote it, for the model's history: a fenced `jigs`
90
- * block with its id, and its source without the pragma line.
90
+ * block with its name as the fence id, and its source without the pragma line.
91
91
  */
92
92
  function chatBlockText(block) {
93
93
  const body = splitPragma(block.source).body.replace(/\n+$/, "");
94
- return `\`\`\`jigs ${block.id}\n${body}\n\`\`\``;
94
+ return `\`\`\`jigs ${block.name}\n${body}\n\`\`\``;
95
95
  }
96
96
  /** The block's id from the fence's info string, made kebab-case. */
97
97
  function slug(info) {
@@ -133,18 +133,18 @@ function depthChange(line) {
133
133
  * its program changes.
134
134
  */
135
135
  function createReplyParser(deps, history) {
136
- /** Latest body (no pragma) by id. */
136
+ /** Latest body (no pragma) by name. */
137
137
  const latest = /* @__PURE__ */ new Map();
138
138
  /**
139
- * The body as this reply wrote it, by id, for a block whose `{@body}`
139
+ * The body as this reply wrote it, by name, for a block whose `{@body}`
140
140
  * lines the server moved: an edit later in the same reply was written
141
141
  * against it, not against the moved source.
142
142
  */
143
143
  const asWritten = /* @__PURE__ */ new Map();
144
144
  /** The version `edits` apply to: the latest, or the one the reply wrote. */
145
- const baseFor = (id, edits) => {
146
- const base = latest.get(id);
147
- const written = asWritten.get(id);
145
+ const baseFor = (name, edits) => {
146
+ const base = latest.get(name);
147
+ const written = asWritten.get(name);
148
148
  return base !== void 0 && written !== void 0 && !applyEdits(base, edits).ok && applyEdits(written, edits).ok ? written : base;
149
149
  };
150
150
  const versions = /* @__PURE__ */ new Map();
@@ -156,10 +156,10 @@ function createReplyParser(deps, history) {
156
156
  let opened = 0;
157
157
  let repairsLeft = MAX_BLOCK_REPAIRS;
158
158
  for (const entry of history) {
159
- if (!VALID_ID.test(entry.id)) continue;
160
- latest.set(entry.id, splitPragma(entry.source).body);
161
- versions.set(entry.id, (versions.get(entry.id) ?? 0) + 1);
162
- const auto = AUTO_ID.exec(entry.id);
159
+ if (!VALID_ID.test(entry.name)) continue;
160
+ latest.set(entry.name, splitPragma(entry.source).body);
161
+ versions.set(entry.name, (versions.get(entry.name) ?? 0) + 1);
162
+ const auto = AUTO_ID.exec(entry.name);
163
163
  if (auto !== null) autoId = Math.max(autoId, Number(auto[1]));
164
164
  }
165
165
  const empty = deps.compile("");
@@ -209,14 +209,15 @@ function createReplyParser(deps, history) {
209
209
  const blockOf = (open, body, compiled, status, error) => {
210
210
  const block = {
211
211
  diagnostics: belowPrefix(compiled.diagnostics, pragmaLine(deps.catalogVersion)),
212
- id: open.id,
213
- key: `${open.id}@${open.version}`,
212
+ key: `${open.name}@${open.version}`,
213
+ name: open.name,
214
214
  program: compiled.program,
215
215
  source: withPragma(body, deps.catalogVersion),
216
216
  status,
217
217
  version: open.version
218
218
  };
219
219
  if (error !== void 0) block.error = error;
220
+ if (compiled.ref !== null) block.ref = compiled.ref;
220
221
  return block;
221
222
  };
222
223
  const emit = (block) => {
@@ -236,34 +237,35 @@ function createReplyParser(deps, history) {
236
237
  advice: belowPrefix(advice, pragmaLine(deps.catalogVersion)),
237
238
  diagnostics: edits?.applied === false ? [] : block.diagnostics,
238
239
  fenced: !open.implicit,
239
- id: block.id,
240
240
  key: block.key,
241
+ name: block.name,
241
242
  repairs: [],
242
243
  status: block.status === "repairing" ? "repairing" : "done"
243
244
  };
244
245
  if (edits !== void 0) result.edits = edits;
245
246
  if (block.error !== void 0) result.error = block.error;
246
247
  if (rewrite !== void 0) result.rewrites = [rewrite];
248
+ if (block.ref !== void 0) result.ref = block.ref;
247
249
  results.set(block.key, result);
248
250
  emit(block);
249
251
  };
250
252
  const open = (info, indent, implicit = false) => {
251
253
  const named = slug(info ?? "");
252
- const id = named === "" ? `jigs-${++autoId}` : named;
253
- const version = (versions.get(id) ?? 0) + 1;
254
- versions.set(id, version);
254
+ const name = named === "" ? `jigs-${++autoId}` : named;
255
+ const version = (versions.get(name) ?? 0) + 1;
256
+ versions.set(name, version);
255
257
  opened++;
256
258
  const block = {
257
259
  baseShown: false,
258
260
  depth: 0,
259
261
  dirty: false,
260
262
  editsShown: 0,
261
- id,
262
263
  implicit,
263
264
  indent,
264
265
  lastKey: JSON.stringify(empty.program),
265
266
  lastSentAt: Number.NEGATIVE_INFINITY,
266
267
  lines: [],
268
+ name,
267
269
  over: opened > MAX_BLOCKS,
268
270
  partial: "",
269
271
  version,
@@ -286,7 +288,7 @@ function createReplyParser(deps, history) {
286
288
  const editing = isEditReply(body);
287
289
  if (editing === true) {
288
290
  const edits = parseEdits(body);
289
- const base = baseFor(block.id, edits);
291
+ const base = baseFor(block.name, edits);
290
292
  if (base === void 0) return;
291
293
  if (!block.baseShown) {
292
294
  block.baseShown = true;
@@ -328,11 +330,11 @@ function createReplyParser(deps, history) {
328
330
  let edits;
329
331
  if (isEditReply(written) === true) {
330
332
  const parsed = parseEdits(written);
331
- const base = baseFor(block.id, parsed);
333
+ const base = baseFor(block.name, parsed);
332
334
  if (base === void 0) {
333
335
  settle(block, blockOf(block, "", empty, "done", {
334
336
  code: "NOT_FOUND",
335
- message: `There is no earlier jig "${block.id}" to change.`
337
+ message: `There is no earlier jig "${block.name}" to change.`
336
338
  }), {
337
339
  applied: false,
338
340
  blocks: parsed.length
@@ -364,12 +366,12 @@ function createReplyParser(deps, history) {
364
366
  }
365
367
  let compiled = deps.compile(body);
366
368
  const reordered = reorderDeclarations(body, compiled, deps.compile);
367
- if (reordered === null) asWritten.delete(block.id);
369
+ if (reordered === null) asWritten.delete(block.name);
368
370
  else {
369
- asWritten.set(block.id, body);
371
+ asWritten.set(block.name, body);
370
372
  ({body, compiled} = reordered);
371
373
  }
372
- latest.set(block.id, body);
374
+ latest.set(block.name, body);
373
375
  const advice = compiled.advise("chat");
374
376
  const chosen = advice.filter((entry) => deps.designRepair.has(entry.code));
375
377
  if ((needsRepair(compiled.diagnostics) || chosen.length > 0) && repairable()) {
@@ -544,9 +546,9 @@ function createReplyParser(deps, history) {
544
546
  end,
545
547
  plan: (block) => plans.get(block.key),
546
548
  push,
547
- remember: (id, body) => {
548
- asWritten.delete(id);
549
- latest.set(id, body);
549
+ remember: (name, body) => {
550
+ asWritten.delete(name);
551
+ latest.set(name, body);
550
552
  },
551
553
  result: (key) => results.get(key),
552
554
  results: () => [...results.values()].map((result) => ({ ...result }))
@@ -576,7 +578,7 @@ async function* repairBlock(deps, block) {
576
578
  }
577
579
  const result = deps.parser.result(block.key);
578
580
  const base = plan.kind === "edit" ? plan.base : plan.body;
579
- const ask = `Write the "${block.id}" UI.`;
581
+ const ask = `Write the "${block.name}" UI.`;
580
582
  const messages = [
581
583
  {
582
584
  content: deps.prompt === void 0 ? ask : `${deps.prompt}\n\n${ask}`,
@@ -587,7 +589,7 @@ async function* repairBlock(deps, block) {
587
589
  role: "assistant"
588
590
  },
589
591
  {
590
- content: plan.kind === "edit" ? editRepairText(plan.failure, plan.base) : repairText(plan.diagnostics, plan.advice),
592
+ content: plan.kind === "edit" ? editRepairText(plan.failure, plan.base) : repairText(plan.diagnostics, plan.advice, "answer"),
591
593
  role: "user"
592
594
  }
593
595
  ];
@@ -606,17 +608,27 @@ async function* repairBlock(deps, block) {
606
608
  await forward?.(turn);
607
609
  }
608
610
  };
609
- for await (const event of runSession(session, messages, deps.options, deps.call)) if (event.type === "program") program = event.program;
611
+ for await (const event of runSession(session, {
612
+ document: base,
613
+ messages
614
+ }, deps.options, deps.call)) if (event.type === "program") program = event.program;
610
615
  else if (event.type === "done") {
611
- deps.parser.remember(block.id, splitPragma(event.source).body);
612
- if (result !== void 0) result.status = "done";
613
- yield {
616
+ const body = splitPragma(event.source).body;
617
+ deps.parser.remember(block.name, body);
618
+ const repaired = {
614
619
  ...block,
615
620
  diagnostics: event.diagnostics,
616
621
  program: program ?? block.program,
617
622
  source: event.source,
618
623
  status: "done"
619
624
  };
625
+ const { ref } = deps.session.checker.compile(body);
626
+ if (ref !== null) repaired.ref = ref;
627
+ if (result !== void 0) {
628
+ result.status = "done";
629
+ if (ref !== null) result.ref = ref;
630
+ }
631
+ yield repaired;
620
632
  return;
621
633
  } else failure = event.error;
622
634
  if (result !== void 0) {
@@ -0,0 +1,60 @@
1
+ //#region src/compile-cache.ts
2
+ /** The default bound: 32 MB of programs, as JSON. */
3
+ const DEFAULT_MAX_BYTES = 33554432;
4
+ /**
5
+ * A cache bounded by the size of its programs, least recently used out
6
+ * first. A program larger than the whole bound is not kept.
7
+ */
8
+ function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
9
+ const entries = /* @__PURE__ */ new Map();
10
+ let bytes = 0;
11
+ const drop = (key) => {
12
+ const entry = entries.get(key);
13
+ if (entry !== void 0) {
14
+ entries.delete(key);
15
+ bytes -= entry.size;
16
+ }
17
+ };
18
+ return {
19
+ get bytes() {
20
+ return bytes;
21
+ },
22
+ get: (key) => {
23
+ const entry = entries.get(key);
24
+ if (entry === void 0) return;
25
+ entries.delete(key);
26
+ entries.set(key, entry);
27
+ return entry.value;
28
+ },
29
+ set: (key, value) => {
30
+ drop(key);
31
+ const size = JSON.stringify(value.program).length;
32
+ if (size > maxBytes) return;
33
+ entries.set(key, {
34
+ size,
35
+ value
36
+ });
37
+ bytes += size;
38
+ for (const oldest of entries.keys()) {
39
+ if (bytes <= maxBytes) break;
40
+ drop(oldest);
41
+ }
42
+ },
43
+ get size() {
44
+ return entries.size;
45
+ }
46
+ };
47
+ }
48
+ /**
49
+ * The one cache every server in this process shares. Keys start with a
50
+ * hash of what the server compiles against, so two hosts never read each
51
+ * other's entries.
52
+ */
53
+ const processCompileCache = memoryCompileCache();
54
+ /** SHA-256 of `text`, as hex: collisions here would serve the wrong page. */
55
+ async function sha256(text) {
56
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
57
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
58
+ }
59
+ //#endregion
60
+ export { memoryCompileCache, processCompileCache, sha256 };
package/dist/compile.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import "./design.js";
2
2
  import { Catalog, Host } from "@jigs.design/host";
3
- import { Diagnostic, Program } from "@jigs.design/runtime";
3
+ import { Diagnostic, PageProps, PageRef, Program } from "@jigs.design/protocol";
package/dist/compile.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { designChecks } from "./design.js";
2
- import { JigsCompileError, fatalDiagnostics } from "./errors.js";
2
+ import { JigsCompileError, JigsError, fatalDiagnostics } from "./errors.js";
3
3
  import { inputDiagnostics } from "./inputs.js";
4
4
  import { outputDiagnostics } from "./outputs.js";
5
- import { columnAt, compileJigs, compileLibrary, lineAt } from "@jigs.design/compiler";
5
+ import { splitPragma, versionDiagnostics } from "./pragma.js";
6
+ import { compileJigs, compileLibrary, spanIn } from "@jigs.design/compiler";
6
7
  import { componentSchema } from "@jigs.design/host";
7
- import { PROGRAM_FORMAT } from "@jigs.design/runtime";
8
+ import { PROGRAM_FORMAT } from "@jigs.design/protocol";
8
9
  //#region src/compile.ts
9
10
  /** Elements the parser makes from Markdown; every catalog must be able to draw them. */
10
11
  const MARKDOWN_ELEMENTS = [
@@ -56,19 +57,6 @@ function compileCatalog(catalog) {
56
57
  return out;
57
58
  }
58
59
  const TAG = /<([a-z][A-Za-z0-9]*)/g;
59
- /** The span of `[start, end)` in `source`. */
60
- function rangeIn(source, start, end) {
61
- return {
62
- column: columnAt(source, start),
63
- end,
64
- line: lineAt(source, start),
65
- start
66
- };
67
- }
68
- /** The span of `place` in `source`: its line, and its range and column when it has one. */
69
- function spanIn(source, place) {
70
- return place.start === void 0 || place.end === void 0 ? { line: place.line } : rangeIn(source, place.start, place.end);
71
- }
72
60
  function procedureDiagnostics(host, source, uses) {
73
61
  const diagnostics = [];
74
62
  const report = (use, code, message) => {
@@ -123,83 +111,162 @@ function tagsIn(source) {
123
111
  for (const match of source.matchAll(TAG)) if (match[1] !== void 0) tags.add(match[1]);
124
112
  return tags;
125
113
  }
114
+ /** The longest canonical props, as JSON, a page accepts. */
115
+ const MAX_PAGE_PROPS = 4096;
116
+ /** A JavaScript identifier, as the compiler requires of a define prop. */
117
+ const BINDING_NAME = /^[A-Za-z_$][\w$]*$/;
118
+ /** What the compiler reads as a number literal; anything else is an expression. */
119
+ const NUMBER_LITERAL = /^-?\d+(?:\.\d+)?$/;
120
+ const SCALAR_KINDS = [
121
+ "boolean",
122
+ "number",
123
+ "string"
124
+ ];
125
+ /**
126
+ * Whether `value` is a literal of `kind`. Props reach the server from
127
+ * untyped callers too, so a value may be none of the three.
128
+ */
129
+ function isOfKind(value, kind) {
130
+ return typeof value === kind;
131
+ }
132
+ /** `props` with keys sorted, so equal props make equal bodies. */
133
+ function sortedProps(props) {
134
+ return Object.fromEntries(Object.entries(props).toSorted(([a], [b]) => a < b ? -1 : 1));
135
+ }
136
+ /** Each prop a literal in braces, keys sorted, so equal props make equal bodies. */
137
+ function oneTag(name, props) {
138
+ const attrs = Object.entries(sortedProps(props)).map(([key, value]) => `${key}={${JSON.stringify(value)}}`);
139
+ return attrs.length === 0 ? `<${name}/>` : `<${name} ${attrs.join(" ")}/>`;
140
+ }
141
+ /**
142
+ * Why `props` cannot be a page's props for `define`, or null when they can.
143
+ * Each value must be one the compiler reads as a literal, so the compile
144
+ * that follows types it.
145
+ */
146
+ function pagePropsProblem(define, props) {
147
+ const declared = new Map(define.props.map((prop) => [prop.name, prop]));
148
+ for (const [key, value] of Object.entries(props)) {
149
+ if (key === "name" || key === "key" || !BINDING_NAME.test(key)) return `"${key}" cannot be a prop name`;
150
+ const prop = declared.get(key);
151
+ if (prop === void 0) {
152
+ const known = [...declared.keys()];
153
+ return `<${define.name}> has no prop "${key}"${known.length === 0 ? "" : `; its props are ${known.join(", ")}`}`;
154
+ }
155
+ const kind = SCALAR_KINDS.find((candidate) => isOfKind(value, candidate));
156
+ if (kind === void 0) return `"${key}" must be a string, a number or a boolean`;
157
+ if (prop.kind !== null && prop.kind !== kind) return `"${key}" must be a ${prop.kind}`;
158
+ if (kind === "number" && !NUMBER_LITERAL.test(JSON.stringify(value))) return `"${key}" must be a finite number written without an exponent`;
159
+ if (kind === "string" && /["\\\r\n\u2028\u2029]/.test(String(value))) return `"${key}" must not contain a quote, a backslash or a line break`;
160
+ }
161
+ const missing = define.props.find((prop) => prop.required && !Object.hasOwn(props, prop.name));
162
+ if (missing !== void 0) return `<${define.name}> needs the prop "${missing.name}"`;
163
+ if (JSON.stringify(sortedProps(props)).length > MAX_PAGE_PROPS) return `The props are longer than ${MAX_PAGE_PROPS} characters`;
164
+ return null;
165
+ }
166
+ /**
167
+ * The page a tree names, when it is one call to a library jig that is a
168
+ * page, with no children and literal props only.
169
+ */
170
+ function refOf(tree, pages) {
171
+ const nodes = tree.filter((node) => node.type !== "text" || node.value.trim() !== "");
172
+ const [node] = nodes;
173
+ if (nodes.length !== 1 || node?.type !== "call" || node.origin !== "library" || node.key !== null || node.children.length > 0 || !pages.has(node.name)) return null;
174
+ const props = {};
175
+ for (const [key, value] of Object.entries(node.props)) {
176
+ if (value.kind !== "literal") return null;
177
+ props[key] = value.value;
178
+ }
179
+ return {
180
+ name: node.name,
181
+ props: sortedProps(props)
182
+ };
183
+ }
126
184
  /**
127
185
  * The host, ready to check sources: its catalog in the compiler's form and
128
- * its recipes compiled into a library. Throws when a recipe does not compile
129
- * or calls a procedure the host does not have, so a bad recipe stops the
130
- * server at startup instead of reaching the model. Throws too when a
131
- * situation's example has a diagnostic worse than info, so the prompt never
132
- * shows the model Jigs the checker refuses.
186
+ * its define jigs compiled into two libraries, one of the tags the model
187
+ * may write and one of every define jig, for pages. Throws when a jig does
188
+ * not compile, carries a pragma for a newer language or catalog, calls a
189
+ * procedure the host does not have, or is a tag that uses a page-only jig,
190
+ * so a bad jig stops the server at startup instead of reaching the model.
191
+ * Throws too when a situation's example has a diagnostic worse than info,
192
+ * so the prompt never shows the model Jigs the checker refuses.
133
193
  */
134
194
  function createChecker(host) {
135
195
  const catalog = compileCatalog(host.components);
136
- const library = compileLibrary(host.recipes.map((entry) => ({
196
+ const defines = [];
197
+ for (const entry of host.jigs) {
198
+ const split = splitPragma(entry.source);
199
+ const fatal = fatalDiagnostics(versionDiagnostics(split.versions, host.components.version));
200
+ if (fatal.length > 0) throw new JigsCompileError(`Jig "${entry.name}"`, fatal);
201
+ if (entry.kind === "define") defines.push({
202
+ body: split.body,
203
+ name: entry.name,
204
+ tag: entry.tag
205
+ });
206
+ }
207
+ const tagLibrary = compileLibrary(defines.filter((entry) => entry.tag).map((entry) => ({
208
+ name: entry.name,
209
+ source: entry.body
210
+ })), catalog);
211
+ const fullLibrary = compileLibrary(defines.map((entry) => ({
137
212
  name: entry.name,
138
- source: entry.source
213
+ source: entry.body
139
214
  })), catalog);
140
- const recipes = /* @__PURE__ */ new Map();
141
- for (const entry of host.recipes) {
142
- const uses = [...library.get(entry.name)?.procedures ?? []];
143
- const problem = [...procedureDiagnostics(host, entry.source, uses), ...inputDiagnostics(host, entry.source)][0];
144
- if (problem !== void 0) throw new Error(`Recipe "${entry.name}" failed to compile: line ${problem.span.line}: ${problem.message}`);
145
- recipes.set(entry.name, {
146
- tags: tagsIn(entry.source),
215
+ const allFacts = /* @__PURE__ */ new Map();
216
+ for (const entry of defines) {
217
+ const uses = [...fullLibrary.get(entry.name)?.procedures ?? []];
218
+ const problem = [...procedureDiagnostics(host, entry.body, uses), ...inputDiagnostics(host, entry.body)][0];
219
+ if (problem !== void 0) throw new Error(`Jig "${entry.name}" failed to compile: line ${problem.span.line}: ${problem.message}`);
220
+ allFacts.set(entry.name, {
221
+ tags: tagsIn(entry.body),
147
222
  uses
148
223
  });
149
224
  }
150
- /** The tags a source draws, through every recipe it uses, and the procedures those recipes call. */
151
- const reach = (body) => {
152
- const tags = new Set(MARKDOWN_ELEMENTS);
225
+ const tagFacts = new Map([...allFacts].filter(([name]) => tagLibrary.has(name)));
226
+ const pages = new Set(host.jigs.filter((entry) => entry.page).map((entry) => entry.name));
227
+ const tags = /* @__PURE__ */ new Set([...host.components.components.map((component) => component.name), ...tagLibrary.keys()]);
228
+ /** The tags a source draws, through every jig it uses, and the procedures those jigs call. */
229
+ const reach = (body, facts) => {
230
+ const drawn = new Set(MARKDOWN_ELEMENTS);
153
231
  const uses = [];
154
232
  const pending = [...tagsIn(body)];
155
233
  for (let tag = pending.pop(); tag !== void 0; tag = pending.pop()) {
156
- if (tags.has(tag)) continue;
157
- tags.add(tag);
158
- const facts = recipes.get(tag);
159
- if (facts !== void 0) {
160
- uses.push(...facts.uses);
161
- pending.push(...facts.tags);
234
+ if (drawn.has(tag)) continue;
235
+ drawn.add(tag);
236
+ const found = facts.get(tag);
237
+ if (found !== void 0) {
238
+ uses.push(...found.uses);
239
+ pending.push(...found.tags);
162
240
  }
163
241
  }
164
242
  return {
165
- tags,
243
+ tags: drawn,
166
244
  uses
167
245
  };
168
246
  };
169
- const compile = (body) => {
247
+ const compileWith = (body, library, facts) => {
170
248
  const result = compileJigs(body, {
171
249
  catalog,
172
250
  library
173
251
  });
174
252
  const own = result.procedures;
175
- const reached = reach(result.source);
253
+ const reached = reach(result.source, facts);
254
+ const diagnostics = [
255
+ ...result.diagnostics,
256
+ ...procedureDiagnostics(host, body, own),
257
+ ...outputDiagnostics(host, body, result.reads),
258
+ ...inputDiagnostics(host, body)
259
+ ].toSorted((a, b) => a.span.line - b.span.line);
260
+ let ref = diagnostics.some((diagnostic) => diagnostic.severity === "error") ? null : refOf(result.program.tree, pages);
261
+ const define = ref === null ? void 0 : fullLibrary.get(ref.name);
262
+ if (ref !== null && (define === void 0 || pagePropsProblem(define, ref.props) !== null)) ref = null;
176
263
  return {
177
264
  advise: (placement) => designChecks({
178
265
  catalog: host.components,
179
266
  compiled: result,
180
267
  placement
181
268
  }),
182
- diagnostics: [
183
- ...result.diagnostics.map((diagnostic) => {
184
- const sent = {
185
- code: diagnostic.code,
186
- message: diagnostic.message,
187
- severity: diagnostic.severity === "warning" ? "error" : "info",
188
- span: spanIn(body, diagnostic)
189
- };
190
- if (diagnostic.fix !== void 0) sent.fix = {
191
- edits: diagnostic.fix.edits.map((edit) => ({
192
- span: rangeIn(body, edit.start, edit.end),
193
- text: edit.text
194
- })),
195
- title: diagnostic.fix.title
196
- };
197
- return sent;
198
- }),
199
- ...procedureDiagnostics(host, body, own),
200
- ...outputDiagnostics(host, body, result.reads),
201
- ...inputDiagnostics(host, body)
202
- ].toSorted((a, b) => a.span.line - b.span.line),
269
+ diagnostics,
203
270
  program: {
204
271
  code: result.output.code,
205
272
  components: Object.fromEntries(host.components.components.filter((component) => reached.tags.has(component.name)).map((component) => [component.name, componentSchema(component)])),
@@ -207,15 +274,52 @@ function createChecker(host) {
207
274
  imageHosts: host.imageHosts,
208
275
  procedures: procedureTable(host, [...own, ...reached.uses]),
209
276
  prose: result.output.constants
210
- }
277
+ },
278
+ ref
211
279
  };
212
280
  };
281
+ const compile = (body) => compileWith(body, tagLibrary, tagFacts);
282
+ const definePage = (name) => {
283
+ const define = fullLibrary.get(name);
284
+ if (define === void 0 || !pages.has(name)) throw new Error(`"${name}" is not a define page on this host`);
285
+ return define;
286
+ };
287
+ const pageBody = (name, props) => {
288
+ const problem = pagePropsProblem(definePage(name), props);
289
+ if (problem !== null) throw new JigsError("BAD_INPUT", problem);
290
+ return oneTag(name, props);
291
+ };
292
+ const pageDocument = (name, props) => {
293
+ const use = pageBody(name, props);
294
+ const reached = /* @__PURE__ */ new Set();
295
+ const pending = [...allFacts.get(name)?.tags ?? []];
296
+ for (let tag = pending.pop(); tag !== void 0; tag = pending.pop()) {
297
+ const facts = allFacts.get(tag);
298
+ if (reached.has(tag) || facts === void 0 || tagLibrary.has(tag)) continue;
299
+ reached.add(tag);
300
+ pending.push(...facts.tags);
301
+ }
302
+ const bodies = defines.filter((entry) => entry.name !== name && reached.has(entry.name)).map((entry) => entry.body.trim());
303
+ const own = defines.find((entry) => entry.name === name)?.body ?? "";
304
+ return [
305
+ ...bodies,
306
+ own.trim(),
307
+ use
308
+ ].join("\n\n");
309
+ };
213
310
  for (const entry of host.situations) {
214
311
  if (entry.example === void 0) continue;
215
312
  const fatal = fatalDiagnostics(compile(entry.example).diagnostics);
216
313
  if (fatal.length > 0) throw new JigsCompileError(`Situation "${entry.name}" example`, fatal);
217
314
  }
218
- return { compile };
315
+ return {
316
+ compile,
317
+ needsProps: (name) => definePage(name).props.some((prop) => prop.required),
318
+ page: (name, props) => compileWith(pageBody(name, props), fullLibrary, allFacts),
319
+ pageBody,
320
+ pageDocument,
321
+ tags
322
+ };
219
323
  }
220
324
  /** Props the parser sets on Markdown elements, with the values it uses. */
221
325
  const MARKDOWN_PROPS = [{