@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.
- package/dist/{DesignCanvas-3RRPVPCP.js → DesignCanvas-RSWQCXM3.js} +2 -2
- package/dist/{DesignCanvasEditor-KAMMEX7E.js → DesignCanvasEditor-XZMGIST7.js} +4 -4
- package/dist/assistant/index.d.ts +63 -39
- package/dist/assistant/index.js +23 -5
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-react/index.d.ts +25 -2
- package/dist/chat-react/index.js.map +1 -1
- package/dist/chat-routes/index.js +1 -1
- package/dist/{chunk-ODYE4A7L.js → chunk-3I27SDU3.js} +142 -58
- package/dist/chunk-3I27SDU3.js.map +1 -0
- package/dist/chunk-C3CAYGGQ.js +18 -0
- package/dist/chunk-C3CAYGGQ.js.map +1 -0
- package/dist/chunk-EMLGWEHV.js +33 -0
- package/dist/chunk-EMLGWEHV.js.map +1 -0
- package/dist/{chunk-XEA5EG6P.js → chunk-EPZYYWCT.js} +8 -30
- package/dist/chunk-EPZYYWCT.js.map +1 -0
- package/dist/{chunk-NC2V63EL.js → chunk-FNM7JYME.js} +88 -21
- package/dist/chunk-FNM7JYME.js.map +1 -0
- package/dist/{chunk-WD2B6HGY.js → chunk-K4RA5T3A.js} +11 -32
- package/dist/chunk-K4RA5T3A.js.map +1 -0
- package/dist/{chunk-4QVG4WJQ.js → chunk-OYTMILRK.js} +10 -3
- package/dist/chunk-OYTMILRK.js.map +1 -0
- package/dist/{chunk-OYGM5WLD.js → chunk-PURHUAOR.js} +52 -15
- package/dist/chunk-PURHUAOR.js.map +1 -0
- package/dist/{chunk-AWPK7ZDS.js → chunk-QGOU3VXH.js} +3 -3
- package/dist/{chunk-RT5RKAFX.js → chunk-QGSVF6DZ.js} +8 -18
- package/dist/chunk-QGSVF6DZ.js.map +1 -0
- package/dist/{chunk-OTLEYHOG.js → chunk-RRH23CGS.js} +24 -4
- package/dist/chunk-RRH23CGS.js.map +1 -0
- package/dist/{chunk-N3G3FSM4.js → chunk-YXA3U4JZ.js} +9 -47
- package/dist/chunk-YXA3U4JZ.js.map +1 -0
- package/dist/design-canvas-react/engine.d.ts +15 -1
- package/dist/design-canvas-react/engine.js +6 -2
- package/dist/design-canvas-react/index.d.ts +13 -1
- package/dist/design-canvas-react/index.js +13 -8
- package/dist/design-canvas-react/index.js.map +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/legibility/cli.js +14 -8
- package/dist/legibility/cli.js.map +1 -1
- package/dist/legibility/index.js +2 -1
- package/dist/peer-floors/cli.js +14 -8
- package/dist/peer-floors/cli.js.map +1 -1
- package/dist/preflight/cli.js +4 -2
- package/dist/preflight/cli.js.map +1 -1
- package/dist/{run-tEsZUhAf.d.ts → run-D80hoLKh.d.ts} +1 -1
- package/dist/sandbox/index.d.ts +11 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/signoff/cli.d.ts +2 -2
- package/dist/signoff/cli.js +6 -18
- package/dist/signoff/cli.js.map +1 -1
- package/dist/signoff/index.d.ts +4 -420
- package/dist/signoff/index.js +3 -55
- package/dist/signoff/proof-cli.js +11 -6
- package/dist/signoff/proof-cli.js.map +1 -1
- package/dist/signoff/proof.d.ts +70 -271
- package/dist/signoff/proof.js +3 -73
- package/dist/signoff/proof.js.map +1 -1
- package/dist/theme-contract/cli.js +6 -2
- package/dist/theme-contract/cli.js.map +1 -1
- package/dist/theme-contract/index.js +2 -1
- package/dist/{types-U7Nz-txa.d.ts → types-Bd6uW9vQ.d.ts} +1 -1
- package/dist/web-react/index.d.ts +51 -44
- package/dist/web-react/index.js +1 -1
- package/package.json +14 -3
- package/dist/chunk-4QVG4WJQ.js.map +0 -1
- package/dist/chunk-N3G3FSM4.js.map +0 -1
- package/dist/chunk-NC2V63EL.js.map +0 -1
- package/dist/chunk-ODYE4A7L.js.map +0 -1
- package/dist/chunk-OTLEYHOG.js.map +0 -1
- package/dist/chunk-OYGM5WLD.js.map +0 -1
- package/dist/chunk-RT5RKAFX.js.map +0 -1
- package/dist/chunk-WD2B6HGY.js.map +0 -1
- package/dist/chunk-XEA5EG6P.js.map +0 -1
- /package/dist/{DesignCanvas-3RRPVPCP.js.map → DesignCanvas-RSWQCXM3.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-KAMMEX7E.js.map → DesignCanvasEditor-XZMGIST7.js.map} +0 -0
- /package/dist/{chunk-AWPK7ZDS.js.map → chunk-QGOU3VXH.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/signoff/config.ts","../src/signoff/run.ts","../src/signoff/node-version.ts","../src/signoff/workflow-pin.ts","../src/signoff/exec.ts","../src/signoff/schedule.ts","../src/signoff/seeds.ts","../src/signoff/store.ts","../src/signoff/workspace.ts","../src/signoff/report.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { z } from 'zod'\nimport type { LoadedSignoffConfig, SignoffConfig, SignoffStepSpec } from './types'\n\n/**\n * Where the step list comes from.\n *\n * It is declared by the repo, never hardcoded here, because the three workflows\n * this has to reproduce do not share a shape: tax-agent filters its install to\n * one workspace package and runs a Python toolkit suite; legal-agent runs a\n * react-router typegen and a Worker startup check; agent-app runs a named\n * incident-class gate, a generated-project test and knip. A checker that\n * hardcodes any one of those is wrong for the other two.\n *\n * Three sources, in order:\n * 1. `signoff.config.mjs` (or `.js`) — the full surface, including per-step\n * `env`, `needs` and shuffle specs.\n * 2. a `signoff` key in `package.json` — the same shape, JSON-only.\n * 3. derived from the repo's own `scripts`, so a repo gets a real gate before\n * anyone writes a config. The derivation is documented below and its origin\n * is stamped into the proof, because a derived run is a weaker claim than a\n * declared one.\n */\n\nconst SIGNOFF_CONFIG_FILES: readonly string[] = ['signoff.config.mjs', 'signoff.config.js']\n\nconst shuffleSchema = z.object({\n runs: z.number().int().positive().optional(),\n seeds: z.array(z.number().int()).optional(),\n args: z.array(z.string()).optional(),\n})\n\nconst stepSchema = z.object({\n name: z.string().min(1),\n run: z.string().min(1),\n cwd: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n needs: z.array(z.string()).optional(),\n timeoutMs: z.number().int().positive().optional(),\n shuffle: z.union([z.boolean(), shuffleSchema]).optional(),\n})\n\nconst configSchema = z.object({\n install: z\n .object({\n run: z.string().min(1).optional(),\n storeDirFlag: z.string().nullable().optional(),\n storeEnv: z.string().nullable().optional(),\n cwd: z.string().optional(),\n timeoutMs: z.number().int().positive().optional(),\n env: z.record(z.string(), z.string()).optional(),\n })\n .optional(),\n steps: z.array(stepSchema).min(1),\n maxParallel: z.number().int().positive().optional(),\n env: z.record(z.string(), z.string()).optional(),\n nodeVersion: z.string().min(1).optional(),\n carryFiles: z.array(z.string()).optional(),\n cacheDir: z.string().optional(),\n storeGenerations: z.number().int().positive().optional(),\n})\n\nfunction describeIssues(error: z.ZodError, where: string): string {\n const lines = error.issues.map((issue) => ` ${issue.path.join('.') || '(root)'}: ${issue.message}`)\n return `signoff: ${where} is not a valid config:\\n${lines.join('\\n')}`\n}\n\nexport function parseSignoffConfig(value: unknown, where: string): SignoffConfig {\n const result = configSchema.safeParse(value)\n if (!result.success) throw new Error(describeIssues(result.error, where))\n return result.data as SignoffConfig\n}\n\n/**\n * The script → step derivation, and the dependency edges it asserts.\n *\n * The edges are the speed lever, so each one is a claim about the repo:\n * typecheck, the suite, the build and knip are ASSUMED to read source and not\n * each other's output, so they run concurrently. Only `test:generated`\n * genuinely consumes a build artifact, so only it declares a dependency.\n *\n * **That assumption is a guess, and it is wrong for at least one real repo.**\n * agent-app's own suite copies `dist/` into a generated project while `tsup`\n * (`clean: true`) is deleting it, which fails loudly as \"dist/ not built\" and\n * quietly as a half-copied package. A repo whose suite reads build output MUST\n * declare `needs: ['build']` on it in a `signoff.config.mjs`; the derivation\n * cannot see that from a script name, which is one more reason a derived run is\n * a weaker claim than a declared one.\n *\n * `build:check` supersedes `build` when both exist (legal-agent's `build:check`\n * is `pnpm build && …`, so running both builds twice for one verdict).\n */\nconst DERIVED_STEPS: readonly {\n readonly script: string\n readonly name: string\n readonly shuffle?: boolean\n readonly supersedes?: string\n readonly needsBuild?: boolean\n}[] = [\n { script: 'peer-check', name: 'peer floors' },\n { script: 'typecheck', name: 'typecheck' },\n { script: 'test:gates', name: 'incident-class gates' },\n { script: 'test', name: 'unit tests', shuffle: true },\n { script: 'build', name: 'build' },\n { script: 'build:check', name: 'build + worker checks', supersedes: 'build' },\n { script: 'test:generated', name: 'generated projects', needsBuild: true },\n { script: 'knip', name: 'dead-surface (knip)' },\n]\n\nexport function deriveSignoffConfig(scripts: Readonly<Record<string, string>>): {\n readonly config: SignoffConfig\n readonly used: readonly string[]\n} {\n const present = DERIVED_STEPS.filter((candidate) => scripts[candidate.script] !== undefined)\n const superseded = new Set(present.map((candidate) => candidate.supersedes).filter((name): name is string => !!name))\n const kept = present.filter((candidate) => !superseded.has(candidate.script))\n const buildStep = kept.find((candidate) => candidate.script === 'build' || candidate.script === 'build:check')\n\n const steps: SignoffStepSpec[] = kept.map((candidate) => ({\n name: candidate.name,\n run: `pnpm run ${candidate.script}`,\n ...(candidate.shuffle ? { shuffle: true as const } : {}),\n ...(candidate.needsBuild && buildStep ? { needs: [buildStep.name] } : {}),\n }))\n\n if (steps.length === 0) {\n throw new Error(\n 'signoff: no config and no recognizable scripts. Add a `signoff.config.mjs` naming the steps ' +\n `this repo's CI runs, or a package.json \"signoff\" key. Recognized script names: ` +\n `${DERIVED_STEPS.map((candidate) => candidate.script).join(', ')}.`,\n )\n }\n return { config: { steps }, used: kept.map((candidate) => candidate.script) }\n}\n\nexport interface LoadSignoffConfigOptions {\n readonly repoRoot: string\n /** Explicit config path. Missing file is an error, never a silent fallback. */\n readonly configPath?: string\n}\n\nexport async function loadSignoffConfig(options: LoadSignoffConfigOptions): Promise<LoadedSignoffConfig> {\n const { repoRoot, configPath } = options\n\n if (configPath !== undefined) {\n const abs = resolve(repoRoot, configPath)\n if (!existsSync(abs)) throw new Error(`signoff: no config at ${abs}`)\n return { config: await importConfig(abs), origin: { kind: 'file', path: abs } }\n }\n\n for (const candidate of SIGNOFF_CONFIG_FILES) {\n const abs = join(repoRoot, candidate)\n if (existsSync(abs)) return { config: await importConfig(abs), origin: { kind: 'file', path: abs } }\n }\n\n const pkgPath = join(repoRoot, 'package.json')\n if (!existsSync(pkgPath)) {\n throw new Error(`signoff: ${repoRoot} has no package.json, no signoff.config.mjs, and nothing to derive from.`)\n }\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {\n signoff?: unknown\n scripts?: Record<string, string>\n }\n if (pkg.signoff !== undefined) {\n return { config: parseSignoffConfig(pkg.signoff, `${pkgPath} \"signoff\"`), origin: { kind: 'package-json', path: pkgPath } }\n }\n\n const derived = deriveSignoffConfig(pkg.scripts ?? {})\n return { config: derived.config, origin: { kind: 'derived', path: pkgPath, scripts: derived.used } }\n}\n\nasync function importConfig(abs: string): Promise<SignoffConfig> {\n const mod: unknown = await import(pathToFileURL(abs).href)\n const value = (mod as { default?: unknown }).default\n if (value === undefined) throw new Error(`signoff: ${abs} must have a default export`)\n return parseSignoffConfig(value, abs)\n}\n","import { spawnSync } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { availableParallelism, homedir } from 'node:os'\nimport { basename, join, resolve } from 'node:path'\nimport { loadSignoffConfig } from './config'\nimport { assertNodeVersion, resolveNodeRequirement, type NodeVersionRequirement } from './node-version'\nimport { runCommand, type CommandResult } from './exec'\nimport { runGraph, validateGraph, type TaskOutcome } from './schedule'\nimport { assertShuffleArgsReachTheRunner, newSeedBase, planAttempts } from './seeds'\nimport { resolveStore } from './store'\nimport { materializeCleanTree, removeCleanTree, repoRootOf, type CleanTree } from './workspace'\nimport type {\n SignoffAttempt,\n SignoffEvent,\n SignoffHostFacts,\n SignoffInstallResult,\n SignoffReport,\n SignoffSource,\n SignoffStepResult,\n SignoffStepSpec,\n} from './types'\n\n/**\n * The sign-off run: reproduce a clean CI environment, then go further.\n *\n * Order matters and each stage exists for a failure this fleet actually paid\n * for:\n * 1. a pristine checkout (`workspace.ts`) — no warm `node_modules`, no Vite\n * cache, because that is the entire mechanical difference between \"green\n * locally\" and \"red in CI\";\n * 2. `--frozen-lockfile` into a store keyed on the lockfile (`store.ts`) — the\n * clean install CI does, without paying the download twice;\n * 3. the repo's declared steps (`config.ts`), run as a graph rather than a line\n * (`schedule.ts`), with the suite re-run under randomized file order and\n * recorded seeds (`seeds.ts`) — the part CI does not do at all.\n *\n * Nothing about a failure is inferred. Every step reports its command, exit\n * code, duration, seed and captured output.\n */\n\nexport interface RunSignoffOptions {\n /** Any directory inside the repo. Defaults to the process cwd. */\n readonly repoDir?: string\n readonly configPath?: string\n /** Default `working-tree` — verify what you are about to commit. */\n readonly source?: SignoffSource\n /** Run every step even after one fails. Default false. */\n readonly keepGoing?: boolean\n /** Base seed. Pass a previous run's to reproduce it exactly. */\n readonly seed?: number\n readonly maxParallel?: number\n readonly cacheDir?: string\n /** Override every shuffled step's run count. */\n readonly shuffleRuns?: number\n /** Keep the clean tree for inspection instead of removing it. */\n readonly keepWorkspace?: boolean\n readonly onEvent?: (event: SignoffEvent) => void\n}\n\nconst DEFAULT_CACHE_DIR = join(homedir(), '.cache', 'agent-app-signoff')\n\nfunction hostFacts(treePath: string, requirement: NodeVersionRequirement | null): SignoffHostFacts {\n const pm = spawnSync('pnpm', ['--version'], { cwd: treePath, encoding: 'utf8' })\n return {\n node: process.version,\n nodePinned: requirement?.declared ?? null,\n nodePinSource: requirement?.source ?? null,\n packageManager: pm.status === 0 ? `pnpm ${pm.stdout.trim()}` : 'pnpm (not resolvable)',\n platform: process.platform,\n arch: process.arch,\n cpus: availableParallelism(),\n }\n}\n\n/** Successful steps keep only a tail; a passing 3,000-test run is not evidence. */\nconst SUCCESS_OUTPUT_TAIL = 4_000\n\nfunction toAttempt(result: CommandResult, seed: number | null, ok: boolean): SignoffAttempt {\n return {\n command: result.command,\n seed,\n exitCode: result.exitCode,\n signal: result.signal,\n durationMs: result.durationMs,\n timedOut: result.timedOut,\n output: ok ? result.output.slice(-SUCCESS_OUTPUT_TAIL) : result.output,\n outputTruncated: result.truncated || (ok && result.output.length > SUCCESS_OUTPUT_TAIL),\n }\n}\n\nfunction buildEnv(\n base: Readonly<Record<string, string | undefined>>,\n layers: readonly (Readonly<Record<string, string>> | undefined)[],\n): Record<string, string | undefined> {\n const env: Record<string, string | undefined> = { ...base }\n for (const layer of layers) {\n if (layer) Object.assign(env, layer)\n }\n return env\n}\n\n/** Where the store flag goes: after the subcommand, so `pnpm install X --store-dir Y` stays valid. */\nfunction withStoreDir(command: string, flag: string | null | undefined, storeDir: string): string {\n if (flag === null) return command\n return `${command} ${flag ?? '--store-dir'} ${JSON.stringify(storeDir)}`\n}\n\nexport async function runSignoff(options: RunSignoffOptions = {}): Promise<SignoffReport> {\n const startedAt = new Date()\n const wallStart = Date.now()\n const repoDir = resolve(options.repoDir ?? process.cwd())\n const repoRoot = repoRootOf(repoDir)\n const { config, origin } = await loadSignoffConfig({ repoRoot, configPath: options.configPath })\n\n // Fail before materializing anything: a bad graph is a config error, and\n // paying for an install to learn that is a waste of the speed this exists for.\n validateGraph(config.steps.map((step) => ({ name: step.name, needs: step.needs })))\n assertShuffleArgsReachTheRunner(config.steps)\n const nodeRequirement = resolveNodeRequirement(repoRoot, config.nodeVersion)\n assertNodeVersion(nodeRequirement)\n\n const source = options.source ?? 'working-tree'\n const cacheDir = resolve(options.cacheDir ?? config.cacheDir ?? DEFAULT_CACHE_DIR)\n // The tree lives under the same root as the stores on purpose: pnpm hardlinks\n // from the store into `node_modules`, and a store on another filesystem\n // silently degrades to a full copy.\n const treePath = join(cacheDir, 'trees', `${basename(repoRoot)}-${process.pid}`)\n\n let tree: CleanTree | null = null\n try {\n tree = materializeCleanTree({ repoDir: repoRoot, dest: treePath, source, carryFiles: config.carryFiles })\n options.onEvent?.({ kind: 'tree', path: tree.path, head: tree.head, dirty: tree.dirty })\n\n const store = resolveStore({ treePath: tree.path, cacheDir, generations: config.storeGenerations })\n options.onEvent?.({ kind: 'store', storeDir: store.storeDir, cacheHit: store.hit, cacheKey: store.cacheKey })\n\n const installSpec = config.install ?? {}\n const installCwd = join(tree.path, installSpec.cwd ?? '.')\n const installCommand = withStoreDir(\n installSpec.run ?? 'pnpm install --frozen-lockfile',\n installSpec.storeDirFlag,\n store.storeDir,\n )\n const storeEnvName = installSpec.storeEnv === null ? null : installSpec.storeEnv ?? 'NPM_CONFIG_STORE_DIR'\n const sharedEnv = buildEnv(process.env, [\n // Parity with CI: a runner that behaves differently under `CI` (vitest's\n // reporter, wrangler's prompts) must behave that way here too.\n { CI: 'true' },\n config.env,\n storeEnvName === null ? undefined : { [storeEnvName]: store.storeDir },\n ])\n\n options.onEvent?.({ kind: 'install-start', command: installCommand })\n const installResult = await runCommand({\n command: installCommand,\n cwd: installCwd,\n env: buildEnv(sharedEnv, [installSpec.env]),\n timeoutMs: installSpec.timeoutMs,\n })\n options.onEvent?.({ kind: 'install-end', exitCode: installResult.exitCode, durationMs: installResult.durationMs })\n\n const install: SignoffInstallResult = {\n command: installCommand,\n storeDir: store.storeDir,\n cacheKey: store.cacheKey,\n cacheHit: store.hit,\n keyedOn: store.keyedOn,\n exitCode: installResult.exitCode,\n durationMs: installResult.durationMs,\n output: installResult.exitCode === 0 ? installResult.output.slice(-SUCCESS_OUTPUT_TAIL) : installResult.output,\n outputTruncated: installResult.truncated,\n }\n\n const host = hostFacts(tree.path, nodeRequirement)\n const seedBase = options.seed ?? newSeedBase()\n\n if (installResult.exitCode !== 0) {\n // Every step is unrunnable, and saying so is the honest report. A gate\n // that reports \"0 failures\" because it never ran anything is the failure\n // mode this whole module exists to prevent.\n return finish({\n ok: false,\n startedAt,\n wallStart,\n tree,\n origin,\n host,\n install,\n steps: config.steps.map(\n (step): SignoffStepResult => ({\n name: step.name,\n status: 'skipped',\n attempts: [],\n durationMs: 0,\n startedAtMs: null,\n finishedAtMs: null,\n }),\n ),\n seedBase,\n keepGoing: options.keepGoing ?? false,\n workspaceRetained: options.keepWorkspace ?? false,\n source,\n options,\n })\n }\n\n const treeRoot = tree.path\n const outcomes = await runGraph<SignoffStepSpec, readonly SignoffAttempt[]>({\n nodes: config.steps,\n maxParallel: options.maxParallel ?? config.maxParallel ?? availableParallelism(),\n keepGoing: options.keepGoing ?? false,\n run: async (step, signal) => {\n const attempts: SignoffAttempt[] = []\n for (const plan of planAttempts(step, seedBase, options.shuffleRuns)) {\n options.onEvent?.({ kind: 'step-start', name: step.name, command: plan.command, seed: plan.seed })\n const result = await runCommand({\n command: plan.command,\n cwd: join(treeRoot, step.cwd ?? '.'),\n env: buildEnv(sharedEnv, [step.env]),\n timeoutMs: step.timeoutMs,\n signal,\n })\n const ok = result.exitCode === 0\n attempts.push(toAttempt(result, plan.seed, ok))\n // Stop at the first failing order: the remaining seeds would report\n // the same defect, and the seed that found it is already recorded.\n if (!ok) {\n emitStepEnd(options, step.name, 'failed', attempts)\n return { ok: false, value: attempts }\n }\n }\n emitStepEnd(options, step.name, 'passed', attempts)\n return { ok: true, value: attempts }\n },\n })\n\n const steps = outcomes.map(toStepResult)\n return finish({\n ok: steps.every((step) => step.status === 'passed'),\n startedAt,\n wallStart,\n tree,\n origin,\n host,\n install,\n steps,\n seedBase,\n keepGoing: options.keepGoing ?? false,\n workspaceRetained: options.keepWorkspace ?? false,\n source,\n options,\n })\n } finally {\n if (tree && !options.keepWorkspace && existsSync(tree.path)) removeCleanTree(tree)\n }\n}\n\n/** Emitted from inside the step, so a watching CLI sees a completion when it\n * happens rather than every completion at the end of the run. */\nfunction emitStepEnd(\n options: RunSignoffOptions,\n name: string,\n status: SignoffStepResult['status'],\n attempts: readonly SignoffAttempt[],\n): void {\n options.onEvent?.({\n kind: 'step-end',\n name,\n status,\n durationMs: attempts.reduce((total, attempt) => total + attempt.durationMs, 0),\n })\n}\n\nfunction toStepResult(outcome: TaskOutcome<readonly SignoffAttempt[]>): SignoffStepResult {\n const attempts = outcome.value ?? []\n const durationMs = attempts.reduce((total, attempt) => total + attempt.durationMs, 0)\n return {\n name: outcome.name,\n status: outcome.status,\n attempts,\n durationMs,\n startedAtMs: outcome.startedAtMs,\n finishedAtMs: outcome.finishedAtMs,\n }\n}\n\ninterface FinishInput {\n readonly ok: boolean\n readonly startedAt: Date\n readonly wallStart: number\n readonly tree: CleanTree\n readonly origin: SignoffReport['configOrigin']\n readonly host: SignoffHostFacts\n readonly install: SignoffInstallResult\n readonly steps: readonly SignoffStepResult[]\n readonly seedBase: number\n readonly keepGoing: boolean\n readonly workspaceRetained: boolean\n readonly source: SignoffSource\n readonly options: RunSignoffOptions\n}\n\nfunction finish(input: FinishInput): SignoffReport {\n const serialMs = input.install.durationMs + input.steps.reduce((total, step) => total + step.durationMs, 0)\n const flags = [\n `--source ${input.source}`,\n `--seed ${input.seedBase}`,\n ...(input.keepGoing ? ['--keep-going'] : []),\n ]\n return {\n ok: input.ok,\n startedAt: input.startedAt.toISOString(),\n repo: {\n root: input.tree.root,\n head: input.tree.head,\n branch: input.tree.branch,\n source: input.source,\n dirty: input.tree.dirty,\n diffSha256: input.tree.diffSha256,\n untrackedFiles: input.tree.untrackedFiles,\n carriedFiles: input.tree.carriedFiles,\n },\n configOrigin: input.origin,\n workspace: input.tree.path,\n workspaceRetained: input.workspaceRetained,\n host: input.host,\n install: input.install,\n steps: input.steps,\n seedBase: input.seedBase,\n wallClockMs: Date.now() - input.wallStart,\n serialMs,\n keepGoing: input.keepGoing,\n reproduce: `agent-app-signoff ${input.tree.root} ${flags.join(' ')}`,\n }\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { resolveWorkflowNodePin } from './workflow-pin'\n\n/**\n * Refuse to sign off on a runtime the product does not ship.\n *\n * CI pins its Node version (`actions/setup-node` with `node-version: 22` in all\n * three fleet workflows). A developer's shell does not — this host runs 24. A\n * local gate that inherits whatever is on `PATH` therefore verifies a runtime\n * nobody deploys, and reports it as the runtime that ships. That is the same\n * failure shape as a number measured in a narrower context than it is\n * presented in, and it is not acceptable in the thing that replaces the merge\n * gate.\n *\n * So the pin is read from the repo and enforced, or it is absent and the report\n * says so. Three sources, in order:\n *\n * - `signoff.config.mjs`'s `nodeVersion` — the explicit declaration.\n * - `.nvmrc` — the pin a repo already keeps for humans.\n * - the `pull_request` workflows' `node-version` (`./workflow-pin`) — the pin\n * CI itself runs on.\n *\n * **The third source was added because the first two were empty on the entire\n * fleet, and that produced a false PASS.** legal-agent `4c0d688` failed CI on\n * `Cannot bundle Node.js built-in \"node:sqlite\"` and this gate signed it off.\n * Reproduced in one installed tree, same bytes: Node 22 fails both files, Node\n * 24 passes both, every time. No `.nvmrc` exists in tax-agent, legal-agent or\n * agent-app; all three pin `node-version: 22` in the workflow being replaced.\n * A gate that replaces a workflow has to read the runtime that workflow pins.\n *\n * A `.nvmrc` and a workflow that disagree is not resolved by preference — it is\n * a refusal, because it means the local gate and CI verify different runtimes,\n * which is the exact defect this module exists to prevent.\n *\n * **`engines.node` is deliberately NOT read.** It is a floor (`\">=18\"`), not a\n * pin, so treating it as one manufactures refusals on every version above the\n * floor — and an unsatisfiable gate does not stop bad work, it gets waived.\n */\n\nexport interface NodeVersionRequirement {\n /** The declared major, e.g. `22`. */\n readonly major: number\n readonly declared: string\n /** Where it came from, for the proof. */\n readonly source: string\n}\n\n/** Leading major from a pin like `22`, `v22.22.3`, `22.22`, `lts/iron` (none). */\nfunction majorOf(raw: string): number | null {\n const match = /^v?(\\d+)(?:\\.|$)/.exec(raw.trim())\n return match?.[1] === undefined ? null : Number.parseInt(match[1], 10)\n}\n\nexport function resolveNodeRequirement(\n repoRoot: string,\n configured?: string,\n): NodeVersionRequirement | null {\n if (configured !== undefined) {\n const major = majorOf(configured)\n if (major === null) {\n throw new Error(\n `signoff: nodeVersion \"${configured}\" does not start with a major version. ` +\n 'Declare a pin like \"22\" or \"22.22.3\".',\n )\n }\n return { major, declared: configured.trim(), source: 'signoff config `nodeVersion`' }\n }\n\n let fromNvmrc: NodeVersionRequirement | null = null\n const nvmrc = join(repoRoot, '.nvmrc')\n if (existsSync(nvmrc)) {\n const raw = readFileSync(nvmrc, 'utf8').trim()\n const major = majorOf(raw)\n // An alias (`lts/iron`) is a pin this module cannot resolve without a\n // network call, so it is reported as no requirement rather than guessed.\n if (major !== null) fromNvmrc = { major, declared: raw, source: '.nvmrc' }\n }\n\n const fromWorkflow = resolveWorkflowNodePin(repoRoot, majorOf)\n if (fromNvmrc && fromWorkflow && fromNvmrc.major !== fromWorkflow.major) {\n throw new Error(\n `signoff: .nvmrc pins Node ${fromNvmrc.declared} and ${fromWorkflow.source} pins ` +\n `${fromWorkflow.declared}. A sign-off that replaces CI cannot verify two runtimes, and picking ` +\n 'one silently would sign off a runtime the other half of the repo says is wrong. Make them agree, ' +\n 'or declare `nodeVersion` in the signoff config.',\n )\n }\n if (fromNvmrc) return fromNvmrc\n if (fromWorkflow) return { ...fromWorkflow, source: fromWorkflow.source }\n return null\n}\n\n/**\n * Throw when the running Node cannot stand in for the declared one.\n *\n * Major-only: a patch difference is not a different runtime, and demanding an\n * exact patch would refuse every host that has not just re-installed.\n */\nexport function assertNodeVersion(requirement: NodeVersionRequirement | null, running = process.version): void {\n if (!requirement) return\n const runningMajor = majorOf(running)\n if (runningMajor === requirement.major) return\n throw new Error(\n `signoff: this repo pins Node ${requirement.declared} (${requirement.source}) and you are running ${running}. ` +\n 'A sign-off that replaces CI has to verify the runtime the product ships, so this refuses rather than ' +\n `reporting a pass it did not earn. Switch with \\`nvm use ${requirement.major}\\`, or change the pin if the ` +\n 'product really has moved.',\n )\n}\n","import { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\n/**\n * Read the Node pin out of the workflows that gate a merge.\n *\n * This exists because of a measured false PASS. legal-agent `4c0d688` was red\n * in CI on two test files — `Cannot bundle Node.js built-in \"node:sqlite\"` —\n * and the sign-off gate signed it off. Same bytes, same lockfile, same\n * hermetic tree: on Node 22 both files fail, on Node 24 both pass, 100% of the\n * time. The runtime was the whole difference, and the gate never saw it because\n * it looked for a pin in exactly two places the fleet does not keep one.\n *\n * None of tax-agent, legal-agent or agent-app has a `.nvmrc`. All three pin\n * `node-version: 22` in the workflow that IS the merge gate. So the pin the\n * gate is replacing was sitting in the file it is replacing, unread.\n *\n * Scope is deliberate: only workflows that trigger on `pull_request`. Those are\n * the merge gate. agent-app's `publish.yml` runs Node 24.18.0 for its release\n * jobs and triggers on push, and reading it would put the gate in permanent\n * conflict with `ci.yml`'s Node 22.\n *\n * Nothing here guesses. An expression (`${{ matrix.node }}`) is not a version,\n * a `node-version-file` that does not exist is not a pin, and two merge-gate\n * workflows on different majors is a question this module cannot answer — each\n * one refuses and names the config key that settles it.\n */\n\nexport interface WorkflowNodePin {\n /** Repo-relative workflow path, for the proof. */\n readonly file: string\n /** The version as written: `22`, `24.18.0`, `v20`. */\n readonly value: string\n /** `node-version`, or the `node-version-file` path it was read through. */\n readonly via: string\n}\n\nconst WORKFLOW_DIR = join('.github', 'workflows')\n\n/** Everything after an unquoted ` #`, plus surrounding quotes, is not the value. */\nfunction scalarValue(raw: string): string {\n const withoutComment = raw.replace(/\\s+#.*$/, '').trim()\n const quoted = /^(['\"])(.*)\\1$/.exec(withoutComment)\n return (quoted?.[2] ?? withoutComment).trim()\n}\n\nfunction indentOf(line: string): number {\n return line.length - line.trimStart().length\n}\n\nfunction isBlank(line: string): boolean {\n const trimmed = line.trim()\n return trimmed.length === 0 || trimmed.startsWith('#')\n}\n\n/**\n * Does this workflow run on `pull_request`?\n *\n * Both spellings the fleet uses are handled: the block form every workflow here\n * writes, and the inline form (`on: [push, pull_request]`). `on` is quoted in\n * some repos because YAML 1.1 reads a bare `on` as the boolean true, so the\n * quoted keys are matched too.\n */\nexport function triggersOnPullRequest(source: string): boolean {\n const lines = source.split('\\n')\n for (let index = 0; index < lines.length; index += 1) {\n const line = lines[index] as string\n const header = /^(?:on|\"on\"|'on')\\s*:(.*)$/.exec(line)\n if (!header) continue\n\n const inline = scalarValue(header[1] ?? '')\n if (inline.length > 0) {\n return inline\n .replace(/^\\[|\\]$/g, '')\n .split(',')\n .map((token) => token.trim())\n .includes('pull_request')\n }\n\n // Block form: the trigger names sit at the first nesting level under `on:`.\n // Anything deeper belongs to a trigger's own options (`branches`, `paths`).\n let nesting: number | null = null\n for (let cursor = index + 1; cursor < lines.length; cursor += 1) {\n const body = lines[cursor] as string\n if (isBlank(body)) continue\n const bodyIndent = indentOf(body)\n if (bodyIndent === 0) break\n if (nesting === null) nesting = bodyIndent\n if (bodyIndent !== nesting) continue\n const key = /^\\s*(?:-\\s*)?([A-Za-z_][\\w-]*)\\s*:?\\s*$/.exec(body)\n if (key?.[1] === 'pull_request') return true\n }\n return false\n }\n return false\n}\n\n/** Every Node pin an `actions/setup-node` step declares in one workflow. */\nfunction pinsInWorkflow(repoRoot: string, file: string, source: string): WorkflowNodePin[] {\n const pins: WorkflowNodePin[] = []\n for (const line of source.split('\\n')) {\n const match = /^\\s*(node-version|node-version-file)\\s*:\\s*(\\S.*)$/.exec(line)\n if (!match) continue\n const key = match[1] as string\n const value = scalarValue(match[2] as string)\n // A matrix expression is several runtimes, not one. Refusing to guess is\n // the point; `resolveWorkflowNodePin` turns an all-expression repo into a\n // named refusal rather than a silent \"unpinned\".\n if (value.includes('${{')) continue\n\n if (key === 'node-version') {\n pins.push({ file, value, via: 'node-version' })\n continue\n }\n\n const target = join(repoRoot, value)\n if (!existsSync(target)) {\n throw new Error(\n `signoff: ${file} reads its Node pin from \"${value}\" (node-version-file) and that file does not exist. ` +\n 'The workflow this gate replaces cannot itself run, so there is nothing to verify against.',\n )\n }\n const declared = readFileSync(target, 'utf8')\n .split('\\n')\n .map((entry) => entry.trim())\n .find((entry) => entry.length > 0 && !entry.startsWith('#'))\n if (declared !== undefined) pins.push({ file, value: declared, via: `node-version-file ${value}` })\n }\n return pins\n}\n\n/** Node pins declared by every workflow that gates a merge, in filename order. */\nexport function scanMergeGateNodePins(repoRoot: string): WorkflowNodePin[] {\n const dir = join(repoRoot, WORKFLOW_DIR)\n if (!existsSync(dir)) return []\n\n const pins: WorkflowNodePin[] = []\n const files = readdirSync(dir)\n .filter((name) => name.endsWith('.yml') || name.endsWith('.yaml'))\n .sort()\n for (const name of files) {\n const source = readFileSync(join(dir, name), 'utf8')\n if (!triggersOnPullRequest(source)) continue\n pins.push(...pinsInWorkflow(repoRoot, `${WORKFLOW_DIR}/${name}`, source))\n }\n return pins\n}\n\nexport interface ResolvedWorkflowPin {\n readonly major: number\n readonly declared: string\n readonly source: string\n}\n\n/**\n * One Node major for the whole merge gate, or a refusal that names why not.\n *\n * `majorOf` is injected rather than imported to keep the direction of the\n * dependency one-way: `node-version.ts` owns what a version string means, this\n * module owns where the string lives.\n */\nexport function resolveWorkflowNodePin(\n repoRoot: string,\n majorOf: (raw: string) => number | null,\n): ResolvedWorkflowPin | null {\n const pins = scanMergeGateNodePins(repoRoot)\n if (pins.length === 0) return null\n\n const byMajor = new Map<number, WorkflowNodePin[]>()\n for (const pin of pins) {\n const major = majorOf(pin.value)\n // An alias (`lts/*`) is a pin this module cannot resolve without a network\n // call — the same treatment `.nvmrc` gives one.\n if (major === null) continue\n const bucket = byMajor.get(major)\n if (bucket) bucket.push(pin)\n else byMajor.set(major, [pin])\n }\n\n if (byMajor.size === 0) return null\n if (byMajor.size > 1) {\n const detail = [...byMajor.values()]\n .flat()\n .map((pin) => ` ${pin.file} (${pin.via}): ${pin.value}`)\n .join('\\n')\n throw new Error(\n 'signoff: the workflows that gate a merge here pin different Node majors, so there is no single ' +\n `runtime to verify:\\n${detail}\\n` +\n 'Declare `nodeVersion` in the signoff config to say which one a sign-off means.',\n )\n }\n\n const [entry] = [...byMajor.entries()]\n if (entry === undefined) return null\n const [major, matched] = entry\n const first = matched[0] as WorkflowNodePin\n const files = [...new Set(matched.map((pin: WorkflowNodePin) => pin.file))].join(', ')\n return { major, declared: first.value, source: `${files} (${first.via})` }\n}\n","import { spawn } from 'node:child_process'\n\n/**\n * The subprocess primitive every sign-off step runs through.\n *\n * Three properties the gate depends on, none of which `execSync` gives:\n *\n * 1. **Cancellable.** Under fail-fast, a build still running when typecheck\n * fails is killed rather than waited out — otherwise the \"faster than CI\"\n * claim is spent waiting for work whose verdict no longer matters.\n * 2. **Process-group kill.** Steps run through `sh -c`, so killing the shell\n * leaves `vitest`'s forks and `tsup`'s dts worker orphaned and holding CPU.\n * The child is spawned as a group leader and the whole group is signalled.\n * 3. **Bounded, non-silent capture.** Output is capped, and when the cap is hit\n * the elision is stated in the captured text. A gate that quietly drops the\n * middle of a failure log is a gate that hides the failure.\n */\n\nexport interface CommandResult {\n readonly command: string\n readonly cwd: string\n readonly exitCode: number\n readonly signal: string | null\n readonly durationMs: number\n readonly output: string\n readonly truncated: boolean\n readonly timedOut: boolean\n}\n\nexport interface RunCommandOptions {\n readonly command: string\n readonly cwd: string\n readonly env?: Readonly<Record<string, string | undefined>>\n readonly timeoutMs?: number\n readonly signal?: AbortSignal\n /** Retained bytes before elision. Default 2 MiB. */\n readonly maxOutputBytes?: number\n /** Grace between SIGTERM and SIGKILL. Default 5 s. */\n readonly killGraceMs?: number\n readonly onData?: (chunk: string) => void\n}\n\nconst DEFAULT_MAX_OUTPUT_BYTES = 2 * 1024 * 1024\nconst DEFAULT_KILL_GRACE_MS = 5_000\n/** Fraction of the budget kept from the head; the rest is the tail. The head\n * carries the command echo and the first error, the tail carries the summary. */\nconst HEAD_SHARE = 0.25\n\n/**\n * Ring-ish buffer that keeps a head window and a tail window, so both the first\n * error and the final summary survive a very long log.\n */\nclass BoundedOutput {\n private head = ''\n private tail = ''\n private total = 0\n private readonly headBudget: number\n private readonly tailBudget: number\n\n constructor(private readonly budget: number) {\n this.headBudget = Math.floor(budget * HEAD_SHARE)\n this.tailBudget = budget - this.headBudget\n }\n\n push(chunk: string): void {\n this.total += chunk.length\n if (this.head.length < this.headBudget) {\n const room = this.headBudget - this.head.length\n this.head += chunk.slice(0, room)\n chunk = chunk.slice(room)\n if (chunk.length === 0) return\n }\n this.tail = (this.tail + chunk).slice(-this.tailBudget)\n }\n\n get truncated(): boolean {\n return this.total > this.budget\n }\n\n text(): string {\n if (!this.truncated) return this.head + this.tail\n const elided = this.total - this.head.length - this.tail.length\n return `${this.head}\\n\\n[signoff] ${elided} bytes elided (output exceeded ${this.budget} bytes)\\n\\n${this.tail}`\n }\n}\n\n/** Signal a whole process group, tolerating the race where it already exited. */\nfunction killGroup(pid: number, signal: NodeJS.Signals): void {\n try {\n process.kill(-pid, signal)\n } catch (err) {\n // ESRCH means the group is already gone, which is the outcome we wanted.\n // Anything else is a real failure to signal and must not be swallowed.\n if ((err as NodeJS.ErrnoException).code !== 'ESRCH') throw err\n }\n}\n\n/**\n * Run one shell command to completion and report what happened.\n *\n * Never throws on a non-zero exit — a failing step is data, not an exception.\n * It throws only when the process could not be started at all.\n */\nexport function runCommand(options: RunCommandOptions): Promise<CommandResult> {\n const {\n command,\n cwd,\n env,\n timeoutMs,\n signal,\n maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,\n killGraceMs = DEFAULT_KILL_GRACE_MS,\n onData,\n } = options\n\n return new Promise<CommandResult>((resolve, reject) => {\n const startedAt = Date.now()\n const buffer = new BoundedOutput(maxOutputBytes)\n let timedOut = false\n let killTimer: NodeJS.Timeout | undefined\n let graceTimer: NodeJS.Timeout | undefined\n\n const child = spawn(command, {\n cwd,\n env: env as NodeJS.ProcessEnv | undefined,\n shell: true,\n // Group leader: lets one signal reach `sh` and everything it spawned.\n detached: true,\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n const pid = child.pid\n const terminate = (): void => {\n if (pid === undefined || child.exitCode !== null || child.signalCode !== null) return\n killGroup(pid, 'SIGTERM')\n graceTimer = setTimeout(() => {\n if (child.exitCode === null && child.signalCode === null) killGroup(pid, 'SIGKILL')\n }, killGraceMs)\n graceTimer.unref()\n }\n\n const onAbort = (): void => terminate()\n signal?.addEventListener('abort', onAbort, { once: true })\n\n if (timeoutMs !== undefined) {\n killTimer = setTimeout(() => {\n timedOut = true\n terminate()\n }, timeoutMs)\n killTimer.unref()\n }\n\n const collect = (chunk: Buffer): void => {\n const text = chunk.toString('utf8')\n buffer.push(text)\n onData?.(text)\n }\n child.stdout.on('data', collect)\n child.stderr.on('data', collect)\n\n const cleanup = (): void => {\n if (killTimer) clearTimeout(killTimer)\n if (graceTimer) clearTimeout(graceTimer)\n signal?.removeEventListener('abort', onAbort)\n }\n\n child.on('error', (err) => {\n cleanup()\n reject(new Error(`signoff: could not start \\`${command}\\` in ${cwd}: ${err.message}`))\n })\n\n child.on('close', (code, sig) => {\n cleanup()\n resolve({\n command,\n cwd,\n // A signalled process reports code `null`; 128+n is the shell's own\n // convention and keeps the field a number a caller can compare.\n exitCode: code ?? (sig === 'SIGKILL' ? 137 : 143),\n signal: sig,\n durationMs: Date.now() - startedAt,\n output: buffer.text(),\n truncated: buffer.truncated,\n timedOut,\n })\n })\n })\n}\n","/**\n * The dependency graph, and running it as wide as the graph allows.\n *\n * CI runs these steps in a line because a YAML `steps:` list is a line. Nothing\n * about the work requires that: typecheck, the suite, the build and knip all\n * read source and none reads another's output. On a 32-core host, running them\n * concurrently is free wall-clock, and it is the only reason a strictly larger\n * verification can still finish faster than the CI it replaces.\n *\n * The graph is declared per step (`needs`), validated here, and the schedule is\n * derived from it — so an omitted edge is a correctness bug the repo owns, and\n * a cycle is named rather than deadlocking.\n */\n\nexport interface GraphNode {\n readonly name: string\n readonly needs?: readonly string[]\n}\n\n/** Throws on a duplicate name, a dangling `needs`, or a cycle. */\nexport function validateGraph(nodes: readonly GraphNode[]): void {\n const seen = new Set<string>()\n for (const node of nodes) {\n if (seen.has(node.name)) throw new Error(`signoff: two steps are both named \"${node.name}\"; names must be unique`)\n seen.add(node.name)\n }\n for (const node of nodes) {\n for (const need of node.needs ?? []) {\n if (!seen.has(need)) {\n throw new Error(`signoff: step \"${node.name}\" needs \"${need}\", which is not a step in this config`)\n }\n }\n }\n\n const byName = new Map(nodes.map((node) => [node.name, node]))\n const state = new Map<string, 'visiting' | 'done'>()\n const walk = (name: string, path: readonly string[]): void => {\n const status = state.get(name)\n if (status === 'done') return\n if (status === 'visiting') {\n const cycle = [...path.slice(path.indexOf(name)), name].join(' -> ')\n throw new Error(`signoff: dependency cycle among steps: ${cycle}`)\n }\n state.set(name, 'visiting')\n for (const need of byName.get(name)?.needs ?? []) walk(need, [...path, name])\n state.set(name, 'done')\n }\n for (const node of nodes) walk(node.name, [])\n}\n\ntype TaskStatus = 'passed' | 'failed' | 'skipped' | 'cancelled' | 'blocked'\n\nexport interface TaskOutcome<T> {\n readonly name: string\n readonly status: TaskStatus\n /** Present for anything that actually ran, including a cancelled task. */\n readonly value: T | null\n readonly startedAtMs: number | null\n readonly finishedAtMs: number | null\n}\n\nexport interface RunGraphOptions<TNode extends GraphNode, TValue> {\n readonly nodes: readonly TNode[]\n readonly maxParallel: number\n /** `false` stops scheduling after the first failure and kills what is running. */\n readonly keepGoing: boolean\n /** Runs one node. Resolves `{ ok }` for pass/fail; must not throw for a\n * failing command — a non-zero exit is data. */\n readonly run: (node: TNode, signal: AbortSignal) => Promise<{ readonly ok: boolean; readonly value: TValue }>\n /** Wall-clock origin, so outcomes are comparable across the whole run. */\n readonly now?: () => number\n}\n\n/**\n * Run the graph, honouring dependencies and the parallelism cap.\n *\n * Failure semantics, stated because they are the difference between a gate you\n * trust and one you argue with:\n * - fail-fast (default): nothing new is scheduled, everything in flight is\n * aborted and reported as `cancelled`, everything unstarted as `skipped`.\n * - `keepGoing`: independent work continues, but a step whose dependency\n * failed is reported `blocked`. It is never reported as passed, and never\n * silently omitted.\n */\nexport async function runGraph<TNode extends GraphNode, TValue>(\n options: RunGraphOptions<TNode, TValue>,\n): Promise<TaskOutcome<TValue>[]> {\n const { nodes, maxParallel, keepGoing, run, now = () => Date.now() } = options\n validateGraph(nodes)\n\n const origin = now()\n const outcomes = new Map<string, TaskOutcome<TValue>>()\n const pending = new Map(nodes.map((node) => [node.name, node]))\n const running = new Map<string, { readonly promise: Promise<void>; readonly controller: AbortController }>()\n let aborted = false\n\n const failedNames = new Set<string>()\n const passedNames = new Set<string>()\n\n const blockedBy = (node: TNode): boolean => (node.needs ?? []).some((need) => failedNames.has(need))\n const ready = (node: TNode): boolean => (node.needs ?? []).every((need) => passedNames.has(need))\n\n const settle = (name: string, outcome: TaskOutcome<TValue>): void => {\n outcomes.set(name, outcome)\n if (outcome.status === 'passed') passedNames.add(name)\n else failedNames.add(name)\n }\n\n const start = (node: TNode): void => {\n pending.delete(node.name)\n const controller = new AbortController()\n const startedAtMs = now() - origin\n const promise = run(node, controller.signal).then((result) => {\n const finishedAtMs = now() - origin\n const cancelled = controller.signal.aborted && !result.ok\n settle(node.name, {\n name: node.name,\n status: cancelled ? 'cancelled' : result.ok ? 'passed' : 'failed',\n value: result.value,\n startedAtMs,\n finishedAtMs,\n })\n running.delete(node.name)\n })\n running.set(node.name, { promise, controller })\n }\n\n for (;;) {\n if (!aborted) {\n // Drain everything the graph currently permits, up to the cap.\n for (const node of [...pending.values()]) {\n if (running.size >= maxParallel) break\n if (blockedBy(node)) {\n pending.delete(node.name)\n settle(node.name, { name: node.name, status: 'blocked', value: null, startedAtMs: null, finishedAtMs: null })\n continue\n }\n if (ready(node)) start(node)\n }\n }\n\n if (running.size === 0) {\n // Nothing running: either everything settled, or the rest is unreachable.\n if (pending.size === 0) break\n if (aborted) break\n // A pending node whose dependency neither passed nor failed cannot happen\n // after validateGraph, so anything left here is blocked or ready-next.\n const progressed = [...pending.values()].some((node) => ready(node) || blockedBy(node))\n if (!progressed) break\n continue\n }\n\n await Promise.race([...running.values()].map((entry) => entry.promise))\n\n if (!keepGoing && failedNames.size > 0 && !aborted) {\n aborted = true\n for (const entry of running.values()) entry.controller.abort()\n }\n }\n\n for (const node of pending.values()) {\n settle(node.name, {\n name: node.name,\n status: blockedBy(node) ? 'blocked' : 'skipped',\n value: null,\n startedAtMs: null,\n finishedAtMs: null,\n })\n }\n\n return nodes.map((node) => {\n const outcome = outcomes.get(node.name)\n if (!outcome) throw new Error(`signoff: step \"${node.name}\" produced no outcome — scheduler bug`)\n return outcome\n })\n}\n","import { createHash, randomInt } from 'node:crypto'\nimport type { SignoffShuffleSpec, SignoffStepSpec } from './types'\n\n/**\n * Suite-order randomization — the half of this gate that CI does not have.\n *\n * CI runs one arbitrary file order, so a scheduling-dependent failure is a coin\n * flip it happens to win or lose. The `node:sqlite` bundling failure that\n * started this was exactly that shape: it reproduced under CI's clean install\n * and worker sharding and not under a warm local run, and a single fixed order\n * could have missed it in either direction.\n *\n * Two rules make randomization useful rather than merely noisy:\n *\n * 1. **Every seed is recorded.** A shuffled failure that cannot be replayed is\n * a rumour. The report carries the base seed and every derived seed, and the\n * derivation is a pure function of `(base, step, index)` — so `--seed <base>`\n * reproduces the whole run, and a single seed replays one step.\n * 2. **Files are shuffled; tests inside a file are not.** File order is what\n * module-graph and worker-scheduling failures depend on. Shuffling the tests\n * within a file mostly finds intentional ordering in a `describe` block,\n * which is a false alarm, and a gate that cries wolf gets waived.\n */\n\n/**\n * Vitest file-order shuffle. `{seed}` is substituted per attempt.\n *\n * **No `--` separator, and that is measured, not assumed.** The habit is to\n * write `pnpm run test -- --flag`, and it silently breaks this: pnpm forwards\n * script arguments verbatim without needing the separator, so the `--` reaches\n * vitest, whose CLI treats it as end-of-options and drops everything after it.\n * Measured on this repo, four test files, `--reporter=verbose`:\n *\n * | invocation | seed 1 order | seed 2 order |\n * |---|---|---|\n * | `vitest run -- --sequence.shuffle.files=true --sequence.seed=N` | schedule, store, seeds, config | schedule, store, seeds, config |\n * | `vitest run --sequence.shuffle.files=true --sequence.seed=N` | schedule, config, store, seeds | seeds, schedule, config, store |\n *\n * The first row is the failure this gate exists to prevent, applied to itself:\n * a run that reports \"2 orders, seeds recorded\" while running one fixed order\n * twice. If a runner ever does need a separator, its config declares its own\n * `shuffle.args`.\n */\nconst DEFAULT_SHUFFLE_ARGS: readonly string[] = [\n '--sequence.shuffle.files=true',\n '--sequence.seed={seed}',\n]\n\nexport const DEFAULT_SHUFFLE_RUNS = 2\n\n/** A fresh base seed. Random, then recorded — never a fixed constant, or the\n * \"randomized\" order is one more fixed order. */\nexport function newSeedBase(): number {\n return randomInt(0, 2 ** 31 - 1)\n}\n\n/**\n * Derive a step's Nth seed from the base.\n *\n * A hash rather than `base + n`: adjacent seeds produce correlated orders in\n * some runners, and the point is independent samples of the order space.\n */\nexport function deriveSeed(base: number, stepName: string, index: number): number {\n const digest = createHash('sha256').update(`${base}:${stepName}:${index}`).digest()\n return digest.readUInt32BE(0) % 2 ** 31\n}\n\n/**\n * Refuse a shuffled step whose command cannot receive the appended flags.\n *\n * pnpm only forwards extra arguments to a script through `run`, `exec` or\n * `dlx`. The shorthand form puts pnpm's own option parser in front of them, and\n * what happens next is a version lottery. Measured on this host, appending\n * `--sequence.shuffle.files=true --sequence.seed=7` to a script that prints its\n * `process.argv`:\n *\n * | command | pnpm 9.15.9 | pnpm 10.22.0 |\n * |---|---|---|\n * | `pnpm run t <flags>` | forwarded, exit 0 | forwarded, exit 0 |\n * | `pnpm exec node probe.mjs <flags>` | forwarded, exit 0 | forwarded, exit 0 |\n * | `pnpm t <flags>` | `Unknown options`, exit 1 | exit 254, script never ran |\n * | `pnpm --filter web t <flags>` | `Unknown options`, exit 1 | **exit 0, script never ran** |\n *\n * That last cell is the reason this is a refusal and not a note. tax-agent's CI\n * runs `pnpm --filter web test`, and a config that copied the line verbatim\n * would, on pnpm 10, report a green \"unit tests\" step that executed zero tests.\n * A gate reporting safety it did not provide is the exact failure this module\n * exists to prevent, and the shorthand makes it silent.\n *\n * This is a static approximation — it checks the invocation SHAPE, not what the\n * runner received — so it is deliberately narrow: it fires only on commands\n * that invoke pnpm, and only on steps that get arguments appended.\n */\nexport function assertShuffleArgsReachTheRunner(steps: readonly SignoffStepSpec[]): void {\n for (const step of steps) {\n if (!normalizeShuffle(step.shuffle)) continue\n const tokens = step.run.split(/\\s+/).filter((token) => token.length > 0)\n const pnpmAt = tokens.findIndex((token) => token === 'pnpm' || token.endsWith('/pnpm'))\n if (pnpmAt === -1) continue\n if (tokens.slice(pnpmAt + 1).some((token) => token === 'run' || token === 'exec' || token === 'dlx')) continue\n throw new Error(\n `signoff: step \"${step.name}\" runs \\`${step.run}\\` and is shuffled, but pnpm only forwards appended ` +\n 'arguments to a script through `run`, `exec` or `dlx`. In the shorthand form pnpm 9 errors and pnpm 10 ' +\n 'exits 0 having run nothing, which would report a passing suite that never executed. ' +\n `Write it as \\`${step.run.replace(/\\s(\\S+)$/, ' run $1')}\\`.`,\n )\n }\n}\n\nexport interface StepAttemptPlan {\n readonly command: string\n readonly seed: number | null\n}\n\nfunction normalizeShuffle(shuffle: boolean | SignoffShuffleSpec | undefined): SignoffShuffleSpec | null {\n if (shuffle === undefined || shuffle === false) return null\n return shuffle === true ? {} : shuffle\n}\n\n/**\n * Expand one step into the commands that will actually run.\n *\n * An unshuffled step is one attempt with no seed. A shuffled step is one\n * attempt per seed, each with the seed substituted into the appended arguments.\n */\nexport function planAttempts(\n step: SignoffStepSpec,\n seedBase: number,\n overrideRuns?: number,\n): StepAttemptPlan[] {\n const spec = normalizeShuffle(step.shuffle)\n if (!spec) return [{ command: step.run, seed: null }]\n\n const args = spec.args ?? DEFAULT_SHUFFLE_ARGS\n const seeds =\n spec.seeds && spec.seeds.length > 0\n ? [...spec.seeds]\n : Array.from({ length: overrideRuns ?? spec.runs ?? DEFAULT_SHUFFLE_RUNS }, (_unused, index) =>\n deriveSeed(seedBase, step.name, index),\n )\n\n return seeds.map((seed) => ({\n command: `${step.run} ${args.map((arg) => arg.replaceAll('{seed}', String(seed))).join(' ')}`,\n seed,\n }))\n}\n","import { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs'\nimport { join, relative, sep } from 'node:path'\n\n/**\n * The pristine package store, cached on the lockfile so speed is not paid for\n * with a stale module graph.\n *\n * The whole mechanical difference between \"green locally\" and \"red in CI\" is\n * that CI installs `--frozen-lockfile` into an isolated store with no\n * `node_modules` and no framework cache, and a local run reuses both. Only one\n * of those two is safe to keep:\n *\n * - `node_modules` and any build/framework cache are **never** reused. They are\n * what masked the failure this gate exists to catch, and they are recreated\n * for every run inside a fresh `git worktree`.\n * - The **store** is reused, keyed on the bytes that decide what gets installed.\n * A pnpm store is content-addressed: every entry is named by the hash of what\n * is in it, so a reused entry cannot be a different version of a package than\n * the lockfile asked for. Reusing it skips downloads, not resolution.\n *\n * The key covers every manifest in the tree — lockfile, workspace file, every\n * `package.json`, `.npmrc`. Change any of them and the key changes, so the next\n * run installs into an empty store and pays the honest cold cost. Generations\n * are kept (default 4) rather than one, so moving between a branch and `main`\n * finds both warm instead of thrashing.\n */\n\n/** Basenames whose bytes decide what an install resolves to. */\nconst MANIFEST_FILES: readonly string[] = [\n 'pnpm-lock.yaml',\n 'pnpm-workspace.yaml',\n 'package.json',\n '.npmrc',\n '.nvmrc',\n 'package-lock.json',\n 'npm-shrinkwrap.json',\n 'yarn.lock',\n]\n\n/** Never descended into when hunting manifests. */\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.wrangler', '.react-router'])\n\nexport interface StoreResolution {\n readonly storeDir: string\n readonly cacheKey: string\n /** True when a store for this key already had content. */\n readonly hit: boolean\n /** Repo-relative manifest paths, sorted — the inputs to `cacheKey`. */\n readonly keyedOn: readonly string[]\n /** Store directories removed by the generation cap. */\n readonly pruned: readonly string[]\n}\n\nexport interface ResolveStoreOptions {\n /** The clean tree whose manifests are hashed. */\n readonly treePath: string\n readonly cacheDir: string\n /** Generations to keep. Default 4. */\n readonly generations?: number\n}\n\nfunction collectManifests(dir: string, root: string, out: string[]): void {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue\n collectManifests(join(dir, entry.name), root, out)\n } else if (MANIFEST_FILES.includes(entry.name)) {\n out.push(relative(root, join(dir, entry.name)).split(sep).join('/'))\n }\n }\n}\n\n/** Every manifest in the tree, repo-relative and sorted. */\nexport function manifestFiles(treePath: string): string[] {\n const found: string[] = []\n collectManifests(treePath, treePath, found)\n return found.sort()\n}\n\n/** sha256 over each manifest's path and content — order-independent by sorting. */\nexport function manifestCacheKey(treePath: string, files: readonly string[]): string {\n const hash = createHash('sha256')\n for (const rel of files) {\n hash.update(rel)\n hash.update('\\0')\n hash.update(createHash('sha256').update(readFileSync(join(treePath, rel))).digest('hex'))\n hash.update('\\n')\n }\n return hash.digest('hex')\n}\n\n/**\n * Prune all but the `keep` most recently used store generations.\n *\n * Recency is the directory's mtime, stamped on every use, so the store a run\n * just used is never the one evicted.\n */\nfunction pruneStores(storesRoot: string, keep: number): string[] {\n if (!existsSync(storesRoot)) return []\n const entries = readdirSync(storesRoot, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => {\n const full = join(storesRoot, entry.name)\n return { full, mtimeMs: statSync(full).mtimeMs }\n })\n .sort((a, b) => b.mtimeMs - a.mtimeMs)\n\n const pruned: string[] = []\n for (const stale of entries.slice(keep)) {\n rmSync(stale.full, { recursive: true, force: true })\n pruned.push(stale.full)\n }\n return pruned\n}\n\nexport function resolveStore(options: ResolveStoreOptions): StoreResolution {\n const { treePath, cacheDir, generations = 4 } = options\n const files = manifestFiles(treePath)\n if (files.length === 0) {\n throw new Error(\n `signoff: no package manifest under ${treePath}. A sign-off run installs from a lockfile; ` +\n 'there is nothing here to install.',\n )\n }\n\n const cacheKey = manifestCacheKey(treePath, files)\n const storesRoot = join(cacheDir, 'stores')\n const storeDir = join(storesRoot, cacheKey)\n // A store directory that exists but holds only the marker is a previous run\n // that died before installing; treat it as a miss so the report does not\n // claim a warm store it does not have.\n const marker = join(storeDir, '.signoff-store.json')\n const hit = existsSync(storeDir) && readdirSync(storeDir).some((entry) => entry !== '.signoff-store.json')\n\n mkdirSync(storeDir, { recursive: true })\n writeFileSync(marker, `${JSON.stringify({ cacheKey, keyedOn: files, usedAt: new Date().toISOString() }, null, 2)}\\n`)\n const now = new Date()\n utimesSync(storeDir, now, now)\n\n return { storeDir, cacheKey, hit, keyedOn: files, pruned: pruneStores(storesRoot, generations) }\n}\n","import { spawnSync } from 'node:child_process'\nimport { createHash } from 'node:crypto'\nimport { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\nimport type { SignoffRepoFacts, SignoffSource } from './types'\n\n/**\n * Materialize the pristine checkout every step runs against.\n *\n * **The decision, and the measurement behind it: a `git worktree` of HEAD, not\n * a filtered copy.** Both were timed on two real repos on this host:\n *\n * | repo | `git worktree add --detach` | `rsync -a --exclude node_modules --exclude .git` |\n * |---|---|---|\n * | agent-app (931 tracked files) | **0.04 s / 11 MB** | 0.12 s / 25 MB |\n * | legal-agent (753 tracked files) | **0.06 s / 24 MB** | **3.97 s / 2.7 GB** |\n *\n * The 2.7 GB is the argument. The copy carried `build/`, `.react-router/` and\n * `.wrangler/` — generated output and framework caches — because an exclude\n * list is a hand-maintained enumeration of things to leave behind, and it is\n * never complete. A warm Vite cache is precisely what made the `node:sqlite`\n * bundling failure invisible locally while CI saw it, so a materializer that\n * can leak one has defeated its own purpose. `git` already knows what is source\n * and what is generated, and `.gitignore` is that list, maintained by the repo.\n *\n * The overlay on top is what keeps the gate usable before you commit:\n * `source: 'working-tree'` applies `git diff HEAD` (staged and unstaged) as a\n * patch and copies untracked, non-ignored files in. `source: 'head'` verifies\n * exactly the commit that would merge.\n */\n\nexport interface MaterializeOptions {\n readonly repoDir: string\n readonly dest: string\n readonly source: SignoffSource\n /** Gitignored files the run genuinely needs. Missing ones abort. */\n readonly carryFiles?: readonly string[]\n}\n\nexport interface CleanTree extends SignoffRepoFacts {\n /** Absolute path of the materialized checkout. */\n readonly path: string\n}\n\nfunction git(args: readonly string[], cwd: string): string {\n const result = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 })\n if (result.error) throw new Error(`signoff: git ${args.join(' ')} failed to start: ${result.error.message}`)\n if (result.status !== 0) {\n throw new Error(`signoff: git ${args.join(' ')} exited ${result.status}\\n${result.stderr.trim()}`)\n }\n return result.stdout\n}\n\n/** Split a `-z` separated git list into entries. */\nfunction zsplit(out: string): string[] {\n return out.split('\\0').filter((entry) => entry.length > 0)\n}\n\nexport function repoRootOf(dir: string): string {\n return git(['rev-parse', '--show-toplevel'], dir).trim()\n}\n\nexport function materializeCleanTree(options: MaterializeOptions): CleanTree {\n const { repoDir, dest, source, carryFiles = [] } = options\n const root = repoRootOf(repoDir)\n const head = git(['rev-parse', 'HEAD'], root).trim()\n const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], root).trim()\n\n // A crashed earlier run leaves a registered worktree whose directory is gone;\n // `add` then refuses on a name collision. Pruning first is what makes the gate\n // re-runnable after a kill.\n git(['worktree', 'prune'], root)\n mkdirSync(dirname(dest), { recursive: true })\n if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })\n git(['worktree', 'add', '--detach', '--quiet', dest, head], root)\n\n let diffSha256: string | null = null\n let untrackedFiles: string[] = []\n\n if (source === 'working-tree') {\n // `git diff HEAD` covers staged and unstaged changes to tracked files in one\n // patch, including deletions and renames. `--binary` keeps a changed image\n // or lockfile-adjacent binary from silently dropping out of the patch.\n const patch = git(['diff', 'HEAD', '--binary', '--no-color', '--no-ext-diff'], root)\n if (patch.length > 0) {\n diffSha256 = createHash('sha256').update(patch).digest('hex')\n const patchFile = join(dirname(dest), `${dest.split('/').pop() ?? 'tree'}.patch`)\n writeFileSync(patchFile, patch)\n // Fail loud: a patch that does not apply means the tree we would verify is\n // not the tree the developer has, and verifying the wrong bytes is worse\n // than not verifying.\n git(['apply', '--binary', '--whitespace=nowarn', patchFile], dest)\n rmSync(patchFile, { force: true })\n }\n\n untrackedFiles = zsplit(git(['ls-files', '--others', '--exclude-standard', '-z'], root))\n for (const rel of untrackedFiles) {\n const target = join(dest, rel)\n mkdirSync(dirname(target), { recursive: true })\n copyFileSync(join(root, rel), target)\n }\n }\n\n const carried: string[] = []\n for (const rel of carryFiles) {\n if (isAbsolute(rel)) throw new Error(`signoff: carryFiles must be repo-relative; got \"${rel}\"`)\n const from = resolve(root, rel)\n if (!existsSync(from)) {\n throw new Error(\n `signoff: carryFiles names \"${rel}\", which does not exist at ${from}. ` +\n 'Remove it from the config or create the file — installing without it would resolve ' +\n 'against a different registry than the one you think you are verifying.',\n )\n }\n const target = join(dest, rel)\n mkdirSync(dirname(target), { recursive: true })\n copyFileSync(from, target)\n carried.push(rel)\n }\n\n return {\n path: dest,\n root,\n head,\n branch,\n source,\n dirty: diffSha256 !== null || untrackedFiles.length > 0,\n diffSha256,\n untrackedFiles,\n carriedFiles: carried,\n }\n}\n\n/** Unregister and delete a materialized tree. */\nexport function removeCleanTree(tree: CleanTree): void {\n git(['worktree', 'remove', '--force', tree.path], tree.root)\n}\n","import type { SignoffReport, SignoffStepResult } from './types'\n\n/**\n * The proof a human reads before merging.\n *\n * A sign-off gate is only worth the CI it replaces if its output answers the\n * question CI's green tick answers and the ones it does not: what bytes were\n * verified, in what environment, under which suite orders, and how to reproduce\n * it. So the report leads with the verdict, states the subject (commit + patch\n * digest) and the environment (clean tree, store cache state), lists every step\n * with its seeds, and ends with the exact command that runs it again.\n *\n * Failure output is printed in full, under the step's name. Naming the step is\n * the difference between \"CI is red\" and a fix.\n */\n\nconst BAR = '─'.repeat(72)\n\nfunction ms(value: number): string {\n return value >= 10_000 ? `${(value / 1000).toFixed(1)}s` : `${value}ms`\n}\n\nfunction statusMark(status: SignoffStepResult['status']): string {\n switch (status) {\n case 'passed': return 'ok '\n case 'failed': return 'FAIL'\n case 'cancelled': return 'kill'\n case 'blocked': return 'blkd'\n case 'skipped': return '-- '\n }\n}\n\nfunction seedList(step: SignoffStepResult): string {\n const seeds = step.attempts.map((attempt) => attempt.seed).filter((seed): seed is number => seed !== null)\n return seeds.length === 0 ? '' : ` seeds ${seeds.join(', ')}`\n}\n\n/** Concurrency actually achieved, measured from the step windows rather than\n * asserted from the config. A claimed speedup nobody measured is a wish. */\nexport function peakConcurrency(steps: readonly SignoffStepResult[]): number {\n const events: { at: number; delta: number }[] = []\n for (const step of steps) {\n if (step.startedAtMs === null || step.finishedAtMs === null) continue\n events.push({ at: step.startedAtMs, delta: 1 }, { at: step.finishedAtMs, delta: -1 })\n }\n events.sort((a, b) => a.at - b.at || a.delta - b.delta)\n let current = 0\n let peak = 0\n for (const event of events) {\n current += event.delta\n peak = Math.max(peak, current)\n }\n return peak\n}\n\nexport function formatSignoffReport(report: SignoffReport): string {\n const lines: string[] = []\n const verdict = report.ok ? 'SIGN-OFF PASSED' : 'SIGN-OFF FAILED'\n lines.push(BAR, `${verdict} — ${report.repo.branch} @ ${report.repo.head.slice(0, 12)}`, BAR, '')\n\n lines.push('subject')\n lines.push(` repo ${report.repo.root}`)\n lines.push(` source ${report.repo.source}${report.repo.dirty ? ' (working tree carries uncommitted work)' : ''}`)\n if (report.repo.diffSha256) lines.push(` patch sha256:${report.repo.diffSha256.slice(0, 16)}`)\n if (report.repo.untrackedFiles.length > 0) {\n lines.push(` untracked ${report.repo.untrackedFiles.length} file(s) copied in`)\n }\n if (report.repo.carriedFiles.length > 0) lines.push(` carried ${report.repo.carriedFiles.join(', ')}`)\n lines.push('')\n\n lines.push('environment')\n lines.push(` clean tree ${report.workspace}${report.workspaceRetained ? ' (retained)' : ' (removed)'}`)\n lines.push(` install ${report.install.command}`)\n lines.push(\n ` store ${report.install.cacheHit ? 'warm' : 'cold'} — ${report.install.cacheKey.slice(0, 16)} ` +\n `(keyed on ${report.install.keyedOn.length} manifest file(s))`,\n )\n lines.push(\n ` host ${report.host.node} · ${report.host.packageManager} · ${report.host.cpus} cpus` +\n (report.host.nodePinned === null\n ? ' · node UNPINNED by this repo'\n : ` · pinned ${report.host.nodePinned} (${report.host.nodePinSource})`),\n )\n lines.push(\n ` config ${report.configOrigin.kind === 'derived'\n ? `derived from scripts: ${report.configOrigin.scripts.join(', ')}`\n : report.configOrigin.path}`,\n )\n lines.push('')\n\n const width = Math.max(...report.steps.map((step) => step.name.length), 'install'.length)\n lines.push('steps')\n lines.push(\n ` ${report.install.exitCode === 0 ? 'ok ' : 'FAIL'} ${'install'.padEnd(width)} ${ms(report.install.durationMs).padStart(8)}`,\n )\n for (const step of report.steps) {\n const window =\n step.startedAtMs === null || step.finishedAtMs === null\n ? ''\n : ` [${ms(step.startedAtMs)} → ${ms(step.finishedAtMs)}]`\n lines.push(\n ` ${statusMark(step.status)} ${step.name.padEnd(width)} ${ms(step.durationMs).padStart(8)}` +\n ` ${step.attempts.length} run(s)${window}${seedList(step)}`,\n )\n }\n lines.push('')\n\n const peak = peakConcurrency(report.steps)\n const saved = report.serialMs - report.wallClockMs\n lines.push('timing')\n lines.push(` wall clock ${ms(report.wallClockMs)}`)\n lines.push(` serial sum ${ms(report.serialMs)} (install + every step, one after another)`)\n lines.push(\n ` parallel peak ${peak} step(s) at once — ` +\n (saved > 0 ? `${ms(saved)} saved, ${(report.serialMs / report.wallClockMs).toFixed(2)}x` : 'no overlap available'),\n )\n lines.push('')\n\n const failures = report.steps.filter((step) => step.status === 'failed' || step.status === 'cancelled')\n if (report.install.exitCode !== 0) {\n lines.push(BAR, 'install FAILED — no step could run', BAR, report.install.output.trimEnd(), '')\n }\n for (const step of failures) {\n const last = step.attempts[step.attempts.length - 1]\n lines.push(BAR)\n lines.push(`${step.status === 'cancelled' ? 'CANCELLED' : 'FAILED'}: ${step.name}`)\n if (last) {\n lines.push(` command ${last.command}`)\n lines.push(` exit ${last.exitCode}${last.signal ? ` (${last.signal})` : ''}${last.timedOut ? ' — TIMED OUT' : ''}`)\n if (last.seed !== null) {\n lines.push(` seed ${last.seed} — replay this order alone with the same seed`)\n }\n lines.push(BAR, last.output.trimEnd(), '')\n }\n }\n\n const blocked = report.steps.filter((step) => step.status === 'blocked' || step.status === 'skipped')\n if (blocked.length > 0) {\n lines.push(`not judged: ${blocked.map((step) => `${step.name} (${step.status})`).join(', ')}`)\n lines.push('')\n }\n\n lines.push(`reproduce: ${report.reproduce}`)\n return lines.join('\\n')\n}\n\n/** One line for a commit message, a PR comment, or a chat handoff. */\nexport function formatSignoffLine(report: SignoffReport): string {\n const passed = report.steps.filter((step) => step.status === 'passed').length\n return (\n `${report.ok ? 'signoff PASS' : 'signoff FAIL'} ${report.repo.head.slice(0, 12)} — ` +\n `${passed}/${report.steps.length} steps, ${ms(report.wallClockMs)} wall ` +\n `(${ms(report.serialMs)} serial), seed ${report.seedBase}, ` +\n `${report.install.cacheHit ? 'warm' : 'cold'} store, clean install`\n )\n}\n"],"mappings":"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","names":["spawnSync","existsSync","join","resolve","existsSync","readFileSync","join","existsSync","readFileSync","join","majorOf","major","join","existsSync","readFileSync","resolve","createHash","existsSync","readFileSync","readdirSync","join","createHash","existsSync","mkdirSync","rmSync","writeFileSync","join","resolve","join","spawnSync","resolve","existsSync"]}
|
|
@@ -226,8 +226,9 @@ function createZoomPanMath(config) {
|
|
|
226
226
|
if (!Number.isFinite(paddingPx) || paddingPx < 0) {
|
|
227
227
|
throw new Error(`fitPage: paddingPx must be a non-negative finite number, got ${paddingPx}`);
|
|
228
228
|
}
|
|
229
|
-
const
|
|
230
|
-
const
|
|
229
|
+
const pad = Math.min(paddingPx, Math.min(viewport.width, viewport.height) * 0.2);
|
|
230
|
+
const availW = viewport.width - pad * 2;
|
|
231
|
+
const availH = viewport.height - pad * 2;
|
|
231
232
|
const zoom = Math.min(maxZoom, Math.max(minZoom, Math.min(availW / page.width, availH / page.height)));
|
|
232
233
|
const panX = (viewport.width - page.width * zoom) / 2;
|
|
233
234
|
const panY = (viewport.height - page.height * zoom) / 2;
|
|
@@ -262,6 +263,10 @@ function resolveExportParams(page, opts) {
|
|
|
262
263
|
quality: format === "jpeg" ? 0.92 : void 0
|
|
263
264
|
};
|
|
264
265
|
}
|
|
266
|
+
var NODE_CACHE_PIXEL_RATIO_MAX = 4;
|
|
267
|
+
function resolveNodeCachePixelRatio(absoluteScale, devicePixelRatio) {
|
|
268
|
+
return Math.min(absoluteScale * devicePixelRatio, NODE_CACHE_PIXEL_RATIO_MAX);
|
|
269
|
+
}
|
|
265
270
|
function identifyTaintedSrc(imageSrcs) {
|
|
266
271
|
for (const { src } of imageSrcs) {
|
|
267
272
|
if (isCrossOriginSrc(src)) return src;
|
|
@@ -430,6 +435,8 @@ export {
|
|
|
430
435
|
createZoomPanMath,
|
|
431
436
|
isExportHiddenNodeName,
|
|
432
437
|
resolveExportParams,
|
|
438
|
+
NODE_CACHE_PIXEL_RATIO_MAX,
|
|
439
|
+
resolveNodeCachePixelRatio,
|
|
433
440
|
identifyTaintedSrc,
|
|
434
441
|
isCrossOriginSrc,
|
|
435
442
|
documentCropToStageCoords,
|
|
@@ -440,4 +447,4 @@ export {
|
|
|
440
447
|
buildInsertImageOp,
|
|
441
448
|
DEFAULT_INSERT_TEMPLATES
|
|
442
449
|
};
|
|
443
|
-
//# sourceMappingURL=chunk-
|
|
450
|
+
//# sourceMappingURL=chunk-OYTMILRK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/design-canvas-react/engine/snap.ts","../src/design-canvas-react/engine/selection.ts","../src/design-canvas-react/engine/zoom-pan.ts","../src/design-canvas-react/export-math.ts","../src/design-canvas-react/insert-builders.ts"],"sourcesContent":["/**\n * 2-axis snap engine for the design-canvas editor. Targets come from element\n * AABBs (edges + centers), page edges + center, saved guides, and grid lines\n * near the moving bounds. Grid lines are generated lazily in the neighborhood\n * of the moving element, not the full page — avoids allocating thousands of\n * targets on large pages with fine grids.\n *\n * Threshold is a SCREEN distance (pixels), divided by zoom to convert to\n * document units — what \"feels close\" is a screen distance.\n *\n * Tie-breaking: non-grid kinds beat grid on equal distance; among equals of\n * the same priority the first in iteration order wins.\n */\n\nimport { elementAabb } from '../../design-canvas/model'\nimport type { Bounds, SceneElement, ScenePage } from '../../design-canvas/model'\nimport type { EditorSceneState, SnapEngine, SnapResult, SnapTarget, SnapTargetKind, SnapTargets } from '../contracts'\n\n/** Priority rank for tie-breaking: lower = higher priority (wins the tie). */\nconst KIND_PRIORITY: Record<SnapTargetKind, number> = {\n 'guide': 0,\n 'page-edge': 1,\n 'page-center': 2,\n 'element-edge': 3,\n 'element-center': 4,\n 'grid': 5,\n}\n\n/** Build a SnapEngine instance to manage snapping targets and behavior within an editor scene */\nexport function createSnapEngine(): SnapEngine {\n return {\n collectTargets(state: EditorSceneState, excludeIds: string[]): SnapTargets {\n const page = state.document.pages.find((p) => p.id === state.activePageId)\n if (!page) throw new Error(`collectTargets: active page ${state.activePageId} not found`)\n\n const vertical: SnapTarget[] = []\n const horizontal: SnapTarget[] = []\n const excludeSet = new Set(excludeIds)\n\n // Page edges + center\n vertical.push({ position: 0, kind: 'page-edge' })\n vertical.push({ position: page.width, kind: 'page-edge' })\n vertical.push({ position: page.width / 2, kind: 'page-center' })\n horizontal.push({ position: 0, kind: 'page-edge' })\n horizontal.push({ position: page.height, kind: 'page-edge' })\n horizontal.push({ position: page.height / 2, kind: 'page-center' })\n\n // Saved ruler guides\n for (const pos of page.guides.vertical) {\n vertical.push({ position: pos, kind: 'guide' })\n }\n for (const pos of page.guides.horizontal) {\n horizontal.push({ position: pos, kind: 'guide' })\n }\n\n // Element edges + centers (visible, unlocked, not excluded)\n collectElementTargets(page.elements, excludeSet, vertical, horizontal)\n\n // Grid lines (deferred to apply() with the moving bounds neighborhood)\n // Grid targets are injected by collectGridTargets() when gridEnabled.\n\n return { vertical, horizontal }\n },\n\n apply(bounds: Bounds, targets: SnapTargets, thresholdPx: number, zoom: number): SnapResult {\n if (!Number.isFinite(zoom) || zoom <= 0) {\n throw new Error(`snap.apply: zoom must be a positive finite number, got ${zoom}`)\n }\n if (!Number.isFinite(thresholdPx) || thresholdPx < 0) {\n throw new Error(`snap.apply: thresholdPx must be a non-negative finite number, got ${thresholdPx}`)\n }\n const threshold = thresholdPx / zoom\n\n const snappedX = snapAxis(bounds.x, bounds.x + bounds.width / 2, bounds.x + bounds.width, targets.vertical, threshold)\n const snappedY = snapAxis(bounds.y, bounds.y + bounds.height / 2, bounds.y + bounds.height, targets.horizontal, threshold)\n\n return {\n x: snappedX !== null ? snappedX.docPosition - snappedX.elementOffset : bounds.x,\n y: snappedY !== null ? snappedY.docPosition - snappedY.elementOffset : bounds.y,\n activeVertical: snappedX !== null ? snappedX.target : null,\n activeHorizontal: snappedY !== null ? snappedY.target : null,\n }\n },\n }\n}\n\n/** Generate grid line targets within a neighborhood around the moving bounds.\n * Call this and append to `SnapTargets.vertical`/`horizontal` before\n * passing to `apply()` when `state.gridEnabled`. */\nexport function collectGridTargets(\n bounds: Bounds,\n gridSize: number,\n page: ScenePage,\n thresholdDocPx: number,\n): { vertical: SnapTarget[]; horizontal: SnapTarget[] } {\n if (!Number.isFinite(gridSize) || gridSize <= 0) {\n throw new Error(`collectGridTargets: gridSize must be positive finite, got ${gridSize}`)\n }\n const vertical: SnapTarget[] = []\n const horizontal: SnapTarget[] = []\n\n // Generate lines that fall within the bounds + threshold neighborhood\n const xMin = Math.floor((bounds.x - thresholdDocPx) / gridSize) * gridSize\n const xMax = Math.ceil((bounds.x + bounds.width + thresholdDocPx) / gridSize) * gridSize\n for (let x = xMin; x <= xMax; x += gridSize) {\n if (x >= 0 && x <= page.width) vertical.push({ position: x, kind: 'grid' })\n }\n\n const yMin = Math.floor((bounds.y - thresholdDocPx) / gridSize) * gridSize\n const yMax = Math.ceil((bounds.y + bounds.height + thresholdDocPx) / gridSize) * gridSize\n for (let y = yMin; y <= yMax; y += gridSize) {\n if (y >= 0 && y <= page.height) horizontal.push({ position: y, kind: 'grid' })\n }\n\n return { vertical, horizontal }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\ninterface SnapHit {\n /** The snap target that won. */\n target: SnapTarget\n /** The document-coordinate position of the snap line. */\n docPosition: number\n /** How far the element's reference point (left/center/right or top/mid/bottom)\n * is from the element's x/y origin — used to reconstruct the new origin. */\n elementOffset: number\n}\n\n/** Try snapping each of the three reference points (start/center/end) to the\n * nearest target. Returns the best hit across all three, or null. */\nfunction snapAxis(start: number, center: number, end: number, targets: SnapTarget[], threshold: number): SnapHit | null {\n const candidates: Array<{ value: number; offset: number }> = [\n { value: start, offset: 0 },\n { value: center, offset: center - start },\n { value: end, offset: end - start },\n ]\n\n let best: SnapHit | null = null\n let bestDistance = Infinity\n let bestPriority = Infinity\n\n for (const { value, offset } of candidates) {\n for (const target of targets) {\n const distance = Math.abs(target.position - value)\n if (distance > threshold) continue\n const priority = KIND_PRIORITY[target.kind]\n if (\n distance < bestDistance ||\n (distance === bestDistance && priority < bestPriority)\n ) {\n bestDistance = distance\n bestPriority = priority\n best = { target, docPosition: target.position, elementOffset: offset }\n }\n }\n }\n\n return best\n}\n\nfunction collectElementTargets(\n elements: SceneElement[],\n excludeIds: Set<string>,\n vertical: SnapTarget[],\n horizontal: SnapTarget[],\n): void {\n for (const el of elements) {\n if (!el.visible || el.locked) continue\n if (excludeIds.has(el.id)) continue\n\n const aabb = elementAabb(el)\n // Left, center, right\n vertical.push({ position: aabb.x, kind: 'element-edge' })\n vertical.push({ position: aabb.x + aabb.width / 2, kind: 'element-center' })\n vertical.push({ position: aabb.x + aabb.width, kind: 'element-edge' })\n // Top, middle, bottom\n horizontal.push({ position: aabb.y, kind: 'element-edge' })\n horizontal.push({ position: aabb.y + aabb.height / 2, kind: 'element-center' })\n horizontal.push({ position: aabb.y + aabb.height, kind: 'element-edge' })\n\n if (el.kind === 'group') {\n collectElementTargets(el.children, excludeIds, vertical, horizontal)\n }\n }\n}\n","/**\n * Selection math for the design-canvas editor: marquee hit-tests, keyboard\n * nudge deltas, and the duplicate offset constant. Pure functions — no DOM, no\n * Konva, no React.\n *\n * Marquee inclusion: by default any element whose AABB intersects the marquee\n * rect is included (the \"touch\" model). Pass `requireFullContainment: true` for\n * the \"surround\" model where the marquee must fully contain the element. Locked\n * and invisible elements are never selected regardless.\n */\n\nimport { boundsIntersect, elementAabb } from '../../design-canvas/model'\nimport type { Bounds, SceneElement, ScenePage } from '../../design-canvas/model'\n\n// ---------------------------------------------------------------------------\n// Point hit-test (model-space)\n// ---------------------------------------------------------------------------\n\n/**\n * Top-most selectable element whose AABB contains the document-space point, or\n * null when the point is over empty space.\n *\n * This is the authoritative \"is the pointer over an element?\" test for the\n * pointer-down gesture router. It runs against the scene model in document\n * coordinates rather than Konva's hit-graph canvas, so it is independent of\n * clip groups, listening flags, and hit-canvas redraw timing — all of which\n * could make `stage.getIntersection` misclassify a press as empty space and\n * silently start a marquee instead of an element drag.\n *\n * Z-order: later elements paint on top, so we scan the array in reverse and\n * return the first hit. Groups descend into children (top child wins); a hit on\n * a group child resolves to the group itself, matching drag/selection which\n * operate on the group as a unit. Locked and invisible elements never hit.\n */\nexport function hitTestPoint(page: ScenePage, x: number, y: number): string | null {\n return hitTestIn(page.elements, x, y, null)\n}\n\nfunction hitTestIn(\n elements: SceneElement[],\n x: number,\n y: number,\n resolveTo: string | null,\n): string | null {\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const el = elements[i]!\n if (!el.visible || el.locked) continue\n const aabb = elementAabb(el)\n if (!pointInBounds(aabb, x, y)) continue\n if (el.kind === 'group') {\n // A press anywhere inside the group AABB selects the group as a unit,\n // even over the gaps between children — the group is the draggable node.\n const child = hitTestIn(el.children, x, y, el.id)\n return child ?? el.id\n }\n return resolveTo ?? el.id\n }\n return null\n}\n\nfunction pointInBounds(b: Bounds, x: number, y: number): boolean {\n return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height\n}\n\n/** Define options to configure marquee selection behavior including containment requirements */\nexport interface MarqueeSelectOptions {\n /** When true, the element's AABB must be fully inside the marquee; default is\n * intersection (any overlap selects). */\n requireFullContainment?: boolean\n}\n\n/** Returns the ids of selectable elements on `page` whose AABB intersects (or\n * is contained by) `rect`. Locked and invisible elements are excluded. */\nexport function marqueeSelect(page: ScenePage, rect: Bounds, opts: MarqueeSelectOptions = {}): string[] {\n assertValidBounds(rect, 'marqueeSelect rect')\n const result: string[] = []\n collectMarqueeIds(page.elements, rect, opts.requireFullContainment ?? false, result)\n return result\n}\n\nfunction collectMarqueeIds(\n elements: SceneElement[],\n rect: Bounds,\n requireContainment: boolean,\n result: string[],\n): void {\n for (const el of elements) {\n if (!el.visible || el.locked) continue\n const aabb = elementAabb(el)\n\n if (el.kind === 'group') {\n // Groups are transparent to marquee: descend when the group AABB intersects\n // the rect, collecting children individually. The group entity itself is\n // never added — marquee selects leaf elements, not the group wrapper.\n if (boundsIntersect(rect, aabb)) {\n collectMarqueeIds(el.children, rect, requireContainment, result)\n }\n continue\n }\n\n const hit = requireContainment ? boundsContain(rect, aabb) : boundsIntersect(rect, aabb)\n if (hit) result.push(el.id)\n }\n}\n\n/** Returns true when `outer` fully contains `inner` (edges may touch). */\nfunction boundsContain(outer: Bounds, inner: Bounds): boolean {\n return (\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n inner.x + inner.width <= outer.x + outer.width &&\n inner.y + inner.height <= outer.y + outer.height\n )\n}\n\n// ---------------------------------------------------------------------------\n// Keyboard nudge\n// ---------------------------------------------------------------------------\n\n/** Pixels to move per nudge step at normal and shifted speed. */\nconst NUDGE_NORMAL_PX = 1\nconst NUDGE_SHIFT_PX = 10\n\n/** Represent horizontal and vertical displacement values for nudging elements */\nexport interface NudgeDelta {\n dx: number\n dy: number\n}\n\n/** Map an arrow key to a document-px delta. `shift` engages the 10× step.\n * Throws on unknown keys so callers handle only real nudge keys. */\nexport function nudgeDelta(key: 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown', shift: boolean): NudgeDelta {\n const step = shift ? NUDGE_SHIFT_PX : NUDGE_NORMAL_PX\n switch (key) {\n case 'ArrowLeft': return { dx: -step, dy: 0 }\n case 'ArrowRight': return { dx: step, dy: 0 }\n case 'ArrowUp': return { dx: 0, dy: -step }\n case 'ArrowDown': return { dx: 0, dy: step }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Duplicate offset\n// ---------------------------------------------------------------------------\n\n/** Document-px offset applied to duplicated elements so the copy is visually\n * separated from the original (matching common design-tool convention). */\nexport const DUPLICATE_OFFSET: NudgeDelta = { dx: 10, dy: 10 }\n\n// ---------------------------------------------------------------------------\n// Internal\n// ---------------------------------------------------------------------------\n\nfunction assertValidBounds(bounds: Bounds, label: string): void {\n if (!Number.isFinite(bounds.x) || !Number.isFinite(bounds.y) ||\n !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height)) {\n throw new Error(`${label}: all fields must be finite numbers`)\n }\n}\n","/**\n * Zoom + pan coordinate math for the design-canvas editor. Zoom is pixels-per-\n * document-px (e.g. 2 = 200% magnification). The key invariant for wheel-zoom\n * is that the document point under the cursor stays fixed in screen space:\n *\n * docPoint = (screenPoint - pan) / zoom\n * newPan = screenPoint - docPoint * newZoom\n *\n * This module is pure math — no DOM, no Konva, no React.\n */\n\nimport type { ZoomPanMath } from '../contracts'\n\n/** Define configuration options for minimum and maximum zoom levels in a zoom-pan interface */\nexport interface ZoomPanConfig {\n minZoom: number\n maxZoom: number\n}\n\n/** Create zoom and pan math utilities enforcing valid zoom range constraints */\nexport function createZoomPanMath(config: ZoomPanConfig): ZoomPanMath {\n const { minZoom, maxZoom } = config\n if (!Number.isFinite(minZoom) || minZoom <= 0) {\n throw new Error(`minZoom must be a positive finite number, got ${minZoom}`)\n }\n if (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {\n throw new Error(`maxZoom must be finite and greater than minZoom (${minZoom}), got ${maxZoom}`)\n }\n\n return {\n minZoom,\n maxZoom,\n\n zoomAtPoint(\n state: { zoom: number; panX: number; panY: number },\n factor: number,\n screenX: number,\n screenY: number,\n ): { zoom: number; panX: number; panY: number } {\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(`zoomAtPoint: factor must be a positive finite number, got ${factor}`)\n }\n if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) {\n throw new Error(`zoomAtPoint: screenX/screenY must be finite numbers`)\n }\n if (!Number.isFinite(state.zoom) || state.zoom <= 0) {\n throw new Error(`zoomAtPoint: state.zoom must be a positive finite number, got ${state.zoom}`)\n }\n\n const newZoom = Math.min(maxZoom, Math.max(minZoom, state.zoom * factor))\n\n // The document point under the cursor must remain fixed:\n // docX = (screenX - panX) / zoom\n // newPanX = screenX - docX * newZoom\n const docX = (screenX - state.panX) / state.zoom\n const docY = (screenY - state.panY) / state.zoom\n const panX = screenX - docX * newZoom\n const panY = screenY - docY * newZoom\n\n return { zoom: newZoom, panX, panY }\n },\n\n fitPage(\n page: { width: number; height: number },\n viewport: { width: number; height: number },\n paddingPx = 48,\n ): { zoom: number; panX: number; panY: number } {\n if (!Number.isFinite(page.width) || page.width <= 0) {\n throw new Error(`fitPage: page.width must be a positive finite number, got ${page.width}`)\n }\n if (!Number.isFinite(page.height) || page.height <= 0) {\n throw new Error(`fitPage: page.height must be a positive finite number, got ${page.height}`)\n }\n if (!Number.isFinite(viewport.width) || viewport.width <= 0) {\n throw new Error(`fitPage: viewport.width must be a positive finite number, got ${viewport.width}`)\n }\n if (!Number.isFinite(viewport.height) || viewport.height <= 0) {\n throw new Error(`fitPage: viewport.height must be a positive finite number, got ${viewport.height}`)\n }\n if (!Number.isFinite(paddingPx) || paddingPx < 0) {\n throw new Error(`fitPage: paddingPx must be a non-negative finite number, got ${paddingPx}`)\n }\n\n // Cap padding at 20% of the limiting viewport dimension. On tiny real\n // containers (composite mini-editor cells, thumbnail embeds) a fixed\n // 48px margin eats the canvas — a 135px slot would leave 39px and clamp\n // the fit at minZoom, rendering the page as a speck. Large viewports are\n // unaffected (cap ≥ 48 keeps the historical padding byte-identical).\n const pad = Math.min(paddingPx, Math.min(viewport.width, viewport.height) * 0.2)\n const availW = viewport.width - pad * 2\n const availH = viewport.height - pad * 2\n const zoom = Math.min(maxZoom, Math.max(minZoom, Math.min(availW / page.width, availH / page.height)))\n\n // Center the page in the viewport\n const panX = (viewport.width - page.width * zoom) / 2\n const panY = (viewport.height - page.height * zoom) / 2\n\n return { zoom, panX, panY }\n },\n\n documentToScreen(\n state: { zoom: number; panX: number; panY: number },\n x: number,\n y: number,\n ): { x: number; y: number } {\n return { x: x * state.zoom + state.panX, y: y * state.zoom + state.panY }\n },\n\n screenToDocument(\n state: { zoom: number; panX: number; panY: number },\n x: number,\n y: number,\n ): { x: number; y: number } {\n if (!Number.isFinite(state.zoom) || state.zoom === 0) {\n throw new Error(`screenToDocument: zoom must be a non-zero finite number, got ${state.zoom}`)\n }\n return { x: (x - state.panX) / state.zoom, y: (y - state.panY) / state.zoom }\n },\n }\n}\n","/**\n * Pure export math for the design-canvas surface — no Konva, no DOM, no React.\n *\n * All stage-interaction code (save/restore, node visibility, toDataURL) lives\n * in export.ts and delegates the decision math here so it can be tested\n * without a canvas environment.\n */\n\nimport type { ScenePage } from '../design-canvas/model'\nimport {\n bleedAwareExportBounds,\n scaleForPreset,\n type ExportCropRect,\n type ExportPreset,\n} from '../design-canvas/export-presets'\n\n// ---------------------------------------------------------------------------\n// Re-exports so callers only import from one place\n// ---------------------------------------------------------------------------\n\nexport type { ExportCropRect, ExportPreset }\nexport { bleedAwareExportBounds, scaleForPreset }\n\n// ---------------------------------------------------------------------------\n// Overlay / transformer node name predicates\n// ---------------------------------------------------------------------------\n\n/** Returns true for any Konva node that must be hidden during export. Nodes\n * whose names start with 'overlay:' are editor-only chrome (snap lines,\n * selection indicators, rulers, bleed guides). The transformer node is\n * identified by its canonical name 'Transformer'. */\nexport function isExportHiddenNodeName(name: string): boolean {\n return name.startsWith('overlay:') || name === 'Transformer'\n}\n\n// ---------------------------------------------------------------------------\n// Crop rect + pixel ratio resolution\n// ---------------------------------------------------------------------------\n\n/** Define parameters required to resolve export settings including crop, pixel ratio, type, and quality */\nexport interface ResolvedExportParams {\n cropRect: ExportCropRect\n pixelRatio: number\n mimeType: 'image/png' | 'image/jpeg'\n quality: number | undefined\n}\n\n/**\n * Resolve the full set of toDataURL params from a page, format, pixel ratio,\n * bleed flag, and optional preset. When a preset is supplied it overrides\n * `pixelRatio` and `includeBleed` — the explicit args are ignored for those\n * two values so the preset is the single source of truth.\n *\n * `quality` is set to 0.92 for jpeg, undefined for png (Konva ignores it).\n */\nexport function resolveExportParams(\n page: ScenePage,\n opts: {\n format: 'png' | 'jpeg'\n pixelRatio?: number\n includeBleed?: boolean\n preset?: ExportPreset\n },\n): ResolvedExportParams {\n const includeBleed = opts.preset !== undefined\n ? opts.preset.includeBleed\n : (opts.includeBleed ?? false)\n\n const cropRect = bleedAwareExportBounds(page, includeBleed)\n\n const pixelRatio = opts.preset !== undefined\n ? scaleForPreset(opts.preset, cropRect)\n : (opts.pixelRatio ?? 1)\n\n const format = opts.preset !== undefined ? opts.preset.format : opts.format\n const mimeType: 'image/png' | 'image/jpeg' = format === 'jpeg' ? 'image/jpeg' : 'image/png'\n\n return {\n cropRect,\n pixelRatio,\n mimeType,\n quality: format === 'jpeg' ? 0.92 : undefined,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Node bitmap-cache pixel ratio\n// ---------------------------------------------------------------------------\n\n/** Hard ceiling for node cache rasterization. Without it a deep zoom (max 32)\n * on a retina display would rasterize page-size elements into canvases beyond\n * browser backing-store limits; 4x supersampling covers every realistic\n * crispness-critical range (fit-page → 200% at dpr 2). */\nexport const NODE_CACHE_PIXEL_RATIO_MAX = 4\n\n/**\n * Pixel ratio a Konva node bitmap cache should rasterize at so the cached\n * bitmap matches the screen exactly: the node's absolute scale (content-group\n * zoom) × device pixel ratio. Konva's cache default ignores zoom, so the\n * bitmap is re-scaled at draw time — bilinear-soft at fit-page zooms like\n * 0.47, badly upscaled on zoom-in. ElementNode caches at this ratio and\n * export.ts restores cleared caches at it, keeping one policy in one place.\n */\nexport function resolveNodeCachePixelRatio(absoluteScale: number, devicePixelRatio: number): number {\n return Math.min(absoluteScale * devicePixelRatio, NODE_CACHE_PIXEL_RATIO_MAX)\n}\n\n// ---------------------------------------------------------------------------\n// CORS taint detection\n// ---------------------------------------------------------------------------\n\n/**\n * Walk image nodes to find the src of any that may have caused a SecurityError\n * when the stage called toDataURL. Returns the first offending src found, or\n * null if none can be identified.\n *\n * `imageSrcs` is a flat list of { name, src } records collected from all image\n * nodes on the stage before calling toDataURL — export.ts builds it before\n * attempting the export so that if the SecurityError fires we have the data.\n */\nexport function identifyTaintedSrc(imageSrcs: ReadonlyArray<{ name: string; src: string }>): string | null {\n // We cannot re-probe CORS from a caught SecurityError — the browser does not\n // tell us which src triggered it. Return the first src that looks like a\n // cross-origin resource the browser would taint: anything that is not a\n // same-origin relative path or a data: URL.\n for (const { src } of imageSrcs) {\n if (isCrossOriginSrc(src)) return src\n }\n return null\n}\n\n/**\n * Heuristic: a src is potentially cross-origin when it is an absolute http(s)\n * URL. Same-origin relative paths (/api/...) and data: blobs are safe.\n * This is intentionally conservative — the taint check runs only after a\n * SecurityError fires, so a false positive is \"blamed\" without being silently\n * ignored. NOTE: same-origin https:// assets will also match; if a\n * SecurityError fires, the cross-origin culprit may be a different image than\n * the one this function flags first.\n */\nexport function isCrossOriginSrc(src: string): boolean {\n return /^https?:\\/\\//i.test(src)\n}\n\n// ---------------------------------------------------------------------------\n// View state snapshot (used by save/restore helpers)\n// ---------------------------------------------------------------------------\n\n/** Minimal snapshot of stage view state that export.ts saves before mutating\n * zoom/pan/visibility and restores afterward. */\nexport interface ExportViewSnapshot {\n /** Stage scale factor before export. */\n scaleX: number\n scaleY: number\n /** Stage translation before export. */\n x: number\n y: number\n /** node name → prior visibility, for the nodes that were hidden. */\n hiddenNodeNames: string[]\n}\n\n/**\n * Build the toDataURL crop rect translated to stage output coordinates.\n * Konva's `stage.toDataURL({ x, y, width, height })` takes STAGE OUTPUT\n * coordinates, not document coordinates. When the stage has been scaled to fit\n * (zoom × stageScale), the crop rect in document px must be multiplied by the\n * combined scale to land on the right pixels.\n *\n * `stageScale` is the stage's current uniform scale (scaleX == scaleY);\n * `pixelRatio` is separate and handed directly to toDataURL — Konva multiplies\n * it internally when rendering to the backing canvas, so we must NOT include\n * it in the stage-coordinate rect.\n */\nexport function documentCropToStageCoords(\n cropRect: ExportCropRect,\n stageScale: number,\n stageX: number,\n stageY: number,\n): { x: number; y: number; width: number; height: number } {\n return {\n x: stageX + cropRect.x * stageScale,\n y: stageY + cropRect.y * stageScale,\n width: cropRect.width * stageScale,\n height: cropRect.height * stageScale,\n }\n}\n","/**\n * Pure builders for the human \"add element to the canvas\" path — the math and\n * `SceneOperation` construction behind {@link CanvasInsertPanel}, separated from\n * the React/DOM layer so every insert is unit-testable without Konva or a\n * browser. The panel calls these to produce `add_element` operations and hands\n * them to the host's `onApplyOperations` (server-validated, undoable) — the same\n * pipeline every other edit flows through.\n *\n * Media boundary: image inserts must pass {@link assertSceneMediaSrc} (remote\n * http(s) or a rooted `/api/` path — never `data:` blobs or sandbox-local\n * files). Builders that take a src assert it up front so a bad url fails here,\n * not deep in the apply layer.\n */\n\nimport { assertSceneMediaSrc } from '../design-canvas/model'\nimport type {\n EllipseElement,\n ImageElement,\n RectElement,\n SceneElement,\n TextElement,\n} from '../design-canvas/model'\nimport type { SceneOperation } from '../design-canvas/operations'\n\n/** Largest dimension (document px) a freshly inserted element is fitted to,\n * before the page-size cap applies. Keeps a huge upload from filling the page. */\nexport const MAX_INSERT_DIMENSION = 600\n\n/** Active page geometry an insert lands into — drives centering and fitting. */\nexport interface InsertPageGeometry {\n pageId: string\n width: number\n height: number\n background?: string\n}\n\n/** Mint a DOM-safe element id. Prefers `crypto.randomUUID`; falls back to a\n * time+random id where crypto is unavailable (older runtimes, some test envs). */\nexport function mintElementId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n return `el-${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n\n/** Fit (naturalW, naturalH) under a max-dimension cap (further bounded by the\n * page), preserving aspect ratio. Never upscales past natural size. */\nexport function fittedSize(\n naturalW: number,\n naturalH: number,\n pageWidth: number,\n pageHeight: number,\n): { width: number; height: number } {\n const cap = Math.min(MAX_INSERT_DIMENSION, pageWidth * 0.8, pageHeight * 0.8)\n const longest = Math.max(naturalW, naturalH)\n const scale = naturalW > 0 && naturalH > 0 ? Math.min(1, cap / longest) : 1\n const width = Math.max(1, Math.round((naturalW || cap) * scale))\n const height = Math.max(1, Math.round((naturalH || cap) * scale))\n return { width, height }\n}\n\n/** Top-left position that centers a (width, height) box on the page. */\nexport function centeredPosition(\n width: number,\n height: number,\n pageWidth: number,\n pageHeight: number,\n): { x: number; y: number } {\n return {\n x: Math.round((pageWidth - width) / 2),\n y: Math.round((pageHeight - height) / 2),\n }\n}\n\n/** Common element fields with a freshly minted id and unrotated/visible defaults. */\nfunction baseAttrs(name: string, x: number, y: number) {\n return { id: mintElementId(), name, x, y, rotation: 0, opacity: 1, locked: false, visible: true }\n}\n\n/** Wrap a single element in an `add_element` op for the active page. */\nfunction addElementOp(pageId: string, element: SceneElement): SceneOperation {\n return { type: 'add_element', pageId, element }\n}\n\nfunction hexLuminance(color: string | undefined): number | null {\n if (!color) return null\n const raw = color.trim()\n const hex = raw.startsWith('#') ? raw.slice(1) : ''\n const full = hex.length === 3\n ? hex.split('').map((ch) => ch + ch).join('')\n : hex.length === 6\n ? hex\n : ''\n if (!/^[0-9a-fA-F]{6}$/.test(full)) return null\n const channels = [0, 2, 4].map((start) => parseInt(full.slice(start, start + 2), 16) / 255)\n const [r, g, b] = channels.map((value) => (\n value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4)\n ))\n return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!\n}\n\nfunction pageTextFill(page: InsertPageGeometry, tone: 'primary' | 'secondary'): string {\n const luminance = hexLuminance(page.background)\n if (luminance !== null && luminance < 0.35) {\n return tone === 'primary' ? '#f8fafc' : '#cbd5e1'\n }\n return tone === 'primary' ? '#111827' : '#374151'\n}\n\n/**\n * Build an `add_element` op placing an image, fitted and centered on the page.\n * `naturalSize` is the probed image dimensions (the panel reads them in the\n * browser); pass `{ width: 0, height: 0 }` when unknown to fall back to the cap.\n *\n * Throws (via `assertSceneMediaSrc`) when `src` is not http(s) or a rooted\n * `/api/` path — callers should surface the error, never insert a `data:` src.\n */\nexport function buildInsertImageOp(\n src: string,\n naturalSize: { width: number; height: number },\n page: InsertPageGeometry,\n): SceneOperation {\n assertSceneMediaSrc(src, 'image src')\n const { width, height } = fittedSize(naturalSize.width, naturalSize.height, page.width, page.height)\n const { x, y } = centeredPosition(width, height, page.width, page.height)\n const element: ImageElement = {\n ...baseAttrs('Image', x, y),\n kind: 'image',\n width,\n height,\n src,\n fit: 'contain',\n }\n return addElementOp(page.pageId, element)\n}\n\n/** A template the insert panel can drop without the agent. `build` is pure and\n * produces the operations for the active page geometry. */\nexport interface InsertTemplate {\n id: string\n label: string\n build(page: InsertPageGeometry): SceneOperation[]\n}\n\n/** The built-in starter templates (heading, body text, rectangle, ellipse).\n * Consumers can pass their own list to {@link CanvasInsertPanel}; this is the\n * default so every canvas gets a usable set out of the box. */\nexport const DEFAULT_INSERT_TEMPLATES: readonly InsertTemplate[] = [\n {\n id: 'heading',\n label: 'Heading',\n build(page) {\n const width = Math.min(480, Math.round(page.width * 0.7))\n const { x, y } = centeredPosition(width, 60, page.width, page.height)\n const element: TextElement = {\n ...baseAttrs('Heading', x, y),\n kind: 'text',\n text: 'Add a headline',\n width,\n fontFamily: 'Inter',\n fontSize: 48,\n fontStyle: 'bold',\n fill: pageTextFill(page, 'primary'),\n align: 'left',\n lineHeight: 1.1,\n letterSpacing: 0,\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n {\n id: 'body',\n label: 'Body text',\n build(page) {\n const width = Math.min(420, Math.round(page.width * 0.6))\n const { x, y } = centeredPosition(width, 80, page.width, page.height)\n const element: TextElement = {\n ...baseAttrs('Body', x, y),\n kind: 'text',\n text: 'Add a paragraph of supporting copy.',\n width,\n fontFamily: 'Inter',\n fontSize: 20,\n fontStyle: 'normal',\n fill: pageTextFill(page, 'secondary'),\n align: 'left',\n lineHeight: 1.4,\n letterSpacing: 0,\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n {\n id: 'rect',\n label: 'Rectangle',\n build(page) {\n const width = Math.min(320, Math.round(page.width * 0.4))\n const height = Math.min(200, Math.round(page.height * 0.3))\n const { x, y } = centeredPosition(width, height, page.width, page.height)\n const element: RectElement = {\n ...baseAttrs('Rectangle', x, y),\n kind: 'rect',\n width,\n height,\n fill: '#6366f1',\n cornerRadius: 12,\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n {\n id: 'ellipse',\n label: 'Ellipse',\n build(page) {\n const size = Math.min(220, Math.round(Math.min(page.width, page.height) * 0.3))\n const { x, y } = centeredPosition(size, size, page.width, page.height)\n const element: EllipseElement = {\n ...baseAttrs('Ellipse', x, y),\n kind: 'ellipse',\n width: size,\n height: size,\n fill: '#10b981',\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n]\n"],"mappings":"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","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
DesignCanvas,
|
|
5
5
|
PlusGlyph,
|
|
6
6
|
ShapesGlyph
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FNM7JYME.js";
|
|
8
8
|
import {
|
|
9
9
|
lightTheme
|
|
10
10
|
} from "./chunk-LSQR6HM6.js";
|
|
@@ -18,8 +18,9 @@ import {
|
|
|
18
18
|
identifyTaintedSrc,
|
|
19
19
|
isExportHiddenNodeName,
|
|
20
20
|
marqueeSelect,
|
|
21
|
-
resolveExportParams
|
|
22
|
-
|
|
21
|
+
resolveExportParams,
|
|
22
|
+
resolveNodeCachePixelRatio
|
|
23
|
+
} from "./chunk-OYTMILRK.js";
|
|
23
24
|
import {
|
|
24
25
|
addElementCommand,
|
|
25
26
|
createSceneCommandStack,
|
|
@@ -48,6 +49,26 @@ import {
|
|
|
48
49
|
import { Stage, Layer as Layer4, Rect as Rect2, Group as Group3 } from "react-konva";
|
|
49
50
|
|
|
50
51
|
// src/design-canvas-react/export.ts
|
|
52
|
+
function clearNodeCaches(stage) {
|
|
53
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
54
|
+
const cleared = [];
|
|
55
|
+
const walk = (node) => {
|
|
56
|
+
if (node.isCached()) {
|
|
57
|
+
cleared.push({ node, pixelRatio: resolveNodeCachePixelRatio(node.getAbsoluteScale().x, dpr) });
|
|
58
|
+
node.clearCache();
|
|
59
|
+
}
|
|
60
|
+
for (const child of node.getChildren?.() ?? []) walk(child);
|
|
61
|
+
};
|
|
62
|
+
for (const layer of stage.getLayers()) {
|
|
63
|
+
for (const child of layer.getChildren()) walk(child);
|
|
64
|
+
}
|
|
65
|
+
return cleared;
|
|
66
|
+
}
|
|
67
|
+
function restoreNodeCaches(cleared) {
|
|
68
|
+
for (const { node, pixelRatio } of cleared) {
|
|
69
|
+
node.cache({ pixelRatio });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
51
72
|
async function exportPageDataUrl(stage, page, opts) {
|
|
52
73
|
const { cropRect, pixelRatio, mimeType, quality } = resolveExportParams(page, opts);
|
|
53
74
|
const imageSrcs = collectImageSrcs(stage);
|
|
@@ -63,6 +84,7 @@ async function exportPageDataUrl(stage, page, opts) {
|
|
|
63
84
|
}
|
|
64
85
|
const stageScale = stage.scaleX();
|
|
65
86
|
const stageCrop = documentCropToStageCoords(cropRect, stageScale, stage.x(), stage.y());
|
|
87
|
+
const clearedCaches = clearNodeCaches(stage);
|
|
66
88
|
let dataUrl;
|
|
67
89
|
try {
|
|
68
90
|
dataUrl = stage.toDataURL({
|
|
@@ -78,6 +100,7 @@ async function exportPageDataUrl(stage, page, opts) {
|
|
|
78
100
|
for (const node of hiddenNodes) {
|
|
79
101
|
node.visible(true);
|
|
80
102
|
}
|
|
103
|
+
restoreNodeCaches(clearedCaches);
|
|
81
104
|
if (err instanceof Error && err.name === "SecurityError") {
|
|
82
105
|
const taintedSrc = identifyTaintedSrc(imageSrcs);
|
|
83
106
|
if (taintedSrc !== null) {
|
|
@@ -94,6 +117,7 @@ async function exportPageDataUrl(stage, page, opts) {
|
|
|
94
117
|
for (const node of hiddenNodes) {
|
|
95
118
|
node.visible(true);
|
|
96
119
|
}
|
|
120
|
+
restoreNodeCaches(clearedCaches);
|
|
97
121
|
return dataUrl;
|
|
98
122
|
}
|
|
99
123
|
function collectImageSrcs(stage) {
|
|
@@ -174,7 +198,7 @@ function CanvasEmptyState({
|
|
|
174
198
|
className: `pointer-events-none absolute inset-0 z-20 flex items-center justify-center p-4 ${className ?? ""}`,
|
|
175
199
|
role: "group",
|
|
176
200
|
"aria-label": "Start your design",
|
|
177
|
-
children: /* @__PURE__ */ jsxs("div", { className: "pointer-events-auto flex w-full max-w-lg flex-col items-center gap-5 rounded-xl border border-[var(--border-default)] bg-[var(--bg-input)]
|
|
201
|
+
children: /* @__PURE__ */ jsxs("div", { className: "pointer-events-auto flex w-full max-w-lg flex-col items-center gap-5 rounded-xl border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-input)_95%,transparent)] px-6 py-7 text-center shadow-xl backdrop-blur sm:px-8", children: [
|
|
178
202
|
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-[var(--text-muted)]", children: [
|
|
179
203
|
/* @__PURE__ */ jsx(BrandKnot, { size: 22, className: "shrink-0" }),
|
|
180
204
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium uppercase tracking-[0.14em]", children: "Tangle Design" })
|
|
@@ -190,7 +214,7 @@ function CanvasEmptyState({
|
|
|
190
214
|
onClick,
|
|
191
215
|
className: "group flex min-h-[44px] flex-col items-center gap-2 rounded-lg border border-[var(--border-default)] bg-[hsl(var(--card))] px-3 py-4 text-center transition-colors hover:border-[var(--brand-primary)]",
|
|
192
216
|
children: [
|
|
193
|
-
/* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 items-center justify-center rounded-full bg-[var(--brand-primary)]
|
|
217
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] text-[var(--brand-primary)] transition-colors group-hover:bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)]", children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" }) }),
|
|
194
218
|
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium text-[var(--text-primary)]", children: label }),
|
|
195
219
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] leading-4 text-[var(--text-muted)]", children: hint })
|
|
196
220
|
]
|
|
@@ -392,7 +416,8 @@ function useNodeCache(ref, isSelected, dragging, zoom, invalidateKey) {
|
|
|
392
416
|
node.clearCache();
|
|
393
417
|
return;
|
|
394
418
|
}
|
|
395
|
-
|
|
419
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
420
|
+
node.cache({ pixelRatio: resolveNodeCachePixelRatio(zoom, dpr) });
|
|
396
421
|
return () => {
|
|
397
422
|
node.clearCache();
|
|
398
423
|
};
|
|
@@ -1032,7 +1057,9 @@ function WorkspaceView({
|
|
|
1032
1057
|
const snapEngine = useMemo(() => createSnapEngine(), []);
|
|
1033
1058
|
const containerRef = useRef4(null);
|
|
1034
1059
|
const [containerSize, setContainerSize] = useState2({ width: 800, height: 600 });
|
|
1035
|
-
const
|
|
1060
|
+
const autoFitRef = useRef4(true);
|
|
1061
|
+
const lastAutoFitRef = useRef4(null);
|
|
1062
|
+
const readyRef = useRef4(false);
|
|
1036
1063
|
useLayoutEffect(() => {
|
|
1037
1064
|
const el = containerRef.current;
|
|
1038
1065
|
if (!el) return;
|
|
@@ -1041,11 +1068,19 @@ function WorkspaceView({
|
|
|
1041
1068
|
if (!entry) return;
|
|
1042
1069
|
const { width, height } = entry.contentRect;
|
|
1043
1070
|
setContainerSize({ width, height });
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1071
|
+
if (width <= 0 || height <= 0) return;
|
|
1072
|
+
const view = stack.getState();
|
|
1073
|
+
const last = lastAutoFitRef.current;
|
|
1074
|
+
if (last && (view.zoom !== last.zoom || view.panX !== last.panX || view.panY !== last.panY)) {
|
|
1075
|
+
autoFitRef.current = false;
|
|
1076
|
+
}
|
|
1077
|
+
if (autoFitRef.current && fitOnMount && activePage.width > 0 && activePage.height > 0 && (!last || last.width !== width || last.height !== height)) {
|
|
1078
|
+
const fit = zoomPanMath.fitPage(activePage, { width, height });
|
|
1079
|
+
stack.setView(fit);
|
|
1080
|
+
lastAutoFitRef.current = { ...fit, width, height };
|
|
1081
|
+
}
|
|
1082
|
+
if (!readyRef.current) {
|
|
1083
|
+
readyRef.current = true;
|
|
1049
1084
|
onReady?.();
|
|
1050
1085
|
}
|
|
1051
1086
|
});
|
|
@@ -1538,7 +1573,7 @@ function WorkspaceView({
|
|
|
1538
1573
|
return /* @__PURE__ */ jsxs3(
|
|
1539
1574
|
"div",
|
|
1540
1575
|
{
|
|
1541
|
-
className: `design-canvas-workspace relative overflow-hidden bg-[var(--canvas-backdrop,#1a1a1a)] focus-visible:[outline-offset:-2px] ${className ?? ""}`,
|
|
1576
|
+
className: `design-canvas-workspace relative h-full w-full overflow-hidden bg-[var(--canvas-backdrop,#1a1a1a)] focus-visible:[outline-offset:-2px] ${className ?? ""}`,
|
|
1542
1577
|
ref: containerRef,
|
|
1543
1578
|
tabIndex: 0,
|
|
1544
1579
|
onWheel: handleWheel,
|
|
@@ -1660,8 +1695,10 @@ function WorkspaceView({
|
|
|
1660
1695
|
normalizedMarquee && /* @__PURE__ */ jsx7(
|
|
1661
1696
|
"div",
|
|
1662
1697
|
{
|
|
1663
|
-
className: "pointer-events-none absolute border
|
|
1698
|
+
className: "pointer-events-none absolute border",
|
|
1664
1699
|
style: {
|
|
1700
|
+
borderColor: render.selectionStroke,
|
|
1701
|
+
backgroundColor: `color-mix(in srgb, ${render.selectionStroke} 10%, transparent)`,
|
|
1665
1702
|
left: panX + normalizedMarquee.x * zoom,
|
|
1666
1703
|
top: panY + normalizedMarquee.y * zoom,
|
|
1667
1704
|
width: normalizedMarquee.width * zoom,
|
|
@@ -1911,4 +1948,4 @@ export {
|
|
|
1911
1948
|
Workspace,
|
|
1912
1949
|
DesignCanvasEditor
|
|
1913
1950
|
};
|
|
1914
|
-
//# sourceMappingURL=chunk-
|
|
1951
|
+
//# sourceMappingURL=chunk-PURHUAOR.js.map
|