@north-light/crouter 0.3.197 → 0.3.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +19 -19
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +126 -33
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +8 -8
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +29 -9
- package/dist/clients/attach/render/chat-view.js +64 -35
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -982
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +3 -4
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +101 -36
- package/dist/clients/inbox/tui/slots.d.ts +4 -4
- package/dist/clients/inbox/tui/slots.js +19 -44
- package/dist/clients/inbox/tui/types.d.ts +6 -10
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +118 -24
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +17 -8
- package/dist/commands/human/prompts.js +217 -226
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +4 -15
- package/dist/commands/human/shared.js +19 -61
- package/dist/commands/human.js +32 -19
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +41 -45
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +33 -0
- package/dist/core/canvas/canvas.js +92 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +8 -12
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +138 -4
- package/dist/core/help.d.ts +3 -0
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +20 -0
- package/dist/core/human/__tests__/page-tickets.test.js +68 -34
- package/dist/core/human/__tests__/page.test.js +60 -119
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +53 -0
- package/dist/core/human/answer.js +119 -0
- package/dist/core/human/component-docs.d.ts +18 -0
- package/dist/core/human/component-docs.js +434 -0
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +56 -5
- package/dist/core/human/page-catalog.js +151 -22
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +48 -116
- package/dist/core/human/page-schema.js +100 -72
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +20 -7
- package/dist/core/human/page.d.ts +13 -21
- package/dist/core/human/page.js +147 -391
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -3
- package/dist/core/human/scan.js +21 -21
- package/dist/core/human/summary.d.ts +2 -6
- package/dist/core/human/summary.js +3 -59
- package/dist/core/human/tickets.d.ts +22 -15
- package/dist/core/human/tickets.js +90 -28
- package/dist/core/human/types.d.ts +14 -2
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +12 -4
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +25 -12
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +36 -2
- package/dist/core/user-settings.js +56 -14
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +48 -109
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +29 -17
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +12 -6
- package/dist/daemon/human/finish.js +134 -54
- package/dist/daemon/human/sweep.js +9 -3
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +14 -9
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +31 -13
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +61 -5
- package/dist/types.js +8 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/scripts/install-runtime.mjs +19 -24
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/commands/human/doc.js +0 -144
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/page-render.test.js +0 -49
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
- package/dist/core/human/page-render.d.ts +0 -3
- package/dist/core/human/page-render.js +0 -131
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1347
- package/dist/pages/comments.d.ts +0 -43
- package/dist/pages/comments.js +0 -125
- package/dist/pages/elements/cards.d.ts +0 -17
- package/dist/pages/elements/cards.js +0 -802
- package/dist/pages/elements/chart.js +0 -648
- package/dist/pages/elements/options.d.ts +0 -21
- package/dist/pages/elements/options.js +0 -629
- package/dist/pages/elements/pages.d.ts +0 -24
- package/dist/pages/elements/pages.js +0 -355
- package/dist/pages/elements/slot.d.ts +0 -21
- package/dist/pages/elements/slot.js +0 -105
- package/dist/pages/elements/table.d.ts +0 -19
- package/dist/pages/elements/table.js +0 -935
- package/dist/pages/elements/text.d.ts +0 -25
- package/dist/pages/elements/text.js +0 -968
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -158
- package/dist/pages/host.js +0 -182
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -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,6 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import type { PageComponentRegistration, ProductPageComponents } from '../../types.js';
|
|
2
|
+
/** The page component kinds crtr validates and renders itself. */
|
|
2
3
|
export declare const BUILTIN_PAGE_KINDS: readonly ["options", "text", "table", "cards", "chart"];
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export declare const STRUCTURAL_PAGE_KINDS: readonly ["page", "step"];
|
|
5
|
+
export interface PageComponentCatalogEntry {
|
|
6
|
+
kind: string;
|
|
7
|
+
tag: string;
|
|
8
|
+
description: string;
|
|
9
|
+
useWhen: string;
|
|
10
|
+
doc?: string;
|
|
11
|
+
builtin: boolean;
|
|
12
|
+
structural: boolean;
|
|
13
|
+
}
|
|
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[];
|
|
51
|
+
/** Names-only projection for page validation and rendering consumers. */
|
|
52
|
+
export declare function pageComponentKinds(components: readonly PageComponentRegistration[]): string[];
|
|
53
|
+
/** Full built-in + product catalog for CLI discovery. */
|
|
54
|
+
export declare function pageComponentCatalog(components: readonly PageComponentRegistration[]): PageComponentCatalogEntry[];
|
|
55
|
+
/** Whether crtr or the product catalog registers a page component kind. */
|
|
56
|
+
export declare function productPageComponent(kind: string, components: ProductPageComponents): PageComponentRegistration | undefined;
|
|
57
|
+
export declare function isRegisteredPageKind(kind: string, productComponents: ProductPageComponents): boolean;
|
|
@@ -1,7 +1,38 @@
|
|
|
1
|
-
/** The page
|
|
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 = ['page', 'step'];
|
|
3
4
|
const PAGE_KIND_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
4
5
|
const BUILTIN_PAGE_KIND_SET = new Set(BUILTIN_PAGE_KINDS);
|
|
6
|
+
const BUILTIN_COMPONENTS = [
|
|
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
|
+
];
|
|
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
|
+
]);
|
|
5
36
|
function describeEntry(entry) {
|
|
6
37
|
try {
|
|
7
38
|
const json = JSON.stringify(entry);
|
|
@@ -11,33 +42,131 @@ function describeEntry(entry) {
|
|
|
11
42
|
return String(entry);
|
|
12
43
|
}
|
|
13
44
|
}
|
|
14
|
-
|
|
15
|
-
|
|
45
|
+
function optionalText(value, field, label, index) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
49
|
+
throw new Error(`${label}[${index}].${field} must be a non-empty string; received ${describeEntry(value)}`);
|
|
50
|
+
}
|
|
51
|
+
return value.trim();
|
|
52
|
+
}
|
|
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') {
|
|
16
58
|
if (raw === undefined)
|
|
17
59
|
return [];
|
|
18
|
-
if (!Array.isArray(raw))
|
|
19
|
-
throw new Error(
|
|
20
|
-
|
|
21
|
-
const
|
|
60
|
+
if (!Array.isArray(raw))
|
|
61
|
+
throw new Error(`${label} must be an array; received ${describeEntry(raw)}`);
|
|
62
|
+
const seenKinds = new Set();
|
|
63
|
+
const seenTags = new Set();
|
|
22
64
|
return raw.map((entry, index) => {
|
|
23
|
-
|
|
24
|
-
|
|
65
|
+
let registration;
|
|
66
|
+
if (typeof entry === 'string') {
|
|
67
|
+
registration = { kind: entry.trim() };
|
|
25
68
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
69
|
+
else if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
70
|
+
const object = entry;
|
|
71
|
+
const extra = Object.keys(object).filter((key) => !['kind', 'description', 'useWhen', 'doc', 'display'].includes(key));
|
|
72
|
+
if (extra.length > 0)
|
|
73
|
+
throw new Error(`${label}[${index}] has unknown field(s): ${extra.join(', ')}`);
|
|
74
|
+
if (typeof object['kind'] !== 'string')
|
|
75
|
+
throw new Error(`${label}[${index}].kind must be a string; offending entry: ${describeEntry(entry)}`);
|
|
76
|
+
if (object['display'] !== undefined && typeof object['display'] !== 'boolean')
|
|
77
|
+
throw new Error(`${label}[${index}].display must be a boolean; offending entry: ${describeEntry(entry)}`);
|
|
78
|
+
registration = {
|
|
79
|
+
kind: object['kind'].trim(),
|
|
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) }),
|
|
83
|
+
...(object['display'] === true ? { display: true } : {}),
|
|
84
|
+
};
|
|
29
85
|
}
|
|
30
|
-
|
|
31
|
-
throw new Error(
|
|
86
|
+
else {
|
|
87
|
+
throw new Error(`${label}[${index}] must be a string or component object; offending entry: ${describeEntry(entry)}`);
|
|
32
88
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
89
|
+
const kind = registration.kind;
|
|
90
|
+
if (!PAGE_KIND_PATTERN.test(kind))
|
|
91
|
+
throw new Error(`${label}[${index}] has invalid page kind ${describeEntry(kind)}; expected /^[a-z][a-z0-9-]{0,63}$/`);
|
|
92
|
+
if (BUILTIN_PAGE_KIND_SET.has(kind) || STRUCTURAL_PAGE_KINDS.includes(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);
|
|
103
|
+
return registration;
|
|
38
104
|
});
|
|
39
105
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
147
|
+
/** Names-only projection for page validation and rendering consumers. */
|
|
148
|
+
export function pageComponentKinds(components) {
|
|
149
|
+
return components.map((component) => component.kind);
|
|
150
|
+
}
|
|
151
|
+
/** Full built-in + product catalog for CLI discovery. */
|
|
152
|
+
export function pageComponentCatalog(components) {
|
|
153
|
+
return [
|
|
154
|
+
...BUILTIN_COMPONENTS,
|
|
155
|
+
...components.map((component) => ({
|
|
156
|
+
kind: component.kind,
|
|
157
|
+
tag: pageComponentTag(component.kind),
|
|
158
|
+
description: component.description ?? 'product-registered page component',
|
|
159
|
+
useWhen: component.useWhen ?? 'use when the product component matches the structured UI the user needs',
|
|
160
|
+
...(component.doc === undefined ? {} : { doc: component.doc }),
|
|
161
|
+
builtin: false,
|
|
162
|
+
structural: false,
|
|
163
|
+
})),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
/** Whether crtr or the product catalog registers a page component kind. */
|
|
167
|
+
export function productPageComponent(kind, components) {
|
|
168
|
+
return components.find((component) => component.kind === kind);
|
|
169
|
+
}
|
|
170
|
+
export function isRegisteredPageKind(kind, productComponents) {
|
|
171
|
+
return BUILTIN_PAGE_KIND_SET.has(kind) || productPageComponent(kind, productComponents) !== undefined;
|
|
43
172
|
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import vm from 'node:vm';
|
|
2
|
+
import { PageAuthoringError } from './page-errors.js';
|
|
3
|
+
const EVAL_TIMEOUT_MS = 2000;
|
|
4
|
+
const MAX_DEPTH = 64;
|
|
5
|
+
const FRAGMENT = Symbol('page.fragment');
|
|
6
|
+
/** Generic static-extraction binding for the host hook. It must tolerate any product capability name while leaving unrelated free identifiers as errors. */
|
|
7
|
+
function hostStub() {
|
|
8
|
+
let proxy;
|
|
9
|
+
const callable = () => proxy;
|
|
10
|
+
proxy = new Proxy(callable, { get: () => proxy, apply: () => proxy });
|
|
11
|
+
return proxy;
|
|
12
|
+
}
|
|
13
|
+
/** A page is authored against React's classic runtime, so the compiled module calls
|
|
14
|
+
* `React.createElement`. Nothing here renders: the stub records the tree and returns
|
|
15
|
+
* initial values for the hooks a page might reach for, which is what a manifest wants. */
|
|
16
|
+
function stubReact() {
|
|
17
|
+
// A helper component is expanded here, while the render is still inside the vm, so a
|
|
18
|
+
// component that never returns is stopped by the same timeout as the page itself.
|
|
19
|
+
const createElement = (type, props, ...children) => {
|
|
20
|
+
const { key: _key, ref: _ref, ...rest } = props ?? {};
|
|
21
|
+
const flat = children.flat(Infinity);
|
|
22
|
+
if (typeof type === 'function')
|
|
23
|
+
return type({ ...rest, ...(flat.length === 0 ? {} : { children: flat }) });
|
|
24
|
+
return { $page_element: true, type, props: rest, children: flat };
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
createElement,
|
|
28
|
+
Fragment: FRAGMENT,
|
|
29
|
+
useState: (initial) => [typeof initial === 'function' ? initial() : initial, () => { }],
|
|
30
|
+
useReducer: (_reducer, initial) => [initial, () => { }],
|
|
31
|
+
useMemo: (factory) => factory(),
|
|
32
|
+
useCallback: (fn) => fn,
|
|
33
|
+
useRef: (initial) => ({ current: initial }),
|
|
34
|
+
useEffect: () => { },
|
|
35
|
+
useLayoutEffect: () => { },
|
|
36
|
+
useId: () => 'page-id',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function isRawElement(value) {
|
|
40
|
+
return typeof value === 'object' && value !== null && value.$page_element === true;
|
|
41
|
+
}
|
|
42
|
+
/** Evaluate the compiled page module and return the element tree its default export renders.
|
|
43
|
+
*
|
|
44
|
+
* Each in-scope component name is bound to itself, so `<UserQuestion>` renders as the tag
|
|
45
|
+
* string `"UserQuestion"`. A name outside that set is not defined, and the page fails with
|
|
46
|
+
* the reference error rather than rendering a component the host does not have. */
|
|
47
|
+
export function evaluatePageTree(code, tags) {
|
|
48
|
+
const exported = {};
|
|
49
|
+
const base = {
|
|
50
|
+
React: stubReact(),
|
|
51
|
+
module: { exports: exported },
|
|
52
|
+
exports: exported,
|
|
53
|
+
console: { log: () => { }, warn: () => { }, error: () => { }, info: () => { }, debug: () => { } },
|
|
54
|
+
usePageHost: hostStub(),
|
|
55
|
+
};
|
|
56
|
+
for (const tag of tags)
|
|
57
|
+
base[tag] = tag;
|
|
58
|
+
const deadline = Date.now() + EVAL_TIMEOUT_MS;
|
|
59
|
+
let rendered;
|
|
60
|
+
try {
|
|
61
|
+
vm.createContext(base);
|
|
62
|
+
new vm.Script(code, { filename: 'page.tsx' }).runInContext(base, { timeout: EVAL_TIMEOUT_MS });
|
|
63
|
+
const component = base['module'].exports.default;
|
|
64
|
+
if (typeof component !== 'function')
|
|
65
|
+
throw new PageAuthoringError('page module must `export default` a component function');
|
|
66
|
+
// Render inside the context so a page that never returns is stopped by the timeout
|
|
67
|
+
// rather than hanging the caller.
|
|
68
|
+
rendered = new vm.Script('(module.exports.default)({})', { filename: 'page.tsx' }).runInContext(base, { timeout: EVAL_TIMEOUT_MS });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof PageAuthoringError)
|
|
72
|
+
throw error;
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
throw new PageAuthoringError(`page failed while rendering: ${message}`);
|
|
75
|
+
}
|
|
76
|
+
// The promise is built in the vm's realm, so identity checks against this realm's
|
|
77
|
+
// Promise do not hold; thenable is the honest test.
|
|
78
|
+
if (typeof rendered?.then === 'function')
|
|
79
|
+
throw new PageAuthoringError('page component is async; a page renders synchronously');
|
|
80
|
+
const normalize = (value, depth) => {
|
|
81
|
+
if (value === null || value === undefined || typeof value === 'boolean')
|
|
82
|
+
return [];
|
|
83
|
+
if (depth > MAX_DEPTH)
|
|
84
|
+
throw new PageAuthoringError('page nests deeper than a page can render; check for a component that renders itself');
|
|
85
|
+
if (Date.now() > deadline)
|
|
86
|
+
throw new PageAuthoringError('page took too long to render; check for a component that never finishes');
|
|
87
|
+
if (Array.isArray(value))
|
|
88
|
+
return value.flatMap((item) => normalize(item, depth + 1));
|
|
89
|
+
if (!isRawElement(value))
|
|
90
|
+
return [];
|
|
91
|
+
const { type, props, children } = value;
|
|
92
|
+
const nested = [...normalize(props['children'], depth + 1), ...children.flatMap((child) => normalize(child, depth + 1))];
|
|
93
|
+
if (type === FRAGMENT)
|
|
94
|
+
return nested;
|
|
95
|
+
if (typeof type !== 'string')
|
|
96
|
+
return nested;
|
|
97
|
+
const { children: _children, ...rest } = props;
|
|
98
|
+
return [{ tag: type, props: rest, children: nested }];
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
return normalize(rendered, 0);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof PageAuthoringError)
|
|
105
|
+
throw error;
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
throw new PageAuthoringError(`page failed while rendering: ${message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Keep only what can cross the wire. A manifest is JSON, so handlers and undefined values
|
|
111
|
+
* are dropped rather than rejected — a click handler is legitimate authoring, it simply has
|
|
112
|
+
* no meaning to a host reading the manifest. The round-trip also rebuilds the props in this
|
|
113
|
+
* realm: values born in the vm carry that realm's prototypes, which prototype-sensitive
|
|
114
|
+
* comparisons reject. */
|
|
115
|
+
export function jsonProps(props) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(JSON.stringify(props));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
throw new PageAuthoringError(`page prop cannot cross the wire: ${message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|