@north-light/crouter 0.3.192 → 0.3.193
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 +31 -10
- package/dist/api/client.js +83 -24
- package/dist/api/dto/bash-jobs.d.ts +22 -0
- package/dist/api/dto/bash-jobs.js +9 -0
- package/dist/api/dto/human.d.ts +3 -4
- package/dist/api/dto/inbox.d.ts +106 -65
- package/dist/api/dto/inbox.js +2 -8
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/input/overlay-owner.js +1 -1
- package/dist/clients/attach/overlays/auth.js +34 -8
- package/dist/clients/attach/overlays/pickers.d.ts +5 -8
- package/dist/clients/attach/overlays/pickers.js +37 -43
- package/dist/clients/attach/viewer.js +643 -620
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
- package/dist/clients/inbox/controller.d.ts +2 -3
- package/dist/clients/inbox/controller.js +41 -42
- package/dist/clients/inbox/page-adapter.d.ts +33 -0
- package/dist/clients/inbox/page-adapter.js +54 -0
- package/dist/clients/inbox/resolve.d.ts +2 -2
- package/dist/clients/inbox/resolve.js +1 -1
- package/dist/clients/inbox/review-visibility.d.ts +1 -1
- package/dist/clients/inbox/review-visibility.js +1 -1
- package/dist/clients/inbox/tui/ansi.d.ts +1 -1
- package/dist/clients/inbox/tui/ansi.js +2 -2
- package/dist/clients/inbox/tui/input.d.ts +1 -3
- package/dist/clients/inbox/tui/input.js +242 -553
- package/dist/clients/inbox/tui/panel.d.ts +3 -1
- package/dist/clients/inbox/tui/panel.js +59 -187
- package/dist/clients/inbox/tui/render.d.ts +0 -25
- package/dist/clients/inbox/tui/render.js +165 -416
- package/dist/clients/inbox/tui/slots.d.ts +32 -0
- package/dist/clients/inbox/tui/slots.js +156 -0
- package/dist/clients/inbox/tui/types.d.ts +25 -13
- package/dist/clients/inbox/tui.js +5 -3
- package/dist/commands/attention.js +2 -2
- package/dist/commands/human/doc.js +66 -20
- package/dist/commands/human/prompts.d.ts +3 -3
- package/dist/commands/human/prompts.js +158 -112
- package/dist/commands/human/queue.d.ts +0 -1
- package/dist/commands/human/queue.js +34 -94
- package/dist/commands/human/shared.d.ts +10 -4
- package/dist/commands/human/shared.js +62 -13
- package/dist/commands/human.js +8 -4
- package/dist/commands/memory/read.js +13 -5
- package/dist/commands/node/bash.js +15 -28
- package/dist/commands/surface/node/focus.js +1 -1
- package/dist/core/__tests__/human-deliver.test.js +40 -46
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
- package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
- package/dist/core/auth-interaction.d.ts +2 -2
- package/dist/core/bash-jobs.d.ts +32 -0
- package/dist/core/bash-jobs.js +72 -1
- package/dist/core/canvas/__tests__/attention.test.js +16 -10
- package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
- package/dist/core/canvas/attention.js +1 -1
- package/dist/core/canvas/browse/app.js +1 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/remote-canvas-source.js +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-render.test.js +49 -0
- package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-tickets.test.js +112 -0
- package/dist/core/human/__tests__/page.test.d.ts +1 -0
- package/dist/core/human/__tests__/page.test.js +139 -0
- package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
- package/dist/core/human/claim.js +2 -2
- package/dist/core/human/convention.d.ts +4 -4
- package/dist/core/human/convention.js +6 -13
- package/dist/core/human/markdown-html.d.ts +8 -0
- package/dist/core/human/markdown-html.js +581 -0
- package/dist/core/human/page-catalog.d.ts +6 -0
- package/dist/core/human/page-catalog.js +43 -0
- package/dist/core/human/page-render.d.ts +3 -0
- package/dist/core/human/page-render.js +131 -0
- package/dist/core/human/page-schema.d.ts +312 -0
- package/dist/core/human/page-schema.js +291 -0
- package/dist/core/human/page-synth.d.ts +9 -0
- package/dist/core/human/page-synth.js +17 -0
- package/dist/core/human/page.d.ts +22 -0
- package/dist/core/human/page.js +427 -0
- package/dist/core/human/review-schema.d.ts +19 -0
- package/dist/core/human/review-schema.js +34 -0
- package/dist/core/human/scan.d.ts +12 -3
- package/dist/core/human/scan.js +69 -26
- package/dist/core/human/summary.d.ts +5 -6
- package/dist/core/human/summary.js +54 -37
- package/dist/core/human/tickets.d.ts +61 -11
- package/dist/core/human/tickets.js +100 -57
- package/dist/core/human/types.d.ts +21 -66
- package/dist/core/human/types.js +1 -5
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/ticket-filter.d.ts +1 -1
- package/dist/core/review/ticket-filter.js +2 -8
- package/dist/core/runtime/broker/read-ops.js +3 -0
- package/dist/core/runtime/broker.js +19 -10
- package/dist/core/runtime/package-health.js +1 -1
- package/dist/core/termrender/termrender.d.ts +1 -1
- package/dist/core/termrender/termrender.js +2 -2
- package/dist/core/tui/page-host.d.ts +1 -1
- package/dist/core/tui/page-host.js +2 -2
- package/dist/core/user-settings.d.ts +19 -0
- package/dist/core/user-settings.js +22 -0
- package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
- package/dist/daemon/api/handlers/bash-jobs.js +89 -0
- package/dist/daemon/api/handlers/human.js +16 -14
- package/dist/daemon/api/handlers/inbox.js +311 -227
- package/dist/daemon/api/router.d.ts +5 -1
- package/dist/daemon/api/router.js +5 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +23 -10
- package/dist/daemon/human/finish.d.ts +8 -3
- package/dist/daemon/human/finish.js +35 -15
- package/dist/pages/bundle.css +1 -0
- package/dist/pages/bundle.js +1347 -0
- package/dist/pages/comments.d.ts +34 -0
- package/dist/pages/comments.js +72 -0
- package/dist/pages/elements/cards.d.ts +17 -0
- package/dist/pages/elements/cards.js +785 -0
- package/dist/pages/elements/chart.d.ts +1 -0
- package/dist/pages/elements/chart.js +648 -0
- package/dist/pages/elements/options.d.ts +21 -0
- package/dist/pages/elements/options.js +604 -0
- package/dist/pages/elements/pages.d.ts +24 -0
- package/dist/pages/elements/pages.js +355 -0
- package/dist/pages/elements/slot.d.ts +21 -0
- package/dist/pages/elements/slot.js +105 -0
- package/dist/pages/elements/table.d.ts +19 -0
- package/dist/pages/elements/table.js +906 -0
- package/dist/pages/elements/text.d.ts +25 -0
- package/dist/pages/elements/text.js +951 -0
- package/dist/pages/entry.d.ts +8 -0
- package/dist/pages/entry.js +8 -0
- package/dist/pages/host.d.ts +132 -0
- package/dist/pages/host.js +149 -0
- package/dist/pages/register.d.ts +2 -0
- package/dist/pages/register.js +3 -0
- package/dist/pages/slot-config.d.ts +36 -0
- package/dist/pages/slot-config.js +50 -0
- package/dist/pages/types.d.ts +98 -0
- package/dist/pages/types.js +23 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +2 -0
- package/package.json +8 -8
- package/runtime.lock.json +156 -101
- package/dist/clients/inbox/deck-adapter.d.ts +0 -29
- package/dist/clients/inbox/deck-adapter.js +0 -63
- package/dist/core/human/deck-factories.d.ts +0 -8
- package/dist/core/human/deck-factories.js +0 -15
- package/dist/core/human/deck-schema.d.ts +0 -70
- package/dist/core/human/deck-schema.js +0 -92
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { checkMarkdown } from '../termrender/termrender.js';
|
|
4
|
+
import { BUILTIN_PAGE_KINDS, isRegisteredPageKind } from './page-catalog.js';
|
|
5
|
+
import { isResponseBearingSlot, validatePageManifest } from './page-schema.js';
|
|
6
|
+
/** A page failed authoring or validation; the caller can report it as bad page input. */
|
|
7
|
+
export class PageAuthoringError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'PageAuthoringError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function asPageAuthoringError(error) {
|
|
14
|
+
if (error instanceof PageAuthoringError)
|
|
15
|
+
return error;
|
|
16
|
+
return new PageAuthoringError(error instanceof Error ? error.message : String(error));
|
|
17
|
+
}
|
|
18
|
+
function validateAuthoredManifest(parsed, productKinds) {
|
|
19
|
+
try {
|
|
20
|
+
return validatePageManifest(parsed, productKinds);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw asPageAuthoringError(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function parseJsonObject(raw, thing) {
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new PageAuthoringError(`${thing} must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
if (value === null || Array.isArray(value) || typeof value !== 'object')
|
|
35
|
+
throw new PageAuthoringError(`${thing} JSON must be one object`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function parseFrontMatter(document) {
|
|
39
|
+
if (!(document.startsWith('---\n') || document.startsWith('---\r\n')))
|
|
40
|
+
throw new PageAuthoringError('md page front matter must begin at the first bytes with a --- delimiter line');
|
|
41
|
+
const firstEnd = document.indexOf('\n') + 1;
|
|
42
|
+
let cursor = firstEnd;
|
|
43
|
+
let closingStart = -1;
|
|
44
|
+
while (cursor <= document.length) {
|
|
45
|
+
const end = document.indexOf('\n', cursor);
|
|
46
|
+
const lineEnd = end === -1 ? document.length : end;
|
|
47
|
+
const line = document.slice(cursor, lineEnd).replace(/\r$/, '');
|
|
48
|
+
if (line === '---') {
|
|
49
|
+
closingStart = cursor;
|
|
50
|
+
cursor = end === -1 ? document.length : end + 1;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (end === -1)
|
|
54
|
+
break;
|
|
55
|
+
cursor = end + 1;
|
|
56
|
+
}
|
|
57
|
+
if (closingStart === -1)
|
|
58
|
+
throw new PageAuthoringError('md page front matter is missing its closing --- delimiter line');
|
|
59
|
+
const raw = document.slice(firstEnd, closingStart);
|
|
60
|
+
const values = new Map();
|
|
61
|
+
const rawLines = raw.split(/\r?\n/);
|
|
62
|
+
if (rawLines.at(-1) === '')
|
|
63
|
+
rawLines.pop();
|
|
64
|
+
for (const [index, line] of rawLines.entries()) {
|
|
65
|
+
const colon = line.indexOf(':');
|
|
66
|
+
if (colon <= 0)
|
|
67
|
+
throw new PageAuthoringError(`front matter line ${index + 2} must be "key: value"`);
|
|
68
|
+
const key = line.slice(0, colon).trim();
|
|
69
|
+
const value = line.slice(colon + 1).trim();
|
|
70
|
+
if (!['title', 'subtitle', 'placement'].includes(key))
|
|
71
|
+
throw new PageAuthoringError(`unknown page front matter key "${key}"`);
|
|
72
|
+
if (values.has(key))
|
|
73
|
+
throw new PageAuthoringError(`duplicate page front matter key "${key}"`);
|
|
74
|
+
values.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
const title = values.get('title');
|
|
77
|
+
if (title === undefined || title === '')
|
|
78
|
+
throw new PageAuthoringError('page front matter title is required and must be nonempty');
|
|
79
|
+
const placement = values.get('placement') ?? 'panel';
|
|
80
|
+
if (placement !== 'inline' && placement !== 'panel')
|
|
81
|
+
throw new PageAuthoringError(`page front matter placement must be exactly inline or panel; received "${placement}"`);
|
|
82
|
+
const subtitle = values.get('subtitle');
|
|
83
|
+
return { title, ...(subtitle === undefined ? {} : { subtitle }), placement, bodyStart: cursor };
|
|
84
|
+
}
|
|
85
|
+
function sourceLines(source, start) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
let cursor = start;
|
|
88
|
+
while (cursor < source.length) {
|
|
89
|
+
const newline = source.indexOf('\n', cursor);
|
|
90
|
+
const end = newline === -1 ? source.length : newline + 1;
|
|
91
|
+
const contentEnd = newline === -1 ? source.length : newline;
|
|
92
|
+
lines.push({ start: cursor, end, contentEnd, text: source.slice(cursor, contentEnd).replace(/\r$/, '') });
|
|
93
|
+
cursor = end;
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
/** CommonMark closing fence: at least as many of the opener's fence character, nothing else on the line. */
|
|
98
|
+
function closesFence(text, opener) {
|
|
99
|
+
const trimmed = text.trimEnd();
|
|
100
|
+
return trimmed.length >= opener.length && trimmed.split('').every((character) => character === opener.char);
|
|
101
|
+
}
|
|
102
|
+
function addProse(projection, raw) {
|
|
103
|
+
const markdown = raw.trim();
|
|
104
|
+
if (markdown !== '')
|
|
105
|
+
projection.push({ kind: 'prose', markdown });
|
|
106
|
+
}
|
|
107
|
+
function scanMarkdown(document, productKinds) {
|
|
108
|
+
const meta = parseFrontMatter(document);
|
|
109
|
+
const body = document.slice(meta.bodyStart);
|
|
110
|
+
const markdownCheck = checkMarkdown(body);
|
|
111
|
+
if (!markdownCheck.ok)
|
|
112
|
+
throw new PageAuthoringError(markdownCheck.error);
|
|
113
|
+
const lines = sourceLines(document, meta.bodyStart);
|
|
114
|
+
// Fence errors report document-absolute line numbers, past the front matter.
|
|
115
|
+
const lineOffset = document.slice(0, meta.bodyStart).split('\n').length - 1;
|
|
116
|
+
const slots = [];
|
|
117
|
+
const projection = [];
|
|
118
|
+
let proseStart = meta.bodyStart;
|
|
119
|
+
let ordinaryFence = null;
|
|
120
|
+
for (let index = 0; index < lines.length; index++) {
|
|
121
|
+
const line = lines[index];
|
|
122
|
+
if (ordinaryFence !== null) {
|
|
123
|
+
if (closesFence(line.text, ordinaryFence))
|
|
124
|
+
ordinaryFence = null;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const opening = line.text.match(/^(`{3,}|~{3,})(.*)$/);
|
|
128
|
+
if (opening === null)
|
|
129
|
+
continue;
|
|
130
|
+
const run = opening[1];
|
|
131
|
+
const fence = { char: run[0], length: run.length };
|
|
132
|
+
const info = opening[2];
|
|
133
|
+
if (fence.char !== '`' || !info.startsWith('slot:')) {
|
|
134
|
+
ordinaryFence = fence;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const kind = info.slice('slot:'.length);
|
|
138
|
+
if (kind === '' || info !== `slot:${kind}` || !isRegisteredPageKind(kind, productKinds)) {
|
|
139
|
+
throw new PageAuthoringError(`unregistered or invalid page slot kind "${kind}" in md fence at line ${lineOffset + index + 1}`);
|
|
140
|
+
}
|
|
141
|
+
let closeIndex = -1;
|
|
142
|
+
for (let candidate = index + 1; candidate < lines.length; candidate++) {
|
|
143
|
+
if (closesFence(lines[candidate].text, fence)) {
|
|
144
|
+
closeIndex = candidate;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (closeIndex === -1)
|
|
149
|
+
throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has no matching ${run.length}-backtick closing fence`);
|
|
150
|
+
addProse(projection, document.slice(proseStart, line.start));
|
|
151
|
+
const rawObject = parseJsonObject(document.slice(line.end, lines[closeIndex].start), `slot:${kind} fence at line ${lineOffset + index + 1}`);
|
|
152
|
+
const { id, ...config } = rawObject;
|
|
153
|
+
if (id !== undefined && typeof id !== 'string')
|
|
154
|
+
throw new PageAuthoringError(`slot:${kind} fence at line ${lineOffset + index + 1} has non-string reserved id`);
|
|
155
|
+
slots.push({ ...(id === undefined ? {} : { id }), kind, step: 0, config, ...(BUILTIN_PAGE_KINDS.includes(kind) ? {} : { unvalidated: true }) });
|
|
156
|
+
projection.push({ kind: 'slot', slotIndex: slots.length - 1 });
|
|
157
|
+
proseStart = lines[closeIndex].end;
|
|
158
|
+
index = closeIndex;
|
|
159
|
+
}
|
|
160
|
+
addProse(projection, document.slice(proseStart));
|
|
161
|
+
return { title: meta.title, ...(meta.subtitle === undefined ? {} : { subtitle: meta.subtitle }), placement: meta.placement, steps: 1, slots, projection };
|
|
162
|
+
}
|
|
163
|
+
const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
164
|
+
function tagEnd(html, start) {
|
|
165
|
+
let quote = null;
|
|
166
|
+
for (let index = start + 1; index < html.length; index++) {
|
|
167
|
+
const character = html[index];
|
|
168
|
+
if (quote !== null) {
|
|
169
|
+
if (character === quote)
|
|
170
|
+
quote = null;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (character === '"' || character === "'") {
|
|
174
|
+
quote = character;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (character === '>')
|
|
178
|
+
return index + 1;
|
|
179
|
+
}
|
|
180
|
+
throw new PageAuthoringError(`unterminated HTML tag beginning at byte ${start}`);
|
|
181
|
+
}
|
|
182
|
+
function parseAttributes(source) {
|
|
183
|
+
const attrs = new Map();
|
|
184
|
+
const tagNameEnd = source.search(/\s/);
|
|
185
|
+
const body = tagNameEnd === -1 ? '' : source.slice(tagNameEnd);
|
|
186
|
+
const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
187
|
+
for (const match of body.matchAll(pattern))
|
|
188
|
+
attrs.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '');
|
|
189
|
+
return attrs;
|
|
190
|
+
}
|
|
191
|
+
function parseHtmlElements(html) {
|
|
192
|
+
const roots = [];
|
|
193
|
+
const all = [];
|
|
194
|
+
const stack = [];
|
|
195
|
+
let cursor = 0;
|
|
196
|
+
while (cursor < html.length) {
|
|
197
|
+
const start = html.indexOf('<', cursor);
|
|
198
|
+
if (start === -1)
|
|
199
|
+
break;
|
|
200
|
+
if (html.startsWith('<!--', start)) {
|
|
201
|
+
const end = html.indexOf('-->', start + 4);
|
|
202
|
+
if (end === -1)
|
|
203
|
+
throw new PageAuthoringError(`unterminated HTML comment at byte ${start}`);
|
|
204
|
+
cursor = end + 3;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const end = tagEnd(html, start);
|
|
208
|
+
const token = html.slice(start, end);
|
|
209
|
+
if (/^<!/i.test(token) || /^<\?/i.test(token)) {
|
|
210
|
+
cursor = end;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const closing = token.match(/^<\s*\/\s*([^\s>]+)/);
|
|
214
|
+
if (closing !== null) {
|
|
215
|
+
const tag = closing[1].toLowerCase();
|
|
216
|
+
let match = stack.length - 1;
|
|
217
|
+
while (match >= 0 && stack[match].tag !== tag)
|
|
218
|
+
match--;
|
|
219
|
+
if (match >= 0)
|
|
220
|
+
stack.length = match;
|
|
221
|
+
cursor = end;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const opening = token.match(/^<\s*([^\s/>]+)/);
|
|
225
|
+
if (opening === null) {
|
|
226
|
+
cursor = end;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const tag = opening[1].toLowerCase();
|
|
230
|
+
const parent = stack.at(-1);
|
|
231
|
+
const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, ...(parent === undefined ? {} : { parent }), children: [] };
|
|
232
|
+
if (parent === undefined)
|
|
233
|
+
roots.push(element);
|
|
234
|
+
else
|
|
235
|
+
parent.children.push(element);
|
|
236
|
+
all.push(element);
|
|
237
|
+
const selfClosing = /\/\s*>$/.test(token) || VOID_TAGS.has(tag);
|
|
238
|
+
if (tag === 'script' && !selfClosing) {
|
|
239
|
+
const closePattern = /<\s*\/\s*script\s*>/gi;
|
|
240
|
+
closePattern.lastIndex = end;
|
|
241
|
+
const close = closePattern.exec(html);
|
|
242
|
+
if (close === null)
|
|
243
|
+
throw new PageAuthoringError(`unterminated <script> element at byte ${start}`);
|
|
244
|
+
element.text = html.slice(end, close.index);
|
|
245
|
+
cursor = closePattern.lastIndex;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (!selfClosing)
|
|
249
|
+
stack.push(element);
|
|
250
|
+
cursor = end;
|
|
251
|
+
}
|
|
252
|
+
return all;
|
|
253
|
+
}
|
|
254
|
+
function ancestor(element, tag) {
|
|
255
|
+
let current = element.parent;
|
|
256
|
+
while (current !== undefined) {
|
|
257
|
+
if (current.tag === tag)
|
|
258
|
+
return current;
|
|
259
|
+
current = current.parent;
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
function decodeHtmlAttribute(value) {
|
|
264
|
+
return value.replace(/&(?:amp|quot|apos|lt|gt);|&#(?:x[0-9a-f]+|\d+);/gi, (entity) => {
|
|
265
|
+
const named = { '&': '&', '"': '"', ''': "'", '<': '<', '>': '>' };
|
|
266
|
+
const lower = entity.toLowerCase();
|
|
267
|
+
if (named[lower] !== undefined)
|
|
268
|
+
return named[lower];
|
|
269
|
+
const numeric = lower.startsWith('&#x') ? Number.parseInt(lower.slice(3, -1), 16) : Number.parseInt(lower.slice(2, -1), 10);
|
|
270
|
+
return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : entity;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function validateHtmlStructure(html) {
|
|
274
|
+
const counts = [
|
|
275
|
+
['doctype', /<!doctype\b[^>]*>/gi, 'doctype'],
|
|
276
|
+
['<head>', /<head(?=\s|>)/gi, '<head> opening tag'],
|
|
277
|
+
['<body>', /<body(?=\s|>)/gi, '<body> opening tag'],
|
|
278
|
+
];
|
|
279
|
+
for (const [, pattern, label] of counts) {
|
|
280
|
+
const count = html.match(pattern)?.length ?? 0;
|
|
281
|
+
if (count !== 1)
|
|
282
|
+
throw new PageAuthoringError(`Expected exactly one ${label}, found ${count}.`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function readHtmlMetadata(elements) {
|
|
286
|
+
const recognized = new Map();
|
|
287
|
+
for (const element of elements.filter((candidate) => candidate.tag === 'meta')) {
|
|
288
|
+
const name = element.attrs.get('name');
|
|
289
|
+
if (name === undefined || !name.startsWith('crtr-page-'))
|
|
290
|
+
continue;
|
|
291
|
+
if (!['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement'].includes(name))
|
|
292
|
+
throw new PageAuthoringError(`unknown page metadata element <meta name="${name}">`);
|
|
293
|
+
if (ancestor(element, 'head') === undefined)
|
|
294
|
+
throw new PageAuthoringError(`<meta name="${name}"> must appear inside <head>`);
|
|
295
|
+
const entries = recognized.get(name) ?? [];
|
|
296
|
+
entries.push(element);
|
|
297
|
+
recognized.set(name, entries);
|
|
298
|
+
}
|
|
299
|
+
for (const name of ['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement']) {
|
|
300
|
+
const count = recognized.get(name)?.length ?? 0;
|
|
301
|
+
if ((name === 'crtr-page-title' && count !== 1) || (name !== 'crtr-page-title' && count > 1)) {
|
|
302
|
+
const expected = name === 'crtr-page-title' ? 'exactly one' : 'at most one';
|
|
303
|
+
throw new PageAuthoringError(`Expected ${expected} <meta name="${name}">, found ${count}.`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const titleElement = recognized.get('crtr-page-title')[0];
|
|
307
|
+
const title = decodeHtmlAttribute(titleElement.attrs.get('content') ?? '').trim();
|
|
308
|
+
if (title === '')
|
|
309
|
+
throw new PageAuthoringError(`<meta name="crtr-page-title"> requires nonempty content`);
|
|
310
|
+
const subtitleElement = recognized.get('crtr-page-subtitle')?.[0];
|
|
311
|
+
const subtitle = subtitleElement === undefined ? undefined : decodeHtmlAttribute(subtitleElement.attrs.get('content') ?? '').trim();
|
|
312
|
+
const placementElement = recognized.get('crtr-page-placement')?.[0];
|
|
313
|
+
const placementValue = placementElement === undefined ? 'panel' : decodeHtmlAttribute(placementElement.attrs.get('content') ?? '').trim();
|
|
314
|
+
if (placementValue !== 'inline' && placementValue !== 'panel')
|
|
315
|
+
throw new PageAuthoringError(`<meta name="crtr-page-placement"> content must be exactly inline or panel; received "${placementValue}"`);
|
|
316
|
+
return { title, ...(subtitle === undefined ? {} : { subtitle }), placement: placementValue };
|
|
317
|
+
}
|
|
318
|
+
function configForHtmlElement(element) {
|
|
319
|
+
const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
|
|
320
|
+
if (script === undefined)
|
|
321
|
+
throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} requires a direct <script type="application/json"> child`);
|
|
322
|
+
return parseJsonObject(script.text ?? '', `<${element.tag}> at byte ${element.start}`);
|
|
323
|
+
}
|
|
324
|
+
function scanHtml(document, productKinds) {
|
|
325
|
+
validateHtmlStructure(document);
|
|
326
|
+
const elements = parseHtmlElements(document);
|
|
327
|
+
const metadata = readHtmlMetadata(elements);
|
|
328
|
+
const wrappers = elements.filter((element) => element.tag === 'crtr-pages');
|
|
329
|
+
if (wrappers.some((candidate) => ancestor(candidate, 'crtr-pages') !== undefined))
|
|
330
|
+
throw new PageAuthoringError('nested <crtr-pages> element is not allowed');
|
|
331
|
+
if (wrappers.length > 1)
|
|
332
|
+
throw new PageAuthoringError(`more than one <crtr-pages> element: found ${wrappers.length}`);
|
|
333
|
+
const wrapper = wrappers[0];
|
|
334
|
+
const pages = elements.filter((element) => element.tag === 'crtr-page');
|
|
335
|
+
for (const page of pages)
|
|
336
|
+
if (ancestor(page, 'crtr-pages') !== wrapper || wrapper === undefined)
|
|
337
|
+
throw new PageAuthoringError(`<crtr-page> outside <crtr-pages> at byte ${page.start}`);
|
|
338
|
+
if (wrapper !== undefined && pages.length === 0)
|
|
339
|
+
throw new PageAuthoringError('empty <crtr-pages> element must contain at least one <crtr-page>');
|
|
340
|
+
const pageIndexes = new Map(pages.map((page, index) => [page, index]));
|
|
341
|
+
const rawSlots = [];
|
|
342
|
+
const slotElements = [];
|
|
343
|
+
for (const element of elements) {
|
|
344
|
+
if (element.tag === 'crtr-pages' || element.tag === 'crtr-page')
|
|
345
|
+
continue;
|
|
346
|
+
let kind;
|
|
347
|
+
let unvalidated = false;
|
|
348
|
+
if (element.tag === 'crtr-slot') {
|
|
349
|
+
kind = element.attrs.get('kind');
|
|
350
|
+
if (kind === undefined || !productKinds.includes(kind) || BUILTIN_PAGE_KINDS.includes(kind))
|
|
351
|
+
throw new PageAuthoringError(`<crtr-slot> at byte ${element.start} names unregistered product kind "${kind ?? ''}"`);
|
|
352
|
+
unvalidated = true;
|
|
353
|
+
}
|
|
354
|
+
else if (element.tag.startsWith('crtr-')) {
|
|
355
|
+
kind = element.tag.slice('crtr-'.length);
|
|
356
|
+
if (!BUILTIN_PAGE_KINDS.includes(kind))
|
|
357
|
+
throw new PageAuthoringError(`unregistered page element <${element.tag}> at byte ${element.start}; product kinds use <crtr-slot kind="…">`);
|
|
358
|
+
}
|
|
359
|
+
if (kind === undefined)
|
|
360
|
+
continue;
|
|
361
|
+
const id = element.attrs.get('id');
|
|
362
|
+
const containingPage = ancestor(element, 'crtr-page');
|
|
363
|
+
const step = containingPage === undefined ? 0 : pageIndexes.get(containingPage) ?? 0;
|
|
364
|
+
rawSlots.push({ ...(id === undefined ? {} : { id }), kind, step, config: configForHtmlElement(element), ...(unvalidated ? { unvalidated: true } : {}) });
|
|
365
|
+
slotElements.push(element);
|
|
366
|
+
}
|
|
367
|
+
const steps = wrapper === undefined ? 1 : pages.length;
|
|
368
|
+
const provisional = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: 'html', ...metadata, document: 'page.html', steps, slots: rawSlots }, productKinds);
|
|
369
|
+
for (let index = 0; index < provisional.slots.length; index++) {
|
|
370
|
+
const slot = provisional.slots[index];
|
|
371
|
+
if (!isResponseBearingSlot(slot))
|
|
372
|
+
continue;
|
|
373
|
+
const element = slotElements[index];
|
|
374
|
+
if (ancestor(element, 'crtr-page') === undefined) {
|
|
375
|
+
throw new PageAuthoringError(`response-bearing element <${element.tag}> for slot "${slot.id}" is outside any <crtr-page>.\nWrap interactive elements with:\n<crtr-pages><crtr-page>…</crtr-page></crtr-pages>`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { ...metadata, steps, slots: provisional.slots, projection: provisional.slots.map((_, slotIndex) => ({ kind: 'slot', slotIndex })) };
|
|
379
|
+
}
|
|
380
|
+
function scanDocument(document, dialect, productKinds) {
|
|
381
|
+
return dialect === 'md' ? scanMarkdown(document, productKinds) : scanHtml(document, productKinds);
|
|
382
|
+
}
|
|
383
|
+
/** Derive the transport manifest from one authored page document. */
|
|
384
|
+
export function derivePageManifestFromDocument(document, dialect, productKinds = [], source) {
|
|
385
|
+
const scanned = scanDocument(document, dialect, productKinds);
|
|
386
|
+
return validateAuthoredManifest({ schema: 'crtr.page/v1', dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: dialect === 'md' ? 'page.md' : 'page.html', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productKinds);
|
|
387
|
+
}
|
|
388
|
+
/** Derive the transport manifest from one authored page source file. */
|
|
389
|
+
export function derivePageManifest(sourceFile, dialect, productKinds = [], source) {
|
|
390
|
+
return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), dialect, productKinds, source);
|
|
391
|
+
}
|
|
392
|
+
/** Read and validate page.json from a ticket directory. */
|
|
393
|
+
export function parsePage(dir, productKinds = []) {
|
|
394
|
+
let raw;
|
|
395
|
+
try {
|
|
396
|
+
raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
throw new PageAuthoringError('page.json is not valid JSON');
|
|
400
|
+
}
|
|
401
|
+
return validatePageManifest(raw, productKinds);
|
|
402
|
+
}
|
|
403
|
+
/** Key-order-independent value identity, so a re-scan compares by content. */
|
|
404
|
+
function canonicalJson(value) {
|
|
405
|
+
if (value === null || typeof value !== 'object')
|
|
406
|
+
return JSON.stringify(value) ?? 'null';
|
|
407
|
+
if (Array.isArray(value))
|
|
408
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
409
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => (left < right ? -1 : 1));
|
|
410
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
|
|
411
|
+
}
|
|
412
|
+
/** Project authored source for local rendering and verify its slot references against the manifest. */
|
|
413
|
+
export function parsePageSource(document, manifest, productKinds = []) {
|
|
414
|
+
const canonical = validateAuthoredManifest(manifest, productKinds);
|
|
415
|
+
const scanned = scanDocument(document, canonical.dialect, productKinds);
|
|
416
|
+
// Normalize the re-scan through the same schema pass the manifest went through,
|
|
417
|
+
// so per-kind config defaults are on both sides of the comparison.
|
|
418
|
+
const derived = validateAuthoredManifest({ schema: 'crtr.page/v1', dialect: canonical.dialect, title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), placement: scanned.placement, document: canonical.document, steps: scanned.steps, slots: scanned.slots }, productKinds);
|
|
419
|
+
if (derived.slots.length !== canonical.slots.length)
|
|
420
|
+
throw new PageAuthoringError(`page source has ${derived.slots.length} slots but manifest has ${canonical.slots.length}`);
|
|
421
|
+
for (let index = 0; index < canonical.slots.length; index++) {
|
|
422
|
+
const left = derived.slots[index];
|
|
423
|
+
if (canonicalJson(left) !== canonicalJson(canonical.slots[index]))
|
|
424
|
+
throw new PageAuthoringError(`page source slot #${index} (${left.id ?? left.kind}) does not match manifest slot #${index}`);
|
|
425
|
+
}
|
|
426
|
+
return scanned.projection;
|
|
427
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ReviewDescriptor } from './types.js';
|
|
3
|
+
export declare const reviewDescriptorSchema: z.ZodObject<{
|
|
4
|
+
schema: z.ZodLiteral<"humanloop.review/v1">;
|
|
5
|
+
file: z.ZodString;
|
|
6
|
+
output: z.ZodString;
|
|
7
|
+
title: z.ZodString;
|
|
8
|
+
subtitle: z.ZodString;
|
|
9
|
+
source: z.ZodObject<{
|
|
10
|
+
sessionName: z.ZodOptional<z.ZodString>;
|
|
11
|
+
askedBy: z.ZodOptional<z.ZodString>;
|
|
12
|
+
blockedSince: z.ZodOptional<z.ZodString>;
|
|
13
|
+
nodeId: z.ZodOptional<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
blockedSince: z.ZodString;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export declare function validateReviewDescriptor(parsed: unknown): ReviewDescriptor;
|
|
18
|
+
/** Canonicalize and authorize the one review projection boundary before every write. */
|
|
19
|
+
export declare function validateReviewProjection(dir: string, parsed: unknown): ReviewDescriptor;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { claimPath, pageManifestPath, progressPath, responsePath, reviewPath } from './convention.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { checkReviewableFile } from './visible-paths.js';
|
|
6
|
+
const reviewSourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), nodeId: z.string().optional() });
|
|
7
|
+
export const reviewDescriptorSchema = z.object({
|
|
8
|
+
schema: z.literal('humanloop.review/v1'),
|
|
9
|
+
file: z.string().min(1).refine(isAbsolute, 'file must be absolute'),
|
|
10
|
+
output: z.string().min(1).refine(isAbsolute, 'output must be absolute'),
|
|
11
|
+
title: z.string().trim().min(1),
|
|
12
|
+
subtitle: z.string().trim().min(1),
|
|
13
|
+
source: reviewSourceSchema,
|
|
14
|
+
blockedSince: z.string().datetime({ offset: true }),
|
|
15
|
+
}).strict();
|
|
16
|
+
export function validateReviewDescriptor(parsed) { return reviewDescriptorSchema.parse(parsed); }
|
|
17
|
+
/** Canonicalize and authorize the one review projection boundary before every write. */
|
|
18
|
+
export function validateReviewProjection(dir, parsed) {
|
|
19
|
+
const descriptor = validateReviewDescriptor(parsed);
|
|
20
|
+
// Any reviewable UTF-8 text file: a .md artifact renders as a document, a
|
|
21
|
+
// source file as a syntax-highlighted code panel.
|
|
22
|
+
if (!existsSync(descriptor.file))
|
|
23
|
+
throw new Error('review file must be an existing absolute path');
|
|
24
|
+
const reviewable = checkReviewableFile(descriptor.file);
|
|
25
|
+
if (!reviewable.ok)
|
|
26
|
+
throw new Error(`review file is not reviewable: ${reviewable.reason}`);
|
|
27
|
+
const file = realpathSync(descriptor.file);
|
|
28
|
+
const output = resolve(realpathSync(dirname(descriptor.output)), basename(descriptor.output));
|
|
29
|
+
const reserved = new Set(['page.json', 'review.json', 'response.json', 'progress.json', 'claim.json']);
|
|
30
|
+
const ownProtocolPaths = new Set([pageManifestPath(dir), reviewPath(dir), responsePath(dir), progressPath(dir), claimPath(dir)]);
|
|
31
|
+
if (output === file || reserved.has(basename(output)) || ownProtocolPaths.has(output))
|
|
32
|
+
throw new Error('review output must not alias the source or ticket protocol files');
|
|
33
|
+
return { ...descriptor, file, output };
|
|
34
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
import type { TicketSummary } from './types.js';
|
|
2
|
-
/** Read all unresolved
|
|
3
|
-
export declare function scanInbox(): TicketSummary[];
|
|
1
|
+
import type { PageTicketSummary, TicketSummary } from './types.js';
|
|
2
|
+
/** Read all unresolved page/review tickets, newest first. Progress never affects visibility. */
|
|
3
|
+
export declare function scanInbox(productKinds?: string[]): TicketSummary[];
|
|
4
|
+
export interface PageHistoryEntry extends PageTicketSummary {
|
|
5
|
+
state: 'pending' | 'resolved' | 'canceled';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Every page ticket raised by one node — pending, resolved or canceled — oldest
|
|
9
|
+
* first. This is the durable discovery seam for transcript placement, so it is a
|
|
10
|
+
* separate walk from the pending-only inbox scan and never filters on a terminal file.
|
|
11
|
+
*/
|
|
12
|
+
export declare function scanPageHistory(nodeId: string, productKinds?: string[]): PageHistoryEntry[];
|
package/dist/core/human/scan.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
|
-
import { claimPath,
|
|
3
|
+
import { claimPath, isResolved, pageManifestPath, reviewPath } from './convention.js';
|
|
4
4
|
import { readJsonOrNull } from '../fs-utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { validateReviewDescriptor } from './review-schema.js';
|
|
6
|
+
import { isResponseBearingSlot, validatePageManifest } from './page-schema.js';
|
|
7
|
+
import { pageTicketState } from './tickets.js';
|
|
6
8
|
import { ticketsRoot } from './root.js';
|
|
7
9
|
function claimSummary(dir) {
|
|
8
10
|
const claim = readJsonOrNull(claimPath(dir));
|
|
@@ -10,46 +12,88 @@ function claimSummary(dir) {
|
|
|
10
12
|
return undefined;
|
|
11
13
|
return { owner: claim.host, claimedAt: claim.claimedAt, heartbeatAt: claim.heartbeatAt };
|
|
12
14
|
}
|
|
13
|
-
function
|
|
14
|
-
const raw = readJsonOrNull(
|
|
15
|
+
function reviewSummary(dir, id) {
|
|
16
|
+
const raw = readJsonOrNull(reviewPath(dir));
|
|
15
17
|
if (raw === null)
|
|
16
18
|
return null;
|
|
17
|
-
let
|
|
19
|
+
let review;
|
|
18
20
|
try {
|
|
19
|
-
|
|
21
|
+
review = validateReviewDescriptor(raw);
|
|
20
22
|
}
|
|
21
23
|
catch {
|
|
22
24
|
return null;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
return { dir, id, kind: 'review', title: review.title, subtitle: review.subtitle, file: review.file, output: review.output, source: review.source, blockedSince: review.blockedSince, claim: claimSummary(dir) };
|
|
27
|
+
}
|
|
28
|
+
function pageSummary(dir, id, productKinds) {
|
|
29
|
+
const raw = readJsonOrNull(pageManifestPath(dir));
|
|
30
|
+
if (raw === null)
|
|
31
|
+
return null;
|
|
32
|
+
let manifest;
|
|
33
|
+
try {
|
|
34
|
+
manifest = validatePageManifest(raw, productKinds);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
26
37
|
return null;
|
|
27
|
-
|
|
38
|
+
}
|
|
39
|
+
let blockedSince = manifest.source?.blockedSince;
|
|
28
40
|
if (blockedSince === undefined) {
|
|
29
41
|
try {
|
|
30
|
-
blockedSince = statSync(
|
|
42
|
+
blockedSince = statSync(pageManifestPath(dir)).mtime.toISOString();
|
|
31
43
|
}
|
|
32
44
|
catch {
|
|
33
45
|
return null;
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
dir,
|
|
50
|
+
id,
|
|
51
|
+
kind: 'page',
|
|
52
|
+
title: manifest.title,
|
|
53
|
+
subtitle: manifest.subtitle ?? '',
|
|
54
|
+
dialect: manifest.dialect,
|
|
55
|
+
placement: manifest.placement,
|
|
56
|
+
steps: manifest.steps,
|
|
57
|
+
slotKinds: manifest.slots.map((slot) => slot.kind),
|
|
58
|
+
awaitsResponse: manifest.slots.some(isResponseBearingSlot),
|
|
59
|
+
source: manifest.source ?? {},
|
|
60
|
+
blockedSince,
|
|
61
|
+
claim: claimSummary(dir),
|
|
62
|
+
};
|
|
37
63
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
let review;
|
|
64
|
+
/** Read all unresolved page/review tickets, newest first. Progress never affects visibility. */
|
|
65
|
+
export function scanInbox(productKinds = []) {
|
|
66
|
+
let entries;
|
|
43
67
|
try {
|
|
44
|
-
|
|
68
|
+
entries = readdirSync(ticketsRoot());
|
|
45
69
|
}
|
|
46
70
|
catch {
|
|
47
|
-
return
|
|
71
|
+
return [];
|
|
48
72
|
}
|
|
49
|
-
|
|
73
|
+
const items = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const dir = join(ticketsRoot(), entry);
|
|
76
|
+
try {
|
|
77
|
+
if (!statSync(dir).isDirectory())
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (isResolved(dir))
|
|
84
|
+
continue;
|
|
85
|
+
const item = reviewSummary(dir, basename(dir)) ?? pageSummary(dir, basename(dir), productKinds);
|
|
86
|
+
if (item !== null)
|
|
87
|
+
items.push(item);
|
|
88
|
+
}
|
|
89
|
+
return items.sort((a, b) => b.blockedSince.localeCompare(a.blockedSince) || a.id.localeCompare(b.id));
|
|
50
90
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Every page ticket raised by one node — pending, resolved or canceled — oldest
|
|
93
|
+
* first. This is the durable discovery seam for transcript placement, so it is a
|
|
94
|
+
* separate walk from the pending-only inbox scan and never filters on a terminal file.
|
|
95
|
+
*/
|
|
96
|
+
export function scanPageHistory(nodeId, productKinds = []) {
|
|
53
97
|
let entries;
|
|
54
98
|
try {
|
|
55
99
|
entries = readdirSync(ticketsRoot());
|
|
@@ -67,11 +111,10 @@ export function scanInbox() {
|
|
|
67
111
|
catch {
|
|
68
112
|
continue;
|
|
69
113
|
}
|
|
70
|
-
|
|
114
|
+
const summary = pageSummary(dir, basename(dir), productKinds);
|
|
115
|
+
if (summary === null || summary.source.nodeId !== nodeId)
|
|
71
116
|
continue;
|
|
72
|
-
|
|
73
|
-
if (item !== null)
|
|
74
|
-
items.push(item);
|
|
117
|
+
items.push({ ...summary, state: pageTicketState(dir) });
|
|
75
118
|
}
|
|
76
|
-
return items.sort((a, b) =>
|
|
119
|
+
return items.sort((a, b) => Date.parse(a.blockedSince) - Date.parse(b.blockedSince) || a.id.localeCompare(b.id));
|
|
77
120
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PageManifest, type PageResponses } from './page-schema.js';
|
|
2
2
|
/**
|
|
3
|
-
* Deterministic, no-LLM resolution summary — one line per answered
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* time).
|
|
3
|
+
* Deterministic, no-LLM page resolution summary — one line per answered slot in
|
|
4
|
+
* document order: `"<slot id>: <answer>"`. An acknowledgement (`responses: {}`)
|
|
5
|
+
* summarizes as the empty string.
|
|
7
6
|
*/
|
|
8
|
-
export declare function
|
|
7
|
+
export declare function buildPageSummary(manifest: PageManifest, responses: PageResponses): string;
|