@north-light/crouter 0.3.192 → 0.3.194

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/dist/api/client.d.ts +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +24 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +7 -0
  12. package/dist/builtin-memory/02-lifecycle/01-resident.md +1 -1
  13. package/dist/builtin-memory/internal/examples/imessage-assistant.md +1 -1
  14. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  15. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  20. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  21. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  22. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  23. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  24. package/dist/clients/attach/input/overlay-owner.js +1 -1
  25. package/dist/clients/attach/overlays/auth.js +34 -8
  26. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  27. package/dist/clients/attach/overlays/pickers.js +37 -43
  28. package/dist/clients/attach/viewer.js +643 -620
  29. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  30. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  31. package/dist/clients/inbox/controller.d.ts +2 -3
  32. package/dist/clients/inbox/controller.js +41 -42
  33. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  34. package/dist/clients/inbox/page-adapter.js +54 -0
  35. package/dist/clients/inbox/resolve.d.ts +2 -2
  36. package/dist/clients/inbox/resolve.js +1 -1
  37. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  38. package/dist/clients/inbox/review-visibility.js +1 -1
  39. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  40. package/dist/clients/inbox/tui/ansi.js +2 -2
  41. package/dist/clients/inbox/tui/input.d.ts +1 -3
  42. package/dist/clients/inbox/tui/input.js +242 -553
  43. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  44. package/dist/clients/inbox/tui/panel.js +59 -187
  45. package/dist/clients/inbox/tui/render.d.ts +0 -25
  46. package/dist/clients/inbox/tui/render.js +165 -416
  47. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  48. package/dist/clients/inbox/tui/slots.js +156 -0
  49. package/dist/clients/inbox/tui/types.d.ts +25 -13
  50. package/dist/clients/inbox/tui.js +5 -3
  51. package/dist/commands/attention.js +2 -2
  52. package/dist/commands/human/doc.js +66 -20
  53. package/dist/commands/human/prompts.d.ts +3 -3
  54. package/dist/commands/human/prompts.js +158 -112
  55. package/dist/commands/human/queue.d.ts +0 -1
  56. package/dist/commands/human/queue.js +34 -94
  57. package/dist/commands/human/shared.d.ts +10 -4
  58. package/dist/commands/human/shared.js +62 -13
  59. package/dist/commands/human.js +8 -4
  60. package/dist/commands/memory/read.js +13 -5
  61. package/dist/commands/node/bash.js +18 -30
  62. package/dist/commands/node/lifecycle.js +2 -2
  63. package/dist/commands/surface/node/focus.js +1 -1
  64. package/dist/core/__tests__/human-deliver.test.js +40 -46
  65. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  66. package/dist/core/__tests__/seam/dormancy-release.test.js +64 -10
  67. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  68. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  69. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  70. package/dist/core/auth-interaction.d.ts +2 -2
  71. package/dist/core/bash-jobs.d.ts +43 -0
  72. package/dist/core/bash-jobs.js +96 -2
  73. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  74. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  75. package/dist/core/canvas/attention.js +1 -1
  76. package/dist/core/canvas/browse/app.js +1 -1
  77. package/dist/core/canvas/nav-model.d.ts +1 -1
  78. package/dist/core/canvas/nav-model.js +1 -1
  79. package/dist/core/canvas/remote-canvas-source.js +1 -1
  80. package/dist/core/canvas/types.d.ts +2 -1
  81. package/dist/core/config.js +4 -1
  82. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  83. package/dist/core/human/__tests__/page-render.test.js +49 -0
  84. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  85. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  86. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  87. package/dist/core/human/__tests__/page.test.js +139 -0
  88. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  89. package/dist/core/human/claim.js +2 -2
  90. package/dist/core/human/convention.d.ts +4 -4
  91. package/dist/core/human/convention.js +6 -13
  92. package/dist/core/human/markdown-html.d.ts +8 -0
  93. package/dist/core/human/markdown-html.js +581 -0
  94. package/dist/core/human/page-catalog.d.ts +6 -0
  95. package/dist/core/human/page-catalog.js +43 -0
  96. package/dist/core/human/page-render.d.ts +3 -0
  97. package/dist/core/human/page-render.js +131 -0
  98. package/dist/core/human/page-schema.d.ts +312 -0
  99. package/dist/core/human/page-schema.js +291 -0
  100. package/dist/core/human/page-synth.d.ts +9 -0
  101. package/dist/core/human/page-synth.js +17 -0
  102. package/dist/core/human/page.d.ts +22 -0
  103. package/dist/core/human/page.js +427 -0
  104. package/dist/core/human/review-schema.d.ts +19 -0
  105. package/dist/core/human/review-schema.js +34 -0
  106. package/dist/core/human/scan.d.ts +12 -3
  107. package/dist/core/human/scan.js +69 -26
  108. package/dist/core/human/summary.d.ts +5 -6
  109. package/dist/core/human/summary.js +54 -37
  110. package/dist/core/human/tickets.d.ts +61 -11
  111. package/dist/core/human/tickets.js +100 -57
  112. package/dist/core/human/types.d.ts +21 -66
  113. package/dist/core/human/types.js +1 -5
  114. package/dist/core/inspector/model.d.ts +5 -0
  115. package/dist/core/inspector/model.js +5 -0
  116. package/dist/core/inspector/text.js +2 -2
  117. package/dist/core/inspector/tui.js +10 -3
  118. package/dist/core/keybindings/catalog.d.ts +1 -1
  119. package/dist/core/keybindings/catalog.js +4 -2
  120. package/dist/core/preview-registry.js +0 -1
  121. package/dist/core/review/ticket-filter.d.ts +1 -1
  122. package/dist/core/review/ticket-filter.js +2 -8
  123. package/dist/core/runtime/broker/read-ops.js +3 -0
  124. package/dist/core/runtime/broker.js +19 -10
  125. package/dist/core/runtime/package-health.js +1 -1
  126. package/dist/core/runtime/persona.js +2 -1
  127. package/dist/core/runtime/tmux-bindings.js +8 -1
  128. package/dist/core/termrender/termrender.d.ts +1 -1
  129. package/dist/core/termrender/termrender.js +2 -2
  130. package/dist/core/termrender/version.d.ts +1 -1
  131. package/dist/core/termrender/version.js +1 -1
  132. package/dist/core/tui/page-host.d.ts +1 -1
  133. package/dist/core/tui/page-host.js +2 -2
  134. package/dist/core/user-settings.d.ts +19 -0
  135. package/dist/core/user-settings.js +22 -0
  136. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  137. package/dist/daemon/api/handlers/bash-jobs.js +90 -0
  138. package/dist/daemon/api/handlers/human.js +16 -14
  139. package/dist/daemon/api/handlers/inbox.js +311 -227
  140. package/dist/daemon/api/router.d.ts +5 -1
  141. package/dist/daemon/api/router.js +5 -0
  142. package/dist/daemon/api/server.js +2 -0
  143. package/dist/daemon/cron-run.js +23 -10
  144. package/dist/daemon/human/finish.d.ts +8 -3
  145. package/dist/daemon/human/finish.js +35 -15
  146. package/dist/daemon/reconcilers/broker-supervision.js +7 -1
  147. package/dist/daemon/reconcilers/dormant-inbox.js +8 -0
  148. package/dist/daemon/reconcilers/live-obligation.d.ts +2 -0
  149. package/dist/daemon/reconcilers/live-obligation.js +11 -0
  150. package/dist/pages/bundle.css +1 -0
  151. package/dist/pages/bundle.js +1347 -0
  152. package/dist/pages/comments.d.ts +34 -0
  153. package/dist/pages/comments.js +72 -0
  154. package/dist/pages/elements/cards.d.ts +17 -0
  155. package/dist/pages/elements/cards.js +785 -0
  156. package/dist/pages/elements/chart.d.ts +1 -0
  157. package/dist/pages/elements/chart.js +648 -0
  158. package/dist/pages/elements/options.d.ts +21 -0
  159. package/dist/pages/elements/options.js +604 -0
  160. package/dist/pages/elements/pages.d.ts +24 -0
  161. package/dist/pages/elements/pages.js +355 -0
  162. package/dist/pages/elements/slot.d.ts +21 -0
  163. package/dist/pages/elements/slot.js +105 -0
  164. package/dist/pages/elements/table.d.ts +19 -0
  165. package/dist/pages/elements/table.js +906 -0
  166. package/dist/pages/elements/text.d.ts +25 -0
  167. package/dist/pages/elements/text.js +951 -0
  168. package/dist/pages/entry.d.ts +8 -0
  169. package/dist/pages/entry.js +8 -0
  170. package/dist/pages/host.d.ts +132 -0
  171. package/dist/pages/host.js +149 -0
  172. package/dist/pages/register.d.ts +2 -0
  173. package/dist/pages/register.js +3 -0
  174. package/dist/pages/slot-config.d.ts +36 -0
  175. package/dist/pages/slot-config.js +50 -0
  176. package/dist/pages/types.d.ts +98 -0
  177. package/dist/pages/types.js +23 -0
  178. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -2
  179. package/dist/pi-extensions/canvas-bash-valve.js +25 -7
  180. package/dist/types.d.ts +4 -0
  181. package/dist/types.js +2 -0
  182. package/package.json +8 -8
  183. package/runtime.lock.json +156 -101
  184. package/scripts/install-runtime.mjs +172 -5
  185. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  186. package/dist/clients/inbox/deck-adapter.js +0 -63
  187. package/dist/core/human/deck-factories.d.ts +0 -8
  188. package/dist/core/human/deck-factories.js +0 -15
  189. package/dist/core/human/deck-schema.d.ts +0 -70
  190. package/dist/core/human/deck-schema.js +0 -92
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+ const home = mkdtempSync(join(tmpdir(), 'crtr-page-tickets-'));
7
+ process.env['CRTR_HOME'] = home;
8
+ const { atomicWriteJson, pageManifestPath, pagePath, progressPath, responsePath } = await import('../convention.js');
9
+ const { claimTicket } = await import('../claim.js');
10
+ const { scanInbox, scanPageHistory } = await import('../scan.js');
11
+ const { ticketDir } = await import('../root.js');
12
+ const { cancelTicket, completePage, readPageProgress, readPageTicket, readTicketResult, submitPage, takeoverAndCompletePage, writePageProgress, } = await import('../tickets.js');
13
+ const sources = mkdtempSync(join(tmpdir(), 'crtr-page-sources-'));
14
+ function sourceFile(name, body) {
15
+ const path = join(sources, name);
16
+ writeFileSync(path, body);
17
+ return path;
18
+ }
19
+ const askPage = sourceFile('ask.md', `---
20
+ title: Rollout scope
21
+ subtitle: Pick tonight's rollout
22
+ placement: inline
23
+ ---
24
+
25
+ Which rollout do you want?
26
+
27
+ \`\`\`slot:options
28
+ {"id": "rollout", "mode": "single", "options": [{"id": "canary", "label": "Canary first"}, {"id": "full", "label": "Full"}]}
29
+ \`\`\`
30
+ `);
31
+ const displayPage = sourceFile('display.html', '<!doctype html><html><head><meta name="crtr-page-title" content="Nightly report"></head><body><h1>All green</h1></body></html>');
32
+ function submit(id, source, dialect, blockedSince, nodeId = 'node-1') {
33
+ return submitPage({ dir: ticketDir(id), sourceFile: source, dialect, source: { nodeId, blockedSince } }).dir;
34
+ }
35
+ test('submitPage publishes the authored source, the derived manifest and the blockedSince stamp', () => {
36
+ const submitted = submitPage({ dir: ticketDir('ask'), sourceFile: askPage, dialect: 'md', source: { nodeId: 'node-1' } });
37
+ assert.equal(submitted.kind, 'page');
38
+ assert.equal(submitted.id, 'ask');
39
+ assert.equal(readFileSync(pagePath(submitted.dir, 'md'), 'utf8'), readFileSync(askPage, 'utf8'));
40
+ const manifest = JSON.parse(readFileSync(pageManifestPath(submitted.dir), 'utf8'));
41
+ assert.equal(manifest['schema'], 'crtr.page/v1');
42
+ assert.equal(manifest['document'], 'page.md');
43
+ assert.equal(manifest['placement'], 'inline');
44
+ const source = manifest['source'];
45
+ assert.equal(source.nodeId, 'node-1');
46
+ assert.ok(Date.parse(source.blockedSince) > 0, 'submission stamps blockedSince');
47
+ assert.throws(() => submitPage({ dir: ticketDir('ask'), sourceFile: askPage, dialect: 'md' }), /already exists/);
48
+ const listed = scanInbox().find((item) => item.id === 'ask');
49
+ assert.equal(listed?.kind, 'page');
50
+ assert.equal(listed?.kind === 'page' ? listed.awaitsResponse : undefined, true);
51
+ assert.deepEqual(listed?.kind === 'page' ? listed.slotKinds : undefined, ['options']);
52
+ });
53
+ test('progress round-trips through validation and rejects malformed saved work', () => {
54
+ const dir = ticketDir('ask');
55
+ assert.equal(readPageProgress(dir), null, 'no progress file reads as null');
56
+ writePageProgress(dir, { rollout: { selectedOptionIds: ['canary'], comments: [] } });
57
+ assert.deepEqual(readPageProgress(dir), { rollout: { selectedOptionIds: ['canary'], comments: [] } });
58
+ assert.throws(() => writePageProgress(dir, { rollout: { selectedOptionIds: ['nope'], comments: [] } }), /nope/);
59
+ assert.throws(() => writePageProgress(dir, { unknown: { selectedOptionIds: [], comments: [] } }), /unknown/);
60
+ atomicWriteJson(progressPath(dir), { kind: 'page', responses: { rollout: 'not an object' }, savedAt: new Date().toISOString() });
61
+ assert.throws(() => readPageProgress(dir), /malformed/, 'malformed progress is an error, never silently discarded');
62
+ writePageProgress(dir, { rollout: { selectedOptionIds: ['canary'], comments: [] } });
63
+ });
64
+ test('readPageTicket derives pending, resolved and canceled state and carries the document', () => {
65
+ const dir = ticketDir('ask');
66
+ const pending = readPageTicket(dir);
67
+ assert.equal(pending.state, 'pending');
68
+ assert.equal(pending.manifest.title, 'Rollout scope');
69
+ assert.ok(pending.document.includes('Which rollout do you want?'));
70
+ assert.deepEqual(pending.progress, { rollout: { selectedOptionIds: ['canary'], comments: [] } });
71
+ const claim = claimTicket(dir);
72
+ assert.notEqual(claim, null, 'a page ticket is claimable');
73
+ assert.throws(() => completePage(dir, { rollout: { selectedOptionIds: [], comments: [] } }, claim.token), /rollout/, 'single-select requires one selection');
74
+ const completed = completePage(dir, { rollout: { selectedOptionIds: ['full'], comments: [] }, }, claim.token);
75
+ assert.equal(completed.won, true);
76
+ const written = JSON.parse(readFileSync(responsePath(dir), 'utf8'));
77
+ assert.equal(written['schema'], 'crtr.page-response/v1');
78
+ assert.equal(written['kind'], 'page');
79
+ assert.deepEqual(written['responses'], { rollout: { selectedOptionIds: ['full'], comments: [] } });
80
+ assert.equal(written['summary'], 'rollout: Full');
81
+ assert.equal(readTicketResult(dir)?.kind, 'page', 'the strict decoder accepts the page result');
82
+ assert.equal(existsSync(progressPath(dir)), false, 'completion clears owned work');
83
+ const resolved = readPageTicket(dir);
84
+ assert.equal(resolved.state, 'resolved');
85
+ assert.equal(resolved.progress, null);
86
+ assert.equal(scanInbox().some((item) => item.id === 'ask'), false, 'a resolved page leaves the pending list');
87
+ const canceledDir = submit('canceled', askPage, 'md', '2026-01-03T00:00:00.000Z');
88
+ cancelTicket(canceledDir, { reason: 'no longer needed' });
89
+ assert.equal(readPageTicket(canceledDir).state, 'canceled');
90
+ });
91
+ test('a display page acknowledges with the empty responses map', () => {
92
+ const dir = submit('display', displayPage, 'html', '2026-01-04T00:00:00.000Z');
93
+ const ticket = readPageTicket(dir);
94
+ assert.equal(ticket.manifest.dialect, 'html');
95
+ assert.equal(ticket.manifest.slots.length, 0);
96
+ assert.equal(scanInbox().find((item) => item.id === 'display')?.kind === 'page', true);
97
+ const acknowledged = takeoverAndCompletePage(dir, {}, { host: 'crtrd', pid: process.pid });
98
+ assert.equal(acknowledged.won, true);
99
+ const written = JSON.parse(readFileSync(responsePath(dir), 'utf8'));
100
+ assert.equal(written['schema'], 'crtr.page-response/v1');
101
+ assert.deepEqual(written['responses'], {});
102
+ assert.equal(written['summary'], '');
103
+ });
104
+ test('scanPageHistory returns every page of one node, oldest first, regardless of terminal file', () => {
105
+ submit('older', askPage, 'md', '2026-01-01T00:00:00.000Z');
106
+ submit('other-node', askPage, 'md', '2026-01-02T00:00:00.000Z', 'node-2');
107
+ const history = scanPageHistory('node-1');
108
+ assert.deepEqual(history.map((entry) => entry.id), ['older', 'canceled', 'display', 'ask']);
109
+ assert.deepEqual(history.map((entry) => entry.state), ['pending', 'canceled', 'resolved', 'resolved']);
110
+ assert.deepEqual(scanPageHistory('node-2').map((entry) => entry.id), ['other-node']);
111
+ assert.deepEqual(scanPageHistory('node-missing'), []);
112
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,139 @@
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
+ import test from 'node:test';
6
+ import { derivePageManifest, parsePageSource } from '../page.js';
7
+ import { isResponseBearingSlot, validatePageResponses, validatePartialPageResponses } from '../page-schema.js';
8
+ function withSource(name, source, run) {
9
+ const dir = mkdtempSync(join(tmpdir(), 'crtr-page-test-'));
10
+ try {
11
+ const path = join(dir, name);
12
+ writeFileSync(path, source);
13
+ run(path);
14
+ }
15
+ finally {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ }
18
+ }
19
+ const htmlHead = '<!doctype html><html><head><meta name="crtr-page-title" content="Test"></head><body>';
20
+ const htmlTail = '</body></html>';
21
+ const optionsScript = '<script type="application/json">{"mode":"single","options":[{"id":"yes","label":"Yes"}]}</script>';
22
+ function deriveHtml(body, head = htmlHead) {
23
+ let manifest;
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('md dialect derives a normalized manifest and preserves top-level slot scanning', () => {
31
+ const source = `---
32
+ title: Rollout
33
+ subtitle: Choose carefully
34
+ placement: inline
35
+ ---
36
+
37
+ Before.
38
+
39
+ \`\`\`\`markdown
40
+ \`\`\`slot:text
41
+ {"id":"nested","initialText":"not a slot"}
42
+ \`\`\`
43
+ \`\`\`\`
44
+
45
+ \`\`\`slot:table
46
+ {"columns":[{"id":"day","label":"Day"}],"rows":[{"id":"r1","cells":{"day":"Mon"}}]}
47
+ \`\`\`
48
+
49
+ \`\`\`slot:options
50
+ {"id":"rollout","mode":"single","options":[{"id":"yes","label":"Yes"}]}
51
+ \`\`\`
52
+ `;
53
+ withSource('input.md', source, (path) => {
54
+ const manifest = derivePageManifest(path, 'md');
55
+ // A display-only table's schema defaults must survive the manifest→source round trip.
56
+ assert.deepEqual(parsePageSource(source, manifest).at(-1), { kind: 'slot', slotIndex: 1 });
57
+ assert.equal(manifest.title, 'Rollout');
58
+ assert.equal(manifest.subtitle, 'Choose carefully');
59
+ assert.equal(manifest.placement, 'inline');
60
+ assert.equal(manifest.document, 'page.md');
61
+ assert.equal(manifest.steps, 1);
62
+ assert.deepEqual(manifest.slots.map(({ id, kind, step }) => ({ id, kind, step })), [{ id: undefined, kind: 'table', step: 0 }, { id: 'rollout', kind: 'options', step: 0 }]);
63
+ });
64
+ });
65
+ test('html dialect derives document-order slots and wrapper steps', () => {
66
+ 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>`;
67
+ 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>');
68
+ assert.equal(manifest.title, 'HTML');
69
+ assert.equal(manifest.placement, 'inline');
70
+ assert.equal(manifest.steps, 2);
71
+ assert.deepEqual(manifest.slots.map(({ kind, step }) => ({ kind, step })), [{ kind: 'options', step: 0 }, { kind: 'chart', step: 1 }]);
72
+ });
73
+ test('html wrapper rejects multiple crtr-pages', () => expectHtmlError('<crtr-pages><crtr-page></crtr-page></crtr-pages><crtr-pages><crtr-page></crtr-page></crtr-pages>', /more than one <crtr-pages>/));
74
+ test('html wrapper rejects crtr-page outside crtr-pages', () => expectHtmlError('<crtr-page></crtr-page>', /<crtr-page> outside <crtr-pages>/));
75
+ test('html wrapper rejects empty crtr-pages', () => expectHtmlError('<crtr-pages></crtr-pages>', /empty <crtr-pages>/));
76
+ 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>/));
77
+ 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>/));
78
+ test('md front matter names unknown, empty-title, and placement errors', () => {
79
+ for (const [source, pattern] of [
80
+ ['---\ntitle: T\nother: no\n---\n', /unknown page front matter key "other"/],
81
+ ['---\ntitle: \n---\n', /title is required and must be nonempty/],
82
+ ['---\ntitle: T\nplacement: sidebar\n---\n', /placement must be exactly inline or panel/],
83
+ ])
84
+ withSource('input.md', source, (path) => assert.throws(() => derivePageManifest(path, 'md'), pattern));
85
+ });
86
+ test('html metadata enforces cardinality, known names, and placement content', () => {
87
+ const duplicateTitle = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-title" content="B"></head><body>';
88
+ 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>';
89
+ const unknown = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-extra" content="x"></head><body>';
90
+ const badPlacement = '<!doctype html><html><head><meta name="crtr-page-title" content="A"><meta name="crtr-page-placement" content="drawer"></head><body>';
91
+ expectHtmlError('', /Expected exactly one <meta name="crtr-page-title">, found 2/, duplicateTitle);
92
+ expectHtmlError('', /Expected at most one <meta name="crtr-page-subtitle">, found 2/, duplicateSubtitle);
93
+ expectHtmlError('', /unknown page metadata element <meta name="crtr-page-extra">/, unknown);
94
+ expectHtmlError('', /content must be exactly inline or panel/, badPlacement);
95
+ });
96
+ test('the response-bearing predicate handles table selection and catalog edges', () => {
97
+ const slot = (kind, config, unvalidated) => ({ kind, step: 0, config, ...(unvalidated ? { unvalidated } : {}) });
98
+ assert.equal(isResponseBearingSlot(slot('table', { columns: [{ id: 'c', label: 'C' }], rowSelect: 'none', columnSelect: 'none' })), false);
99
+ assert.equal(isResponseBearingSlot(slot('table', { columns: [{ id: 'c', label: 'C' }], rowSelect: 'single', columnSelect: 'none' })), true);
100
+ assert.equal(isResponseBearingSlot(slot('chart', { kind: 'bar', x: 'x', y: [{ field: 'y' }] })), false);
101
+ assert.equal(isResponseBearingSlot(slot('product-kind', {}, true)), true);
102
+ });
103
+ test('final and partial response validation share slot-addressed rules', () => {
104
+ const manifest = deriveHtml(`<crtr-pages><crtr-page><crtr-options id="choice">${optionsScript}</crtr-options><crtr-text id="note"><script type="application/json">{"initialText":""}</script></crtr-text></crtr-page></crtr-pages>`);
105
+ assert.deepEqual(validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), { choice: { selectedOptionIds: ['yes'], comments: [] } });
106
+ assert.throws(() => validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] } }), /missing response-bearing slot id "note"/);
107
+ assert.deepEqual(validatePageResponses(manifest, { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false, comments: [] } }), { choice: { selectedOptionIds: ['yes'], comments: [] }, note: { text: '', edited: false, comments: [] } });
108
+ assert.throws(() => validatePartialPageResponses(manifest, { choice: { selectedOptionIds: ['maybe'], comments: [] } }), /unknown option id "maybe"/);
109
+ assert.throws(() => validatePartialPageResponses(manifest, { extra: {} }), /unknown or non-response-bearing slot id "extra"/);
110
+ });
111
+ test('PageSourceProjection associates leading, interstitial, and trailing prose', () => {
112
+ const source = `---
113
+ title: Projection
114
+ ---
115
+ Leading prose.
116
+
117
+ \`\`\`slot:text
118
+ {"id":"first","initialText":""}
119
+ \`\`\`
120
+
121
+ Interstitial prose.
122
+
123
+ \`\`\`slot:text
124
+ {"id":"second","initialText":""}
125
+ \`\`\`
126
+
127
+ Trailing prose.
128
+ `;
129
+ withSource('input.md', source, (path) => {
130
+ const manifest = derivePageManifest(path, 'md');
131
+ assert.deepEqual(parsePageSource(source, manifest), [
132
+ { kind: 'prose', markdown: 'Leading prose.' },
133
+ { kind: 'slot', slotIndex: 0 },
134
+ { kind: 'prose', markdown: 'Interstitial prose.' },
135
+ { kind: 'slot', slotIndex: 1 },
136
+ { kind: 'prose', markdown: 'Trailing prose.' },
137
+ ]);
138
+ });
139
+ });
@@ -3,19 +3,29 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync,
3
3
  import { spawn } from 'node:child_process';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { submitDeck, submitReview, completeDeck, cancelTicket, readTicketResult } from '../../tickets.js';
6
+ import { submitPage, submitReview, completePage, cancelTicket, readTicketResult } from '../../tickets.js';
7
7
  import { claimTicket } from '../../claim.js';
8
8
  import { scanInbox } from '../../scan.js';
9
9
  import { atomicWriteJson, progressPath, responsePath } from '../../convention.js';
10
- import { validateDeck } from '../../deck-schema.js';
11
10
  import { ticketDir } from '../../root.js';
12
11
  const temp = mkdtempSync(join(tmpdir(), 'crtr-inbox-core-'));
13
12
  process.env['CRTR_HOME'] = temp;
14
13
  const source = join(temp, 'source.md');
15
14
  writeFileSync(source, '# Source\n');
16
- const deck = (blockedSince) => ({ title: 'A deck', source: { blockedSince, nodeId: 'node-1' }, interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', options: [{ id: 'yes', label: 'Yes' }] }] });
17
- assert.throws(() => validateDeck({ interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', options: [] }] }), /title/, 'a deck must carry its own explicit inbox title');
18
- assert.throws(() => validateDeck({ title: 'Missing subtitle', interactions: [{ id: 'go', title: 'Go?', options: [] }] }), /subtitle/, 'a deck interaction without an authored subtitle is invalid');
15
+ const pageSource = join(temp, 'page.md');
16
+ writeFileSync(pageSource, [
17
+ '---',
18
+ 'title: A page',
19
+ 'subtitle: Go? requires your attention.',
20
+ '---',
21
+ '',
22
+ '```slot:text',
23
+ '{ "id": "go", "initialText": "", "editable": true }',
24
+ '```',
25
+ '',
26
+ ].join('\n'));
27
+ const page = (id, blockedSince) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, dialect: 'md', source: { blockedSince, nodeId: 'node-1' } });
28
+ const answer = { go: { text: 'yes', edited: true, comments: [] } };
19
29
  async function waitFor(paths) {
20
30
  for (let attempts = 0; !paths.every(existsSync); attempts++) {
21
31
  if (attempts > 1_000)
@@ -23,60 +33,60 @@ async function waitFor(paths) {
23
33
  await new Promise((resolvePromise) => setTimeout(resolvePromise, 1));
24
34
  }
25
35
  }
26
- const oldDeck = submitDeck({ dir: ticketDir('z-last'), deck: deck('2025-01-01T00:00:00.000Z') });
27
- const newDeck = submitDeck({ dir: ticketDir('a-first'), deck: deck('2025-01-02T00:00:00.000Z') });
36
+ page('z-last', '2025-01-01T00:00:00.000Z');
37
+ const newPage = page('a-first', '2025-01-02T00:00:00.000Z');
28
38
  const review = submitReview({ dir: ticketDir('review'), review: { file: source, title: 'Review source', subtitle: 'Review findings require your decision.', source: { nodeId: 'node-1' }, blockedSince: '2025-01-02T00:00:00.000Z' } });
29
39
  assert.throws(() => submitReview({ dir: ticketDir('missing-review-subtitle'), review: { file: source, title: 'Missing subtitle', source: {} } }), /subtitle/, 'a review without an authored subtitle is invalid');
30
40
  assert.throws(() => submitReview({ dir: ticketDir('bad-review'), review: { file: join(temp, 'missing.md'), title: 'Bad', subtitle: 'This invalid review must be rejected.', source: {} } }), /existing absolute path/);
31
41
  assert.equal(existsSync(ticketDir('bad-review')), false, 'invalid submission does not strand an id');
32
42
  const prepared = ticketDir('prepared');
33
43
  mkdirSync(prepared);
34
- writeFileSync(join(prepared, 'body.md'), 'prepared body');
35
- const preparedTicket = submitDeck({ dir: prepared, deck: { ...deck('2025-01-02T00:00:00.000Z'), interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', bodyPath: 'body.md', options: [{ id: 'yes', label: 'Yes' }] }] } });
44
+ writeFileSync(join(prepared, 'asset.png'), 'prepared asset');
45
+ const preparedTicket = submitPage({ dir: prepared, sourceFile: pageSource, dialect: 'md', source: { blockedSince: '2025-01-02T00:00:00.000Z', nodeId: 'node-1' } });
36
46
  assert.equal(preparedTicket.dir, realpathSync(prepared), 'submission accepts a crouter-precreated direct child and colocated assets');
37
47
  // Same timestamp is stable by id; progress is resumable work, never a hidden lease.
38
- atomicWriteJson(progressPath(newDeck.dir), { kind: 'deck', responses: [], savedAt: new Date().toISOString() });
48
+ atomicWriteJson(progressPath(newPage.dir), { kind: 'page', responses: {}, savedAt: new Date().toISOString() });
39
49
  const scanned = scanInbox();
40
50
  assert.deepEqual(scanned.map((item) => item.id), ['a-first', 'prepared', 'review', 'z-last']);
41
51
  assert.equal(scanned.find((item) => item.id === 'a-first')?.claim, undefined);
42
52
  assert.equal(scanned.find((item) => item.id === 'review')?.kind, 'review');
43
53
  assert.equal(scanned.find((item) => item.id === 'review')?.subtitle, 'Review findings require your decision.', 'review scanning exposes its required subtitle');
44
- const firstClaim = claimTicket(newDeck.dir);
54
+ const firstClaim = claimTicket(newPage.dir);
45
55
  assert.notEqual(firstClaim, null);
46
- assert.equal(claimTicket(newDeck.dir), null, 'a second controller cannot claim a live ticket');
56
+ assert.equal(claimTicket(newPage.dir), null, 'a second controller cannot claim a live ticket');
47
57
  // A separate process writes a genuine remote claim whose heartbeat is already
48
58
  // stale, then exits; this process recovers it — cross-process stale recovery.
49
- const staleDir = submitDeck({ dir: ticketDir('stale'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
59
+ const staleDir = page('stale', '2025-01-03T00:00:00.000Z').dir;
50
60
  const staleClaimReady = join(temp, 'stale-claim-ready');
51
61
  await new Promise((resolveWorker, rejectWorker) => spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-stale-claim-worker.ts', staleDir, staleClaimReady], { cwd: process.cwd(), stdio: ['ignore', 'ignore', 'inherit'] }).once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error('stale-claim worker failed'))));
52
62
  assert.notEqual(JSON.parse(readFileSync(staleClaimReady, 'utf8')), null, 'a separate process wrote a stale remote claim on disk');
53
63
  assert.notEqual(claimTicket(staleDir), null, 'a remote claim written by another process recovers once its heartbeat has expired');
54
- const malformedDir = submitDeck({ dir: ticketDir('malformed-claim'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
64
+ const malformedDir = page('malformed-claim', '2025-01-03T00:00:00.000Z').dir;
55
65
  writeFileSync(join(malformedDir, 'claim.json'), '{');
56
66
  assert.notEqual(claimTicket(malformedDir), null, 'partial claim crash artifact recovers');
57
- const final = completeDeck(newDeck.dir, [{ id: 'go', selectedOptionId: 'yes' }], firstClaim.token);
67
+ const final = completePage(newPage.dir, answer, firstClaim.token);
58
68
  assert.equal(final.won, true);
59
- assert.equal(readTicketResult(newDeck.dir)?.kind, 'deck');
60
- assert.equal(claimTicket(newDeck.dir), null, 'terminal tickets cannot be claimed');
69
+ assert.equal(readTicketResult(newPage.dir)?.kind, 'page');
70
+ assert.equal(claimTicket(newPage.dir), null, 'terminal tickets cannot be claimed');
61
71
  const exactResults = [
62
- { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: '2025-01-04T00:00:00.000Z' },
72
+ { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '', completedAt: '2025-01-04T00:00:00.000Z' },
63
73
  { schema: 'humanloop.review-response/v1', kind: 'review', result: { file: source, submitted: true, approved: true, comments: [], submittedAt: '2025-01-04T00:00:00.000Z', savedAt: '2025-01-04T00:00:00.000Z', reviewId: 'review-1', changed: false, baselineSha256: 'baseline', approvedSha256: 'approved', companionNodeId: 'companion-1', commentsTotal: 0, commentsUnresolved: 0 }, completedAt: '2025-01-04T00:00:00.000Z' },
64
74
  { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: '2025-01-04T00:00:00.000Z', reason: 'stop', actor: 'test' },
65
75
  ];
66
76
  for (const [index, result] of exactResults.entries()) {
67
- const dir = submitDeck({ dir: ticketDir(`strict-${index}`), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
77
+ const dir = page(`strict-${index}`, '2025-01-03T00:00:00.000Z').dir;
68
78
  writeFileSync(responsePath(dir), JSON.stringify(result));
69
79
  assert.equal(readTicketResult(dir)?.kind, result.kind, `strict decoder accepts exact ${result.kind} result`);
70
80
  }
71
81
  const validFeedback = exactResults[1].result;
72
82
  const iso = '2025-01-04T00:00:00.000Z';
73
83
  const rejectFixtures = [
74
- { why: 'a wrong kind for a known schema', result: { schema: 'humanloop.response/v2', kind: 'review', responses: [], summary: '', completedAt: iso } },
75
- { why: 'an unknown schema id', result: { schema: 'humanloop.response/v9', kind: 'deck', responses: [], summary: '', completedAt: iso } },
76
- { why: 'a deck missing its required completedAt', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '' } },
77
- { why: 'a deck with a non-ISO completedAt', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: 'now' } },
78
- { why: 'a deck with an unrecognized nested response field', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [{ id: 'go', bogus: true }], summary: '', completedAt: iso } },
79
- { why: 'a deck with an extra top-level field', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: iso, extra: true } },
84
+ { why: 'a wrong kind for a known schema', result: { schema: 'crtr.page-response/v1', kind: 'review', responses: {}, summary: '', completedAt: iso } },
85
+ { why: 'an unknown schema id', result: { schema: 'crtr.page-response/v9', kind: 'page', responses: {}, summary: '', completedAt: iso } },
86
+ { why: 'a page missing its required completedAt', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '' } },
87
+ { why: 'a page with a non-ISO completedAt', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '', completedAt: 'now' } },
88
+ { why: 'a page whose responses are not a slot-keyed map', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: [{ id: 'go' }], summary: '', completedAt: iso } },
89
+ { why: 'a page with an extra top-level field', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '', completedAt: iso, extra: true } },
80
90
  { why: 'a review missing its required result', result: { schema: 'humanloop.review-response/v1', kind: 'review', completedAt: iso } },
81
91
  { why: 'a review whose nested result is not submitted', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, submitted: false }, completedAt: iso } },
82
92
  { why: 'a review with incomplete comment columns', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, approved: true, comments: [{ id: 'c1', line: 1, endLine: 1, colStart: 1, lineText: '# Source', comment: 'x', createdAt: iso }] }, completedAt: iso } },
@@ -87,29 +97,29 @@ const rejectFixtures = [
87
97
  { why: 'a canceled result with an extra top-level field', result: { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: iso, extra: true } },
88
98
  ];
89
99
  for (const [index, fixture] of rejectFixtures.entries()) {
90
- const dir = submitDeck({ dir: ticketDir(`strict-reject-${index}`), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
100
+ const dir = page(`strict-reject-${index}`, '2025-01-03T00:00:00.000Z').dir;
91
101
  writeFileSync(responsePath(dir), JSON.stringify(fixture.result));
92
102
  assert.equal(readTicketResult(dir), null, `strict decoder rejects ${fixture.why}`);
93
103
  }
94
104
  const protocolDir = ticketDir('stale-protocol');
95
105
  mkdirSync(protocolDir);
96
106
  writeFileSync(responsePath(protocolDir), '{}');
97
- assert.throws(() => submitDeck({ dir: protocolDir, deck: deck('2025-01-03T00:00:00.000Z') }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
98
- const cancelDir = submitDeck({ dir: ticketDir('cancel-race'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
107
+ assert.throws(() => submitPage({ dir: protocolDir, sourceFile: pageSource, dialect: 'md' }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
108
+ const cancelDir = page('cancel-race', '2025-01-03T00:00:00.000Z').dir;
99
109
  const canceled = cancelTicket(cancelDir, { reason: 'stop' });
100
110
  assert.equal(canceled.status, 'canceled');
101
111
  assert.equal(cancelTicket(cancelDir).status, 'already_resolved', 'first final writer wins');
102
- const finalRaceDir = submitDeck({ dir: ticketDir('final-race'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
112
+ const finalRaceDir = page('final-race', '2025-01-03T00:00:00.000Z').dir;
103
113
  const finalRaceClaim = claimTicket(finalRaceDir);
104
114
  const finalBarrier = join(temp, 'final-race-barrier');
105
115
  const finalReady = join(temp, 'final-race-ready');
106
116
  const cancelWorker = spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-cancel-worker.ts', finalRaceDir, finalBarrier, finalReady], { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
107
117
  await waitFor([finalReady]);
108
118
  writeFileSync(finalBarrier, 'go');
109
- const finalRace = completeDeck(finalRaceDir, [{ id: 'go', selectedOptionId: 'yes' }], finalRaceClaim.token);
119
+ const finalRace = completePage(finalRaceDir, answer, finalRaceClaim.token);
110
120
  await new Promise((resolveWorker, rejectWorker) => cancelWorker.once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error('cancel worker failed'))));
111
- assert.equal(readTicketResult(finalRaceDir)?.kind, finalRace.won ? 'deck' : 'canceled', 'submit and cancellation race has one canonical winner');
112
- const concurrentDir = submitDeck({ dir: ticketDir('concurrent-claim'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
121
+ assert.equal(readTicketResult(finalRaceDir)?.kind, finalRace.won ? 'page' : 'canceled', 'submit and cancellation race has one canonical winner');
122
+ const concurrentDir = page('concurrent-claim', '2025-01-03T00:00:00.000Z').dir;
113
123
  const claimBarrier = join(temp, 'claim-race-barrier');
114
124
  // Both workers block on this release until the parent has read both results, so
115
125
  // neither winner's pid dies during the peer's attempt (a dead same-host pid is
@@ -1,7 +1,7 @@
1
1
  import { existsSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { claimPath, deckPath, responsePath, reviewPath } from './convention.js';
4
+ import { claimPath, pageManifestPath, responsePath, reviewPath } from './convention.js';
5
5
  import { readJsonOrNull } from '../fs-utils.js';
6
6
  import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
7
7
  const REMOTE_STALE_MS = 30_000;
@@ -30,7 +30,7 @@ export function isStaleClaim(claim, now = Date.now(), localHost = hostname()) {
30
30
  /** Acquire a visible claim only for a still-pending ticket. Malformed crash artifacts are stale claims. */
31
31
  export function claimTicket(dir, opts = {}) {
32
32
  return withExclusiveDirectoryLock(`${dir}/.ticket-lock`, () => {
33
- if (existsSync(responsePath(dir)) || (!existsSync(deckPath(dir)) && !existsSync(reviewPath(dir))))
33
+ if (existsSync(responsePath(dir)) || (!existsSync(pageManifestPath(dir)) && !existsSync(reviewPath(dir))))
34
34
  return null;
35
35
  const path = claimPath(dir);
36
36
  const existing = readTicketClaim(dir);
@@ -1,5 +1,7 @@
1
- import type { Deck, InteractionResponse } from './types.js';
2
- export declare function deckPath(dir: string): string;
1
+ import type { PageManifest } from './page-schema.js';
2
+ /** The authored page source, colocated with its derived manifest in the bridge node directory. */
3
+ export declare function pagePath(dir: string, dialect: PageManifest['dialect']): string;
4
+ export declare function pageManifestPath(dir: string): string;
3
5
  export declare function reviewPath(dir: string): string;
4
6
  export declare function responsePath(dir: string): string;
5
7
  export declare function progressPath(dir: string): string;
@@ -13,6 +15,4 @@ export declare function requireTicket(dir: string): string;
13
15
  export declare function atomicWriteJson(path: string, value: unknown): void;
14
16
  /** Publish one immutable JSON record without replacing an existing winner. */
15
17
  export declare function publishJsonExclusive(path: string, value: unknown): boolean;
16
- export declare function writeResponse(dir: string, responses: InteractionResponse[], completedAt: string, deck?: Deck): string;
17
- export declare function writeProgress(dir: string, responses: InteractionResponse[]): void;
18
18
  export declare function clearProgress(dir: string): void;
@@ -1,13 +1,14 @@
1
1
  import { existsSync, linkSync, mkdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
- import { buildSummary } from './summary.js';
4
- export function deckPath(dir) { return `${dir}/deck.json`; }
3
+ /** The authored page source, colocated with its derived manifest in the bridge node directory. */
4
+ export function pagePath(dir, dialect) { return `${dir}/page.${dialect}`; }
5
+ export function pageManifestPath(dir) { return `${dir}/page.json`; }
5
6
  export function reviewPath(dir) { return `${dir}/review.json`; }
6
7
  export function responsePath(dir) { return `${dir}/response.json`; }
7
8
  export function progressPath(dir) { return `${dir}/progress.json`; }
8
9
  export function claimPath(dir) { return `${dir}/claim.json`; }
9
10
  export function interactionState(dir) {
10
- if (!existsSync(deckPath(dir)) && !existsSync(reviewPath(dir)))
11
+ if (!existsSync(pageManifestPath(dir)) && !existsSync(reviewPath(dir)))
11
12
  return 'missing';
12
13
  if (existsSync(responsePath(dir)))
13
14
  return 'resolved';
@@ -18,8 +19,8 @@ export function isClaimed(dir) { return existsSync(claimPath(dir)); }
18
19
  /** Resolve and verify a ticket directory without a registry. */
19
20
  export function requireTicket(dir) {
20
21
  const canonicalDir = realpathSync(dir);
21
- if (!existsSync(deckPath(canonicalDir)) && !existsSync(reviewPath(canonicalDir))) {
22
- throw new Error('ticket directory has no deck or review');
22
+ if (!existsSync(pageManifestPath(canonicalDir)) && !existsSync(reviewPath(canonicalDir))) {
23
+ throw new Error('ticket directory has no page or review');
23
24
  }
24
25
  return canonicalDir;
25
26
  }
@@ -47,14 +48,6 @@ export function publishJsonExclusive(path, value) {
47
48
  unlinkSync(tmp);
48
49
  }
49
50
  }
50
- export function writeResponse(dir, responses, completedAt, deck) {
51
- const summary = deck === undefined ? '' : buildSummary(deck, responses);
52
- atomicWriteJson(responsePath(dir), { schema: 'humanloop.response/v2', kind: 'deck', responses, summary, completedAt });
53
- return responsePath(dir);
54
- }
55
- export function writeProgress(dir, responses) {
56
- atomicWriteJson(progressPath(dir), { kind: 'deck', responses, savedAt: new Date().toISOString() });
57
- }
58
51
  export function clearProgress(dir) {
59
52
  try {
60
53
  unlinkSync(progressPath(dir));
@@ -0,0 +1,8 @@
1
+ /** Escape text for an HTML text node. */
2
+ export declare function escapeHtmlText(raw: string): string;
3
+ /** Escape a value for a double-quoted HTML attribute. */
4
+ export declare function escapeHtmlAttribute(raw: string): string;
5
+ /** Render one markdown document fragment as HTML. */
6
+ export declare function renderMarkdownToHtml(markdown: string): string;
7
+ /** Render inline markdown. Exported for callers rendering a single line. */
8
+ export declare function renderInline(source: string): string;