@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
package/dist/core/human/page.js
CHANGED
|
@@ -1,396 +1,177 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.name = 'PageAuthoringError';
|
|
11
|
-
}
|
|
12
|
-
}
|
|
3
|
+
import { transformSync } from 'esbuild';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { BUILTIN_KIND_BY_TAG, BUILTIN_PAGE_COMPONENT_TAGS, DISPLAY_PAGE_COMPONENT_TAGS, pageComponentTag } from './page-catalog.js';
|
|
6
|
+
import { isResponseBearingSlot, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
|
|
7
|
+
import { evaluatePageTree, jsonProps } from './page-eval.js';
|
|
8
|
+
import { PageAuthoringError } from './page-errors.js';
|
|
9
|
+
export { PageAuthoringError };
|
|
13
10
|
function asPageAuthoringError(error) {
|
|
14
11
|
if (error instanceof PageAuthoringError)
|
|
15
12
|
return error;
|
|
16
13
|
return new PageAuthoringError(error instanceof Error ? error.message : String(error));
|
|
17
14
|
}
|
|
18
|
-
function validateAuthoredManifest(parsed,
|
|
15
|
+
function validateAuthoredManifest(parsed, productComponents) {
|
|
19
16
|
try {
|
|
20
|
-
|
|
17
|
+
const manifest = validateAuthoredPageManifest(parsed, productComponents);
|
|
18
|
+
const responseBearing = manifest.slots.filter(isResponseBearingSlot);
|
|
19
|
+
if (responseBearing.length >= 2) {
|
|
20
|
+
for (const slot of responseBearing) {
|
|
21
|
+
const label = slot.config.label;
|
|
22
|
+
if (typeof label !== 'string' || label === '')
|
|
23
|
+
throw new PageAuthoringError(`response-bearing slot "${slot.id}" requires a nonempty config.label when the page has multiple response-bearing slots`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return manifest;
|
|
21
27
|
}
|
|
22
28
|
catch (error) {
|
|
23
29
|
throw asPageAuthoringError(error);
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
const IMPORTLESS_SCOPE = 'a page is evaluated with React and every registry component already in scope, so it takes no imports — write `<Page>`, `<UserQuestion>`, `<Card>` and friends directly.';
|
|
33
|
+
/** Compile one authored TSX page to the CommonJS module the Gateway evaluates.
|
|
34
|
+
* Registry names stay free identifiers; the Gateway supplies them at eval. */
|
|
35
|
+
export function compilePage(document) {
|
|
28
36
|
try {
|
|
29
|
-
|
|
37
|
+
return transformSync(document, {
|
|
38
|
+
loader: 'tsx',
|
|
39
|
+
format: 'cjs',
|
|
40
|
+
jsx: 'transform',
|
|
41
|
+
jsxFactory: 'React.createElement',
|
|
42
|
+
jsxFragment: 'React.Fragment',
|
|
43
|
+
target: 'es2020',
|
|
44
|
+
sourcefile: 'page.tsx',
|
|
45
|
+
}).code;
|
|
30
46
|
}
|
|
31
47
|
catch (error) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const errors = error.errors;
|
|
49
|
+
const first = errors?.[0];
|
|
50
|
+
if (first === undefined)
|
|
51
|
+
throw asPageAuthoringError(error);
|
|
52
|
+
const at = first.location == null ? '' : ` (page.tsx:${first.location.line}:${first.location.column})`;
|
|
53
|
+
throw new PageAuthoringError(`page does not compile: ${first.text ?? 'unknown error'}${at}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseTsx(document) {
|
|
57
|
+
return ts.createSourceFile('page.tsx', document, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
|
|
58
|
+
}
|
|
59
|
+
function eachNode(node, visit) {
|
|
60
|
+
visit(node);
|
|
61
|
+
node.forEachChild((child) => eachNode(child, visit));
|
|
62
|
+
}
|
|
63
|
+
/** Reject module plumbing: the compiled module is evaluated with no loader behind it,
|
|
64
|
+
* so any `import` or `require` could only fail at render time. */
|
|
65
|
+
function rejectModulePlumbing(sourceFile) {
|
|
66
|
+
let offense;
|
|
67
|
+
eachNode(sourceFile, (node) => {
|
|
68
|
+
if (offense !== undefined)
|
|
69
|
+
return;
|
|
70
|
+
if (ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node))
|
|
71
|
+
offense = 'import';
|
|
72
|
+
else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== undefined)
|
|
73
|
+
offense = 'export … from';
|
|
74
|
+
else if (ts.isCallExpression(node)) {
|
|
75
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword)
|
|
76
|
+
offense = 'dynamic import()';
|
|
77
|
+
else if (ts.isIdentifier(node.expression) && node.expression.text === 'require')
|
|
78
|
+
offense = 'require()';
|
|
52
79
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (const [index, line] of rawLines.entries()) {
|
|
65
|
-
const colon = line.indexOf(':');
|
|
66
|
-
if (colon <= 0)
|
|
67
|
-
throw new PageAuthoringError(`front matter line ${index + 2} must be "key: value"`);
|
|
68
|
-
const key = line.slice(0, colon).trim();
|
|
69
|
-
const value = line.slice(colon + 1).trim();
|
|
70
|
-
if (!['title', 'subtitle', 'placement'].includes(key))
|
|
71
|
-
throw new PageAuthoringError(`unknown page front matter key "${key}"`);
|
|
72
|
-
if (values.has(key))
|
|
73
|
-
throw new PageAuthoringError(`duplicate page front matter key "${key}"`);
|
|
74
|
-
values.set(key, value);
|
|
75
|
-
}
|
|
76
|
-
const title = values.get('title');
|
|
77
|
-
if (title === undefined || title === '')
|
|
78
|
-
throw new PageAuthoringError('page front matter title is required and must be nonempty');
|
|
79
|
-
const placement = values.get('placement') ?? 'panel';
|
|
80
|
-
if (placement !== 'inline' && placement !== 'panel')
|
|
81
|
-
throw new PageAuthoringError(`page front matter placement must be exactly inline or panel; received "${placement}"`);
|
|
82
|
-
const subtitle = values.get('subtitle');
|
|
83
|
-
return { title, ...(subtitle === undefined ? {} : { subtitle }), placement, bodyStart: cursor };
|
|
80
|
+
});
|
|
81
|
+
if (offense !== undefined)
|
|
82
|
+
throw new PageAuthoringError(`page source uses ${offense}: ${IMPORTLESS_SCOPE}`);
|
|
83
|
+
}
|
|
84
|
+
function stringProp(props, name) {
|
|
85
|
+
const value = props[name];
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (typeof value !== 'string')
|
|
89
|
+
throw new PageAuthoringError(`<Page> prop ${name} must be a string; it is part of the page manifest`);
|
|
90
|
+
return value;
|
|
84
91
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
function scanPage(document, code, productComponents) {
|
|
93
|
+
rejectModulePlumbing(parseTsx(document));
|
|
94
|
+
const inScope = [...BUILTIN_PAGE_COMPONENT_TAGS, ...DISPLAY_PAGE_COMPONENT_TAGS, ...productComponents.map((component) => pageComponentTag(component.kind))];
|
|
95
|
+
const roots = evaluatePageTree(code, inScope);
|
|
96
|
+
const pages = [];
|
|
97
|
+
const collectPages = (nodes) => {
|
|
98
|
+
for (const node of nodes) {
|
|
99
|
+
if (node.tag === 'Page')
|
|
100
|
+
pages.push(node);
|
|
101
|
+
collectPages(node.children);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
collectPages(roots);
|
|
105
|
+
if (pages.length === 0)
|
|
106
|
+
throw new PageAuthoringError('page default export must render exactly one <Page title="…"> root; it rendered none');
|
|
107
|
+
if (pages.length > 1)
|
|
108
|
+
throw new PageAuthoringError(`page default export renders ${pages.length} <Page> elements; exactly one is allowed`);
|
|
109
|
+
const page = pages[0];
|
|
110
|
+
const title = (stringProp(page.props, 'title') ?? '').trim();
|
|
111
|
+
if (title === '')
|
|
112
|
+
throw new PageAuthoringError('<Page> requires a nonempty title prop');
|
|
113
|
+
const subtitle = stringProp(page.props, 'subtitle')?.trim();
|
|
114
|
+
if (page.props['placement'] !== undefined)
|
|
115
|
+
throw new PageAuthoringError('<Page> placement is not authored; choose --placement inline or --placement panel when sending the page.');
|
|
116
|
+
const productTags = new Map();
|
|
117
|
+
for (const component of productComponents) {
|
|
118
|
+
const tag = pageComponentTag(component.kind);
|
|
119
|
+
if (BUILTIN_PAGE_COMPONENT_TAGS.has(tag))
|
|
120
|
+
throw new PageAuthoringError(`product page component kind "${component.kind}" derives JSX tag "${tag}", which collides with a built-in page component`);
|
|
121
|
+
if (productTags.has(tag))
|
|
122
|
+
throw new PageAuthoringError(`product page component kind "${component.kind}" derives JSX tag "${tag}", which duplicates product page component kind "${productTags.get(tag)}"`);
|
|
123
|
+
productTags.set(tag, component.kind);
|
|
94
124
|
}
|
|
95
|
-
return lines;
|
|
96
|
-
}
|
|
97
|
-
/** CommonMark closing fence: at least as many of the opener's fence character, nothing else on the line. */
|
|
98
|
-
function closesFence(text, opener) {
|
|
99
|
-
const trimmed = text.trimEnd();
|
|
100
|
-
return trimmed.length >= opener.length && trimmed.split('').every((character) => character === opener.char);
|
|
101
|
-
}
|
|
102
|
-
function addProse(projection, raw) {
|
|
103
|
-
const markdown = raw.trim();
|
|
104
|
-
if (markdown !== '')
|
|
105
|
-
projection.push({ kind: 'prose', markdown });
|
|
106
|
-
}
|
|
107
|
-
function scanMarkdown(document, productKinds) {
|
|
108
|
-
const meta = parseFrontMatter(document);
|
|
109
|
-
const body = document.slice(meta.bodyStart);
|
|
110
|
-
const markdownCheck = checkMarkdown(body);
|
|
111
|
-
if (!markdownCheck.ok)
|
|
112
|
-
throw new PageAuthoringError(markdownCheck.error);
|
|
113
|
-
const lines = sourceLines(document, meta.bodyStart);
|
|
114
|
-
// Fence errors report document-absolute line numbers, past the front matter.
|
|
115
|
-
const lineOffset = document.slice(0, meta.bodyStart).split('\n').length - 1;
|
|
116
125
|
const slots = [];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
ordinaryFence = fence;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const kind = info.slice('slot:'.length);
|
|
138
|
-
if (kind === '' || info !== `slot:${kind}` || !isRegisteredPageKind(kind, productKinds)) {
|
|
139
|
-
throw new PageAuthoringError(`unregistered or invalid page slot kind "${kind}" in md fence at line ${lineOffset + index + 1}`);
|
|
140
|
-
}
|
|
141
|
-
let closeIndex = -1;
|
|
142
|
-
for (let candidate = index + 1; candidate < lines.length; candidate++) {
|
|
143
|
-
if (closesFence(lines[candidate].text, fence)) {
|
|
144
|
-
closeIndex = candidate;
|
|
145
|
-
break;
|
|
126
|
+
let stepCount = 0;
|
|
127
|
+
const collectSlots = (nodes, step) => {
|
|
128
|
+
for (const node of nodes) {
|
|
129
|
+
const inStep = node.tag === 'Step' ? stepCount++ : step;
|
|
130
|
+
const builtin = BUILTIN_KIND_BY_TAG[node.tag];
|
|
131
|
+
const product = productTags.get(node.tag);
|
|
132
|
+
if (builtin !== undefined || product !== undefined) {
|
|
133
|
+
const { id, ...config } = jsonProps(node.props);
|
|
134
|
+
if (id !== undefined && typeof id !== 'string')
|
|
135
|
+
throw new PageAuthoringError(`<${node.tag}> id prop must be a string`);
|
|
136
|
+
slots.push({
|
|
137
|
+
...(id === undefined ? {} : { id }),
|
|
138
|
+
kind: builtin ?? product,
|
|
139
|
+
step: inStep,
|
|
140
|
+
config,
|
|
141
|
+
...(builtin === undefined ? { unvalidated: true } : {}),
|
|
142
|
+
});
|
|
146
143
|
}
|
|
144
|
+
collectSlots(node.children, inStep);
|
|
147
145
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const rawObject = parseJsonObject(document.slice(line.end, lines[closeIndex].start), `slot:${kind} fence at line ${lineOffset + index + 1}`);
|
|
152
|
-
const { id, ...config } = rawObject;
|
|
153
|
-
if (id !== undefined && typeof id !== 'string')
|
|
154
|
-
throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has non-string reserved id`);
|
|
155
|
-
slots.push({ ...(id === undefined ? {} : { id }), kind, step: 0, config, ...(BUILTIN_PAGE_KINDS.includes(kind) ? {} : { unvalidated: true }) });
|
|
156
|
-
projection.push({ kind: 'slot', slotIndex: slots.length - 1 });
|
|
157
|
-
proseStart = lines[closeIndex].end;
|
|
158
|
-
index = closeIndex;
|
|
159
|
-
}
|
|
160
|
-
addProse(projection, document.slice(proseStart));
|
|
161
|
-
return { title: meta.title, ...(meta.subtitle === undefined ? {} : { subtitle: meta.subtitle }), placement: meta.placement, steps: 1, slots, projection };
|
|
146
|
+
};
|
|
147
|
+
collectSlots(page.children, 0);
|
|
148
|
+
return { title, ...(subtitle === undefined || subtitle === '' ? {} : { subtitle }), steps: Math.max(stepCount, 1), slots };
|
|
162
149
|
}
|
|
163
|
-
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const character = html[index];
|
|
168
|
-
if (quote !== null) {
|
|
169
|
-
if (character === quote)
|
|
170
|
-
quote = null;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (character === '"' || character === "'") {
|
|
174
|
-
quote = character;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (character === '>')
|
|
178
|
-
return index + 1;
|
|
179
|
-
}
|
|
180
|
-
throw new PageAuthoringError(`unterminated HTML tag beginning at byte ${start}`);
|
|
181
|
-
}
|
|
182
|
-
function parseAttributes(source) {
|
|
183
|
-
const attrs = new Map();
|
|
184
|
-
const tagNameEnd = source.search(/\s/);
|
|
185
|
-
const body = tagNameEnd === -1 ? '' : source.slice(tagNameEnd);
|
|
186
|
-
const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
187
|
-
for (const match of body.matchAll(pattern))
|
|
188
|
-
attrs.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '');
|
|
189
|
-
return attrs;
|
|
150
|
+
/** Derive the transport manifest from one authored TSX page document. */
|
|
151
|
+
export function derivePageManifestFromDocument(document, productComponents = [], source, delivery = { placement: 'inline', inbox: false, reply: false }) {
|
|
152
|
+
const scanned = scanPage(document, compilePage(document), productComponents);
|
|
153
|
+
return validateAuthoredManifest({ schema: 'crtr.page/v2', dialect: 'jsx', title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), delivery, document: 'page.tsx', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productComponents);
|
|
190
154
|
}
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (html.startsWith('<!--', start)) {
|
|
201
|
-
const end = html.indexOf('-->', start + 4);
|
|
202
|
-
if (end === -1)
|
|
203
|
-
throw new PageAuthoringError(`unterminated HTML comment at byte ${start}`);
|
|
204
|
-
cursor = end + 3;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const end = tagEnd(html, start);
|
|
208
|
-
const token = html.slice(start, end);
|
|
209
|
-
if (/^<!/i.test(token) || /^<\?/i.test(token)) {
|
|
210
|
-
cursor = end;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const closing = token.match(/^<\s*\/\s*([^\s>]+)/);
|
|
214
|
-
if (closing !== null) {
|
|
215
|
-
const tag = closing[1].toLowerCase();
|
|
216
|
-
let match = stack.length - 1;
|
|
217
|
-
while (match >= 0 && stack[match].tag !== tag)
|
|
218
|
-
match--;
|
|
219
|
-
if (match >= 0)
|
|
220
|
-
stack.length = match;
|
|
221
|
-
cursor = end;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
const opening = token.match(/^<\s*([^\s/>]+)/);
|
|
225
|
-
if (opening === null) {
|
|
226
|
-
cursor = end;
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const tag = opening[1].toLowerCase();
|
|
230
|
-
const parent = stack.at(-1);
|
|
231
|
-
const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, ...(parent === undefined ? {} : { parent }), children: [] };
|
|
232
|
-
if (parent === undefined)
|
|
233
|
-
roots.push(element);
|
|
234
|
-
else
|
|
235
|
-
parent.children.push(element);
|
|
236
|
-
all.push(element);
|
|
237
|
-
const selfClosing = /\/\s*>$/.test(token) || VOID_TAGS.has(tag);
|
|
238
|
-
if (tag === 'script' && !selfClosing) {
|
|
239
|
-
const closePattern = /<\s*\/\s*script\s*>/gi;
|
|
240
|
-
closePattern.lastIndex = end;
|
|
241
|
-
const close = closePattern.exec(html);
|
|
242
|
-
if (close === null)
|
|
243
|
-
throw new PageAuthoringError(`unterminated <script> element at byte ${start}`);
|
|
244
|
-
element.text = html.slice(end, close.index);
|
|
245
|
-
cursor = closePattern.lastIndex;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
if (!selfClosing)
|
|
249
|
-
stack.push(element);
|
|
250
|
-
cursor = end;
|
|
251
|
-
}
|
|
252
|
-
return all;
|
|
253
|
-
}
|
|
254
|
-
function ancestor(element, tag) {
|
|
255
|
-
let current = element.parent;
|
|
256
|
-
while (current !== undefined) {
|
|
257
|
-
if (current.tag === tag)
|
|
258
|
-
return current;
|
|
259
|
-
current = current.parent;
|
|
260
|
-
}
|
|
261
|
-
return undefined;
|
|
262
|
-
}
|
|
263
|
-
function decodeHtmlAttribute(value) {
|
|
264
|
-
return value.replace(/&(?:amp|quot|apos|lt|gt);|&#(?:x[0-9a-f]+|\d+);/gi, (entity) => {
|
|
265
|
-
const named = { '&': '&', '"': '"', ''': "'", '<': '<', '>': '>' };
|
|
266
|
-
const lower = entity.toLowerCase();
|
|
267
|
-
if (named[lower] !== undefined)
|
|
268
|
-
return named[lower];
|
|
269
|
-
const numeric = lower.startsWith('&#x') ? Number.parseInt(lower.slice(3, -1), 16) : Number.parseInt(lower.slice(2, -1), 10);
|
|
270
|
-
return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : entity;
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
function validateHtmlStructure(html) {
|
|
274
|
-
const counts = [
|
|
275
|
-
['doctype', /<!doctype\b[^>]*>/gi, 'doctype'],
|
|
276
|
-
['<head>', /<head(?=\s|>)/gi, '<head> opening tag'],
|
|
277
|
-
['<body>', /<body(?=\s|>)/gi, '<body> opening tag'],
|
|
278
|
-
];
|
|
279
|
-
for (const [, pattern, label] of counts) {
|
|
280
|
-
const count = html.match(pattern)?.length ?? 0;
|
|
281
|
-
if (count !== 1)
|
|
282
|
-
throw new PageAuthoringError(`Expected exactly one ${label}, found ${count}.`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function readHtmlMetadata(elements) {
|
|
286
|
-
const recognized = new Map();
|
|
287
|
-
for (const element of elements.filter((candidate) => candidate.tag === 'meta')) {
|
|
288
|
-
const name = element.attrs.get('name');
|
|
289
|
-
if (name === undefined || !name.startsWith('crtr-page-'))
|
|
290
|
-
continue;
|
|
291
|
-
if (!['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement'].includes(name))
|
|
292
|
-
throw new PageAuthoringError(`unknown page metadata element <meta name="${name}">`);
|
|
293
|
-
if (ancestor(element, 'head') === undefined)
|
|
294
|
-
throw new PageAuthoringError(`<meta name="${name}"> must appear inside <head>`);
|
|
295
|
-
const entries = recognized.get(name) ?? [];
|
|
296
|
-
entries.push(element);
|
|
297
|
-
recognized.set(name, entries);
|
|
298
|
-
}
|
|
299
|
-
for (const name of ['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement']) {
|
|
300
|
-
const count = recognized.get(name)?.length ?? 0;
|
|
301
|
-
if ((name === 'crtr-page-title' && count !== 1) || (name !== 'crtr-page-title' && count > 1)) {
|
|
302
|
-
const expected = name === 'crtr-page-title' ? 'exactly one' : 'at most one';
|
|
303
|
-
throw new PageAuthoringError(`Expected ${expected} <meta name="${name}">, found ${count}.`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
const titleElement = recognized.get('crtr-page-title')[0];
|
|
307
|
-
const title = decodeHtmlAttribute(titleElement.attrs.get('content') ?? '').trim();
|
|
308
|
-
if (title === '')
|
|
309
|
-
throw new PageAuthoringError(`<meta name="crtr-page-title"> requires nonempty content`);
|
|
310
|
-
const subtitleElement = recognized.get('crtr-page-subtitle')?.[0];
|
|
311
|
-
const subtitle = subtitleElement === undefined ? undefined : decodeHtmlAttribute(subtitleElement.attrs.get('content') ?? '').trim();
|
|
312
|
-
const placementElement = recognized.get('crtr-page-placement')?.[0];
|
|
313
|
-
const placementValue = placementElement === undefined ? 'panel' : decodeHtmlAttribute(placementElement.attrs.get('content') ?? '').trim();
|
|
314
|
-
if (placementValue !== 'inline' && placementValue !== 'panel')
|
|
315
|
-
throw new PageAuthoringError(`<meta name="crtr-page-placement"> content must be exactly inline or panel; received "${placementValue}"`);
|
|
316
|
-
return { title, ...(subtitle === undefined ? {} : { subtitle }), placement: placementValue };
|
|
317
|
-
}
|
|
318
|
-
function configForHtmlElement(element) {
|
|
319
|
-
const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
|
|
320
|
-
if (script === undefined)
|
|
321
|
-
throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} requires a direct <script type="application/json"> child`);
|
|
322
|
-
return parseJsonObject(script.text ?? '', `<${element.tag}> at byte ${element.start}`);
|
|
323
|
-
}
|
|
324
|
-
function scanHtml(document, productKinds) {
|
|
325
|
-
validateHtmlStructure(document);
|
|
326
|
-
const elements = parseHtmlElements(document);
|
|
327
|
-
const metadata = readHtmlMetadata(elements);
|
|
328
|
-
const wrappers = elements.filter((element) => element.tag === 'crtr-pages');
|
|
329
|
-
if (wrappers.some((candidate) => ancestor(candidate, 'crtr-pages') !== undefined))
|
|
330
|
-
throw new PageAuthoringError('nested <crtr-pages> element is not allowed');
|
|
331
|
-
if (wrappers.length > 1)
|
|
332
|
-
throw new PageAuthoringError(`more than one <crtr-pages> element: found ${wrappers.length}`);
|
|
333
|
-
const wrapper = wrappers[0];
|
|
334
|
-
const pages = elements.filter((element) => element.tag === 'crtr-page');
|
|
335
|
-
for (const page of pages)
|
|
336
|
-
if (ancestor(page, 'crtr-pages') !== wrapper || wrapper === undefined)
|
|
337
|
-
throw new PageAuthoringError(`<crtr-page> outside <crtr-pages> at byte ${page.start}`);
|
|
338
|
-
if (wrapper !== undefined && pages.length === 0)
|
|
339
|
-
throw new PageAuthoringError('empty <crtr-pages> element must contain at least one <crtr-page>');
|
|
340
|
-
const pageIndexes = new Map(pages.map((page, index) => [page, index]));
|
|
341
|
-
const rawSlots = [];
|
|
342
|
-
const slotElements = [];
|
|
343
|
-
for (const element of elements) {
|
|
344
|
-
if (element.tag === 'crtr-pages' || element.tag === 'crtr-page')
|
|
345
|
-
continue;
|
|
346
|
-
let kind;
|
|
347
|
-
let unvalidated = false;
|
|
348
|
-
if (element.tag === 'crtr-slot') {
|
|
349
|
-
kind = element.attrs.get('kind');
|
|
350
|
-
if (kind === undefined || !productKinds.includes(kind) || BUILTIN_PAGE_KINDS.includes(kind))
|
|
351
|
-
throw new PageAuthoringError(`<crtr-slot> at byte ${element.start} names unregistered product kind "${kind ?? ''}"`);
|
|
352
|
-
unvalidated = true;
|
|
353
|
-
}
|
|
354
|
-
else if (element.tag.startsWith('crtr-')) {
|
|
355
|
-
kind = element.tag.slice('crtr-'.length);
|
|
356
|
-
if (!BUILTIN_PAGE_KINDS.includes(kind))
|
|
357
|
-
throw new PageAuthoringError(`unregistered page element <${element.tag}> at byte ${element.start}; product kinds use <crtr-slot kind="…">`);
|
|
358
|
-
}
|
|
359
|
-
if (kind === undefined)
|
|
360
|
-
continue;
|
|
361
|
-
const id = element.attrs.get('id');
|
|
362
|
-
const containingPage = ancestor(element, 'crtr-page');
|
|
363
|
-
const step = containingPage === undefined ? 0 : pageIndexes.get(containingPage) ?? 0;
|
|
364
|
-
rawSlots.push({ ...(id === undefined ? {} : { id }), kind, step, config: configForHtmlElement(element), ...(unvalidated ? { unvalidated: true } : {}) });
|
|
365
|
-
slotElements.push(element);
|
|
366
|
-
}
|
|
367
|
-
const steps = wrapper === undefined ? 1 : pages.length;
|
|
368
|
-
const provisional = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: 'html', ...metadata, document: 'page.html', steps, slots: rawSlots }, productKinds);
|
|
369
|
-
for (let index = 0; index < provisional.slots.length; index++) {
|
|
370
|
-
const slot = provisional.slots[index];
|
|
371
|
-
if (!isResponseBearingSlot(slot))
|
|
372
|
-
continue;
|
|
373
|
-
const element = slotElements[index];
|
|
374
|
-
if (ancestor(element, 'crtr-page') === undefined) {
|
|
375
|
-
throw new PageAuthoringError(`response-bearing element <${element.tag}> for slot "${slot.id}" is outside any <crtr-page>.\nWrap interactive elements with:\n<crtr-pages><crtr-page>…</crtr-page></crtr-pages>`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return { ...metadata, steps, slots: provisional.slots, projection: provisional.slots.map((_, slotIndex) => ({ kind: 'slot', slotIndex })) };
|
|
379
|
-
}
|
|
380
|
-
function scanDocument(document, dialect, productKinds) {
|
|
381
|
-
return dialect === 'md' ? scanMarkdown(document, productKinds) : scanHtml(document, productKinds);
|
|
155
|
+
function htmlMetadata(document) {
|
|
156
|
+
const title = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(document)?.[1]?.replace(/<[^>]+>/g, '').trim();
|
|
157
|
+
if (title === undefined || title === '')
|
|
158
|
+
throw new PageAuthoringError('HTML page requires a nonempty <title>.');
|
|
159
|
+
const description = [...document.matchAll(/<meta\b[^>]*>/gi)].map((match) => match[0]).map((tag) => {
|
|
160
|
+
const attrs = new Map([...tag.matchAll(/\b([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/g)].map((match) => [match[1].toLowerCase(), match[3]]));
|
|
161
|
+
return attrs.get('name')?.toLowerCase() === 'description' ? attrs.get('content')?.trim() : undefined;
|
|
162
|
+
}).find((value) => value !== undefined);
|
|
163
|
+
return { title, ...(description === undefined || description === '' ? {} : { subtitle: description }) };
|
|
382
164
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return validateAuthoredManifest({ schema: 'crtr.page/v1', dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: dialect === 'md' ? 'page.md' : 'page.html', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productKinds);
|
|
165
|
+
export function deriveHtmlPageManifest(document, source, delivery = { placement: 'inline', inbox: false, reply: false }) {
|
|
166
|
+
const metadata = htmlMetadata(document);
|
|
167
|
+
return validateAuthoredManifest({ schema: 'crtr.page/v2', dialect: 'html', ...metadata, delivery, document: 'page.html', steps: 1, slots: [], ...(source === undefined ? {} : { source }) }, []);
|
|
387
168
|
}
|
|
388
|
-
/** Derive the transport manifest from one authored page source file. */
|
|
389
|
-
export function derivePageManifest(sourceFile,
|
|
390
|
-
return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'),
|
|
169
|
+
/** Derive the transport manifest from one authored TSX page source file. */
|
|
170
|
+
export function derivePageManifest(sourceFile, productComponents = [], source, delivery) {
|
|
171
|
+
return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), productComponents, source, delivery);
|
|
391
172
|
}
|
|
392
173
|
/** Read and validate page.json from a ticket directory. */
|
|
393
|
-
export function parsePage(dir
|
|
174
|
+
export function parsePage(dir) {
|
|
394
175
|
let raw;
|
|
395
176
|
try {
|
|
396
177
|
raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
|
|
@@ -398,30 +179,5 @@ export function parsePage(dir, productKinds = []) {
|
|
|
398
179
|
catch {
|
|
399
180
|
throw new PageAuthoringError('page.json is not valid JSON');
|
|
400
181
|
}
|
|
401
|
-
return validatePageManifest(raw
|
|
402
|
-
}
|
|
403
|
-
/** Key-order-independent value identity, so a re-scan compares by content. */
|
|
404
|
-
function canonicalJson(value) {
|
|
405
|
-
if (value === null || typeof value !== 'object')
|
|
406
|
-
return JSON.stringify(value) ?? 'null';
|
|
407
|
-
if (Array.isArray(value))
|
|
408
|
-
return `[${value.map(canonicalJson).join(',')}]`;
|
|
409
|
-
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => (left < right ? -1 : 1));
|
|
410
|
-
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
|
|
411
|
-
}
|
|
412
|
-
/** Project authored source for local rendering and verify its slot references against the manifest. */
|
|
413
|
-
export function parsePageSource(document, manifest, productKinds = []) {
|
|
414
|
-
const canonical = validateAuthoredManifest(manifest, productKinds);
|
|
415
|
-
const scanned = scanDocument(document, canonical.dialect, productKinds);
|
|
416
|
-
// Normalize the re-scan through the same schema pass the manifest went through,
|
|
417
|
-
// so per-kind config defaults are on both sides of the comparison.
|
|
418
|
-
const derived = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: canonical.dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: canonical.document, steps: scanned.steps, slots: scanned.slots }, productKinds);
|
|
419
|
-
if (derived.slots.length !== canonical.slots.length)
|
|
420
|
-
throw new PageAuthoringError(`page source has ${derived.slots.length} slots but manifest has ${canonical.slots.length}`);
|
|
421
|
-
for (let index = 0; index < canonical.slots.length; index++) {
|
|
422
|
-
const left = derived.slots[index];
|
|
423
|
-
if (canonicalJson(left) !== canonicalJson(canonical.slots[index]))
|
|
424
|
-
throw new PageAuthoringError(`page source slot #${index} (${left.id ?? left.kind}) does not match manifest slot #${index}`);
|
|
425
|
-
}
|
|
426
|
-
return scanned.projection;
|
|
182
|
+
return validatePageManifest(raw);
|
|
427
183
|
}
|
|
@@ -10,6 +10,7 @@ export declare const reviewDescriptorSchema: z.ZodObject<{
|
|
|
10
10
|
sessionName: z.ZodOptional<z.ZodString>;
|
|
11
11
|
askedBy: z.ZodOptional<z.ZodString>;
|
|
12
12
|
blockedSince: z.ZodOptional<z.ZodString>;
|
|
13
|
+
profileName: z.ZodOptional<z.ZodString>;
|
|
13
14
|
nodeId: z.ZodOptional<z.ZodString>;
|
|
14
15
|
}, z.core.$strip>;
|
|
15
16
|
blockedSince: z.ZodString;
|
|
@@ -3,7 +3,7 @@ import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
|
3
3
|
import { claimPath, pageManifestPath, progressPath, responsePath, reviewPath } from './convention.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { checkReviewableFile } from './visible-paths.js';
|
|
6
|
-
const reviewSourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), nodeId: z.string().optional() });
|
|
6
|
+
const reviewSourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), profileName: z.string().optional(), nodeId: z.string().optional() });
|
|
7
7
|
export const reviewDescriptorSchema = z.object({
|
|
8
8
|
schema: z.literal('humanloop.review/v1'),
|
|
9
9
|
file: z.string().min(1).refine(isAbsolute, 'file must be absolute'),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export declare function ticketsRoot(): string;
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/** The opaque id the `/v1/human/inbox` routes address a ticket by — a hash of
|
|
3
|
+
* the canonical tickets root and the ticket directory basename. The daemon
|
|
4
|
+
* derives the same value when it resolves a route id back to a directory. */
|
|
5
|
+
export declare function opaqueInboxTicketId(ticketId: string): string;
|
|
6
|
+
/** Ticket directories share the canvas node root. Reply-bearing tickets may use their bridge id; standalone pages use an independent ticket id. */
|
|
7
|
+
export declare function ticketDir(ticketId: string): string;
|
|
4
8
|
export declare function inboxActivityPath(): string;
|
|
5
9
|
/** Signal one durable ticket mutation to every open inbox without watching every ticket directory. */
|
|
6
10
|
export declare function signalInboxActivity(): void;
|
package/dist/core/human/root.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { mkdirSync, watch, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, realpathSync, watch, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { basename, dirname, join } from 'node:path';
|
|
3
4
|
import { crtrHome, nodeDir, nodesRoot } from '../canvas/paths.js';
|
|
4
5
|
export function ticketsRoot() {
|
|
5
6
|
return nodesRoot();
|
|
6
7
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** The opaque id the `/v1/human/inbox` routes address a ticket by — a hash of
|
|
9
|
+
* the canonical tickets root and the ticket directory basename. The daemon
|
|
10
|
+
* derives the same value when it resolves a route id back to a directory. */
|
|
11
|
+
export function opaqueInboxTicketId(ticketId) {
|
|
12
|
+
const canonicalRoot = realpathSync(ticketsRoot());
|
|
13
|
+
return createHash('sha256').update(`${canonicalRoot}\0${ticketId}`, 'utf8').digest('hex');
|
|
14
|
+
}
|
|
15
|
+
/** Ticket directories share the canvas node root. Reply-bearing tickets may use their bridge id; standalone pages use an independent ticket id. */
|
|
16
|
+
export function ticketDir(ticketId) {
|
|
17
|
+
return nodeDir(ticketId);
|
|
10
18
|
}
|
|
11
19
|
export function inboxActivityPath() {
|
|
12
20
|
return join(crtrHome(), 'signals', 'inbox-activity');
|