@tangle-network/agent-app 0.45.27 → 0.45.29

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 (76) hide show
  1. package/dist/{DesignCanvas-3RRPVPCP.js → DesignCanvas-RSWQCXM3.js} +2 -2
  2. package/dist/{DesignCanvasEditor-KAMMEX7E.js → DesignCanvasEditor-XZMGIST7.js} +4 -4
  3. package/dist/assistant/index.d.ts +63 -39
  4. package/dist/assistant/index.js +23 -5
  5. package/dist/assistant/index.js.map +1 -1
  6. package/dist/chat-react/index.d.ts +25 -2
  7. package/dist/chat-react/index.js.map +1 -1
  8. package/dist/chat-routes/index.js +1 -1
  9. package/dist/{chunk-ODYE4A7L.js → chunk-3I27SDU3.js} +142 -58
  10. package/dist/chunk-3I27SDU3.js.map +1 -0
  11. package/dist/chunk-C3CAYGGQ.js +18 -0
  12. package/dist/chunk-C3CAYGGQ.js.map +1 -0
  13. package/dist/chunk-EMLGWEHV.js +33 -0
  14. package/dist/chunk-EMLGWEHV.js.map +1 -0
  15. package/dist/{chunk-XEA5EG6P.js → chunk-EPZYYWCT.js} +8 -30
  16. package/dist/chunk-EPZYYWCT.js.map +1 -0
  17. package/dist/{chunk-NC2V63EL.js → chunk-FNM7JYME.js} +88 -21
  18. package/dist/chunk-FNM7JYME.js.map +1 -0
  19. package/dist/{chunk-WD2B6HGY.js → chunk-K4RA5T3A.js} +11 -32
  20. package/dist/chunk-K4RA5T3A.js.map +1 -0
  21. package/dist/{chunk-4QVG4WJQ.js → chunk-OYTMILRK.js} +10 -3
  22. package/dist/chunk-OYTMILRK.js.map +1 -0
  23. package/dist/{chunk-OYGM5WLD.js → chunk-PURHUAOR.js} +52 -15
  24. package/dist/chunk-PURHUAOR.js.map +1 -0
  25. package/dist/{chunk-AWPK7ZDS.js → chunk-QGOU3VXH.js} +3 -3
  26. package/dist/{chunk-RT5RKAFX.js → chunk-QGSVF6DZ.js} +8 -18
  27. package/dist/chunk-QGSVF6DZ.js.map +1 -0
  28. package/dist/{chunk-OTLEYHOG.js → chunk-RRH23CGS.js} +24 -4
  29. package/dist/chunk-RRH23CGS.js.map +1 -0
  30. package/dist/{chunk-N3G3FSM4.js → chunk-YXA3U4JZ.js} +9 -47
  31. package/dist/chunk-YXA3U4JZ.js.map +1 -0
  32. package/dist/design-canvas-react/engine.d.ts +15 -1
  33. package/dist/design-canvas-react/engine.js +6 -2
  34. package/dist/design-canvas-react/index.d.ts +13 -1
  35. package/dist/design-canvas-react/index.js +13 -8
  36. package/dist/design-canvas-react/index.js.map +1 -1
  37. package/dist/design-canvas-react/lazy.js +1 -1
  38. package/dist/legibility/cli.js +14 -8
  39. package/dist/legibility/cli.js.map +1 -1
  40. package/dist/legibility/index.js +2 -1
  41. package/dist/peer-floors/cli.js +14 -8
  42. package/dist/peer-floors/cli.js.map +1 -1
  43. package/dist/preflight/cli.js +4 -2
  44. package/dist/preflight/cli.js.map +1 -1
  45. package/dist/{run-tEsZUhAf.d.ts → run-D80hoLKh.d.ts} +1 -1
  46. package/dist/sandbox/index.d.ts +11 -1
  47. package/dist/sandbox/index.js +1 -1
  48. package/dist/signoff/cli.d.ts +2 -2
  49. package/dist/signoff/cli.js +6 -18
  50. package/dist/signoff/cli.js.map +1 -1
  51. package/dist/signoff/index.d.ts +4 -420
  52. package/dist/signoff/index.js +3 -55
  53. package/dist/signoff/proof-cli.js +11 -6
  54. package/dist/signoff/proof-cli.js.map +1 -1
  55. package/dist/signoff/proof.d.ts +70 -271
  56. package/dist/signoff/proof.js +3 -73
  57. package/dist/signoff/proof.js.map +1 -1
  58. package/dist/theme-contract/cli.js +6 -2
  59. package/dist/theme-contract/cli.js.map +1 -1
  60. package/dist/theme-contract/index.js +2 -1
  61. package/dist/{types-U7Nz-txa.d.ts → types-Bd6uW9vQ.d.ts} +1 -1
  62. package/dist/web-react/index.d.ts +51 -44
  63. package/dist/web-react/index.js +1 -1
  64. package/package.json +14 -3
  65. package/dist/chunk-4QVG4WJQ.js.map +0 -1
  66. package/dist/chunk-N3G3FSM4.js.map +0 -1
  67. package/dist/chunk-NC2V63EL.js.map +0 -1
  68. package/dist/chunk-ODYE4A7L.js.map +0 -1
  69. package/dist/chunk-OTLEYHOG.js.map +0 -1
  70. package/dist/chunk-OYGM5WLD.js.map +0 -1
  71. package/dist/chunk-RT5RKAFX.js.map +0 -1
  72. package/dist/chunk-WD2B6HGY.js.map +0 -1
  73. package/dist/chunk-XEA5EG6P.js.map +0 -1
  74. /package/dist/{DesignCanvas-3RRPVPCP.js.map → DesignCanvas-RSWQCXM3.js.map} +0 -0
  75. /package/dist/{DesignCanvasEditor-KAMMEX7E.js.map → DesignCanvasEditor-XZMGIST7.js.map} +0 -0
  76. /package/dist/{chunk-AWPK7ZDS.js.map → chunk-QGOU3VXH.js.map} +0 -0
@@ -1,3 +1,11 @@
1
+ // src/signoff/proof-record.ts
2
+ import { spawnSync as spawnSync2 } from "child_process";
3
+ import { createHash, createHmac, timingSafeEqual } from "crypto";
4
+ import { readFileSync } from "fs";
5
+ import { hostname, userInfo } from "os";
6
+ import { join as join2 } from "path";
7
+ import { z } from "zod";
8
+
1
9
  // src/signoff/proof-git.ts
2
10
  import { spawnSync } from "child_process";
3
11
  import { mkdtempSync, rmSync } from "fs";
@@ -69,12 +77,6 @@ function computeWorktreeTree(repoDir) {
69
77
  }
70
78
 
71
79
  // src/signoff/proof-record.ts
72
- import { spawnSync as spawnSync2 } from "child_process";
73
- import { createHash, createHmac, timingSafeEqual } from "crypto";
74
- import { readFileSync } from "fs";
75
- import { hostname, userInfo } from "os";
76
- import { join as join2 } from "path";
77
- import { z } from "zod";
78
80
  var SIGNOFF_PROOF_VERSION = 1;
79
81
  var isoString = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp");
80
82
  var sha1Hex = z.string().regex(/^[0-9a-f]{40}$/, "must be a 40-hex git object id");
@@ -259,10 +261,6 @@ function buildSignoffProof(input) {
259
261
  function parseSignoffProof(json) {
260
262
  return signoffProofSchema.parse(JSON.parse(json));
261
263
  }
262
- function serializeSignoffProof(proof) {
263
- return `${JSON.stringify(proof, null, 2)}
264
- `;
265
- }
266
264
  function formatDuration(ms) {
267
265
  const seconds = Math.round(ms / 1e3);
268
266
  if (seconds < 60) return `${seconds}s`;
@@ -344,13 +342,6 @@ var SIGNOFF_REQUIRED_STEPS = {
344
342
  "tax-agent": ["install", "peer-check", "typecheck", "test", "toolkit-deps", "toolkit-test", "build", "worker-startup"],
345
343
  "legal-agent": ["install", "peer-check", "typegen", "typecheck", "test", "build:check"]
346
344
  };
347
- function requiredStepsFor(repo) {
348
- const steps = SIGNOFF_REQUIRED_STEPS[repo];
349
- if (steps === void 0) {
350
- throw new Error(`no required-step table for repo ${JSON.stringify(repo)}; known: ${Object.keys(SIGNOFF_REQUIRED_STEPS).sort().join(", ")}`);
351
- }
352
- return steps;
353
- }
354
345
  function verifySignoffProof(proof, options) {
355
346
  const failures = [];
356
347
  const { body, seal } = proof;
@@ -465,45 +456,16 @@ function formatSignoffVerification(result) {
465
456
  }
466
457
 
467
458
  export {
468
- SignoffGitError,
469
- runGit,
470
- gitText,
471
- gitIsAncestor,
472
- resolveCommit,
473
459
  readCommitFacts,
474
- computeWorktreeTree,
475
- SIGNOFF_PROOF_VERSION,
476
- SIGNOFF_STEP_STATUSES,
477
- signoffProofStepSchema,
478
- signoffProofBodySchema,
479
- signoffProofSealSchema,
480
- signoffProofSchema,
481
- canonicalJson,
482
- canonicalizeProofBody,
483
- hashProofBody,
484
460
  hashStepOutput,
485
- signoffKeyId,
486
461
  readSignoffKey,
487
- sealProof,
488
- macMatches,
489
- collectToolingFacts,
490
- readInstalledVersion,
491
- tangleDependencyNames,
492
462
  buildSignoffProof,
493
463
  parseSignoffProof,
494
- serializeSignoffProof,
495
464
  formatSignoffSummary,
496
- SIGNOFF_NOTES_REF,
497
- SIGNOFF_NOTES_GIT_CONFIG,
498
465
  attachSignoffProof,
499
- readSignoffProofNote,
500
- listSignoffProofs,
501
- resolveSignoffProof,
502
- SIGNOFF_REQUIRED_STEPS,
503
- requiredStepsFor,
504
466
  verifySignoffProof,
505
467
  verifySignoffAtRev,
506
468
  verifySignoffProofFile,
507
469
  formatSignoffVerification
508
470
  };
509
- //# sourceMappingURL=chunk-N3G3FSM4.js.map
471
+ //# sourceMappingURL=chunk-YXA3U4JZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/signoff/proof-record.ts","../src/signoff/proof-git.ts","../src/signoff/proof-attach.ts","../src/signoff/proof-verify.ts"],"sourcesContent":["/**\n * The sign-off proof record: what a local verification run produces so that a\n * merged commit can be interrogated later.\n *\n * ## Threat model — read this before trusting a proof\n *\n * A proof defends against **accident and drift**:\n * - a check that was never run, or ran and failed, being reported as green\n * - a proof produced over a different tree than the commit carries\n * - a proof copied from one commit onto another\n * - a field edited by hand after the fact\n * - a run whose peer versions or tool versions differ from what a reader assumes\n *\n * It does **not** defend against a malicious operator. The HMAC key is a local\n * file on the same machine that runs the checks, so anyone who can run a\n * sign-off can also mint a proof for checks that never ran. There is no secret\n * server, by requirement. What the seal buys is that a proof cannot be produced\n * or altered by someone WITHOUT that key, and that a proof cannot be silently\n * retargeted, which is the whole failure class an absent CI leaves open.\n *\n * Trust in the numbers themselves comes from the steps being real commands with\n * real exit codes and an output digest — not from cryptography.\n */\nimport { spawnSync } from 'node:child_process'\nimport { createHash, createHmac, timingSafeEqual } from 'node:crypto'\nimport { readFileSync } from 'node:fs'\nimport { hostname, userInfo } from 'node:os'\nimport { join } from 'node:path'\nimport { z } from 'zod'\nimport { computeWorktreeTree, readCommitFacts } from './proof-git'\nimport type { SignoffStepStatus } from './types'\n\n/** Bumped when the canonical body shape changes; a verifier refuses versions it does not know. */\nexport const SIGNOFF_PROOF_VERSION = 1\n\nconst isoString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$/, 'must be a UTC ISO-8601 timestamp')\nconst sha1Hex = z.string().regex(/^[0-9a-f]{40}$/, 'must be a 40-hex git object id')\nconst sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, 'must be a 64-hex sha256 digest')\n\n/**\n * The runner's step outcomes, restated as proof vocabulary.\n *\n * A status is carried alongside the exit code because they answer different\n * questions and only one of them is safe on its own: a step that was `skipped`,\n * `cancelled` or `blocked` never produced an exit code at all, and defaulting\n * that to `0` is precisely how a run that did not happen reads as a pass.\n *\n * The `satisfies` and the `Exclude` below pin this list to the runner's own\n * union in BOTH directions, so a status added there fails this file's typecheck\n * instead of quietly arriving as an unmodelled string.\n */\nconst SIGNOFF_STEP_STATUSES = ['passed', 'failed', 'skipped', 'cancelled', 'blocked'] as const satisfies readonly SignoffStepStatus[]\ntype UnmodelledStatus = Exclude<SignoffStepStatus, (typeof SIGNOFF_STEP_STATUSES)[number]>\nconst _everyRunnerStatusIsModelled: UnmodelledStatus[] = []\nvoid _everyRunnerStatusIsModelled\n\nconst signoffProofStepSchema = z.object({\n /** Stable id a repo's required-step table refers to (`typecheck`, `test`, `knip`, …). */\n id: z.string().min(1),\n command: z.string().min(1),\n cwd: z.string().min(1),\n /** How the runner judged the step. Only `passed` can satisfy a requirement. */\n status: z.enum(SIGNOFF_STEP_STATUSES),\n exitCode: z.number().int(),\n durationMs: z.number().int().nonnegative(),\n startedAt: isoString,\n /** sha256 of the step's combined stdout+stderr. Logs are not carried; the digest is. */\n outputSha256: sha256Hex,\n})\n\nconst signoffProofPeerSchema = z.object({\n name: z.string().min(1),\n /** `null` when the package is not resolvable on disk — recorded, never guessed. */\n version: z.string().min(1).nullable(),\n})\n\nconst signoffProofSubjectSchema = z.object({\n repo: z.string().min(1),\n commit: sha1Hex,\n /** Tree the checks actually ran against, including uncommitted work. */\n tree: sha1Hex,\n /** Tree the commit itself carries. Equal to `tree` on a clean sign-off. */\n commitTree: sha1Hex,\n parents: z.array(sha1Hex),\n committedAt: isoString,\n})\n\nconst signoffProofBodySchema = z.object({\n proofVersion: z.number().int().positive(),\n subject: signoffProofSubjectSchema,\n signedAt: isoString,\n host: z.object({\n hostname: z.string().min(1),\n platform: z.string().min(1),\n arch: z.string().min(1),\n user: z.string().min(1),\n }),\n tooling: z.object({\n node: z.string().min(1),\n pnpm: z.string().min(1).nullable(),\n peers: z.array(signoffProofPeerSchema),\n }),\n /**\n * Real elapsed time for the whole run. Recorded separately from the steps\n * because the runner schedules them as wide as their dependencies allow, so\n * the sum of step durations is the SERIAL cost and would overstate this.\n */\n wallClockMs: z.number().int().nonnegative(),\n /** Seeds fed to anything non-deterministic, so a reader can reproduce the same run. */\n seeds: z.record(z.string(), z.union([z.string(), z.number()])),\n /** The step ids this run claims were required. The verifier holds the authoritative table. */\n declaredRequired: z.array(z.string().min(1)),\n steps: z.array(signoffProofStepSchema),\n verdict: z.enum(['pass', 'fail']),\n})\n\nconst signoffProofSealSchema = z.object({\n algorithm: z.enum(['sha256', 'hmac-sha256']),\n /** sha256 over the canonical body. Chains the seal to every field, including the commit and tree. */\n bodySha256: sha256Hex,\n /** First 12 hex of sha256(key), so a reader can tell WHICH key sealed this. */\n keyId: z.string().regex(/^[0-9a-f]{12}$/).nullable(),\n mac: sha256Hex.nullable(),\n})\n\nconst signoffProofSchema = z.object({\n body: signoffProofBodySchema,\n seal: signoffProofSealSchema,\n})\n\nexport type SignoffProofStep = z.infer<typeof signoffProofStepSchema>\nexport type SignoffProofPeer = z.infer<typeof signoffProofPeerSchema>\nexport type SignoffProofSubject = z.infer<typeof signoffProofSubjectSchema>\nexport type SignoffProofBody = z.infer<typeof signoffProofBodySchema>\nexport type SignoffProofSeal = z.infer<typeof signoffProofSealSchema>\nexport type SignoffProof = z.infer<typeof signoffProofSchema>\n\ntype CanonicalValue = string | number | boolean | null | readonly CanonicalValue[] | { readonly [key: string]: CanonicalValue }\n\n/**\n * Deterministic JSON: object keys sorted, array order preserved, no whitespace.\n *\n * The seal is a hash over this string, so two readers must produce byte-identical\n * bytes from the same record. `undefined` throws rather than vanishing — a field\n * that silently disappears is a field the hash stops covering.\n */\nexport function canonicalJson(value: CanonicalValue): string {\n if (value === null) return 'null'\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new Error(`canonicalJson: ${String(value)} is not representable`)\n return JSON.stringify(value)\n }\n if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`\n const record = value as { readonly [key: string]: CanonicalValue }\n const keys = Object.keys(record).sort()\n const fields = keys.map((key) => {\n const entry = record[key]\n if (entry === undefined) throw new Error(`canonicalJson: field ${JSON.stringify(key)} is undefined`)\n return `${JSON.stringify(key)}:${canonicalJson(entry)}`\n })\n return `{${fields.join(',')}}`\n}\n\nexport function canonicalizeProofBody(body: SignoffProofBody): string {\n return canonicalJson(body as unknown as CanonicalValue)\n}\n\nexport function hashProofBody(body: SignoffProofBody): string {\n return createHash('sha256').update(canonicalizeProofBody(body), 'utf8').digest('hex')\n}\n\n/** The digest a runner records for a step's combined output. Shared so both halves agree. */\nexport function hashStepOutput(output: string): string {\n return createHash('sha256').update(output, 'utf8').digest('hex')\n}\n\nexport function signoffKeyId(key: Uint8Array): string {\n return createHash('sha256').update(key).digest('hex').slice(0, 12)\n}\n\n/** Read the local sign-off key. Throws when absent — an unreadable key is never a silent downgrade to unsealed. */\nexport function readSignoffKey(path: string): Uint8Array {\n const raw = readFileSync(path)\n if (raw.byteLength < 16) throw new Error(`sign-off key at ${path} is ${raw.byteLength} bytes; at least 16 are required`)\n return new Uint8Array(raw)\n}\n\nexport function sealProof(body: SignoffProofBody, key?: Uint8Array): SignoffProof {\n const canonical = canonicalizeProofBody(body)\n const bodySha256 = createHash('sha256').update(canonical, 'utf8').digest('hex')\n if (key === undefined) {\n return { body, seal: { algorithm: 'sha256', bodySha256, keyId: null, mac: null } }\n }\n return {\n body,\n seal: {\n algorithm: 'hmac-sha256',\n bodySha256,\n keyId: signoffKeyId(key),\n mac: createHmac('sha256', key).update(canonical, 'utf8').digest('hex'),\n },\n }\n}\n\n/** Constant-time comparison of two hex digests of equal length. */\nexport function macMatches(expected: string, actual: string): boolean {\n if (expected.length !== actual.length) return false\n return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(actual, 'hex'))\n}\n\ninterface ToolingFactsInput {\n readonly repoDir: string\n /** Package names whose resolved on-disk version belongs in the proof. */\n readonly peerNames: readonly string[]\n}\n\n/**\n * The versions that actually resolved on disk — the field CI's clean install\n * makes trustworthy and a warm local `node_modules` does not. A below-floor peer\n * is invisible to typecheck and to a green suite (it fails at the wire call), so\n * the proof records what was really there rather than what the manifest asks for.\n */\nfunction collectToolingFacts(input: ToolingFactsInput): SignoffProofBody['tooling'] {\n return {\n node: process.version,\n pnpm: readPnpmVersion(input.repoDir),\n peers: input.peerNames.map((name) => ({ name, version: readInstalledVersion(input.repoDir, name) })),\n }\n}\n\n/** `null` when pnpm is not on PATH — a missing version is recorded as missing, never as a guess. */\nfunction readPnpmVersion(repoDir: string): string | null {\n const result = spawnSync('pnpm', ['--version'], { cwd: repoDir, encoding: 'utf8' })\n if (result.error || result.status !== 0) return null\n return result.stdout.trim()\n}\n\n/** Resolved version from the consumer's own `node_modules`, or `null` when the package is not there. */\nfunction readInstalledVersion(repoDir: string, packageName: string): string | null {\n try {\n const manifest = JSON.parse(readFileSync(join(repoDir, 'node_modules', packageName, 'package.json'), 'utf8')) as { version?: unknown }\n return typeof manifest.version === 'string' ? manifest.version : null\n } catch {\n return null\n }\n}\n\n/** Every `@tangle-network/*` name the repo declares as a peer or a dependency. */\nfunction tangleDependencyNames(repoDir: string): readonly string[] {\n const manifest = JSON.parse(readFileSync(join(repoDir, 'package.json'), 'utf8')) as {\n dependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n }\n const names = new Set<string>()\n for (const block of [manifest.dependencies, manifest.peerDependencies, manifest.devDependencies]) {\n for (const name of Object.keys(block ?? {})) {\n if (name.startsWith('@tangle-network/')) names.add(name)\n }\n }\n return [...names].sort()\n}\n\nexport interface BuildSignoffProofInput {\n readonly repoDir: string\n /** Repo identity the verifier's required-step table is keyed on. */\n readonly repo: string\n /** Revision the sign-off is for; defaults to `HEAD`. */\n readonly rev?: string\n readonly steps: readonly SignoffProofStep[]\n /** Measured elapsed time for the run. Required — it is not derivable from the steps. */\n readonly wallClockMs: number\n /** Step ids this run treated as required. Checked against the verifier's table. */\n readonly declaredRequired: readonly string[]\n readonly seeds: Readonly<Record<string, string | number>>\n readonly peerNames?: readonly string[]\n readonly key?: Uint8Array\n readonly now?: Date\n}\n\nexport function buildSignoffProof(input: BuildSignoffProofInput): SignoffProof {\n const facts = readCommitFacts(input.repoDir, input.rev ?? 'HEAD')\n const body: SignoffProofBody = {\n proofVersion: SIGNOFF_PROOF_VERSION,\n subject: {\n repo: input.repo,\n commit: facts.commit,\n tree: computeWorktreeTree(input.repoDir),\n commitTree: facts.commitTree,\n parents: [...facts.parents],\n committedAt: facts.committedAt,\n },\n signedAt: (input.now ?? new Date()).toISOString(),\n wallClockMs: Math.max(0, Math.round(input.wallClockMs)),\n host: { hostname: hostname(), platform: process.platform, arch: process.arch, user: userInfo().username },\n tooling: collectToolingFacts({ repoDir: input.repoDir, peerNames: input.peerNames ?? tangleDependencyNames(input.repoDir) }),\n seeds: { ...input.seeds },\n declaredRequired: [...input.declaredRequired],\n steps: input.steps.map((step) => ({ ...step })),\n verdict: input.steps.every((step) => step.status === 'passed' && step.exitCode === 0) ? 'pass' : 'fail',\n }\n return sealProof(signoffProofBodySchema.parse(body), input.key)\n}\n\n/** Parse an untrusted proof document, failing loud on any shape the verifier cannot reason about. */\nexport function parseSignoffProof(json: string): SignoffProof {\n return signoffProofSchema.parse(JSON.parse(json))\n}\n\nexport function serializeSignoffProof(proof: SignoffProof): string {\n return `${JSON.stringify(proof, null, 2)}\\n`\n}\n\nfunction formatDuration(ms: number): string {\n const seconds = Math.round(ms / 1000)\n if (seconds < 60) return `${seconds}s`\n return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`\n}\n\n/**\n * The single line an operator pastes into a PR or a merge commit: how many steps\n * ran, how long it took, the seeds, and the verdict.\n */\nexport function formatSignoffSummary(proof: SignoffProof): string {\n const { body, seal } = proof\n const passed = body.steps.filter((step) => step.status === 'passed' && step.exitCode === 0).length\n const serialMs = body.steps.reduce((total, step) => total + step.durationMs, 0)\n // Wall clock is the number an operator is deciding on. The serial total rides\n // alongside it only when the schedule actually overlapped, so a parallel run\n // shows its saving instead of claiming one.\n const wall = serialMs > body.wallClockMs ? `${formatDuration(body.wallClockMs)} (serial ${formatDuration(serialMs)})` : formatDuration(body.wallClockMs)\n const seeds = Object.entries(body.seeds)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([name, value]) => `${name}=${value}`)\n .join(' ')\n const seal_ = seal.algorithm === 'hmac-sha256' ? `sealed ${seal.algorithm} key ${seal.keyId ?? 'unknown'}` : `unsealed sha256 ${seal.bodySha256.slice(0, 12)}`\n const dirty = body.subject.tree === body.subject.commitTree ? '' : ' DIRTY-TREE'\n return [\n `signoff ${body.verdict}${dirty}`,\n `${passed}/${body.steps.length} steps`,\n wall,\n `${body.subject.repo}@${body.subject.commit.slice(0, 9)} tree ${body.subject.tree.slice(0, 9)}`,\n seeds.length === 0 ? 'seeds none' : `seeds ${seeds}`,\n `node ${body.tooling.node} pnpm ${body.tooling.pnpm ?? 'unresolved'}`,\n seal_,\n body.signedAt,\n ].join(' · ')\n}\n","/**\n * The git facts a sign-off proof is bound to, read straight from the object\n * database rather than reported by the process that ran the checks.\n *\n * Everything here is re-derivable by anyone holding the repository, which is\n * what makes the proof checkable: a verifier never trusts a field in the JSON,\n * it recomputes the same value with the same commands and compares.\n */\nimport { spawnSync } from 'node:child_process'\nimport { mkdtempSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\n/** A git invocation that exited non-zero, carrying stderr so the caller can act. */\nexport class SignoffGitError extends Error {\n readonly args: readonly string[]\n readonly status: number | null\n readonly stderr: string\n\n constructor(args: readonly string[], status: number | null, stderr: string) {\n super(`git ${args.join(' ')} exited ${status ?? 'null'}: ${stderr.trim()}`)\n this.name = 'SignoffGitError'\n this.args = args\n this.status = status\n this.stderr = stderr\n }\n}\n\nexport interface GitResult {\n readonly status: number | null\n readonly stdout: string\n readonly stderr: string\n}\n\n/**\n * Run git and hand back the raw result. `GIT_OPTIONAL_LOCKS=0` keeps a read\n * from touching the index while another agent works the same worktree — this\n * repo is shared by concurrent sessions.\n */\nexport function runGit(repoDir: string, args: readonly string[], options: { readonly input?: string; readonly env?: Readonly<Record<string, string>> } = {}): GitResult {\n const result = spawnSync('git', [...args], {\n cwd: repoDir,\n encoding: 'utf8',\n input: options.input,\n env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...options.env },\n maxBuffer: 128 * 1024 * 1024,\n })\n if (result.error) throw new Error(`git ${args.join(' ')} could not run: ${result.error.message}`)\n return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }\n}\n\n/** Run git, or throw. Trailing newline is stripped — every caller wants the value, not the line. */\nexport function gitText(repoDir: string, args: readonly string[], options: { readonly input?: string; readonly env?: Readonly<Record<string, string>> } = {}): string {\n const result = runGit(repoDir, args, options)\n if (result.status !== 0) throw new SignoffGitError(args, result.status, result.stderr)\n return result.stdout.replace(/\\n$/, '')\n}\n\n/**\n * Is `ancestor` reachable from `descendant`? A port rather than a direct call so\n * the verifier stays a pure function over facts a caller supplies.\n */\nexport type IsAncestorFn = (ancestor: string, descendant: string) => boolean\n\nexport function gitIsAncestor(repoDir: string): IsAncestorFn {\n return (ancestor, descendant) => {\n const result = runGit(repoDir, ['merge-base', '--is-ancestor', ancestor, descendant])\n if (result.status === 0) return true\n if (result.status === 1) return false\n throw new SignoffGitError(['merge-base', '--is-ancestor', ancestor, descendant], result.status, result.stderr)\n }\n}\n\nexport interface CommitFacts {\n /** 40-hex commit id. */\n readonly commit: string\n /** 40-hex id of the tree the COMMIT points at. */\n readonly commitTree: string\n readonly parents: readonly string[]\n /** Committer date, UTC ISO-8601 with a `Z` suffix. */\n readonly committedAt: string\n}\n\n/** Resolve a revision to the commit it names, failing loud on an unknown rev. */\nexport function resolveCommit(repoDir: string, rev: string): string {\n return gitText(repoDir, ['rev-parse', '--verify', `${rev}^{commit}`])\n}\n\nexport function readCommitFacts(repoDir: string, rev: string): CommitFacts {\n const commit = resolveCommit(repoDir, rev)\n const record = gitText(repoDir, ['show', '--no-patch', '--format=%T%n%P%n%cI', commit])\n const [tree, parents, committedAt] = record.split('\\n')\n if (tree === undefined || parents === undefined || committedAt === undefined) {\n throw new Error(`git show returned an unreadable record for ${commit}: ${JSON.stringify(record)}`)\n }\n return {\n commit,\n commitTree: tree,\n parents: parents.length === 0 ? [] : parents.split(' '),\n committedAt: new Date(committedAt).toISOString(),\n }\n}\n\n/**\n * Hash the tree the checks actually ran against, including uncommitted and\n * untracked (non-ignored) files.\n *\n * This is the field that makes drift detectable. A sign-off that ran over an\n * edited worktree produces a tree id no commit carries, so it cannot verify\n * against the commit it claims — which is the intended outcome, not a bug.\n *\n * Written through a throwaway index (`GIT_INDEX_FILE`) so a concurrent agent's\n * staged work in the real index is neither read nor disturbed. `git add -A`\n * honours `.gitignore`, so `node_modules` / `dist` stay out.\n */\nexport function computeWorktreeTree(repoDir: string): string {\n const scratch = mkdtempSync(join(tmpdir(), 'agent-app-signoff-index-'))\n const indexFile = join(scratch, 'index')\n try {\n const env = { GIT_INDEX_FILE: indexFile }\n gitText(repoDir, ['add', '-A', '--'], { env })\n return gitText(repoDir, ['write-tree'], { env })\n } finally {\n rmSync(scratch, { recursive: true, force: true })\n }\n}\n","/**\n * Attaching a proof to the commit it verified.\n *\n * ## Why `git notes`, and what the two rejected carriers cost\n *\n * **A commit-message trailer** cannot work for the field that matters. The\n * trailer is part of the commit object, so writing it changes the SHA the proof\n * claims to cover — the proof would have to be produced before the commit it\n * names exists. Amending re-writes the SHA again, and GitHub's squash-merge\n * rebuilds the message from the PR body, so the trailer does not survive the one\n * transition it would need to.\n *\n * **A committed artifact directory** (`.signoff/<sha>.json`) changes the tree,\n * so the tree hash inside the proof can never cover the file carrying it. It\n * also lands in every diff, and it is exactly the \"file someone can forget\"\n * shape the requirement rules out — deleting it is a normal-looking change.\n *\n * **`git notes` wins because it is the only carrier that is addressed BY the\n * SHA without being part of it.** The note is written after the commit exists,\n * it changes neither the SHA nor the tree, and `git notes show <sha>` is a\n * definite yes/no for any commit anyone can name.\n *\n * ## The tradeoff `git notes` really carries, and how this module answers it\n *\n * Notes are attached to a commit id, and a rebase or a squash-merge produces a\n * NEW commit id, so the note does not follow by itself. Two mechanisms close\n * that gap, and both are needed:\n *\n * 1. `notes.rewriteRef` (see `SIGNOFF_NOTES_GIT_CONFIG`) makes local rebase and\n * amend copy the note forward automatically.\n * 2. `resolveSignoffProof` falls back to a **tree scan**: it reads every proof\n * under the notes ref and matches on the TREE the commit carries. A rebase\n * or a squash of one branch onto an unchanged base preserves the tree, so\n * the content that was verified is still provably the content that merged,\n * even though the SHA changed. The verifier reports which binding it used —\n * `exact` or `tree-equivalent` — and never conflates them.\n *\n * Notes are neither pushed nor fetched by default; `SIGNOFF_NOTES_GIT_CONFIG`\n * carries the refspecs that fix that, and a missing note is reported as\n * `not-found`, never as a pass.\n */\nimport { parseSignoffProof, type SignoffProof } from './proof-record'\nimport { gitText, readCommitFacts, resolveCommit, runGit, SignoffGitError } from './proof-git'\n\nexport const SIGNOFF_NOTES_REF = 'refs/notes/signoff'\n\n/**\n * The git configuration a repo needs for notes to travel. Emitted by the CLI\n * when a proof cannot be found, because the usual cause is an unconfigured repo\n * rather than an unsigned commit.\n */\nexport const SIGNOFF_NOTES_GIT_CONFIG: readonly string[] = [\n `git config --add remote.origin.fetch '+${SIGNOFF_NOTES_REF}:${SIGNOFF_NOTES_REF}'`,\n `git config --add remote.origin.push '${SIGNOFF_NOTES_REF}'`,\n `git config notes.rewriteRef '${SIGNOFF_NOTES_REF}'`,\n 'git config notes.rewrite.amend true',\n 'git config notes.rewrite.rebase true',\n]\n\nexport interface AttachSignoffProofInput {\n readonly repoDir: string\n readonly proof: SignoffProof\n /** Revision to annotate; defaults to the commit the proof names. */\n readonly rev?: string\n /** Replace an existing note. Off by default so a second sign-off cannot quietly overwrite the first. */\n readonly overwrite?: boolean\n}\n\nexport interface AttachedSignoffProof {\n readonly commit: string\n readonly ref: string\n}\n\nexport function attachSignoffProof(input: AttachSignoffProofInput): AttachedSignoffProof {\n const commit = resolveCommit(input.repoDir, input.rev ?? input.proof.body.subject.commit)\n const args = ['notes', `--ref=${SIGNOFF_NOTES_REF}`, 'add']\n if (input.overwrite === true) args.push('-f')\n args.push('-F', '-', commit)\n const result = runGit(input.repoDir, args, { input: `${JSON.stringify(input.proof, null, 2)}\\n` })\n if (result.status !== 0) throw new SignoffGitError(args, result.status, result.stderr)\n return { commit, ref: SIGNOFF_NOTES_REF }\n}\n\nexport type SignoffProofLookup =\n | { readonly found: true; readonly proof: SignoffProof; readonly commit: string; readonly binding: 'exact' | 'tree-equivalent' }\n | { readonly found: false; readonly commit: string }\n\n/** The note attached to exactly this commit, or `found: false`. Never searches. */\nexport function readSignoffProofNote(repoDir: string, rev: string): SignoffProofLookup {\n const commit = resolveCommit(repoDir, rev)\n const result = runGit(repoDir, ['notes', `--ref=${SIGNOFF_NOTES_REF}`, 'show', commit])\n if (result.status !== 0) return { found: false, commit }\n return { found: true, proof: parseSignoffProof(result.stdout), commit, binding: 'exact' }\n}\n\n/** Every proof stored under the notes ref, paired with the commit it annotates. */\nexport function listSignoffProofs(repoDir: string): readonly { readonly commit: string; readonly proof: SignoffProof }[] {\n const listed = runGit(repoDir, ['notes', `--ref=${SIGNOFF_NOTES_REF}`, 'list'])\n if (listed.status !== 0) return []\n const out: { commit: string; proof: SignoffProof }[] = []\n for (const line of listed.stdout.split('\\n')) {\n const [noteBlob, commit] = line.trim().split(/\\s+/)\n if (noteBlob === undefined || commit === undefined) continue\n out.push({ commit, proof: parseSignoffProof(gitText(repoDir, ['cat-file', 'blob', noteBlob])) })\n }\n return out\n}\n\n/**\n * Answer \"was this SHA signed off\" for any commit, including one produced by a\n * rebase or a squash-merge of the branch that was actually verified.\n *\n * Exact note first. Then the tree scan — a proof whose `commitTree` equals this\n * commit's tree verified byte-identical content, which is the strongest claim\n * available once the SHA has been rewritten. The binding is returned so a caller\n * can tell the two apart; nothing here treats them as the same thing.\n */\nexport function resolveSignoffProof(repoDir: string, rev: string): SignoffProofLookup {\n const direct = readSignoffProofNote(repoDir, rev)\n if (direct.found) return direct\n const facts = readCommitFacts(repoDir, rev)\n for (const entry of listSignoffProofs(repoDir)) {\n if (entry.proof.body.subject.commitTree === facts.commitTree) {\n return { found: true, proof: entry.proof, commit: facts.commit, binding: 'tree-equivalent' }\n }\n }\n return { found: false, commit: facts.commit }\n}\n","/**\n * `verify-proof` — the half a reader runs, on a machine that did not produce\n * the proof, to decide whether a commit was really signed off.\n *\n * The verifier never believes the document. Every claim it can re-derive, it\n * re-derives from git and compares; every claim it cannot re-derive is reported\n * as recorded, not as checked. The one field the proof is deliberately not\n * allowed to own is **which steps were required**: a run that declares its own\n * bar can declare an empty one, so the authoritative table lives here and a\n * proof declaring fewer requirements than its repo's table fails as\n * `lowered-bar`.\n */\nimport type { CommitFacts, IsAncestorFn } from './proof-git'\nimport { gitIsAncestor, readCommitFacts } from './proof-git'\nimport { readFileSync } from 'node:fs'\nimport { resolveSignoffProof, SIGNOFF_NOTES_GIT_CONFIG } from './proof-attach'\nimport { canonicalizeProofBody, hashProofBody, macMatches, parseSignoffProof, SIGNOFF_PROOF_VERSION, type SignoffProof } from './proof-record'\nimport { createHmac } from 'node:crypto'\n\n/**\n * The steps a repo's sign-off MUST cover, transcribed from each repo's CI job.\n * A runner is free to run more; it may never run fewer.\n *\n * agent-app .github/workflows/ci.yml\n * tax-agent .github/workflows/deploy.yml (the `ci` job)\n * legal-agent .github/workflows/deploy.yml (the `ci` job)\n */\nexport const SIGNOFF_REQUIRED_STEPS: Readonly<Record<string, readonly string[]>> = {\n 'agent-app': ['install', 'typecheck', 'test:gates', 'test', 'build', 'test:generated', 'knip'],\n 'tax-agent': ['install', 'peer-check', 'typecheck', 'test', 'toolkit-deps', 'toolkit-test', 'build', 'worker-startup'],\n 'legal-agent': ['install', 'peer-check', 'typegen', 'typecheck', 'test', 'build:check'],\n}\n\n/** Throws for a repo with no table — an unrecognised repo has no bar, and no bar is not a pass. */\nexport function requiredStepsFor(repo: string): readonly string[] {\n const steps = SIGNOFF_REQUIRED_STEPS[repo]\n if (steps === undefined) {\n throw new Error(`no required-step table for repo ${JSON.stringify(repo)}; known: ${Object.keys(SIGNOFF_REQUIRED_STEPS).sort().join(', ')}`)\n }\n return steps\n}\n\nexport type SignoffFailureCode =\n | 'unsupported-version'\n | 'body-tampered'\n | 'mac-missing'\n | 'mac-invalid'\n | 'unknown-repo'\n | 'repo-mismatch'\n | 'tree-mismatch'\n | 'dirty-worktree'\n | 'commit-unbound'\n | 'missing-required-step'\n | 'lowered-bar'\n | 'duplicate-step'\n | 'step-failed'\n | 'verdict-fail'\n | 'stale-proof'\n\nexport interface SignoffFailure {\n readonly code: SignoffFailureCode\n readonly detail: string\n}\n\n/** How the proof attaches to the commit that was asked about. */\nexport type SignoffCommitBinding =\n /** The proof names this exact commit. */\n | 'exact'\n /** A different commit id, but byte-identical content — a rebase or a squash of what was verified. */\n | 'tree-equivalent'\n /** Neither. The proof does not describe this commit. */\n | 'none'\n\nexport interface SignoffVerification {\n readonly ok: boolean\n readonly commitBinding: SignoffCommitBinding\n /** True only when a key was supplied AND the mac over the canonical body matched. */\n readonly macChecked: boolean\n readonly failures: readonly SignoffFailure[]\n readonly proof: SignoffProof\n readonly target: CommitFacts\n readonly requiredSteps: readonly string[]\n}\n\nexport interface VerifySignoffProofOptions {\n /** The commit the reader is asking about, read from git — never from the proof. */\n readonly target: CommitFacts\n /**\n * Reachability in the target's history. Required, because the clock cannot\n * answer staleness on its own: git records committer time to the SECOND, so a\n * proof and the commit it is being replayed onto routinely share a timestamp.\n * Ancestry is exact and clock-free.\n */\n readonly isAncestor: IsAncestorFn\n /** Local HMAC key. Omitted, the mac is reported unchecked rather than assumed good. */\n readonly key?: Uint8Array\n /** Overrides the built-in table. Supplying `[]` is a deliberate no-bar check, and says so. */\n readonly requiredSteps?: readonly string[]\n /** Repo identity the caller expects; a mismatch against the proof is a failure, not a rename. */\n readonly expectRepo?: string\n}\n\nexport function verifySignoffProof(proof: SignoffProof, options: VerifySignoffProofOptions): SignoffVerification {\n const failures: SignoffFailure[] = []\n const { body, seal } = proof\n const target = options.target\n\n if (body.proofVersion !== SIGNOFF_PROOF_VERSION) {\n failures.push({ code: 'unsupported-version', detail: `proof declares version ${body.proofVersion}; this verifier reads ${SIGNOFF_PROOF_VERSION}` })\n }\n\n const recomputed = hashProofBody(body)\n if (recomputed !== seal.bodySha256) {\n failures.push({ code: 'body-tampered', detail: `seal claims body sha256 ${seal.bodySha256}; the body hashes to ${recomputed}` })\n }\n\n let macChecked = false\n if (options.key !== undefined) {\n if (seal.mac === null) {\n failures.push({ code: 'mac-missing', detail: 'a key was supplied but the proof carries no mac; it was produced unsealed' })\n } else {\n const expected = createHmac('sha256', options.key).update(canonicalizeProofBody(body), 'utf8').digest('hex')\n if (macMatches(expected, seal.mac)) macChecked = true\n else failures.push({ code: 'mac-invalid', detail: `mac does not verify under the supplied key (proof keyId ${seal.keyId ?? 'none'})` })\n }\n }\n\n if (options.expectRepo !== undefined && options.expectRepo !== body.subject.repo) {\n failures.push({ code: 'repo-mismatch', detail: `proof is for repo ${body.subject.repo}; ${options.expectRepo} was requested` })\n }\n\n let requiredSteps: readonly string[] = options.requiredSteps ?? []\n if (options.requiredSteps === undefined) {\n const known = SIGNOFF_REQUIRED_STEPS[body.subject.repo]\n if (known === undefined) {\n failures.push({ code: 'unknown-repo', detail: `no required-step table for ${body.subject.repo}; a repo with no declared bar cannot be signed off` })\n } else {\n requiredSteps = known\n }\n }\n\n const seen = new Map<string, number>()\n for (const step of body.steps) seen.set(step.id, (seen.get(step.id) ?? 0) + 1)\n for (const [id, count] of seen) {\n if (count > 1) failures.push({ code: 'duplicate-step', detail: `step ${id} appears ${count} times; a repeated id makes coverage ambiguous` })\n }\n\n const missing = requiredSteps.filter((id) => !seen.has(id))\n if (missing.length > 0) {\n failures.push({ code: 'missing-required-step', detail: `required step(s) never ran: ${missing.join(', ')}` })\n }\n\n const declared = new Set(body.declaredRequired)\n const understated = requiredSteps.filter((id) => !declared.has(id))\n if (understated.length > 0) {\n failures.push({ code: 'lowered-bar', detail: `proof declares a smaller required set than ${body.subject.repo}'s table; missing: ${understated.join(', ')}` })\n }\n\n for (const step of body.steps) {\n if (step.status !== 'passed') {\n failures.push({ code: 'step-failed', detail: `step ${step.id} is ${step.status}, not passed (exit ${step.exitCode}, ${step.command})` })\n } else if (step.exitCode !== 0) {\n // An internally inconsistent record: the runner called it passed and the\n // process disagreed. Reported rather than resolved in either direction.\n failures.push({ code: 'step-failed', detail: `step ${step.id} claims status passed but exited ${step.exitCode} (${step.command})` })\n }\n }\n\n if (body.verdict !== 'pass') {\n failures.push({ code: 'verdict-fail', detail: `the run recorded verdict ${body.verdict}` })\n }\n\n if (body.subject.tree !== body.subject.commitTree) {\n failures.push({\n code: 'dirty-worktree',\n detail: `checks ran against tree ${body.subject.tree} while the commit carries ${body.subject.commitTree}; uncommitted work was in the tree`,\n })\n }\n\n if (body.subject.commitTree !== target.commitTree) {\n failures.push({ code: 'tree-mismatch', detail: `proof covers tree ${body.subject.commitTree}; ${target.commit} carries ${target.commitTree}` })\n }\n\n const commitBinding: SignoffCommitBinding =\n body.subject.commit === target.commit ? 'exact' : body.subject.commitTree === target.commitTree ? 'tree-equivalent' : 'none'\n if (commitBinding === 'none') {\n failures.push({ code: 'commit-unbound', detail: `proof names commit ${body.subject.commit}, which is neither ${target.commit} nor its content` })\n }\n\n // Staleness, checked two ways because neither is sufficient alone.\n //\n // The clock catches a proof back-dated relative to the commit it names. Its\n // resolution is git's, one second, so it is deliberately not the only check.\n const signedAt = Date.parse(body.signedAt)\n if (signedAt < Date.parse(body.subject.committedAt)) {\n failures.push({ code: 'stale-proof', detail: `signed at ${body.signedAt}, before the commit it names was written at ${body.subject.committedAt}` })\n }\n // Ancestry catches the case the clock cannot: a proof accepted on CONTENT\n // grounds for a LATER commit in the same history. A rebase or a squash\n // replaces the verified commit, so it is not reachable from the result — but a\n // revert restores an old tree on top of work nobody verified, and there the\n // proof's commit IS an ancestor. Same tree, different history, unverified\n // commits in between.\n if (commitBinding === 'tree-equivalent' && options.isAncestor(body.subject.commit, target.commit)) {\n failures.push({\n code: 'stale-proof',\n detail: `${body.subject.commit} is an ancestor of ${target.commit}; the tree matches only because later work was undone, and that work was never signed off`,\n })\n }\n\n return { ok: failures.length === 0, commitBinding, macChecked, failures, proof, target, requiredSteps }\n}\n\nexport interface VerifySignoffAtRevInput {\n readonly repoDir: string\n readonly rev: string\n readonly key?: Uint8Array\n readonly requiredSteps?: readonly string[]\n readonly expectRepo?: string\n}\n\ntype SignoffLookupFailure = { readonly found: false; readonly commit: string; readonly hint: readonly string[] }\nexport type SignoffVerifyOutcome = ({ readonly found: true } & SignoffVerification) | SignoffLookupFailure\n\n/** Verify by revision: find the proof attached to that SHA (or to its content), then check it. */\nexport function verifySignoffAtRev(input: VerifySignoffAtRevInput): SignoffVerifyOutcome {\n const lookup = resolveSignoffProof(input.repoDir, input.rev)\n const target = readCommitFacts(input.repoDir, input.rev)\n if (!lookup.found) return { found: false, commit: lookup.commit, hint: SIGNOFF_NOTES_GIT_CONFIG }\n return {\n found: true,\n ...verifySignoffProof(lookup.proof, {\n target,\n isAncestor: gitIsAncestor(input.repoDir),\n key: input.key,\n requiredSteps: input.requiredSteps,\n expectRepo: input.expectRepo,\n }),\n }\n}\n\nexport interface VerifySignoffFileInput {\n readonly repoDir: string\n readonly file: string\n /**\n * The commit to check the file against. REQUIRED, and deliberately not\n * defaulted to the commit the proof names: a proof checked against its own\n * subject can never fail the commit binding, which is a check that reads as\n * strong and cannot catch anything. The reader always states what they are\n * asking about.\n */\n readonly rev: string\n readonly key?: Uint8Array\n readonly requiredSteps?: readonly string[]\n readonly expectRepo?: string\n}\n\n/** Verify a proof document on disk. The repo is still required — \"matches the tree it claims\" is not answerable without it. */\nexport function verifySignoffProofFile(input: VerifySignoffFileInput): SignoffVerification {\n const proof = parseSignoffProof(readFileSync(input.file, 'utf8'))\n const target = readCommitFacts(input.repoDir, input.rev)\n return verifySignoffProof(proof, {\n target,\n isAncestor: gitIsAncestor(input.repoDir),\n key: input.key,\n requiredSteps: input.requiredSteps,\n expectRepo: input.expectRepo,\n })\n}\n\n/** One line per failure, prefixed by its code, in the order the checks ran. */\nexport function formatSignoffVerification(result: SignoffVerification): string {\n const head = result.ok\n ? `VERIFIED ${result.proof.body.subject.repo}@${result.target.commit.slice(0, 9)} binding=${result.commitBinding} mac=${result.macChecked ? 'checked' : 'unchecked'}`\n : `REJECTED ${result.proof.body.subject.repo}@${result.target.commit.slice(0, 9)} binding=${result.commitBinding} (${result.failures.length} failure${result.failures.length === 1 ? '' : 's'})`\n return [head, ...result.failures.map((failure) => ` ${failure.code}: ${failure.detail}`)].join('\\n')\n}\n"],"mappings":"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","names":["spawnSync","join","spawnSync","join","readFileSync","createHmac","createHmac","readFileSync"]}
@@ -267,6 +267,20 @@ declare function resolveExportParams(page: ScenePage, opts: {
267
267
  includeBleed?: boolean;
268
268
  preset?: ExportPreset;
269
269
  }): ResolvedExportParams;
270
+ /** Hard ceiling for node cache rasterization. Without it a deep zoom (max 32)
271
+ * on a retina display would rasterize page-size elements into canvases beyond
272
+ * browser backing-store limits; 4x supersampling covers every realistic
273
+ * crispness-critical range (fit-page → 200% at dpr 2). */
274
+ declare const NODE_CACHE_PIXEL_RATIO_MAX = 4;
275
+ /**
276
+ * Pixel ratio a Konva node bitmap cache should rasterize at so the cached
277
+ * bitmap matches the screen exactly: the node's absolute scale (content-group
278
+ * zoom) × device pixel ratio. Konva's cache default ignores zoom, so the
279
+ * bitmap is re-scaled at draw time — bilinear-soft at fit-page zooms like
280
+ * 0.47, badly upscaled on zoom-in. ElementNode caches at this ratio and
281
+ * export.ts restores cleared caches at it, keeping one policy in one place.
282
+ */
283
+ declare function resolveNodeCachePixelRatio(absoluteScale: number, devicePixelRatio: number): number;
270
284
  /**
271
285
  * Walk image nodes to find the src of any that may have caused a SecurityError
272
286
  * when the stage called toDataURL. Returns the first offending src found, or
@@ -383,4 +397,4 @@ interface InsertTemplate {
383
397
  * default so every canvas gets a usable set out of the box. */
384
398
  declare const DEFAULT_INSERT_TEMPLATES: readonly InsertTemplate[];
385
399
 
386
- export { type AddElementInput, type AddPageInput, type BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, type DeleteElementInput, type DeletePageInput, type DuplicatePageInput, ExportCropRect, ExportPreset, type ExportViewSnapshot, type GroupElementsInput, type InsertPageGeometry, type InsertTemplate, MAX_INSERT_DIMENSION, type MarqueeSelectOptions, type MultiSetAttrsEntry, type NudgeDelta, type ReorderElementInput, type ReorderPageInput, type ResolvedExportParams, SceneCommand, type SetAttrsInput, type SetDocumentTitleInput, type SetPageGuidesInput, type SetPagePropsInput, SnapEngine, SnapTarget, type UngroupElementInput, type ZoomPanConfig, ZoomPanMath, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand };
400
+ export { type AddElementInput, type AddPageInput, type BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, type DeleteElementInput, type DeletePageInput, type DuplicatePageInput, ExportCropRect, ExportPreset, type ExportViewSnapshot, type GroupElementsInput, type InsertPageGeometry, type InsertTemplate, MAX_INSERT_DIMENSION, type MarqueeSelectOptions, type MultiSetAttrsEntry, NODE_CACHE_PIXEL_RATIO_MAX, type NudgeDelta, type ReorderElementInput, type ReorderPageInput, type ResolvedExportParams, SceneCommand, type SetAttrsInput, type SetDocumentTitleInput, type SetPageGuidesInput, type SetPagePropsInput, SnapEngine, SnapTarget, type UngroupElementInput, type ZoomPanConfig, ZoomPanMath, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, resolveNodeCachePixelRatio, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand };
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_INSERT_TEMPLATES,
4
4
  DUPLICATE_OFFSET,
5
5
  MAX_INSERT_DIMENSION,
6
+ NODE_CACHE_PIXEL_RATIO_MAX,
6
7
  buildInsertImageOp,
7
8
  centeredPosition,
8
9
  collectGridTargets,
@@ -17,8 +18,9 @@ import {
17
18
  marqueeSelect,
18
19
  mintElementId,
19
20
  nudgeDelta,
20
- resolveExportParams
21
- } from "../chunk-4QVG4WJQ.js";
21
+ resolveExportParams,
22
+ resolveNodeCachePixelRatio
23
+ } from "../chunk-OYTMILRK.js";
22
24
  import {
23
25
  SCENE_COMMAND_HISTORY_LIMIT,
24
26
  addElementCommand,
@@ -47,6 +49,7 @@ export {
47
49
  DEFAULT_INSERT_TEMPLATES,
48
50
  DUPLICATE_OFFSET,
49
51
  MAX_INSERT_DIMENSION,
52
+ NODE_CACHE_PIXEL_RATIO_MAX,
50
53
  SCENE_COMMAND_HISTORY_LIMIT,
51
54
  addElementCommand,
52
55
  addPageCommand,
@@ -75,6 +78,7 @@ export {
75
78
  reorderElementCommand,
76
79
  reorderPageCommand,
77
80
  resolveExportParams,
81
+ resolveNodeCachePixelRatio,
78
82
  scaleForPreset,
79
83
  setAttrsCommand,
80
84
  setDocumentTitleCommand,
@@ -1,7 +1,7 @@
1
1
  import { D as DesignCanvasProps, c as createSceneCommandStack, E as ExportTriggerOptions, a as DesignCanvasMode } from '../command-stack-TNPWJCzW.js';
2
2
  export { A as ApplySceneResult, b as EditorSceneState, S as SCENE_COMMAND_HISTORY_LIMIT, d as SceneCommand, e as SceneCommandStack, f as SceneCommandStackWithReapply, g as SnapEngine, h as SnapResult, i as SnapTarget, j as SnapTargetKind, k as SnapTargets, Z as ZoomPanMath } from '../command-stack-TNPWJCzW.js';
3
3
  import { InsertPageGeometry, InsertTemplate, MultiSetAttrsEntry } from './engine.js';
4
- export { AddElementInput, AddPageInput, BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, DeleteElementInput, DeletePageInput, DuplicatePageInput, ExportViewSnapshot, GroupElementsInput, MAX_INSERT_DIMENSION, MarqueeSelectOptions, NudgeDelta, ReorderElementInput, ReorderPageInput, ResolvedExportParams, SetAttrsInput, SetDocumentTitleInput, SetPageGuidesInput, SetPagePropsInput, UngroupElementInput, ZoomPanConfig, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand } from './engine.js';
4
+ export { AddElementInput, AddPageInput, BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, DeleteElementInput, DeletePageInput, DuplicatePageInput, ExportViewSnapshot, GroupElementsInput, MAX_INSERT_DIMENSION, MarqueeSelectOptions, NODE_CACHE_PIXEL_RATIO_MAX, NudgeDelta, ReorderElementInput, ReorderPageInput, ResolvedExportParams, SetAttrsInput, SetDocumentTitleInput, SetPageGuidesInput, SetPagePropsInput, UngroupElementInput, ZoomPanConfig, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, resolveNodeCachePixelRatio, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand } from './engine.js';
5
5
  import { c as SceneDocument, S as ScenePage, a as SceneElement, P as PageBleed, b as PageGuides } from '../model-DnfO_PdJ.js';
6
6
  import { f as ExportPreset } from '../export-presets-mgVulRaV.js';
7
7
  export { d as ExportCropRect, h as bleedAwareExportBounds, s as scaleForPreset } from '../export-presets-mgVulRaV.js';
@@ -31,6 +31,18 @@ interface KonvaNodeLike {
31
31
  visible(): boolean;
32
32
  visible(v: boolean): void;
33
33
  getAttr(key: string): unknown;
34
+ isCached(): boolean;
35
+ clearCache(): void;
36
+ cache(config?: {
37
+ pixelRatio?: number;
38
+ }): void;
39
+ getAbsoluteScale(): {
40
+ x: number;
41
+ y: number;
42
+ };
43
+ /** Konva.Container only — leaf shapes lack it; the cache walk treats its
44
+ * absence as "no children". */
45
+ getChildren?(): KonvaNodeLike[];
34
46
  }
35
47
  interface KonvaLayerLike {
36
48
  getChildren(): KonvaNodeLike[];
@@ -12,7 +12,7 @@ import {
12
12
  downloadDataUrl,
13
13
  exportDocumentJson,
14
14
  exportPageDataUrl
15
- } from "../chunk-OYGM5WLD.js";
15
+ } from "../chunk-PURHUAOR.js";
16
16
  import {
17
17
  BTN,
18
18
  BTN_ACTIVE,
@@ -41,13 +41,14 @@ import {
41
41
  buildRulerTicks,
42
42
  formatRulerLabel,
43
43
  selectTickStep
44
- } from "../chunk-NC2V63EL.js";
44
+ } from "../chunk-FNM7JYME.js";
45
45
  import "../chunk-LSQR6HM6.js";
46
46
  import "../chunk-ZN5J47UX.js";
47
47
  import {
48
48
  DEFAULT_INSERT_TEMPLATES,
49
49
  DUPLICATE_OFFSET,
50
50
  MAX_INSERT_DIMENSION,
51
+ NODE_CACHE_PIXEL_RATIO_MAX,
51
52
  buildInsertImageOp,
52
53
  centeredPosition,
53
54
  collectGridTargets,
@@ -62,8 +63,9 @@ import {
62
63
  marqueeSelect,
63
64
  mintElementId,
64
65
  nudgeDelta,
65
- resolveExportParams
66
- } from "../chunk-4QVG4WJQ.js";
66
+ resolveExportParams,
67
+ resolveNodeCachePixelRatio
68
+ } from "../chunk-OYTMILRK.js";
67
69
  import {
68
70
  SCENE_COMMAND_HISTORY_LIMIT,
69
71
  addElementCommand,
@@ -91,7 +93,7 @@ import {
91
93
  import {
92
94
  DesignCanvasChromeLazy,
93
95
  DesignCanvasLazy
94
- } from "../chunk-AWPK7ZDS.js";
96
+ } from "../chunk-QGOU3VXH.js";
95
97
  import "../chunk-F7VH4G5C.js";
96
98
 
97
99
  // src/design-canvas-react/components/CanvasInsertPanel.tsx
@@ -283,7 +285,7 @@ function CanvasInsertPanel({
283
285
  setDragOver(false);
284
286
  if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files);
285
287
  },
286
- className: `flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${dragOver ? "border-[var(--brand-primary)] bg-[var(--brand-primary)]/5" : "border-[var(--border-default)] hover:border-[var(--brand-primary)]/40"}`,
288
+ className: `flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${dragOver ? "border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_5%,transparent)]" : "border-[var(--border-default)] hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]"}`,
287
289
  children: [
288
290
  busy ? /* @__PURE__ */ jsx(SpinnerGlyph, { className: "h-6 w-6 text-[var(--brand-primary)]" }) : /* @__PURE__ */ jsx(UploadGlyph, { className: "h-6 w-6 text-[var(--text-muted)]" }),
289
291
  /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-primary)]", children: busy ? "Uploading\u2026" : "Drop an image or click to upload" }),
@@ -312,7 +314,7 @@ function CanvasInsertPanel({
312
314
  type: "button",
313
315
  disabled: busy,
314
316
  onClick: () => void runInsert(() => tpl.build(page)),
315
- className: "flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[var(--brand-primary)]/40 disabled:opacity-50",
317
+ className: "flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)] disabled:opacity-50",
316
318
  children: [
317
319
  /* @__PURE__ */ jsx("span", { className: "flex h-7 items-center justify-center", children: /* @__PURE__ */ jsx(TemplatePreview, { shape: templateShape(tpl) }) }),
318
320
  /* @__PURE__ */ jsx("span", { children: tpl.label })
@@ -409,6 +411,7 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
409
411
  return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col overflow-hidden text-[var(--text-primary)]", children: [
410
412
  /* @__PURE__ */ jsx2("div", { className: "shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]", children: "Layers" }),
411
413
  /* @__PURE__ */ jsxs2("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
414
+ visible.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "px-3 py-2 text-[11px] text-[var(--text-muted)]", children: "No layers yet" }) : null,
412
415
  visible.map((row) => {
413
416
  const { element } = row;
414
417
  const isSelected = selectedElementIds.includes(element.id);
@@ -446,7 +449,7 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
446
449
  },
447
450
  className: [
448
451
  "group flex items-center gap-1.5 py-1 pr-2 text-[13px] transition-colors",
449
- isSelected ? "bg-[var(--brand-primary)]/15 text-[var(--text-primary)]" : "hover:bg-[var(--border-default)]/40 text-[var(--text-secondary)]",
452
+ isSelected ? "bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)] text-[var(--text-primary)]" : "hover:bg-[color-mix(in_srgb,var(--border-default)_40%,transparent)] text-[var(--text-secondary)]",
450
453
  dragOverIndex === row.ownerIndex ? "border-t border-[var(--brand-primary)]" : ""
451
454
  ].join(" "),
452
455
  style: { paddingLeft: 8 + row.depth * INDENT_PX },
@@ -556,6 +559,7 @@ export {
556
559
  LAYERS_PANEL_ROW_LIMIT,
557
560
  LayersPanel,
558
561
  MAX_INSERT_DIMENSION,
562
+ NODE_CACHE_PIXEL_RATIO_MAX,
559
563
  PagesStrip,
560
564
  Rulers,
561
565
  SCENE_COMMAND_HISTORY_LIMIT,
@@ -600,6 +604,7 @@ export {
600
604
  reorderElementCommand,
601
605
  reorderPageCommand,
602
606
  resolveExportParams,
607
+ resolveNodeCachePixelRatio,
603
608
  scaleForPreset,
604
609
  selectTickStep,
605
610
  setAttrsCommand,