@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 +20 -14
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +46 -34
- package/dist/compile-cache.js +60 -0
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +169 -65
- package/dist/{server.d.ts → core.d.ts} +7 -3
- package/dist/{server.js → core.js} +310 -396
- package/dist/design.d.ts +6 -3
- package/dist/design.js +0 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +3 -3
- package/dist/events.js +129 -0
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +344 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -2
- package/dist/model-server.js +154 -0
- package/dist/outputs.js +2 -7
- package/dist/session.d.ts +1 -2
- package/dist/session.js +116 -52
- package/dist/types.d.ts +277 -51
- package/package.json +6 -5
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
|
|
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
|
-
|
|
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`, `
|
|
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/
|
|
50
|
+
It re-exports the shared types from `@jigs.design/protocol`.
|
|
48
51
|
|
|
49
52
|
## Source
|
|
50
53
|
|
|
51
|
-
- `src/
|
|
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
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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 = (
|
|
146
|
-
const base = latest.get(
|
|
147
|
-
const written = asWritten.get(
|
|
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.
|
|
160
|
-
latest.set(entry.
|
|
161
|
-
versions.set(entry.
|
|
162
|
-
const auto = AUTO_ID.exec(entry.
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
253
|
-
const version = (versions.get(
|
|
254
|
-
versions.set(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
369
|
+
if (reordered === null) asWritten.delete(block.name);
|
|
368
370
|
else {
|
|
369
|
-
asWritten.set(block.
|
|
371
|
+
asWritten.set(block.name, body);
|
|
370
372
|
({body, compiled} = reordered);
|
|
371
373
|
}
|
|
372
|
-
latest.set(block.
|
|
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: (
|
|
548
|
-
asWritten.delete(
|
|
549
|
-
latest.set(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
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 {
|
|
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/
|
|
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
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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
|
|
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.
|
|
213
|
+
source: entry.body
|
|
139
214
|
})), catalog);
|
|
140
|
-
const
|
|
141
|
-
for (const entry of
|
|
142
|
-
const uses = [...
|
|
143
|
-
const problem = [...procedureDiagnostics(host, entry.
|
|
144
|
-
if (problem !== void 0) throw new Error(`
|
|
145
|
-
|
|
146
|
-
tags: tagsIn(entry.
|
|
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
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
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 (
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
uses.push(...
|
|
161
|
-
pending.push(...
|
|
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
|
|
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 {
|
|
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 = [{
|