@penpair/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/adapters.js +58 -11
  2. package/dist/approvals.js +143 -29
  3. package/dist/atomic-output.js +9 -4
  4. package/dist/audit-rules.js +345 -79
  5. package/dist/backup-note.js +29 -8
  6. package/dist/baseline-coverage.js +169 -35
  7. package/dist/baselines-config.js +119 -29
  8. package/dist/baselines.js +280 -57
  9. package/dist/cli-arguments.js +18 -6
  10. package/dist/cli-main.js +1621 -392
  11. package/dist/client.d.ts +5 -2
  12. package/dist/client.js +127 -28
  13. package/dist/command-result.js +341 -52
  14. package/dist/coordination.js +91 -29
  15. package/dist/credentials.js +57 -13
  16. package/dist/deep-check.js +56 -12
  17. package/dist/design-spec.js +189 -49
  18. package/dist/endpoint.js +102 -34
  19. package/dist/export.js +86 -18
  20. package/dist/file-archive.js +33 -7
  21. package/dist/file-guard.js +29 -11
  22. package/dist/formats.js +215 -14
  23. package/dist/index.d.ts +8 -7
  24. package/dist/index.js +8 -7
  25. package/dist/inspection.d.ts +1 -1
  26. package/dist/inspection.js +378 -65
  27. package/dist/instance/config-schema.js +400 -41
  28. package/dist/instance/config.js +101 -30
  29. package/dist/instance/connect.js +151 -44
  30. package/dist/instance/connection.d.ts +26 -5
  31. package/dist/instance/connection.js +194 -21
  32. package/dist/instance/gitignore.js +5 -2
  33. package/dist/instance/init.js +32 -7
  34. package/dist/instance/runner.js +32 -10
  35. package/dist/instance/setup.js +46 -13
  36. package/dist/instance/tty.js +26 -8
  37. package/dist/journal.d.ts +10 -0
  38. package/dist/journal.js +212 -49
  39. package/dist/kit-smoke.js +82 -20
  40. package/dist/kit-source.js +10 -40
  41. package/dist/kit.js +102 -34
  42. package/dist/layout-policy.d.ts +2 -0
  43. package/dist/layout-policy.js +86 -0
  44. package/dist/layout.d.ts +7 -1
  45. package/dist/layout.js +27 -70
  46. package/dist/live-smoke.js +13 -4
  47. package/dist/local-profile.js +2 -1
  48. package/dist/mcp-server.js +360 -47
  49. package/dist/native-api.js +47 -11
  50. package/dist/onboard.js +33 -7
  51. package/dist/operation.d.ts +2 -2
  52. package/dist/operation.js +279 -69
  53. package/dist/page-session.d.ts +23 -0
  54. package/dist/page-session.js +142 -0
  55. package/dist/png.js +18 -3
  56. package/dist/portable-tokens.js +254 -57
  57. package/dist/probe.d.ts +1 -1
  58. package/dist/probe.js +48 -11
  59. package/dist/project-input.js +24 -10
  60. package/dist/project-snippets.js +18 -6
  61. package/dist/project-status.js +133 -24
  62. package/dist/registry.js +217 -60
  63. package/dist/run-file.js +129 -30
  64. package/dist/shots.js +108 -26
  65. package/dist/skill-instructions.js +91 -22
  66. package/dist/skills.js +129 -39
  67. package/dist/snippet.js +27 -7
  68. package/dist/snippets/read.js +8 -2
  69. package/dist/structure-snapshot.d.ts +12 -0
  70. package/dist/structure-snapshot.js +439 -85
  71. package/dist/token-edit.js +219 -64
  72. package/dist/token-projection.d.ts +56 -0
  73. package/dist/token-projection.js +286 -0
  74. package/dist/tokens-diff.js +125 -32
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +1 -1
  78. package/skills/penpair-design/SKILL.md +3 -0
  79. package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
  80. package/skills/penpair-design/references/project-facts.md +2 -0
  81. package/skills/penpair-handoff/SKILL.md +2 -0
  82. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  83. package/skills/penpair-handoff/references/handoff-flow.md +2 -0
  84. package/skills/penpair-recipes/SKILL.md +5 -0
  85. package/skills/penpair-setup/SKILL.md +3 -1
  86. package/dist/cli.d.ts +0 -2
  87. package/dist/cli.js +0 -3
  88. package/dist/docker/lifecycle.d.ts +0 -96
  89. package/dist/docker/lifecycle.js +0 -273
  90. package/dist/docker/secrets.d.ts +0 -11
  91. package/dist/docker/secrets.js +0 -38
  92. package/dist/docker/template.d.ts +0 -8
  93. package/dist/docker/template.js +0 -158
  94. package/dist/docker-profile.d.ts +0 -114
  95. package/dist/docker-profile.js +0 -18
  96. package/dist/instance/lifecycle.d.ts +0 -129
  97. package/dist/instance/lifecycle.js +0 -336
  98. package/dist/instance/secrets.d.ts +0 -11
  99. package/dist/instance/secrets.js +0 -38
  100. package/dist/instance/template.d.ts +0 -8
  101. package/dist/instance/template.js +0 -158
@@ -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 = ["geometry", "text", "layout", "component", "tokens", "style", "metadata"];
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, b = 0x41c6ce57;
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
- else if (code < 2048)
37
+ }
38
+ else if (code < 2048) {
28
39
  count += 2;
29
- else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length && text.charCodeAt(i + 1) >= 0xdc00 && text.charCodeAt(i + 1) <= 0xdfff) {
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 (!["shape", "tokens"].includes(kind) || kind === "shape" && (typeof query.shapeId !== "string" || !query.shapeId || query.shapeId.length > 200 || typeof query.page !== "string" || !query.page || query.page.length > 500))
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
- if (kind === "tokens" && (query.shapeId !== undefined || query.page !== undefined || query.fields !== undefined || query.depth !== undefined))
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
- const fields = [...new Set(requested)].sort();
184
- const config = { kind, shapeId: query.shapeId, page: query.page, fields, depth: query.depth ?? 64, maxNodes: query.maxNodes ?? 20000, responseBytes: query.responseBytes ?? 65536, timeBudgetMs: query.timeBudgetMs ?? 5000, documentBytes: query.documentBytes ?? 16 * 1024 * 1024 };
185
- for (const [key, min, max] of [["depth", 0, 10000], ["maxNodes", 1, 50000], ["responseBytes", 4096, 262144], ["timeBudgetMs", 1, 30000], ["documentBytes", 4096, 128 * 1024 * 1024]])
186
- if (!Number.isInteger(config[key]) || config[key] < min || config[key] > max)
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
- const queryHash = observationHash({ kind, shapeId: config.shapeId, page: config.page, fields, depth: config.depth, maxNodes: config.maxNodes, documentBytes: config.documentBytes, timeBudgetMs: config.timeBudgetMs });
189
- return { ...config, queryHash, producerVersion: version };
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 || typeof value.fileId !== "string" || !value.fileId || value.pageId !== null && typeof value.pageId !== "string" || !Number.isInteger(value.revision) || !/^[a-f0-9]{64}$/.test(value.queryHash) || !/^[a-f0-9]+$/.test(value.digest) || !Number.isInteger(value.offset) || value.offset < 0)
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({ schemaVersion: 1, fileId: chunk.fileId, pageId: chunk.pageId, revision: chunk.revision, queryHash: chunk.queryHash, digest: chunk.digest, offset })).toString("base64url");
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), expected = query.cursor ? decodeCursor(query.cursor) : undefined;
209
- if (expected && expected.queryHash !== config.queryHash)
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
- const source = snippet("inspect-chunk", body, { ...config, expected }, "read");
212
- const options = { effect: "read", retries: 0, timeoutMs: Math.max(1, Math.min(requestTimeoutMs ?? 120000, config.timeBudgetMs + 2000)) };
213
- const record = config.kind === "shape" ? await runOnPage(client, source, config.page, options) : await runSnippet(client, source, options);
214
- if (record.stale === true)
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 || value.schemaVersion !== 1 || value.kind !== "inspection-chunk" || typeof value.fileId !== "string" || value.pageId !== null && typeof value.pageId !== "string" || !Number.isInteger(value.revision) || value.queryHash !== config.queryHash || typeof value.digest !== "string" || !/^[a-f0-9]{1,16}$/.test(value.digest) || typeof value.sourceComplete !== "boolean" || typeof value.complete !== "boolean" || !Number.isInteger(value.offset) || !Number.isInteger(value.totalCodeUnits) || value.totalCodeUnits < 1 || value.digestAlgorithm !== "cyrb53-change-detector" || !value.counts || typeof value.counts !== "object" || Object.values(value.counts).some(count => count !== null && (!Number.isInteger(count) || count < 0)) || !Number.isFinite(value.observationMs) || value.observationMs < 0 || typeof value.chunk !== "string" || !value.chunk.length || value.offset !== (expected?.offset ?? 0) || value.offset + value.chunk.length > value.totalCodeUnits || value.nextOffset !== (value.offset + value.chunk.length < value.totalCodeUnits ? value.offset + value.chunk.length : null))
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
- if (value.complete !== (value.sourceComplete && value.offset === 0 && value.nextOffset === null) || value.offset > 0 && /^[\uDC00-\uDFFF]/.test(value.chunk) || value.nextOffset !== null && /[\uD800-\uDBFF]$/.test(value.chunk))
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
- const metrics = record.execution?.metrics ?? { requestSnippetBytes: Buffer.byteLength(source.code), responsePayloadBytes: Buffer.byteLength(JSON.stringify(record)) };
222
- if (!Number.isInteger(metrics.requestSnippetBytes) || metrics.requestSnippetBytes <= 0 || metrics.requestSnippetBytes > maxSnippetBytes || !Number.isInteger(metrics.responsePayloadBytes) || metrics.responsePayloadBytes <= 0)
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
- if (metrics.responsePayloadBytes > config.responseBytes)
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 = { ...data, metrics, nextCursor: null };
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([result]);
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 || !["shape-inspection", "token-inspection"].includes(value.kind) || typeof value.fileId !== "string" || !value.fileId || !Number.isInteger(value.revision) || value.revision < 0 || typeof value.complete !== "boolean" || !Array.isArray(value.reasons) || value.reasons.some(reason => typeof reason !== "string") || value.complete && value.reasons.length > 0 || !value.complete && !value.reasons.length || !counts || typeof counts !== "object" || Object.values(counts).some(count => count !== null && (!Number.isInteger(count) || count < 0)) || !Number.isInteger(counts.visited) || !Number.isInteger(counts.returned) || value.producer?.name !== "penpair" || typeof value.producer?.version !== "string")
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" || !value.pageId || !scope || typeof scope.shapeId !== "string" || !Array.isArray(scope.fields) || scope.fields.some(field => !inspectionFields.includes(field)) || !Number.isInteger(scope.depth) || !Array.isArray(value.nodes) || counts.returned !== value.nodes.length)
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 || typeof row.id !== "string" || !row.id || nodes.has(row.id) || row.parentId !== null && typeof row.parentId !== "string" || !Number.isInteger(row.index) || Number(row.index) < 0 || !Number.isInteger(row.depth) || Number(row.depth) < 0 || row.type !== null && typeof row.type !== "string" || row.name !== null && typeof row.name !== "string" || !Array.isArray(row.childIds) || row.childIds.some(id => typeof id !== "string") || !Number.isInteger(row.childrenTotal) || Number(row.childrenTotal) < row.childIds.length || !Array.isArray(row.unknowns) || row.unknowns.some(key => typeof key !== "string"))
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
- for (const field of scope.fields)
250
- if (!(field in row))
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
- if (row.geometry && typeof row.geometry === "object")
253
- for (const key of ["x", "y", "width", "height", "rotation"]) {
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) || row !== value.nodes[0] && row.parentId === null)
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 || parent.childIds[Number(row.index)] !== row.id || Number(row.depth) !== Number(parent.depth) + 1)
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 && (row.childIds.length !== row.childrenTotal || row.childIds.some((id, index) => !nodes.has(id) || nodes.get(id).parentId !== row.id || nodes.get(id).index !== index)))
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 && (!value.nodes.length || counts.total !== value.nodes.length || counts.visited !== value.nodes.length))
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 || value.complete && counts.visited !== tokens + value.sets.length + value.themes.length)
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 || chunk.fileId !== first.fileId || chunk.pageId !== first.pageId || chunk.revision !== first.revision || chunk.queryHash !== first.queryHash || chunk.digest !== first.digest || chunk.totalCodeUnits !== first.totalCodeUnits || chunk.sourceComplete !== first.sourceComplete)
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 || !["shape-inspection", "token-inspection"].includes(value.kind) || value.fileId !== first.fileId || value.pageId !== first.pageId || value.revision !== first.revision || value.complete !== first.sourceComplete)
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, maxTotalBytes = budget.maxTotalBytes ?? 16 * 1024 * 1024, timeoutMs = budget.timeoutMs ?? 60000;
310
- if (!Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > 2048 || !Number.isInteger(maxTotalBytes) || maxTotalBytes < 4096 || maxTotalBytes > 128 * 1024 * 1024 || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300000)
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
- return { maxCalls, maxTotalBytes, timeoutMs };
582
+ }
583
+ return {
584
+ maxCalls,
585
+ maxTotalBytes,
586
+ timeoutMs,
587
+ };
313
588
  }
314
589
  export async function captureInspection(client, query, budget = {}) {
315
- if (query.cursor)
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(), chunks = [];
320
- const metrics = { calls: 0, requestSnippetBytes: 0, responsePayloadBytes: 0, maxRequestSnippetBytes: 0, maxResponsePayloadBytes: 0, assembledBytes: 0, durationMs: 0 };
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) => ({ schemaVersion: 1, kind: "inspection-partial", fileId: chunks[0]?.fileId ?? null, pageId: chunks[0]?.pageId ?? null, complete: false, reason, chunks, nextCursor, metrics: { ...metrics, durationMs: Date.now() - started } });
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
- const chunk = await inspectChunk(client, { ...query, cursor }, Math.max(1, timeoutMs - (Date.now() - started)));
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 { ...document, capturedAt: new Date().toISOString(), sha256: createHash("sha256").update(chunks.map(entry => entry.chunk).join(""), "utf8").digest("hex"), metrics };
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
  }