@penpair/core 0.2.0 → 0.3.1
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/adapters.js +58 -11
- package/dist/approvals.js +143 -29
- package/dist/atomic-output.js +9 -4
- package/dist/audit-rules.js +345 -79
- package/dist/backup-note.js +29 -8
- package/dist/baseline-coverage.js +169 -35
- package/dist/baselines-config.js +119 -29
- package/dist/baselines.js +280 -57
- package/dist/cli-arguments.js +18 -6
- package/dist/cli-main.js +1645 -392
- package/dist/client.d.ts +5 -2
- package/dist/client.js +127 -28
- package/dist/command-result.js +341 -52
- package/dist/coordination.js +91 -29
- package/dist/credentials.js +57 -13
- package/dist/deep-check.js +56 -12
- package/dist/design-spec.js +189 -49
- package/dist/endpoint.js +102 -34
- package/dist/export.js +86 -18
- package/dist/file-archive.js +33 -7
- package/dist/file-guard.js +29 -11
- package/dist/formats.js +215 -14
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/inspection.d.ts +1 -1
- package/dist/inspection.js +378 -65
- package/dist/instance/config-schema.js +400 -41
- package/dist/instance/config.js +101 -30
- package/dist/instance/connect.js +151 -44
- package/dist/instance/connection.d.ts +26 -5
- package/dist/instance/connection.js +194 -21
- package/dist/instance/gitignore.js +5 -2
- package/dist/instance/init.js +32 -7
- package/dist/instance/runner.js +32 -10
- package/dist/instance/setup.js +46 -13
- package/dist/instance/tty.js +26 -8
- package/dist/journal.d.ts +10 -0
- package/dist/journal.js +212 -49
- package/dist/kit-smoke.js +82 -20
- package/dist/kit-source.js +10 -40
- package/dist/kit.js +102 -34
- package/dist/layout-policy.d.ts +2 -0
- package/dist/layout-policy.js +86 -0
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +27 -70
- package/dist/live-smoke.js +13 -4
- package/dist/local-profile.js +2 -1
- package/dist/mcp-server.js +360 -47
- package/dist/native-api.js +47 -11
- package/dist/onboard.js +33 -7
- package/dist/operation.d.ts +2 -2
- package/dist/operation.js +279 -69
- package/dist/page-session.d.ts +23 -0
- package/dist/page-session.js +142 -0
- package/dist/png.js +18 -3
- package/dist/portable-tokens.js +254 -57
- package/dist/probe.d.ts +1 -1
- package/dist/probe.js +48 -11
- package/dist/project-input.js +24 -10
- package/dist/project-snippets.js +18 -6
- package/dist/project-status.js +133 -24
- package/dist/registry.js +217 -60
- package/dist/run-file.js +129 -30
- package/dist/shots.js +108 -26
- package/dist/skill-check-entry.d.ts +17 -0
- package/dist/skill-check-entry.js +89 -0
- package/dist/skill-check-instructions.d.ts +7 -0
- package/dist/skill-check-instructions.js +39 -0
- package/dist/skill-check.d.ts +15 -0
- package/dist/skill-check.js +36 -0
- package/dist/skill-file.d.ts +2 -0
- package/dist/skill-file.js +31 -0
- package/dist/skill-instructions.d.ts +4 -0
- package/dist/skill-instructions.js +97 -22
- package/dist/skill-metadata.d.ts +8 -0
- package/dist/skill-metadata.js +52 -0
- package/dist/skill-routing.d.ts +2 -0
- package/dist/skill-routing.js +36 -0
- package/dist/skills.js +131 -41
- package/dist/snippet.js +27 -7
- package/dist/snippets/read.js +8 -2
- package/dist/structure-snapshot.d.ts +12 -0
- package/dist/structure-snapshot.js +439 -85
- package/dist/token-edit.js +219 -64
- package/dist/token-projection.d.ts +56 -0
- package/dist/token-projection.js +286 -0
- package/dist/tokens-diff.js +125 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/skills/design-judgment/SKILL.md +1 -1
- package/skills/design-judgment/agents/openai.yaml +6 -0
- package/skills/penpair-design/SKILL.md +14 -1
- package/skills/penpair-design/agents/openai.yaml +6 -0
- package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
- package/skills/penpair-design/references/project-facts.md +2 -0
- package/skills/penpair-handoff/SKILL.md +14 -1
- package/skills/penpair-handoff/agents/openai.yaml +6 -0
- package/skills/penpair-handoff/references/comparison-companion.md +16 -0
- package/skills/penpair-handoff/references/handoff-flow.md +2 -0
- package/skills/penpair-recipes/SKILL.md +13 -1
- package/skills/penpair-recipes/agents/openai.yaml +6 -0
- package/skills/penpair-recipes/assets/visual-checkpoint.md +48 -0
- package/skills/penpair-recipes/references/visual-checkpoint.md +37 -0
- package/skills/penpair-setup/SKILL.md +9 -3
- package/skills/penpair-setup/agents/openai.yaml +6 -0
- package/skills/penpair-verify/SKILL.md +49 -0
- package/skills/penpair-verify/agents/openai.yaml +6 -0
- package/skills/penpair-verify/references/browser-evidence.md +33 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -3
- package/dist/docker/lifecycle.d.ts +0 -96
- package/dist/docker/lifecycle.js +0 -273
- package/dist/docker/secrets.d.ts +0 -11
- package/dist/docker/secrets.js +0 -38
- package/dist/docker/template.d.ts +0 -8
- package/dist/docker/template.js +0 -158
- package/dist/docker-profile.d.ts +0 -114
- package/dist/docker-profile.js +0 -18
- package/dist/instance/lifecycle.d.ts +0 -129
- package/dist/instance/lifecycle.js +0 -336
- package/dist/instance/secrets.d.ts +0 -11
- package/dist/instance/secrets.js +0 -38
- package/dist/instance/template.d.ts +0 -8
- package/dist/instance/template.js +0 -158
package/dist/inspection.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withPageSession } from "./page-session.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { runOnPage } from "./operation.js";
|
|
3
4
|
import { runSnippet, snippet, requirePage } from "./snippet.js";
|
|
@@ -5,10 +6,19 @@ import { PenpairError } from "./errors.js";
|
|
|
5
6
|
import { maxSnippetBytes } from "./client.js";
|
|
6
7
|
import { observationHash } from "./journal.js";
|
|
7
8
|
import { version } from "./version.js";
|
|
8
|
-
export const inspectionFields = [
|
|
9
|
+
export const inspectionFields = [
|
|
10
|
+
"geometry",
|
|
11
|
+
"text",
|
|
12
|
+
"layout",
|
|
13
|
+
"component",
|
|
14
|
+
"tokens",
|
|
15
|
+
"style",
|
|
16
|
+
"metadata",
|
|
17
|
+
];
|
|
9
18
|
// A deterministic change detector, not an authentication hash. Final reassembled evidence receives SHA-256 on the host.
|
|
10
19
|
function fingerprint(text) {
|
|
11
|
-
let a = 0xdeadbeef
|
|
20
|
+
let a = 0xdeadbeef;
|
|
21
|
+
let b = 0x41c6ce57;
|
|
12
22
|
for (let i = 0; i < text.length; i++) {
|
|
13
23
|
const code = text.charCodeAt(i);
|
|
14
24
|
a = Math.imul(a ^ code, 2654435761);
|
|
@@ -22,16 +32,23 @@ function utf8Bytes(text) {
|
|
|
22
32
|
let count = 0;
|
|
23
33
|
for (let i = 0; i < text.length; i++) {
|
|
24
34
|
const code = text.charCodeAt(i);
|
|
25
|
-
if (code < 128)
|
|
35
|
+
if (code < 128) {
|
|
26
36
|
count++;
|
|
27
|
-
|
|
37
|
+
}
|
|
38
|
+
else if (code < 2048) {
|
|
28
39
|
count += 2;
|
|
29
|
-
|
|
40
|
+
}
|
|
41
|
+
else if (code >= 0xd800 &&
|
|
42
|
+
code <= 0xdbff &&
|
|
43
|
+
i + 1 < text.length &&
|
|
44
|
+
text.charCodeAt(i + 1) >= 0xdc00 &&
|
|
45
|
+
text.charCodeAt(i + 1) <= 0xdfff) {
|
|
30
46
|
count += 4;
|
|
31
47
|
i++;
|
|
32
48
|
}
|
|
33
|
-
else
|
|
49
|
+
else {
|
|
34
50
|
count += 3;
|
|
51
|
+
}
|
|
35
52
|
}
|
|
36
53
|
return count;
|
|
37
54
|
}
|
|
@@ -173,28 +190,108 @@ best.observationMs=Date.now()-started;
|
|
|
173
190
|
return best;`;
|
|
174
191
|
export function validateInspectionQuery(query) {
|
|
175
192
|
const kind = query.kind ?? "shape";
|
|
176
|
-
if (![
|
|
193
|
+
if (![
|
|
194
|
+
"shape",
|
|
195
|
+
"tokens",
|
|
196
|
+
].includes(kind) ||
|
|
197
|
+
(kind === "shape" &&
|
|
198
|
+
(typeof query.shapeId !== "string" ||
|
|
199
|
+
!query.shapeId ||
|
|
200
|
+
query.shapeId.length > 200 ||
|
|
201
|
+
typeof query.page !== "string" ||
|
|
202
|
+
!query.page ||
|
|
203
|
+
query.page.length > 500))) {
|
|
177
204
|
throw new Error("Shape inspection requires an ID and explicit page");
|
|
178
|
-
|
|
205
|
+
}
|
|
206
|
+
if (kind === "tokens" &&
|
|
207
|
+
(query.shapeId !== undefined ||
|
|
208
|
+
query.page !== undefined ||
|
|
209
|
+
query.fields !== undefined ||
|
|
210
|
+
query.depth !== undefined)) {
|
|
179
211
|
throw new Error("Token inspection is file-scoped and does not accept shape field/depth selectors");
|
|
212
|
+
}
|
|
180
213
|
const requested = query.fields ?? inspectionFields;
|
|
181
|
-
if (!Array.isArray(requested) || requested.some(field => !inspectionFields.includes(field)))
|
|
214
|
+
if (!Array.isArray(requested) || requested.some((field) => !inspectionFields.includes(field))) {
|
|
182
215
|
throw new Error("Unknown inspection field group; supported: " + inspectionFields.join(", "));
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
216
|
+
}
|
|
217
|
+
const fields = [
|
|
218
|
+
...new Set(requested),
|
|
219
|
+
].sort();
|
|
220
|
+
const config = {
|
|
221
|
+
kind,
|
|
222
|
+
shapeId: query.shapeId,
|
|
223
|
+
page: query.page,
|
|
224
|
+
fields,
|
|
225
|
+
depth: query.depth ?? 64,
|
|
226
|
+
maxNodes: query.maxNodes ?? 20000,
|
|
227
|
+
responseBytes: query.responseBytes ?? 65536,
|
|
228
|
+
timeBudgetMs: query.timeBudgetMs ?? 5000,
|
|
229
|
+
documentBytes: query.documentBytes ?? 16 * 1024 * 1024,
|
|
230
|
+
};
|
|
231
|
+
for (const [key, min, max] of [
|
|
232
|
+
[
|
|
233
|
+
"depth",
|
|
234
|
+
0,
|
|
235
|
+
10000,
|
|
236
|
+
],
|
|
237
|
+
[
|
|
238
|
+
"maxNodes",
|
|
239
|
+
1,
|
|
240
|
+
50000,
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
"responseBytes",
|
|
244
|
+
4096,
|
|
245
|
+
262144,
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"timeBudgetMs",
|
|
249
|
+
1,
|
|
250
|
+
30000,
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
"documentBytes",
|
|
254
|
+
4096,
|
|
255
|
+
128 * 1024 * 1024,
|
|
256
|
+
],
|
|
257
|
+
]) {
|
|
258
|
+
if (!Number.isInteger(config[key]) || config[key] < min || config[key] > max) {
|
|
187
259
|
throw new Error(`${key} must be an integer from ${min} to ${max}`);
|
|
188
|
-
|
|
189
|
-
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const queryHash = observationHash({
|
|
263
|
+
kind,
|
|
264
|
+
shapeId: config.shapeId,
|
|
265
|
+
page: config.page,
|
|
266
|
+
fields,
|
|
267
|
+
depth: config.depth,
|
|
268
|
+
maxNodes: config.maxNodes,
|
|
269
|
+
documentBytes: config.documentBytes,
|
|
270
|
+
timeBudgetMs: config.timeBudgetMs,
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
...config,
|
|
274
|
+
queryHash,
|
|
275
|
+
producerVersion: version,
|
|
276
|
+
};
|
|
190
277
|
}
|
|
191
278
|
function decodeCursor(cursor) {
|
|
192
279
|
try {
|
|
193
|
-
if (cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(cursor))
|
|
280
|
+
if (cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(cursor)) {
|
|
194
281
|
throw new Error();
|
|
282
|
+
}
|
|
195
283
|
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
196
|
-
if (value.schemaVersion !== 1 ||
|
|
284
|
+
if (value.schemaVersion !== 1 ||
|
|
285
|
+
typeof value.fileId !== "string" ||
|
|
286
|
+
!value.fileId ||
|
|
287
|
+
(value.pageId !== null && typeof value.pageId !== "string") ||
|
|
288
|
+
!Number.isInteger(value.revision) ||
|
|
289
|
+
!/^[a-f0-9]{64}$/.test(value.queryHash) ||
|
|
290
|
+
!/^[a-f0-9]+$/.test(value.digest) ||
|
|
291
|
+
!Number.isInteger(value.offset) ||
|
|
292
|
+
value.offset < 0) {
|
|
197
293
|
throw new Error();
|
|
294
|
+
}
|
|
198
295
|
return value;
|
|
199
296
|
}
|
|
200
297
|
catch {
|
|
@@ -202,142 +299,358 @@ function decodeCursor(cursor) {
|
|
|
202
299
|
}
|
|
203
300
|
}
|
|
204
301
|
function cursorAt(chunk, offset) {
|
|
205
|
-
return Buffer.from(JSON.stringify({
|
|
302
|
+
return Buffer.from(JSON.stringify({
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
fileId: chunk.fileId,
|
|
305
|
+
pageId: chunk.pageId,
|
|
306
|
+
revision: chunk.revision,
|
|
307
|
+
queryHash: chunk.queryHash,
|
|
308
|
+
digest: chunk.digest,
|
|
309
|
+
offset,
|
|
310
|
+
})).toString("base64url");
|
|
206
311
|
}
|
|
207
312
|
export async function inspectChunk(client, query, requestTimeoutMs) {
|
|
208
|
-
const config = validateInspectionQuery(query)
|
|
209
|
-
|
|
313
|
+
const config = validateInspectionQuery(query);
|
|
314
|
+
const expected = query.cursor ? decodeCursor(query.cursor) : undefined;
|
|
315
|
+
if (expected && expected.queryHash !== config.queryHash) {
|
|
210
316
|
throw new Error("Inspection query changed; discard the cursor and start fresh");
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
317
|
+
}
|
|
318
|
+
const source = snippet("inspect-chunk", body, {
|
|
319
|
+
...config,
|
|
320
|
+
expected,
|
|
321
|
+
}, "read");
|
|
322
|
+
const options = {
|
|
323
|
+
effect: "read",
|
|
324
|
+
retries: 0,
|
|
325
|
+
timeoutMs: Math.max(1, Math.min(requestTimeoutMs ?? 120000, config.timeBudgetMs + 2000)),
|
|
326
|
+
};
|
|
327
|
+
const record = config.kind === "shape"
|
|
328
|
+
? await runOnPage(client, source, config.page, options)
|
|
329
|
+
: await runSnippet(client, source, options);
|
|
330
|
+
if (record.stale === true) {
|
|
215
331
|
throw new PenpairError("stale-inspection", "Inspection cursor is stale; start a fresh capture");
|
|
332
|
+
}
|
|
216
333
|
const value = record;
|
|
217
|
-
if (record.ok !== true ||
|
|
334
|
+
if (record.ok !== true ||
|
|
335
|
+
value.schemaVersion !== 1 ||
|
|
336
|
+
value.kind !== "inspection-chunk" ||
|
|
337
|
+
typeof value.fileId !== "string" ||
|
|
338
|
+
(value.pageId !== null && typeof value.pageId !== "string") ||
|
|
339
|
+
!Number.isInteger(value.revision) ||
|
|
340
|
+
value.queryHash !== config.queryHash ||
|
|
341
|
+
typeof value.digest !== "string" ||
|
|
342
|
+
!/^[a-f0-9]{1,16}$/.test(value.digest) ||
|
|
343
|
+
typeof value.sourceComplete !== "boolean" ||
|
|
344
|
+
typeof value.complete !== "boolean" ||
|
|
345
|
+
!Number.isInteger(value.offset) ||
|
|
346
|
+
!Number.isInteger(value.totalCodeUnits) ||
|
|
347
|
+
value.totalCodeUnits < 1 ||
|
|
348
|
+
value.digestAlgorithm !== "cyrb53-change-detector" ||
|
|
349
|
+
!value.counts ||
|
|
350
|
+
typeof value.counts !== "object" ||
|
|
351
|
+
Object.values(value.counts).some((count) => count !== null && (!Number.isInteger(count) || count < 0)) ||
|
|
352
|
+
!Number.isFinite(value.observationMs) ||
|
|
353
|
+
value.observationMs < 0 ||
|
|
354
|
+
typeof value.chunk !== "string" ||
|
|
355
|
+
!value.chunk.length ||
|
|
356
|
+
value.offset !== (expected?.offset ?? 0) ||
|
|
357
|
+
value.offset + value.chunk.length > value.totalCodeUnits ||
|
|
358
|
+
value.nextOffset !==
|
|
359
|
+
(value.offset + value.chunk.length < value.totalCodeUnits
|
|
360
|
+
? value.offset + value.chunk.length
|
|
361
|
+
: null)) {
|
|
218
362
|
throw new Error("Malformed inspection chunk evidence");
|
|
219
|
-
|
|
363
|
+
}
|
|
364
|
+
if (value.complete !== (value.sourceComplete && value.offset === 0 && value.nextOffset === null) ||
|
|
365
|
+
(value.offset > 0 && /^[\uDC00-\uDFFF]/.test(value.chunk)) ||
|
|
366
|
+
(value.nextOffset !== null && /[\uD800-\uDBFF]$/.test(value.chunk))) {
|
|
220
367
|
throw new Error("Malformed inspection completeness or Unicode boundary");
|
|
221
|
-
|
|
222
|
-
|
|
368
|
+
}
|
|
369
|
+
const metrics = record.execution?.metrics ?? {
|
|
370
|
+
requestSnippetBytes: Buffer.byteLength(source.code),
|
|
371
|
+
responsePayloadBytes: Buffer.byteLength(JSON.stringify(record)),
|
|
372
|
+
};
|
|
373
|
+
if (!Number.isInteger(metrics.requestSnippetBytes) ||
|
|
374
|
+
metrics.requestSnippetBytes <= 0 ||
|
|
375
|
+
metrics.requestSnippetBytes > maxSnippetBytes ||
|
|
376
|
+
!Number.isInteger(metrics.responsePayloadBytes) ||
|
|
377
|
+
metrics.responsePayloadBytes <= 0) {
|
|
223
378
|
throw new Error("Inspection metrics are missing or invalid");
|
|
224
|
-
|
|
379
|
+
}
|
|
380
|
+
if (metrics.responsePayloadBytes > config.responseBytes) {
|
|
225
381
|
throw new PenpairError("incomplete-output", "Inspection response exceeded the configured payload budget");
|
|
382
|
+
}
|
|
226
383
|
const { execution: _execution, ok: _ok, ...data } = record;
|
|
227
|
-
const result = {
|
|
384
|
+
const result = {
|
|
385
|
+
...data,
|
|
386
|
+
metrics,
|
|
387
|
+
nextCursor: null,
|
|
388
|
+
};
|
|
228
389
|
result.nextCursor = result.nextOffset === null ? null : cursorAt(result, result.nextOffset);
|
|
229
|
-
if (result.offset === 0 && result.nextOffset === null)
|
|
230
|
-
reassembleInspection([
|
|
390
|
+
if (result.offset === 0 && result.nextOffset === null) {
|
|
391
|
+
reassembleInspection([
|
|
392
|
+
result,
|
|
393
|
+
]);
|
|
394
|
+
}
|
|
231
395
|
return result;
|
|
232
396
|
}
|
|
233
397
|
/** Validate structural evidence independently of the transport envelope. Unknown API fields remain explicit nulls. */
|
|
234
398
|
export function validateInspectionDocument(input) {
|
|
235
|
-
if (!input || typeof input !== "object")
|
|
399
|
+
if (!input || typeof input !== "object") {
|
|
236
400
|
throw new Error("Malformed inspection document");
|
|
401
|
+
}
|
|
237
402
|
const value = input;
|
|
238
403
|
const counts = value.counts;
|
|
239
|
-
if (value.schemaVersion !== 1 ||
|
|
404
|
+
if (value.schemaVersion !== 1 ||
|
|
405
|
+
![
|
|
406
|
+
"shape-inspection",
|
|
407
|
+
"token-inspection",
|
|
408
|
+
].includes(value.kind) ||
|
|
409
|
+
typeof value.fileId !== "string" ||
|
|
410
|
+
!value.fileId ||
|
|
411
|
+
!Number.isInteger(value.revision) ||
|
|
412
|
+
value.revision < 0 ||
|
|
413
|
+
typeof value.complete !== "boolean" ||
|
|
414
|
+
!Array.isArray(value.reasons) ||
|
|
415
|
+
value.reasons.some((reason) => typeof reason !== "string") ||
|
|
416
|
+
(value.complete && value.reasons.length > 0) ||
|
|
417
|
+
(!value.complete && !value.reasons.length) ||
|
|
418
|
+
!counts ||
|
|
419
|
+
typeof counts !== "object" ||
|
|
420
|
+
Object.values(counts).some((count) => count !== null && (!Number.isInteger(count) || count < 0)) ||
|
|
421
|
+
!Number.isInteger(counts.visited) ||
|
|
422
|
+
!Number.isInteger(counts.returned) ||
|
|
423
|
+
value.producer?.name !== "penpair" ||
|
|
424
|
+
typeof value.producer?.version !== "string") {
|
|
240
425
|
throw new Error("Malformed inspection provenance/counts");
|
|
426
|
+
}
|
|
241
427
|
if (value.kind === "shape-inspection") {
|
|
242
428
|
const scope = value.scope;
|
|
243
|
-
if (typeof value.pageId !== "string" ||
|
|
429
|
+
if (typeof value.pageId !== "string" ||
|
|
430
|
+
!value.pageId ||
|
|
431
|
+
!scope ||
|
|
432
|
+
typeof scope.shapeId !== "string" ||
|
|
433
|
+
!Array.isArray(scope.fields) ||
|
|
434
|
+
scope.fields.some((field) => !inspectionFields.includes(field)) ||
|
|
435
|
+
!Number.isInteger(scope.depth) ||
|
|
436
|
+
!Array.isArray(value.nodes) ||
|
|
437
|
+
counts.returned !== value.nodes.length) {
|
|
244
438
|
throw new Error("Malformed shape inspection scope");
|
|
439
|
+
}
|
|
245
440
|
const nodes = new Map();
|
|
246
441
|
for (const row of value.nodes) {
|
|
247
|
-
if (!row ||
|
|
442
|
+
if (!row ||
|
|
443
|
+
typeof row.id !== "string" ||
|
|
444
|
+
!row.id ||
|
|
445
|
+
nodes.has(row.id) ||
|
|
446
|
+
(row.parentId !== null && typeof row.parentId !== "string") ||
|
|
447
|
+
!Number.isInteger(row.index) ||
|
|
448
|
+
Number(row.index) < 0 ||
|
|
449
|
+
!Number.isInteger(row.depth) ||
|
|
450
|
+
Number(row.depth) < 0 ||
|
|
451
|
+
(row.type !== null && typeof row.type !== "string") ||
|
|
452
|
+
(row.name !== null && typeof row.name !== "string") ||
|
|
453
|
+
!Array.isArray(row.childIds) ||
|
|
454
|
+
row.childIds.some((id) => typeof id !== "string") ||
|
|
455
|
+
!Number.isInteger(row.childrenTotal) ||
|
|
456
|
+
Number(row.childrenTotal) < row.childIds.length ||
|
|
457
|
+
!Array.isArray(row.unknowns) ||
|
|
458
|
+
row.unknowns.some((key) => typeof key !== "string")) {
|
|
248
459
|
throw new Error("Malformed shape node evidence");
|
|
249
|
-
|
|
250
|
-
|
|
460
|
+
}
|
|
461
|
+
for (const field of scope.fields) {
|
|
462
|
+
if (!(field in row)) {
|
|
251
463
|
throw new Error("Inspection omitted a requested field group");
|
|
252
|
-
|
|
253
|
-
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (row.geometry && typeof row.geometry === "object") {
|
|
467
|
+
for (const key of [
|
|
468
|
+
"x",
|
|
469
|
+
"y",
|
|
470
|
+
"width",
|
|
471
|
+
"height",
|
|
472
|
+
"rotation",
|
|
473
|
+
]) {
|
|
254
474
|
const entry = row.geometry[key];
|
|
255
|
-
if (entry !== null && (typeof entry !== "number" || !Number.isFinite(entry)))
|
|
475
|
+
if (entry !== null && (typeof entry !== "number" || !Number.isFinite(entry))) {
|
|
256
476
|
throw new Error("Malformed shape geometry");
|
|
477
|
+
}
|
|
257
478
|
}
|
|
479
|
+
}
|
|
258
480
|
nodes.set(row.id, row);
|
|
259
481
|
}
|
|
260
|
-
if (value.nodes.length && value.nodes[0].id !== scope.shapeId)
|
|
482
|
+
if (value.nodes.length && value.nodes[0].id !== scope.shapeId) {
|
|
261
483
|
throw new Error("Inspection root identity does not match its scope");
|
|
484
|
+
}
|
|
262
485
|
for (const row of value.nodes) {
|
|
263
|
-
if (row === value.nodes[0] && (row.parentId !== null || row.depth !== 0 || row.index !== 0) ||
|
|
486
|
+
if ((row === value.nodes[0] && (row.parentId !== null || row.depth !== 0 || row.index !== 0)) ||
|
|
487
|
+
(row !== value.nodes[0] && row.parentId === null)) {
|
|
264
488
|
throw new Error("Inspection has an invalid subtree root");
|
|
489
|
+
}
|
|
265
490
|
if (row.parentId !== null) {
|
|
266
491
|
const parent = nodes.get(String(row.parentId));
|
|
267
|
-
if (!parent ||
|
|
492
|
+
if (!parent ||
|
|
493
|
+
parent.childIds[Number(row.index)] !== row.id ||
|
|
494
|
+
Number(row.depth) !== Number(parent.depth) + 1) {
|
|
268
495
|
throw new Error("Inspection parent/child evidence is inconsistent");
|
|
496
|
+
}
|
|
269
497
|
}
|
|
270
|
-
if (value.complete &&
|
|
498
|
+
if (value.complete &&
|
|
499
|
+
(row.childIds.length !== row.childrenTotal ||
|
|
500
|
+
row.childIds.some((id, index) => !nodes.has(id) ||
|
|
501
|
+
nodes.get(id).parentId !== row.id ||
|
|
502
|
+
nodes.get(id).index !== index))) {
|
|
271
503
|
throw new Error("Complete inspection is missing a child");
|
|
504
|
+
}
|
|
272
505
|
}
|
|
273
|
-
if (value.complete &&
|
|
506
|
+
if (value.complete &&
|
|
507
|
+
(!value.nodes.length ||
|
|
508
|
+
counts.total !== value.nodes.length ||
|
|
509
|
+
counts.visited !== value.nodes.length)) {
|
|
274
510
|
throw new Error("Complete inspection node counts disagree");
|
|
511
|
+
}
|
|
275
512
|
}
|
|
276
513
|
else {
|
|
277
|
-
if (value.pageId !== null || !Array.isArray(value.sets) || !Array.isArray(value.themes))
|
|
514
|
+
if (value.pageId !== null || !Array.isArray(value.sets) || !Array.isArray(value.themes)) {
|
|
278
515
|
throw new Error("Malformed token inspection scope");
|
|
516
|
+
}
|
|
279
517
|
let tokens = 0;
|
|
280
518
|
for (const set of value.sets) {
|
|
281
|
-
if (!set || !Number.isInteger(set.order) || !Array.isArray(set.tokens))
|
|
519
|
+
if (!set || !Number.isInteger(set.order) || !Array.isArray(set.tokens)) {
|
|
282
520
|
throw new Error("Malformed token set evidence");
|
|
521
|
+
}
|
|
283
522
|
tokens += set.tokens.length;
|
|
284
523
|
}
|
|
285
|
-
if (counts.returned !== tokens ||
|
|
524
|
+
if (counts.returned !== tokens ||
|
|
525
|
+
(value.complete && counts.visited !== tokens + value.sets.length + value.themes.length)) {
|
|
286
526
|
throw new Error("Token inspection counts disagree");
|
|
527
|
+
}
|
|
287
528
|
}
|
|
288
529
|
return value;
|
|
289
530
|
}
|
|
290
531
|
export function reassembleInspection(chunks) {
|
|
291
|
-
if (!chunks.length || chunks[0].offset !== 0 || chunks.at(-1).nextOffset !== null)
|
|
532
|
+
if (!chunks.length || chunks[0].offset !== 0 || chunks.at(-1).nextOffset !== null) {
|
|
292
533
|
throw new Error("Inspection is missing its beginning or final chunk");
|
|
534
|
+
}
|
|
293
535
|
const first = chunks[0];
|
|
294
536
|
let offset = 0;
|
|
295
537
|
for (const chunk of chunks) {
|
|
296
|
-
if (chunk.offset !== offset ||
|
|
538
|
+
if (chunk.offset !== offset ||
|
|
539
|
+
chunk.fileId !== first.fileId ||
|
|
540
|
+
chunk.pageId !== first.pageId ||
|
|
541
|
+
chunk.revision !== first.revision ||
|
|
542
|
+
chunk.queryHash !== first.queryHash ||
|
|
543
|
+
chunk.digest !== first.digest ||
|
|
544
|
+
chunk.totalCodeUnits !== first.totalCodeUnits ||
|
|
545
|
+
chunk.sourceComplete !== first.sourceComplete) {
|
|
297
546
|
throw new Error("Inspection chunks are mixed, missing or stale");
|
|
547
|
+
}
|
|
298
548
|
offset += chunk.chunk.length;
|
|
299
549
|
}
|
|
300
|
-
const serialized = chunks.map(chunk => chunk.chunk).join("");
|
|
301
|
-
if (serialized.length !== first.totalCodeUnits || fingerprint(serialized) !== first.digest)
|
|
550
|
+
const serialized = chunks.map((chunk) => chunk.chunk).join("");
|
|
551
|
+
if (serialized.length !== first.totalCodeUnits || fingerprint(serialized) !== first.digest) {
|
|
302
552
|
throw new Error("Inspection chunk content does not match its fingerprint");
|
|
553
|
+
}
|
|
303
554
|
const value = validateInspectionDocument(JSON.parse(serialized));
|
|
304
|
-
if (value.schemaVersion !== 1 ||
|
|
555
|
+
if (value.schemaVersion !== 1 ||
|
|
556
|
+
![
|
|
557
|
+
"shape-inspection",
|
|
558
|
+
"token-inspection",
|
|
559
|
+
].includes(value.kind) ||
|
|
560
|
+
value.fileId !== first.fileId ||
|
|
561
|
+
value.pageId !== first.pageId ||
|
|
562
|
+
value.revision !== first.revision ||
|
|
563
|
+
value.complete !== first.sourceComplete) {
|
|
305
564
|
throw new Error("Inspection document lacks matching provenance");
|
|
565
|
+
}
|
|
306
566
|
return value;
|
|
307
567
|
}
|
|
308
568
|
export function validateCaptureBudget(budget = {}) {
|
|
309
|
-
const maxCalls = budget.maxCalls ?? 256
|
|
310
|
-
|
|
569
|
+
const maxCalls = budget.maxCalls ?? 256;
|
|
570
|
+
const maxTotalBytes = budget.maxTotalBytes ?? 16 * 1024 * 1024;
|
|
571
|
+
const timeoutMs = budget.timeoutMs ?? 60000;
|
|
572
|
+
if (!Number.isInteger(maxCalls) ||
|
|
573
|
+
maxCalls < 1 ||
|
|
574
|
+
maxCalls > 2048 ||
|
|
575
|
+
!Number.isInteger(maxTotalBytes) ||
|
|
576
|
+
maxTotalBytes < 4096 ||
|
|
577
|
+
maxTotalBytes > 128 * 1024 * 1024 ||
|
|
578
|
+
!Number.isInteger(timeoutMs) ||
|
|
579
|
+
timeoutMs < 1 ||
|
|
580
|
+
timeoutMs > 300000) {
|
|
311
581
|
throw new Error("Invalid inspection capture budgets");
|
|
312
|
-
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
maxCalls,
|
|
585
|
+
maxTotalBytes,
|
|
586
|
+
timeoutMs,
|
|
587
|
+
};
|
|
313
588
|
}
|
|
314
589
|
export async function captureInspection(client, query, budget = {}) {
|
|
315
|
-
|
|
590
|
+
return withPageSession(client, () => captureInspectionInSession(client, query, budget));
|
|
591
|
+
}
|
|
592
|
+
async function captureInspectionInSession(client, query, budget) {
|
|
593
|
+
if (query.cursor) {
|
|
316
594
|
throw new Error("Full capture starts at offset zero; use inspectChunk for manual cursor continuation");
|
|
595
|
+
}
|
|
317
596
|
const { maxCalls, maxTotalBytes, timeoutMs } = validateCaptureBudget(budget);
|
|
318
597
|
validateInspectionQuery(query);
|
|
319
|
-
const started = Date.now()
|
|
320
|
-
const
|
|
598
|
+
const started = Date.now();
|
|
599
|
+
const chunks = [];
|
|
600
|
+
const metrics = {
|
|
601
|
+
calls: 0,
|
|
602
|
+
requestSnippetBytes: 0,
|
|
603
|
+
responsePayloadBytes: 0,
|
|
604
|
+
maxRequestSnippetBytes: 0,
|
|
605
|
+
maxResponsePayloadBytes: 0,
|
|
606
|
+
assembledBytes: 0,
|
|
607
|
+
durationMs: 0,
|
|
608
|
+
};
|
|
321
609
|
let cursor;
|
|
322
|
-
const partial = (reason, nextCursor = cursor ?? null) => ({
|
|
610
|
+
const partial = (reason, nextCursor = cursor ?? null) => ({
|
|
611
|
+
schemaVersion: 1,
|
|
612
|
+
kind: "inspection-partial",
|
|
613
|
+
fileId: chunks[0]?.fileId ?? null,
|
|
614
|
+
pageId: chunks[0]?.pageId ?? null,
|
|
615
|
+
complete: false,
|
|
616
|
+
reason,
|
|
617
|
+
chunks,
|
|
618
|
+
nextCursor,
|
|
619
|
+
metrics: {
|
|
620
|
+
...metrics,
|
|
621
|
+
durationMs: Date.now() - started,
|
|
622
|
+
},
|
|
623
|
+
});
|
|
323
624
|
while (true) {
|
|
324
|
-
if (metrics.calls >= maxCalls || Date.now() - started >= timeoutMs)
|
|
625
|
+
if (metrics.calls >= maxCalls || Date.now() - started >= timeoutMs) {
|
|
325
626
|
return partial("Capture call/time budget exhausted");
|
|
326
|
-
|
|
627
|
+
}
|
|
628
|
+
const chunk = await inspectChunk(client, {
|
|
629
|
+
...query,
|
|
630
|
+
cursor,
|
|
631
|
+
}, Math.max(1, timeoutMs - (Date.now() - started)));
|
|
327
632
|
metrics.calls++;
|
|
328
633
|
metrics.requestSnippetBytes += chunk.metrics.requestSnippetBytes;
|
|
329
634
|
metrics.responsePayloadBytes += chunk.metrics.responsePayloadBytes;
|
|
330
635
|
metrics.maxRequestSnippetBytes = Math.max(metrics.maxRequestSnippetBytes, chunk.metrics.requestSnippetBytes);
|
|
331
636
|
metrics.maxResponsePayloadBytes = Math.max(metrics.maxResponsePayloadBytes, chunk.metrics.responsePayloadBytes);
|
|
332
637
|
const size = Buffer.byteLength(chunk.chunk, "utf8");
|
|
333
|
-
if (metrics.assembledBytes + size > maxTotalBytes)
|
|
638
|
+
if (metrics.assembledBytes + size > maxTotalBytes) {
|
|
334
639
|
return partial("Capture total-byte budget exhausted", cursorAt(chunk, chunk.offset));
|
|
640
|
+
}
|
|
335
641
|
metrics.assembledBytes += size;
|
|
336
642
|
chunks.push(chunk);
|
|
337
643
|
if (!chunk.nextCursor) {
|
|
338
644
|
const document = reassembleInspection(chunks);
|
|
339
645
|
metrics.durationMs = Date.now() - started;
|
|
340
|
-
return {
|
|
646
|
+
return {
|
|
647
|
+
...document,
|
|
648
|
+
capturedAt: new Date().toISOString(),
|
|
649
|
+
sha256: createHash("sha256")
|
|
650
|
+
.update(chunks.map((entry) => entry.chunk).join(""), "utf8")
|
|
651
|
+
.digest("hex"),
|
|
652
|
+
metrics,
|
|
653
|
+
};
|
|
341
654
|
}
|
|
342
655
|
cursor = chunk.nextCursor;
|
|
343
656
|
}
|