@north-light/crouter 0.3.192 → 0.3.194
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/api/client.d.ts +31 -10
- package/dist/api/client.js +83 -24
- package/dist/api/dto/bash-jobs.d.ts +24 -0
- package/dist/api/dto/bash-jobs.js +9 -0
- package/dist/api/dto/human.d.ts +3 -4
- package/dist/api/dto/inbox.d.ts +106 -65
- package/dist/api/dto/inbox.js +2 -8
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-memory/02-lifecycle/01-resident.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/input/overlay-owner.js +1 -1
- package/dist/clients/attach/overlays/auth.js +34 -8
- package/dist/clients/attach/overlays/pickers.d.ts +5 -8
- package/dist/clients/attach/overlays/pickers.js +37 -43
- package/dist/clients/attach/viewer.js +643 -620
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
- package/dist/clients/inbox/controller.d.ts +2 -3
- package/dist/clients/inbox/controller.js +41 -42
- package/dist/clients/inbox/page-adapter.d.ts +33 -0
- package/dist/clients/inbox/page-adapter.js +54 -0
- package/dist/clients/inbox/resolve.d.ts +2 -2
- package/dist/clients/inbox/resolve.js +1 -1
- package/dist/clients/inbox/review-visibility.d.ts +1 -1
- package/dist/clients/inbox/review-visibility.js +1 -1
- package/dist/clients/inbox/tui/ansi.d.ts +1 -1
- package/dist/clients/inbox/tui/ansi.js +2 -2
- package/dist/clients/inbox/tui/input.d.ts +1 -3
- package/dist/clients/inbox/tui/input.js +242 -553
- package/dist/clients/inbox/tui/panel.d.ts +3 -1
- package/dist/clients/inbox/tui/panel.js +59 -187
- package/dist/clients/inbox/tui/render.d.ts +0 -25
- package/dist/clients/inbox/tui/render.js +165 -416
- package/dist/clients/inbox/tui/slots.d.ts +32 -0
- package/dist/clients/inbox/tui/slots.js +156 -0
- package/dist/clients/inbox/tui/types.d.ts +25 -13
- package/dist/clients/inbox/tui.js +5 -3
- package/dist/commands/attention.js +2 -2
- package/dist/commands/human/doc.js +66 -20
- package/dist/commands/human/prompts.d.ts +3 -3
- package/dist/commands/human/prompts.js +158 -112
- package/dist/commands/human/queue.d.ts +0 -1
- package/dist/commands/human/queue.js +34 -94
- package/dist/commands/human/shared.d.ts +10 -4
- package/dist/commands/human/shared.js +62 -13
- package/dist/commands/human.js +8 -4
- package/dist/commands/memory/read.js +13 -5
- package/dist/commands/node/bash.js +18 -30
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/surface/node/focus.js +1 -1
- package/dist/core/__tests__/human-deliver.test.js +40 -46
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
- package/dist/core/__tests__/seam/dormancy-release.test.js +64 -10
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
- package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
- package/dist/core/auth-interaction.d.ts +2 -2
- package/dist/core/bash-jobs.d.ts +43 -0
- package/dist/core/bash-jobs.js +96 -2
- package/dist/core/canvas/__tests__/attention.test.js +16 -10
- package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
- package/dist/core/canvas/attention.js +1 -1
- package/dist/core/canvas/browse/app.js +1 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/remote-canvas-source.js +1 -1
- package/dist/core/canvas/types.d.ts +2 -1
- package/dist/core/config.js +4 -1
- package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-render.test.js +49 -0
- package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-tickets.test.js +112 -0
- package/dist/core/human/__tests__/page.test.d.ts +1 -0
- package/dist/core/human/__tests__/page.test.js +139 -0
- package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
- package/dist/core/human/claim.js +2 -2
- package/dist/core/human/convention.d.ts +4 -4
- package/dist/core/human/convention.js +6 -13
- package/dist/core/human/markdown-html.d.ts +8 -0
- package/dist/core/human/markdown-html.js +581 -0
- package/dist/core/human/page-catalog.d.ts +6 -0
- package/dist/core/human/page-catalog.js +43 -0
- package/dist/core/human/page-render.d.ts +3 -0
- package/dist/core/human/page-render.js +131 -0
- package/dist/core/human/page-schema.d.ts +312 -0
- package/dist/core/human/page-schema.js +291 -0
- package/dist/core/human/page-synth.d.ts +9 -0
- package/dist/core/human/page-synth.js +17 -0
- package/dist/core/human/page.d.ts +22 -0
- package/dist/core/human/page.js +427 -0
- package/dist/core/human/review-schema.d.ts +19 -0
- package/dist/core/human/review-schema.js +34 -0
- package/dist/core/human/scan.d.ts +12 -3
- package/dist/core/human/scan.js +69 -26
- package/dist/core/human/summary.d.ts +5 -6
- package/dist/core/human/summary.js +54 -37
- package/dist/core/human/tickets.d.ts +61 -11
- package/dist/core/human/tickets.js +100 -57
- package/dist/core/human/types.d.ts +21 -66
- package/dist/core/human/types.js +1 -5
- package/dist/core/inspector/model.d.ts +5 -0
- package/dist/core/inspector/model.js +5 -0
- package/dist/core/inspector/text.js +2 -2
- package/dist/core/inspector/tui.js +10 -3
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +4 -2
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/ticket-filter.d.ts +1 -1
- package/dist/core/review/ticket-filter.js +2 -8
- package/dist/core/runtime/broker/read-ops.js +3 -0
- package/dist/core/runtime/broker.js +19 -10
- package/dist/core/runtime/package-health.js +1 -1
- package/dist/core/runtime/persona.js +2 -1
- package/dist/core/runtime/tmux-bindings.js +8 -1
- package/dist/core/termrender/termrender.d.ts +1 -1
- package/dist/core/termrender/termrender.js +2 -2
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/tui/page-host.d.ts +1 -1
- package/dist/core/tui/page-host.js +2 -2
- package/dist/core/user-settings.d.ts +19 -0
- package/dist/core/user-settings.js +22 -0
- package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
- package/dist/daemon/api/handlers/bash-jobs.js +90 -0
- package/dist/daemon/api/handlers/human.js +16 -14
- package/dist/daemon/api/handlers/inbox.js +311 -227
- package/dist/daemon/api/router.d.ts +5 -1
- package/dist/daemon/api/router.js +5 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +23 -10
- package/dist/daemon/human/finish.d.ts +8 -3
- package/dist/daemon/human/finish.js +35 -15
- package/dist/daemon/reconcilers/broker-supervision.js +7 -1
- package/dist/daemon/reconcilers/dormant-inbox.js +8 -0
- package/dist/daemon/reconcilers/live-obligation.d.ts +2 -0
- package/dist/daemon/reconcilers/live-obligation.js +11 -0
- package/dist/pages/bundle.css +1 -0
- package/dist/pages/bundle.js +1347 -0
- package/dist/pages/comments.d.ts +34 -0
- package/dist/pages/comments.js +72 -0
- package/dist/pages/elements/cards.d.ts +17 -0
- package/dist/pages/elements/cards.js +785 -0
- package/dist/pages/elements/chart.d.ts +1 -0
- package/dist/pages/elements/chart.js +648 -0
- package/dist/pages/elements/options.d.ts +21 -0
- package/dist/pages/elements/options.js +604 -0
- package/dist/pages/elements/pages.d.ts +24 -0
- package/dist/pages/elements/pages.js +355 -0
- package/dist/pages/elements/slot.d.ts +21 -0
- package/dist/pages/elements/slot.js +105 -0
- package/dist/pages/elements/table.d.ts +19 -0
- package/dist/pages/elements/table.js +906 -0
- package/dist/pages/elements/text.d.ts +25 -0
- package/dist/pages/elements/text.js +951 -0
- package/dist/pages/entry.d.ts +8 -0
- package/dist/pages/entry.js +8 -0
- package/dist/pages/host.d.ts +132 -0
- package/dist/pages/host.js +149 -0
- package/dist/pages/register.d.ts +2 -0
- package/dist/pages/register.js +3 -0
- package/dist/pages/slot-config.d.ts +36 -0
- package/dist/pages/slot-config.js +50 -0
- package/dist/pages/types.d.ts +98 -0
- package/dist/pages/types.js +23 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -2
- package/dist/pi-extensions/canvas-bash-valve.js +25 -7
- package/dist/types.d.ts +4 -0
- package/dist/types.js +2 -0
- package/package.json +8 -8
- package/runtime.lock.json +156 -101
- package/scripts/install-runtime.mjs +172 -5
- package/dist/clients/inbox/deck-adapter.d.ts +0 -29
- package/dist/clients/inbox/deck-adapter.js +0 -63
- package/dist/core/human/deck-factories.d.ts +0 -8
- package/dist/core/human/deck-factories.js +0 -15
- package/dist/core/human/deck-schema.d.ts +0 -70
- package/dist/core/human/deck-schema.js +0 -92
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BUILTIN_PAGE_KINDS, isRegisteredPageKind } from './page-catalog.js';
|
|
3
|
+
export const PAGE_SLOT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
4
|
+
const idSchema = z.string().regex(PAGE_SLOT_ID_PATTERN, { error: 'slot id must match /^[A-Za-z0-9_-]{1,64}$/' });
|
|
5
|
+
const itemIdSchema = z.string().min(1);
|
|
6
|
+
const selectModeSchema = z.enum(['none', 'single', 'multi']);
|
|
7
|
+
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
8
|
+
function uniqueIds(items, ctx, label) {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
items.forEach((item, index) => {
|
|
11
|
+
if (seen.has(item.id))
|
|
12
|
+
ctx.addIssue({ code: 'custom', path: [index, 'id'], message: `duplicate ${label} id "${item.id}"` });
|
|
13
|
+
seen.add(item.id);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export const commentAnchorSchema = z.discriminatedUnion('kind', [
|
|
17
|
+
z.object({ kind: z.literal('whole') }).strict(),
|
|
18
|
+
z.object({ kind: z.literal('option'), optionId: itemIdSchema }).strict(),
|
|
19
|
+
z.object({ kind: z.literal('row'), rowId: itemIdSchema }).strict(),
|
|
20
|
+
z.object({ kind: z.literal('column'), columnId: itemIdSchema }).strict(),
|
|
21
|
+
z.object({ kind: z.literal('card'), cardId: itemIdSchema }).strict(),
|
|
22
|
+
z.object({ kind: z.literal('range'), start: z.number().int().nonnegative(), end: z.number().int().nonnegative(), quote: z.string() }).strict().superRefine((anchor, ctx) => {
|
|
23
|
+
if (anchor.end < anchor.start)
|
|
24
|
+
ctx.addIssue({ code: 'custom', path: ['end'], message: 'range end must be greater than or equal to start' });
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
export const commentSchema = z.object({ id: z.string().min(1), anchor: commentAnchorSchema, text: z.string() }).strict();
|
|
28
|
+
export const optionSchema = z.object({ id: itemIdSchema, label: z.string().min(1), description: z.string().optional() }).strict();
|
|
29
|
+
export const optionsConfigSchema = z.object({
|
|
30
|
+
options: z.array(optionSchema).min(1).superRefine((items, ctx) => uniqueIds(items, ctx, 'option')),
|
|
31
|
+
mode: z.enum(['single', 'multi']),
|
|
32
|
+
allowFreetext: z.boolean().optional(),
|
|
33
|
+
freetextLabel: z.string().optional(),
|
|
34
|
+
freetextPlaceholder: z.string().optional(),
|
|
35
|
+
}).strict();
|
|
36
|
+
export const optionsResponseSchema = z.object({ selectedOptionIds: z.array(itemIdSchema), comments: z.array(commentSchema), freetext: z.string().optional() }).strict();
|
|
37
|
+
export const textConfigSchema = z.object({ initialText: z.string(), editable: z.boolean().optional(), placeholder: z.string().optional() }).strict();
|
|
38
|
+
export const textResponseSchema = z.object({ text: z.string(), edited: z.boolean(), comments: z.array(commentSchema) }).strict();
|
|
39
|
+
export const tableColumnSchema = z.object({ id: itemIdSchema, label: z.string().min(1), align: z.enum(['left', 'right']).optional(), mono: z.boolean().optional() }).strict();
|
|
40
|
+
export const tableCellSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
41
|
+
export const tableRowSchema = z.object({ id: itemIdSchema, cells: z.record(z.string(), tableCellSchema) }).strict();
|
|
42
|
+
export const tableConfigSchema = z.object({
|
|
43
|
+
columns: z.array(tableColumnSchema).min(1).superRefine((items, ctx) => uniqueIds(items, ctx, 'column')),
|
|
44
|
+
rows: z.array(tableRowSchema).superRefine((items, ctx) => uniqueIds(items, ctx, 'row')).optional(),
|
|
45
|
+
source: z.string().optional(),
|
|
46
|
+
rowSelect: selectModeSchema.default('none'),
|
|
47
|
+
columnSelect: selectModeSchema.default('none'),
|
|
48
|
+
caption: z.string().optional(),
|
|
49
|
+
}).strict().superRefine((config, ctx) => {
|
|
50
|
+
if (config.rows !== undefined && config.source !== undefined)
|
|
51
|
+
ctx.addIssue({ code: 'custom', message: 'table rows and source are mutually exclusive' });
|
|
52
|
+
});
|
|
53
|
+
export const tableResponseSchema = z.object({ selectedRowIds: z.array(itemIdSchema), selectedColumnIds: z.array(itemIdSchema), comments: z.array(commentSchema) }).strict();
|
|
54
|
+
export const cardSchema = z.object({ id: itemIdSchema, title: z.string().min(1), subtitle: z.string().optional(), body: z.string().optional(), tag: z.string().optional() }).strict();
|
|
55
|
+
export const cardsConfigSchema = z.object({
|
|
56
|
+
cards: z.array(cardSchema).superRefine((items, ctx) => uniqueIds(items, ctx, 'card')).optional(),
|
|
57
|
+
source: z.string().optional(),
|
|
58
|
+
mode: z.enum(['single', 'multi']),
|
|
59
|
+
columns: z.union([z.literal(2), z.literal(3)]).optional(),
|
|
60
|
+
}).strict().superRefine((config, ctx) => {
|
|
61
|
+
if (config.cards !== undefined && config.source !== undefined)
|
|
62
|
+
ctx.addIssue({ code: 'custom', message: 'cards data and source are mutually exclusive' });
|
|
63
|
+
});
|
|
64
|
+
export const cardsResponseSchema = z.object({ selectedCardIds: z.array(itemIdSchema), comments: z.array(commentSchema) }).strict();
|
|
65
|
+
export const chartSeriesSchema = z.object({ field: z.string().min(1), label: z.string().optional() }).strict();
|
|
66
|
+
export const chartConfigSchema = z.object({
|
|
67
|
+
kind: z.enum(['line', 'bar', 'area']),
|
|
68
|
+
x: z.string().min(1),
|
|
69
|
+
y: z.array(chartSeriesSchema).min(1),
|
|
70
|
+
title: z.string().optional(),
|
|
71
|
+
data: z.unknown().optional(),
|
|
72
|
+
source: z.string().optional(),
|
|
73
|
+
}).strict();
|
|
74
|
+
const sourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), nodeId: z.string().optional() }).strict();
|
|
75
|
+
const rawSlotSchema = z.object({ id: idSchema.optional(), kind: z.string().min(1), step: z.number().int().nonnegative(), config: jsonObjectSchema, unvalidated: z.literal(true).optional() }).strict();
|
|
76
|
+
const rawPageManifestSchema = z.object({
|
|
77
|
+
schema: z.literal('crtr.page/v1'),
|
|
78
|
+
dialect: z.enum(['md', 'html']),
|
|
79
|
+
title: z.string().trim().min(1),
|
|
80
|
+
subtitle: z.string().optional(),
|
|
81
|
+
placement: z.enum(['inline', 'panel']),
|
|
82
|
+
document: z.enum(['page.md', 'page.html']),
|
|
83
|
+
steps: z.number().int().positive(),
|
|
84
|
+
slots: z.array(rawSlotSchema),
|
|
85
|
+
source: sourceSchema.optional(),
|
|
86
|
+
}).strict();
|
|
87
|
+
const configSchemas = {
|
|
88
|
+
options: optionsConfigSchema,
|
|
89
|
+
text: textConfigSchema,
|
|
90
|
+
table: tableConfigSchema,
|
|
91
|
+
cards: cardsConfigSchema,
|
|
92
|
+
chart: chartConfigSchema,
|
|
93
|
+
};
|
|
94
|
+
function issueText(error) {
|
|
95
|
+
return error.issues.map((issue) => `${issue.path.length === 0 ? 'value' : issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
96
|
+
}
|
|
97
|
+
/** The canonical response-bearing predicate used by every page consumer. */
|
|
98
|
+
export function isResponseBearingSlot(slot) {
|
|
99
|
+
if (slot.unvalidated === true)
|
|
100
|
+
return true;
|
|
101
|
+
if (slot.kind === 'options' || slot.kind === 'text' || slot.kind === 'cards')
|
|
102
|
+
return true;
|
|
103
|
+
if (slot.kind !== 'table')
|
|
104
|
+
return false;
|
|
105
|
+
const config = slot.config;
|
|
106
|
+
return (config.rowSelect ?? 'none') !== 'none' || (config.columnSelect ?? 'none') !== 'none';
|
|
107
|
+
}
|
|
108
|
+
/** Validate and normalize a derived or stored manifest against the resolved product catalog. */
|
|
109
|
+
export function validatePageManifest(parsed, productKinds = []) {
|
|
110
|
+
const manifest = rawPageManifestSchema.parse(parsed);
|
|
111
|
+
if (manifest.document !== (manifest.dialect === 'md' ? 'page.md' : 'page.html'))
|
|
112
|
+
throw new Error(`page document "${manifest.document}" does not match ${manifest.dialect} dialect`);
|
|
113
|
+
if (manifest.dialect === 'md' && manifest.steps !== 1)
|
|
114
|
+
throw new Error(`md page steps must be 1; received ${manifest.steps}`);
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const slots = manifest.slots.map((raw, index) => {
|
|
117
|
+
if (raw.step >= manifest.steps)
|
|
118
|
+
throw new Error(`slot ${raw.id === undefined ? `#${index}` : `"${raw.id}"`} has step ${raw.step}, outside page steps ${manifest.steps}`);
|
|
119
|
+
let slot;
|
|
120
|
+
if (BUILTIN_PAGE_KINDS.includes(raw.kind)) {
|
|
121
|
+
if (raw.unvalidated === true)
|
|
122
|
+
throw new Error(`built-in slot kind "${raw.kind}" cannot be unvalidated`);
|
|
123
|
+
const result = configSchemas[raw.kind].safeParse(raw.config);
|
|
124
|
+
if (!result.success)
|
|
125
|
+
throw new Error(`invalid config for slot ${raw.id === undefined ? `#${index}` : `"${raw.id}"`} (${raw.kind}): ${issueText(result.error)}`);
|
|
126
|
+
slot = { ...(raw.id === undefined ? {} : { id: raw.id }), kind: raw.kind, step: raw.step, config: result.data };
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
if (!isRegisteredPageKind(raw.kind, productKinds))
|
|
130
|
+
throw new Error(`unregistered page slot kind "${raw.kind}" at slot #${index}`);
|
|
131
|
+
if (raw.unvalidated !== true)
|
|
132
|
+
throw new Error(`registered product slot kind "${raw.kind}" must set unvalidated: true`);
|
|
133
|
+
slot = { ...(raw.id === undefined ? {} : { id: raw.id }), kind: raw.kind, step: raw.step, config: raw.config, unvalidated: true };
|
|
134
|
+
}
|
|
135
|
+
if (isResponseBearingSlot(slot) && slot.id === undefined)
|
|
136
|
+
throw new Error(`response-bearing slot kind "${slot.kind}" at slot #${index} requires an id`);
|
|
137
|
+
if (slot.id !== undefined) {
|
|
138
|
+
if (seen.has(slot.id))
|
|
139
|
+
throw new Error(`duplicate slot id "${slot.id}"`);
|
|
140
|
+
seen.add(slot.id);
|
|
141
|
+
}
|
|
142
|
+
return slot;
|
|
143
|
+
});
|
|
144
|
+
return { ...manifest, slots };
|
|
145
|
+
}
|
|
146
|
+
export function createPageManifestSchema(productKinds = []) {
|
|
147
|
+
return z.custom((value) => {
|
|
148
|
+
try {
|
|
149
|
+
validatePageManifest(value, productKinds);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}, { error: 'invalid page manifest' }).transform((value) => validatePageManifest(value, productKinds));
|
|
156
|
+
}
|
|
157
|
+
/** Built-in-only manifest schema; product-aware callers use createPageManifestSchema. */
|
|
158
|
+
export const pageManifestSchema = createPageManifestSchema();
|
|
159
|
+
function assertUnique(values, slotId, field) {
|
|
160
|
+
if (new Set(values).size !== values.length)
|
|
161
|
+
throw new Error(`slot "${slotId}" has duplicate id in ${field}`);
|
|
162
|
+
}
|
|
163
|
+
function validateComments(slot, comments) {
|
|
164
|
+
const config = slot.config;
|
|
165
|
+
for (const comment of comments) {
|
|
166
|
+
const anchor = comment.anchor;
|
|
167
|
+
if (anchor.kind === 'whole')
|
|
168
|
+
continue;
|
|
169
|
+
if (slot.kind === 'options' && anchor.kind === 'option') {
|
|
170
|
+
if (!config.options.some((option) => option.id === anchor.optionId))
|
|
171
|
+
throw new Error(`slot "${slot.id}" comment names unknown option id "${anchor.optionId}"`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (slot.kind === 'text' && anchor.kind === 'range')
|
|
175
|
+
continue;
|
|
176
|
+
if (slot.kind === 'table' && anchor.kind === 'row') {
|
|
177
|
+
if (!(config.rows ?? []).some((row) => row.id === anchor.rowId))
|
|
178
|
+
throw new Error(`slot "${slot.id}" comment names unknown row id "${anchor.rowId}"`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (slot.kind === 'table' && anchor.kind === 'column') {
|
|
182
|
+
if (!config.columns.some((column) => column.id === anchor.columnId))
|
|
183
|
+
throw new Error(`slot "${slot.id}" comment names unknown column id "${anchor.columnId}"`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (slot.kind === 'cards' && anchor.kind === 'card') {
|
|
187
|
+
if (!(config.cards ?? []).some((card) => card.id === anchor.cardId))
|
|
188
|
+
throw new Error(`slot "${slot.id}" comment names unknown card id "${anchor.cardId}"`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`slot "${slot.id}" does not allow comment anchor kind "${anchor.kind}"`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function parseResponse(slot, raw) {
|
|
195
|
+
if (slot.unvalidated === true) {
|
|
196
|
+
const result = jsonObjectSchema.safeParse(raw);
|
|
197
|
+
if (!result.success)
|
|
198
|
+
throw new Error(`response for unvalidated slot "${slot.id}" must be an object`);
|
|
199
|
+
return result.data;
|
|
200
|
+
}
|
|
201
|
+
if (slot.kind === 'options') {
|
|
202
|
+
const result = optionsResponseSchema.safeParse(raw);
|
|
203
|
+
if (!result.success)
|
|
204
|
+
throw new Error(`invalid response for slot "${slot.id}": ${issueText(result.error)}`);
|
|
205
|
+
const response = result.data;
|
|
206
|
+
const config = slot.config;
|
|
207
|
+
assertUnique(response.selectedOptionIds, slot.id, 'selectedOptionIds');
|
|
208
|
+
for (const id of response.selectedOptionIds)
|
|
209
|
+
if (!config.options.some((option) => option.id === id))
|
|
210
|
+
throw new Error(`slot "${slot.id}" selects unknown option id "${id}"`);
|
|
211
|
+
if (response.freetext !== undefined && config.allowFreetext !== true)
|
|
212
|
+
throw new Error(`slot "${slot.id}" does not allow freetext`);
|
|
213
|
+
if (config.mode === 'single' && response.selectedOptionIds.length > 1)
|
|
214
|
+
throw new Error(`slot "${slot.id}" is single-select but received ${response.selectedOptionIds.length} option ids`);
|
|
215
|
+
const hasFreetext = config.allowFreetext === true && (response.freetext?.trim() ?? '') !== '';
|
|
216
|
+
if (config.mode === 'single' && response.selectedOptionIds.length !== 1 && !hasFreetext)
|
|
217
|
+
throw new Error(`single-select slot "${slot.id}" requires exactly one selection or nonempty allowed freetext`);
|
|
218
|
+
if (config.mode === 'multi' && response.selectedOptionIds.length === 0 && response.comments.length === 0 && !hasFreetext)
|
|
219
|
+
throw new Error(`multi-select slot "${slot.id}" requires a selection, comment, or nonempty allowed freetext`);
|
|
220
|
+
validateComments(slot, response.comments);
|
|
221
|
+
return response;
|
|
222
|
+
}
|
|
223
|
+
if (slot.kind === 'text') {
|
|
224
|
+
const result = textResponseSchema.safeParse(raw);
|
|
225
|
+
if (!result.success)
|
|
226
|
+
throw new Error(`invalid response for slot "${slot.id}": ${issueText(result.error)}`);
|
|
227
|
+
validateComments(slot, result.data.comments);
|
|
228
|
+
return result.data;
|
|
229
|
+
}
|
|
230
|
+
if (slot.kind === 'table') {
|
|
231
|
+
const result = tableResponseSchema.safeParse(raw);
|
|
232
|
+
if (!result.success)
|
|
233
|
+
throw new Error(`invalid response for slot "${slot.id}": ${issueText(result.error)}`);
|
|
234
|
+
const response = result.data;
|
|
235
|
+
const config = slot.config;
|
|
236
|
+
assertUnique(response.selectedRowIds, slot.id, 'selectedRowIds');
|
|
237
|
+
assertUnique(response.selectedColumnIds, slot.id, 'selectedColumnIds');
|
|
238
|
+
const rowIds = new Set((config.rows ?? []).map((row) => row.id));
|
|
239
|
+
const columnIds = new Set(config.columns.map((column) => column.id));
|
|
240
|
+
for (const id of response.selectedRowIds)
|
|
241
|
+
if (!rowIds.has(id))
|
|
242
|
+
throw new Error(`slot "${slot.id}" selects unknown row id "${id}"`);
|
|
243
|
+
for (const id of response.selectedColumnIds)
|
|
244
|
+
if (!columnIds.has(id))
|
|
245
|
+
throw new Error(`slot "${slot.id}" selects unknown column id "${id}"`);
|
|
246
|
+
if ((config.rowSelect === 'none' && response.selectedRowIds.length > 0) || (config.rowSelect === 'single' && response.selectedRowIds.length > 1))
|
|
247
|
+
throw new Error(`slot "${slot.id}" response violates row select mode "${config.rowSelect}"`);
|
|
248
|
+
if ((config.columnSelect === 'none' && response.selectedColumnIds.length > 0) || (config.columnSelect === 'single' && response.selectedColumnIds.length > 1))
|
|
249
|
+
throw new Error(`slot "${slot.id}" response violates column select mode "${config.columnSelect}"`);
|
|
250
|
+
validateComments(slot, response.comments);
|
|
251
|
+
return response;
|
|
252
|
+
}
|
|
253
|
+
if (slot.kind === 'cards') {
|
|
254
|
+
const result = cardsResponseSchema.safeParse(raw);
|
|
255
|
+
if (!result.success)
|
|
256
|
+
throw new Error(`invalid response for slot "${slot.id}": ${issueText(result.error)}`);
|
|
257
|
+
const response = result.data;
|
|
258
|
+
const config = slot.config;
|
|
259
|
+
assertUnique(response.selectedCardIds, slot.id, 'selectedCardIds');
|
|
260
|
+
for (const id of response.selectedCardIds)
|
|
261
|
+
if (!(config.cards ?? []).some((card) => card.id === id))
|
|
262
|
+
throw new Error(`slot "${slot.id}" selects unknown card id "${id}"`);
|
|
263
|
+
if (config.mode === 'single' && response.selectedCardIds.length > 1)
|
|
264
|
+
throw new Error(`slot "${slot.id}" is single-select but received ${response.selectedCardIds.length} card ids`);
|
|
265
|
+
validateComments(slot, response.comments);
|
|
266
|
+
return response;
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`slot "${slot.id}" kind "${slot.kind}" does not accept a response`);
|
|
269
|
+
}
|
|
270
|
+
function validateResponses(manifest, raw, partial) {
|
|
271
|
+
const result = jsonObjectSchema.safeParse(raw);
|
|
272
|
+
if (!result.success)
|
|
273
|
+
throw new Error('page responses must be an object keyed by slot id');
|
|
274
|
+
const expected = new Map(manifest.slots.filter(isResponseBearingSlot).map((slot) => [slot.id, slot]));
|
|
275
|
+
const responses = {};
|
|
276
|
+
for (const [id, value] of Object.entries(result.data)) {
|
|
277
|
+
const slot = expected.get(id);
|
|
278
|
+
if (slot === undefined)
|
|
279
|
+
throw new Error(`response names unknown or non-response-bearing slot id "${id}"`);
|
|
280
|
+
responses[id] = parseResponse(slot, value);
|
|
281
|
+
}
|
|
282
|
+
if (!partial)
|
|
283
|
+
for (const id of expected.keys())
|
|
284
|
+
if (!(id in responses))
|
|
285
|
+
throw new Error(`final page response is missing response-bearing slot id "${id}"`);
|
|
286
|
+
return responses;
|
|
287
|
+
}
|
|
288
|
+
/** Validate a complete final response map. */
|
|
289
|
+
export function validatePageResponses(manifest, responses) { return validateResponses(manifest, responses, false); }
|
|
290
|
+
/** Validate an autosaved response map, permitting omitted response-bearing slots. */
|
|
291
|
+
export function validatePartialPageResponses(manifest, responses) { return validateResponses(manifest, responses, true); }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The single response slot every synthesized question page carries. */
|
|
2
|
+
export declare const ASK_RESPONSE_SLOT = "```slot:text\n{ \"id\": \"response\", \"initialText\": \"\", \"editable\": true }\n```";
|
|
3
|
+
/** Build a page document from title/subtitle/body — the shape crtr synthesizes when no page file is authored. */
|
|
4
|
+
export declare function synthesizedPageDocument(title: string, subtitle: string, body: string, responseBearing: boolean): string;
|
|
5
|
+
/** Materialize a synthesized document into a private temp dir; the caller removes that dir once submitted. */
|
|
6
|
+
export declare function writeSynthesizedPage(document: string): {
|
|
7
|
+
dir: string;
|
|
8
|
+
path: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
/** The single response slot every synthesized question page carries. */
|
|
5
|
+
export const ASK_RESPONSE_SLOT = '```slot:text\n{ "id": "response", "initialText": "", "editable": true }\n```';
|
|
6
|
+
/** Build a page document from title/subtitle/body — the shape crtr synthesizes when no page file is authored. */
|
|
7
|
+
export function synthesizedPageDocument(title, subtitle, body, responseBearing) {
|
|
8
|
+
const slot = responseBearing ? `\n\n${ASK_RESPONSE_SLOT}` : '';
|
|
9
|
+
return `---\ntitle: ${title}\nsubtitle: ${subtitle}\n---\n\n${body}${slot}\n`;
|
|
10
|
+
}
|
|
11
|
+
/** Materialize a synthesized document into a private temp dir; the caller removes that dir once submitted. */
|
|
12
|
+
export function writeSynthesizedPage(document) {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-human-page-'));
|
|
14
|
+
const path = join(dir, 'page.md');
|
|
15
|
+
writeFileSync(path, document, { encoding: 'utf8', mode: 0o600 });
|
|
16
|
+
return { dir, path };
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PageManifest, type PageSource } from './page-schema.js';
|
|
2
|
+
export type PageDialect = 'md' | 'html';
|
|
3
|
+
export type PageSourceBlock = {
|
|
4
|
+
kind: 'prose';
|
|
5
|
+
markdown: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'slot';
|
|
8
|
+
slotIndex: number;
|
|
9
|
+
};
|
|
10
|
+
export type PageSourceProjection = PageSourceBlock[];
|
|
11
|
+
/** A page failed authoring or validation; the caller can report it as bad page input. */
|
|
12
|
+
export declare class PageAuthoringError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
/** Derive the transport manifest from one authored page document. */
|
|
16
|
+
export declare function derivePageManifestFromDocument(document: string, dialect: PageDialect, productKinds?: string[], source?: PageSource): PageManifest;
|
|
17
|
+
/** Derive the transport manifest from one authored page source file. */
|
|
18
|
+
export declare function derivePageManifest(sourceFile: string, dialect: PageDialect, productKinds?: string[], source?: PageSource): PageManifest;
|
|
19
|
+
/** Read and validate page.json from a ticket directory. */
|
|
20
|
+
export declare function parsePage(dir: string, productKinds?: string[]): PageManifest;
|
|
21
|
+
/** Project authored source for local rendering and verify its slot references against the manifest. */
|
|
22
|
+
export declare function parsePageSource(document: string, manifest: PageManifest, productKinds?: string[]): PageSourceProjection;
|