@jigs.design/server 0.2.0 → 0.3.1

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/dist/errors.d.ts CHANGED
@@ -10,8 +10,8 @@ export declare class JigsError extends Error {
10
10
  constructor(code: ThrownCode, message: string);
11
11
  }
12
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
13
+ * A source that failed a strict compile, or a page that failed at startup
14
+ * or at its first call. `diagnostics` holds the error and warning diagnostics; the message
15
15
  * lists them with their lines.
16
16
  */
17
17
  export declare class JigsCompileError extends Error {
package/dist/errors.js CHANGED
@@ -13,13 +13,13 @@ var JigsError = class extends Error {
13
13
  function isJigsError(cause) {
14
14
  return typeof cause === "object" && cause !== null && BRAND in cause;
15
15
  }
16
- /** Diagnostics that make a stored spec or a strict compile fail. */
16
+ /** Diagnostics that make a page or a strict compile fail. */
17
17
  function fatalDiagnostics(diagnostics) {
18
18
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === "warning");
19
19
  }
20
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
21
+ * A source that failed a strict compile, or a page that failed at startup
22
+ * or at its first call. `diagnostics` holds the error and warning diagnostics; the message
23
23
  * lists them with their lines.
24
24
  */
25
25
  var JigsCompileError = class extends Error {
package/dist/events.js ADDED
@@ -0,0 +1,129 @@
1
+ import { INTERNAL_MESSAGE } from "./errors.js";
2
+ import { sseFrame, sseResponse } from "./handler.js";
3
+ //#region src/events.ts
4
+ /** The longest `Last-Event-ID`, and event id, the stream accepts. */
5
+ const MAX_EVENT_ID = 200;
6
+ const PRINTABLE = /^[ -~]*$/;
7
+ /** The reconnect delay the stream asks for: 3 s, plus up to 2 s so a fleet of tabs spreads out. */
8
+ const RETRY_MS = 3e3;
9
+ const RETRY_JITTER_MS = 2e3;
10
+ const isEventId = (id) => id.length <= MAX_EVENT_ID && PRINTABLE.test(id);
11
+ /** An event as the client reads it: a page's `saved` mark stays here. */
12
+ function onTheWire(event) {
13
+ if (event.type !== "invalidate-pages") return event;
14
+ return {
15
+ pages: event.pages.map((page) => page.props === void 0 ? { name: page.name } : {
16
+ name: page.name,
17
+ props: page.props
18
+ }),
19
+ type: event.type
20
+ };
21
+ }
22
+ /** The client's `Last-Event-ID`, or null when it has none or one out of bounds. */
23
+ function sinceOf(request) {
24
+ const since = request.headers.get("last-event-id");
25
+ return since === null || since === "" || !isEventId(since) ? null : since;
26
+ }
27
+ /**
28
+ * Text frames written as they come and read by the response one at a time:
29
+ * `push` queues one, `end` ends the iteration once the queue drains.
30
+ */
31
+ function frameQueue() {
32
+ const queued = [];
33
+ let ended = false;
34
+ let wake = null;
35
+ const notify = () => {
36
+ wake?.();
37
+ wake = null;
38
+ };
39
+ return {
40
+ end: () => {
41
+ ended = true;
42
+ notify();
43
+ },
44
+ get ended() {
45
+ return ended;
46
+ },
47
+ async *frames() {
48
+ for (;;) {
49
+ const next = queued.shift();
50
+ if (next !== void 0) {
51
+ yield next;
52
+ continue;
53
+ }
54
+ if (ended) return;
55
+ await new Promise((resolve) => {
56
+ wake = resolve;
57
+ });
58
+ }
59
+ },
60
+ push: (frame) => {
61
+ if (ended) return;
62
+ queued.push(frame);
63
+ notify();
64
+ }
65
+ };
66
+ }
67
+ /**
68
+ * One client's push stream: runs the host's `events` with a `send` that
69
+ * writes to it, and the unsubscribe it returned once the stream ends.
70
+ */
71
+ async function eventStream(deps, request, context) {
72
+ const queue = frameQueue();
73
+ const abort = new AbortController();
74
+ const leave = () => abort.abort();
75
+ if (context.signal.aborted) abort.abort();
76
+ else context.signal.addEventListener("abort", leave, { once: true });
77
+ let unsubscribe = null;
78
+ let unsubscribed = false;
79
+ const stop = () => {
80
+ context.signal.removeEventListener("abort", leave);
81
+ queue.end();
82
+ if (!unsubscribed && unsubscribe !== null) {
83
+ unsubscribed = true;
84
+ try {
85
+ unsubscribe();
86
+ } catch (error) {
87
+ deps.onError(error, { type: "events" });
88
+ }
89
+ }
90
+ };
91
+ abort.signal.addEventListener("abort", stop, { once: true });
92
+ const send = (event, options) => {
93
+ if (options?.id !== void 0 && !isEventId(options.id)) throw new Error(`An event id must be at most ${MAX_EVENT_ID} printable characters; got "${options.id.slice(0, 40)}"`);
94
+ queue.push(sseFrame(onTheWire(event), options?.id));
95
+ };
96
+ const close = () => {
97
+ queue.push(sseFrame({ type: "close" }));
98
+ queue.end();
99
+ };
100
+ try {
101
+ unsubscribe = await deps.events({
102
+ close,
103
+ ctx: context.ctx,
104
+ send,
105
+ signal: abort.signal,
106
+ since: sinceOf(request)
107
+ });
108
+ } catch (error) {
109
+ deps.onError(error, { type: "events" });
110
+ abort.abort();
111
+ return deps.http.failure(request, {
112
+ code: "INTERNAL",
113
+ message: INTERNAL_MESSAGE
114
+ });
115
+ }
116
+ if (abort.signal.aborted) stop();
117
+ return sseResponse(deps.http, request, queue.frames(), {
118
+ abort,
119
+ cleanup: () => abort.abort(),
120
+ failed: (cause) => {
121
+ deps.onError(cause, { type: "events" });
122
+ return sseFrame({ type: "close" });
123
+ },
124
+ retryMs: RETRY_MS + Math.floor(Math.random() * RETRY_JITTER_MS),
125
+ vary: "Accept"
126
+ });
127
+ }
128
+ //#endregion
129
+ export { eventStream };
@@ -0,0 +1,4 @@
1
+ import "./types.js";
2
+ import { Procedures } from "@jigs.design/host";
3
+ import { JigsErrorData, PageProps, SessionEvent } from "@jigs.design/protocol";
4
+ import { z } from "zod";
@@ -0,0 +1,344 @@
1
+ import { INTERNAL_MESSAGE, statusOf } from "./errors.js";
2
+ import { z } from "zod";
3
+ //#region src/handler.ts
4
+ const VERSION_HEADER = "jigs-version";
5
+ const HEARTBEAT_MS = 15e3;
6
+ /**
7
+ * The body as text, or null once it passes `max` bytes. Reading stops there,
8
+ * so an oversized body is never held in memory.
9
+ */
10
+ async function readLimited(request, max) {
11
+ if (request.body === null) return "";
12
+ const reader = request.body.getReader();
13
+ const chunks = [];
14
+ let size = 0;
15
+ for (;;) {
16
+ const chunk = await reader.read();
17
+ if (chunk.done) break;
18
+ size += chunk.value.byteLength;
19
+ if (size > max) {
20
+ await reader.cancel();
21
+ return null;
22
+ }
23
+ chunks.push(chunk.value);
24
+ }
25
+ const bytes = new Uint8Array(size);
26
+ let offset = 0;
27
+ for (const chunk of chunks) {
28
+ bytes.set(chunk, offset);
29
+ offset += chunk.byteLength;
30
+ }
31
+ return new TextDecoder().decode(bytes);
32
+ }
33
+ /**
34
+ * An origin such as `https://app.example.com`. Throws on anything else,
35
+ * including an entry with no `http:` or `https:` scheme: `localhost:3000`
36
+ * parses as a URL whose origin is `"null"`, the `Origin` that sandboxed
37
+ * frames and `file:` pages send.
38
+ */
39
+ function originOf(entry) {
40
+ let url = null;
41
+ try {
42
+ url = new URL(entry);
43
+ } catch {}
44
+ if (url === null || url.protocol !== "http:" && url.protocol !== "https:" || url.origin === "null") throw new Error(`"${entry}" is not an origin: write its scheme and host, such as https://app.example.com`);
45
+ return url.origin;
46
+ }
47
+ const bodySchema = z.discriminatedUnion("type", [
48
+ z.object({
49
+ confirmation: z.string().max(1e3).optional(),
50
+ input: z.json().optional(),
51
+ kind: z.enum([
52
+ "action",
53
+ "mutation",
54
+ "query"
55
+ ], { error: "Send the procedure's kind as \"kind\": \"query\", \"mutation\" or \"action\"" }),
56
+ name: z.string().max(200),
57
+ type: z.literal("call")
58
+ }),
59
+ z.object({
60
+ document: z.json().optional(),
61
+ messages: z.json(),
62
+ options: z.json().optional(),
63
+ type: z.literal("session")
64
+ }),
65
+ z.object({
66
+ name: z.string().min(1).max(200),
67
+ props: z.record(z.string(), z.union([
68
+ z.boolean(),
69
+ z.number(),
70
+ z.string()
71
+ ]), { error: "Send props as an object of strings, numbers and booleans" }).optional(),
72
+ type: z.literal("page")
73
+ })
74
+ ]);
75
+ /**
76
+ * Runs a `safeParse`, with a value nested too deeply to walk reported as a
77
+ * failure instead of a thrown `RangeError`.
78
+ */
79
+ function parseBounded(parse) {
80
+ let parsed;
81
+ try {
82
+ parsed = parse();
83
+ } catch {
84
+ return {
85
+ message: "The request body is nested too deeply.",
86
+ success: false
87
+ };
88
+ }
89
+ return parsed.success ? {
90
+ data: parsed.data,
91
+ success: true
92
+ } : {
93
+ message: issuesText(parsed.error),
94
+ success: false
95
+ };
96
+ }
97
+ function issuesText(error) {
98
+ return error.issues.slice(0, 3).map((issue) => issue.path.length === 0 ? issue.message : `${issue.path.join(".")}: ${issue.message}`).join("; ");
99
+ }
100
+ function createHttp(options) {
101
+ const serverOrigin = (request) => options.ownOrigin ?? new URL(request.url).origin;
102
+ const corsHeaders = (request) => {
103
+ const headers = new Headers({ [VERSION_HEADER]: options.version });
104
+ const origin = request.headers.get("origin");
105
+ if (origin !== null && options.allowedOrigins.has(origin)) {
106
+ headers.set("access-control-allow-origin", origin);
107
+ headers.set("access-control-allow-credentials", "true");
108
+ headers.set("access-control-expose-headers", VERSION_HEADER);
109
+ headers.append("vary", "Origin");
110
+ }
111
+ return headers;
112
+ };
113
+ const json = (request, status, value) => {
114
+ const headers = corsHeaders(request);
115
+ headers.set("content-type", "application/json");
116
+ headers.set("cache-control", "no-store");
117
+ return new Response(JSON.stringify(value), {
118
+ headers,
119
+ status
120
+ });
121
+ };
122
+ const foreignOrigin = (request) => {
123
+ const origin = request.headers.get("origin");
124
+ return origin !== null && origin !== serverOrigin(request) && !options.allowedOrigins.has(origin) ? {
125
+ code: "FORBIDDEN",
126
+ message: "This origin may not call this server."
127
+ } : null;
128
+ };
129
+ const refusal = (request) => {
130
+ const foreign = foreignOrigin(request);
131
+ if (foreign !== null) return foreign;
132
+ if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) return {
133
+ code: "BAD_INPUT",
134
+ message: "Send the request body as application/json."
135
+ };
136
+ return null;
137
+ };
138
+ return {
139
+ corsHeaders,
140
+ failure: (request, error) => json(request, statusOf(error.code), { error }),
141
+ foreignOrigin,
142
+ json,
143
+ refusal
144
+ };
145
+ }
146
+ /** One server-sent event: an optional `id:` line, then its JSON as `data:`. */
147
+ function sseFrame(value, id) {
148
+ const data = `data: ${JSON.stringify(value)}\n\n`;
149
+ return id === void 0 ? data : `id: ${id}\n${data}`;
150
+ }
151
+ /**
152
+ * A server-sent event stream of already-framed text, with a `: keep-alive`
153
+ * comment every 15 s. Cancelling the body (the client left) aborts `abort`
154
+ * and ends `frames`. Session turns and the push stream share it.
155
+ */
156
+ function sseResponse(http, request, frames, options) {
157
+ const encoder = new TextEncoder();
158
+ const iterator = frames[Symbol.asyncIterator]();
159
+ let heartbeat;
160
+ let open = true;
161
+ const finish = () => {
162
+ if (!open) return;
163
+ open = false;
164
+ clearInterval(heartbeat);
165
+ options.cleanup();
166
+ };
167
+ const body = new ReadableStream({
168
+ async cancel() {
169
+ finish();
170
+ options.abort.abort();
171
+ await iterator.return?.();
172
+ },
173
+ async pull(controller) {
174
+ let next;
175
+ try {
176
+ next = await iterator.next();
177
+ } catch (error) {
178
+ finish();
179
+ controller.enqueue(encoder.encode(options.failed(error)));
180
+ controller.close();
181
+ return;
182
+ }
183
+ if (next.done === true) {
184
+ finish();
185
+ controller.close();
186
+ return;
187
+ }
188
+ controller.enqueue(encoder.encode(next.value));
189
+ },
190
+ start(controller) {
191
+ if (options.retryMs !== void 0) controller.enqueue(encoder.encode(`retry: ${options.retryMs}\n\n`));
192
+ heartbeat = setInterval(() => {
193
+ if (!open) return;
194
+ try {
195
+ controller.enqueue(encoder.encode(": keep-alive\n\n"));
196
+ } catch {
197
+ finish();
198
+ }
199
+ }, HEARTBEAT_MS);
200
+ }
201
+ });
202
+ const headers = http.corsHeaders(request);
203
+ headers.set("content-type", "text/event-stream; charset=utf-8");
204
+ headers.set("cache-control", "no-store");
205
+ headers.set("x-accel-buffering", "no");
206
+ if (options.vary !== void 0) headers.append("vary", options.vary);
207
+ return new Response(body, {
208
+ headers,
209
+ status: 200
210
+ });
211
+ }
212
+ /** Session events as a server-sent event stream. */
213
+ function sessionStream(deps, request, events, abort, cleanup) {
214
+ async function* frames() {
215
+ for await (const event of events) yield sseFrame(event);
216
+ }
217
+ return sseResponse(deps.http, request, frames(), {
218
+ abort,
219
+ cleanup,
220
+ failed: (cause) => {
221
+ deps.onError(cause, { type: "session" });
222
+ return sseFrame({
223
+ error: {
224
+ code: "INTERNAL",
225
+ message: INTERNAL_MESSAGE
226
+ },
227
+ type: "error"
228
+ });
229
+ }
230
+ });
231
+ }
232
+ /**
233
+ * Whether a `GET` asks for the push stream: its `Accept` lists
234
+ * `text/event-stream` by name. A wildcard is not a match, so the frame's own
235
+ * loads (an iframe's `text/html`, the view's policy probe) still get the
236
+ * frame.
237
+ */
238
+ function acceptsEventStream(request) {
239
+ return (request.headers.get("accept") ?? "").split(",").some((range) => (range.split(";")[0] ?? "").trim().toLowerCase() === "text/event-stream");
240
+ }
241
+ /**
242
+ * The fetch handler: preflight, the frame, the push stream, calls, pages
243
+ * and session turns. The core
244
+ * and the model server build one each, differing only in `session`, so they
245
+ * read and refuse requests the same way.
246
+ */
247
+ function createHandler(deps) {
248
+ const { http, maxBodyBytes } = deps;
249
+ const tooLarge = (request) => http.failure(request, {
250
+ code: "BAD_INPUT",
251
+ message: `The request body is larger than ${maxBodyBytes} bytes.`
252
+ });
253
+ return async (request) => {
254
+ const origin = request.headers.get("origin");
255
+ if (request.method === "OPTIONS") {
256
+ const headers = http.corsHeaders(request);
257
+ if (origin !== null && deps.allowedOrigins.has(origin)) {
258
+ headers.set("access-control-allow-methods", "GET, POST");
259
+ headers.set("access-control-allow-headers", request.headers.get("access-control-request-headers") ?? "content-type");
260
+ headers.set("access-control-max-age", "600");
261
+ }
262
+ return new Response(null, {
263
+ headers,
264
+ status: 204
265
+ });
266
+ }
267
+ if (request.method === "GET") return acceptsEventStream(request) ? deps.events(request) : deps.frame(request);
268
+ if (request.method !== "POST") {
269
+ const headers = http.corsHeaders(request);
270
+ headers.set("allow", "GET, POST, OPTIONS");
271
+ return new Response(null, {
272
+ headers,
273
+ status: 405
274
+ });
275
+ }
276
+ const refused = http.refusal(request);
277
+ if (refused !== null) return http.failure(request, refused);
278
+ if (Number(request.headers.get("content-length") ?? "0") > maxBodyBytes) return tooLarge(request);
279
+ let text;
280
+ try {
281
+ text = await readLimited(request, maxBodyBytes);
282
+ } catch {
283
+ return http.failure(request, {
284
+ code: "BAD_INPUT",
285
+ message: "The request body could not be read."
286
+ });
287
+ }
288
+ if (text === null) return tooLarge(request);
289
+ let raw;
290
+ try {
291
+ raw = JSON.parse(text);
292
+ } catch {
293
+ return http.failure(request, {
294
+ code: "BAD_INPUT",
295
+ message: "The request body is not JSON."
296
+ });
297
+ }
298
+ const body = parseBounded(() => bodySchema.safeParse(raw));
299
+ if (!body.success) return http.failure(request, {
300
+ code: "BAD_INPUT",
301
+ message: body.message
302
+ });
303
+ const context = await deps.contextFor(request, body.data.type);
304
+ if ("error" in context) return http.failure(request, context.error);
305
+ if (body.data.type === "page") {
306
+ const reply = await deps.page({
307
+ name: body.data.name,
308
+ props: body.data.props ?? {}
309
+ }, {
310
+ ctx: context.ctx,
311
+ signal: request.signal
312
+ });
313
+ return "error" in reply ? http.failure(request, reply.error) : http.json(request, 200, reply);
314
+ }
315
+ if (body.data.type === "call") {
316
+ const callRequest = {
317
+ input: body.data.input,
318
+ kind: body.data.kind,
319
+ name: body.data.name
320
+ };
321
+ if (body.data.confirmation !== void 0) callRequest.confirmation = body.data.confirmation;
322
+ const outcome = await deps.call(callRequest, {
323
+ ctx: context.ctx,
324
+ signal: request.signal
325
+ });
326
+ return http.json(request, "error" in outcome ? statusOf(outcome.error.code) : 200, outcome);
327
+ }
328
+ const routed = await deps.session(body.data, {
329
+ ctx: context.ctx,
330
+ signal: request.signal
331
+ });
332
+ if ("error" in routed) return http.failure(request, routed.error);
333
+ const abort = new AbortController();
334
+ const forward = () => abort.abort();
335
+ if (request.signal.aborted) abort.abort();
336
+ else request.signal.addEventListener("abort", forward, { once: true });
337
+ return sessionStream(deps, request, routed.turn({
338
+ ctx: context.ctx,
339
+ signal: abort.signal
340
+ }), abort, () => request.signal.removeEventListener("abort", forward));
341
+ };
342
+ }
343
+ //#endregion
344
+ export { VERSION_HEADER, acceptsEventStream, createHandler, createHttp, issuesText, originOf, parseBounded, sseFrame, sseResponse };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Advice, DesignCheckCode, Placement } from "./design.js";
2
2
  import { ConfirmationStore, memoryConfirmations } from "./confirm.js";
3
- import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, JigsServer, JigsServerOptions, ModelFunction, ModelRequest, ModelRequestKind, PolicyCall, RequiresAction, RunFunctions, SessionRequest, Turn, TurnEdits, TurnRewrite, TurnRound, Usage } from "./types.js";
3
+ import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, EventSubscriber, EventsFunction, JigsModelServer, JigsServer, JigsServerOptions, ModelFunction, ModelOptions, ModelRequest, ModelRequestKind, PageCall, PageFunction, PagePolicy, PageResolver, PolicyCall, PushInvalidate, PushInvalidatePages, PushInvalidation, PushPage, PushSend, RequiresAction, ResolvedPage, RunFunctions, SavedPage, SessionRequest, Turn, TurnEdits, TurnRewrite, TurnRound, Usage } from "./types.js";
4
4
  import { chatBlockText } from "./chat.js";
5
5
  import { JigsCompileError, JigsError, ThrownCode } from "./errors.js";
6
- import { createJigsServer, defineRun } from "./server.js";
6
+ import { createJigsServer, defineRun } from "./core.js";
7
7
  import { addUsage } from "./usage.js";
8
- import { PromptMode } from "@jigs.design/prompt";
9
- import { ChatBlock, ChatBlockStatus, Diagnostic, JigsErrorCode, JigsErrorData, Message, Program, SessionEvent, Severity, Span } from "@jigs.design/protocol";
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, JigsCompileError, JigsError, type JigsErrorCode, type JigsErrorData, type JigsServer, type JigsServerOptions, type Message, type ModelFunction, type ModelRequest, type ModelRequestKind, type Placement, type PolicyCall, type Program, type PromptMode, type RequiresAction, type RunFunctions, type SessionEvent, type SessionRequest, type Severity, type Span, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, addUsage, chatBlockText, createJigsServer, defineRun, memoryConfirmations };
8
+ import { PromptMode, askOf, withDocument } from "@jigs.design/prompt";
9
+ import { ChatBlock, ChatBlockStatus, Diagnostic, JigsErrorCode, JigsErrorData, Message, PageProps, PageRef, Program, SessionDocument, SessionEvent, Severity, Span } from "@jigs.design/protocol";
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 EventSubscriber, type EventsFunction, JigsCompileError, JigsError, type JigsErrorCode, type JigsErrorData, type JigsModelServer, type JigsServer, type JigsServerOptions, type Message, type ModelFunction, type ModelOptions, type ModelRequest, type ModelRequestKind, type PageCall, type PageFunction, type PagePolicy, type PageProps, type PageRef, type PageResolver, type Placement, type PolicyCall, type Program, type PromptMode, type PushInvalidate, type PushInvalidatePages, type PushInvalidation, type PushPage, type PushSend, type RequiresAction, type ResolvedPage, type RunFunctions, type SavedPage, type SessionDocument, type SessionEvent, type SessionRequest, type Severity, type Span, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, addUsage, askOf, chatBlockText, createJigsServer, defineRun, memoryConfirmations, withDocument };
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ import { JigsCompileError, JigsError } from "./errors.js";
2
2
  import { addUsage } from "./usage.js";
3
3
  import { chatBlockText } from "./chat.js";
4
4
  import { memoryConfirmations } from "./confirm.js";
5
- import { createJigsServer, defineRun } from "./server.js";
6
- export { JigsCompileError, JigsError, addUsage, chatBlockText, createJigsServer, defineRun, memoryConfirmations };
5
+ import { createJigsServer, defineRun } from "./core.js";
6
+ import { askOf, withDocument } from "@jigs.design/prompt";
7
+ export { JigsCompileError, JigsError, addUsage, askOf, chatBlockText, createJigsServer, defineRun, memoryConfirmations, withDocument };
package/dist/inputs.js CHANGED
@@ -3,6 +3,11 @@ import { z } from "zod";
3
3
  //#region src/inputs.ts
4
4
  /** A handle declared as `const name = useMutation("procedure")`. */
5
5
  const MUTATION_HANDLE = /(?<![\w$.])(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*useMutation\(\s*(["'`])([A-Za-z_$][\w$]*)\2\s*\)/g;
6
+ /**
7
+ * What may follow a provided slot's name in `useQuery`: the call's end, or
8
+ * an empty input (`undefined` or `null`) before or without options.
9
+ */
10
+ const NO_INPUT = /^(?:,\s*)?\)|^,\s*(?:undefined|null)(?![\w$])/;
6
11
  const QUERY_CALL = /(?<![\w$.])useQuery\(\s*(["'`])([A-Za-z_$][\w$]*)\1\s*/g;
7
12
  /** At most this many problems per call, so one bad literal does not flood the repair. */
8
13
  const MAX_PROBLEMS = 3;
@@ -192,8 +197,18 @@ function inputDiagnostics(host, source) {
192
197
  for (const match of source.matchAll(QUERY_CALL)) {
193
198
  const name = match[2] ?? "";
194
199
  const procedure = procedureOf(name);
195
- if (procedure?.kind !== "query" || procedure.input === void 0) continue;
196
200
  const rest = match.index + match[0].length;
201
+ if (procedure?.kind === "data") {
202
+ if (NO_INPUT.test(source.slice(rest))) continue;
203
+ diagnostics.push({
204
+ code: "data_takes_no_input",
205
+ message: `useQuery("${name}") reads provided data, which takes no input. Drop the input; the app provides this data`,
206
+ severity: "error",
207
+ span: { line: lineAt(source, match.index) }
208
+ });
209
+ continue;
210
+ }
211
+ if (procedure?.kind !== "query" || procedure.input === void 0) continue;
197
212
  const char = source.charAt(rest);
198
213
  if (char !== "," && char !== ")") continue;
199
214
  const [input] = char === ")" ? [] : readArguments(source, rest + 1);