@north-light/crouter 0.3.198 → 0.3.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/dist/api/client.d.ts +18 -18
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +81 -34
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +6 -6
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  33. package/dist/clients/attach/render/chat-view.d.ts +26 -1
  34. package/dist/clients/attach/render/chat-view.js +61 -11
  35. package/dist/clients/attach/render/page-block.d.ts +34 -0
  36. package/dist/clients/attach/render/page-block.js +226 -0
  37. package/dist/clients/attach/session/whip.d.ts +2 -0
  38. package/dist/clients/attach/session/whip.js +2 -0
  39. package/dist/clients/attach/viewer.js +1419 -993
  40. package/dist/clients/conversation/projection.js +18 -3
  41. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  42. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
  43. package/dist/clients/inbox/controller.js +10 -16
  44. package/dist/clients/inbox/page-adapter.d.ts +1 -3
  45. package/dist/clients/inbox/page-adapter.js +3 -4
  46. package/dist/clients/inbox/resolve.d.ts +1 -1
  47. package/dist/clients/inbox/resolve.js +3 -3
  48. package/dist/clients/inbox/review/document-surface.js +3 -1
  49. package/dist/clients/inbox/review/state.d.ts +2 -1
  50. package/dist/clients/inbox/review/state.js +5 -16
  51. package/dist/clients/inbox/tui/input.js +37 -19
  52. package/dist/clients/inbox/tui/panel.js +5 -31
  53. package/dist/clients/inbox/tui/render.d.ts +11 -0
  54. package/dist/clients/inbox/tui/render.js +71 -27
  55. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  56. package/dist/clients/inbox/tui/slots.js +16 -12
  57. package/dist/clients/inbox/tui/types.d.ts +2 -7
  58. package/dist/clients/inbox/tui.js +2 -2
  59. package/dist/commands/__tests__/human.test.js +71 -114
  60. package/dist/commands/__tests__/node-message.test.js +14 -0
  61. package/dist/commands/attention.js +4 -4
  62. package/dist/commands/canvas-snapshot.js +1 -1
  63. package/dist/commands/canvas.js +1 -1
  64. package/dist/commands/dashboard.js +1 -1
  65. package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
  66. package/dist/commands/human/components.js +77 -0
  67. package/dist/commands/human/feedback.d.ts +2 -0
  68. package/dist/commands/human/feedback.js +108 -0
  69. package/dist/commands/human/prompts.d.ts +13 -5
  70. package/dist/commands/human/prompts.js +160 -97
  71. package/dist/commands/human/queue.js +57 -88
  72. package/dist/commands/human/shared.d.ts +2 -12
  73. package/dist/commands/human/shared.js +17 -11
  74. package/dist/commands/human.js +29 -21
  75. package/dist/commands/memory/origin.js +2 -2
  76. package/dist/commands/node/bash.js +4 -4
  77. package/dist/commands/node/inspect.js +3 -3
  78. package/dist/commands/node/lifecycle.js +1 -1
  79. package/dist/commands/node/message.js +2 -2
  80. package/dist/commands/node-worktree.js +8 -8
  81. package/dist/commands/pkg/browse.js +1 -1
  82. package/dist/commands/pkg/plugin-manage.js +129 -10
  83. package/dist/commands/profile/delete.js +41 -15
  84. package/dist/commands/profile.js +1 -1
  85. package/dist/commands/push.js +1 -1
  86. package/dist/commands/surface/node/focus.js +29 -14
  87. package/dist/commands/surface-edit.js +3 -3
  88. package/dist/commands/surface-inbox.d.ts +2 -0
  89. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  90. package/dist/commands/surface.js +2 -1
  91. package/dist/commands/sys/doctor.js +60 -3
  92. package/dist/commands/sys/feedback.js +79 -25
  93. package/dist/commands/sys/settings.js +1 -1
  94. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  95. package/dist/core/__tests__/extension-abort.test.js +5 -2
  96. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  97. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  98. package/dist/core/__tests__/helpers/harness.js +17 -55
  99. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  100. package/dist/core/__tests__/human-deliver.test.js +31 -26
  101. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  102. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  103. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +4 -5
  104. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  105. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  106. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  107. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  108. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  109. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  110. package/dist/core/__tests__/session-cycles.test.js +22 -0
  111. package/dist/core/__tests__/stop-guard.test.js +4 -4
  112. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  113. package/dist/core/bash-jobs.d.ts +6 -0
  114. package/dist/core/bash-jobs.js +10 -0
  115. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  116. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  117. package/dist/core/canvas/attention.js +8 -6
  118. package/dist/core/canvas/browse/model.d.ts +8 -2
  119. package/dist/core/canvas/browse/model.js +11 -4
  120. package/dist/core/canvas/canvas.d.ts +7 -0
  121. package/dist/core/canvas/canvas.js +37 -0
  122. package/dist/core/canvas/crons.d.ts +9 -0
  123. package/dist/core/canvas/crons.js +51 -0
  124. package/dist/core/canvas/node-order.d.ts +22 -1
  125. package/dist/core/canvas/node-order.js +26 -1
  126. package/dist/core/canvas/render-source.d.ts +5 -0
  127. package/dist/core/canvas/render-source.js +1 -0
  128. package/dist/core/canvas/types.d.ts +9 -3
  129. package/dist/core/command-plugins/bundle.d.ts +6 -1
  130. package/dist/core/command-plugins/bundle.js +26 -13
  131. package/dist/core/command.js +2 -1
  132. package/dist/core/config.d.ts +47 -1
  133. package/dist/core/config.js +137 -4
  134. package/dist/core/help.js +1 -1
  135. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  136. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  137. package/dist/core/human/__tests__/page.test.js +56 -98
  138. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  139. package/dist/core/human/answer-text.js +3 -3
  140. package/dist/core/human/answer.d.ts +6 -4
  141. package/dist/core/human/answer.js +4 -10
  142. package/dist/core/human/component-docs.d.ts +11 -7
  143. package/dist/core/human/component-docs.js +361 -88
  144. package/dist/core/human/convention.d.ts +17 -3
  145. package/dist/core/human/convention.js +36 -5
  146. package/dist/core/human/feedback-companion.d.ts +20 -0
  147. package/dist/core/human/feedback-companion.js +98 -0
  148. package/dist/core/human/feedback.d.ts +65 -0
  149. package/dist/core/human/feedback.js +116 -0
  150. package/dist/core/human/page-catalog.d.ts +40 -5
  151. package/dist/core/human/page-catalog.js +102 -30
  152. package/dist/core/human/page-errors.d.ts +4 -0
  153. package/dist/core/human/page-errors.js +7 -0
  154. package/dist/core/human/page-eval.d.ts +20 -0
  155. package/dist/core/human/page-eval.js +123 -0
  156. package/dist/core/human/page-schema.d.ts +23 -191
  157. package/dist/core/human/page-schema.js +60 -89
  158. package/dist/core/human/page-synth.d.ts +1 -1
  159. package/dist/core/human/page-synth.js +15 -18
  160. package/dist/core/human/page.d.ts +12 -27
  161. package/dist/core/human/page.js +142 -474
  162. package/dist/core/human/root.d.ts +6 -2
  163. package/dist/core/human/root.js +12 -4
  164. package/dist/core/human/scan.d.ts +3 -4
  165. package/dist/core/human/scan.js +16 -15
  166. package/dist/core/human/summary.d.ts +1 -2
  167. package/dist/core/human/summary.js +2 -2
  168. package/dist/core/human/tickets.d.ts +20 -14
  169. package/dist/core/human/tickets.js +91 -31
  170. package/dist/core/human/types.d.ts +10 -2
  171. package/dist/core/preview-registry.js +8 -10
  172. package/dist/core/profiles/default-binding.d.ts +4 -0
  173. package/dist/core/profiles/default-binding.js +24 -0
  174. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  175. package/dist/core/profiles/deletion-reservation.js +38 -0
  176. package/dist/core/profiles/manifest.d.ts +3 -0
  177. package/dist/core/profiles/manifest.js +12 -0
  178. package/dist/core/profiles/select.js +2 -2
  179. package/dist/core/review/realize.js +3 -3
  180. package/dist/core/review/store.d.ts +6 -0
  181. package/dist/core/review/store.js +31 -0
  182. package/dist/core/runtime/bearings.d.ts +0 -4
  183. package/dist/core/runtime/bearings.js +4 -62
  184. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  185. package/dist/core/runtime/bin-contributions.js +186 -0
  186. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  187. package/dist/core/runtime/broker/client-registry.js +13 -1
  188. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  189. package/dist/core/runtime/broker/extension-abort.js +4 -2
  190. package/dist/core/runtime/broker/fault-retry.js +4 -0
  191. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  192. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  193. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  194. package/dist/core/runtime/broker/read-ops.js +2 -2
  195. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  196. package/dist/core/runtime/broker/rebind.js +6 -1
  197. package/dist/core/runtime/broker-extension-render.js +19 -0
  198. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  199. package/dist/core/runtime/broker.d.ts +1 -1
  200. package/dist/core/runtime/broker.js +17 -6
  201. package/dist/core/runtime/front-door-env.d.ts +5 -0
  202. package/dist/core/runtime/front-door-env.js +16 -0
  203. package/dist/core/runtime/front-door.d.ts +1 -5
  204. package/dist/core/runtime/front-door.js +2 -5
  205. package/dist/core/runtime/host.d.ts +1 -1
  206. package/dist/core/runtime/invocation.d.ts +6 -0
  207. package/dist/core/runtime/invocation.js +10 -0
  208. package/dist/core/runtime/kickoff.js +34 -0
  209. package/dist/core/runtime/launch.d.ts +2 -6
  210. package/dist/core/runtime/node-read.js +4 -1
  211. package/dist/core/runtime/nodes.js +2 -0
  212. package/dist/core/runtime/promote.d.ts +1 -0
  213. package/dist/core/runtime/promote.js +3 -1
  214. package/dist/core/runtime/session-cycles.d.ts +6 -0
  215. package/dist/core/runtime/session-cycles.js +15 -1
  216. package/dist/core/runtime/session-visibility.d.ts +17 -8
  217. package/dist/core/runtime/session-visibility.js +19 -10
  218. package/dist/core/runtime/spawn-env.d.ts +9 -1
  219. package/dist/core/runtime/spawn-env.js +62 -2
  220. package/dist/core/runtime/stop-guard.d.ts +2 -2
  221. package/dist/core/runtime/stop-guard.js +2 -2
  222. package/dist/core/runtime/stop-signals.d.ts +1 -1
  223. package/dist/core/runtime/stop-signals.js +2 -2
  224. package/dist/core/runtime/tmux-bindings.js +1 -1
  225. package/dist/core/session-model/session-state.d.ts +4 -3
  226. package/dist/core/session-model/session-state.js +14 -1
  227. package/dist/core/user-settings.d.ts +21 -2
  228. package/dist/core/user-settings.js +32 -11
  229. package/dist/core/worktree.d.ts +15 -45
  230. package/dist/core/worktree.js +59 -261
  231. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  232. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  233. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  234. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  235. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  236. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  237. package/dist/daemon/api/handlers/human.js +24 -20
  238. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  239. package/dist/daemon/api/handlers/inbox.js +39 -115
  240. package/dist/daemon/api/handlers/profiles.js +11 -29
  241. package/dist/daemon/api/map.d.ts +6 -1
  242. package/dist/daemon/api/map.js +23 -0
  243. package/dist/daemon/api/server.js +3 -1
  244. package/dist/daemon/companion-retire.d.ts +2 -0
  245. package/dist/daemon/companion-retire.js +33 -0
  246. package/dist/daemon/cron-run.js +14 -16
  247. package/dist/daemon/human/finish.d.ts +12 -7
  248. package/dist/daemon/human/finish.js +101 -38
  249. package/dist/daemon/human/sweep.js +5 -1
  250. package/dist/daemon/profile-delete.d.ts +7 -0
  251. package/dist/daemon/profile-delete.js +306 -0
  252. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  253. package/dist/daemon/review/deliver.js +3 -3
  254. package/dist/daemon/review/finish.d.ts +0 -2
  255. package/dist/daemon/review/finish.js +1 -29
  256. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  257. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  258. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  259. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  260. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  261. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  262. package/dist/pi-extensions/canvas-stophook.js +4 -4
  263. package/dist/shared/generated-context.js +2 -2
  264. package/dist/types.d.ts +44 -2
  265. package/dist/types.js +7 -2
  266. package/package.json +6 -5
  267. package/runtime.lock.json +5 -33
  268. package/dist/builtin-memory/init.md +0 -38
  269. package/dist/builtin-memory/plan.md +0 -19
  270. package/dist/builtin-memory/spec.md +0 -19
  271. package/dist/commands/human/doc.d.ts +0 -2
  272. package/dist/commands/human/doc.js +0 -91
  273. package/dist/commands/human/html.js +0 -81
  274. package/dist/commands/human/inbox.d.ts +0 -2
  275. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  276. package/dist/core/human/__tests__/page-render.test.js +0 -66
  277. package/dist/core/human/html-markdown.d.ts +0 -3
  278. package/dist/core/human/html-markdown.js +0 -288
  279. package/dist/core/human/page-render.d.ts +0 -4
  280. package/dist/core/human/page-render.js +0 -105
  281. package/dist/pages/bundle.css +0 -1
  282. package/dist/pages/bundle.js +0 -1778
  283. package/dist/pages/comments.d.ts +0 -72
  284. package/dist/pages/comments.js +0 -176
  285. package/dist/pages/controls.d.ts +0 -98
  286. package/dist/pages/controls.js +0 -261
  287. package/dist/pages/elements/cards.d.ts +0 -28
  288. package/dist/pages/elements/cards.js +0 -915
  289. package/dist/pages/elements/chart.d.ts +0 -23
  290. package/dist/pages/elements/chart.js +0 -664
  291. package/dist/pages/elements/options.d.ts +0 -30
  292. package/dist/pages/elements/options.js +0 -742
  293. package/dist/pages/elements/pages.d.ts +0 -30
  294. package/dist/pages/elements/pages.js +0 -377
  295. package/dist/pages/elements/slot.d.ts +0 -26
  296. package/dist/pages/elements/slot.js +0 -112
  297. package/dist/pages/elements/table.d.ts +0 -36
  298. package/dist/pages/elements/table.js +0 -1043
  299. package/dist/pages/elements/text.d.ts +0 -31
  300. package/dist/pages/elements/text.js +0 -1175
  301. package/dist/pages/entry.d.ts +0 -8
  302. package/dist/pages/entry.js +0 -8
  303. package/dist/pages/host.d.ts +0 -134
  304. package/dist/pages/host.js +0 -165
  305. package/dist/pages/readonly.d.ts +0 -24
  306. package/dist/pages/readonly.js +0 -31
  307. package/dist/pages/register.d.ts +0 -2
  308. package/dist/pages/register.js +0 -3
  309. package/dist/pages/responses.d.ts +0 -37
  310. package/dist/pages/responses.js +0 -56
  311. package/dist/pages/slot-config.d.ts +0 -36
  312. package/dist/pages/slot-config.js +0 -50
  313. package/dist/pages/types.d.ts +0 -98
  314. package/dist/pages/types.js +0 -23
  315. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  316. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -0,0 +1,20 @@
1
+ export declare function feedbackCompanionBranchPath(dir: string): string;
2
+ /** Hex sha256 of the ticket's page document, recorded at first comment so the
3
+ * settle-time completion can say whether the page changed during feedback. */
4
+ export declare function pageDocumentSha256(dir: string): string;
5
+ /** A companion node nothing on the ticket points at must not stay resident:
6
+ * the comment that would have bound it was refused, failed to commit, or lost
7
+ * the first-comment race to a concurrently recorded companion. */
8
+ export declare function discardFeedbackCompanion(nodeId: string): void;
9
+ /**
10
+ * Fork the sending node's current state into the ticket-bound companion and
11
+ * wait for its durable boundary bind. Throws on any failure — the caller
12
+ * refuses the comment so no feedback is accepted without a visible
13
+ * conversation able to receive it.
14
+ */
15
+ export declare function realizeFeedbackCompanion(args: {
16
+ ticketId: string;
17
+ dir: string;
18
+ senderNodeId: string;
19
+ title: string;
20
+ }): Promise<string>;
@@ -0,0 +1,98 @@
1
+ // Feedback-companion realization — the one path that creates a page ticket's
2
+ // companion conversation. The first feedback comment forks the page's sending
3
+ // node at its then-current state; the ticket then records that companion and
4
+ // every later comment reaches the same conversation. No fork point is captured
5
+ // at page send and no placeholder exists before feedback.
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+ import { getNode } from '../canvas/index.js';
10
+ import { awaitCompanionBind, captureOrigin } from '../review/companion.js';
11
+ import { COMPANION_BIND_DEADLINE_MS } from '../review/types.js';
12
+ import { SessionManager } from '../runtime/broker-sdk.js';
13
+ import { buildLaunchSpec } from '../runtime/launch.js';
14
+ import { transition } from '../runtime/lifecycle.js';
15
+ import { headlessBrokerHost } from '../runtime/host.js';
16
+ import { spawnNode } from '../runtime/nodes.js';
17
+ import { reviveNode } from '../runtime/revive.js';
18
+ import { materializeSessionBranchAt } from '../runtime/session-branch.js';
19
+ import { pagePath } from './convention.js';
20
+ export function feedbackCompanionBranchPath(dir) { return `${dir}/companion-branch.jsonl`; }
21
+ /** Hex sha256 of the ticket's page document, recorded at first comment so the
22
+ * settle-time completion can say whether the page changed during feedback. */
23
+ export function pageDocumentSha256(dir) {
24
+ return createHash('sha256').update(readFileSync(pagePath(dir, 'jsx'))).digest('hex');
25
+ }
26
+ /** Materialize the companion's fork branch, reusing only an already-valid one. */
27
+ function materializeFeedbackBranch(dir, capture) {
28
+ const branchFile = feedbackCompanionBranchPath(dir);
29
+ if (existsSync(branchFile)) {
30
+ const branch = SessionManager.open(branchFile);
31
+ if (branch.getSessionFile() === resolve(branchFile) && branch.getSessionId() !== '')
32
+ return branchFile;
33
+ }
34
+ materializeSessionBranchAt(capture, branchFile);
35
+ return branchFile;
36
+ }
37
+ /** A companion node nothing on the ticket points at must not stay resident:
38
+ * the comment that would have bound it was refused, failed to commit, or lost
39
+ * the first-comment race to a concurrently recorded companion. */
40
+ export function discardFeedbackCompanion(nodeId) {
41
+ try {
42
+ transition(nodeId, 'cancel');
43
+ headlessBrokerHost.teardown(nodeId);
44
+ }
45
+ catch { /* the refusal is authoritative; a straggler node is inert */ }
46
+ }
47
+ /**
48
+ * Fork the sending node's current state into the ticket-bound companion and
49
+ * wait for its durable boundary bind. Throws on any failure — the caller
50
+ * refuses the comment so no feedback is accepted without a visible
51
+ * conversation able to receive it.
52
+ */
53
+ export async function realizeFeedbackCompanion(args) {
54
+ const origin = getNode(args.senderNodeId);
55
+ if (origin === null)
56
+ throw new Error(`page sender was not found: ${args.senderNodeId}`);
57
+ const capture = await captureOrigin(args.senderNodeId);
58
+ const branchFile = materializeFeedbackBranch(args.dir, capture);
59
+ const { launch } = buildLaunchSpec(origin.kind, origin.mode, {
60
+ lifecycle: 'resident',
61
+ hasManager: false,
62
+ model: capture.modelSpec,
63
+ modelExact: true,
64
+ cwd: origin.cwd,
65
+ profileId: origin.profile_id ?? null,
66
+ });
67
+ const node = spawnNode({
68
+ kind: origin.kind,
69
+ mode: origin.mode,
70
+ lifecycle: 'resident',
71
+ cwd: origin.cwd,
72
+ profile_id: origin.profile_id ?? null,
73
+ parent: args.senderNodeId,
74
+ subscribe: false,
75
+ forkFrom: branchFile,
76
+ modelOverride: capture.modelSpec,
77
+ reviewBinding: {
78
+ kind: 'page_feedback',
79
+ review_id: args.ticketId,
80
+ origin_node_id: args.senderNodeId,
81
+ branch_file: branchFile,
82
+ target_file: pagePath(args.dir, 'jsx'),
83
+ },
84
+ name: `Feedback · ${args.title}`,
85
+ launch,
86
+ });
87
+ try {
88
+ reviveNode(node.node_id, { resume: false });
89
+ if (!await awaitCompanionBind(node.node_id, COMPANION_BIND_DEADLINE_MS)) {
90
+ throw new Error(`feedback companion did not bind within ${COMPANION_BIND_DEADLINE_MS}ms`);
91
+ }
92
+ }
93
+ catch (error) {
94
+ discardFeedbackCompanion(node.node_id);
95
+ throw error;
96
+ }
97
+ return node.node_id;
98
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ export declare const PAGE_FEEDBACK_SCHEMA: "crtr.page-feedback/v1";
3
+ export declare function pageFeedbackPath(dir: string): string;
4
+ declare const pageFeedbackCommentSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ quote: z.ZodString;
7
+ context: z.ZodOptional<z.ZodString>;
8
+ note: z.ZodString;
9
+ status: z.ZodEnum<{
10
+ open: "open";
11
+ resolved: "resolved";
12
+ }>;
13
+ createdAt: z.ZodString;
14
+ resolvedAt: z.ZodOptional<z.ZodString>;
15
+ }, z.core.$strict>;
16
+ declare const pageFeedbackSchema: z.ZodObject<{
17
+ schema: z.ZodLiteral<"crtr.page-feedback/v1">;
18
+ companionNodeId: z.ZodOptional<z.ZodString>;
19
+ firstCommentedSha256: z.ZodOptional<z.ZodString>;
20
+ comments: z.ZodArray<z.ZodObject<{
21
+ id: z.ZodString;
22
+ quote: z.ZodString;
23
+ context: z.ZodOptional<z.ZodString>;
24
+ note: z.ZodString;
25
+ status: z.ZodEnum<{
26
+ open: "open";
27
+ resolved: "resolved";
28
+ }>;
29
+ createdAt: z.ZodString;
30
+ resolvedAt: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strict>>;
32
+ }, z.core.$strict>;
33
+ export type PageFeedbackComment = z.infer<typeof pageFeedbackCommentSchema>;
34
+ export type PageFeedback = z.infer<typeof pageFeedbackSchema>;
35
+ /** Read a ticket's feedback comments; a ticket without any has an empty record. */
36
+ export declare function readPageFeedback(dir: string): PageFeedback;
37
+ export interface CreatePageFeedbackCommentInput {
38
+ quote: string;
39
+ context?: string;
40
+ note: string;
41
+ /** Bound as the ticket's companion when none is recorded yet. An already
42
+ * recorded companion wins; the first writer's companion stays the target. */
43
+ companionNodeId?: string;
44
+ /** Recorded with the companion binding; ignored once one is recorded. */
45
+ firstCommentedSha256?: string;
46
+ }
47
+ export interface PageFeedbackMutation {
48
+ comment: PageFeedbackComment;
49
+ feedback: PageFeedback;
50
+ }
51
+ /** Append one open feedback comment under the ticket lock. */
52
+ export declare function createPageFeedbackComment(dir: string, input: CreatePageFeedbackCommentInput): PageFeedbackMutation;
53
+ export declare class PageTicketTerminalError extends Error {
54
+ constructor();
55
+ }
56
+ export declare class UnknownPageFeedbackCommentError extends Error {
57
+ constructor(commentId: string);
58
+ }
59
+ export declare class PageFeedbackCommentAlreadyResolvedError extends Error {
60
+ constructor(commentId: string);
61
+ }
62
+ /** Mark one open comment resolved under the ticket lock. Terminal: a resolved
63
+ * comment never reopens; a renewed concern is a new comment. */
64
+ export declare function resolvePageFeedbackComment(dir: string, commentId: string): PageFeedbackMutation;
65
+ export {};
@@ -0,0 +1,116 @@
1
+ // Page feedback comments — ticket-attached notes on highlighted page content.
2
+ // Feedback is about the agent's WORK, never part of the page answer: it lives
3
+ // in its own file beside the page, delivers immediately to the ticket's
4
+ // companion conversation, and reaches the response the sender reads at settle
5
+ // only as the unresolved-item addendum. A sent comment is immutable — it is
6
+ // either open or resolved, and only the companion resolves it. The file name
7
+ // is `feedback-comments.json` — `feedback.json` is the review subsystem's
8
+ // output file and must not be shadowed.
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { z } from 'zod';
12
+ import { atomicWriteJson } from './convention.js';
13
+ import { withTicketLock } from './claim.js';
14
+ import { pageTicketState } from './tickets.js';
15
+ export const PAGE_FEEDBACK_SCHEMA = 'crtr.page-feedback/v1';
16
+ export function pageFeedbackPath(dir) { return `${dir}/feedback-comments.json`; }
17
+ const pageFeedbackCommentSchema = z.object({
18
+ id: z.string().min(1),
19
+ /** The highlighted text the note is about; for a chart, its title. */
20
+ quote: z.string().min(1),
21
+ /** Surrounding text that locates the quote on the page. */
22
+ context: z.string().optional(),
23
+ note: z.string().min(1),
24
+ /** Open until the companion has dealt with it; resolution is terminal. */
25
+ status: z.enum(['open', 'resolved']),
26
+ createdAt: z.string().datetime({ offset: true }),
27
+ resolvedAt: z.string().datetime({ offset: true }).optional(),
28
+ }).strict();
29
+ const pageFeedbackSchema = z.object({
30
+ schema: z.literal(PAGE_FEEDBACK_SCHEMA),
31
+ /** The ticket-bound companion conversation every comment delivers to;
32
+ * recorded by the first comment and never replaced. */
33
+ companionNodeId: z.string().min(1).optional(),
34
+ /** Hex sha256 of the page document at first comment, for the settle-time
35
+ * change notice to the sender. */
36
+ firstCommentedSha256: z.string().min(1).optional(),
37
+ comments: z.array(pageFeedbackCommentSchema),
38
+ }).strict();
39
+ function emptyFeedback() {
40
+ return { schema: PAGE_FEEDBACK_SCHEMA, comments: [] };
41
+ }
42
+ /** Read a ticket's feedback comments; a ticket without any has an empty record. */
43
+ export function readPageFeedback(dir) {
44
+ const path = pageFeedbackPath(dir);
45
+ if (!existsSync(path))
46
+ return emptyFeedback();
47
+ let raw;
48
+ try {
49
+ raw = JSON.parse(readFileSync(path, 'utf8'));
50
+ }
51
+ catch {
52
+ throw new Error(`page feedback is not valid JSON: ${path}`);
53
+ }
54
+ const parsed = pageFeedbackSchema.safeParse(raw);
55
+ if (!parsed.success)
56
+ throw new Error(`page feedback is malformed: ${path}`);
57
+ return parsed.data;
58
+ }
59
+ /** Append one open feedback comment under the ticket lock. */
60
+ export function createPageFeedbackComment(dir, input) {
61
+ return withTicketLock(dir, () => {
62
+ requirePendingTicket(dir);
63
+ const feedback = readPageFeedback(dir);
64
+ if (feedback.companionNodeId === undefined && input.companionNodeId !== undefined) {
65
+ feedback.companionNodeId = input.companionNodeId;
66
+ if (input.firstCommentedSha256 !== undefined)
67
+ feedback.firstCommentedSha256 = input.firstCommentedSha256;
68
+ }
69
+ const comment = {
70
+ id: randomUUID(),
71
+ quote: input.quote,
72
+ ...(input.context === undefined ? {} : { context: input.context }),
73
+ note: input.note,
74
+ status: 'open',
75
+ createdAt: new Date().toISOString(),
76
+ };
77
+ feedback.comments.push(comment);
78
+ atomicWriteJson(pageFeedbackPath(dir), feedback);
79
+ return { comment, feedback };
80
+ });
81
+ }
82
+ export class PageTicketTerminalError extends Error {
83
+ constructor() { super('ticket already has a terminal outcome'); }
84
+ }
85
+ /** Settlement commits its terminal outcome under this same ticket lock, so an
86
+ * in-lock pending check is the race-free gate: no feedback mutation may land
87
+ * after the first ticket-ending action, however long the caller's own
88
+ * pre-lock work (a companion fork await) took. */
89
+ function requirePendingTicket(dir) {
90
+ const state = pageTicketState(dir);
91
+ if (state === 'resolved' || state === 'canceled')
92
+ throw new PageTicketTerminalError();
93
+ }
94
+ export class UnknownPageFeedbackCommentError extends Error {
95
+ constructor(commentId) { super(`no feedback comment ${commentId} on this ticket`); }
96
+ }
97
+ export class PageFeedbackCommentAlreadyResolvedError extends Error {
98
+ constructor(commentId) { super(`feedback comment ${commentId} is already resolved`); }
99
+ }
100
+ /** Mark one open comment resolved under the ticket lock. Terminal: a resolved
101
+ * comment never reopens; a renewed concern is a new comment. */
102
+ export function resolvePageFeedbackComment(dir, commentId) {
103
+ return withTicketLock(dir, () => {
104
+ requirePendingTicket(dir);
105
+ const feedback = readPageFeedback(dir);
106
+ const comment = feedback.comments.find((entry) => entry.id === commentId);
107
+ if (comment === undefined)
108
+ throw new UnknownPageFeedbackCommentError(commentId);
109
+ if (comment.status === 'resolved')
110
+ throw new PageFeedbackCommentAlreadyResolvedError(commentId);
111
+ comment.status = 'resolved';
112
+ comment.resolvedAt = new Date().toISOString();
113
+ atomicWriteJson(pageFeedbackPath(dir), feedback);
114
+ return { comment, feedback };
115
+ });
116
+ }
@@ -1,7 +1,7 @@
1
1
  import type { PageComponentRegistration, ProductPageComponents } from '../../types.js';
2
- /** The page slot kinds crtr validates and renders itself. */
2
+ /** The page component kinds crtr validates and renders itself. */
3
3
  export declare const BUILTIN_PAGE_KINDS: readonly ["options", "text", "table", "cards", "chart"];
4
- export declare const STRUCTURAL_PAGE_KINDS: readonly ["crtr-pages", "crtr-page"];
4
+ export declare const STRUCTURAL_PAGE_KINDS: readonly ["page", "step"];
5
5
  export interface PageComponentCatalogEntry {
6
6
  kind: string;
7
7
  tag: string;
@@ -11,12 +11,47 @@ export interface PageComponentCatalogEntry {
11
11
  builtin: boolean;
12
12
  structural: boolean;
13
13
  }
14
- /** Normalize legacy kind strings and documented product component registrations. */
15
- export declare function normalizePageComponents(raw: unknown): PageComponentRegistration[];
14
+ /** The JSX component name a registered kind is authored as. Registry names reach page
15
+ * code as JS identifiers, so a hyphenated kind is authored in its PascalCase form
16
+ * (`worker-card` → `<WorkerCard>`); the registered kind string is what travels on
17
+ * the wire. */
18
+ export declare function pageComponentTag(kind: string): string;
19
+ /** The built-in component kind each authored JSX component name maps to. */
20
+ export declare const BUILTIN_KIND_BY_TAG: Readonly<Record<string, string>>;
21
+ /** Every JSX identifier reserved by the built-in page registry, including structure. */
22
+ export declare const BUILTIN_PAGE_COMPONENT_TAGS: ReadonlySet<string>;
23
+ /** The shadcn display set a page may compose with. These carry no response and never
24
+ * appear in the page manifest; they are in scope so an authored page can lay its
25
+ * content out. */
26
+ export declare const DISPLAY_PAGE_COMPONENT_TAGS: ReadonlySet<string>;
27
+ /** Normalize legacy kind strings and documented product component registrations.
28
+ * `label` names the source in every error message — the default is a scope
29
+ * `config.json` block; a plugin contribution passes its own plugin-named
30
+ * label so a bad registration points at the package that shipped it. */
31
+ export declare function normalizePageComponents(raw: unknown, label?: string): PageComponentRegistration[];
32
+ /** One contributing source of product page components: a scope's own
33
+ * `config.json` block, or one installed plugin's manifest contributions. */
34
+ export interface PageComponentLayer {
35
+ /** How this source is named in a collision error, e.g. `plugin "northlight"`. */
36
+ origin: string;
37
+ components: readonly PageComponentRegistration[];
38
+ }
39
+ /** Concatenate every contributing layer into one active catalog, rejecting a
40
+ * kind — or a derived JSX tag — that two sources both register. There is no
41
+ * precedence here on purpose: two sources claiming one component kind is an
42
+ * authoring defect, and silently letting either win would ship a page component
43
+ * bound to a renderer nobody chose. Each layer's own entries are already
44
+ * validated by `normalizePageComponents`; this is the cross-source gate. */
45
+ export declare function mergePageComponentLayers(layers: readonly PageComponentLayer[]): PageComponentRegistration[];
46
+ /** STRICT install-time validation of a plugin-declared `page_components` block
47
+ * — the loud counterpart to a read path that must never ship a half-valid
48
+ * catalog. Returns one human-readable reason per defect; empty = valid. Used
49
+ * by the archive-bundle validator and by source plugin installs. */
50
+ export declare function invalidPageComponentsReasons(raw: unknown): string[];
16
51
  /** Names-only projection for page validation and rendering consumers. */
17
52
  export declare function pageComponentKinds(components: readonly PageComponentRegistration[]): string[];
18
53
  /** Full built-in + product catalog for CLI discovery. */
19
54
  export declare function pageComponentCatalog(components: readonly PageComponentRegistration[]): PageComponentCatalogEntry[];
20
- /** Whether crtr or the product catalog registers a page slot kind. */
55
+ /** Whether crtr or the product catalog registers a page component kind. */
21
56
  export declare function productPageComponent(kind: string, components: ProductPageComponents): PageComponentRegistration | undefined;
22
57
  export declare function isRegisteredPageKind(kind: string, productComponents: ProductPageComponents): boolean;
@@ -1,17 +1,38 @@
1
- /** The page slot kinds crtr validates and renders itself. */
1
+ /** The page component kinds crtr validates and renders itself. */
2
2
  export const BUILTIN_PAGE_KINDS = ['options', 'text', 'table', 'cards', 'chart'];
3
- export const STRUCTURAL_PAGE_KINDS = ['crtr-pages', 'crtr-page'];
3
+ export const STRUCTURAL_PAGE_KINDS = ['page', 'step'];
4
4
  const PAGE_KIND_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
5
5
  const BUILTIN_PAGE_KIND_SET = new Set(BUILTIN_PAGE_KINDS);
6
6
  const BUILTIN_COMPONENTS = [
7
- { kind: 'crtr-pages', tag: 'crtr-pages', description: 'ordered wrapper for one or more page steps', useWhen: 'use when the page has a response-bearing component or multiple steps', builtin: true, structural: true },
8
- { kind: 'crtr-page', tag: 'crtr-page', description: 'one step inside a crtr-pages flow', useWhen: 'use for each step the human sees and completes in sequence', builtin: true, structural: true },
9
- { kind: 'options', tag: 'crtr-options', description: 'known alternatives with single or multiple selection', useWhen: 'use when the human picks among known alternatives, optionally with freetext', builtin: true, structural: false },
10
- { kind: 'text', tag: 'crtr-text', description: 'editable or read-only text surface', useWhen: 'use when the human writes, edits, or reviews text', builtin: true, structural: false },
11
- { kind: 'table', tag: 'crtr-table', description: 'rows and columns with optional selection', useWhen: 'use when structured records are easiest to compare in rows and columns', builtin: true, structural: false },
12
- { kind: 'cards', tag: 'crtr-cards', description: 'visual records with optional selection', useWhen: 'use when the human compares or picks richer items than a compact option list can carry', builtin: true, structural: false },
13
- { kind: 'chart', tag: 'crtr-chart', description: 'line, bar, or area data visualization', useWhen: 'use when shape, trend, or magnitude matters more than exact tabular values', builtin: true, structural: false },
7
+ { kind: 'page', tag: 'Page', description: 'the page itself: title, subtitle, and every step it contains', useWhen: 'use as the single root element every page returns', builtin: true, structural: true },
8
+ { kind: 'step', tag: 'Step', description: 'one step inside a Page', useWhen: 'use for each step the user sees and completes in sequence', builtin: true, structural: true },
9
+ { kind: 'options', tag: 'UserQuestion', description: 'known alternatives with single or multiple selection', useWhen: 'use when the user picks among known alternatives, optionally with freetext', builtin: true, structural: false },
10
+ { kind: 'text', tag: 'UserText', description: 'writing surface whose answer is the text the user hands back', useWhen: 'use when you need prose from the user — a value they write, or a draft they revise; prose they only read is a plain element', builtin: true, structural: false },
11
+ { kind: 'table', tag: 'UserTable', description: 'rows and columns with optional selection', useWhen: 'use when structured records are easiest to compare in rows and columns', builtin: true, structural: false },
12
+ { kind: 'cards', tag: 'UserCards', description: 'visual records with optional selection', useWhen: 'use when the user compares or picks richer items than a compact option list can carry', builtin: true, structural: false },
13
+ { kind: 'chart', tag: 'Chart', description: 'line, bar, or area data visualization', useWhen: 'use when shape, trend, or magnitude matters more than exact tabular values', builtin: true, structural: false },
14
14
  ];
15
+ /** The JSX component name a registered kind is authored as. Registry names reach page
16
+ * code as JS identifiers, so a hyphenated kind is authored in its PascalCase form
17
+ * (`worker-card` → `<WorkerCard>`); the registered kind string is what travels on
18
+ * the wire. */
19
+ export function pageComponentTag(kind) {
20
+ return kind.split('-').map((part) => (part === '' ? '' : `${part[0].toUpperCase()}${part.slice(1)}`)).join('');
21
+ }
22
+ /** The built-in component kind each authored JSX component name maps to. */
23
+ export const BUILTIN_KIND_BY_TAG = Object.fromEntries(BUILTIN_COMPONENTS.filter((entry) => !entry.structural).map((entry) => [entry.tag, entry.kind]));
24
+ /** Every JSX identifier reserved by the built-in page registry, including structure. */
25
+ export const BUILTIN_PAGE_COMPONENT_TAGS = new Set(BUILTIN_COMPONENTS.map((entry) => entry.tag));
26
+ /** The shadcn display set a page may compose with. These carry no response and never
27
+ * appear in the page manifest; they are in scope so an authored page can lay its
28
+ * content out. */
29
+ export const DISPLAY_PAGE_COMPONENT_TAGS = new Set([
30
+ 'Card', 'CardHeader', 'CardTitle', 'CardDescription', 'CardContent', 'CardFooter', 'CardAction',
31
+ 'Badge', 'Button', 'Separator', 'Progress', 'Alert', 'AlertTitle', 'AlertDescription',
32
+ 'Table', 'TableHeader', 'TableBody', 'TableFooter', 'TableRow', 'TableHead', 'TableCell', 'TableCaption',
33
+ 'Tabs', 'TabsList', 'TabsTrigger', 'TabsContent',
34
+ 'Accordion', 'AccordionItem', 'AccordionTrigger', 'AccordionContent',
35
+ ]);
15
36
  function describeEntry(entry) {
16
37
  try {
17
38
  const json = JSON.stringify(entry);
@@ -21,21 +42,25 @@ function describeEntry(entry) {
21
42
  return String(entry);
22
43
  }
23
44
  }
24
- function optionalText(value, field, index) {
45
+ function optionalText(value, field, label, index) {
25
46
  if (value === undefined)
26
47
  return undefined;
27
48
  if (typeof value !== 'string' || value.trim() === '') {
28
- throw new Error(`page_components[${index}].${field} must be a non-empty string; received ${describeEntry(value)}`);
49
+ throw new Error(`${label}[${index}].${field} must be a non-empty string; received ${describeEntry(value)}`);
29
50
  }
30
51
  return value.trim();
31
52
  }
32
- /** Normalize legacy kind strings and documented product component registrations. */
33
- export function normalizePageComponents(raw) {
53
+ /** Normalize legacy kind strings and documented product component registrations.
54
+ * `label` names the source in every error message — the default is a scope
55
+ * `config.json` block; a plugin contribution passes its own plugin-named
56
+ * label so a bad registration points at the package that shipped it. */
57
+ export function normalizePageComponents(raw, label = 'page_components') {
34
58
  if (raw === undefined)
35
59
  return [];
36
60
  if (!Array.isArray(raw))
37
- throw new Error(`page_components must be an array; received ${describeEntry(raw)}`);
38
- const seen = new Set();
61
+ throw new Error(`${label} must be an array; received ${describeEntry(raw)}`);
62
+ const seenKinds = new Set();
63
+ const seenTags = new Set();
39
64
  return raw.map((entry, index) => {
40
65
  let registration;
41
66
  if (typeof entry === 'string') {
@@ -45,33 +70,80 @@ export function normalizePageComponents(raw) {
45
70
  const object = entry;
46
71
  const extra = Object.keys(object).filter((key) => !['kind', 'description', 'useWhen', 'doc', 'display'].includes(key));
47
72
  if (extra.length > 0)
48
- throw new Error(`page_components[${index}] has unknown field(s): ${extra.join(', ')}`);
73
+ throw new Error(`${label}[${index}] has unknown field(s): ${extra.join(', ')}`);
49
74
  if (typeof object['kind'] !== 'string')
50
- throw new Error(`page_components[${index}].kind must be a string; offending entry: ${describeEntry(entry)}`);
75
+ throw new Error(`${label}[${index}].kind must be a string; offending entry: ${describeEntry(entry)}`);
51
76
  if (object['display'] !== undefined && typeof object['display'] !== 'boolean')
52
- throw new Error(`page_components[${index}].display must be a boolean; offending entry: ${describeEntry(entry)}`);
77
+ throw new Error(`${label}[${index}].display must be a boolean; offending entry: ${describeEntry(entry)}`);
53
78
  registration = {
54
79
  kind: object['kind'].trim(),
55
- ...(optionalText(object['description'], 'description', index) === undefined ? {} : { description: optionalText(object['description'], 'description', index) }),
56
- ...(optionalText(object['useWhen'], 'useWhen', index) === undefined ? {} : { useWhen: optionalText(object['useWhen'], 'useWhen', index) }),
57
- ...(optionalText(object['doc'], 'doc', index) === undefined ? {} : { doc: optionalText(object['doc'], 'doc', index) }),
80
+ ...(optionalText(object['description'], 'description', label, index) === undefined ? {} : { description: optionalText(object['description'], 'description', label, index) }),
81
+ ...(optionalText(object['useWhen'], 'useWhen', label, index) === undefined ? {} : { useWhen: optionalText(object['useWhen'], 'useWhen', label, index) }),
82
+ ...(optionalText(object['doc'], 'doc', label, index) === undefined ? {} : { doc: optionalText(object['doc'], 'doc', label, index) }),
58
83
  ...(object['display'] === true ? { display: true } : {}),
59
84
  };
60
85
  }
61
86
  else {
62
- throw new Error(`page_components[${index}] must be a string or component object; offending entry: ${describeEntry(entry)}`);
87
+ throw new Error(`${label}[${index}] must be a string or component object; offending entry: ${describeEntry(entry)}`);
63
88
  }
64
89
  const kind = registration.kind;
65
90
  if (!PAGE_KIND_PATTERN.test(kind))
66
- throw new Error(`page_components[${index}] has invalid page kind ${describeEntry(kind)}; expected /^[a-z][a-z0-9-]{0,63}$/`);
91
+ throw new Error(`${label}[${index}] has invalid page kind ${describeEntry(kind)}; expected /^[a-z][a-z0-9-]{0,63}$/`);
67
92
  if (BUILTIN_PAGE_KIND_SET.has(kind) || STRUCTURAL_PAGE_KINDS.includes(kind))
68
- throw new Error(`page_components[${index}] collides with built-in page kind "${kind}"`);
69
- if (seen.has(kind))
70
- throw new Error(`page_components[${index}] duplicates page kind "${kind}"`);
71
- seen.add(kind);
93
+ throw new Error(`${label}[${index}] collides with built-in page kind "${kind}"`);
94
+ if (seenKinds.has(kind))
95
+ throw new Error(`${label}[${index}] duplicates page kind "${kind}"`);
96
+ const tag = pageComponentTag(kind);
97
+ if (BUILTIN_PAGE_COMPONENT_TAGS.has(tag))
98
+ throw new Error(`${label}[${index}] kind "${kind}" derives JSX tag "${tag}", which collides with a built-in page component`);
99
+ if (seenTags.has(tag))
100
+ throw new Error(`${label}[${index}] kind "${kind}" derives JSX tag "${tag}", which duplicates another product page component`);
101
+ seenKinds.add(kind);
102
+ seenTags.add(tag);
72
103
  return registration;
73
104
  });
74
105
  }
106
+ /** Concatenate every contributing layer into one active catalog, rejecting a
107
+ * kind — or a derived JSX tag — that two sources both register. There is no
108
+ * precedence here on purpose: two sources claiming one component kind is an
109
+ * authoring defect, and silently letting either win would ship a page component
110
+ * bound to a renderer nobody chose. Each layer's own entries are already
111
+ * validated by `normalizePageComponents`; this is the cross-source gate. */
112
+ export function mergePageComponentLayers(layers) {
113
+ const originByKind = new Map();
114
+ const claimByTag = new Map();
115
+ const out = [];
116
+ for (const layer of layers) {
117
+ for (const component of layer.components) {
118
+ const kindOrigin = originByKind.get(component.kind);
119
+ if (kindOrigin !== undefined) {
120
+ throw new Error(`page component kind "${component.kind}" is registered by both ${kindOrigin} and ${layer.origin}`);
121
+ }
122
+ const tag = pageComponentTag(component.kind);
123
+ const tagClaim = claimByTag.get(tag);
124
+ if (tagClaim !== undefined) {
125
+ throw new Error(`page component kind "${component.kind}" (${layer.origin}) derives JSX tag "${tag}", which duplicates kind "${tagClaim.kind}" from ${tagClaim.origin}`);
126
+ }
127
+ originByKind.set(component.kind, layer.origin);
128
+ claimByTag.set(tag, { kind: component.kind, origin: layer.origin });
129
+ out.push(component);
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ /** STRICT install-time validation of a plugin-declared `page_components` block
135
+ * — the loud counterpart to a read path that must never ship a half-valid
136
+ * catalog. Returns one human-readable reason per defect; empty = valid. Used
137
+ * by the archive-bundle validator and by source plugin installs. */
138
+ export function invalidPageComponentsReasons(raw) {
139
+ try {
140
+ normalizePageComponents(raw);
141
+ return [];
142
+ }
143
+ catch (error) {
144
+ return [error instanceof Error ? error.message : String(error)];
145
+ }
146
+ }
75
147
  /** Names-only projection for page validation and rendering consumers. */
76
148
  export function pageComponentKinds(components) {
77
149
  return components.map((component) => component.kind);
@@ -82,16 +154,16 @@ export function pageComponentCatalog(components) {
82
154
  ...BUILTIN_COMPONENTS,
83
155
  ...components.map((component) => ({
84
156
  kind: component.kind,
85
- tag: component.kind,
157
+ tag: pageComponentTag(component.kind),
86
158
  description: component.description ?? 'product-registered page component',
87
- useWhen: component.useWhen ?? 'use when the product component matches the structured UI the human needs',
159
+ useWhen: component.useWhen ?? 'use when the product component matches the structured UI the user needs',
88
160
  ...(component.doc === undefined ? {} : { doc: component.doc }),
89
161
  builtin: false,
90
162
  structural: false,
91
163
  })),
92
164
  ];
93
165
  }
94
- /** Whether crtr or the product catalog registers a page slot kind. */
166
+ /** Whether crtr or the product catalog registers a page component kind. */
95
167
  export function productPageComponent(kind, components) {
96
168
  return components.find((component) => component.kind === kind);
97
169
  }
@@ -0,0 +1,4 @@
1
+ /** A page failed authoring or validation; the caller can report it as bad page input. */
2
+ export declare class PageAuthoringError extends Error {
3
+ constructor(message: string);
4
+ }
@@ -0,0 +1,7 @@
1
+ /** A page failed authoring or validation; the caller can report it as bad page input. */
2
+ export class PageAuthoringError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'PageAuthoringError';
6
+ }
7
+ }
@@ -0,0 +1,20 @@
1
+ /** One element of the tree an authored page returns. `tag` is the JSX identifier the
2
+ * author wrote: a registry component (`UserQuestion`), a display component (`Card`),
3
+ * or a plain element (`div`). Helper components are expanded before they get here. */
4
+ export interface PageNode {
5
+ tag: string;
6
+ props: Record<string, unknown>;
7
+ children: PageNode[];
8
+ }
9
+ /** Evaluate the compiled page module and return the element tree its default export renders.
10
+ *
11
+ * Each in-scope component name is bound to itself, so `<UserQuestion>` renders as the tag
12
+ * string `"UserQuestion"`. A name outside that set is not defined, and the page fails with
13
+ * the reference error rather than rendering a component the host does not have. */
14
+ export declare function evaluatePageTree(code: string, tags: Iterable<string>): PageNode[];
15
+ /** Keep only what can cross the wire. A manifest is JSON, so handlers and undefined values
16
+ * are dropped rather than rejected — a click handler is legitimate authoring, it simply has
17
+ * no meaning to a host reading the manifest. The round-trip also rebuilds the props in this
18
+ * realm: values born in the vm carry that realm's prototypes, which prototype-sensitive
19
+ * comparisons reject. */
20
+ export declare function jsonProps(props: Record<string, unknown>): Record<string, unknown>;