@north-light/crouter 0.3.198 → 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 (314) 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__/human-cancel-guard.test.js +31 -10
  99. package/dist/core/__tests__/human-deliver.test.js +31 -26
  100. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  101. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  102. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  103. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  104. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  105. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  106. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  107. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  108. package/dist/core/__tests__/session-cycles.test.js +22 -0
  109. package/dist/core/__tests__/stop-guard.test.js +4 -4
  110. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  111. package/dist/core/bash-jobs.d.ts +6 -0
  112. package/dist/core/bash-jobs.js +10 -0
  113. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  114. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  115. package/dist/core/canvas/attention.js +8 -6
  116. package/dist/core/canvas/browse/model.d.ts +8 -2
  117. package/dist/core/canvas/browse/model.js +11 -4
  118. package/dist/core/canvas/canvas.d.ts +7 -0
  119. package/dist/core/canvas/canvas.js +37 -0
  120. package/dist/core/canvas/crons.d.ts +9 -0
  121. package/dist/core/canvas/crons.js +51 -0
  122. package/dist/core/canvas/node-order.d.ts +22 -1
  123. package/dist/core/canvas/node-order.js +26 -1
  124. package/dist/core/canvas/render-source.d.ts +5 -0
  125. package/dist/core/canvas/render-source.js +1 -0
  126. package/dist/core/canvas/types.d.ts +9 -3
  127. package/dist/core/command-plugins/bundle.d.ts +6 -1
  128. package/dist/core/command-plugins/bundle.js +26 -13
  129. package/dist/core/command.js +2 -1
  130. package/dist/core/config.d.ts +47 -1
  131. package/dist/core/config.js +137 -4
  132. package/dist/core/help.js +1 -1
  133. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  134. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  135. package/dist/core/human/__tests__/page.test.js +56 -98
  136. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  137. package/dist/core/human/answer-text.js +3 -3
  138. package/dist/core/human/answer.d.ts +6 -4
  139. package/dist/core/human/answer.js +4 -10
  140. package/dist/core/human/component-docs.d.ts +11 -7
  141. package/dist/core/human/component-docs.js +361 -88
  142. package/dist/core/human/convention.d.ts +17 -3
  143. package/dist/core/human/convention.js +36 -5
  144. package/dist/core/human/feedback-companion.d.ts +20 -0
  145. package/dist/core/human/feedback-companion.js +98 -0
  146. package/dist/core/human/feedback.d.ts +65 -0
  147. package/dist/core/human/feedback.js +116 -0
  148. package/dist/core/human/page-catalog.d.ts +40 -5
  149. package/dist/core/human/page-catalog.js +102 -30
  150. package/dist/core/human/page-errors.d.ts +4 -0
  151. package/dist/core/human/page-errors.js +7 -0
  152. package/dist/core/human/page-eval.d.ts +20 -0
  153. package/dist/core/human/page-eval.js +123 -0
  154. package/dist/core/human/page-schema.d.ts +23 -191
  155. package/dist/core/human/page-schema.js +60 -89
  156. package/dist/core/human/page-synth.d.ts +1 -1
  157. package/dist/core/human/page-synth.js +15 -18
  158. package/dist/core/human/page.d.ts +12 -27
  159. package/dist/core/human/page.js +142 -474
  160. package/dist/core/human/root.d.ts +6 -2
  161. package/dist/core/human/root.js +12 -4
  162. package/dist/core/human/scan.d.ts +3 -4
  163. package/dist/core/human/scan.js +16 -15
  164. package/dist/core/human/summary.d.ts +1 -2
  165. package/dist/core/human/summary.js +2 -2
  166. package/dist/core/human/tickets.d.ts +20 -14
  167. package/dist/core/human/tickets.js +91 -31
  168. package/dist/core/human/types.d.ts +10 -2
  169. package/dist/core/preview-registry.js +8 -10
  170. package/dist/core/profiles/default-binding.d.ts +4 -0
  171. package/dist/core/profiles/default-binding.js +24 -0
  172. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  173. package/dist/core/profiles/deletion-reservation.js +38 -0
  174. package/dist/core/profiles/manifest.d.ts +3 -0
  175. package/dist/core/profiles/manifest.js +12 -0
  176. package/dist/core/profiles/select.js +2 -2
  177. package/dist/core/review/realize.js +3 -3
  178. package/dist/core/review/store.d.ts +6 -0
  179. package/dist/core/review/store.js +31 -0
  180. package/dist/core/runtime/bearings.d.ts +0 -4
  181. package/dist/core/runtime/bearings.js +4 -62
  182. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  183. package/dist/core/runtime/bin-contributions.js +186 -0
  184. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  185. package/dist/core/runtime/broker/client-registry.js +13 -1
  186. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  187. package/dist/core/runtime/broker/extension-abort.js +4 -2
  188. package/dist/core/runtime/broker/fault-retry.js +4 -0
  189. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  190. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  191. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  192. package/dist/core/runtime/broker/read-ops.js +2 -2
  193. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  194. package/dist/core/runtime/broker/rebind.js +6 -1
  195. package/dist/core/runtime/broker-extension-render.js +19 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  197. package/dist/core/runtime/broker.d.ts +1 -1
  198. package/dist/core/runtime/broker.js +17 -6
  199. package/dist/core/runtime/front-door-env.d.ts +5 -0
  200. package/dist/core/runtime/front-door-env.js +16 -0
  201. package/dist/core/runtime/front-door.d.ts +1 -5
  202. package/dist/core/runtime/front-door.js +2 -5
  203. package/dist/core/runtime/host.d.ts +1 -1
  204. package/dist/core/runtime/invocation.d.ts +6 -0
  205. package/dist/core/runtime/invocation.js +10 -0
  206. package/dist/core/runtime/kickoff.js +34 -0
  207. package/dist/core/runtime/launch.d.ts +2 -6
  208. package/dist/core/runtime/node-read.js +4 -1
  209. package/dist/core/runtime/nodes.js +2 -0
  210. package/dist/core/runtime/promote.d.ts +1 -0
  211. package/dist/core/runtime/promote.js +3 -1
  212. package/dist/core/runtime/session-cycles.d.ts +6 -0
  213. package/dist/core/runtime/session-cycles.js +15 -1
  214. package/dist/core/runtime/session-visibility.d.ts +17 -8
  215. package/dist/core/runtime/session-visibility.js +19 -10
  216. package/dist/core/runtime/spawn-env.d.ts +9 -1
  217. package/dist/core/runtime/spawn-env.js +62 -2
  218. package/dist/core/runtime/stop-guard.d.ts +2 -2
  219. package/dist/core/runtime/stop-guard.js +2 -2
  220. package/dist/core/runtime/stop-signals.d.ts +1 -1
  221. package/dist/core/runtime/stop-signals.js +2 -2
  222. package/dist/core/runtime/tmux-bindings.js +1 -1
  223. package/dist/core/session-model/session-state.d.ts +4 -3
  224. package/dist/core/session-model/session-state.js +14 -1
  225. package/dist/core/user-settings.d.ts +21 -2
  226. package/dist/core/user-settings.js +32 -11
  227. package/dist/core/worktree.d.ts +15 -45
  228. package/dist/core/worktree.js +59 -261
  229. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  230. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  231. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  232. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  233. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  234. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  235. package/dist/daemon/api/handlers/human.js +24 -20
  236. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  237. package/dist/daemon/api/handlers/inbox.js +39 -115
  238. package/dist/daemon/api/handlers/profiles.js +11 -29
  239. package/dist/daemon/api/map.d.ts +6 -1
  240. package/dist/daemon/api/map.js +23 -0
  241. package/dist/daemon/api/server.js +3 -1
  242. package/dist/daemon/companion-retire.d.ts +2 -0
  243. package/dist/daemon/companion-retire.js +33 -0
  244. package/dist/daemon/cron-run.js +14 -16
  245. package/dist/daemon/human/finish.d.ts +12 -7
  246. package/dist/daemon/human/finish.js +101 -38
  247. package/dist/daemon/human/sweep.js +5 -1
  248. package/dist/daemon/profile-delete.d.ts +7 -0
  249. package/dist/daemon/profile-delete.js +306 -0
  250. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  251. package/dist/daemon/review/deliver.js +3 -3
  252. package/dist/daemon/review/finish.d.ts +0 -2
  253. package/dist/daemon/review/finish.js +1 -29
  254. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  255. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  256. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  257. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  258. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  259. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  260. package/dist/pi-extensions/canvas-stophook.js +4 -4
  261. package/dist/shared/generated-context.js +2 -2
  262. package/dist/types.d.ts +44 -2
  263. package/dist/types.js +7 -2
  264. package/package.json +4 -4
  265. package/runtime.lock.json +5 -33
  266. package/dist/builtin-memory/init.md +0 -38
  267. package/dist/builtin-memory/plan.md +0 -19
  268. package/dist/builtin-memory/spec.md +0 -19
  269. package/dist/commands/human/doc.d.ts +0 -2
  270. package/dist/commands/human/doc.js +0 -91
  271. package/dist/commands/human/html.js +0 -81
  272. package/dist/commands/human/inbox.d.ts +0 -2
  273. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  274. package/dist/core/human/__tests__/page-render.test.js +0 -66
  275. package/dist/core/human/html-markdown.d.ts +0 -3
  276. package/dist/core/human/html-markdown.js +0 -288
  277. package/dist/core/human/page-render.d.ts +0 -4
  278. package/dist/core/human/page-render.js +0 -105
  279. package/dist/pages/bundle.css +0 -1
  280. package/dist/pages/bundle.js +0 -1778
  281. package/dist/pages/comments.d.ts +0 -72
  282. package/dist/pages/comments.js +0 -176
  283. package/dist/pages/controls.d.ts +0 -98
  284. package/dist/pages/controls.js +0 -261
  285. package/dist/pages/elements/cards.d.ts +0 -28
  286. package/dist/pages/elements/cards.js +0 -915
  287. package/dist/pages/elements/chart.d.ts +0 -23
  288. package/dist/pages/elements/chart.js +0 -664
  289. package/dist/pages/elements/options.d.ts +0 -30
  290. package/dist/pages/elements/options.js +0 -742
  291. package/dist/pages/elements/pages.d.ts +0 -30
  292. package/dist/pages/elements/pages.js +0 -377
  293. package/dist/pages/elements/slot.d.ts +0 -26
  294. package/dist/pages/elements/slot.js +0 -112
  295. package/dist/pages/elements/table.d.ts +0 -36
  296. package/dist/pages/elements/table.js +0 -1043
  297. package/dist/pages/elements/text.d.ts +0 -31
  298. package/dist/pages/elements/text.js +0 -1175
  299. package/dist/pages/entry.d.ts +0 -8
  300. package/dist/pages/entry.js +0 -8
  301. package/dist/pages/host.d.ts +0 -134
  302. package/dist/pages/host.js +0 -165
  303. package/dist/pages/readonly.d.ts +0 -24
  304. package/dist/pages/readonly.js +0 -31
  305. package/dist/pages/register.d.ts +0 -2
  306. package/dist/pages/register.js +0 -3
  307. package/dist/pages/responses.d.ts +0 -37
  308. package/dist/pages/responses.js +0 -56
  309. package/dist/pages/slot-config.d.ts +0 -36
  310. package/dist/pages/slot-config.js +0 -50
  311. package/dist/pages/types.d.ts +0 -98
  312. package/dist/pages/types.js +0 -23
  313. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  314. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,14 +1,12 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { BUILTIN_PAGE_KINDS, isRegisteredPageKind } from './page-catalog.js';
4
- import { cardsSchema, chartDataRowsSchema, isResponseBearingSlot, issueText, tableRowsSchema, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
5
- /** A page failed authoring or validation; the caller can report it as bad page input. */
6
- export class PageAuthoringError extends Error {
7
- constructor(message) {
8
- super(message);
9
- this.name = 'PageAuthoringError';
10
- }
11
- }
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 };
12
10
  function asPageAuthoringError(error) {
13
11
  if (error instanceof PageAuthoringError)
14
12
  return error;
@@ -31,443 +29,149 @@ function validateAuthoredManifest(parsed, productComponents) {
31
29
  throw asPageAuthoringError(error);
32
30
  }
33
31
  }
34
- function parseJsonObject(raw, thing) {
35
- 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) {
36
36
  try {
37
- 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;
38
46
  }
39
47
  catch (error) {
40
- throw new PageAuthoringError(`${thing} must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`);
41
- }
42
- if (value === null || Array.isArray(value) || typeof value !== 'object')
43
- throw new PageAuthoringError(`${thing} JSON must be one object`);
44
- return value;
45
- }
46
- function sourceLines(source, start) {
47
- const lines = [];
48
- let cursor = start;
49
- while (cursor < source.length) {
50
- const newline = source.indexOf('\n', cursor);
51
- const end = newline === -1 ? source.length : newline + 1;
52
- const contentEnd = newline === -1 ? source.length : newline;
53
- lines.push({ start: cursor, end, contentEnd, text: source.slice(cursor, contentEnd).replace(/\r$/, '') });
54
- cursor = end;
55
- }
56
- return lines;
57
- }
58
- /** CommonMark closing fence: at least as many of the opener's fence character, nothing else on the line. */
59
- function closesFence(text, opener) {
60
- const trimmed = text.trimEnd();
61
- return trimmed.length >= opener.length && trimmed.split('').every((character) => character === opener.char);
62
- }
63
- function addProse(projection, raw) {
64
- const markdown = raw.trim();
65
- if (markdown !== '')
66
- projection.push({ kind: 'prose', markdown });
67
- }
68
- /**
69
- * Project a pre-cutover Markdown ticket for terminal replay only. This is not
70
- * an authoring contract: no new page path accepts Markdown, and its old front
71
- * matter is merely skipped to recover the stored source body.
72
- */
73
- function projectLegacyMarkdownSource(document, productComponents) {
74
- if (!(document.startsWith('---\n') || document.startsWith('---\r\n'))) {
75
- throw new PageAuthoringError('stored md page is missing its opening front matter delimiter');
76
- }
77
- const firstEnd = document.indexOf('\n') + 1;
78
- let bodyStart = -1;
79
- let cursor = firstEnd;
80
- while (cursor <= document.length) {
81
- const newline = document.indexOf('\n', cursor);
82
- const lineEnd = newline === -1 ? document.length : newline;
83
- if (document.slice(cursor, lineEnd).replace(/\r$/, '') === '---') {
84
- bodyStart = newline === -1 ? document.length : newline + 1;
85
- break;
86
- }
87
- if (newline === -1)
88
- break;
89
- cursor = newline + 1;
90
- }
91
- if (bodyStart === -1)
92
- throw new PageAuthoringError('stored md page is missing its closing front matter delimiter');
93
- const lines = sourceLines(document, bodyStart);
94
- const slots = [];
95
- const projection = [];
96
- let proseStart = bodyStart;
97
- let ordinaryFence = null;
98
- for (let index = 0; index < lines.length; index++) {
99
- const line = lines[index];
100
- if (ordinaryFence !== null) {
101
- if (closesFence(line.text, ordinaryFence))
102
- ordinaryFence = null;
103
- continue;
104
- }
105
- const opening = line.text.match(/^(`{3,}|~{3,})(.*)$/);
106
- if (opening === null)
107
- continue;
108
- const run = opening[1];
109
- const fence = { char: run[0], length: run.length };
110
- const info = opening[2];
111
- if (fence.char !== '`' || !info.startsWith('slot:')) {
112
- ordinaryFence = fence;
113
- continue;
114
- }
115
- const kind = info.slice('slot:'.length);
116
- if (kind === '' || info !== `slot:${kind}` || !isRegisteredPageKind(kind, productComponents)) {
117
- throw new PageAuthoringError(`unregistered or invalid page slot kind "${kind}" in stored md fence`);
118
- }
119
- let closeIndex = -1;
120
- for (let candidate = index + 1; candidate < lines.length; candidate++) {
121
- if (closesFence(lines[candidate].text, fence)) {
122
- closeIndex = candidate;
123
- break;
124
- }
125
- }
126
- if (closeIndex === -1)
127
- throw new PageAuthoringError(`slot:${kind} fence has no matching ${run.length}-backtick closing fence`);
128
- addProse(projection, document.slice(proseStart, line.start));
129
- const rawObject = parseJsonObject(document.slice(line.end, lines[closeIndex].start), `stored slot:${kind} fence`);
130
- const { id, ...config } = rawObject;
131
- if (id !== undefined && typeof id !== 'string')
132
- throw new PageAuthoringError(`stored slot:${kind} fence has non-string reserved id`);
133
- slots.push({ ...(id === undefined ? {} : { id }), kind, step: 0, config, ...(BUILTIN_PAGE_KINDS.includes(kind) ? {} : { unvalidated: true }) });
134
- projection.push({ kind: 'slot', slotIndex: slots.length - 1 });
135
- proseStart = lines[closeIndex].end;
136
- index = closeIndex;
137
- }
138
- addProse(projection, document.slice(proseStart));
139
- return { steps: 1, slots, projection };
140
- }
141
- const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
142
- function tagEnd(html, start) {
143
- let quote = null;
144
- for (let index = start + 1; index < html.length; index++) {
145
- const character = html[index];
146
- if (quote !== null) {
147
- if (character === quote)
148
- quote = null;
149
- continue;
150
- }
151
- if (character === '"' || character === "'") {
152
- quote = character;
153
- continue;
154
- }
155
- if (character === '>')
156
- return index + 1;
157
- }
158
- throw new PageAuthoringError(`unterminated HTML tag beginning at byte ${start}`);
159
- }
160
- function parseAttributes(source) {
161
- const attrs = new Map();
162
- const tagNameEnd = source.search(/\s/);
163
- const body = tagNameEnd === -1 ? '' : source.slice(tagNameEnd);
164
- const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
165
- for (const match of body.matchAll(pattern))
166
- attrs.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '');
167
- return attrs;
168
- }
169
- function parseHtmlElements(html) {
170
- const roots = [];
171
- const all = [];
172
- const stack = [];
173
- let cursor = 0;
174
- while (cursor < html.length) {
175
- const start = html.indexOf('<', cursor);
176
- if (start === -1)
177
- break;
178
- if (html.startsWith('<!--', start)) {
179
- const end = html.indexOf('-->', start + 4);
180
- if (end === -1)
181
- throw new PageAuthoringError(`unterminated HTML comment at byte ${start}`);
182
- cursor = end + 3;
183
- continue;
184
- }
185
- const end = tagEnd(html, start);
186
- const token = html.slice(start, end);
187
- if (/^<!/i.test(token) || /^<\?/i.test(token)) {
188
- cursor = end;
189
- continue;
190
- }
191
- const closing = token.match(/^<\s*\/\s*([^\s>]+)/);
192
- if (closing !== null) {
193
- const tag = closing[1].toLowerCase();
194
- let match = stack.length - 1;
195
- while (match >= 0 && stack[match].tag !== tag)
196
- match--;
197
- if (match >= 0) {
198
- const element = stack[match];
199
- element.closeStart = start;
200
- element.end = end;
201
- stack.length = match;
202
- }
203
- cursor = end;
204
- continue;
205
- }
206
- const opening = token.match(/^<\s*([^\s/>]+)/);
207
- if (opening === null) {
208
- cursor = end;
209
- continue;
210
- }
211
- const tag = opening[1].toLowerCase();
212
- const parent = stack.at(-1);
213
- const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, openEnd: end, end, ...(parent === undefined ? {} : { parent }), children: [] };
214
- if (parent === undefined)
215
- roots.push(element);
216
- else
217
- parent.children.push(element);
218
- all.push(element);
219
- const selfClosing = /\/\s*>$/.test(token) || VOID_TAGS.has(tag);
220
- if (tag === 'script' && !selfClosing) {
221
- const closePattern = /<\s*\/\s*script\s*>/gi;
222
- closePattern.lastIndex = end;
223
- const close = closePattern.exec(html);
224
- if (close === null)
225
- throw new PageAuthoringError(`unterminated <script> element at byte ${start}`);
226
- element.text = html.slice(end, close.index);
227
- element.closeStart = close.index;
228
- element.end = closePattern.lastIndex;
229
- cursor = closePattern.lastIndex;
230
- continue;
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()';
231
79
  }
232
- if (!selfClosing)
233
- stack.push(element);
234
- cursor = end;
235
- }
236
- return all;
237
- }
238
- function ancestor(element, tag) {
239
- let current = element.parent;
240
- while (current !== undefined) {
241
- if (current.tag === tag)
242
- return current;
243
- current = current.parent;
244
- }
245
- return undefined;
246
- }
247
- function decodeHtmlAttribute(value) {
248
- return value.replace(/&(?:amp|quot|apos|lt|gt);|&#(?:x[0-9a-f]+|\d+);/gi, (entity) => {
249
- const named = { '&amp;': '&', '&quot;': '"', '&apos;': "'", '&lt;': '<', '&gt;': '>' };
250
- const lower = entity.toLowerCase();
251
- if (named[lower] !== undefined)
252
- return named[lower];
253
- const numeric = lower.startsWith('&#x') ? Number.parseInt(lower.slice(3, -1), 16) : Number.parseInt(lower.slice(2, -1), 10);
254
- return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : entity;
255
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;
256
91
  }
257
- function validateHtmlStructure(html) {
258
- const counts = [
259
- ['doctype', /<!doctype\b[^>]*>/gi, 'doctype'],
260
- ['<head>', /<head(?=\s|>)/gi, '<head> opening tag'],
261
- ['<body>', /<body(?=\s|>)/gi, '<body> opening tag'],
262
- ];
263
- for (const [, pattern, label] of counts) {
264
- const count = html.match(pattern)?.length ?? 0;
265
- if (count !== 1)
266
- throw new PageAuthoringError(`Expected exactly one ${label}, found ${count}.`);
267
- }
268
- }
269
- function readHtmlMetadata(elements) {
270
- const recognized = new Map();
271
- for (const element of elements.filter((candidate) => candidate.tag === 'meta')) {
272
- const name = element.attrs.get('name');
273
- if (name === undefined || !name.startsWith('crtr-page-'))
274
- continue;
275
- if (!['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement'].includes(name))
276
- throw new PageAuthoringError(`unknown page metadata element <meta name="${name}">`);
277
- if (ancestor(element, 'head') === undefined)
278
- throw new PageAuthoringError(`<meta name="${name}"> must appear inside <head>`);
279
- const entries = recognized.get(name) ?? [];
280
- entries.push(element);
281
- recognized.set(name, entries);
282
- }
283
- for (const name of ['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement']) {
284
- const count = recognized.get(name)?.length ?? 0;
285
- if ((name === 'crtr-page-title' && count !== 1) || (name !== 'crtr-page-title' && count > 1)) {
286
- const expected = name === 'crtr-page-title' ? 'exactly one' : 'at most one';
287
- throw new PageAuthoringError(`Expected ${expected} <meta name="${name}">, found ${count}.`);
288
- }
289
- }
290
- const titleElement = recognized.get('crtr-page-title')[0];
291
- const title = decodeHtmlAttribute(titleElement.attrs.get('content') ?? '').trim();
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();
292
111
  if (title === '')
293
- throw new PageAuthoringError(`<meta name="crtr-page-title"> requires nonempty content`);
294
- const subtitleElement = recognized.get('crtr-page-subtitle')?.[0];
295
- const subtitle = subtitleElement === undefined ? undefined : decodeHtmlAttribute(subtitleElement.attrs.get('content') ?? '').trim();
296
- const placementElement = recognized.get('crtr-page-placement')?.[0];
297
- const placementValue = placementElement === undefined ? 'panel' : decodeHtmlAttribute(placementElement.attrs.get('content') ?? '').trim();
298
- if (placementValue !== 'inline' && placementValue !== 'panel')
299
- throw new PageAuthoringError(`<meta name="crtr-page-placement"> content must be exactly inline or panel; received "${placementValue}"`);
300
- return { title, ...(subtitle === undefined ? {} : { subtitle }), placement: placementValue };
301
- }
302
- function configForHtmlElement(element) {
303
- const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
304
- if (script === undefined)
305
- throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} requires a direct <script type="application/json"> child`);
306
- return parseJsonObject(script.text ?? '', `<${element.tag}> at byte ${element.start}`);
307
- }
308
- /** Project only body prose: slots remain terminal panels, while the pager's
309
- * structural tags are invisible in the terminal. The raw fragments flow to
310
- * the renderer, which downconverts HTML according to the manifest dialect. */
311
- function htmlProjection(document, elements, slotElements) {
312
- const body = elements.find((element) => element.tag === 'body');
313
- if (body === undefined)
314
- throw new PageAuthoringError('page document has no <body> element');
315
- const bodyEnd = body.closeStart ?? document.length;
316
- const excluded = slotElements.map((element) => ({ start: element.start, end: element.end }));
317
- for (const element of elements) {
318
- if (element.tag !== 'crtr-pages' && element.tag !== 'crtr-page')
319
- continue;
320
- excluded.push({ start: element.start, end: element.openEnd });
321
- if (element.closeStart !== undefined)
322
- excluded.push({ start: element.closeStart, end: element.end });
323
- }
324
- const ranges = excluded
325
- .map((range) => ({ start: Math.max(range.start, body.openEnd), end: Math.min(range.end, bodyEnd) }))
326
- .filter((range) => range.start < range.end)
327
- .sort((left, right) => left.start - right.start || left.end - right.end);
328
- const projection = [];
329
- let cursor = body.openEnd;
330
- for (const range of ranges) {
331
- if (range.end <= cursor)
332
- continue;
333
- addProse(projection, document.slice(cursor, range.start));
334
- const element = slotElements.find((slot) => slot.start === range.start && slot.end === range.end);
335
- if (element !== undefined)
336
- projection.push({ kind: 'slot', slotIndex: slotElements.indexOf(element) });
337
- cursor = range.end;
338
- }
339
- addProse(projection, document.slice(cursor, bodyEnd));
340
- return projection;
341
- }
342
- function scanHtml(document, productComponents) {
343
- validateHtmlStructure(document);
344
- const elements = parseHtmlElements(document);
345
- const metadata = readHtmlMetadata(elements);
346
- const wrappers = elements.filter((element) => element.tag === 'crtr-pages');
347
- if (wrappers.some((candidate) => ancestor(candidate, 'crtr-pages') !== undefined))
348
- throw new PageAuthoringError('nested <crtr-pages> element is not allowed');
349
- if (wrappers.length > 1)
350
- throw new PageAuthoringError(`more than one <crtr-pages> element: found ${wrappers.length}`);
351
- const wrapper = wrappers[0];
352
- const pages = elements.filter((element) => element.tag === 'crtr-page');
353
- for (const page of pages)
354
- if (ancestor(page, 'crtr-pages') !== wrapper || wrapper === undefined)
355
- throw new PageAuthoringError(`<crtr-page> outside <crtr-pages> at byte ${page.start}`);
356
- if (wrapper !== undefined && pages.length === 0)
357
- throw new PageAuthoringError('empty <crtr-pages> element must contain at least one <crtr-page>');
358
- const pageIndexes = new Map(pages.map((page, index) => [page, index]));
359
- const rawSlots = [];
360
- const slotElements = [];
361
- for (const element of elements) {
362
- if (element.tag === 'crtr-pages' || element.tag === 'crtr-page')
363
- continue;
364
- let kind;
365
- let unvalidated = false;
366
- if (element.tag === 'crtr-slot') {
367
- kind = element.attrs.get('kind');
368
- if (kind === undefined || !isRegisteredPageKind(kind, productComponents) || BUILTIN_PAGE_KINDS.includes(kind))
369
- throw new PageAuthoringError(`<crtr-slot> at byte ${element.start} names unregistered product kind "${kind ?? ''}"`);
370
- unvalidated = true;
371
- }
372
- else if (element.tag.startsWith('crtr-')) {
373
- kind = element.tag.slice('crtr-'.length);
374
- if (!BUILTIN_PAGE_KINDS.includes(kind))
375
- throw new PageAuthoringError(`unregistered page element <${element.tag}> at byte ${element.start}; product kinds use <crtr-slot kind="…">`);
376
- }
377
- if (kind === undefined)
378
- continue;
379
- if (element.attrs.has('slot-id'))
380
- throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} has a slot-id attribute; author the slot id as id="…" — slot-id is written by the renderer`);
381
- const id = element.attrs.get('id');
382
- const containingPage = ancestor(element, 'crtr-page');
383
- const step = containingPage === undefined ? 0 : pageIndexes.get(containingPage) ?? 0;
384
- rawSlots.push({ ...(id === undefined ? {} : { id }), kind, step, config: configForHtmlElement(element), ...(unvalidated ? { unvalidated: true } : {}) });
385
- slotElements.push(element);
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);
386
124
  }
387
- const steps = wrapper === undefined ? 1 : pages.length;
388
- const provisional = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: 'html', ...metadata, document: 'page.html', steps, slots: rawSlots }, productComponents);
389
- for (let index = 0; index < provisional.slots.length; index++) {
390
- const slot = provisional.slots[index];
391
- if (!isResponseBearingSlot(slot))
392
- continue;
393
- const element = slotElements[index];
394
- if (ancestor(element, 'crtr-page') === undefined) {
395
- throw new PageAuthoringError(`response-bearing element <${element.tag}> for slot "${slot.id}" is outside any <crtr-page>.\nWrap interactive elements with:\n<crtr-pages><crtr-page>…</crtr-page></crtr-pages>`);
396
- }
397
- }
398
- return { ...metadata, steps, slots: provisional.slots, projection: htmlProjection(document, elements, slotElements) };
399
- }
400
- /** Derive the transport manifest from one authored HTML page document. */
401
- export function derivePageManifestFromDocument(document, dialect, productComponents = [], source) {
402
- const scanned = scanHtml(document, productComponents);
403
- return validateAuthoredManifest({ schema: 'crtr.page/v1', dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: 'page.html', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productComponents);
404
- }
405
- /** Derive the transport manifest from one authored HTML page source file. */
406
- export function derivePageManifest(sourceFile, dialect, productComponents = [], source) {
407
- return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), dialect, productComponents, source);
408
- }
409
- const SOURCE_EMBED = {
410
- table: { field: 'rows', schema: tableRowsSchema },
411
- cards: { field: 'cards', schema: cardsSchema },
412
- chart: { field: 'data', schema: chartDataRowsSchema },
413
- };
414
- function readSourceFile(path, context) {
415
- if (!existsSync(path))
416
- throw new PageAuthoringError(`${context} source file does not exist: ${path}`);
417
- let raw;
418
- try {
419
- raw = readFileSync(path, 'utf8');
420
- }
421
- catch (error) {
422
- throw new PageAuthoringError(`${context} source file is not readable: ${path} (${error instanceof Error ? error.message : String(error)})`);
423
- }
424
- try {
425
- return JSON.parse(raw);
426
- }
427
- catch (error) {
428
- throw new PageAuthoringError(`${context} source file is not valid JSON: ${path} (${error instanceof Error ? error.message : String(error)})`);
429
- }
430
- }
431
- /** Read source files into their built-in slots, returning a self-contained stored manifest. */
432
- export function snapshotPageSources(manifest, productComponents = []) {
433
- const slots = manifest.slots.map((slot, index) => {
434
- const embed = slot.unvalidated === true ? undefined : SOURCE_EMBED[slot.kind];
435
- const config = slot.config;
436
- if (embed === undefined || typeof config.source !== 'string')
437
- return slot;
438
- const context = `slot ${slot.id === undefined ? `#${index}` : `"${slot.id}"`} (${slot.kind})`;
439
- const raw = readSourceFile(config.source, context);
440
- const parsed = embed.schema.safeParse(raw);
441
- if (!parsed.success)
442
- throw new PageAuthoringError(`${context} source data is invalid: ${issueText(parsed.error)}`);
443
- const { source: _drop, ...rest } = config;
444
- return { ...slot, config: { ...rest, [embed.field]: parsed.data } };
445
- });
446
- return validatePageManifest({ ...manifest, slots }, productComponents);
447
- }
448
- /** Replace every authored slot config with its stored, self-contained config for runtime rendering. */
449
- export function embedPageSlotConfigs(document, manifest) {
450
- const replacements = [];
451
- let slotIndex = 0;
452
- for (const element of parseHtmlElements(document)) {
453
- const isSlot = element.tag === 'crtr-slot' || (element.tag.startsWith('crtr-') && element.tag !== 'crtr-pages' && element.tag !== 'crtr-page');
454
- if (!isSlot)
455
- continue;
456
- const slot = manifest.slots[slotIndex];
457
- if (slot === undefined)
458
- throw new PageAuthoringError(`page document has more slots than its manifest at slot #${slotIndex}`);
459
- const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
460
- if (script?.closeStart === undefined)
461
- throw new PageAuthoringError(`slot #${slotIndex} has no JSON config script`);
462
- replacements.push({ start: script.openEnd, end: script.closeStart, text: JSON.stringify(slot.config).replace(/</g, '\\u003c') });
463
- slotIndex += 1;
464
- }
465
- if (slotIndex !== manifest.slots.length)
466
- throw new PageAuthoringError(`page document has ${slotIndex} slots but manifest has ${manifest.slots.length}`);
467
- return replacements.reduceRight((result, replacement) => `${result.slice(0, replacement.start)}${replacement.text}${result.slice(replacement.end)}`, document);
125
+ const slots = [];
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
+ });
143
+ }
144
+ collectSlots(node.children, inStep);
145
+ }
146
+ };
147
+ collectSlots(page.children, 0);
148
+ return { title, ...(subtitle === undefined || subtitle === '' ? {} : { subtitle }), steps: Math.max(stepCount, 1), slots };
149
+ }
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);
154
+ }
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 }) };
164
+ }
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 }) }, []);
168
+ }
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);
468
172
  }
469
173
  /** Read and validate page.json from a ticket directory. */
470
- export function parsePage(dir, productComponents = []) {
174
+ export function parsePage(dir) {
471
175
  let raw;
472
176
  try {
473
177
  raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
@@ -475,41 +179,5 @@ export function parsePage(dir, productComponents = []) {
475
179
  catch {
476
180
  throw new PageAuthoringError('page.json is not valid JSON');
477
181
  }
478
- return validatePageManifest(raw, productComponents);
479
- }
480
- /** Key-order-independent value identity, so a re-scan compares by content. */
481
- function canonicalJson(value) {
482
- if (value === null || typeof value !== 'object')
483
- return JSON.stringify(value) ?? 'null';
484
- if (Array.isArray(value))
485
- return `[${value.map(canonicalJson).join(',')}]`;
486
- const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => (left < right ? -1 : 1));
487
- return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
488
- }
489
- function comparableConfig(kind, config, sourceProjection) {
490
- const embed = SOURCE_EMBED[kind];
491
- if (embed === undefined || !sourceProjection)
492
- return config;
493
- const { source: _source, [embed.field]: _data, ...rest } = config;
494
- return rest;
495
- }
496
- /** Project authored source for local rendering and verify its slot references against the manifest. */
497
- export function parsePageSource(document, manifest, productComponents = []) {
498
- const canonical = validatePageManifest(manifest, productComponents);
499
- const scanned = canonical.dialect === 'md'
500
- ? projectLegacyMarkdownSource(document, productComponents)
501
- : scanHtml(document, productComponents);
502
- // Normalize the re-scan through the same schema pass the manifest went through,
503
- // so per-kind config defaults are on both sides of the comparison.
504
- const derived = validateAuthoredPageManifest({ schema: 'crtr.page/v1', dialect: canonical.dialect, title: canonical.title, ...(canonical.subtitle === undefined ? {} : { subtitle: canonical.subtitle }), placement: canonical.placement, document: canonical.document, steps: scanned.steps, slots: scanned.slots }, productComponents);
505
- if (derived.slots.length !== canonical.slots.length)
506
- throw new PageAuthoringError(`page source has ${derived.slots.length} slots but manifest has ${canonical.slots.length}`);
507
- for (let index = 0; index < canonical.slots.length; index++) {
508
- const left = derived.slots[index];
509
- const right = canonical.slots[index];
510
- const sourceProjection = typeof left.config.source === 'string';
511
- if (canonicalJson({ ...left, config: comparableConfig(left.kind, left.config, sourceProjection) }) !== canonicalJson({ ...right, config: comparableConfig(right.kind, right.config, sourceProjection) }))
512
- throw new PageAuthoringError(`page source slot #${index} (${left.id ?? left.kind}) does not match manifest slot #${index}`);
513
- }
514
- return scanned.projection;
182
+ return validatePageManifest(raw);
515
183
  }