@north-light/crouter 0.3.198 → 0.3.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +18 -18
- 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 +81 -34
- 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 +6 -6
- 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 +2 -2
- 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/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- 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/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- 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 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- 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 +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- 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/{html.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 +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- 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__/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__/helpers/harness.js +17 -55
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +31 -26
- 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 +4 -5
- package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- 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/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -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 +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- 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 +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- 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 +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- 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 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- 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 +3 -3
- 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/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 +17 -6
- 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/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 +21 -2
- package/dist/core/user-settings.js +32 -11
- 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 +39 -115
- 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 +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -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/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.js +19 -5
- 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 +44 -2
- package/dist/types.js +7 -2
- package/package.json +6 -5
- package/runtime.lock.json +5 -33
- 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/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- 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__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -5,67 +5,73 @@ import { join } from 'node:path';
|
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
const home = mkdtempSync(join(tmpdir(), 'crtr-page-tickets-'));
|
|
7
7
|
process.env['CRTR_HOME'] = home;
|
|
8
|
-
const { atomicWriteJson, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
|
|
8
|
+
const { atomicWriteJson, bindTicketReplyRoute, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
|
|
9
9
|
const { claimTicket } = await import('../claim.js');
|
|
10
10
|
const { scanInbox, scanPageHistory } = await import('../scan.js');
|
|
11
11
|
const { ticketDir } = await import('../root.js');
|
|
12
|
-
const { cancelTicket, completePage, readPageProgress, readPageTicket, preparePage, readTicketResult, submitPage, takeoverAndCompletePage, writePageProgress, } = await import('../tickets.js');
|
|
12
|
+
const { cancelTicket, completePage, readPageProgress, readPageTicket, preparePage, readTicketResult, submitPage, takeoverAndCompletePage, validatePageTicketResponses, writePageProgress, } = await import('../tickets.js');
|
|
13
13
|
const sources = mkdtempSync(join(tmpdir(), 'crtr-page-sources-'));
|
|
14
14
|
function sourceFile(name, body) {
|
|
15
15
|
const path = join(sources, name);
|
|
16
16
|
writeFileSync(path, body);
|
|
17
17
|
return path;
|
|
18
18
|
}
|
|
19
|
-
const askPage = sourceFile('ask.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const askPage = sourceFile('ask.tsx', `export default function Rollout() {
|
|
20
|
+
return (
|
|
21
|
+
<Page title="Rollout scope" subtitle="Pick tonight\u2019s rollout">
|
|
22
|
+
<p>Which rollout do you want?</p>
|
|
23
|
+
<UserQuestion id="rollout" label="Rollout lane" body="Canary limits the blast radius." mode="single" options={[{ id: 'canary', label: 'Canary first' }, { id: 'full', label: 'Full' }]} />
|
|
24
|
+
</Page>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
const displayPage = sourceFile('display.tsx', `export default function Nightly() {
|
|
29
|
+
return (
|
|
30
|
+
<Page title="Nightly report">
|
|
31
|
+
<h1>All green</h1>
|
|
32
|
+
</Page>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
function submit(id, source, emittedAt, nodeId = 'node-1', replyExpected = true) {
|
|
37
|
+
const dir = submitPage({ dir: ticketDir(id), sourceFile: source, source: { nodeId, emittedAt }, delivery: { placement: 'inline', inbox: replyExpected, reply: replyExpected } }).dir;
|
|
38
|
+
if (replyExpected)
|
|
39
|
+
bindTicketReplyRoute(dir, id);
|
|
24
40
|
return dir;
|
|
25
41
|
}
|
|
26
|
-
test('submitPage publishes the authored source, the derived manifest and the
|
|
27
|
-
const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage,
|
|
42
|
+
test('submitPage publishes the authored source, the derived manifest and the emittedAt stamp', () => {
|
|
43
|
+
const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage, source: { nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
28
44
|
assert.equal(submitted.kind, 'page');
|
|
29
45
|
assert.equal(submitted.id, 'ask');
|
|
30
|
-
assert.equal(readFileSync(pagePath(submitted.dir
|
|
46
|
+
assert.equal(readFileSync(pagePath(submitted.dir), 'utf8'), readFileSync(askPage, 'utf8'));
|
|
31
47
|
const manifest = JSON.parse(readFileSync(pageManifestPath(submitted.dir), 'utf8'));
|
|
32
|
-
assert.equal(manifest['schema'], 'crtr.page/
|
|
33
|
-
assert.equal(manifest['document'], 'page.
|
|
34
|
-
assert.
|
|
48
|
+
assert.equal(manifest['schema'], 'crtr.page/v2');
|
|
49
|
+
assert.equal(manifest['document'], 'page.tsx');
|
|
50
|
+
assert.deepEqual(manifest['delivery'], { placement: 'inline', inbox: true, reply: true });
|
|
35
51
|
const source = manifest['source'];
|
|
36
52
|
assert.equal(source.nodeId, 'node-1');
|
|
37
|
-
assert.ok(Date.parse(source.
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
assert.ok(Date.parse(source.emittedAt) > 0, 'submission stamps emittedAt');
|
|
54
|
+
// A ticket's request is replaceable in place — what `human send --replace`
|
|
55
|
+
// does. Republishing over the same directory revises the page and leaves one
|
|
56
|
+
// ticket behind, rather than refusing or raising a second one.
|
|
57
|
+
const stamp = { nodeId: 'node-replace', emittedAt: '2026-01-05T00:00:00.000Z' };
|
|
58
|
+
const first = submitPage({ dir: ticketDir('revisable'), sourceFile: askPage, source: stamp });
|
|
59
|
+
const revised = submitPage({ dir: ticketDir('revisable'), sourceFile: displayPage, source: stamp });
|
|
60
|
+
assert.equal(revised.id, first.id);
|
|
61
|
+
assert.equal(revised.manifest.title, 'Nightly report');
|
|
62
|
+
assert.equal(readFileSync(pagePath(revised.dir), 'utf8'), readFileSync(displayPage, 'utf8'));
|
|
63
|
+
assert.deepEqual(scanPageHistory('node-replace').map((entry) => entry.id), ['revisable'], 'a replaced page is one ticket, not two');
|
|
64
|
+
bindTicketReplyRoute(submitted.dir, 'ask');
|
|
40
65
|
const listed = scanInbox().find((item) => item.id === 'ask');
|
|
41
66
|
assert.equal(listed?.kind, 'page');
|
|
42
67
|
assert.equal(listed?.kind === 'page' ? listed.awaitsResponse : undefined, true);
|
|
43
68
|
assert.deepEqual(listed?.kind === 'page' ? listed.slotKinds : undefined, ['options']);
|
|
44
69
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const page = sourceFile('source-table.html', tableSourcePage(data));
|
|
51
|
-
const submitted = submitPage({ dir: ticketDir('source-table'), sourceFile: page, dialect: 'html' });
|
|
52
|
-
const config = submitted.manifest.slots[0].config;
|
|
53
|
-
assert.deepEqual(config.rows, [{ id: 'one', cells: { name: 'One' } }]);
|
|
54
|
-
assert.equal('source' in config, false);
|
|
55
|
-
});
|
|
56
|
-
test('invalid source data fails before a ticket is written', () => {
|
|
57
|
-
const cases = [
|
|
58
|
-
['missing', join(sources, 'missing.json'), /does not exist/],
|
|
59
|
-
['invalid-json', sourceFile('invalid.json', '{nope'), /not valid JSON/],
|
|
60
|
-
['invalid-shape', sourceFile('invalid-shape.json', JSON.stringify([{ cells: { name: 'Missing id' } }])), /source data is invalid/],
|
|
61
|
-
];
|
|
62
|
-
for (const [id, data, expected] of cases) {
|
|
63
|
-
const page = sourceFile(`${id}.html`, tableSourcePage(data));
|
|
64
|
-
const dir = ticketDir(`source-${id}`);
|
|
65
|
-
assert.throws(() => preparePage({ sourceFile: page, dialect: 'html' }), expected);
|
|
66
|
-
assert.throws(() => submitPage({ dir, sourceFile: page, dialect: 'html' }), expected);
|
|
67
|
-
assert.equal(existsSync(dir), false, `${id} source validation writes no ticket`);
|
|
68
|
-
}
|
|
70
|
+
test('an HTML page derives a slotless manifest from declared metadata', () => {
|
|
71
|
+
const report = sourceFile('report.html', '<!doctype html><html><head><meta content="Summary" name="description"><title>Report</title></head><body>done</body></html>');
|
|
72
|
+
const prepared = preparePage({ sourceFile: report });
|
|
73
|
+
assert.equal(prepared.module, undefined);
|
|
74
|
+
assert.deepEqual(prepared.manifest, { schema: 'crtr.page/v2', dialect: 'html', title: 'Report', subtitle: 'Summary', delivery: { placement: 'inline', inbox: false, reply: false }, document: 'page.html', steps: 1, slots: [], source: prepared.manifest.source });
|
|
69
75
|
});
|
|
70
76
|
test('progress round-trips through validation and rejects malformed saved work', () => {
|
|
71
77
|
const dir = ticketDir('ask');
|
|
@@ -76,6 +82,11 @@ test('progress round-trips through validation and rejects malformed saved work',
|
|
|
76
82
|
assert.throws(() => writePageProgress(dir, { unknown: { selectedOptionIds: [], comments: [] } }), /unknown/);
|
|
77
83
|
atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: 'not an object' }, savedAt: new Date().toISOString() });
|
|
78
84
|
assert.throws(() => readPageProgress(dir), /malformed/, 'malformed progress is an error, never silently discarded');
|
|
85
|
+
// Saved work can outlive the slot it answered, because the agent may replace
|
|
86
|
+
// the page under it. The stale entry is dropped on read so the ticket stays
|
|
87
|
+
// readable; every entry the current page still declares hydrates.
|
|
88
|
+
atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: { selectedOptionIds: ['canary'], comments: [] }, retired: { selectedOptionIds: ['gone'], comments: [] } }, savedAt: new Date().toISOString() });
|
|
89
|
+
assert.deepEqual(readPageProgress(dir), { rollout: { selectedOptionIds: ['canary'], comments: [] } });
|
|
79
90
|
writePageProgress(dir, { rollout: { selectedOptionIds: ['canary'], comments: [] } });
|
|
80
91
|
});
|
|
81
92
|
test('readPageTicket derives pending, resolved and canceled state and carries the document', () => {
|
|
@@ -101,20 +112,22 @@ test('readPageTicket derives pending, resolved and canceled state and carries th
|
|
|
101
112
|
assert.equal(resolved.state, 'resolved');
|
|
102
113
|
assert.equal(resolved.progress, null);
|
|
103
114
|
assert.equal(scanInbox().some((item) => item.id === 'ask'), false, 'a resolved page leaves the pending list');
|
|
104
|
-
const canceledDir = submit('canceled', askPage, '
|
|
115
|
+
const canceledDir = submit('canceled', askPage, '2026-01-03T00:00:00.000Z');
|
|
105
116
|
cancelTicket(canceledDir, { reason: 'no longer needed' });
|
|
106
117
|
assert.equal(readPageTicket(canceledDir).state, 'canceled');
|
|
107
118
|
});
|
|
108
|
-
test('page scan derives response expectation from
|
|
109
|
-
const dir = submit('display', displayPage, '
|
|
119
|
+
test('page scan derives response expectation from manifest delivery', () => {
|
|
120
|
+
const dir = submit('display', displayPage, '2026-01-04T00:00:00.000Z');
|
|
110
121
|
const ticket = readPageTicket(dir);
|
|
111
|
-
assert.equal(ticket.manifest.dialect, '
|
|
122
|
+
assert.equal(ticket.manifest.dialect, 'jsx');
|
|
112
123
|
assert.equal(ticket.manifest.slots.length, 0);
|
|
113
124
|
const display = scanInbox().find((item) => item.id === 'display');
|
|
114
|
-
assert.equal(display?.kind === 'page' ? display.awaitsResponse : undefined, true, '
|
|
115
|
-
submit('
|
|
116
|
-
const
|
|
117
|
-
assert.equal(
|
|
125
|
+
assert.equal(display?.kind === 'page' ? display.awaitsResponse : undefined, true, 'a reply-bearing page waits even with no response-bearing slots');
|
|
126
|
+
const announcementDir = submit('announcement-with-options', askPage, '2026-01-04T00:00:01.000Z', 'node-1', false);
|
|
127
|
+
const announcement = scanInbox().find((item) => item.id === 'announcement-with-options');
|
|
128
|
+
assert.equal(announcement, undefined, 'a passive page stays out of the inbox');
|
|
129
|
+
assert.equal(readPageTicket(announcementDir).state, 'passive');
|
|
130
|
+
assert.throws(() => takeoverAndCompletePage(announcementDir, {}, { host: 'crtrd', pid: process.pid }), /page_has_no_response_lifecycle/);
|
|
118
131
|
const acknowledged = takeoverAndCompletePage(dir, {}, { host: 'crtrd', pid: process.pid });
|
|
119
132
|
assert.equal(acknowledged.won, true);
|
|
120
133
|
const written = JSON.parse(readFileSync(responsePath(dir), 'utf8'));
|
|
@@ -123,11 +136,11 @@ test('page scan derives response expectation from the ticket mode', () => {
|
|
|
123
136
|
assert.equal(written['summary'], '');
|
|
124
137
|
});
|
|
125
138
|
test('scanPageHistory returns every page of one node, oldest first, regardless of terminal file', () => {
|
|
126
|
-
submit('older', askPage, '
|
|
127
|
-
submit('other-node', askPage, '
|
|
139
|
+
submit('older', askPage, '2026-01-01T00:00:00.000Z');
|
|
140
|
+
submit('other-node', askPage, '2026-01-02T00:00:00.000Z', 'node-2');
|
|
128
141
|
const history = scanPageHistory('node-1');
|
|
129
|
-
assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', '
|
|
130
|
-
assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', '
|
|
142
|
+
assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', 'announcement-with-options', 'ask']);
|
|
143
|
+
assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', 'passive', 'resolved']);
|
|
131
144
|
assert.deepEqual(scanPageHistory('node-2').map((entry) => entry.id), ['other-node']);
|
|
132
145
|
assert.deepEqual(scanPageHistory('node-missing'), []);
|
|
133
146
|
});
|
|
@@ -1,54 +1,51 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
2
|
import test from 'node:test';
|
|
6
|
-
import {
|
|
3
|
+
import { derivePageManifestFromDocument } from '../page.js';
|
|
7
4
|
import { isResponseBearingSlot, validatePageManifest, validatePageResponses, validatePartialPageResponses } from '../page-schema.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const path = join(dir, name);
|
|
12
|
-
writeFileSync(path, source);
|
|
13
|
-
run(path);
|
|
14
|
-
}
|
|
15
|
-
finally {
|
|
16
|
-
rmSync(dir, { recursive: true, force: true });
|
|
17
|
-
}
|
|
5
|
+
const OPTIONS = '<UserQuestion id="choice" label="Choose an option" body="One of these ships tonight." mode="single" options={[{ id: "yes", label: "Yes" }]} />';
|
|
6
|
+
function page(body, props = 'title="Test"') {
|
|
7
|
+
return `export default function Page_() {\n return (\n <Page ${props}>\n ${body}\n </Page>\n );\n}\n`;
|
|
18
8
|
}
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
withSource('input.html', `${head}${body}${htmlTail}`, (path) => { manifest = derivePageManifest(path, 'html'); });
|
|
25
|
-
return manifest;
|
|
26
|
-
}
|
|
27
|
-
function expectHtmlError(body, pattern, head = htmlHead) {
|
|
28
|
-
withSource('input.html', `${head}${body}${htmlTail}`, (path) => assert.throws(() => derivePageManifest(path, 'html'), pattern));
|
|
29
|
-
}
|
|
30
|
-
test('html dialect derives document-order slots and wrapper steps', () => {
|
|
31
|
-
const body = `<crtr-pages><crtr-page><section><crtr-options id="choice">${optionsScript}</crtr-options></section></crtr-page><crtr-page><crtr-chart><script type="application/json">{"kind":"bar","x":"day","y":[{"field":"count"}]}</script></crtr-chart></crtr-page></crtr-pages>`;
|
|
32
|
-
const manifest = deriveHtml(body, '<!doctype html><html placement="inline"><head><meta name="crtr-page-title" content="HTML"><meta name="crtr-page-subtitle" content="Two steps"><meta name="crtr-page-placement" content="inline"></head><body>');
|
|
33
|
-
assert.equal(manifest.title, 'HTML');
|
|
34
|
-
assert.equal(manifest.placement, 'inline');
|
|
9
|
+
test('jsx pages derive page props, steps, and document-order slots', () => {
|
|
10
|
+
const manifest = derivePageManifestFromDocument(page(`<Step>${OPTIONS}</Step>\n <Step><Chart kind="bar" x="day" y={[{ field: "count" }]} /></Step>`, 'title="JSX" subtitle="Two steps"'));
|
|
11
|
+
assert.equal(manifest.title, 'JSX');
|
|
12
|
+
assert.equal(manifest.subtitle, 'Two steps');
|
|
13
|
+
assert.deepEqual(manifest.delivery, { placement: 'inline', inbox: false, reply: false });
|
|
35
14
|
assert.equal(manifest.steps, 2);
|
|
36
|
-
assert.deepEqual(manifest.slots.map(({ kind, step }) => ({ kind, step })), [
|
|
15
|
+
assert.deepEqual(manifest.slots.map(({ kind, step, id }) => ({ kind, step, id })), [
|
|
16
|
+
{ kind: 'options', step: 0, id: 'choice' },
|
|
17
|
+
{ kind: 'chart', step: 1, id: undefined },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
test('a page without steps is one step, and a slot config computed at render time still reaches the manifest', () => {
|
|
21
|
+
const manifest = derivePageManifestFromDocument(page('<UserText id="note" label="Add a note" initialText="" />'));
|
|
22
|
+
assert.equal(manifest.steps, 1);
|
|
23
|
+
assert.deepEqual(manifest.slots, [{ id: 'note', kind: 'text', step: 0, config: { label: 'Add a note', initialText: '' } }]);
|
|
24
|
+
// The manifest is harvested by rendering the page, so mapped data, helper components,
|
|
25
|
+
// and hook state are all present; handlers are dropped because a manifest is JSON.
|
|
26
|
+
const computed = derivePageManifestFromDocument(`const ROWS = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
|
|
27
|
+
function Pick({ rows }) { return <UserQuestion id="picks" label="Pick" body="Rows come from data." mode="single" options={rows.map((r) => ({ id: r.id, label: r.label }))} onSelect={() => 1} />; }
|
|
28
|
+
export default function Computed() { const [title] = React.useState('Computed'); return <Page title={title}><Pick rows={ROWS} /></Page>; }
|
|
29
|
+
`);
|
|
30
|
+
assert.equal(computed.title, 'Computed');
|
|
31
|
+
assert.deepEqual(computed.slots, [{ id: 'picks', kind: 'options', step: 0, config: { label: 'Pick', body: 'Rows come from data.', mode: 'single', options: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] } }]);
|
|
32
|
+
});
|
|
33
|
+
test('a page that cannot render is rejected with the reason', () => {
|
|
34
|
+
assert.throws(() => derivePageManifestFromDocument(page(OPTIONS, 'title=""')), /nonempty title prop/);
|
|
35
|
+
assert.throws(() => derivePageManifestFromDocument(page(OPTIONS, 'title="T" placement="drawer"')), /placement is not authored/);
|
|
36
|
+
assert.throws(() => derivePageManifestFromDocument('function Page_() { return <Page title="T" />; }\n'), /must `export default` a component function/);
|
|
37
|
+
assert.throws(() => derivePageManifestFromDocument('export default function Page_() { return (<><Page title="A" /><Page title="B" /></>); }'), /exactly one is allowed/);
|
|
38
|
+
assert.throws(() => derivePageManifestFromDocument('export default () => <Page title="T"><Mystery /></Page>;'), /Mystery is not defined/);
|
|
39
|
+
assert.throws(() => derivePageManifestFromDocument('export default async () => <Page title="T" />;'), /async/);
|
|
40
|
+
});
|
|
41
|
+
test('module plumbing is rejected with the in-scope teaching', () => {
|
|
42
|
+
const importing = `import React from 'react';\n${page(OPTIONS)}`;
|
|
43
|
+
assert.throws(() => derivePageManifestFromDocument(importing), /page source uses import: .*already in scope/s);
|
|
44
|
+
const requiring = `const x = require('./data.json');\n${page(OPTIONS)}`;
|
|
45
|
+
assert.throws(() => derivePageManifestFromDocument(requiring), /page source uses require\(\)/);
|
|
37
46
|
});
|
|
38
|
-
test('
|
|
39
|
-
|
|
40
|
-
test('html wrapper rejects empty crtr-pages', () => expectHtmlError('<crtr-pages></crtr-pages>', /empty <crtr-pages>/));
|
|
41
|
-
test('html wrapper rejects a response-bearing element outside a step with the fix', () => expectHtmlError(`<crtr-options id="choice">${optionsScript}</crtr-options>`, /response-bearing element <crtr-options>[\s\S]*<crtr-pages><crtr-page>…<\/crtr-page><\/crtr-pages>/));
|
|
42
|
-
test('html wrapper rejects nested crtr-pages', () => expectHtmlError('<crtr-pages><crtr-page><crtr-pages><crtr-page></crtr-page></crtr-pages></crtr-page></crtr-pages>', /nested <crtr-pages>/));
|
|
43
|
-
test('html metadata enforces cardinality, known names, and placement content', () => {
|
|
44
|
-
const duplicateTitle = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-title" content="B"></head><body>';
|
|
45
|
-
const duplicateSubtitle = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-subtitle" content="A"><meta name="crtr-page-subtitle" content="B"></head><body>';
|
|
46
|
-
const unknown = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-extra" content="x"></head><body>';
|
|
47
|
-
const badPlacement = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-placement" content="drawer"></head><body>';
|
|
48
|
-
expectHtmlError('', /Expected exactly one <meta name="crtr-page-title">, found 2/, duplicateTitle);
|
|
49
|
-
expectHtmlError('', /Expected at most one <meta name="crtr-page-subtitle">, found 2/, duplicateSubtitle);
|
|
50
|
-
expectHtmlError('', /unknown page metadata element <meta name="crtr-page-extra">/, unknown);
|
|
51
|
-
expectHtmlError('', /content must be exactly inline or panel/, badPlacement);
|
|
47
|
+
test('a page that does not compile is reported with its location', () => {
|
|
48
|
+
assert.throws(() => derivePageManifestFromDocument('export default function Page_() { return (<Page title="Broken">; }'), /page does not compile: .*page\.tsx:\d+:\d+/s);
|
|
52
49
|
});
|
|
53
50
|
test('the response-bearing predicate handles table selection and catalog edges', () => {
|
|
54
51
|
const slot = (kind, config, unvalidated) => ({ kind, step: 0, config, ...(unvalidated ? { unvalidated } : {}) });
|
|
@@ -57,66 +54,27 @@ test('the response-bearing predicate handles table selection and catalog edges',
|
|
|
57
54
|
assert.equal(isResponseBearingSlot(slot('chart', { kind: 'bar', x: 'x', y: [{ field: 'y' }] })), false);
|
|
58
55
|
assert.equal(isResponseBearingSlot(slot('product-kind', {}, true)), true);
|
|
59
56
|
});
|
|
60
|
-
test('
|
|
57
|
+
test('product components are authored under their PascalCase tag', () => {
|
|
61
58
|
const displayComponent = [{ kind: 'nl-thread', display: true }];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
assert.
|
|
66
|
-
assert.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
59
|
+
const manifest = derivePageManifestFromDocument(page('<NlThread messages={[]} />'), displayComponent);
|
|
60
|
+
assert.deepEqual(manifest.slots, [{ kind: 'nl-thread', step: 0, config: { messages: [] }, unvalidated: true, display: true }]);
|
|
61
|
+
assert.deepEqual(validatePageManifest(manifest).slots, manifest.slots);
|
|
62
|
+
assert.equal(isResponseBearingSlot(manifest.slots[0]), false);
|
|
63
|
+
assert.throws(() => derivePageManifestFromDocument(page('<ProductInput />'), [{ kind: 'product-input' }]), /product slot kind "product-input" at slot #0 requires an id prop/);
|
|
64
|
+
});
|
|
65
|
+
test('a question requires its label and body at authoring time', () => {
|
|
66
|
+
assert.throws(() => derivePageManifestFromDocument(page('<UserQuestion id="choice" body="Context." mode="single" options={[{ id: "yes", label: "Yes" }]} />')), /slot "choice" \(options\): label is required/);
|
|
67
|
+
assert.throws(() => derivePageManifestFromDocument(page('<UserQuestion id="choice" label="Choose" mode="single" options={[{ id: "yes", label: "Yes" }]} />')), /slot "choice" \(options\): body is required/);
|
|
72
68
|
});
|
|
73
69
|
test('multi-response pages require a label on every response-bearing slot', () => {
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
assert.throws(() => derivePageManifestFromDocument(page(`<UserText id="first" initialText="" />\n <UserText id="note" label="Add a note" initialText="" />`)), /response-bearing slot "first" requires a nonempty config\.label/);
|
|
71
|
+
assert.throws(() => derivePageManifestFromDocument(page(`${OPTIONS}\n <UserTable id="table" label="Pick a row" columns={[{ id: "name", label: "Name" }]} rows={[{ id: "one", cells: { name: "One" } }]} rowSelect="single" caption="old" />`)), /invalid config for slot "table"/);
|
|
76
72
|
});
|
|
77
73
|
test('final and partial response validation share slot-addressed rules', () => {
|
|
78
|
-
const manifest =
|
|
74
|
+
const manifest = derivePageManifestFromDocument(page(`${OPTIONS}\n <UserText id="note" label="Add a note" initialText="" />`));
|
|
79
75
|
assert.deepEqual(validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), { choice: { selectedOptionIds: ['yes'], comments: [] } });
|
|
80
76
|
assert.throws(() => validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), /missing response-bearing slot id "note"/);
|
|
81
|
-
assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false
|
|
77
|
+
assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false } }), { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false } });
|
|
82
78
|
assert.throws(() => validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['maybe'], comments: [] } }), /unknown option id "maybe"/);
|
|
83
79
|
assert.throws(() => validatePartialPageResponses(manifest, { extra: {} }), /unknown or non-response-bearing slot id "extra"/);
|
|
84
80
|
});
|
|
85
|
-
test('PageSourceProjection rejects changed inline component data', () => {
|
|
86
|
-
const body = '<crtr-table><script type="application/json">{"columns":[{"id":"name","label":"Name"}],"rows":[{"id":"one","cells":{"name":"One"}}]}</script></crtr-table>';
|
|
87
|
-
const manifest = deriveHtml(body);
|
|
88
|
-
const changed = {
|
|
89
|
-
...manifest,
|
|
90
|
-
slots: [{ ...manifest.slots[0], config: { columns: [{ id: 'name', label: 'Name' }], rows: [{ id: 'two', cells: { name: 'Two' } }] } }],
|
|
91
|
-
};
|
|
92
|
-
assert.throws(() => parsePageSource(`${htmlHead}${body}${htmlTail}`, changed), /does not match manifest slot/);
|
|
93
|
-
});
|
|
94
|
-
test('PageSourceProjection preserves legacy markdown replay and interleaves HTML prose', () => {
|
|
95
|
-
const legacySource = `---
|
|
96
|
-
title: Projection
|
|
97
|
-
---
|
|
98
|
-
Leading prose.
|
|
99
|
-
|
|
100
|
-
\`\`\`slot:text
|
|
101
|
-
{"id":"first","label":"First answer","initialText":""}
|
|
102
|
-
\`\`\`
|
|
103
|
-
|
|
104
|
-
Trailing prose.
|
|
105
|
-
`;
|
|
106
|
-
const legacyManifest = {
|
|
107
|
-
schema: 'crtr.page/v1', dialect: 'md', title: 'Projection', placement: 'panel', document: 'page.md', steps: 1,
|
|
108
|
-
slots: [{ id: 'first', kind: 'text', step: 0, config: { label: 'First answer', initialText: '' } }],
|
|
109
|
-
};
|
|
110
|
-
assert.deepEqual(parsePageSource(legacySource, legacyManifest), [
|
|
111
|
-
{ kind: 'prose', markdown: 'Leading prose.' },
|
|
112
|
-
{ kind: 'slot', slotIndex: 0 },
|
|
113
|
-
{ kind: 'prose', markdown: 'Trailing prose.' },
|
|
114
|
-
]);
|
|
115
|
-
const htmlSource = '<!doctype html><html><head><meta name="crtr-page-title" content="Test"></head><body><p>Leading prose.</p><crtr-pages><crtr-page><crtr-text id="first"><script type="application/json">{"label":"First answer","initialText":""}</script></crtr-text><p>Trailing prose.</p></crtr-page></crtr-pages></body></html>';
|
|
116
|
-
const htmlManifest = deriveHtml('<p>Leading prose.</p><crtr-pages><crtr-page><crtr-text id="first"><script type="application/json">{"label":"First answer","initialText":""}</script></crtr-text><p>Trailing prose.</p></crtr-page></crtr-pages>');
|
|
117
|
-
assert.deepEqual(parsePageSource(htmlSource, htmlManifest), [
|
|
118
|
-
{ kind: 'prose', markdown: '<p>Leading prose.</p>' },
|
|
119
|
-
{ kind: 'slot', slotIndex: 0 },
|
|
120
|
-
{ kind: 'prose', markdown: '<p>Trailing prose.</p>' },
|
|
121
|
-
]);
|
|
122
|
-
});
|
|
@@ -12,10 +12,10 @@ const temp = mkdtempSync(join(tmpdir(), 'crtr-inbox-core-'));
|
|
|
12
12
|
process.env['CRTR_HOME'] = temp;
|
|
13
13
|
const source = join(temp, 'source.md');
|
|
14
14
|
writeFileSync(source, '# Source\n');
|
|
15
|
-
const pageSource = join(temp, 'page.
|
|
16
|
-
writeFileSync(pageSource, '
|
|
17
|
-
const page = (id,
|
|
18
|
-
const answer = { go: { text: 'yes', edited: true
|
|
15
|
+
const pageSource = join(temp, 'page.tsx');
|
|
16
|
+
writeFileSync(pageSource, 'export default () => (\n <Page title="A page" subtitle="Go? requires your attention.">\n <UserText id="go" initialText="" />\n </Page>\n);\n');
|
|
17
|
+
const page = (id, emittedAt) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, source: { emittedAt, nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
18
|
+
const answer = { go: { text: 'yes', edited: true } };
|
|
19
19
|
async function waitFor(paths) {
|
|
20
20
|
for (let attempts = 0; !paths.every(existsSync); attempts++) {
|
|
21
21
|
if (attempts > 1_000)
|
|
@@ -32,7 +32,7 @@ assert.equal(existsSync(ticketDir('bad-review')), false, 'invalid submission doe
|
|
|
32
32
|
const prepared = ticketDir('prepared');
|
|
33
33
|
mkdirSync(prepared);
|
|
34
34
|
writeFileSync(join(prepared, 'asset.png'), 'prepared asset');
|
|
35
|
-
const preparedTicket = submitPage({ dir: prepared, sourceFile: pageSource,
|
|
35
|
+
const preparedTicket = submitPage({ dir: prepared, sourceFile: pageSource, source: { emittedAt: '2025-01-02T00:00:00.000Z', nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
36
36
|
assert.equal(preparedTicket.dir, realpathSync(prepared), 'submission accepts a crouter-precreated direct child and colocated assets');
|
|
37
37
|
// Same timestamp is stable by id; progress is resumable work, never a hidden lease.
|
|
38
38
|
atomicWriteJson(progressPath(newPage.dir), { kind: 'page', responses: {}, savedAt: new Date().toISOString() });
|
|
@@ -94,7 +94,7 @@ for (const [index, fixture] of rejectFixtures.entries()) {
|
|
|
94
94
|
const protocolDir = ticketDir('stale-protocol');
|
|
95
95
|
mkdirSync(protocolDir);
|
|
96
96
|
writeFileSync(responsePath(protocolDir), '{}');
|
|
97
|
-
assert.throws(() => submitPage({ dir: protocolDir, sourceFile: pageSource
|
|
97
|
+
assert.throws(() => submitPage({ dir: protocolDir, sourceFile: pageSource }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
|
|
98
98
|
const cancelDir = page('cancel-race', '2025-01-03T00:00:00.000Z').dir;
|
|
99
99
|
const canceled = cancelTicket(cancelDir, { reason: 'stop' });
|
|
100
100
|
assert.equal(canceled.status, 'canceled');
|
|
@@ -26,7 +26,7 @@ function groupLines(group) {
|
|
|
26
26
|
return [`${groupName(group)}: ${group.picked.length === 0 ? 'none' : group.picked.map(item).join(', ')}`];
|
|
27
27
|
}
|
|
28
28
|
function commentLines(comment) {
|
|
29
|
-
const prefix = comment.anchor.kind === 'whole' ? 'Comment: ' : `Comment on ${
|
|
29
|
+
const prefix = comment.anchor.kind === 'whole' ? 'Comment: ' : `Comment on ${inline(comment.anchorLabel)}: `;
|
|
30
30
|
return verbatim(prefix, comment.text);
|
|
31
31
|
}
|
|
32
32
|
function shape(slot) {
|
|
@@ -84,13 +84,13 @@ function slotLines(slot, totalSlots) {
|
|
|
84
84
|
export function renderAnswerText(answer, responsePath) {
|
|
85
85
|
if (answer.kind === 'acknowledgement') {
|
|
86
86
|
return [
|
|
87
|
-
`The
|
|
87
|
+
`The user saw **${answer.title}** and dismissed it. The page asked for nothing, so the acknowledgement is the whole answer.`,
|
|
88
88
|
'',
|
|
89
89
|
`Acknowledged ${answer.completedAt}. Response record: ${responsePath}`,
|
|
90
90
|
].join('\n');
|
|
91
91
|
}
|
|
92
92
|
const answered = answer.slots.filter((slot) => slot.state === 'answered');
|
|
93
|
-
const lines = [`The
|
|
93
|
+
const lines = [`The user answered **${answer.title}**.`];
|
|
94
94
|
for (const slot of answered)
|
|
95
95
|
lines.push('', ...slotLines(slot, answer.slots.length));
|
|
96
96
|
lines.push('', `Answered ${answer.completedAt}. Typed responses: ${responsePath}`);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ProductPageComponents } from '../../types.js';
|
|
2
1
|
import { type CommentAnchor, type PageResponses, type PageSlot } from './page-schema.js';
|
|
3
2
|
export type PageAnswer = {
|
|
4
3
|
title: string;
|
|
@@ -34,16 +33,19 @@ export type PageAnswerGroup = {
|
|
|
34
33
|
label: string;
|
|
35
34
|
}[];
|
|
36
35
|
};
|
|
36
|
+
/** Stored responses predating the `whole` anchor's removal still carry it; answers keep rendering them. */
|
|
37
|
+
export type LegacyCommentAnchor = CommentAnchor | {
|
|
38
|
+
kind: 'whole';
|
|
39
|
+
};
|
|
37
40
|
export type PageAnswerComment = {
|
|
38
|
-
anchor:
|
|
41
|
+
anchor: LegacyCommentAnchor;
|
|
39
42
|
anchorLabel: string;
|
|
40
43
|
text: string;
|
|
41
44
|
};
|
|
42
45
|
export interface DescribePageAnswerOptions {
|
|
43
46
|
completedAt: string;
|
|
44
|
-
productKinds?: ProductPageComponents;
|
|
45
47
|
}
|
|
46
|
-
export declare function answerCommentAnchorLabel(slot: PageSlot, anchor:
|
|
48
|
+
export declare function answerCommentAnchorLabel(slot: PageSlot, anchor: LegacyCommentAnchor): string;
|
|
47
49
|
/** Derive the one shared answer model from the immutable page manifest and a response map. */
|
|
48
50
|
/** `manifest` is unknown deliberately: this boundary validates persisted JSON before deriving. */
|
|
49
51
|
export declare function describePageAnswer(manifest: unknown, responses: PageResponses, opts: DescribePageAnswerOptions): PageAnswer;
|
|
@@ -28,15 +28,9 @@ function slotLabel(slot) {
|
|
|
28
28
|
}
|
|
29
29
|
export function answerCommentAnchorLabel(slot, anchor) {
|
|
30
30
|
if (anchor.kind === 'whole')
|
|
31
|
-
return slotLabel(slot);
|
|
32
|
-
if (anchor.kind === 'range')
|
|
33
|
-
return anchor.quote;
|
|
31
|
+
return slotLabel(slot); // legacy stored data only; new anchors are option/row
|
|
34
32
|
if (anchor.kind === 'option')
|
|
35
33
|
return slot.config.options.find((option) => option.id === anchor.optionId)?.label ?? anchor.optionId;
|
|
36
|
-
if (anchor.kind === 'card')
|
|
37
|
-
return slot.config.cards?.find((card) => card.id === anchor.cardId)?.title ?? anchor.cardId;
|
|
38
|
-
if (anchor.kind === 'column')
|
|
39
|
-
return slot.config.columns.find((column) => column.id === anchor.columnId)?.label ?? anchor.columnId;
|
|
40
34
|
return rowLabel(slot.config, anchor.rowId);
|
|
41
35
|
}
|
|
42
36
|
function describeComments(slot, response) {
|
|
@@ -65,7 +59,7 @@ function describeSlot(slot, response) {
|
|
|
65
59
|
const text = value;
|
|
66
60
|
const written = typeof text.text === 'string' ? text.text : '';
|
|
67
61
|
const singleLine = slot.config.singleLine === true;
|
|
68
|
-
return { ...base, blank: singleLine && written === '', groups: [], text: { value: written, edited: text.edited === true, ...(singleLine ? { singleLine: true } : {}) }, comments:
|
|
62
|
+
return { ...base, blank: singleLine && written === '', groups: [], text: { value: written, edited: text.edited === true, ...(singleLine ? { singleLine: true } : {}) }, comments: [] };
|
|
69
63
|
}
|
|
70
64
|
if (slot.kind === 'table') {
|
|
71
65
|
const config = slot.config;
|
|
@@ -80,14 +74,14 @@ function describeSlot(slot, response) {
|
|
|
80
74
|
const config = slot.config;
|
|
81
75
|
const cards = (config.cards ?? []).map((card) => ({ id: card.id, label: card.title }));
|
|
82
76
|
const group = { group: 'card', mode: config.mode, total: cards.length, picked: picked(strings(value.selectedCardIds), cards) };
|
|
83
|
-
return { ...base, blank: group.picked.length === 0, groups: [group], comments:
|
|
77
|
+
return { ...base, blank: group.picked.length === 0, groups: [group], comments: [] };
|
|
84
78
|
}
|
|
85
79
|
throw new Error(`slot "${slot.id}" kind "${slot.kind}" does not accept a response`);
|
|
86
80
|
}
|
|
87
81
|
/** Derive the one shared answer model from the immutable page manifest and a response map. */
|
|
88
82
|
/** `manifest` is unknown deliberately: this boundary validates persisted JSON before deriving. */
|
|
89
83
|
export function describePageAnswer(manifest, responses, opts) {
|
|
90
|
-
const page = validatePageManifest(manifest
|
|
84
|
+
const page = validatePageManifest(manifest);
|
|
91
85
|
const slots = page.slots.filter(isResponseBearingSlot).map((slot) => describeSlot(slot, hasOwn(responses, slot.id) ? responses[slot.id] : undefined));
|
|
92
86
|
const answered = slots.filter((slot) => slot.state === 'answered');
|
|
93
87
|
return {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { PageComponentRegistration } from '../../types.js';
|
|
2
2
|
export interface ComponentDoc {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/** Registry names this doc covers. A family (Card, CardHeader, …) shares one contract. */
|
|
4
|
+
names: readonly string[];
|
|
5
|
+
/** `structural` builds the page, `slot` is a registry component that gets its own manifest
|
|
6
|
+
* record (and, unless display-only, its own response), `display` is presentation only. */
|
|
7
|
+
group: 'structural' | 'slot' | 'display';
|
|
5
8
|
selectionLine: string;
|
|
6
9
|
contract: string;
|
|
7
10
|
}
|
|
8
11
|
export declare const BUILTIN_COMPONENT_DOCS: readonly ComponentDoc[];
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
export declare
|
|
12
|
-
export declare function
|
|
12
|
+
/** The structural and response-bearing rows — the ones that decide what a page can ask. `human -h` carries these;
|
|
13
|
+
* the display set is one line there and the full catalog is `human components list`. */
|
|
14
|
+
export declare const CORE_COMPONENT_SELECTION_LINES: string[];
|
|
15
|
+
export declare function componentDoc(name: string): ComponentDoc | undefined;
|
|
16
|
+
export declare function componentNames(productComponents: readonly PageComponentRegistration[]): string[];
|
|
13
17
|
export declare function renderComponentCatalog(productComponents: readonly PageComponentRegistration[]): string;
|
|
14
|
-
export declare function renderComponentContract(
|
|
18
|
+
export declare function renderComponentContract(name: string, productComponents: readonly PageComponentRegistration[]): string | undefined;
|