@north-light/crouter 0.3.197 → 0.3.199

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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  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 +126 -33
  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 +8 -8
  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 +3 -3
  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/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,396 +1,177 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { checkMarkdown } from '../termrender/termrender.js';
4
- import { BUILTIN_PAGE_KINDS, isRegisteredPageKind } from './page-catalog.js';
5
- import { isResponseBearingSlot, validatePageManifest } from './page-schema.js';
6
- /** A page failed authoring or validation; the caller can report it as bad page input. */
7
- export class PageAuthoringError extends Error {
8
- constructor(message) {
9
- super(message);
10
- this.name = 'PageAuthoringError';
11
- }
12
- }
3
+ import { transformSync } from 'esbuild';
4
+ import ts from 'typescript';
5
+ import { BUILTIN_KIND_BY_TAG, BUILTIN_PAGE_COMPONENT_TAGS, DISPLAY_PAGE_COMPONENT_TAGS, pageComponentTag } from './page-catalog.js';
6
+ import { isResponseBearingSlot, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
7
+ import { evaluatePageTree, jsonProps } from './page-eval.js';
8
+ import { PageAuthoringError } from './page-errors.js';
9
+ export { PageAuthoringError };
13
10
  function asPageAuthoringError(error) {
14
11
  if (error instanceof PageAuthoringError)
15
12
  return error;
16
13
  return new PageAuthoringError(error instanceof Error ? error.message : String(error));
17
14
  }
18
- function validateAuthoredManifest(parsed, productKinds) {
15
+ function validateAuthoredManifest(parsed, productComponents) {
19
16
  try {
20
- return validatePageManifest(parsed, productKinds);
17
+ const manifest = validateAuthoredPageManifest(parsed, productComponents);
18
+ const responseBearing = manifest.slots.filter(isResponseBearingSlot);
19
+ if (responseBearing.length >= 2) {
20
+ for (const slot of responseBearing) {
21
+ const label = slot.config.label;
22
+ if (typeof label !== 'string' || label === '')
23
+ throw new PageAuthoringError(`response-bearing slot "${slot.id}" requires a nonempty config.label when the page has multiple response-bearing slots`);
24
+ }
25
+ }
26
+ return manifest;
21
27
  }
22
28
  catch (error) {
23
29
  throw asPageAuthoringError(error);
24
30
  }
25
31
  }
26
- function parseJsonObject(raw, thing) {
27
- let value;
32
+ const IMPORTLESS_SCOPE = 'a page is evaluated with React and every registry component already in scope, so it takes no imports — write `<Page>`, `<UserQuestion>`, `<Card>` and friends directly.';
33
+ /** Compile one authored TSX page to the CommonJS module the Gateway evaluates.
34
+ * Registry names stay free identifiers; the Gateway supplies them at eval. */
35
+ export function compilePage(document) {
28
36
  try {
29
- value = JSON.parse(raw);
37
+ return transformSync(document, {
38
+ loader: 'tsx',
39
+ format: 'cjs',
40
+ jsx: 'transform',
41
+ jsxFactory: 'React.createElement',
42
+ jsxFragment: 'React.Fragment',
43
+ target: 'es2020',
44
+ sourcefile: 'page.tsx',
45
+ }).code;
30
46
  }
31
47
  catch (error) {
32
- throw new PageAuthoringError(`${thing} must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`);
33
- }
34
- if (value === null || Array.isArray(value) || typeof value !== 'object')
35
- throw new PageAuthoringError(`${thing} JSON must be one object`);
36
- return value;
37
- }
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;
48
+ const errors = error.errors;
49
+ const first = errors?.[0];
50
+ if (first === undefined)
51
+ throw asPageAuthoringError(error);
52
+ const at = first.location == null ? '' : ` (page.tsx:${first.location.line}:${first.location.column})`;
53
+ throw new PageAuthoringError(`page does not compile: ${first.text ?? 'unknown error'}${at}`);
54
+ }
55
+ }
56
+ function parseTsx(document) {
57
+ return ts.createSourceFile('page.tsx', document, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
58
+ }
59
+ function eachNode(node, visit) {
60
+ visit(node);
61
+ node.forEachChild((child) => eachNode(child, visit));
62
+ }
63
+ /** Reject module plumbing: the compiled module is evaluated with no loader behind it,
64
+ * so any `import` or `require` could only fail at render time. */
65
+ function rejectModulePlumbing(sourceFile) {
66
+ let offense;
67
+ eachNode(sourceFile, (node) => {
68
+ if (offense !== undefined)
69
+ return;
70
+ if (ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node))
71
+ offense = 'import';
72
+ else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== undefined)
73
+ offense = 'export … from';
74
+ else if (ts.isCallExpression(node)) {
75
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword)
76
+ offense = 'dynamic import()';
77
+ else if (ts.isIdentifier(node.expression) && node.expression.text === 'require')
78
+ offense = 'require()';
52
79
  }
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 };
80
+ });
81
+ if (offense !== undefined)
82
+ throw new PageAuthoringError(`page source uses ${offense}: ${IMPORTLESS_SCOPE}`);
83
+ }
84
+ function stringProp(props, name) {
85
+ const value = props[name];
86
+ if (value === undefined)
87
+ return undefined;
88
+ if (typeof value !== 'string')
89
+ throw new PageAuthoringError(`<Page> prop ${name} must be a string; it is part of the page manifest`);
90
+ return value;
84
91
  }
85
- function sourceLines(source, start) {
86
- const lines = [];
87
- let cursor = start;
88
- while (cursor < source.length) {
89
- const newline = source.indexOf('\n', cursor);
90
- const end = newline === -1 ? source.length : newline + 1;
91
- const contentEnd = newline === -1 ? source.length : newline;
92
- lines.push({ start: cursor, end, contentEnd, text: source.slice(cursor, contentEnd).replace(/\r$/, '') });
93
- cursor = end;
92
+ function scanPage(document, code, productComponents) {
93
+ rejectModulePlumbing(parseTsx(document));
94
+ const inScope = [...BUILTIN_PAGE_COMPONENT_TAGS, ...DISPLAY_PAGE_COMPONENT_TAGS, ...productComponents.map((component) => pageComponentTag(component.kind))];
95
+ const roots = evaluatePageTree(code, inScope);
96
+ const pages = [];
97
+ const collectPages = (nodes) => {
98
+ for (const node of nodes) {
99
+ if (node.tag === 'Page')
100
+ pages.push(node);
101
+ collectPages(node.children);
102
+ }
103
+ };
104
+ collectPages(roots);
105
+ if (pages.length === 0)
106
+ throw new PageAuthoringError('page default export must render exactly one <Page title="…"> root; it rendered none');
107
+ if (pages.length > 1)
108
+ throw new PageAuthoringError(`page default export renders ${pages.length} <Page> elements; exactly one is allowed`);
109
+ const page = pages[0];
110
+ const title = (stringProp(page.props, 'title') ?? '').trim();
111
+ if (title === '')
112
+ throw new PageAuthoringError('<Page> requires a nonempty title prop');
113
+ const subtitle = stringProp(page.props, 'subtitle')?.trim();
114
+ if (page.props['placement'] !== undefined)
115
+ throw new PageAuthoringError('<Page> placement is not authored; choose --placement inline or --placement panel when sending the page.');
116
+ const productTags = new Map();
117
+ for (const component of productComponents) {
118
+ const tag = pageComponentTag(component.kind);
119
+ if (BUILTIN_PAGE_COMPONENT_TAGS.has(tag))
120
+ throw new PageAuthoringError(`product page component kind "${component.kind}" derives JSX tag "${tag}", which collides with a built-in page component`);
121
+ if (productTags.has(tag))
122
+ throw new PageAuthoringError(`product page component kind "${component.kind}" derives JSX tag "${tag}", which duplicates product page component kind "${productTags.get(tag)}"`);
123
+ productTags.set(tag, component.kind);
94
124
  }
95
- return lines;
96
- }
97
- /** CommonMark closing fence: at least as many of the opener's fence character, nothing else on the line. */
98
- function closesFence(text, opener) {
99
- const trimmed = text.trimEnd();
100
- return trimmed.length >= opener.length && trimmed.split('').every((character) => character === opener.char);
101
- }
102
- function addProse(projection, raw) {
103
- const markdown = raw.trim();
104
- if (markdown !== '')
105
- projection.push({ kind: 'prose', markdown });
106
- }
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;
116
125
  const slots = [];
117
- const projection = [];
118
- let proseStart = meta.bodyStart;
119
- let ordinaryFence = null;
120
- for (let index = 0; index < lines.length; index++) {
121
- const line = lines[index];
122
- if (ordinaryFence !== null) {
123
- if (closesFence(line.text, ordinaryFence))
124
- ordinaryFence = null;
125
- continue;
126
- }
127
- const opening = line.text.match(/^(`{3,}|~{3,})(.*)$/);
128
- if (opening === null)
129
- continue;
130
- const run = opening[1];
131
- const fence = { char: run[0], length: run.length };
132
- const info = opening[2];
133
- if (fence.char !== '`' || !info.startsWith('slot:')) {
134
- ordinaryFence = fence;
135
- continue;
136
- }
137
- 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}`);
140
- }
141
- let closeIndex = -1;
142
- for (let candidate = index + 1; candidate < lines.length; candidate++) {
143
- if (closesFence(lines[candidate].text, fence)) {
144
- closeIndex = candidate;
145
- break;
126
+ let stepCount = 0;
127
+ const collectSlots = (nodes, step) => {
128
+ for (const node of nodes) {
129
+ const inStep = node.tag === 'Step' ? stepCount++ : step;
130
+ const builtin = BUILTIN_KIND_BY_TAG[node.tag];
131
+ const product = productTags.get(node.tag);
132
+ if (builtin !== undefined || product !== undefined) {
133
+ const { id, ...config } = jsonProps(node.props);
134
+ if (id !== undefined && typeof id !== 'string')
135
+ throw new PageAuthoringError(`<${node.tag}> id prop must be a string`);
136
+ slots.push({
137
+ ...(id === undefined ? {} : { id }),
138
+ kind: builtin ?? product,
139
+ step: inStep,
140
+ config,
141
+ ...(builtin === undefined ? { unvalidated: true } : {}),
142
+ });
146
143
  }
144
+ collectSlots(node.children, inStep);
147
145
  }
148
- if (closeIndex === -1)
149
- throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has no matching ${run.length}-backtick closing fence`);
150
- 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}`);
152
- const { id, ...config } = rawObject;
153
- if (id !== undefined && typeof id !== 'string')
154
- throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has non-string reserved id`);
155
- slots.push({ ...(id === undefined ? {} : { id }), kind, step: 0, config, ...(BUILTIN_PAGE_KINDS.includes(kind) ? {} : { unvalidated: true }) });
156
- projection.push({ kind: 'slot', slotIndex: slots.length - 1 });
157
- proseStart = lines[closeIndex].end;
158
- index = closeIndex;
159
- }
160
- addProse(projection, document.slice(proseStart));
161
- return { title: meta.title, ...(meta.subtitle === undefined ? {} : { subtitle: meta.subtitle }), placement: meta.placement, steps: 1, slots, projection };
146
+ };
147
+ collectSlots(page.children, 0);
148
+ return { title, ...(subtitle === undefined || subtitle === '' ? {} : { subtitle }), steps: Math.max(stepCount, 1), slots };
162
149
  }
163
- const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
164
- function tagEnd(html, start) {
165
- let quote = null;
166
- for (let index = start + 1; index < html.length; index++) {
167
- const character = html[index];
168
- if (quote !== null) {
169
- if (character === quote)
170
- quote = null;
171
- continue;
172
- }
173
- if (character === '"' || character === "'") {
174
- quote = character;
175
- continue;
176
- }
177
- if (character === '>')
178
- return index + 1;
179
- }
180
- throw new PageAuthoringError(`unterminated HTML tag beginning at byte ${start}`);
181
- }
182
- function parseAttributes(source) {
183
- const attrs = new Map();
184
- const tagNameEnd = source.search(/\s/);
185
- const body = tagNameEnd === -1 ? '' : source.slice(tagNameEnd);
186
- const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
187
- for (const match of body.matchAll(pattern))
188
- attrs.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '');
189
- return attrs;
150
+ /** Derive the transport manifest from one authored TSX page document. */
151
+ export function derivePageManifestFromDocument(document, productComponents = [], source, delivery = { placement: 'inline', inbox: false, reply: false }) {
152
+ const scanned = scanPage(document, compilePage(document), productComponents);
153
+ return validateAuthoredManifest({ schema: 'crtr.page/v2', dialect: 'jsx', title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), delivery, document: 'page.tsx', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productComponents);
190
154
  }
191
- function parseHtmlElements(html) {
192
- const roots = [];
193
- const all = [];
194
- const stack = [];
195
- let cursor = 0;
196
- while (cursor < html.length) {
197
- const start = html.indexOf('<', cursor);
198
- if (start === -1)
199
- break;
200
- if (html.startsWith('<!--', start)) {
201
- const end = html.indexOf('-->', start + 4);
202
- if (end === -1)
203
- throw new PageAuthoringError(`unterminated HTML comment at byte ${start}`);
204
- cursor = end + 3;
205
- continue;
206
- }
207
- const end = tagEnd(html, start);
208
- const token = html.slice(start, end);
209
- if (/^<!/i.test(token) || /^<\?/i.test(token)) {
210
- cursor = end;
211
- continue;
212
- }
213
- const closing = token.match(/^<\s*\/\s*([^\s>]+)/);
214
- if (closing !== null) {
215
- const tag = closing[1].toLowerCase();
216
- let match = stack.length - 1;
217
- while (match >= 0 && stack[match].tag !== tag)
218
- match--;
219
- if (match >= 0)
220
- stack.length = match;
221
- cursor = end;
222
- continue;
223
- }
224
- const opening = token.match(/^<\s*([^\s/>]+)/);
225
- if (opening === null) {
226
- cursor = end;
227
- continue;
228
- }
229
- const tag = opening[1].toLowerCase();
230
- const parent = stack.at(-1);
231
- const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, ...(parent === undefined ? {} : { parent }), children: [] };
232
- if (parent === undefined)
233
- roots.push(element);
234
- else
235
- parent.children.push(element);
236
- all.push(element);
237
- const selfClosing = /\/\s*>$/.test(token) || VOID_TAGS.has(tag);
238
- if (tag === 'script' && !selfClosing) {
239
- const closePattern = /<\s*\/\s*script\s*>/gi;
240
- closePattern.lastIndex = end;
241
- const close = closePattern.exec(html);
242
- if (close === null)
243
- throw new PageAuthoringError(`unterminated <script> element at byte ${start}`);
244
- element.text = html.slice(end, close.index);
245
- cursor = closePattern.lastIndex;
246
- continue;
247
- }
248
- if (!selfClosing)
249
- stack.push(element);
250
- cursor = end;
251
- }
252
- return all;
253
- }
254
- function ancestor(element, tag) {
255
- let current = element.parent;
256
- while (current !== undefined) {
257
- if (current.tag === tag)
258
- return current;
259
- current = current.parent;
260
- }
261
- return undefined;
262
- }
263
- function decodeHtmlAttribute(value) {
264
- return value.replace(/&(?:amp|quot|apos|lt|gt);|&#(?:x[0-9a-f]+|\d+);/gi, (entity) => {
265
- const named = { '&amp;': '&', '&quot;': '"', '&apos;': "'", '&lt;': '<', '&gt;': '>' };
266
- const lower = entity.toLowerCase();
267
- if (named[lower] !== undefined)
268
- return named[lower];
269
- const numeric = lower.startsWith('&#x') ? Number.parseInt(lower.slice(3, -1), 16) : Number.parseInt(lower.slice(2, -1), 10);
270
- return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : entity;
271
- });
272
- }
273
- function validateHtmlStructure(html) {
274
- const counts = [
275
- ['doctype', /<!doctype\b[^>]*>/gi, 'doctype'],
276
- ['<head>', /<head(?=\s|>)/gi, '<head> opening tag'],
277
- ['<body>', /<body(?=\s|>)/gi, '<body> opening tag'],
278
- ];
279
- for (const [, pattern, label] of counts) {
280
- const count = html.match(pattern)?.length ?? 0;
281
- if (count !== 1)
282
- throw new PageAuthoringError(`Expected exactly one ${label}, found ${count}.`);
283
- }
284
- }
285
- function readHtmlMetadata(elements) {
286
- const recognized = new Map();
287
- for (const element of elements.filter((candidate) => candidate.tag === 'meta')) {
288
- const name = element.attrs.get('name');
289
- if (name === undefined || !name.startsWith('crtr-page-'))
290
- continue;
291
- if (!['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement'].includes(name))
292
- throw new PageAuthoringError(`unknown page metadata element <meta name="${name}">`);
293
- if (ancestor(element, 'head') === undefined)
294
- throw new PageAuthoringError(`<meta name="${name}"> must appear inside <head>`);
295
- const entries = recognized.get(name) ?? [];
296
- entries.push(element);
297
- recognized.set(name, entries);
298
- }
299
- for (const name of ['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement']) {
300
- const count = recognized.get(name)?.length ?? 0;
301
- if ((name === 'crtr-page-title' && count !== 1) || (name !== 'crtr-page-title' && count > 1)) {
302
- const expected = name === 'crtr-page-title' ? 'exactly one' : 'at most one';
303
- throw new PageAuthoringError(`Expected ${expected} <meta name="${name}">, found ${count}.`);
304
- }
305
- }
306
- const titleElement = recognized.get('crtr-page-title')[0];
307
- const title = decodeHtmlAttribute(titleElement.attrs.get('content') ?? '').trim();
308
- if (title === '')
309
- throw new PageAuthoringError(`<meta name="crtr-page-title"> requires nonempty content`);
310
- const subtitleElement = recognized.get('crtr-page-subtitle')?.[0];
311
- const subtitle = subtitleElement === undefined ? undefined : decodeHtmlAttribute(subtitleElement.attrs.get('content') ?? '').trim();
312
- const placementElement = recognized.get('crtr-page-placement')?.[0];
313
- const placementValue = placementElement === undefined ? 'panel' : decodeHtmlAttribute(placementElement.attrs.get('content') ?? '').trim();
314
- if (placementValue !== 'inline' && placementValue !== 'panel')
315
- throw new PageAuthoringError(`<meta name="crtr-page-placement"> content must be exactly inline or panel; received "${placementValue}"`);
316
- return { title, ...(subtitle === undefined ? {} : { subtitle }), placement: placementValue };
317
- }
318
- function configForHtmlElement(element) {
319
- const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
320
- if (script === undefined)
321
- throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} requires a direct <script type="application/json"> child`);
322
- return parseJsonObject(script.text ?? '', `<${element.tag}> at byte ${element.start}`);
323
- }
324
- function scanHtml(document, productKinds) {
325
- validateHtmlStructure(document);
326
- const elements = parseHtmlElements(document);
327
- const metadata = readHtmlMetadata(elements);
328
- const wrappers = elements.filter((element) => element.tag === 'crtr-pages');
329
- if (wrappers.some((candidate) => ancestor(candidate, 'crtr-pages') !== undefined))
330
- throw new PageAuthoringError('nested <crtr-pages> element is not allowed');
331
- if (wrappers.length > 1)
332
- throw new PageAuthoringError(`more than one <crtr-pages> element: found ${wrappers.length}`);
333
- const wrapper = wrappers[0];
334
- const pages = elements.filter((element) => element.tag === 'crtr-page');
335
- for (const page of pages)
336
- if (ancestor(page, 'crtr-pages') !== wrapper || wrapper === undefined)
337
- throw new PageAuthoringError(`<crtr-page> outside <crtr-pages> at byte ${page.start}`);
338
- if (wrapper !== undefined && pages.length === 0)
339
- throw new PageAuthoringError('empty <crtr-pages> element must contain at least one <crtr-page>');
340
- const pageIndexes = new Map(pages.map((page, index) => [page, index]));
341
- const rawSlots = [];
342
- const slotElements = [];
343
- for (const element of elements) {
344
- if (element.tag === 'crtr-pages' || element.tag === 'crtr-page')
345
- continue;
346
- let kind;
347
- let unvalidated = false;
348
- if (element.tag === 'crtr-slot') {
349
- kind = element.attrs.get('kind');
350
- if (kind === undefined || !productKinds.includes(kind) || BUILTIN_PAGE_KINDS.includes(kind))
351
- throw new PageAuthoringError(`<crtr-slot> at byte ${element.start} names unregistered product kind "${kind ?? ''}"`);
352
- unvalidated = true;
353
- }
354
- else if (element.tag.startsWith('crtr-')) {
355
- kind = element.tag.slice('crtr-'.length);
356
- if (!BUILTIN_PAGE_KINDS.includes(kind))
357
- throw new PageAuthoringError(`unregistered page element <${element.tag}> at byte ${element.start}; product kinds use <crtr-slot kind="…">`);
358
- }
359
- if (kind === undefined)
360
- continue;
361
- const id = element.attrs.get('id');
362
- const containingPage = ancestor(element, 'crtr-page');
363
- const step = containingPage === undefined ? 0 : pageIndexes.get(containingPage) ?? 0;
364
- rawSlots.push({ ...(id === undefined ? {} : { id }), kind, step, config: configForHtmlElement(element), ...(unvalidated ? { unvalidated: true } : {}) });
365
- slotElements.push(element);
366
- }
367
- 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);
369
- for (let index = 0; index < provisional.slots.length; index++) {
370
- const slot = provisional.slots[index];
371
- if (!isResponseBearingSlot(slot))
372
- continue;
373
- const element = slotElements[index];
374
- if (ancestor(element, 'crtr-page') === undefined) {
375
- 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
- }
377
- }
378
- return { ...metadata, steps, slots: provisional.slots, projection: provisional.slots.map((_, slotIndex) => ({ kind: 'slot', slotIndex })) };
379
- }
380
- function scanDocument(document, dialect, productKinds) {
381
- return dialect === 'md' ? scanMarkdown(document, productKinds) : scanHtml(document, productKinds);
155
+ function htmlMetadata(document) {
156
+ const title = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(document)?.[1]?.replace(/<[^>]+>/g, '').trim();
157
+ if (title === undefined || title === '')
158
+ throw new PageAuthoringError('HTML page requires a nonempty <title>.');
159
+ const description = [...document.matchAll(/<meta\b[^>]*>/gi)].map((match) => match[0]).map((tag) => {
160
+ const attrs = new Map([...tag.matchAll(/\b([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/g)].map((match) => [match[1].toLowerCase(), match[3]]));
161
+ return attrs.get('name')?.toLowerCase() === 'description' ? attrs.get('content')?.trim() : undefined;
162
+ }).find((value) => value !== undefined);
163
+ return { title, ...(description === undefined || description === '' ? {} : { subtitle: description }) };
382
164
  }
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);
165
+ export function deriveHtmlPageManifest(document, source, delivery = { placement: 'inline', inbox: false, reply: false }) {
166
+ const metadata = htmlMetadata(document);
167
+ return validateAuthoredManifest({ schema: 'crtr.page/v2', dialect: 'html', ...metadata, delivery, document: 'page.html', steps: 1, slots: [], ...(source === undefined ? {} : { source }) }, []);
387
168
  }
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);
169
+ /** Derive the transport manifest from one authored TSX page source file. */
170
+ export function derivePageManifest(sourceFile, productComponents = [], source, delivery) {
171
+ return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), productComponents, source, delivery);
391
172
  }
392
173
  /** Read and validate page.json from a ticket directory. */
393
- export function parsePage(dir, productKinds = []) {
174
+ export function parsePage(dir) {
394
175
  let raw;
395
176
  try {
396
177
  raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
@@ -398,30 +179,5 @@ export function parsePage(dir, productKinds = []) {
398
179
  catch {
399
180
  throw new PageAuthoringError('page.json is not valid JSON');
400
181
  }
401
- return validatePageManifest(raw, productKinds);
402
- }
403
- /** Key-order-independent value identity, so a re-scan compares by content. */
404
- function canonicalJson(value) {
405
- if (value === null || typeof value !== 'object')
406
- return JSON.stringify(value) ?? 'null';
407
- if (Array.isArray(value))
408
- return `[${value.map(canonicalJson).join(',')}]`;
409
- const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => (left < right ? -1 : 1));
410
- return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
411
- }
412
- /** 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);
416
- // Normalize the re-scan through the same schema pass the manifest went through,
417
- // 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);
419
- if (derived.slots.length !== canonical.slots.length)
420
- throw new PageAuthoringError(`page source has ${derived.slots.length} slots but manifest has ${canonical.slots.length}`);
421
- for (let index = 0; index < canonical.slots.length; index++) {
422
- const left = derived.slots[index];
423
- if (canonicalJson(left) !== canonicalJson(canonical.slots[index]))
424
- throw new PageAuthoringError(`page source slot #${index} (${left.id ?? left.kind}) does not match manifest slot #${index}`);
425
- }
426
- return scanned.projection;
182
+ return validatePageManifest(raw);
427
183
  }
@@ -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,10 @@
1
1
  export declare function ticketsRoot(): string;
2
- /** A ticket directory is the bridge node's canvas directory, the same path by construction. */
3
- export declare function ticketDir(nodeId: string): string;
2
+ /** The opaque id the `/v1/human/inbox` routes address a ticket by — a hash of
3
+ * the canonical tickets root and the ticket directory basename. The daemon
4
+ * derives the same value when it resolves a route id back to a directory. */
5
+ export declare function opaqueInboxTicketId(ticketId: string): string;
6
+ /** Ticket directories share the canvas node root. Reply-bearing tickets may use their bridge id; standalone pages use an independent ticket id. */
7
+ export declare function ticketDir(ticketId: string): string;
4
8
  export declare function inboxActivityPath(): string;
5
9
  /** Signal one durable ticket mutation to every open inbox without watching every ticket directory. */
6
10
  export declare function signalInboxActivity(): void;
@@ -1,12 +1,20 @@
1
- import { mkdirSync, watch, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, realpathSync, watch, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { basename, dirname, join } from 'node:path';
3
4
  import { crtrHome, nodeDir, nodesRoot } from '../canvas/paths.js';
4
5
  export function ticketsRoot() {
5
6
  return nodesRoot();
6
7
  }
7
- /** A ticket directory is the bridge node's canvas directory, the same path by construction. */
8
- export function ticketDir(nodeId) {
9
- return nodeDir(nodeId);
8
+ /** The opaque id the `/v1/human/inbox` routes address a ticket by — a hash of
9
+ * the canonical tickets root and the ticket directory basename. The daemon
10
+ * derives the same value when it resolves a route id back to a directory. */
11
+ export function opaqueInboxTicketId(ticketId) {
12
+ const canonicalRoot = realpathSync(ticketsRoot());
13
+ return createHash('sha256').update(`${canonicalRoot}\0${ticketId}`, 'utf8').digest('hex');
14
+ }
15
+ /** Ticket directories share the canvas node root. Reply-bearing tickets may use their bridge id; standalone pages use an independent ticket id. */
16
+ export function ticketDir(ticketId) {
17
+ return nodeDir(ticketId);
10
18
  }
11
19
  export function inboxActivityPath() {
12
20
  return join(crtrHome(), 'signals', 'inbox-activity');