@north-light/crouter 0.3.198 → 0.3.200

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 (316) hide show
  1. package/dist/api/client.d.ts +18 -18
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +81 -34
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +6 -6
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  33. package/dist/clients/attach/render/chat-view.d.ts +26 -1
  34. package/dist/clients/attach/render/chat-view.js +61 -11
  35. package/dist/clients/attach/render/page-block.d.ts +34 -0
  36. package/dist/clients/attach/render/page-block.js +226 -0
  37. package/dist/clients/attach/session/whip.d.ts +2 -0
  38. package/dist/clients/attach/session/whip.js +2 -0
  39. package/dist/clients/attach/viewer.js +1419 -993
  40. package/dist/clients/conversation/projection.js +18 -3
  41. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  42. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
  43. package/dist/clients/inbox/controller.js +10 -16
  44. package/dist/clients/inbox/page-adapter.d.ts +1 -3
  45. package/dist/clients/inbox/page-adapter.js +3 -4
  46. package/dist/clients/inbox/resolve.d.ts +1 -1
  47. package/dist/clients/inbox/resolve.js +3 -3
  48. package/dist/clients/inbox/review/document-surface.js +3 -1
  49. package/dist/clients/inbox/review/state.d.ts +2 -1
  50. package/dist/clients/inbox/review/state.js +5 -16
  51. package/dist/clients/inbox/tui/input.js +37 -19
  52. package/dist/clients/inbox/tui/panel.js +5 -31
  53. package/dist/clients/inbox/tui/render.d.ts +11 -0
  54. package/dist/clients/inbox/tui/render.js +71 -27
  55. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  56. package/dist/clients/inbox/tui/slots.js +16 -12
  57. package/dist/clients/inbox/tui/types.d.ts +2 -7
  58. package/dist/clients/inbox/tui.js +2 -2
  59. package/dist/commands/__tests__/human.test.js +71 -114
  60. package/dist/commands/__tests__/node-message.test.js +14 -0
  61. package/dist/commands/attention.js +4 -4
  62. package/dist/commands/canvas-snapshot.js +1 -1
  63. package/dist/commands/canvas.js +1 -1
  64. package/dist/commands/dashboard.js +1 -1
  65. package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
  66. package/dist/commands/human/components.js +77 -0
  67. package/dist/commands/human/feedback.d.ts +2 -0
  68. package/dist/commands/human/feedback.js +108 -0
  69. package/dist/commands/human/prompts.d.ts +13 -5
  70. package/dist/commands/human/prompts.js +160 -97
  71. package/dist/commands/human/queue.js +57 -88
  72. package/dist/commands/human/shared.d.ts +2 -12
  73. package/dist/commands/human/shared.js +17 -11
  74. package/dist/commands/human.js +29 -21
  75. package/dist/commands/memory/origin.js +2 -2
  76. package/dist/commands/node/bash.js +4 -4
  77. package/dist/commands/node/inspect.js +3 -3
  78. package/dist/commands/node/lifecycle.js +1 -1
  79. package/dist/commands/node/message.js +2 -2
  80. package/dist/commands/node-worktree.js +8 -8
  81. package/dist/commands/pkg/browse.js +1 -1
  82. package/dist/commands/pkg/plugin-manage.js +129 -10
  83. package/dist/commands/profile/delete.js +41 -15
  84. package/dist/commands/profile.js +1 -1
  85. package/dist/commands/push.js +1 -1
  86. package/dist/commands/surface/node/focus.js +29 -14
  87. package/dist/commands/surface-edit.js +3 -3
  88. package/dist/commands/surface-inbox.d.ts +2 -0
  89. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  90. package/dist/commands/surface.js +2 -1
  91. package/dist/commands/sys/doctor.js +60 -3
  92. package/dist/commands/sys/feedback.js +79 -25
  93. package/dist/commands/sys/settings.js +1 -1
  94. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  95. package/dist/core/__tests__/extension-abort.test.js +5 -2
  96. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  97. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  98. package/dist/core/__tests__/helpers/harness.js +17 -55
  99. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  100. package/dist/core/__tests__/human-deliver.test.js +31 -26
  101. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  102. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  103. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +4 -5
  104. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  105. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  106. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  107. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  108. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  109. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  110. package/dist/core/__tests__/session-cycles.test.js +22 -0
  111. package/dist/core/__tests__/stop-guard.test.js +4 -4
  112. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  113. package/dist/core/bash-jobs.d.ts +6 -0
  114. package/dist/core/bash-jobs.js +10 -0
  115. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  116. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  117. package/dist/core/canvas/attention.js +8 -6
  118. package/dist/core/canvas/browse/model.d.ts +8 -2
  119. package/dist/core/canvas/browse/model.js +11 -4
  120. package/dist/core/canvas/canvas.d.ts +7 -0
  121. package/dist/core/canvas/canvas.js +37 -0
  122. package/dist/core/canvas/crons.d.ts +9 -0
  123. package/dist/core/canvas/crons.js +51 -0
  124. package/dist/core/canvas/node-order.d.ts +22 -1
  125. package/dist/core/canvas/node-order.js +26 -1
  126. package/dist/core/canvas/render-source.d.ts +5 -0
  127. package/dist/core/canvas/render-source.js +1 -0
  128. package/dist/core/canvas/types.d.ts +9 -3
  129. package/dist/core/command-plugins/bundle.d.ts +6 -1
  130. package/dist/core/command-plugins/bundle.js +26 -13
  131. package/dist/core/command.js +2 -1
  132. package/dist/core/config.d.ts +47 -1
  133. package/dist/core/config.js +137 -4
  134. package/dist/core/help.js +1 -1
  135. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  136. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  137. package/dist/core/human/__tests__/page.test.js +56 -98
  138. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  139. package/dist/core/human/answer-text.js +3 -3
  140. package/dist/core/human/answer.d.ts +6 -4
  141. package/dist/core/human/answer.js +4 -10
  142. package/dist/core/human/component-docs.d.ts +11 -7
  143. package/dist/core/human/component-docs.js +361 -88
  144. package/dist/core/human/convention.d.ts +17 -3
  145. package/dist/core/human/convention.js +36 -5
  146. package/dist/core/human/feedback-companion.d.ts +20 -0
  147. package/dist/core/human/feedback-companion.js +98 -0
  148. package/dist/core/human/feedback.d.ts +65 -0
  149. package/dist/core/human/feedback.js +116 -0
  150. package/dist/core/human/page-catalog.d.ts +40 -5
  151. package/dist/core/human/page-catalog.js +102 -30
  152. package/dist/core/human/page-errors.d.ts +4 -0
  153. package/dist/core/human/page-errors.js +7 -0
  154. package/dist/core/human/page-eval.d.ts +20 -0
  155. package/dist/core/human/page-eval.js +123 -0
  156. package/dist/core/human/page-schema.d.ts +23 -191
  157. package/dist/core/human/page-schema.js +60 -89
  158. package/dist/core/human/page-synth.d.ts +1 -1
  159. package/dist/core/human/page-synth.js +15 -18
  160. package/dist/core/human/page.d.ts +12 -27
  161. package/dist/core/human/page.js +142 -474
  162. package/dist/core/human/root.d.ts +6 -2
  163. package/dist/core/human/root.js +12 -4
  164. package/dist/core/human/scan.d.ts +3 -4
  165. package/dist/core/human/scan.js +16 -15
  166. package/dist/core/human/summary.d.ts +1 -2
  167. package/dist/core/human/summary.js +2 -2
  168. package/dist/core/human/tickets.d.ts +20 -14
  169. package/dist/core/human/tickets.js +91 -31
  170. package/dist/core/human/types.d.ts +10 -2
  171. package/dist/core/preview-registry.js +8 -10
  172. package/dist/core/profiles/default-binding.d.ts +4 -0
  173. package/dist/core/profiles/default-binding.js +24 -0
  174. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  175. package/dist/core/profiles/deletion-reservation.js +38 -0
  176. package/dist/core/profiles/manifest.d.ts +3 -0
  177. package/dist/core/profiles/manifest.js +12 -0
  178. package/dist/core/profiles/select.js +2 -2
  179. package/dist/core/review/realize.js +3 -3
  180. package/dist/core/review/store.d.ts +6 -0
  181. package/dist/core/review/store.js +31 -0
  182. package/dist/core/runtime/bearings.d.ts +0 -4
  183. package/dist/core/runtime/bearings.js +4 -62
  184. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  185. package/dist/core/runtime/bin-contributions.js +186 -0
  186. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  187. package/dist/core/runtime/broker/client-registry.js +13 -1
  188. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  189. package/dist/core/runtime/broker/extension-abort.js +4 -2
  190. package/dist/core/runtime/broker/fault-retry.js +4 -0
  191. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  192. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  193. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  194. package/dist/core/runtime/broker/read-ops.js +2 -2
  195. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  196. package/dist/core/runtime/broker/rebind.js +6 -1
  197. package/dist/core/runtime/broker-extension-render.js +19 -0
  198. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  199. package/dist/core/runtime/broker.d.ts +1 -1
  200. package/dist/core/runtime/broker.js +17 -6
  201. package/dist/core/runtime/front-door-env.d.ts +5 -0
  202. package/dist/core/runtime/front-door-env.js +16 -0
  203. package/dist/core/runtime/front-door.d.ts +1 -5
  204. package/dist/core/runtime/front-door.js +2 -5
  205. package/dist/core/runtime/host.d.ts +1 -1
  206. package/dist/core/runtime/invocation.d.ts +6 -0
  207. package/dist/core/runtime/invocation.js +10 -0
  208. package/dist/core/runtime/kickoff.js +34 -0
  209. package/dist/core/runtime/launch.d.ts +2 -6
  210. package/dist/core/runtime/node-read.js +4 -1
  211. package/dist/core/runtime/nodes.js +2 -0
  212. package/dist/core/runtime/promote.d.ts +1 -0
  213. package/dist/core/runtime/promote.js +3 -1
  214. package/dist/core/runtime/session-cycles.d.ts +6 -0
  215. package/dist/core/runtime/session-cycles.js +15 -1
  216. package/dist/core/runtime/session-visibility.d.ts +17 -8
  217. package/dist/core/runtime/session-visibility.js +19 -10
  218. package/dist/core/runtime/spawn-env.d.ts +9 -1
  219. package/dist/core/runtime/spawn-env.js +62 -2
  220. package/dist/core/runtime/stop-guard.d.ts +2 -2
  221. package/dist/core/runtime/stop-guard.js +2 -2
  222. package/dist/core/runtime/stop-signals.d.ts +1 -1
  223. package/dist/core/runtime/stop-signals.js +2 -2
  224. package/dist/core/runtime/tmux-bindings.js +1 -1
  225. package/dist/core/session-model/session-state.d.ts +4 -3
  226. package/dist/core/session-model/session-state.js +14 -1
  227. package/dist/core/user-settings.d.ts +21 -2
  228. package/dist/core/user-settings.js +32 -11
  229. package/dist/core/worktree.d.ts +15 -45
  230. package/dist/core/worktree.js +59 -261
  231. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  232. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  233. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  234. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  235. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  236. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  237. package/dist/daemon/api/handlers/human.js +24 -20
  238. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  239. package/dist/daemon/api/handlers/inbox.js +39 -115
  240. package/dist/daemon/api/handlers/profiles.js +11 -29
  241. package/dist/daemon/api/map.d.ts +6 -1
  242. package/dist/daemon/api/map.js +23 -0
  243. package/dist/daemon/api/server.js +3 -1
  244. package/dist/daemon/companion-retire.d.ts +2 -0
  245. package/dist/daemon/companion-retire.js +33 -0
  246. package/dist/daemon/cron-run.js +14 -16
  247. package/dist/daemon/human/finish.d.ts +12 -7
  248. package/dist/daemon/human/finish.js +101 -38
  249. package/dist/daemon/human/sweep.js +5 -1
  250. package/dist/daemon/profile-delete.d.ts +7 -0
  251. package/dist/daemon/profile-delete.js +306 -0
  252. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  253. package/dist/daemon/review/deliver.js +3 -3
  254. package/dist/daemon/review/finish.d.ts +0 -2
  255. package/dist/daemon/review/finish.js +1 -29
  256. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  257. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  258. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  259. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  260. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  261. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  262. package/dist/pi-extensions/canvas-stophook.js +4 -4
  263. package/dist/shared/generated-context.js +2 -2
  264. package/dist/types.d.ts +44 -2
  265. package/dist/types.js +7 -2
  266. package/package.json +6 -5
  267. package/runtime.lock.json +5 -33
  268. package/dist/builtin-memory/init.md +0 -38
  269. package/dist/builtin-memory/plan.md +0 -19
  270. package/dist/builtin-memory/spec.md +0 -19
  271. package/dist/commands/human/doc.d.ts +0 -2
  272. package/dist/commands/human/doc.js +0 -91
  273. package/dist/commands/human/html.js +0 -81
  274. package/dist/commands/human/inbox.d.ts +0 -2
  275. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  276. package/dist/core/human/__tests__/page-render.test.js +0 -66
  277. package/dist/core/human/html-markdown.d.ts +0 -3
  278. package/dist/core/human/html-markdown.js +0 -288
  279. package/dist/core/human/page-render.d.ts +0 -4
  280. package/dist/core/human/page-render.js +0 -105
  281. package/dist/pages/bundle.css +0 -1
  282. package/dist/pages/bundle.js +0 -1778
  283. package/dist/pages/comments.d.ts +0 -72
  284. package/dist/pages/comments.js +0 -176
  285. package/dist/pages/controls.d.ts +0 -98
  286. package/dist/pages/controls.js +0 -261
  287. package/dist/pages/elements/cards.d.ts +0 -28
  288. package/dist/pages/elements/cards.js +0 -915
  289. package/dist/pages/elements/chart.d.ts +0 -23
  290. package/dist/pages/elements/chart.js +0 -664
  291. package/dist/pages/elements/options.d.ts +0 -30
  292. package/dist/pages/elements/options.js +0 -742
  293. package/dist/pages/elements/pages.d.ts +0 -30
  294. package/dist/pages/elements/pages.js +0 -377
  295. package/dist/pages/elements/slot.d.ts +0 -26
  296. package/dist/pages/elements/slot.js +0 -112
  297. package/dist/pages/elements/table.d.ts +0 -36
  298. package/dist/pages/elements/table.js +0 -1043
  299. package/dist/pages/elements/text.d.ts +0 -31
  300. package/dist/pages/elements/text.js +0 -1175
  301. package/dist/pages/entry.d.ts +0 -8
  302. package/dist/pages/entry.js +0 -8
  303. package/dist/pages/host.d.ts +0 -134
  304. package/dist/pages/host.js +0 -165
  305. package/dist/pages/readonly.d.ts +0 -24
  306. package/dist/pages/readonly.js +0 -31
  307. package/dist/pages/register.d.ts +0 -2
  308. package/dist/pages/register.js +0 -3
  309. package/dist/pages/responses.d.ts +0 -37
  310. package/dist/pages/responses.js +0 -56
  311. package/dist/pages/slot-config.d.ts +0 -36
  312. package/dist/pages/slot-config.js +0 -50
  313. package/dist/pages/types.d.ts +0 -98
  314. package/dist/pages/types.js +0 -23
  315. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  316. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -5,67 +5,73 @@ import { join } from 'node:path';
5
5
  import test from 'node:test';
6
6
  const home = mkdtempSync(join(tmpdir(), 'crtr-page-tickets-'));
7
7
  process.env['CRTR_HOME'] = home;
8
- const { atomicWriteJson, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
8
+ const { atomicWriteJson, bindTicketReplyRoute, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
9
9
  const { claimTicket } = await import('../claim.js');
10
10
  const { scanInbox, scanPageHistory } = await import('../scan.js');
11
11
  const { ticketDir } = await import('../root.js');
12
- const { cancelTicket, completePage, readPageProgress, readPageTicket, preparePage, readTicketResult, submitPage, takeoverAndCompletePage, writePageProgress, } = await import('../tickets.js');
12
+ const { cancelTicket, completePage, readPageProgress, readPageTicket, preparePage, readTicketResult, submitPage, takeoverAndCompletePage, validatePageTicketResponses, writePageProgress, } = await import('../tickets.js');
13
13
  const sources = mkdtempSync(join(tmpdir(), 'crtr-page-sources-'));
14
14
  function sourceFile(name, body) {
15
15
  const path = join(sources, name);
16
16
  writeFileSync(path, body);
17
17
  return path;
18
18
  }
19
- const askPage = sourceFile('ask.html', '<!doctype html><html><head><meta name="crtr-page-title" content="Rollout scope"><meta name="crtr-page-subtitle" content="Pick tonight’s rollout"><meta name="crtr-page-placement" content="inline"></head><body><p>Which rollout do you want?</p><crtr-pages><crtr-page><crtr-options id="rollout"><script type="application/json">{"mode":"single","options":[{"id":"canary","label":"Canary first"},{"id":"full","label":"Full"}]}</script></crtr-options></crtr-page></crtr-pages></body></html>');
20
- const displayPage = sourceFile('display.html', '<!doctype html><html><head><meta name="crtr-page-title" content="Nightly report"></head><body><h1>All green</h1></body></html>');
21
- function submit(id, source, dialect, blockedSince, nodeId = 'node-1', mode = 'ask') {
22
- const dir = submitPage({ dir: ticketDir(id), sourceFile: source, dialect, source: { nodeId, blockedSince } }).dir;
23
- atomicWriteJson(join(dir, 'run.json'), { mode });
19
+ const askPage = sourceFile('ask.tsx', `export default function Rollout() {
20
+ return (
21
+ <Page title="Rollout scope" subtitle="Pick tonight\u2019s rollout">
22
+ <p>Which rollout do you want?</p>
23
+ <UserQuestion id="rollout" label="Rollout lane" body="Canary limits the blast radius." mode="single" options={[{ id: 'canary', label: 'Canary first' }, { id: 'full', label: 'Full' }]} />
24
+ </Page>
25
+ );
26
+ }
27
+ `);
28
+ const displayPage = sourceFile('display.tsx', `export default function Nightly() {
29
+ return (
30
+ <Page title="Nightly report">
31
+ <h1>All green</h1>
32
+ </Page>
33
+ );
34
+ }
35
+ `);
36
+ function submit(id, source, emittedAt, nodeId = 'node-1', replyExpected = true) {
37
+ const dir = submitPage({ dir: ticketDir(id), sourceFile: source, source: { nodeId, emittedAt }, delivery: { placement: 'inline', inbox: replyExpected, reply: replyExpected } }).dir;
38
+ if (replyExpected)
39
+ bindTicketReplyRoute(dir, id);
24
40
  return dir;
25
41
  }
26
- test('submitPage publishes the authored source, the derived manifest and the blockedSince stamp', () => {
27
- const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage, dialect: 'html', source: { nodeId: 'node-1' } });
42
+ test('submitPage publishes the authored source, the derived manifest and the emittedAt stamp', () => {
43
+ const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage, source: { nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
28
44
  assert.equal(submitted.kind, 'page');
29
45
  assert.equal(submitted.id, 'ask');
30
- assert.equal(readFileSync(pagePath(submitted.dir, 'html'), 'utf8'), readFileSync(askPage, 'utf8'));
46
+ assert.equal(readFileSync(pagePath(submitted.dir), 'utf8'), readFileSync(askPage, 'utf8'));
31
47
  const manifest = JSON.parse(readFileSync(pageManifestPath(submitted.dir), 'utf8'));
32
- assert.equal(manifest['schema'], 'crtr.page/v1');
33
- assert.equal(manifest['document'], 'page.html');
34
- assert.equal(manifest['placement'], 'inline');
48
+ assert.equal(manifest['schema'], 'crtr.page/v2');
49
+ assert.equal(manifest['document'], 'page.tsx');
50
+ assert.deepEqual(manifest['delivery'], { placement: 'inline', inbox: true, reply: true });
35
51
  const source = manifest['source'];
36
52
  assert.equal(source.nodeId, 'node-1');
37
- assert.ok(Date.parse(source.blockedSince) > 0, 'submission stamps blockedSince');
38
- assert.throws(() => submitPage({ dir: ticketDir('ask'), sourceFile: askPage, dialect: 'html' }), /already exists/);
39
- atomicWriteJson(join(submitted.dir, 'run.json'), { mode: 'ask' });
53
+ assert.ok(Date.parse(source.emittedAt) > 0, 'submission stamps emittedAt');
54
+ // A ticket's request is replaceable in place — what `human send --replace`
55
+ // does. Republishing over the same directory revises the page and leaves one
56
+ // ticket behind, rather than refusing or raising a second one.
57
+ const stamp = { nodeId: 'node-replace', emittedAt: '2026-01-05T00:00:00.000Z' };
58
+ const first = submitPage({ dir: ticketDir('revisable'), sourceFile: askPage, source: stamp });
59
+ const revised = submitPage({ dir: ticketDir('revisable'), sourceFile: displayPage, source: stamp });
60
+ assert.equal(revised.id, first.id);
61
+ assert.equal(revised.manifest.title, 'Nightly report');
62
+ assert.equal(readFileSync(pagePath(revised.dir), 'utf8'), readFileSync(displayPage, 'utf8'));
63
+ assert.deepEqual(scanPageHistory('node-replace').map((entry) => entry.id), ['revisable'], 'a replaced page is one ticket, not two');
64
+ bindTicketReplyRoute(submitted.dir, 'ask');
40
65
  const listed = scanInbox().find((item) => item.id === 'ask');
41
66
  assert.equal(listed?.kind, 'page');
42
67
  assert.equal(listed?.kind === 'page' ? listed.awaitsResponse : undefined, true);
43
68
  assert.deepEqual(listed?.kind === 'page' ? listed.slotKinds : undefined, ['options']);
44
69
  });
45
- function tableSourcePage(dataFile) {
46
- return `<!doctype html><html><head><meta name="crtr-page-title" content="Data"><meta name="crtr-page-placement" content="inline"></head><body><crtr-pages><crtr-page><crtr-table id="rows"><script type="application/json">${JSON.stringify({ columns: [{ id: 'name', label: 'Name' }], source: dataFile, rowSelect: 'single' })}</script></crtr-table></crtr-page></crtr-pages></body></html>`;
47
- }
48
- test('source data is embedded into the submitted manifest', () => {
49
- const data = sourceFile('rows.json', JSON.stringify([{ id: 'one', cells: { name: 'One' } }]));
50
- const page = sourceFile('source-table.html', tableSourcePage(data));
51
- const submitted = submitPage({ dir: ticketDir('source-table'), sourceFile: page, dialect: 'html' });
52
- const config = submitted.manifest.slots[0].config;
53
- assert.deepEqual(config.rows, [{ id: 'one', cells: { name: 'One' } }]);
54
- assert.equal('source' in config, false);
55
- });
56
- test('invalid source data fails before a ticket is written', () => {
57
- const cases = [
58
- ['missing', join(sources, 'missing.json'), /does not exist/],
59
- ['invalid-json', sourceFile('invalid.json', '{nope'), /not valid JSON/],
60
- ['invalid-shape', sourceFile('invalid-shape.json', JSON.stringify([{ cells: { name: 'Missing id' } }])), /source data is invalid/],
61
- ];
62
- for (const [id, data, expected] of cases) {
63
- const page = sourceFile(`${id}.html`, tableSourcePage(data));
64
- const dir = ticketDir(`source-${id}`);
65
- assert.throws(() => preparePage({ sourceFile: page, dialect: 'html' }), expected);
66
- assert.throws(() => submitPage({ dir, sourceFile: page, dialect: 'html' }), expected);
67
- assert.equal(existsSync(dir), false, `${id} source validation writes no ticket`);
68
- }
70
+ test('an HTML page derives a slotless manifest from declared metadata', () => {
71
+ const report = sourceFile('report.html', '<!doctype html><html><head><meta content="Summary" name="description"><title>Report</title></head><body>done</body></html>');
72
+ const prepared = preparePage({ sourceFile: report });
73
+ assert.equal(prepared.module, undefined);
74
+ assert.deepEqual(prepared.manifest, { schema: 'crtr.page/v2', dialect: 'html', title: 'Report', subtitle: 'Summary', delivery: { placement: 'inline', inbox: false, reply: false }, document: 'page.html', steps: 1, slots: [], source: prepared.manifest.source });
69
75
  });
70
76
  test('progress round-trips through validation and rejects malformed saved work', () => {
71
77
  const dir = ticketDir('ask');
@@ -76,6 +82,11 @@ test('progress round-trips through validation and rejects malformed saved work',
76
82
  assert.throws(() => writePageProgress(dir, { unknown: { selectedOptionIds: [], comments: [] } }), /unknown/);
77
83
  atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: 'not an object' }, savedAt: new Date().toISOString() });
78
84
  assert.throws(() => readPageProgress(dir), /malformed/, 'malformed progress is an error, never silently discarded');
85
+ // Saved work can outlive the slot it answered, because the agent may replace
86
+ // the page under it. The stale entry is dropped on read so the ticket stays
87
+ // readable; every entry the current page still declares hydrates.
88
+ atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: { selectedOptionIds: ['canary'], comments: [] }, retired: { selectedOptionIds: ['gone'], comments: [] } }, savedAt: new Date().toISOString() });
89
+ assert.deepEqual(readPageProgress(dir), { rollout: { selectedOptionIds: ['canary'], comments: [] } });
79
90
  writePageProgress(dir, { rollout: { selectedOptionIds: ['canary'], comments: [] } });
80
91
  });
81
92
  test('readPageTicket derives pending, resolved and canceled state and carries the document', () => {
@@ -101,20 +112,22 @@ test('readPageTicket derives pending, resolved and canceled state and carries th
101
112
  assert.equal(resolved.state, 'resolved');
102
113
  assert.equal(resolved.progress, null);
103
114
  assert.equal(scanInbox().some((item) => item.id === 'ask'), false, 'a resolved page leaves the pending list');
104
- const canceledDir = submit('canceled', askPage, 'html', '2026-01-03T00:00:00.000Z');
115
+ const canceledDir = submit('canceled', askPage, '2026-01-03T00:00:00.000Z');
105
116
  cancelTicket(canceledDir, { reason: 'no longer needed' });
106
117
  assert.equal(readPageTicket(canceledDir).state, 'canceled');
107
118
  });
108
- test('page scan derives response expectation from the ticket mode', () => {
109
- const dir = submit('display', displayPage, 'html', '2026-01-04T00:00:00.000Z');
119
+ test('page scan derives response expectation from manifest delivery', () => {
120
+ const dir = submit('display', displayPage, '2026-01-04T00:00:00.000Z');
110
121
  const ticket = readPageTicket(dir);
111
- assert.equal(ticket.manifest.dialect, 'html');
122
+ assert.equal(ticket.manifest.dialect, 'jsx');
112
123
  assert.equal(ticket.manifest.slots.length, 0);
113
124
  const display = scanInbox().find((item) => item.id === 'display');
114
- assert.equal(display?.kind === 'page' ? display.awaitsResponse : undefined, true, 'an ask waits even when its page has no response-bearing slots');
115
- submit('notify-with-options', askPage, 'html', '2026-01-04T00:00:01.000Z', 'node-1', 'notify');
116
- const notify = scanInbox().find((item) => item.id === 'notify-with-options');
117
- assert.equal(notify?.kind === 'page' ? notify.awaitsResponse : undefined, false, 'a notify never waits even when its page has a response-bearing slot');
125
+ assert.equal(display?.kind === 'page' ? display.awaitsResponse : undefined, true, 'a reply-bearing page waits even with no response-bearing slots');
126
+ const announcementDir = submit('announcement-with-options', askPage, '2026-01-04T00:00:01.000Z', 'node-1', false);
127
+ const announcement = scanInbox().find((item) => item.id === 'announcement-with-options');
128
+ assert.equal(announcement, undefined, 'a passive page stays out of the inbox');
129
+ assert.equal(readPageTicket(announcementDir).state, 'passive');
130
+ assert.throws(() => takeoverAndCompletePage(announcementDir, {}, { host: 'crtrd', pid: process.pid }), /page_has_no_response_lifecycle/);
118
131
  const acknowledged = takeoverAndCompletePage(dir, {}, { host: 'crtrd', pid: process.pid });
119
132
  assert.equal(acknowledged.won, true);
120
133
  const written = JSON.parse(readFileSync(responsePath(dir), 'utf8'));
@@ -123,11 +136,11 @@ test('page scan derives response expectation from the ticket mode', () => {
123
136
  assert.equal(written['summary'], '');
124
137
  });
125
138
  test('scanPageHistory returns every page of one node, oldest first, regardless of terminal file', () => {
126
- submit('older', askPage, 'html', '2026-01-01T00:00:00.000Z');
127
- submit('other-node', askPage, 'html', '2026-01-02T00:00:00.000Z', 'node-2');
139
+ submit('older', askPage, '2026-01-01T00:00:00.000Z');
140
+ submit('other-node', askPage, '2026-01-02T00:00:00.000Z', 'node-2');
128
141
  const history = scanPageHistory('node-1');
129
- assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', 'notify-with-options', 'ask']);
130
- assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', 'pending', 'resolved']);
142
+ assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', 'announcement-with-options', 'ask']);
143
+ assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', 'passive', 'resolved']);
131
144
  assert.deepEqual(scanPageHistory('node-2').map((entry) => entry.id), ['other-node']);
132
145
  assert.deepEqual(scanPageHistory('node-missing'), []);
133
146
  });
@@ -1,54 +1,51 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
2
  import test from 'node:test';
6
- import { derivePageManifest, parsePageSource } from '../page.js';
3
+ import { derivePageManifestFromDocument } from '../page.js';
7
4
  import { isResponseBearingSlot, validatePageManifest, validatePageResponses, validatePartialPageResponses } from '../page-schema.js';
8
- function withSource(name, source, run) {
9
- const dir = mkdtempSync(join(tmpdir(), 'crtr-page-test-'));
10
- try {
11
- const path = join(dir, name);
12
- writeFileSync(path, source);
13
- run(path);
14
- }
15
- finally {
16
- rmSync(dir, { recursive: true, force: true });
17
- }
5
+ const OPTIONS = '<UserQuestion id="choice" label="Choose an option" body="One of these ships tonight." mode="single" options={[{ id: "yes", label: "Yes" }]} />';
6
+ function page(body, props = 'title="Test"') {
7
+ return `export default function Page_() {\n return (\n <Page ${props}>\n ${body}\n </Page>\n );\n}\n`;
18
8
  }
19
- const htmlHead = '<!doctype html><html><head><meta name="crtr-page-title" content="Test"></head><body>';
20
- const htmlTail = '</body></html>';
21
- const optionsScript = '<script type="application/json">{"label":"Choose an option","mode":"single","options":[{"id":"yes","label":"Yes"}]}</script>';
22
- function deriveHtml(body, head = htmlHead) {
23
- let manifest;
24
- withSource('input.html', `${head}${body}${htmlTail}`, (path) => { manifest = derivePageManifest(path, 'html'); });
25
- return manifest;
26
- }
27
- function expectHtmlError(body, pattern, head = htmlHead) {
28
- withSource('input.html', `${head}${body}${htmlTail}`, (path) => assert.throws(() => derivePageManifest(path, 'html'), pattern));
29
- }
30
- test('html dialect derives document-order slots and wrapper steps', () => {
31
- const body = `<crtr-pages><crtr-page><section><crtr-options id="choice">${optionsScript}</crtr-options></section></crtr-page><crtr-page><crtr-chart><script type="application/json">{"kind":"bar","x":"day","y":[{"field":"count"}]}</script></crtr-chart></crtr-page></crtr-pages>`;
32
- const manifest = deriveHtml(body, '<!doctype html><html placement="inline"><head><meta name="crtr-page-title" content="HTML"><meta name="crtr-page-subtitle" content="Two steps"><meta name="crtr-page-placement" content="inline"></head><body>');
33
- assert.equal(manifest.title, 'HTML');
34
- assert.equal(manifest.placement, 'inline');
9
+ test('jsx pages derive page props, steps, and document-order slots', () => {
10
+ const manifest = derivePageManifestFromDocument(page(`<Step>${OPTIONS}</Step>\n <Step><Chart kind="bar" x="day" y={[{ field: "count" }]} /></Step>`, 'title="JSX" subtitle="Two steps"'));
11
+ assert.equal(manifest.title, 'JSX');
12
+ assert.equal(manifest.subtitle, 'Two steps');
13
+ assert.deepEqual(manifest.delivery, { placement: 'inline', inbox: false, reply: false });
35
14
  assert.equal(manifest.steps, 2);
36
- assert.deepEqual(manifest.slots.map(({ kind, step }) => ({ kind, step })), [{ kind: 'options', step: 0 }, { kind: 'chart', step: 1 }]);
15
+ assert.deepEqual(manifest.slots.map(({ kind, step, id }) => ({ kind, step, id })), [
16
+ { kind: 'options', step: 0, id: 'choice' },
17
+ { kind: 'chart', step: 1, id: undefined },
18
+ ]);
19
+ });
20
+ test('a page without steps is one step, and a slot config computed at render time still reaches the manifest', () => {
21
+ const manifest = derivePageManifestFromDocument(page('<UserText id="note" label="Add a note" initialText="" />'));
22
+ assert.equal(manifest.steps, 1);
23
+ assert.deepEqual(manifest.slots, [{ id: 'note', kind: 'text', step: 0, config: { label: 'Add a note', initialText: '' } }]);
24
+ // The manifest is harvested by rendering the page, so mapped data, helper components,
25
+ // and hook state are all present; handlers are dropped because a manifest is JSON.
26
+ const computed = derivePageManifestFromDocument(`const ROWS = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
27
+ function Pick({ rows }) { return <UserQuestion id="picks" label="Pick" body="Rows come from data." mode="single" options={rows.map((r) => ({ id: r.id, label: r.label }))} onSelect={() => 1} />; }
28
+ export default function Computed() { const [title] = React.useState('Computed'); return <Page title={title}><Pick rows={ROWS} /></Page>; }
29
+ `);
30
+ assert.equal(computed.title, 'Computed');
31
+ assert.deepEqual(computed.slots, [{ id: 'picks', kind: 'options', step: 0, config: { label: 'Pick', body: 'Rows come from data.', mode: 'single', options: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] } }]);
32
+ });
33
+ test('a page that cannot render is rejected with the reason', () => {
34
+ assert.throws(() => derivePageManifestFromDocument(page(OPTIONS, 'title=""')), /nonempty title prop/);
35
+ assert.throws(() => derivePageManifestFromDocument(page(OPTIONS, 'title="T" placement="drawer"')), /placement is not authored/);
36
+ assert.throws(() => derivePageManifestFromDocument('function Page_() { return <Page title="T" />; }\n'), /must `export default` a component function/);
37
+ assert.throws(() => derivePageManifestFromDocument('export default function Page_() { return (<><Page title="A" /><Page title="B" /></>); }'), /exactly one is allowed/);
38
+ assert.throws(() => derivePageManifestFromDocument('export default () => <Page title="T"><Mystery /></Page>;'), /Mystery is not defined/);
39
+ assert.throws(() => derivePageManifestFromDocument('export default async () => <Page title="T" />;'), /async/);
40
+ });
41
+ test('module plumbing is rejected with the in-scope teaching', () => {
42
+ const importing = `import React from 'react';\n${page(OPTIONS)}`;
43
+ assert.throws(() => derivePageManifestFromDocument(importing), /page source uses import: .*already in scope/s);
44
+ const requiring = `const x = require('./data.json');\n${page(OPTIONS)}`;
45
+ assert.throws(() => derivePageManifestFromDocument(requiring), /page source uses require\(\)/);
37
46
  });
38
- test('html wrapper rejects multiple crtr-pages', () => expectHtmlError('<crtr-pages><crtr-page></crtr-page></crtr-pages><crtr-pages><crtr-page></crtr-page></crtr-pages>', /more than one <crtr-pages>/));
39
- test('html wrapper rejects crtr-page outside crtr-pages', () => expectHtmlError('<crtr-page></crtr-page>', /<crtr-page> outside <crtr-pages>/));
40
- test('html wrapper rejects empty crtr-pages', () => expectHtmlError('<crtr-pages></crtr-pages>', /empty <crtr-pages>/));
41
- test('html wrapper rejects a response-bearing element outside a step with the fix', () => expectHtmlError(`<crtr-options id="choice">${optionsScript}</crtr-options>`, /response-bearing element <crtr-options>[\s\S]*<crtr-pages><crtr-page>…<\/crtr-page><\/crtr-pages>/));
42
- test('html wrapper rejects nested crtr-pages', () => expectHtmlError('<crtr-pages><crtr-page><crtr-pages><crtr-page></crtr-page></crtr-pages></crtr-page></crtr-pages>', /nested <crtr-pages>/));
43
- test('html metadata enforces cardinality, known names, and placement content', () => {
44
- const duplicateTitle = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-title" content="B"></head><body>';
45
- const duplicateSubtitle = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-subtitle" content="A"><meta name="crtr-page-subtitle" content="B"></head><body>';
46
- const unknown = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-extra" content="x"></head><body>';
47
- const badPlacement = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-placement" content="drawer"></head><body>';
48
- expectHtmlError('', /Expected exactly one <meta name="crtr-page-title">, found 2/, duplicateTitle);
49
- expectHtmlError('', /Expected at most one <meta name="crtr-page-subtitle">, found 2/, duplicateSubtitle);
50
- expectHtmlError('', /unknown page metadata element <meta name="crtr-page-extra">/, unknown);
51
- expectHtmlError('', /content must be exactly inline or panel/, badPlacement);
47
+ test('a page that does not compile is reported with its location', () => {
48
+ assert.throws(() => derivePageManifestFromDocument('export default function Page_() { return (<Page title="Broken">; }'), /page does not compile: .*page\.tsx:\d+:\d+/s);
52
49
  });
53
50
  test('the response-bearing predicate handles table selection and catalog edges', () => {
54
51
  const slot = (kind, config, unvalidated) => ({ kind, step: 0, config, ...(unvalidated ? { unvalidated } : {}) });
@@ -57,66 +54,27 @@ test('the response-bearing predicate handles table selection and catalog edges',
57
54
  assert.equal(isResponseBearingSlot(slot('chart', { kind: 'bar', x: 'x', y: [{ field: 'y' }] })), false);
58
55
  assert.equal(isResponseBearingSlot(slot('product-kind', {}, true)), true);
59
56
  });
60
- test('display product slots derive as non-response-bearing without interactive structure', () => {
57
+ test('product components are authored under their PascalCase tag', () => {
61
58
  const displayComponent = [{ kind: 'nl-thread', display: true }];
62
- const displayBody = '<crtr-slot kind="nl-thread"><script type="application/json">{"messages":[]}</script></crtr-slot>';
63
- let displayManifest;
64
- withSource('input.html', `${htmlHead}${displayBody}${htmlTail}`, (path) => { displayManifest = derivePageManifest(path, 'html', displayComponent); });
65
- assert.deepEqual(displayManifest.slots, [{ kind: 'nl-thread', step: 0, config: { messages: [] }, unvalidated: true, display: true }]);
66
- assert.deepEqual(validatePageManifest(displayManifest, displayComponent).slots, displayManifest.slots);
67
- assert.equal(isResponseBearingSlot(displayManifest.slots[0]), false);
68
- const responseBody = '<crtr-slot kind="product-input"><script type="application/json">{}</script></crtr-slot>';
69
- withSource('input.html', `${htmlHead}${responseBody}${htmlTail}`, (path) => {
70
- assert.throws(() => derivePageManifest(path, 'html', [{ kind: 'product-input' }]), /product slot kind "product-input" at slot #0 requires an id="…" attribute on its <crtr-slot> element/);
71
- });
59
+ const manifest = derivePageManifestFromDocument(page('<NlThread messages={[]} />'), displayComponent);
60
+ assert.deepEqual(manifest.slots, [{ kind: 'nl-thread', step: 0, config: { messages: [] }, unvalidated: true, display: true }]);
61
+ assert.deepEqual(validatePageManifest(manifest).slots, manifest.slots);
62
+ assert.equal(isResponseBearingSlot(manifest.slots[0]), false);
63
+ assert.throws(() => derivePageManifestFromDocument(page('<ProductInput />'), [{ kind: 'product-input' }]), /product slot kind "product-input" at slot #0 requires an id prop/);
64
+ });
65
+ test('a question requires its label and body at authoring time', () => {
66
+ assert.throws(() => derivePageManifestFromDocument(page('<UserQuestion id="choice" body="Context." mode="single" options={[{ id: "yes", label: "Yes" }]} />')), /slot "choice" \(options\): label is required/);
67
+ assert.throws(() => derivePageManifestFromDocument(page('<UserQuestion id="choice" label="Choose" mode="single" options={[{ id: "yes", label: "Yes" }]} />')), /slot "choice" \(options\): body is required/);
72
68
  });
73
69
  test('multi-response pages require a label on every response-bearing slot', () => {
74
- expectHtmlError(`<crtr-pages><crtr-page><crtr-options id="choice"><script type="application/json">{"mode":"single","options":[{"id":"yes","label":"Yes"}]}</script></crtr-options><crtr-text id="note"><script type="application/json">{"label":"Add a note","initialText":""}</script></crtr-text></crtr-page></crtr-pages>`, /response-bearing slot "choice" requires a nonempty config\.label/);
75
- expectHtmlError(`<crtr-pages><crtr-page><crtr-options id="choice"><script type="application/json">{"label":"Choose","mode":"single","options":[{"id":"yes","label":"Yes"}]}</script></crtr-options><crtr-table id="table"><script type="application/json">{"label":"Pick a row","columns":[{"id":"name","label":"Name"}],"rows":[{"id":"one","cells":{"name":"One"}}],"rowSelect":"single","caption":"old"}</script></crtr-table></crtr-page></crtr-pages>`, /invalid config for slot "table"/);
70
+ assert.throws(() => derivePageManifestFromDocument(page(`<UserText id="first" initialText="" />\n <UserText id="note" label="Add a note" initialText="" />`)), /response-bearing slot "first" requires a nonempty config\.label/);
71
+ assert.throws(() => derivePageManifestFromDocument(page(`${OPTIONS}\n <UserTable id="table" label="Pick a row" columns={[{ id: "name", label: "Name" }]} rows={[{ id: "one", cells: { name: "One" } }]} rowSelect="single" caption="old" />`)), /invalid config for slot "table"/);
76
72
  });
77
73
  test('final and partial response validation share slot-addressed rules', () => {
78
- const manifest = deriveHtml(`<crtr-pages><crtr-page><crtr-options id="choice">${optionsScript}</crtr-options><crtr-text id="note"><script type="application/json">{"label":"Add a note","initialText":""}</script></crtr-text></crtr-page></crtr-pages>`);
74
+ const manifest = derivePageManifestFromDocument(page(`${OPTIONS}\n <UserText id="note" label="Add a note" initialText="" />`));
79
75
  assert.deepEqual(validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), { choice: { selectedOptionIds: ['yes'], comments: [] } });
80
76
  assert.throws(() => validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), /missing response-bearing slot id "note"/);
81
- assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false, comments: [] } }), { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false, comments: [] } });
77
+ assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false } }), { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false } });
82
78
  assert.throws(() => validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['maybe'], comments: [] } }), /unknown option id "maybe"/);
83
79
  assert.throws(() => validatePartialPageResponses(manifest, { extra: {} }), /unknown or non-response-bearing slot id "extra"/);
84
80
  });
85
- test('PageSourceProjection rejects changed inline component data', () => {
86
- const body = '<crtr-table><script type="application/json">{"columns":[{"id":"name","label":"Name"}],"rows":[{"id":"one","cells":{"name":"One"}}]}</script></crtr-table>';
87
- const manifest = deriveHtml(body);
88
- const changed = {
89
- ...manifest,
90
- slots: [{ ...manifest.slots[0], config: { columns: [{ id: 'name', label: 'Name' }], rows: [{ id: 'two', cells: { name: 'Two' } }] } }],
91
- };
92
- assert.throws(() => parsePageSource(`${htmlHead}${body}${htmlTail}`, changed), /does not match manifest slot/);
93
- });
94
- test('PageSourceProjection preserves legacy markdown replay and interleaves HTML prose', () => {
95
- const legacySource = `---
96
- title: Projection
97
- ---
98
- Leading prose.
99
-
100
- \`\`\`slot:text
101
- {"id":"first","label":"First answer","initialText":""}
102
- \`\`\`
103
-
104
- Trailing prose.
105
- `;
106
- const legacyManifest = {
107
- schema: 'crtr.page/v1', dialect: 'md', title: 'Projection', placement: 'panel', document: 'page.md', steps: 1,
108
- slots: [{ id: 'first', kind: 'text', step: 0, config: { label: 'First answer', initialText: '' } }],
109
- };
110
- assert.deepEqual(parsePageSource(legacySource, legacyManifest), [
111
- { kind: 'prose', markdown: 'Leading prose.' },
112
- { kind: 'slot', slotIndex: 0 },
113
- { kind: 'prose', markdown: 'Trailing prose.' },
114
- ]);
115
- const htmlSource = '<!doctype html><html><head><meta name="crtr-page-title" content="Test"></head><body><p>Leading prose.</p><crtr-pages><crtr-page><crtr-text id="first"><script type="application/json">{"label":"First answer","initialText":""}</script></crtr-text><p>Trailing prose.</p></crtr-page></crtr-pages></body></html>';
116
- const htmlManifest = deriveHtml('<p>Leading prose.</p><crtr-pages><crtr-page><crtr-text id="first"><script type="application/json">{"label":"First answer","initialText":""}</script></crtr-text><p>Trailing prose.</p></crtr-page></crtr-pages>');
117
- assert.deepEqual(parsePageSource(htmlSource, htmlManifest), [
118
- { kind: 'prose', markdown: '<p>Leading prose.</p>' },
119
- { kind: 'slot', slotIndex: 0 },
120
- { kind: 'prose', markdown: '<p>Trailing prose.</p>' },
121
- ]);
122
- });
@@ -12,10 +12,10 @@ const temp = mkdtempSync(join(tmpdir(), 'crtr-inbox-core-'));
12
12
  process.env['CRTR_HOME'] = temp;
13
13
  const source = join(temp, 'source.md');
14
14
  writeFileSync(source, '# Source\n');
15
- const pageSource = join(temp, 'page.html');
16
- writeFileSync(pageSource, '<!doctype html><html><head><meta name="crtr-page-title" content="A page"><meta name="crtr-page-subtitle" content="Go? requires your attention."></head><body><crtr-pages><crtr-page><crtr-text id="go"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
17
- const page = (id, blockedSince) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, dialect: 'html', source: { blockedSince, nodeId: 'node-1' } });
18
- const answer = { go: { text: 'yes', edited: true, comments: [] } };
15
+ const pageSource = join(temp, 'page.tsx');
16
+ writeFileSync(pageSource, 'export default () => (\n <Page title="A page" subtitle="Go? requires your attention.">\n <UserText id="go" initialText="" />\n </Page>\n);\n');
17
+ const page = (id, emittedAt) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, source: { emittedAt, nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
18
+ const answer = { go: { text: 'yes', edited: true } };
19
19
  async function waitFor(paths) {
20
20
  for (let attempts = 0; !paths.every(existsSync); attempts++) {
21
21
  if (attempts > 1_000)
@@ -32,7 +32,7 @@ assert.equal(existsSync(ticketDir('bad-review')), false, 'invalid submission doe
32
32
  const prepared = ticketDir('prepared');
33
33
  mkdirSync(prepared);
34
34
  writeFileSync(join(prepared, 'asset.png'), 'prepared asset');
35
- const preparedTicket = submitPage({ dir: prepared, sourceFile: pageSource, dialect: 'html', source: { blockedSince: '2025-01-02T00:00:00.000Z', nodeId: 'node-1' } });
35
+ const preparedTicket = submitPage({ dir: prepared, sourceFile: pageSource, source: { emittedAt: '2025-01-02T00:00:00.000Z', nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
36
36
  assert.equal(preparedTicket.dir, realpathSync(prepared), 'submission accepts a crouter-precreated direct child and colocated assets');
37
37
  // Same timestamp is stable by id; progress is resumable work, never a hidden lease.
38
38
  atomicWriteJson(progressPath(newPage.dir), { kind: 'page', responses: {}, savedAt: new Date().toISOString() });
@@ -94,7 +94,7 @@ for (const [index, fixture] of rejectFixtures.entries()) {
94
94
  const protocolDir = ticketDir('stale-protocol');
95
95
  mkdirSync(protocolDir);
96
96
  writeFileSync(responsePath(protocolDir), '{}');
97
- assert.throws(() => submitPage({ dir: protocolDir, sourceFile: pageSource, dialect: 'html' }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
97
+ assert.throws(() => submitPage({ dir: protocolDir, sourceFile: pageSource }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
98
98
  const cancelDir = page('cancel-race', '2025-01-03T00:00:00.000Z').dir;
99
99
  const canceled = cancelTicket(cancelDir, { reason: 'stop' });
100
100
  assert.equal(canceled.status, 'canceled');
@@ -26,7 +26,7 @@ function groupLines(group) {
26
26
  return [`${groupName(group)}: ${group.picked.length === 0 ? 'none' : group.picked.map(item).join(', ')}`];
27
27
  }
28
28
  function commentLines(comment) {
29
- const prefix = comment.anchor.kind === 'whole' ? 'Comment: ' : `Comment on ${comment.anchor.kind === 'range' ? inline(comment.anchorLabel) : `“${comment.anchorLabel}”`}: `;
29
+ const prefix = comment.anchor.kind === 'whole' ? 'Comment: ' : `Comment on ${inline(comment.anchorLabel)}: `;
30
30
  return verbatim(prefix, comment.text);
31
31
  }
32
32
  function shape(slot) {
@@ -84,13 +84,13 @@ function slotLines(slot, totalSlots) {
84
84
  export function renderAnswerText(answer, responsePath) {
85
85
  if (answer.kind === 'acknowledgement') {
86
86
  return [
87
- `The human saw **${answer.title}** and dismissed it. The page asked for nothing, so the acknowledgement is the whole answer.`,
87
+ `The user saw **${answer.title}** and dismissed it. The page asked for nothing, so the acknowledgement is the whole answer.`,
88
88
  '',
89
89
  `Acknowledged ${answer.completedAt}. Response record: ${responsePath}`,
90
90
  ].join('\n');
91
91
  }
92
92
  const answered = answer.slots.filter((slot) => slot.state === 'answered');
93
- const lines = [`The human answered **${answer.title}**.`];
93
+ const lines = [`The user answered **${answer.title}**.`];
94
94
  for (const slot of answered)
95
95
  lines.push('', ...slotLines(slot, answer.slots.length));
96
96
  lines.push('', `Answered ${answer.completedAt}. Typed responses: ${responsePath}`);
@@ -1,4 +1,3 @@
1
- import type { ProductPageComponents } from '../../types.js';
2
1
  import { type CommentAnchor, type PageResponses, type PageSlot } from './page-schema.js';
3
2
  export type PageAnswer = {
4
3
  title: string;
@@ -34,16 +33,19 @@ export type PageAnswerGroup = {
34
33
  label: string;
35
34
  }[];
36
35
  };
36
+ /** Stored responses predating the `whole` anchor's removal still carry it; answers keep rendering them. */
37
+ export type LegacyCommentAnchor = CommentAnchor | {
38
+ kind: 'whole';
39
+ };
37
40
  export type PageAnswerComment = {
38
- anchor: CommentAnchor;
41
+ anchor: LegacyCommentAnchor;
39
42
  anchorLabel: string;
40
43
  text: string;
41
44
  };
42
45
  export interface DescribePageAnswerOptions {
43
46
  completedAt: string;
44
- productKinds?: ProductPageComponents;
45
47
  }
46
- export declare function answerCommentAnchorLabel(slot: PageSlot, anchor: CommentAnchor): string;
48
+ export declare function answerCommentAnchorLabel(slot: PageSlot, anchor: LegacyCommentAnchor): string;
47
49
  /** Derive the one shared answer model from the immutable page manifest and a response map. */
48
50
  /** `manifest` is unknown deliberately: this boundary validates persisted JSON before deriving. */
49
51
  export declare function describePageAnswer(manifest: unknown, responses: PageResponses, opts: DescribePageAnswerOptions): PageAnswer;
@@ -28,15 +28,9 @@ function slotLabel(slot) {
28
28
  }
29
29
  export function answerCommentAnchorLabel(slot, anchor) {
30
30
  if (anchor.kind === 'whole')
31
- return slotLabel(slot);
32
- if (anchor.kind === 'range')
33
- return anchor.quote;
31
+ return slotLabel(slot); // legacy stored data only; new anchors are option/row
34
32
  if (anchor.kind === 'option')
35
33
  return slot.config.options.find((option) => option.id === anchor.optionId)?.label ?? anchor.optionId;
36
- if (anchor.kind === 'card')
37
- return slot.config.cards?.find((card) => card.id === anchor.cardId)?.title ?? anchor.cardId;
38
- if (anchor.kind === 'column')
39
- return slot.config.columns.find((column) => column.id === anchor.columnId)?.label ?? anchor.columnId;
40
34
  return rowLabel(slot.config, anchor.rowId);
41
35
  }
42
36
  function describeComments(slot, response) {
@@ -65,7 +59,7 @@ function describeSlot(slot, response) {
65
59
  const text = value;
66
60
  const written = typeof text.text === 'string' ? text.text : '';
67
61
  const singleLine = slot.config.singleLine === true;
68
- return { ...base, blank: singleLine && written === '', groups: [], text: { value: written, edited: text.edited === true, ...(singleLine ? { singleLine: true } : {}) }, comments: describeComments(slot, value) };
62
+ return { ...base, blank: singleLine && written === '', groups: [], text: { value: written, edited: text.edited === true, ...(singleLine ? { singleLine: true } : {}) }, comments: [] };
69
63
  }
70
64
  if (slot.kind === 'table') {
71
65
  const config = slot.config;
@@ -80,14 +74,14 @@ function describeSlot(slot, response) {
80
74
  const config = slot.config;
81
75
  const cards = (config.cards ?? []).map((card) => ({ id: card.id, label: card.title }));
82
76
  const group = { group: 'card', mode: config.mode, total: cards.length, picked: picked(strings(value.selectedCardIds), cards) };
83
- return { ...base, blank: group.picked.length === 0, groups: [group], comments: describeComments(slot, value) };
77
+ return { ...base, blank: group.picked.length === 0, groups: [group], comments: [] };
84
78
  }
85
79
  throw new Error(`slot "${slot.id}" kind "${slot.kind}" does not accept a response`);
86
80
  }
87
81
  /** Derive the one shared answer model from the immutable page manifest and a response map. */
88
82
  /** `manifest` is unknown deliberately: this boundary validates persisted JSON before deriving. */
89
83
  export function describePageAnswer(manifest, responses, opts) {
90
- const page = validatePageManifest(manifest, opts.productKinds);
84
+ const page = validatePageManifest(manifest);
91
85
  const slots = page.slots.filter(isResponseBearingSlot).map((slot) => describeSlot(slot, hasOwn(responses, slot.id) ? responses[slot.id] : undefined));
92
86
  const answered = slots.filter((slot) => slot.state === 'answered');
93
87
  return {
@@ -1,14 +1,18 @@
1
1
  import type { PageComponentRegistration } from '../../types.js';
2
2
  export interface ComponentDoc {
3
- kind: string;
4
- tag: string;
3
+ /** Registry names this doc covers. A family (Card, CardHeader, …) shares one contract. */
4
+ names: readonly string[];
5
+ /** `structural` builds the page, `slot` is a registry component that gets its own manifest
6
+ * record (and, unless display-only, its own response), `display` is presentation only. */
7
+ group: 'structural' | 'slot' | 'display';
5
8
  selectionLine: string;
6
9
  contract: string;
7
10
  }
8
11
  export declare const BUILTIN_COMPONENT_DOCS: readonly ComponentDoc[];
9
- /** Compact catalog rows shared by `human -h` and `human html list`. */
10
- export declare const BUILTIN_COMPONENT_SELECTION_LINES: string[];
11
- export declare function componentDoc(kind: string): ComponentDoc | undefined;
12
- export declare function componentKinds(productKinds: readonly string[]): string[];
12
+ /** The structural and response-bearing rows — the ones that decide what a page can ask. `human -h` carries these;
13
+ * the display set is one line there and the full catalog is `human components list`. */
14
+ export declare const CORE_COMPONENT_SELECTION_LINES: string[];
15
+ export declare function componentDoc(name: string): ComponentDoc | undefined;
16
+ export declare function componentNames(productComponents: readonly PageComponentRegistration[]): string[];
13
17
  export declare function renderComponentCatalog(productComponents: readonly PageComponentRegistration[]): string;
14
- export declare function renderComponentContract(kind: string, productComponents: readonly PageComponentRegistration[]): string | undefined;
18
+ export declare function renderComponentContract(name: string, productComponents: readonly PageComponentRegistration[]): string | undefined;