@jigs.design/server 0.0.0-stage → 0.1.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.
@@ -0,0 +1,249 @@
1
+ import { designChecks } from "./design.js";
2
+ import { PickleCompileError, fatalDiagnostics } from "./errors.js";
3
+ import { inputDiagnostics } from "./inputs.js";
4
+ import { outputDiagnostics } from "./outputs.js";
5
+ import { columnAt, compileLibrary, compilePickle, lineAt } from "@jigs.design/compiler";
6
+ import { componentSchema } from "@jigs.design/host";
7
+ import { PROGRAM_FORMAT } from "@jigs.design/runtime";
8
+ //#region src/compile.ts
9
+ /** Elements the parser makes from Markdown; every catalog must be able to draw them. */
10
+ const MARKDOWN_ELEMENTS = [
11
+ "bold",
12
+ "code",
13
+ "divider",
14
+ "item",
15
+ "italic",
16
+ "list",
17
+ "text",
18
+ "title"
19
+ ];
20
+ function propSpec(definition) {
21
+ const { schema } = definition;
22
+ switch (schema.kind) {
23
+ case "boolean":
24
+ case "handler":
25
+ case "string": return { kind: schema.kind };
26
+ case "enum": return definition.default === void 0 ? {
27
+ kind: "enum",
28
+ values: schema.values
29
+ } : {
30
+ default: String(definition.default),
31
+ kind: "enum",
32
+ values: schema.values
33
+ };
34
+ case "image":
35
+ case "link": return { kind: "string" };
36
+ case "list": return { kind: "rows" };
37
+ case "number": {
38
+ const spec = { kind: "number" };
39
+ if (schema.min !== void 0) spec.min = schema.min;
40
+ if (schema.max !== void 0) spec.max = schema.max;
41
+ return spec;
42
+ }
43
+ }
44
+ }
45
+ /** The host catalog in the form today's compiler checks against. */
46
+ function compileCatalog(catalog) {
47
+ const out = {};
48
+ for (const component of catalog.components) {
49
+ const props = {};
50
+ for (const [name, definition] of Object.entries(component.props)) props[name] = propSpec(definition);
51
+ out[component.name] = {
52
+ children: component.children,
53
+ props
54
+ };
55
+ }
56
+ return out;
57
+ }
58
+ 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
+ function procedureDiagnostics(host, source, uses) {
73
+ const diagnostics = [];
74
+ const report = (use, code, message) => {
75
+ diagnostics.push({
76
+ code,
77
+ message,
78
+ severity: "error",
79
+ span: spanIn(source, use)
80
+ });
81
+ };
82
+ const queries = Object.entries(host.procedures).filter(([, procedure]) => procedure.kind === "query").map(([name]) => name);
83
+ for (const use of uses) {
84
+ if (use.hook === "useAction") {
85
+ report(use, "procedure_kind", `There is no \`useAction\`: an action is called like a mutation. Write \`useMutation("${use.name ?? "name"}")\` and call \`mutate\` from a handler`);
86
+ continue;
87
+ }
88
+ if (use.tanstack) {
89
+ report(use, "tanstack_form", `\`${use.hook}({ queryKey, queryFn })\` is TanStack Query's form. Write \`${use.hook}("${use.hook === "useQuery" ? queries[0] ?? "name" : "name"}"${use.hook === "useQuery" ? ", input" : ""})\` with a name from the host`);
90
+ continue;
91
+ }
92
+ if (use.name === null) {
93
+ report(use, "procedure_name", `\`${use.hook}\` needs the procedure name as a string literal, such as \`${use.hook}("${use.hook === "useQuery" ? queries[0] ?? "name" : "name"}")\``);
94
+ continue;
95
+ }
96
+ const procedure = Object.hasOwn(host.procedures, use.name) ? host.procedures[use.name] : void 0;
97
+ if (procedure === void 0) {
98
+ report(use, "unknown_procedure", `"${use.name}" is not a query, mutation or action on this host; its handle reports UNKNOWN_PROCEDURE`);
99
+ continue;
100
+ }
101
+ const expected = procedure.kind === "query" ? "useQuery" : "useMutation";
102
+ if (expected !== use.hook) report(use, "procedure_kind", `\`${use.hook}("${use.name}")\` names ${procedure.kind === "action" ? "an" : "a"} ${procedure.kind}. Write \`${expected}("${use.name}")\`${procedure.kind === "query" ? "" : " and call `mutate` from a handler"}`);
103
+ }
104
+ return diagnostics;
105
+ }
106
+ function procedureTable(host, uses) {
107
+ const table = /* @__PURE__ */ new Map();
108
+ for (const use of uses) {
109
+ if (use.name === null || use.tanstack) continue;
110
+ const procedure = Object.hasOwn(host.procedures, use.name) ? host.procedures[use.name] : void 0;
111
+ const expected = procedure?.kind === "query" ? "useQuery" : "useMutation";
112
+ if (procedure === void 0 || expected !== use.hook) continue;
113
+ table.set(use.name, {
114
+ invalidates: procedure.kind === "mutation" ? procedure.invalidates : [],
115
+ kind: procedure.kind
116
+ });
117
+ }
118
+ return Object.fromEntries(table);
119
+ }
120
+ /** Tag names written in a source. */
121
+ function tagsIn(source) {
122
+ const tags = /* @__PURE__ */ new Set();
123
+ for (const match of source.matchAll(TAG)) if (match[1] !== void 0) tags.add(match[1]);
124
+ return tags;
125
+ }
126
+ /**
127
+ * 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 Pickle the checker refuses.
133
+ */
134
+ function createChecker(host) {
135
+ const catalog = compileCatalog(host.components);
136
+ const library = compileLibrary(host.recipes.map((entry) => ({
137
+ name: entry.name,
138
+ source: entry.source
139
+ })), 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),
147
+ uses
148
+ });
149
+ }
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);
153
+ const uses = [];
154
+ const pending = [...tagsIn(body)];
155
+ 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);
162
+ }
163
+ }
164
+ return {
165
+ tags,
166
+ uses
167
+ };
168
+ };
169
+ const compile = (body) => {
170
+ const result = compilePickle(body, {
171
+ catalog,
172
+ library
173
+ });
174
+ const own = result.procedures;
175
+ const reached = reach(result.source);
176
+ return {
177
+ advise: (placement) => designChecks({
178
+ catalog: host.components,
179
+ compiled: result,
180
+ placement
181
+ }),
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),
203
+ program: {
204
+ code: result.output.code,
205
+ components: Object.fromEntries(host.components.components.filter((component) => reached.tags.has(component.name)).map((component) => [component.name, componentSchema(component)])),
206
+ format: PROGRAM_FORMAT,
207
+ imageHosts: host.imageHosts,
208
+ procedures: procedureTable(host, [...own, ...reached.uses]),
209
+ prose: result.output.constants
210
+ }
211
+ };
212
+ };
213
+ for (const entry of host.situations) {
214
+ if (entry.example === void 0) continue;
215
+ const fatal = fatalDiagnostics(compile(entry.example).diagnostics);
216
+ if (fatal.length > 0) throw new PickleCompileError(`Situation "${entry.name}" example`, fatal);
217
+ }
218
+ return { compile };
219
+ }
220
+ /** Props the parser sets on Markdown elements, with the values it uses. */
221
+ const MARKDOWN_PROPS = [{
222
+ component: "title",
223
+ prop: "size",
224
+ values: [
225
+ "xl",
226
+ "lg",
227
+ "md"
228
+ ]
229
+ }, {
230
+ component: "list",
231
+ prop: "ordered",
232
+ values: [true]
233
+ }];
234
+ /**
235
+ * Throws when the catalog cannot draw what Markdown produces: the elements,
236
+ * and the props the parser sets on them. Without them every heading or list
237
+ * would be a diagnostic, and every turn a repair round.
238
+ */
239
+ function checkMarkdownElements(catalog) {
240
+ const byName = new Map(catalog.components.map((component) => [component.name, component]));
241
+ const missing = MARKDOWN_ELEMENTS.filter((name) => !byName.has(name));
242
+ if (missing.length > 0) throw new Error(`The catalog has no ${missing.map((name) => `<${name}>`).join(", ")}, which Markdown prose compiles to`);
243
+ for (const need of MARKDOWN_PROPS) {
244
+ const schema = (byName.get(need.component)?.props[need.prop])?.schema;
245
+ if (!(schema !== void 0 && need.values.every((value) => schema.kind === "enum" ? schema.values.includes(String(value)) : schema.kind === "boolean" && value === true))) throw new Error(`<${need.component}> needs a \`${need.prop}\` prop that accepts ${need.values.map((value) => JSON.stringify(value)).join(", ")}, which Markdown sets`);
246
+ }
247
+ }
248
+ //#endregion
249
+ export { MARKDOWN_ELEMENTS, checkMarkdownElements, createChecker };
@@ -0,0 +1,16 @@
1
+ //#region src/confirm.d.ts
2
+ /**
3
+ * Where the server records which tokens were used. `claim` returns true the
4
+ * first time it sees `id` and false after; `expiresAt` (ms since the epoch)
5
+ * is when it may forget the id. A store that throws refuses the call.
6
+ */
7
+ export interface ConfirmationStore {
8
+ claim(id: string, expiresAt: number): Promise<boolean> | boolean;
9
+ }
10
+ /**
11
+ * The default store: a map in this process's memory, pruned of expired ids.
12
+ * Only valid for a single server process; behind a load balancer, pass a
13
+ * shared store, such as Redis `SET id 1 NX PXAT expiresAt`.
14
+ */
15
+ export declare function memoryConfirmations(now: () => number): ConfirmationStore;
16
+ //#endregion
@@ -0,0 +1,97 @@
1
+ //#region src/confirm.ts
2
+ /**
3
+ * Single-use confirmation tokens: HMAC-SHA-256 through Web Crypto, so they
4
+ * work on every server target. A token binds one call (procedure, input and
5
+ * subject), expires, and is claimed once through a `ConfirmationStore`.
6
+ */
7
+ /** How long a token stays valid: five minutes. */
8
+ const CONFIRM_TTL_MS = 3e5;
9
+ /**
10
+ * The host's confirmation text, made fit for one dialog. The host writes
11
+ * the message, but the input it reads came from the program, which a prompt
12
+ * injection may have written, so an unbounded value could fill the dialog
13
+ * or push the question out of view. Control characters and line breaks
14
+ * become spaces, runs of spaces collapse, and text past
15
+ * `MAX_CONFIRM_MESSAGE` characters ends in an ellipsis.
16
+ */
17
+ function confirmMessage(text) {
18
+ const flat = text.replaceAll(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim();
19
+ if (flat.length <= 200) return flat;
20
+ return `${flat.slice(0, 199).trimEnd()}…`;
21
+ }
22
+ /**
23
+ * The default store: a map in this process's memory, pruned of expired ids.
24
+ * Only valid for a single server process; behind a load balancer, pass a
25
+ * shared store, such as Redis `SET id 1 NX PXAT expiresAt`.
26
+ */
27
+ function memoryConfirmations(now) {
28
+ const claimed = /* @__PURE__ */ new Map();
29
+ return { claim(id, expiresAt) {
30
+ const time = now();
31
+ for (const [key, expiry] of claimed) if (expiry <= time) claimed.delete(key);
32
+ if (claimed.has(id)) return false;
33
+ claimed.set(id, expiresAt);
34
+ return true;
35
+ } };
36
+ }
37
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
38
+ /** JSON with every object's keys sorted, so equal inputs give equal text. */
39
+ function canonicalJson(value) {
40
+ return JSON.stringify(value ?? null, (_key, entry) => isObject(entry) ? Object.fromEntries(Object.keys(entry).toSorted().map((key) => [key, entry[key]])) : entry);
41
+ }
42
+ const UUID = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/;
43
+ function base64url(bytes) {
44
+ let text = "";
45
+ for (const byte of new Uint8Array(bytes)) text += String.fromCharCode(byte);
46
+ return btoa(text).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
47
+ }
48
+ /** The bytes of a base64url string, or null when it is not one. */
49
+ function fromBase64url(text) {
50
+ if (!/^[\w-]*$/.test(text)) return null;
51
+ let binary;
52
+ try {
53
+ binary = atob(text.replaceAll("-", "+").replaceAll("_", "/"));
54
+ } catch {
55
+ return null;
56
+ }
57
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
58
+ }
59
+ /**
60
+ * Issues and checks tokens of the form `expiresAt.id.mac`. The MAC covers
61
+ * the binding as well, so the binding never travels and a token for another
62
+ * call fails as "invalid".
63
+ */
64
+ function createConfirmer(secret, store, now) {
65
+ const encoder = new TextEncoder();
66
+ const key = crypto.subtle.importKey("raw", encoder.encode(secret), {
67
+ hash: "SHA-256",
68
+ name: "HMAC"
69
+ }, false, ["sign", "verify"]);
70
+ const signed = (binding, expiresAt, id) => encoder.encode(JSON.stringify([
71
+ "pickle-confirm-1",
72
+ binding.name,
73
+ binding.subject,
74
+ expiresAt,
75
+ id,
76
+ canonicalJson(binding.input)
77
+ ]));
78
+ return {
79
+ async check(token, binding) {
80
+ const [expires = "", id = "", mac = "", ...rest] = token.split(".");
81
+ const expiresAt = Number(expires);
82
+ const signature = fromBase64url(mac);
83
+ if (rest.length > 0 || !Number.isSafeInteger(expiresAt) || !UUID.test(id) || signature === null) return "invalid";
84
+ if (String(expiresAt) !== expires || base64url(signature.buffer) !== mac) return "invalid";
85
+ if (!await crypto.subtle.verify("HMAC", await key, signature, signed(binding, expiresAt, id))) return "invalid";
86
+ if (expiresAt <= now()) return "expired";
87
+ return await store.claim(id, expiresAt) ? "ok" : "used";
88
+ },
89
+ async issue(binding) {
90
+ const expiresAt = now() + CONFIRM_TTL_MS;
91
+ const id = crypto.randomUUID();
92
+ return `${expiresAt}.${id}.${base64url(await crypto.subtle.sign("HMAC", await key, signed(binding, expiresAt, id)))}`;
93
+ }
94
+ };
95
+ }
96
+ //#endregion
97
+ export { CONFIRM_TTL_MS, canonicalJson, confirmMessage, createConfirmer, memoryConfirmations };
@@ -0,0 +1,17 @@
1
+ import "@jigs.design/compiler";
2
+ import { Catalog } from "@jigs.design/host";
3
+ import { Span } from "@jigs.design/runtime";
4
+ //#region src/design.d.ts
5
+ /** The design checks, by code. */
6
+ export type DesignCheckCode = "design/literal-braces" | "design/many-actions" | "design/many-primary" | "design/nested-card" | "design/trailing-divider" | "design/unlabelled-button" | "design/unlabelled-chart" | "design/unlabelled-field";
7
+ /** A design check's finding: advice, never an error. */
8
+ export interface Advice {
9
+ code: DesignCheckCode;
10
+ message: string;
11
+ severity: "advice";
12
+ /** The line the advice is about, numbered like the diagnostics beside it. */
13
+ span: Span;
14
+ }
15
+ /** Where the UI sits: a whole document, or one block of a chat reply. */
16
+ export type Placement = "chat" | "document";
17
+ //#endregion
package/dist/design.js ADDED
@@ -0,0 +1,145 @@
1
+ //#region src/design.ts
2
+ /** A chat block holds at most this many actions. */
3
+ const MAX_BLOCK_ACTIONS = 2;
4
+ const meaningful = (node) => node.type !== "text" || node.value.trim() !== "";
5
+ /** A prop's literal value, its default when unset, or `undefined` when an expression decides it. */
6
+ function literalProp(node, component, name) {
7
+ const value = node.props[name];
8
+ if (value === void 0) return component.props[name]?.default;
9
+ return value.kind === "literal" ? value.value : void 0;
10
+ }
11
+ /** A prop that is unset, or set to a blank string. */
12
+ function missing(node, name) {
13
+ const value = node.props[name];
14
+ return value === void 0 || value.kind === "literal" && String(value.value).trim() === "";
15
+ }
16
+ const INTERPOLATION = /\{([A-Za-z_$][\w$]*)\}/;
17
+ /** Every design check's findings on one compiled source. */
18
+ function designChecks(options) {
19
+ const { compiled } = options;
20
+ const components = new Map(options.catalog.components.map((component) => [component.name, component]));
21
+ const defines = new Map([...compiled.library, ...compiled.defines.map((entry) => [entry.name, entry])]);
22
+ const advice = [];
23
+ const seen = /* @__PURE__ */ new Set();
24
+ const report = (code, line, message) => {
25
+ const key = `${code}:${line}`;
26
+ if (!seen.has(key)) {
27
+ seen.add(key);
28
+ advice.push({
29
+ code,
30
+ message,
31
+ severity: "advice",
32
+ span: { line }
33
+ });
34
+ }
35
+ };
36
+ const actions = [];
37
+ let primaryProps = [];
38
+ const element = (node, frame, line) => {
39
+ const component = components.get(node.component);
40
+ const design = component?.design;
41
+ if (component === void 0 || design === void 0) return;
42
+ const tag = `<${node.component}>`;
43
+ const labelled = (prop) => Object.hasOwn(component.props, prop) && !missing(node, prop);
44
+ switch (design.role) {
45
+ case "action": {
46
+ const primary = design.primary !== void 0 && Object.entries(design.primary).every(([name, value]) => literalProp(node, component, name) === value);
47
+ if (primary && design.primary !== void 0) primaryProps = Object.keys(design.primary);
48
+ actions.push({
49
+ line,
50
+ primary,
51
+ repeated: frame.repeated
52
+ });
53
+ if (!node.children.some((child) => meaningful(child)) && Object.hasOwn(component.props, "ariaLabel") && !labelled("ariaLabel")) report("design/unlabelled-button", line, `This ${tag} has no label, so a screen reader cannot say what it does. Give it an \`ariaLabel\`, such as \`ariaLabel="Delete"\`.`);
54
+ break;
55
+ }
56
+ case "chart":
57
+ if (Object.hasOwn(component.props, "ariaLabel") && !labelled("ariaLabel")) report("design/unlabelled-chart", line, `This ${tag} has no \`ariaLabel\`. Give it one that states its takeaway, such as \`ariaLabel="Sales rose 12% in March"\`.`);
58
+ break;
59
+ case "field": {
60
+ const names = ["label", "ariaLabel"].filter((prop) => Object.hasOwn(component.props, prop));
61
+ if (names.length > 0 && !names.some((prop) => labelled(prop))) report("design/unlabelled-field", line, `This ${tag} has no ${names.map((prop) => `\`${prop}\``).join(" or ")}, so nothing names what it is for. Give it a \`${names[0] ?? "label"}\`.`);
62
+ break;
63
+ }
64
+ case "surface": if (frame.inCard) report("design/nested-card", line, `This ${tag} is inside another one. Keep one card and group what is inside it with a layout tag and \`gap\`, or put the cards one after the other.`);
65
+ }
66
+ };
67
+ const literalBraces = (props, line) => {
68
+ for (const [name, value] of Object.entries(props)) {
69
+ if (value.kind !== "literal") continue;
70
+ const found = INTERPOLATION.exec(String(value.value));
71
+ if (found !== null) report("design/literal-braces", line, `\`${name}="…{${found[1] ?? ""}}…"\` is plain text, so it shows the braces. Write the value as an expression: \`${name}={\`…\${${found[1] ?? ""}}…\`}\`.`);
72
+ }
73
+ };
74
+ const trailingDivider = (each, siblings, frame) => {
75
+ if (siblings.findLast((node) => meaningful(node)) !== each) return;
76
+ const last = each.children.findLast((node) => meaningful(node));
77
+ if (last?.type === "element" && last.component === "divider") report("design/trailing-divider", frame.line ?? last.line, `This <divider> ends every row, so one hangs under the last. Give the <each> \`index="i"\` and put the divider first, inside \`<if test={i > 0}>\`.`);
78
+ };
79
+ const walk = (nodes, frame) => {
80
+ for (const node of nodes) switch (node.type) {
81
+ case "element": {
82
+ const line = frame.line ?? node.line;
83
+ element(node, frame, line);
84
+ literalBraces(node.props, line);
85
+ const surface = components.get(node.component)?.design?.role === "surface";
86
+ walk(node.children, {
87
+ ...frame,
88
+ inCard: frame.inCard || surface
89
+ });
90
+ break;
91
+ }
92
+ case "each":
93
+ trailingDivider(node, nodes, frame);
94
+ walk(node.children, {
95
+ ...frame,
96
+ repeated: true
97
+ });
98
+ break;
99
+ case "if":
100
+ walk(node.children, frame);
101
+ break;
102
+ case "call": {
103
+ const define = defines.get(node.name);
104
+ if (define === void 0 || frame.within.has(node.name)) break;
105
+ walk(define.body, {
106
+ inCard: frame.inCard,
107
+ line: frame.line ?? (define.origin === "library" ? node.line : null),
108
+ repeated: frame.repeated,
109
+ slot: {
110
+ children: node.children,
111
+ frame
112
+ },
113
+ within: /* @__PURE__ */ new Set([...frame.within, node.name])
114
+ });
115
+ break;
116
+ }
117
+ case "slot": if (frame.slot !== null) walk(frame.slot.children, {
118
+ ...frame.slot.frame,
119
+ inCard: frame.inCard,
120
+ repeated: frame.repeated
121
+ });
122
+ }
123
+ };
124
+ walk(compiled.program.tree, {
125
+ inCard: false,
126
+ line: null,
127
+ repeated: false,
128
+ slot: null,
129
+ within: /* @__PURE__ */ new Set()
130
+ });
131
+ const primaries = actions.filter((action) => action.primary);
132
+ const second = primaries.find((action) => action.repeated) ?? primaries[1];
133
+ if (second !== void 0) {
134
+ const others = primaryProps.map((prop) => `\`${prop}\``).join(" or ");
135
+ report("design/many-primary", second.line, second.repeated ? `This primary button repeats with every row, so the view has many. Keep one primary button for the view's main action, and give the row buttons another ${others}.` : `This is a second primary button. Keep one for the view's main action, and give the others another ${others}.`);
136
+ }
137
+ if (options.placement === "chat") {
138
+ const repeated = actions.find((action) => action.repeated);
139
+ const over = repeated ?? actions[MAX_BLOCK_ACTIONS];
140
+ if (over !== void 0) report("design/many-actions", over.line, repeated === void 0 ? `This block has ${actions.length} buttons; a block in a chat reply has at most ${MAX_BLOCK_ACTIONS}. Keep the ones that matter most: the reader can ask for the rest.` : `This button repeats with every row, so the block has more than ${MAX_BLOCK_ACTIONS}. A block in a chat reply has at most ${MAX_BLOCK_ACTIONS} buttons: move the row's action to one button for the whole block, or leave it to the reader to ask.`);
141
+ }
142
+ return advice.toSorted((a, b) => a.span.line - b.span.line);
143
+ }
144
+ //#endregion
145
+ export { designChecks };
@@ -0,0 +1,22 @@
1
+ import { Diagnostic, PickleErrorCode, PickleErrorData } from "@jigs.design/runtime";
2
+ //#region src/errors.d.ts
3
+ /** Codes a product may throw from `context` or `run`. */
4
+ export type ThrownCode = Exclude<PickleErrorCode, "DECLINED" | "NETWORK" | "UNAVAILABLE" | "UNKNOWN_PROCEDURE">;
5
+ declare const BRAND: unique symbol;
6
+ /** An error whose code and message reach the browser. */
7
+ export declare class PickleError extends Error {
8
+ readonly [BRAND] = true;
9
+ readonly code: ThrownCode;
10
+ constructor(code: ThrownCode, message: string);
11
+ }
12
+ /**
13
+ * A source that failed a strict compile, or a spec in `specs` that failed at
14
+ * startup. `diagnostics` holds the error and warning diagnostics; the message
15
+ * lists them with their lines.
16
+ */
17
+ export declare class PickleCompileError extends Error {
18
+ readonly diagnostics: ReadonlyArray<Diagnostic>;
19
+ constructor(subject: string, diagnostics: ReadonlyArray<Diagnostic>);
20
+ }
21
+ //#endregion
22
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,62 @@
1
+ //#region src/errors.ts
2
+ const BRAND = Symbol.for("@jigs.design/server.PickleError");
3
+ /** An error whose code and message reach the browser. */
4
+ var PickleError = class extends Error {
5
+ [BRAND] = true;
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = "PickleError";
11
+ }
12
+ };
13
+ function isPickleError(cause) {
14
+ return typeof cause === "object" && cause !== null && BRAND in cause;
15
+ }
16
+ /** Diagnostics that make a stored spec or a strict compile fail. */
17
+ function fatalDiagnostics(diagnostics) {
18
+ return diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === "warning");
19
+ }
20
+ /**
21
+ * A source that failed a strict compile, or a spec in `specs` that failed at
22
+ * startup. `diagnostics` holds the error and warning diagnostics; the message
23
+ * lists them with their lines.
24
+ */
25
+ var PickleCompileError = class extends Error {
26
+ diagnostics;
27
+ constructor(subject, diagnostics) {
28
+ const lines = diagnostics.map((diagnostic) => `line ${diagnostic.span.line}: ${diagnostic.message}`);
29
+ super(`${subject} failed to compile:\n${lines.join("\n")}`);
30
+ this.diagnostics = diagnostics;
31
+ this.name = "PickleCompileError";
32
+ }
33
+ };
34
+ const STATUS = {
35
+ BAD_INPUT: 400,
36
+ CONFLICT: 409,
37
+ DECLINED: 403,
38
+ FORBIDDEN: 403,
39
+ INTERNAL: 500,
40
+ NETWORK: 502,
41
+ NOT_FOUND: 404,
42
+ RATE_LIMITED: 429,
43
+ UNAUTHORIZED: 401,
44
+ UNAVAILABLE: 503,
45
+ UNKNOWN_PROCEDURE: 404
46
+ };
47
+ function statusOf(code) {
48
+ return STATUS[code];
49
+ }
50
+ const INTERNAL_MESSAGE = "Something went wrong.";
51
+ /** The browser's view of a thrown value: a `PickleError` keeps its message, anything else does not. */
52
+ function errorData(error) {
53
+ return error === null ? {
54
+ code: "INTERNAL",
55
+ message: INTERNAL_MESSAGE
56
+ } : {
57
+ code: error.code,
58
+ message: error.message
59
+ };
60
+ }
61
+ //#endregion
62
+ export { INTERNAL_MESSAGE, PickleCompileError, PickleError, errorData, fatalDiagnostics, isPickleError, statusOf };
@@ -0,0 +1,10 @@
1
+ import { Advice, DesignCheckCode, Placement } from "./design.js";
2
+ import { ConfirmationStore, memoryConfirmations } from "./confirm.js";
3
+ import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, ModelFunction, ModelRequest, ModelRequestKind, PickleServer, PickleServerOptions, PolicyCall, RequiresAction, RunFunctions, SessionRequest, Turn, TurnEdits, TurnRewrite, TurnRound, Usage } from "./types.js";
4
+ import { chatBlockText } from "./chat.js";
5
+ import { PickleCompileError, PickleError, ThrownCode } from "./errors.js";
6
+ import { createPickleServer, defineRun } from "./server.js";
7
+ import { addUsage } from "./usage.js";
8
+ import { PromptMode } from "@jigs.design/prompt";
9
+ import { ChatBlock, ChatBlockStatus, Diagnostic, Message, PickleErrorCode, PickleErrorData, Program, SessionEvent, Span } from "@jigs.design/runtime";
10
+ export { type Advice, type CallContext, type CallOutcome, type CallReport, type CallReportOutcome, type CallRequest, type ChatBlock, type ChatBlockResult, type ChatBlockSource, type ChatBlockStatus, type ChatEvent, type ChatOptions, type ChatTurn, type CompileOptions, type ConfirmationStore, type ContextFunction, type DesignCheckCode, type Diagnostic, type EditFailureReason, type ErrorInfo, type Message, type ModelFunction, type ModelRequest, type ModelRequestKind, PickleCompileError, PickleError, type PickleErrorCode, type PickleErrorData, type PickleServer, type PickleServerOptions, type Placement, type PolicyCall, type Program, type PromptMode, type RequiresAction, type RunFunctions, type SessionEvent, type SessionRequest, type Span, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, addUsage, chatBlockText, createPickleServer, defineRun, memoryConfirmations };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { PickleCompileError, PickleError } from "./errors.js";
2
+ import { addUsage } from "./usage.js";
3
+ import { chatBlockText } from "./chat.js";
4
+ import { memoryConfirmations } from "./confirm.js";
5
+ import { createPickleServer, defineRun } from "./server.js";
6
+ export { PickleCompileError, PickleError, addUsage, chatBlockText, createPickleServer, defineRun, memoryConfirmations };