@north-light/crouter 0.3.231 → 0.3.233
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 +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/canvas.d.ts +10 -0
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/health.d.ts +2 -1
- package/dist/api/dto/lifecycle.d.ts +3 -4
- package/dist/api/dto/messages.d.ts +5 -4
- package/dist/api/dto/nodes.d.ts +2 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
- package/dist/builtin-memory/insights/capture.md +3 -2
- package/dist/builtin-memory/internal/memory-loading.md +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
- package/dist/clients/attach/render/diagram.js +13 -5
- package/dist/clients/attach/render/page-block.d.ts +0 -1
- package/dist/clients/attach/render/page-block.js +4 -57
- package/dist/clients/attach/viewer.js +570 -563
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
- package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
- package/dist/clients/inbox/controller.d.ts +10 -0
- package/dist/clients/inbox/controller.js +56 -12
- package/dist/clients/inbox/tui/input.js +38 -10
- package/dist/clients/inbox/tui/page-body.d.ts +10 -0
- package/dist/clients/inbox/tui/page-body.js +66 -0
- package/dist/clients/inbox/tui/panel.js +6 -4
- package/dist/clients/inbox/tui/render.js +89 -17
- package/dist/clients/inbox/tui/types.d.ts +4 -4
- package/dist/commands/__tests__/human.test.js +18 -3
- package/dist/commands/__tests__/node-message.test.js +3 -3
- package/dist/commands/api-client.js +1 -7
- package/dist/commands/canvas-config.js +6 -14
- package/dist/commands/canvas-use.js +4 -6
- package/dist/commands/cron.js +16 -22
- package/dist/commands/human/prompts.d.ts +1 -1
- package/dist/commands/human/prompts.js +118 -112
- package/dist/commands/human/request.js +13 -14
- package/dist/commands/human/review.js +3 -4
- package/dist/commands/human/shared.d.ts +6 -0
- package/dist/commands/human/shared.js +43 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/delete.js +4 -6
- package/dist/commands/memory/edit.js +0 -4
- package/dist/commands/memory/move.js +3 -5
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +11 -7
- package/dist/commands/memory/write.js +60 -29
- package/dist/commands/memory.js +1 -1
- package/dist/commands/node/bash.js +6 -9
- package/dist/commands/node/create.js +89 -22
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +30 -27
- package/dist/commands/node/message.js +24 -45
- package/dist/commands/node/subscription.js +6 -15
- package/dist/commands/node/wait.js +2 -3
- package/dist/commands/node-lifecycle-revive.js +1 -12
- package/dist/commands/pkg/browse/actions.js +2 -3
- package/dist/commands/pkg/market-manage.js +2 -5
- package/dist/commands/pkg/plugin-manage.js +7 -8
- package/dist/commands/profile/default.js +5 -5
- package/dist/commands/profile/delete.js +1 -1
- package/dist/commands/profile/env.js +9 -15
- package/dist/commands/profile/kind.js +3 -7
- package/dist/commands/profile/meta.js +3 -5
- package/dist/commands/profile/new.js +0 -6
- package/dist/commands/profile/pause.js +4 -8
- package/dist/commands/profile/project.js +5 -9
- package/dist/commands/profile/rename.js +3 -7
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/profile.js +4 -3
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/config.js +3 -4
- package/dist/commands/sys/support/prepare.js +8 -4
- package/dist/commands/sys/support/submit.js +2 -3
- package/dist/commands/sys/sync-deps.js +1 -9
- package/dist/commands/sys/sync-project-guidance.js +1 -7
- package/dist/commands/sys/sync-skills.js +1 -11
- package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/human-deliver.test.js +2 -1
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
- package/dist/core/__tests__/lifecycle.test.js +30 -2
- package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
- package/dist/core/canvas/attention.d.ts +2 -0
- package/dist/core/canvas/attention.js +25 -18
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.js +20 -2
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.js +33 -8
- package/dist/core/help.d.ts +28 -2
- package/dist/core/help.js +46 -10
- package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
- package/dist/core/human/component-docs.js +4 -4
- package/dist/core/human/page-html-markdown.d.ts +8 -0
- package/dist/core/human/page-html-markdown.js +260 -0
- package/dist/core/memory/lint.d.ts +15 -0
- package/dist/core/memory/lint.js +150 -90
- package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +14 -7
- package/dist/core/profiles/manifest.js +62 -12
- package/dist/core/profiles/select.d.ts +3 -1
- package/dist/core/profiles/select.js +5 -3
- package/dist/core/profiles/state-block.js +4 -3
- package/dist/core/runtime/boot-root.d.ts +3 -2
- package/dist/core/runtime/canvas-extensions.d.ts +7 -1
- package/dist/core/runtime/canvas-extensions.js +8 -1
- package/dist/core/runtime/lifecycle.d.ts +11 -2
- package/dist/core/runtime/lifecycle.js +15 -2
- package/dist/core/runtime/model-selection.d.ts +4 -0
- package/dist/core/runtime/model-selection.js +5 -0
- package/dist/core/runtime/nodes.js +5 -0
- package/dist/core/runtime/reopen.d.ts +6 -0
- package/dist/core/runtime/reopen.js +12 -1
- package/dist/core/runtime/revive.d.ts +6 -0
- package/dist/core/runtime/revive.js +22 -2
- package/dist/core/runtime/spawn.d.ts +5 -2
- package/dist/core/runtime/spawn.js +18 -32
- package/dist/core/runtime/structured-output.d.ts +6 -0
- package/dist/core/runtime/structured-output.js +6 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +24 -0
- package/dist/core/substrate/on-read.d.ts +17 -1
- package/dist/core/substrate/on-read.js +38 -3
- package/dist/core/substrate/schema.d.ts +3 -3
- package/dist/core/substrate/schema.js +4 -4
- package/dist/core/substrate/surface-match.d.ts +19 -0
- package/dist/core/substrate/surface-match.js +224 -12
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
- package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
- package/dist/daemon/api/handlers/broker-ops.js +21 -0
- package/dist/daemon/api/handlers/canvas.js +10 -0
- package/dist/daemon/api/handlers/messages.js +25 -16
- package/dist/daemon/api/handlers/nodes.js +5 -0
- package/dist/daemon/api/handlers/profiles.js +22 -1
- package/dist/daemon/cron-run.js +19 -1
- package/dist/daemon/manage.d.ts +16 -1
- package/dist/daemon/manage.js +20 -1
- package/dist/daemon/park-pending.d.ts +13 -0
- package/dist/daemon/park-pending.js +42 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
- package/dist/daemon/reconcilers/broker-supervision.js +139 -21
- package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
- package/dist/daemon/reconcilers/live-obligation.js +7 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
- package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
- package/dist/pi-extensions/__tests__/pre-command-gate.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +75 -2
- package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
- package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
- package/dist/shared/generated-context.d.ts +7 -0
- package/dist/shared/generated-context.js +11 -0
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +0 -47
|
@@ -133,7 +133,7 @@ test('an already idle resident with no live obligation reconciles into completed
|
|
|
133
133
|
subscribe(root, nodeId, true);
|
|
134
134
|
await h.tick();
|
|
135
135
|
assert.equal(h.node(nodeId)?.status, 'done', 'an older idle row becomes pruning-eligible without needing a broker revive');
|
|
136
|
-
assert.equal(h.node(nodeId)?.intent, '
|
|
136
|
+
assert.equal(h.node(nodeId)?.intent, 'parked', 'the reconciled row carries the durable park marker (isParked)');
|
|
137
137
|
const wake = readInboxSince(root).find((entry) => entry.from === nodeId && entry.data?.['reason'] === 'child-auto-done');
|
|
138
138
|
assert.ok(wake, 'the subscribing root learns that the parked resident completed without a final report');
|
|
139
139
|
});
|
|
@@ -141,8 +141,19 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
141
141
|
// The park clock is read per supervision pass (unattendedParkMsForDaemon), so
|
|
142
142
|
// it is armed for THIS test only — a file-global 1500ms park clock would also
|
|
143
143
|
// apply to the idle-release manager above and race its own release path.
|
|
144
|
+
//
|
|
145
|
+
// The clock no longer terminalizes directly: it arms a pending park and asks
|
|
146
|
+
// the live engine for one summary turn, and broker SETTLEMENT enacts the park
|
|
147
|
+
// on the good path. That settlement runs in the harness's separate api-server
|
|
148
|
+
// process, where the (deliberately process-local) pending park does not
|
|
149
|
+
// exist — so this seam pins the DEGRADED path: the summary grace expires on
|
|
150
|
+
// the supervision clock and the daemon parks without a report, fanning the
|
|
151
|
+
// doctrine wake. The settlement good path is covered by the in-process
|
|
152
|
+
// broker-settle tests.
|
|
144
153
|
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
154
|
+
const priorGraceMs = process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'];
|
|
145
155
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
156
|
+
process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'] = '1000';
|
|
146
157
|
try {
|
|
147
158
|
const nodeId = h.fabricateBrokerNode({
|
|
148
159
|
id: 'settled-no-work',
|
|
@@ -167,15 +178,20 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
167
178
|
await h.tick(seenAt);
|
|
168
179
|
await h.tick(seenAt + 1499);
|
|
169
180
|
assert.equal(h.fleet.has(nodeId), true, 'the daemon keeps the broker through the full grace');
|
|
181
|
+
// Clock expiry: arms the pending park and requests the summary turn.
|
|
170
182
|
await h.tick(seenAt + 1500);
|
|
183
|
+
// Summary grace expiry: the degraded enactment parks without a report.
|
|
184
|
+
// (If the summary turn's settlement already released the broker in the
|
|
185
|
+
// api-server process, the dormant-inbox reconciler applies the same park.)
|
|
186
|
+
await h.tick(seenAt + 1500 + 1000);
|
|
171
187
|
await h.waitFor(() => {
|
|
172
188
|
const node = h.node(nodeId);
|
|
173
|
-
return node?.status === 'done' && node.intent === '
|
|
174
|
-
}, { timeoutMs: 10_000, intervalMs: 20, label: 'unattended
|
|
189
|
+
return node?.status === 'done' && node.intent === 'parked' && !isPidAlive(boot.pid);
|
|
190
|
+
}, { timeoutMs: 10_000, intervalMs: 20, label: 'unattended park and broker exit' });
|
|
175
191
|
// The pid vanishing is not the fleet learning about it: wait for the child
|
|
176
192
|
// 'exit' delivery + its policy job, which is what actually frees the slot.
|
|
177
193
|
await h.awaitFleetExit(nodeId);
|
|
178
|
-
assert.equal(h.fleet.has(nodeId), false, 'exit policy forgets the
|
|
194
|
+
assert.equal(h.fleet.has(nodeId), false, 'exit policy forgets the parked broker');
|
|
179
195
|
assert.equal(h.node(nodeId)?.status, 'done', 'the row is terminal and eligible for history pruning');
|
|
180
196
|
const wake = readInboxSince(root).find((entry) => entry.from === nodeId && entry.data?.['reason'] === 'child-auto-done');
|
|
181
197
|
assert.ok(wake, 'the subscribing root is woken when the unattended clock completes the resident');
|
|
@@ -188,11 +204,17 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
188
204
|
delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
189
205
|
else
|
|
190
206
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
207
|
+
if (priorGraceMs === undefined)
|
|
208
|
+
delete process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'];
|
|
209
|
+
else
|
|
210
|
+
process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'] = priorGraceMs;
|
|
191
211
|
}
|
|
192
212
|
});
|
|
193
213
|
test('a non-attending observer does not prevent unattended resident parking', { timeout: 30_000 }, async () => {
|
|
194
214
|
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
215
|
+
const priorGraceMs = process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'];
|
|
195
216
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
217
|
+
process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'] = '1000';
|
|
196
218
|
const nodeId = h.fabricateBrokerNode({
|
|
197
219
|
id: 'settled-with-system-observer',
|
|
198
220
|
parent: root,
|
|
@@ -209,10 +231,11 @@ test('a non-attending observer does not prevent unattended resident parking', {
|
|
|
209
231
|
const seenAt = 12_000;
|
|
210
232
|
await h.tick(seenAt);
|
|
211
233
|
await h.tick(seenAt + 1500);
|
|
234
|
+
await h.tick(seenAt + 1500 + 1000);
|
|
212
235
|
await h.waitFor(() => h.node(nodeId)?.status === 'done' && !isPidAlive(boot.pid), {
|
|
213
236
|
timeoutMs: 10_000,
|
|
214
237
|
intervalMs: 20,
|
|
215
|
-
label: 'system-observed unattended
|
|
238
|
+
label: 'system-observed unattended park and broker exit',
|
|
216
239
|
});
|
|
217
240
|
await h.awaitFleetExit(nodeId);
|
|
218
241
|
}
|
|
@@ -221,6 +244,10 @@ test('a non-attending observer does not prevent unattended resident parking', {
|
|
|
221
244
|
delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
222
245
|
else
|
|
223
246
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
247
|
+
if (priorGraceMs === undefined)
|
|
248
|
+
delete process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'];
|
|
249
|
+
else
|
|
250
|
+
process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'] = priorGraceMs;
|
|
224
251
|
}
|
|
225
252
|
});
|
|
226
253
|
test('a connected broker viewer keeps an obligation-free resident live', { timeout: 30_000 }, async () => {
|
|
@@ -3,6 +3,8 @@ export interface TicketEntry {
|
|
|
3
3
|
name: string;
|
|
4
4
|
cwd: string;
|
|
5
5
|
count: number;
|
|
6
|
+
/** Open ticket titles for this node, newest first (scan order). */
|
|
7
|
+
subjects: string[];
|
|
6
8
|
}
|
|
7
9
|
/** Drop the cached walk. Callers that swap the canvas home out from under a live
|
|
8
10
|
* process (tests, and any in-process home switch) must call this: the cache is
|
|
@@ -33,57 +33,64 @@ let ticketCountCache = null;
|
|
|
33
33
|
export function invalidateTicketCounts() {
|
|
34
34
|
ticketCountCache = null;
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function ticketSubjects() {
|
|
37
37
|
const now = Date.now();
|
|
38
38
|
if (ticketCountCache !== null && now - ticketCountCache.at < TICKET_COUNT_TTL_MS) {
|
|
39
|
-
return ticketCountCache.
|
|
39
|
+
return ticketCountCache.subjects;
|
|
40
40
|
}
|
|
41
|
-
const
|
|
41
|
+
const subjects = new Map();
|
|
42
42
|
try {
|
|
43
43
|
for (const ticket of filterTerminalReviewTickets(scanInbox())) {
|
|
44
44
|
const nodeId = askingNodeId(ticket.dir);
|
|
45
|
-
if (nodeId
|
|
46
|
-
|
|
45
|
+
if (nodeId === null)
|
|
46
|
+
continue;
|
|
47
|
+
const list = subjects.get(nodeId);
|
|
48
|
+
if (list === undefined)
|
|
49
|
+
subjects.set(nodeId, [ticket.title]);
|
|
50
|
+
else
|
|
51
|
+
list.push(ticket.title);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
catch {
|
|
50
55
|
// An unavailable ticket store or absent root has no display consequence.
|
|
51
56
|
}
|
|
52
|
-
ticketCountCache = { at: Date.now(),
|
|
53
|
-
return
|
|
57
|
+
ticketCountCache = { at: Date.now(), subjects };
|
|
58
|
+
return subjects;
|
|
54
59
|
}
|
|
55
60
|
/** Count pending tickets asked by one node. */
|
|
56
61
|
export function countTickets(nodeId) {
|
|
57
|
-
return
|
|
62
|
+
return ticketSubjects().get(nodeId)?.length ?? 0;
|
|
58
63
|
}
|
|
59
64
|
/** Pending tickets for every reachable asking node, including the root. */
|
|
60
65
|
export function pendingTicketsForView(rootId) {
|
|
61
|
-
const
|
|
66
|
+
const found = ticketSubjects();
|
|
62
67
|
const entries = [];
|
|
63
68
|
for (const id of [rootId, ...view(rootId)]) {
|
|
64
69
|
const node = getNode(id);
|
|
65
|
-
const
|
|
66
|
-
if (node !== null &&
|
|
67
|
-
entries.push({ node_id: id, name: node.name, cwd: node.cwd, count });
|
|
70
|
+
const subjects = found.get(id) ?? [];
|
|
71
|
+
if (node !== null && subjects.length > 0) {
|
|
72
|
+
entries.push({ node_id: id, name: node.name, cwd: node.cwd, count: subjects.length, subjects });
|
|
73
|
+
}
|
|
68
74
|
}
|
|
69
75
|
return entries;
|
|
70
76
|
}
|
|
71
77
|
/** Per-node pending ticket counts for an explicit node set. */
|
|
72
78
|
export function ticketCountsForNodes(ids) {
|
|
73
|
-
const found =
|
|
79
|
+
const found = ticketSubjects();
|
|
74
80
|
const counts = {};
|
|
75
81
|
for (const id of ids)
|
|
76
|
-
counts[id] = found.get(id) ?? 0;
|
|
82
|
+
counts[id] = found.get(id)?.length ?? 0;
|
|
77
83
|
return counts;
|
|
78
84
|
}
|
|
79
85
|
/** Pending tickets across the entire canvas, attributed to asking nodes. */
|
|
80
86
|
export function ticketsAcrossCanvas() {
|
|
81
|
-
const
|
|
87
|
+
const found = ticketSubjects();
|
|
82
88
|
const entries = [];
|
|
83
89
|
for (const node of listNodes()) {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
entries.push({ node_id: node.node_id, name: node.name, cwd: node.cwd, count });
|
|
90
|
+
const subjects = found.get(node.node_id) ?? [];
|
|
91
|
+
if (subjects.length > 0) {
|
|
92
|
+
entries.push({ node_id: node.node_id, name: node.name, cwd: node.cwd, count: subjects.length, subjects });
|
|
93
|
+
}
|
|
87
94
|
}
|
|
88
95
|
return entries;
|
|
89
96
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-review-boundary", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve", "canvas-preview-result"];
|
|
1
|
+
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-review-boundary", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve", "canvas-preview-result", "canvas-prompt-scrub"];
|
|
2
2
|
/** Structural shipped-hook identity: a canvas-hook stem directly under a
|
|
3
3
|
* `pi-extensions` directory. A user's own extension elsewhere may share the
|
|
4
4
|
* stem and is not shipped. */
|
|
@@ -7,9 +7,15 @@ export const CANVAS_EXTENSION_NAMES = [
|
|
|
7
7
|
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
8
8
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
9
9
|
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
10
|
-
'canvas-bash-valve', 'canvas-preview-result',
|
|
10
|
+
'canvas-bash-valve', 'canvas-preview-result', 'canvas-prompt-scrub',
|
|
11
11
|
];
|
|
12
12
|
const HISTORICAL_CANVAS_EXTENSION_PREFIXES = [
|
|
13
|
+
[
|
|
14
|
+
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
15
|
+
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
16
|
+
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
17
|
+
'canvas-bash-valve', 'canvas-preview-result',
|
|
18
|
+
],
|
|
13
19
|
[
|
|
14
20
|
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
15
21
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
@@ -451,16 +451,34 @@ export function nodeArtifacts(nodeId, types) {
|
|
|
451
451
|
const want = new Set(types !== undefined && types.length > 0 ? types : ['report', 'doc', 'roadmap']);
|
|
452
452
|
const desc = node.description ?? '';
|
|
453
453
|
const out = [];
|
|
454
|
+
// One malformed file must not break the whole listing — the frontmatter
|
|
455
|
+
// parser throws by design, so this collection layer isolates per file, same
|
|
456
|
+
// as buildCorpus. A context dir often holds foreign markdown (a checked-out
|
|
457
|
+
// repo, node_modules) whose `---` header is not crouter frontmatter.
|
|
454
458
|
if (want.has('report')) {
|
|
455
459
|
for (const f of listReportFiles(nodeId)) {
|
|
456
|
-
|
|
460
|
+
let a;
|
|
461
|
+
try {
|
|
462
|
+
a = reportArtifact(row, desc, f);
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
warnInvalidFrontmatter(join(reportsDir(nodeId), f), e);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
457
468
|
if (a !== null)
|
|
458
469
|
out.push(a);
|
|
459
470
|
}
|
|
460
471
|
}
|
|
461
472
|
if (want.has('doc') || want.has('roadmap')) {
|
|
462
473
|
for (const abs of listDocFiles(nodeId)) {
|
|
463
|
-
|
|
474
|
+
let a;
|
|
475
|
+
try {
|
|
476
|
+
a = docArtifact(row, desc, abs);
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
warnInvalidFrontmatter(abs, e);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
464
482
|
if (a.source === 'roadmap' && !want.has('roadmap'))
|
|
465
483
|
continue;
|
|
466
484
|
if (a.source === 'doc' && !want.has('doc'))
|
|
@@ -17,7 +17,7 @@ export type Lifecycle = 'terminal' | 'resident';
|
|
|
17
17
|
/** base = hands-on worker; orchestrator = delegating manager. Bespoke per kind. */
|
|
18
18
|
export type Mode = 'base' | 'orchestrator';
|
|
19
19
|
/** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
|
|
20
|
-
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | null;
|
|
20
|
+
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
|
|
21
21
|
/** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
|
|
22
22
|
* org chart, views, completion routing). `spawned_by` is audit only. */
|
|
23
23
|
export type EdgeType = 'subscribes_to' | 'spawned_by';
|
package/dist/core/command.js
CHANGED
|
@@ -189,11 +189,16 @@ function renderNode(node) {
|
|
|
189
189
|
return renderLeafArgv(node.help);
|
|
190
190
|
}
|
|
191
191
|
/** Render effective Effects for a hook-eligible core leaf, then append any
|
|
192
|
-
* commands.json helpAddenda beneath the contract.
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
|
|
192
|
+
* commands.json helpAddenda beneath the ordinary contract. Focused parameter
|
|
193
|
+
* help returns only the leaf-owned parameter view, because the caller already
|
|
194
|
+
* read the ordinary contract and addenda before following its road sign. Both
|
|
195
|
+
* plugin lookups read stored manifest bytes only. Addenda validation receives
|
|
196
|
+
* the install gate's exact reserved core names and full core path set, so a
|
|
197
|
+
* manifest rejected at ingress contributes nothing here either. */
|
|
198
|
+
async function renderNodeWithAddenda(node, path, focusedFlag) {
|
|
199
|
+
if (focusedFlag !== undefined && node.kind === 'leaf') {
|
|
200
|
+
return renderLeafArgv(node.help, focusedFlag);
|
|
201
|
+
}
|
|
197
202
|
const body = node.kind === 'leaf' && node.effectiveHelp !== undefined
|
|
198
203
|
? renderLeafArgv(await node.effectiveHelp())
|
|
199
204
|
: renderNode(node);
|
|
@@ -215,6 +220,21 @@ async function renderNodeWithAddenda(node, path) {
|
|
|
215
220
|
function helpRequested(remaining) {
|
|
216
221
|
return remaining.some((t) => t === '-h' || t === '--help');
|
|
217
222
|
}
|
|
223
|
+
/** Resolve the canonical focused-help form `crtr <leaf> --<flag> -h`.
|
|
224
|
+
* Only flags that explicitly own a focusedHelp contract select that view;
|
|
225
|
+
* every other historical help invocation renders ordinary leaf help. */
|
|
226
|
+
function focusedHelpFlag(node, remaining) {
|
|
227
|
+
if (node.kind !== 'leaf' || remaining.length !== 2)
|
|
228
|
+
return undefined;
|
|
229
|
+
const [flagToken, helpToken] = remaining;
|
|
230
|
+
if ((helpToken !== '-h' && helpToken !== '--help')
|
|
231
|
+
|| !flagToken.startsWith('--')
|
|
232
|
+
|| flagToken.includes('='))
|
|
233
|
+
return undefined;
|
|
234
|
+
const name = flagToken.slice(2);
|
|
235
|
+
const flag = (node.help.params ?? []).find((param) => param.kind === 'flag' && param.name === name);
|
|
236
|
+
return flag?.focusedHelp !== undefined ? name : undefined;
|
|
237
|
+
}
|
|
218
238
|
/** Recover the RAW argv slice after a matched passthrough branch's path,
|
|
219
239
|
* unaffected by the `--json`-stripping done to build `tokens`. `path` is a
|
|
220
240
|
* prefix of non-`--json` tokens in order, so walking `rawTokens` while
|
|
@@ -387,7 +407,10 @@ export async function parseArgv(params, tokens, options) {
|
|
|
387
407
|
}
|
|
388
408
|
const rawVal = inlineValue !== undefined ? inlineValue : tokens[++i];
|
|
389
409
|
if (rawVal === undefined || rawVal.startsWith('--')) {
|
|
390
|
-
|
|
410
|
+
const focusedNext = flagDef.focusedHelp !== undefined && (options?.leafPath?.length ?? 0) > 0
|
|
411
|
+
? `Run \`crtr ${options.leafPath.join(' ')} --${flagName} -h\` and read the focused value contract before retrying.`
|
|
412
|
+
: undefined;
|
|
413
|
+
throw parseArgvError('missing_parameter', `--${flagName} requires a value`, `--${flagName}`, flagName, focusedNext);
|
|
391
414
|
}
|
|
392
415
|
let parsedValue;
|
|
393
416
|
if (flagDef.type === 'int') {
|
|
@@ -620,9 +643,11 @@ export async function runCli(root, argv) {
|
|
|
620
643
|
&& remaining[0] !== '-h' && remaining[0] !== '--help') {
|
|
621
644
|
throw unknownPathError(node, path, remaining[0]);
|
|
622
645
|
}
|
|
623
|
-
// Help anywhere in remaining tokens → print node help and exit
|
|
646
|
+
// Help anywhere in remaining tokens → print node help and exit. The exact
|
|
647
|
+
// leaf form `--<flag> -h` selects that flag's focused continuation when one
|
|
648
|
+
// is declared; no invocation state or acknowledgement token is persisted.
|
|
624
649
|
if (helpRequested(remaining)) {
|
|
625
|
-
process.stdout.write(await renderNodeWithAddenda(node, path) + '\n');
|
|
650
|
+
process.stdout.write(await renderNodeWithAddenda(node, path, focusedHelpFlag(node, remaining)) + '\n');
|
|
626
651
|
process.exitCode = ExitCode.SUCCESS;
|
|
627
652
|
return;
|
|
628
653
|
}
|
package/dist/core/help.d.ts
CHANGED
|
@@ -28,6 +28,20 @@ export interface PositionalParam {
|
|
|
28
28
|
}
|
|
29
29
|
/** How a local file named by a `path` param is encoded into the request. */
|
|
30
30
|
export type FileEncoding = 'text' | 'base64';
|
|
31
|
+
/** Additional guidance for an uncommon flag whose safe selection, value
|
|
32
|
+
* grammar, dynamic choices, or effects do not fit in its compact leaf-schema
|
|
33
|
+
* row. The compact row routes the agent to `crtr <leaf> --<flag> -h`; that
|
|
34
|
+
* focused continuation renders only this parameter contract. */
|
|
35
|
+
export interface FocusedFlagHelp {
|
|
36
|
+
/** Concrete cases that justify overriding the default, plus when to omit. */
|
|
37
|
+
whenToUse: string;
|
|
38
|
+
/** Complete accepted-value grammar and default/inheritance behavior. */
|
|
39
|
+
value: string;
|
|
40
|
+
/** Flag-specific persistent or lifecycle effects and coupled interactions. */
|
|
41
|
+
effects?: string[];
|
|
42
|
+
/** Bounded dynamic choices owned by this flag, rendered only in focused help. */
|
|
43
|
+
dynamicState?: () => string | null;
|
|
44
|
+
}
|
|
31
45
|
/** Long-form flag (`--name`). */
|
|
32
46
|
export interface FlagParam {
|
|
33
47
|
kind: 'flag';
|
|
@@ -37,7 +51,13 @@ export interface FlagParam {
|
|
|
37
51
|
/** Required only when type is 'enum'. */
|
|
38
52
|
choices?: string[];
|
|
39
53
|
required: boolean;
|
|
54
|
+
/** Compact discovery contract. When focusedHelp is present, state what the
|
|
55
|
+
* flag changes and the condition that could justify it; the renderer adds
|
|
56
|
+
* the exact focused-help road sign automatically. */
|
|
40
57
|
constraint: string;
|
|
58
|
+
/** Opt into focused `--flag -h` guidance. Use only when omission is normal
|
|
59
|
+
* and the complete safe contract cannot fit in one compact row. */
|
|
60
|
+
focusedHelp?: FocusedFlagHelp;
|
|
41
61
|
default?: string | number | boolean;
|
|
42
62
|
/** When true, the flag may appear multiple times; values accumulate into an
|
|
43
63
|
* array (parseArgv collects them; body-placed REST params ship as a JSON array). */
|
|
@@ -196,7 +216,9 @@ export interface LeafHelp {
|
|
|
196
216
|
name: string;
|
|
197
217
|
summary: string;
|
|
198
218
|
/** Optional long-form workflow prose rendered immediately after the summary
|
|
199
|
-
* line
|
|
219
|
+
* line, before the schema. Carried only by leaves whose correct use needs
|
|
220
|
+
* prose beyond the schema (e.g. node new's prompt-writing guidance,
|
|
221
|
+
* node yield's pre-yield checklist). */
|
|
200
222
|
guide?: string;
|
|
201
223
|
params?: InputParam[];
|
|
202
224
|
/** Note appended when there is no input (replaces the Input block). */
|
|
@@ -231,4 +253,8 @@ export type EffectiveLeafHelp = Readonly<Omit<LeafHelp, 'effects'> & {
|
|
|
231
253
|
export declare function stateBlock(tag: string, attrs: Record<string, string | number>, body: string): string;
|
|
232
254
|
export declare function renderRoot(h: RootHelp): string;
|
|
233
255
|
export declare function renderBranch(h: BranchHelp): string;
|
|
234
|
-
|
|
256
|
+
/** Render one leaf's ordinary contract, or the focused continuation for one
|
|
257
|
+
* uncommon flag selected through `--flag -h`. The caller reaches focused help
|
|
258
|
+
* from the ordinary row, so that view contains only leaf identity and the new
|
|
259
|
+
* parameter detail instead of repeating information already in context. */
|
|
260
|
+
export declare function renderLeafArgv(h: LeafHelp | EffectiveLeafHelp, focusedFlag?: string): string;
|
package/dist/core/help.js
CHANGED
|
@@ -47,7 +47,7 @@ function pad(s, width) {
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
const IO_CONTRACT = 'I/O contract: flags and positional args on input; stdout is agent-ready markdown/XML you\n' +
|
|
49
49
|
'act on directly — read it as a continuation of your prompt, don\'t parse it as data.\n' +
|
|
50
|
-
'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h.';
|
|
50
|
+
'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h; a leaf may route an uncommon flag to focused --flag -h guidance.';
|
|
51
51
|
// Behavioral instruction (not a schema) — engrained in the appended system
|
|
52
52
|
// prompt so the model treats unfamiliar capabilities as a cue to discover the
|
|
53
53
|
// contract, never to guess, AND reads a command's contract before invoking it.
|
|
@@ -57,6 +57,7 @@ const CAPABILITY_DISCOVERY = 'Before running a crtr command that changes state
|
|
|
57
57
|
"deletes anything — whose exact contract (args, flags, effects) you haven't " +
|
|
58
58
|
'verified this session, run `-h` on it and read the schema first — a reliable ' +
|
|
59
59
|
'read beats a guess that wastes a turn or triggers an unintended effect. ' +
|
|
60
|
+
'When a leaf routes a flag to focused help, read that focused help before using the flag; the compact row is discovery, not its invocation contract. ' +
|
|
60
61
|
"Same when the user names a capability you don't fully recognize: " +
|
|
61
62
|
'`-h` it before acting.';
|
|
62
63
|
/** Lines for a command's subcommand affordance at root: any promoted
|
|
@@ -204,7 +205,12 @@ function envDefaultNote(envVar) {
|
|
|
204
205
|
? ''
|
|
205
206
|
: ` Defaults to $${envVar} when that is set and non-empty in the environment, and counts as supplied.`;
|
|
206
207
|
}
|
|
207
|
-
function
|
|
208
|
+
function focusedFlagHelpNote(flag, leafName) {
|
|
209
|
+
if (flag.focusedHelp === undefined)
|
|
210
|
+
return '';
|
|
211
|
+
return ` Before using this flag, run \`crtr ${leafName} --${flag.name} -h\` and read its focused guidance.`;
|
|
212
|
+
}
|
|
213
|
+
function paramDesc(p, leafName) {
|
|
208
214
|
const req = p.required ? 'required' : 'optional';
|
|
209
215
|
if (p.kind === 'positional') {
|
|
210
216
|
const repeatable = p.repeatable === true
|
|
@@ -222,8 +228,9 @@ function paramDesc(p) {
|
|
|
222
228
|
}
|
|
223
229
|
// flag
|
|
224
230
|
const f = p;
|
|
231
|
+
const focusedNote = focusedFlagHelpNote(f, leafName);
|
|
225
232
|
if (f.type === 'bool')
|
|
226
|
-
return `${req} boolean. Presence means true. ${f.constraint}`.trim();
|
|
233
|
+
return `${req} boolean. Presence means true. ${f.constraint}${focusedNote}`.trim();
|
|
227
234
|
const dflt = f.default !== undefined ? ` Default: ${String(f.default)}.` : '';
|
|
228
235
|
const choices = f.type === 'enum' && f.choices !== undefined
|
|
229
236
|
? ` One of: ${f.choices.join(', ')}.`
|
|
@@ -231,24 +238,53 @@ function paramDesc(p) {
|
|
|
231
238
|
const repeatable = f.repeatable === true
|
|
232
239
|
? ' Repeatable — pass multiple times to accumulate.'
|
|
233
240
|
: '';
|
|
234
|
-
return `${f.type}, ${req}.${choices}${dflt}${fileEncodingNote(f.encoding)}${envDefaultNote(f.defaultFromEnv)} ${f.constraint}${repeatable}`.trim();
|
|
241
|
+
return `${f.type}, ${req}.${choices}${dflt}${fileEncodingNote(f.encoding)}${envDefaultNote(f.defaultFromEnv)} ${f.constraint}${repeatable}${focusedNote}`.trim();
|
|
235
242
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
/** Render one leaf's ordinary contract, or the focused continuation for one
|
|
244
|
+
* uncommon flag selected through `--flag -h`. The caller reaches focused help
|
|
245
|
+
* from the ordinary row, so that view contains only leaf identity and the new
|
|
246
|
+
* parameter detail instead of repeating information already in context. */
|
|
247
|
+
export function renderLeafArgv(h, focusedFlag) {
|
|
248
|
+
const lines = [`${h.name}: ${h.summary}.`];
|
|
249
|
+
// Hidden flags are parsed but never advertised in -h (see FlagParam.hidden).
|
|
250
|
+
const params = (h.params ?? []).filter((p) => !(p.kind === 'flag' && p.hidden === true));
|
|
251
|
+
const focused = focusedFlag === undefined
|
|
252
|
+
? undefined
|
|
253
|
+
: params.find((p) => p.kind === 'flag' && p.name === focusedFlag && p.focusedHelp !== undefined);
|
|
254
|
+
if (focused?.focusedHelp !== undefined) {
|
|
255
|
+
const detail = focused.focusedHelp;
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push(`<parameter name="${focused.name}">`);
|
|
258
|
+
lines.push('When to use');
|
|
259
|
+
lines.push(` ${detail.whenToUse}`);
|
|
260
|
+
lines.push('');
|
|
261
|
+
lines.push('Value');
|
|
262
|
+
lines.push(` ${detail.value}`);
|
|
263
|
+
if ((detail.effects ?? []).length > 0) {
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push('Effects');
|
|
266
|
+
for (const effect of detail.effects ?? [])
|
|
267
|
+
lines.push(` ${effect}`);
|
|
268
|
+
}
|
|
269
|
+
const focusedState = evalDynamic(detail.dynamicState);
|
|
270
|
+
if (focusedState !== null) {
|
|
271
|
+
lines.push('');
|
|
272
|
+
lines.push(focusedState);
|
|
273
|
+
}
|
|
274
|
+
lines.push('</parameter>');
|
|
275
|
+
return lines.join('\n');
|
|
276
|
+
}
|
|
239
277
|
if (h.guide !== undefined) {
|
|
240
278
|
lines.push('');
|
|
241
279
|
lines.push(h.guide);
|
|
242
280
|
}
|
|
243
281
|
lines.push('');
|
|
244
|
-
// Hidden flags are parsed but never advertised in -h (see FlagParam.hidden).
|
|
245
|
-
const params = (h.params ?? []).filter((p) => !(p.kind === 'flag' && p.hidden === true));
|
|
246
282
|
if (params.length > 0) {
|
|
247
283
|
lines.push('Input');
|
|
248
284
|
const labels = params.map(paramLabel);
|
|
249
285
|
const colW = maxLen(labels);
|
|
250
286
|
for (let i = 0; i < params.length; i++) {
|
|
251
|
-
lines.push(` ${pad(labels[i], colW)} ${paramDesc(params[i])}`);
|
|
287
|
+
lines.push(` ${pad(labels[i], colW)} ${paramDesc(params[i], h.name)}`);
|
|
252
288
|
}
|
|
253
289
|
}
|
|
254
290
|
else {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { projectHtmlDocumentMarkdown } from '../page-html-markdown.js';
|
|
4
|
+
test('an authored HTML page projects to readable markdown', () => {
|
|
5
|
+
const markdown = projectHtmlDocumentMarkdown(`<!doctype html>
|
|
6
|
+
<html><head><title>Deploy sign-off</title><style>body{color:red}</style></head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>Deploy sign-off</h1>
|
|
9
|
+
<p>Staging has been green for <strong>6 hours</strong>; the risk is the <em>migration</em>.</p>
|
|
10
|
+
<ul><li>Smoke suite passed</li><li>Rollback rehearsed — 90s</li><li>See <a href="https://ci.example.com/42">run 42</a></li></ul>
|
|
11
|
+
<table><tr><th>Step</th><th>Duration</th></tr><tr><td>Migrate</td><td>4m 10s</td></tr></table>
|
|
12
|
+
<pre><code>kubectl rollout status deploy/orders</code></pre>
|
|
13
|
+
<blockquote>Waiting on your call.</blockquote>
|
|
14
|
+
<script>console.log('nope')</script>
|
|
15
|
+
</body></html>`);
|
|
16
|
+
assert.equal(markdown, [
|
|
17
|
+
'# Deploy sign-off',
|
|
18
|
+
'',
|
|
19
|
+
'Staging has been green for **6 hours**; the risk is the *migration*.',
|
|
20
|
+
'',
|
|
21
|
+
'- Smoke suite passed',
|
|
22
|
+
'- Rollback rehearsed — 90s',
|
|
23
|
+
'- See [run 42](https://ci.example.com/42)',
|
|
24
|
+
'',
|
|
25
|
+
'| Step | Duration |',
|
|
26
|
+
'| --- | --- |',
|
|
27
|
+
'| Migrate | 4m 10s |',
|
|
28
|
+
'',
|
|
29
|
+
'```',
|
|
30
|
+
'kubectl rollout status deploy/orders',
|
|
31
|
+
'```',
|
|
32
|
+
'',
|
|
33
|
+
'> Waiting on your call.',
|
|
34
|
+
].join('\n'));
|
|
35
|
+
});
|
|
36
|
+
test('the projection survives malformed markup rather than losing the document', () => {
|
|
37
|
+
// A stray </style> must not reopen the head, and a stray <script> must not
|
|
38
|
+
// swallow the rest of the page: both are common in hand-written HTML.
|
|
39
|
+
assert.match(projectHtmlDocumentMarkdown('</style><p>Body text</p>'), /Body text/);
|
|
40
|
+
assert.equal(projectHtmlDocumentMarkdown('<p>Before</p><script>x=1;</script><p>After</p>'), 'Before\n\nAfter');
|
|
41
|
+
assert.equal(projectHtmlDocumentMarkdown('<p>Alone<'), 'Alone\\<');
|
|
42
|
+
assert.equal(projectHtmlDocumentMarkdown(''), '');
|
|
43
|
+
// Ordered lists number themselves, and a link with no label leaves no husk.
|
|
44
|
+
assert.equal(projectHtmlDocumentMarkdown('<ol><li>First</li><li>Second</li></ol>'), '1. First\n2. Second');
|
|
45
|
+
assert.equal(projectHtmlDocumentMarkdown('<p>Read <a href="https://x.test"></a>this</p>'), 'Read this');
|
|
46
|
+
// Markdown structure in authored prose is escaped, not obeyed.
|
|
47
|
+
assert.equal(projectHtmlDocumentMarkdown('<p># not a heading</p>'), '\\# not a heading');
|
|
48
|
+
});
|
|
@@ -10,7 +10,7 @@ const SLOT_RULES = `Component contract
|
|
|
10
10
|
- When a page has two or more response-bearing components, every one needs a nonempty \`label\`.
|
|
11
11
|
- Config objects and every documented nested object reject unknown fields.
|
|
12
12
|
- The page is rendered once at submit to build the ticket manifest, so every prop reaches it whatever its shape — mapped data, a helper component, a value from \`useState\`. Event handlers are dropped, because a manifest is JSON. The user's answer lands in \`response.json\` keyed by this \`id\`.`;
|
|
13
|
-
const COMMENT_SHAPE = `Comments are \`{id:string (nonempty), text:string, anchor:Anchor}\` and qualify the answer itself. \`Anchor\` is \`{kind:"option",optionId:string}\` or \`{kind:"row",rowId:string}\`; each component accepts only the anchors named next.
|
|
13
|
+
const COMMENT_SHAPE = `Comments are \`{id:string (nonempty), text:string, anchor:Anchor}\` and qualify the answer itself. \`Anchor\` is \`{kind:"option",optionId:string}\` or \`{kind:"row",rowId:string}\`; each component accepts only the anchors named next. The reader writes one against a specific option or row in whatever host they read in — in the terminal inbox, \`c\` on the selected one — so every option and row needs a label that reads on its own. Feedback on the page's own prose is a separate channel, offered only by hosts that render the page, and never part of a response.`;
|
|
14
14
|
const DISPLAY_RULES = `Display-only: it is not recorded in the page manifest and contributes nothing to the response. Every display component also takes \`className\` for Tailwind utilities, and standard shadcn props otherwise.`;
|
|
15
15
|
export const BUILTIN_COMPONENT_DOCS = [
|
|
16
16
|
{
|
|
@@ -103,12 +103,12 @@ Example
|
|
|
103
103
|
{
|
|
104
104
|
names: ['UserText'],
|
|
105
105
|
group: 'slot',
|
|
106
|
-
selectionLine: '`UserText` — writing surface (wire kind `text`). Use
|
|
106
|
+
selectionLine: '`UserText` — writing surface (wire kind `text`). Use whenever the answer is written rather than chosen: a value, a paragraph, or a draft you seeded for them to revise; `singleLine` for a subject, name, or URL. This is the page\'s surface for freeform input — a picker\'s `allowFreetext` is an escape hatch beside its options, not a way to collect prose. Prose the user only reads is a plain element.',
|
|
107
107
|
contract: `\`<UserText>\` — wire kind \`text\`
|
|
108
108
|
|
|
109
109
|
Response-bearing. The response is \`{text:string, edited:boolean}\`: the text the user hands back, and whether they changed what you drafted.
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
Reach for it whenever what you need back is written rather than picked — a value, a paragraph, or a draft you seeded with \`initialText\` for them to revise. It is the page's one writing surface: \`allowFreetext\` on a picker adds a single "something else" row beside the options, so prose you actually want belongs here instead. It collects text, it does not present it — text the user only reads is a plain element with Tailwind classes, \`<p className="whitespace-pre-wrap">\`, which renders the same and asks nothing of them.
|
|
112
112
|
|
|
113
113
|
${SLOT_RULES}
|
|
114
114
|
|
|
@@ -164,7 +164,7 @@ Example
|
|
|
164
164
|
selectionLine: '`UserCards` — visual item picker (wire kind `cards`). Use when the user chooses among rich, card-shaped alternatives.',
|
|
165
165
|
contract: `\`<UserCards>\` — wire kind \`cards\`
|
|
166
166
|
|
|
167
|
-
Response-bearing. The response is \`{selectedCardIds:string[]}
|
|
167
|
+
Response-bearing. The response is \`{selectedCardIds:string[]}\` — the selection alone; cards carry no comments.
|
|
168
168
|
|
|
169
169
|
${SLOT_RULES}
|
|
170
170
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** An HTML page's document projected to markdown, so a terminal reader sees the
|
|
2
|
+
* page's actual content instead of a placeholder line. HTML pages are authored
|
|
3
|
+
* for a browser surface and carry no slots, so this is a reading projection
|
|
4
|
+
* only — layout, styling, and scripting are dropped, and what survives is the
|
|
5
|
+
* prose, headings, lists, links, code, and table cells the author wrote. */
|
|
6
|
+
/** Project one authored HTML document to markdown. Never throws: a document
|
|
7
|
+
* this cannot make sense of yields whatever text it did find, down to ''. */
|
|
8
|
+
export declare function projectHtmlDocumentMarkdown(document: string): string;
|