@north-light/crouter 0.3.197 → 0.3.198

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.
Files changed (144) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +81 -153
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/user-settings.d.ts +17 -2
  91. package/dist/core/user-settings.js +32 -11
  92. package/dist/daemon/api/handlers/human.js +1 -1
  93. package/dist/daemon/api/handlers/inbox.js +22 -7
  94. package/dist/daemon/cron-run.js +16 -2
  95. package/dist/daemon/crtrd.js +1 -1
  96. package/dist/daemon/fleet.d.ts +22 -10
  97. package/dist/daemon/fleet.js +43 -24
  98. package/dist/daemon/human/finish.d.ts +2 -1
  99. package/dist/daemon/human/finish.js +45 -28
  100. package/dist/daemon/human/sweep.js +4 -2
  101. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  102. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  103. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  104. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  105. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  106. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  107. package/dist/daemon/review/sweep.js +12 -1
  108. package/dist/pages/bundle.css +1 -1
  109. package/dist/pages/bundle.js +1008 -577
  110. package/dist/pages/comments.d.ts +29 -0
  111. package/dist/pages/comments.js +51 -0
  112. package/dist/pages/controls.d.ts +98 -0
  113. package/dist/pages/controls.js +261 -0
  114. package/dist/pages/elements/cards.d.ts +15 -4
  115. package/dist/pages/elements/cards.js +299 -186
  116. package/dist/pages/elements/chart.d.ts +22 -0
  117. package/dist/pages/elements/chart.js +107 -91
  118. package/dist/pages/elements/options.d.ts +9 -0
  119. package/dist/pages/elements/options.js +268 -155
  120. package/dist/pages/elements/pages.d.ts +6 -0
  121. package/dist/pages/elements/pages.js +60 -38
  122. package/dist/pages/elements/slot.d.ts +5 -0
  123. package/dist/pages/elements/slot.js +17 -10
  124. package/dist/pages/elements/table.d.ts +17 -0
  125. package/dist/pages/elements/table.js +336 -228
  126. package/dist/pages/elements/text.d.ts +9 -3
  127. package/dist/pages/elements/text.js +350 -143
  128. package/dist/pages/host.d.ts +0 -24
  129. package/dist/pages/host.js +0 -17
  130. package/dist/pages/readonly.d.ts +24 -0
  131. package/dist/pages/readonly.js +31 -0
  132. package/dist/pages/responses.d.ts +37 -0
  133. package/dist/pages/responses.js +56 -0
  134. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  135. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  136. package/dist/types.d.ts +19 -5
  137. package/dist/types.js +2 -1
  138. package/package.json +1 -1
  139. package/runtime.lock.json +2 -2
  140. package/scripts/install-runtime.mjs +19 -24
  141. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  142. package/dist/clients/attach/render/html-markdown.js +0 -206
  143. package/dist/core/human/markdown-html.d.ts +0 -8
  144. package/dist/core/human/markdown-html.js +0 -581
@@ -1,8 +1,7 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { checkMarkdown } from '../termrender/termrender.js';
4
3
  import { BUILTIN_PAGE_KINDS, isRegisteredPageKind } from './page-catalog.js';
5
- import { isResponseBearingSlot, validatePageManifest } from './page-schema.js';
4
+ import { cardsSchema, chartDataRowsSchema, isResponseBearingSlot, issueText, tableRowsSchema, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
6
5
  /** A page failed authoring or validation; the caller can report it as bad page input. */
7
6
  export class PageAuthoringError extends Error {
8
7
  constructor(message) {
@@ -15,9 +14,18 @@ function asPageAuthoringError(error) {
15
14
  return error;
16
15
  return new PageAuthoringError(error instanceof Error ? error.message : String(error));
17
16
  }
18
- function validateAuthoredManifest(parsed, productKinds) {
17
+ function validateAuthoredManifest(parsed, productComponents) {
19
18
  try {
20
- return validatePageManifest(parsed, productKinds);
19
+ const manifest = validateAuthoredPageManifest(parsed, productComponents);
20
+ const responseBearing = manifest.slots.filter(isResponseBearingSlot);
21
+ if (responseBearing.length >= 2) {
22
+ for (const slot of responseBearing) {
23
+ const label = slot.config.label;
24
+ if (typeof label !== 'string' || label === '')
25
+ throw new PageAuthoringError(`response-bearing slot "${slot.id}" requires a nonempty config.label when the page has multiple response-bearing slots`);
26
+ }
27
+ }
28
+ return manifest;
21
29
  }
22
30
  catch (error) {
23
31
  throw asPageAuthoringError(error);
@@ -35,53 +43,6 @@ function parseJsonObject(raw, thing) {
35
43
  throw new PageAuthoringError(`${thing} JSON must be one object`);
36
44
  return value;
37
45
  }
38
- function parseFrontMatter(document) {
39
- if (!(document.startsWith('---\n') || document.startsWith('---\r\n')))
40
- throw new PageAuthoringError('md page front matter must begin at the first bytes with a --- delimiter line');
41
- const firstEnd = document.indexOf('\n') + 1;
42
- let cursor = firstEnd;
43
- let closingStart = -1;
44
- while (cursor <= document.length) {
45
- const end = document.indexOf('\n', cursor);
46
- const lineEnd = end === -1 ? document.length : end;
47
- const line = document.slice(cursor, lineEnd).replace(/\r$/, '');
48
- if (line === '---') {
49
- closingStart = cursor;
50
- cursor = end === -1 ? document.length : end + 1;
51
- break;
52
- }
53
- if (end === -1)
54
- break;
55
- cursor = end + 1;
56
- }
57
- if (closingStart === -1)
58
- throw new PageAuthoringError('md page front matter is missing its closing --- delimiter line');
59
- const raw = document.slice(firstEnd, closingStart);
60
- const values = new Map();
61
- const rawLines = raw.split(/\r?\n/);
62
- if (rawLines.at(-1) === '')
63
- rawLines.pop();
64
- for (const [index, line] of rawLines.entries()) {
65
- const colon = line.indexOf(':');
66
- if (colon <= 0)
67
- throw new PageAuthoringError(`front matter line ${index + 2} must be "key: value"`);
68
- const key = line.slice(0, colon).trim();
69
- const value = line.slice(colon + 1).trim();
70
- if (!['title', 'subtitle', 'placement'].includes(key))
71
- throw new PageAuthoringError(`unknown page front matter key "${key}"`);
72
- if (values.has(key))
73
- throw new PageAuthoringError(`duplicate page front matter key "${key}"`);
74
- values.set(key, value);
75
- }
76
- const title = values.get('title');
77
- if (title === undefined || title === '')
78
- throw new PageAuthoringError('page front matter title is required and must be nonempty');
79
- const placement = values.get('placement') ?? 'panel';
80
- if (placement !== 'inline' && placement !== 'panel')
81
- throw new PageAuthoringError(`page front matter placement must be exactly inline or panel; received "${placement}"`);
82
- const subtitle = values.get('subtitle');
83
- return { title, ...(subtitle === undefined ? {} : { subtitle }), placement, bodyStart: cursor };
84
- }
85
46
  function sourceLines(source, start) {
86
47
  const lines = [];
87
48
  let cursor = start;
@@ -104,18 +65,35 @@ function addProse(projection, raw) {
104
65
  if (markdown !== '')
105
66
  projection.push({ kind: 'prose', markdown });
106
67
  }
107
- function scanMarkdown(document, productKinds) {
108
- const meta = parseFrontMatter(document);
109
- const body = document.slice(meta.bodyStart);
110
- const markdownCheck = checkMarkdown(body);
111
- if (!markdownCheck.ok)
112
- throw new PageAuthoringError(markdownCheck.error);
113
- const lines = sourceLines(document, meta.bodyStart);
114
- // Fence errors report document-absolute line numbers, past the front matter.
115
- const lineOffset = document.slice(0, meta.bodyStart).split('\n').length - 1;
68
+ /**
69
+ * Project a pre-cutover Markdown ticket for terminal replay only. This is not
70
+ * an authoring contract: no new page path accepts Markdown, and its old front
71
+ * matter is merely skipped to recover the stored source body.
72
+ */
73
+ function projectLegacyMarkdownSource(document, productComponents) {
74
+ if (!(document.startsWith('---\n') || document.startsWith('---\r\n'))) {
75
+ throw new PageAuthoringError('stored md page is missing its opening front matter delimiter');
76
+ }
77
+ const firstEnd = document.indexOf('\n') + 1;
78
+ let bodyStart = -1;
79
+ let cursor = firstEnd;
80
+ while (cursor <= document.length) {
81
+ const newline = document.indexOf('\n', cursor);
82
+ const lineEnd = newline === -1 ? document.length : newline;
83
+ if (document.slice(cursor, lineEnd).replace(/\r$/, '') === '---') {
84
+ bodyStart = newline === -1 ? document.length : newline + 1;
85
+ break;
86
+ }
87
+ if (newline === -1)
88
+ break;
89
+ cursor = newline + 1;
90
+ }
91
+ if (bodyStart === -1)
92
+ throw new PageAuthoringError('stored md page is missing its closing front matter delimiter');
93
+ const lines = sourceLines(document, bodyStart);
116
94
  const slots = [];
117
95
  const projection = [];
118
- let proseStart = meta.bodyStart;
96
+ let proseStart = bodyStart;
119
97
  let ordinaryFence = null;
120
98
  for (let index = 0; index < lines.length; index++) {
121
99
  const line = lines[index];
@@ -135,8 +113,8 @@ function scanMarkdown(document, productKinds) {
135
113
  continue;
136
114
  }
137
115
  const kind = info.slice('slot:'.length);
138
- if (kind === '' || info !== `slot:${kind}` || !isRegisteredPageKind(kind, productKinds)) {
139
- throw new PageAuthoringError(`unregistered or invalid page slot kind "${kind}" in md fence at line ${lineOffset + index + 1}`);
116
+ if (kind === '' || info !== `slot:${kind}` || !isRegisteredPageKind(kind, productComponents)) {
117
+ throw new PageAuthoringError(`unregistered or invalid page slot kind "${kind}" in stored md fence`);
140
118
  }
141
119
  let closeIndex = -1;
142
120
  for (let candidate = index + 1; candidate < lines.length; candidate++) {
@@ -146,19 +124,19 @@ function scanMarkdown(document, productKinds) {
146
124
  }
147
125
  }
148
126
  if (closeIndex === -1)
149
- throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has no matching ${run.length}-backtick closing fence`);
127
+ throw new PageAuthoringError(`slot:${kind} fence has no matching ${run.length}-backtick closing fence`);
150
128
  addProse(projection, document.slice(proseStart, line.start));
151
- const rawObject = parseJsonObject(document.slice(line.end, lines[closeIndex].start), `slot:${kind} fence at line ${lineOffset + index + 1}`);
129
+ const rawObject = parseJsonObject(document.slice(line.end, lines[closeIndex].start), `stored slot:${kind} fence`);
152
130
  const { id, ...config } = rawObject;
153
131
  if (id !== undefined && typeof id !== 'string')
154
- throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has non-string reserved id`);
132
+ throw new PageAuthoringError(`stored slot:${kind} fence has non-string reserved id`);
155
133
  slots.push({ ...(id === undefined ? {} : { id }), kind, step: 0, config, ...(BUILTIN_PAGE_KINDS.includes(kind) ? {} : { unvalidated: true }) });
156
134
  projection.push({ kind: 'slot', slotIndex: slots.length - 1 });
157
135
  proseStart = lines[closeIndex].end;
158
136
  index = closeIndex;
159
137
  }
160
138
  addProse(projection, document.slice(proseStart));
161
- return { title: meta.title, ...(meta.subtitle === undefined ? {} : { subtitle: meta.subtitle }), placement: meta.placement, steps: 1, slots, projection };
139
+ return { steps: 1, slots, projection };
162
140
  }
163
141
  const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
164
142
  function tagEnd(html, start) {
@@ -216,8 +194,12 @@ function parseHtmlElements(html) {
216
194
  let match = stack.length - 1;
217
195
  while (match >= 0 && stack[match].tag !== tag)
218
196
  match--;
219
- if (match >= 0)
197
+ if (match >= 0) {
198
+ const element = stack[match];
199
+ element.closeStart = start;
200
+ element.end = end;
220
201
  stack.length = match;
202
+ }
221
203
  cursor = end;
222
204
  continue;
223
205
  }
@@ -228,7 +210,7 @@ function parseHtmlElements(html) {
228
210
  }
229
211
  const tag = opening[1].toLowerCase();
230
212
  const parent = stack.at(-1);
231
- const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, ...(parent === undefined ? {} : { parent }), children: [] };
213
+ const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, openEnd: end, end, ...(parent === undefined ? {} : { parent }), children: [] };
232
214
  if (parent === undefined)
233
215
  roots.push(element);
234
216
  else
@@ -242,6 +224,8 @@ function parseHtmlElements(html) {
242
224
  if (close === null)
243
225
  throw new PageAuthoringError(`unterminated <script> element at byte ${start}`);
244
226
  element.text = html.slice(end, close.index);
227
+ element.closeStart = close.index;
228
+ element.end = closePattern.lastIndex;
245
229
  cursor = closePattern.lastIndex;
246
230
  continue;
247
231
  }
@@ -321,7 +305,41 @@ function configForHtmlElement(element) {
321
305
  throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} requires a direct <script type="application/json"> child`);
322
306
  return parseJsonObject(script.text ?? '', `<${element.tag}> at byte ${element.start}`);
323
307
  }
324
- function scanHtml(document, productKinds) {
308
+ /** Project only body prose: slots remain terminal panels, while the pager's
309
+ * structural tags are invisible in the terminal. The raw fragments flow to
310
+ * the renderer, which downconverts HTML according to the manifest dialect. */
311
+ function htmlProjection(document, elements, slotElements) {
312
+ const body = elements.find((element) => element.tag === 'body');
313
+ if (body === undefined)
314
+ throw new PageAuthoringError('page document has no <body> element');
315
+ const bodyEnd = body.closeStart ?? document.length;
316
+ const excluded = slotElements.map((element) => ({ start: element.start, end: element.end }));
317
+ for (const element of elements) {
318
+ if (element.tag !== 'crtr-pages' && element.tag !== 'crtr-page')
319
+ continue;
320
+ excluded.push({ start: element.start, end: element.openEnd });
321
+ if (element.closeStart !== undefined)
322
+ excluded.push({ start: element.closeStart, end: element.end });
323
+ }
324
+ const ranges = excluded
325
+ .map((range) => ({ start: Math.max(range.start, body.openEnd), end: Math.min(range.end, bodyEnd) }))
326
+ .filter((range) => range.start < range.end)
327
+ .sort((left, right) => left.start - right.start || left.end - right.end);
328
+ const projection = [];
329
+ let cursor = body.openEnd;
330
+ for (const range of ranges) {
331
+ if (range.end <= cursor)
332
+ continue;
333
+ addProse(projection, document.slice(cursor, range.start));
334
+ const element = slotElements.find((slot) => slot.start === range.start && slot.end === range.end);
335
+ if (element !== undefined)
336
+ projection.push({ kind: 'slot', slotIndex: slotElements.indexOf(element) });
337
+ cursor = range.end;
338
+ }
339
+ addProse(projection, document.slice(cursor, bodyEnd));
340
+ return projection;
341
+ }
342
+ function scanHtml(document, productComponents) {
325
343
  validateHtmlStructure(document);
326
344
  const elements = parseHtmlElements(document);
327
345
  const metadata = readHtmlMetadata(elements);
@@ -347,7 +365,7 @@ function scanHtml(document, productKinds) {
347
365
  let unvalidated = false;
348
366
  if (element.tag === 'crtr-slot') {
349
367
  kind = element.attrs.get('kind');
350
- if (kind === undefined || !productKinds.includes(kind) || BUILTIN_PAGE_KINDS.includes(kind))
368
+ if (kind === undefined || !isRegisteredPageKind(kind, productComponents) || BUILTIN_PAGE_KINDS.includes(kind))
351
369
  throw new PageAuthoringError(`<crtr-slot> at byte ${element.start} names unregistered product kind "${kind ?? ''}"`);
352
370
  unvalidated = true;
353
371
  }
@@ -358,6 +376,8 @@ function scanHtml(document, productKinds) {
358
376
  }
359
377
  if (kind === undefined)
360
378
  continue;
379
+ if (element.attrs.has('slot-id'))
380
+ throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} has a slot-id attribute; author the slot id as id="…" — slot-id is written by the renderer`);
361
381
  const id = element.attrs.get('id');
362
382
  const containingPage = ancestor(element, 'crtr-page');
363
383
  const step = containingPage === undefined ? 0 : pageIndexes.get(containingPage) ?? 0;
@@ -365,7 +385,7 @@ function scanHtml(document, productKinds) {
365
385
  slotElements.push(element);
366
386
  }
367
387
  const steps = wrapper === undefined ? 1 : pages.length;
368
- const provisional = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: 'html', ...metadata, document: 'page.html', steps, slots: rawSlots }, productKinds);
388
+ const provisional = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: 'html', ...metadata, document: 'page.html', steps, slots: rawSlots }, productComponents);
369
389
  for (let index = 0; index < provisional.slots.length; index++) {
370
390
  const slot = provisional.slots[index];
371
391
  if (!isResponseBearingSlot(slot))
@@ -375,22 +395,79 @@ function scanHtml(document, productKinds) {
375
395
  throw new PageAuthoringError(`response-bearing element <${element.tag}> for slot "${slot.id}" is outside any <crtr-page>.\nWrap interactive elements with:\n<crtr-pages><crtr-page>…</crtr-page></crtr-pages>`);
376
396
  }
377
397
  }
378
- return { ...metadata, steps, slots: provisional.slots, projection: provisional.slots.map((_, slotIndex) => ({ kind: 'slot', slotIndex })) };
398
+ return { ...metadata, steps, slots: provisional.slots, projection: htmlProjection(document, elements, slotElements) };
379
399
  }
380
- function scanDocument(document, dialect, productKinds) {
381
- return dialect === 'md' ? scanMarkdown(document, productKinds) : scanHtml(document, productKinds);
400
+ /** Derive the transport manifest from one authored HTML page document. */
401
+ export function derivePageManifestFromDocument(document, dialect, productComponents = [], source) {
402
+ const scanned = scanHtml(document, productComponents);
403
+ return validateAuthoredManifest({ schema: 'crtr.page/v1', dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: 'page.html', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productComponents);
382
404
  }
383
- /** Derive the transport manifest from one authored page document. */
384
- export function derivePageManifestFromDocument(document, dialect, productKinds = [], source) {
385
- const scanned = scanDocument(document, dialect, productKinds);
386
- return validateAuthoredManifest({ schema: 'crtr.page/v1', dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: dialect === 'md' ? 'page.md' : 'page.html', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productKinds);
405
+ /** Derive the transport manifest from one authored HTML page source file. */
406
+ export function derivePageManifest(sourceFile, dialect, productComponents = [], source) {
407
+ return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), dialect, productComponents, source);
387
408
  }
388
- /** Derive the transport manifest from one authored page source file. */
389
- export function derivePageManifest(sourceFile, dialect, productKinds = [], source) {
390
- return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), dialect, productKinds, source);
409
+ const SOURCE_EMBED = {
410
+ table: { field: 'rows', schema: tableRowsSchema },
411
+ cards: { field: 'cards', schema: cardsSchema },
412
+ chart: { field: 'data', schema: chartDataRowsSchema },
413
+ };
414
+ function readSourceFile(path, context) {
415
+ if (!existsSync(path))
416
+ throw new PageAuthoringError(`${context} source file does not exist: ${path}`);
417
+ let raw;
418
+ try {
419
+ raw = readFileSync(path, 'utf8');
420
+ }
421
+ catch (error) {
422
+ throw new PageAuthoringError(`${context} source file is not readable: ${path} (${error instanceof Error ? error.message : String(error)})`);
423
+ }
424
+ try {
425
+ return JSON.parse(raw);
426
+ }
427
+ catch (error) {
428
+ throw new PageAuthoringError(`${context} source file is not valid JSON: ${path} (${error instanceof Error ? error.message : String(error)})`);
429
+ }
430
+ }
431
+ /** Read source files into their built-in slots, returning a self-contained stored manifest. */
432
+ export function snapshotPageSources(manifest, productComponents = []) {
433
+ const slots = manifest.slots.map((slot, index) => {
434
+ const embed = slot.unvalidated === true ? undefined : SOURCE_EMBED[slot.kind];
435
+ const config = slot.config;
436
+ if (embed === undefined || typeof config.source !== 'string')
437
+ return slot;
438
+ const context = `slot ${slot.id === undefined ? `#${index}` : `"${slot.id}"`} (${slot.kind})`;
439
+ const raw = readSourceFile(config.source, context);
440
+ const parsed = embed.schema.safeParse(raw);
441
+ if (!parsed.success)
442
+ throw new PageAuthoringError(`${context} source data is invalid: ${issueText(parsed.error)}`);
443
+ const { source: _drop, ...rest } = config;
444
+ return { ...slot, config: { ...rest, [embed.field]: parsed.data } };
445
+ });
446
+ return validatePageManifest({ ...manifest, slots }, productComponents);
447
+ }
448
+ /** Replace every authored slot config with its stored, self-contained config for runtime rendering. */
449
+ export function embedPageSlotConfigs(document, manifest) {
450
+ const replacements = [];
451
+ let slotIndex = 0;
452
+ for (const element of parseHtmlElements(document)) {
453
+ const isSlot = element.tag === 'crtr-slot' || (element.tag.startsWith('crtr-') && element.tag !== 'crtr-pages' && element.tag !== 'crtr-page');
454
+ if (!isSlot)
455
+ continue;
456
+ const slot = manifest.slots[slotIndex];
457
+ if (slot === undefined)
458
+ throw new PageAuthoringError(`page document has more slots than its manifest at slot #${slotIndex}`);
459
+ const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
460
+ if (script?.closeStart === undefined)
461
+ throw new PageAuthoringError(`slot #${slotIndex} has no JSON config script`);
462
+ replacements.push({ start: script.openEnd, end: script.closeStart, text: JSON.stringify(slot.config).replace(/</g, '\\u003c') });
463
+ slotIndex += 1;
464
+ }
465
+ if (slotIndex !== manifest.slots.length)
466
+ throw new PageAuthoringError(`page document has ${slotIndex} slots but manifest has ${manifest.slots.length}`);
467
+ return replacements.reduceRight((result, replacement) => `${result.slice(0, replacement.start)}${replacement.text}${result.slice(replacement.end)}`, document);
391
468
  }
392
469
  /** Read and validate page.json from a ticket directory. */
393
- export function parsePage(dir, productKinds = []) {
470
+ export function parsePage(dir, productComponents = []) {
394
471
  let raw;
395
472
  try {
396
473
  raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
@@ -398,7 +475,7 @@ export function parsePage(dir, productKinds = []) {
398
475
  catch {
399
476
  throw new PageAuthoringError('page.json is not valid JSON');
400
477
  }
401
- return validatePageManifest(raw, productKinds);
478
+ return validatePageManifest(raw, productComponents);
402
479
  }
403
480
  /** Key-order-independent value identity, so a re-scan compares by content. */
404
481
  function canonicalJson(value) {
@@ -409,18 +486,29 @@ function canonicalJson(value) {
409
486
  const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => (left < right ? -1 : 1));
410
487
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
411
488
  }
489
+ function comparableConfig(kind, config, sourceProjection) {
490
+ const embed = SOURCE_EMBED[kind];
491
+ if (embed === undefined || !sourceProjection)
492
+ return config;
493
+ const { source: _source, [embed.field]: _data, ...rest } = config;
494
+ return rest;
495
+ }
412
496
  /** Project authored source for local rendering and verify its slot references against the manifest. */
413
- export function parsePageSource(document, manifest, productKinds = []) {
414
- const canonical = validateAuthoredManifest(manifest, productKinds);
415
- const scanned = scanDocument(document, canonical.dialect, productKinds);
497
+ export function parsePageSource(document, manifest, productComponents = []) {
498
+ const canonical = validatePageManifest(manifest, productComponents);
499
+ const scanned = canonical.dialect === 'md'
500
+ ? projectLegacyMarkdownSource(document, productComponents)
501
+ : scanHtml(document, productComponents);
416
502
  // Normalize the re-scan through the same schema pass the manifest went through,
417
503
  // so per-kind config defaults are on both sides of the comparison.
418
- const derived = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: canonical.dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: canonical.document, steps: scanned.steps, slots: scanned.slots }, productKinds);
504
+ const derived = validateAuthoredPageManifest({ schema: 'crtr.page/v1', dialect: canonical.dialect, title: canonical.title, ...(canonical.subtitle === undefined ? {} : { subtitle: canonical.subtitle }), placement: canonical.placement, document: canonical.document, steps: scanned.steps, slots: scanned.slots }, productComponents);
419
505
  if (derived.slots.length !== canonical.slots.length)
420
506
  throw new PageAuthoringError(`page source has ${derived.slots.length} slots but manifest has ${canonical.slots.length}`);
421
507
  for (let index = 0; index < canonical.slots.length; index++) {
422
508
  const left = derived.slots[index];
423
- if (canonicalJson(left) !== canonicalJson(canonical.slots[index]))
509
+ const right = canonical.slots[index];
510
+ const sourceProjection = typeof left.config.source === 'string';
511
+ if (canonicalJson({ ...left, config: comparableConfig(left.kind, left.config, sourceProjection) }) !== canonicalJson({ ...right, config: comparableConfig(right.kind, right.config, sourceProjection) }))
424
512
  throw new PageAuthoringError(`page source slot #${index} (${left.id ?? left.kind}) does not match manifest slot #${index}`);
425
513
  }
426
514
  return scanned.projection;
@@ -10,6 +10,7 @@ export declare const reviewDescriptorSchema: z.ZodObject<{
10
10
  sessionName: z.ZodOptional<z.ZodString>;
11
11
  askedBy: z.ZodOptional<z.ZodString>;
12
12
  blockedSince: z.ZodOptional<z.ZodString>;
13
+ profileName: z.ZodOptional<z.ZodString>;
13
14
  nodeId: z.ZodOptional<z.ZodString>;
14
15
  }, z.core.$strip>;
15
16
  blockedSince: z.ZodString;
@@ -3,7 +3,7 @@ import { basename, dirname, isAbsolute, resolve } from 'node:path';
3
3
  import { claimPath, pageManifestPath, progressPath, responsePath, reviewPath } from './convention.js';
4
4
  import { z } from 'zod';
5
5
  import { checkReviewableFile } from './visible-paths.js';
6
- const reviewSourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), nodeId: z.string().optional() });
6
+ const reviewSourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), profileName: z.string().optional(), nodeId: z.string().optional() });
7
7
  export const reviewDescriptorSchema = z.object({
8
8
  schema: z.literal('humanloop.review/v1'),
9
9
  file: z.string().min(1).refine(isAbsolute, 'file must be absolute'),
@@ -1,6 +1,7 @@
1
+ import type { ProductPageComponents } from '../../types.js';
1
2
  import type { PageTicketSummary, TicketSummary } from './types.js';
2
3
  /** Read all unresolved page/review tickets, newest first. Progress never affects visibility. */
3
- export declare function scanInbox(productKinds?: string[]): TicketSummary[];
4
+ export declare function scanInbox(productKinds?: ProductPageComponents): TicketSummary[];
4
5
  export interface PageHistoryEntry extends PageTicketSummary {
5
6
  state: 'pending' | 'resolved' | 'canceled';
6
7
  }
@@ -9,4 +10,4 @@ export interface PageHistoryEntry extends PageTicketSummary {
9
10
  * first. This is the durable discovery seam for transcript placement, so it is a
10
11
  * separate walk from the pending-only inbox scan and never filters on a terminal file.
11
12
  */
12
- export declare function scanPageHistory(nodeId: string, productKinds?: string[]): PageHistoryEntry[];
13
+ export declare function scanPageHistory(nodeId: string, productKinds?: ProductPageComponents): PageHistoryEntry[];
@@ -3,8 +3,8 @@ import { basename, join } from 'node:path';
3
3
  import { claimPath, isResolved, pageManifestPath, reviewPath } from './convention.js';
4
4
  import { readJsonOrNull } from '../fs-utils.js';
5
5
  import { validateReviewDescriptor } from './review-schema.js';
6
- import { isResponseBearingSlot, validatePageManifest } from './page-schema.js';
7
- import { pageTicketState } from './tickets.js';
6
+ import { parsePage } from './page.js';
7
+ import { pageTicketState, readTicketResult } from './tickets.js';
8
8
  import { ticketsRoot } from './root.js';
9
9
  function claimSummary(dir) {
10
10
  const claim = readJsonOrNull(claimPath(dir));
@@ -26,12 +26,9 @@ function reviewSummary(dir, id) {
26
26
  return { dir, id, kind: 'review', title: review.title, subtitle: review.subtitle, file: review.file, output: review.output, source: review.source, blockedSince: review.blockedSince, claim: claimSummary(dir) };
27
27
  }
28
28
  function pageSummary(dir, id, productKinds) {
29
- const raw = readJsonOrNull(pageManifestPath(dir));
30
- if (raw === null)
31
- return null;
32
29
  let manifest;
33
30
  try {
34
- manifest = validatePageManifest(raw, productKinds);
31
+ manifest = parsePage(dir, productKinds);
35
32
  }
36
33
  catch {
37
34
  return null;
@@ -55,7 +52,7 @@ function pageSummary(dir, id, productKinds) {
55
52
  placement: manifest.placement,
56
53
  steps: manifest.steps,
57
54
  slotKinds: manifest.slots.map((slot) => slot.kind),
58
- awaitsResponse: manifest.slots.some(isResponseBearingSlot),
55
+ awaitsResponse: readJsonOrNull(join(dir, 'run.json'))?.mode === 'ask',
59
56
  source: manifest.source ?? {},
60
57
  blockedSince,
61
58
  claim: claimSummary(dir),
@@ -114,7 +111,9 @@ export function scanPageHistory(nodeId, productKinds = []) {
114
111
  const summary = pageSummary(dir, basename(dir), productKinds);
115
112
  if (summary === null || summary.source.nodeId !== nodeId)
116
113
  continue;
117
- items.push({ ...summary, state: pageTicketState(dir) });
114
+ const state = pageTicketState(dir);
115
+ const result = state === 'resolved' ? readTicketResult(dir) : null;
116
+ items.push({ ...summary, state, ...(result?.kind === 'page' ? { answerDigest: result.summary.replace(/\s+/g, ' ').trim() } : {}) });
118
117
  }
119
118
  return items.sort((a, b) => Date.parse(a.blockedSince) - Date.parse(b.blockedSince) || a.id.localeCompare(b.id));
120
119
  }
@@ -1,7 +1,4 @@
1
- import { type PageManifest, type PageResponses } from './page-schema.js';
2
- /**
3
- * Deterministic, no-LLM page resolution summary — one line per answered slot in
4
- * document order: `"<slot id>: <answer>"`. An acknowledgement (`responses: {}`)
5
- * summarizes as the empty string.
6
- */
7
- export declare function buildPageSummary(manifest: PageManifest, responses: PageResponses): string;
1
+ import type { ProductPageComponents } from '../../types.js';
2
+ import type { PageManifest, PageResponses } from './page-schema.js';
3
+ /** The stable one-line digest stored in response.json. */
4
+ export declare function buildPageSummary(manifest: PageManifest, responses: PageResponses, productKinds?: ProductPageComponents): string;
@@ -1,61 +1,5 @@
1
- import { isResponseBearingSlot } from './page-schema.js';
2
- function labelsOf(items, ids, labelKey) {
3
- const byId = new Map((Array.isArray(items) ? items : [])
4
- .filter((item) => typeof item === 'object' && item !== null)
5
- .map((item) => [String(item['id']), typeof item[labelKey] === 'string' ? item[labelKey] : String(item['id'])]));
6
- return (Array.isArray(ids) ? ids : []).map((id) => byId.get(String(id)) ?? String(id));
7
- }
8
- function pickedWithFreetext(picked, freetext) {
9
- const text = typeof freetext === 'string' && freetext !== '' ? freetext : undefined;
10
- const selection = picked.length > 0 ? picked.join(', ') : undefined;
11
- if (selection !== undefined && text !== undefined)
12
- return `${selection} — ${text}`;
13
- return selection ?? text ?? '(no selection)';
14
- }
15
- function commentSuffix(response) {
16
- const comments = response['comments'];
17
- const count = Array.isArray(comments) ? comments.length : 0;
18
- return count === 0 ? '' : ` (${count} comment${count === 1 ? '' : 's'})`;
19
- }
20
- function describeSlotResponse(slot, response) {
21
- const value = response;
22
- const config = slot.config;
23
- switch (slot.kind) {
24
- case 'options':
25
- return `${pickedWithFreetext(labelsOf(config['options'], value['selectedOptionIds'], 'label'), value['freetext'])}${commentSuffix(value)}`;
26
- case 'text': {
27
- const text = typeof value['text'] === 'string' ? value['text'].replace(/\s+/g, ' ').trim() : '';
28
- return `${text === '' ? '(empty)' : text}${commentSuffix(value)}`;
29
- }
30
- case 'table': {
31
- const rows = (Array.isArray(value['selectedRowIds']) ? value['selectedRowIds'] : []).map(String);
32
- const columns = labelsOf(config['columns'], value['selectedColumnIds'], 'label');
33
- const parts = [...(rows.length > 0 ? [`rows ${rows.join(', ')}`] : []), ...(columns.length > 0 ? [`columns ${columns.join(', ')}`] : [])];
34
- return `${parts.length === 0 ? '(no selection)' : parts.join('; ')}${commentSuffix(value)}`;
35
- }
36
- case 'cards': {
37
- const picked = labelsOf(config['cards'], value['selectedCardIds'], 'title');
38
- return `${picked.length === 0 ? '(no selection)' : picked.join(', ')}${commentSuffix(value)}`;
39
- }
40
- default:
41
- // A product kind crtr does not render: its opaque response is the summary.
42
- return JSON.stringify(value);
43
- }
44
- }
45
- /**
46
- * Deterministic, no-LLM page resolution summary — one line per answered slot in
47
- * document order: `"<slot id>: <answer>"`. An acknowledgement (`responses: {}`)
48
- * summarizes as the empty string.
49
- */
50
- export function buildPageSummary(manifest, responses) {
51
- const lines = [];
52
- for (const slot of manifest.slots) {
53
- if (!isResponseBearingSlot(slot) || slot.id === undefined)
54
- continue;
55
- const response = responses[slot.id];
56
- if (response === undefined)
57
- continue;
58
- lines.push(`${slot.id}: ${describeSlotResponse(slot, response)}`);
59
- }
60
- return lines.join('\n');
1
+ import { describePageAnswer } from './answer.js';
2
+ /** The stable one-line digest stored in response.json. */
3
+ export function buildPageSummary(manifest, responses, productKinds) {
4
+ return describePageAnswer(manifest, responses, { completedAt: '', productKinds }).digest;
61
5
  }
@@ -1,5 +1,6 @@
1
1
  import type { ReviewDescriptor, TicketResult } from './types.js';
2
- import { type PageDialect } from './page.js';
2
+ import type { ProductPageComponents } from '../../types.js';
3
+ import { type AuthoredPageDialect } from './page.js';
3
4
  import { type PageManifest, type PageResponses, type PageSource } from './page-schema.js';
4
5
  /** Strict decoder for the only canonical final marker. */
5
6
  export declare function readTicketResult(dir: string): TicketResult | null;
@@ -7,13 +8,13 @@ export interface SubmitPageOptions {
7
8
  dir: string;
8
9
  /** The authored page source; copied into the ticket directory as its `document`. */
9
10
  sourceFile: string;
10
- dialect: PageDialect;
11
+ dialect: AuthoredPageDialect;
11
12
  source?: PageSource;
12
- productKinds?: string[];
13
+ productKinds?: ProductPageComponents;
13
14
  }
14
15
  /** One validated page, ready to publish into a ticket directory. */
15
16
  export interface PreparedPage {
16
- dialect: PageDialect;
17
+ dialect: AuthoredPageDialect;
17
18
  /** The authored source, verbatim, as the ticket's `document`. */
18
19
  document: string;
19
20
  manifest: PageManifest;
@@ -53,13 +54,13 @@ export declare function submitReview(opts: SubmitReviewOptions): {
53
54
  kind: 'review';
54
55
  };
55
56
  /** Validate a page resolution against its canonical ticket before claim takeover. */
56
- export declare function validatePageTicketResponses(dir: string, responses: unknown, productKinds?: string[]): PageResponses;
57
+ export declare function validatePageTicketResponses(dir: string, responses: unknown, productKinds?: ProductPageComponents): PageResponses;
57
58
  /** The three terminal states of a page ticket, derived from its published result. */
58
59
  export declare function pageTicketState(dir: string): 'pending' | 'resolved' | 'canceled';
59
60
  /** Autosave partial page work. Every supplied response is validated; omitted slots are permitted. */
60
- export declare function writePageProgress(dir: string, responses: unknown, productKinds?: string[]): PageResponses;
61
+ export declare function writePageProgress(dir: string, responses: unknown, productKinds?: ProductPageComponents): PageResponses;
61
62
  /** Saved partial work, or null when none exists. Malformed progress is an error, never silently discarded. */
62
- export declare function readPageProgress(dir: string, productKinds?: string[]): PageResponses | null;
63
+ export declare function readPageProgress(dir: string, productKinds?: ProductPageComponents): PageResponses | null;
63
64
  export interface PageTicket {
64
65
  dir: string;
65
66
  id: string;
@@ -70,9 +71,9 @@ export interface PageTicket {
70
71
  progress: PageResponses | null;
71
72
  }
72
73
  /** Read one addressable page ticket — pending, resolved or canceled alike. */
73
- export declare function readPageTicket(dir: string, productKinds?: string[]): PageTicket;
74
+ export declare function readPageTicket(dir: string, productKinds?: ProductPageComponents): PageTicket;
74
75
  export interface CompletePageOptions {
75
- productKinds?: string[];
76
+ productKinds?: ProductPageComponents;
76
77
  completedAt?: string;
77
78
  }
78
79
  export declare function completePage(dir: string, responses: unknown, claimToken: string, opts?: CompletePageOptions): {