stuntdouble 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -9
- package/dist/{chunk-VWTYG7G4.js → chunk-BFI6P5DO.js} +65 -25
- package/dist/chunk-BFI6P5DO.js.map +1 -0
- package/dist/chunk-DFXASIGV.js +431 -0
- package/dist/chunk-DFXASIGV.js.map +1 -0
- package/dist/{chunk-JAB46QV7.js → chunk-F3R3QL7V.js} +64 -8
- package/dist/chunk-F3R3QL7V.js.map +1 -0
- package/dist/{mcp-CYPMT37O.js → chunk-FMZTT2MC.js} +72 -1059
- package/dist/chunk-FMZTT2MC.js.map +1 -0
- package/dist/chunk-MSV5OLSY.js +117 -0
- package/dist/chunk-MSV5OLSY.js.map +1 -0
- package/dist/chunk-P3CI4FTE.js +469 -0
- package/dist/chunk-P3CI4FTE.js.map +1 -0
- package/dist/chunk-QDA5FTUH.js +79 -0
- package/dist/chunk-QDA5FTUH.js.map +1 -0
- package/dist/chunk-XHSDRB3X.js +996 -0
- package/dist/chunk-XHSDRB3X.js.map +1 -0
- package/dist/cli.js +180 -68
- package/dist/cli.js.map +1 -1
- package/dist/cloud-DLLNKYND.js +34 -0
- package/dist/credentials-QHESDY4K.js +27 -0
- package/dist/credentials-QHESDY4K.js.map +1 -0
- package/dist/doctor-ZAYOOCPT.js +13 -0
- package/dist/doctor-ZAYOOCPT.js.map +1 -0
- package/dist/files-UBPF7PKW.js +45 -0
- package/dist/files-UBPF7PKW.js.map +1 -0
- package/dist/mcp-HFIAQ6HP.js +133 -0
- package/dist/mcp-HFIAQ6HP.js.map +1 -0
- package/dist/open-browser-CTOGH5HQ.js +17 -0
- package/dist/open-browser-CTOGH5HQ.js.map +1 -0
- package/dist/run-MPIAR3LQ.js +20 -0
- package/dist/run-MPIAR3LQ.js.map +1 -0
- package/dist/runner-E45EJ76B.js +17 -0
- package/dist/runner-E45EJ76B.js.map +1 -0
- package/dist/studio-FBC66BER.js +360 -0
- package/dist/studio-FBC66BER.js.map +1 -0
- package/dist/studio.js +1621 -0
- package/dist/ui-server-C7CFJWRD.js +12 -0
- package/dist/ui-server-C7CFJWRD.js.map +1 -0
- package/dist/ui.html +210 -103
- package/package.json +2 -2
- package/dist/chunk-JAB46QV7.js.map +0 -1
- package/dist/chunk-VWTYG7G4.js.map +0 -1
- package/dist/mcp-CYPMT37O.js.map +0 -1
- package/dist/ui-server-AYIR7DTN.js +0 -10
- /package/dist/{ui-server-AYIR7DTN.js.map → cloud-DLLNKYND.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/files.ts","../src/vendor/scoring/conditions.ts","../src/vendor/scoring/failure.ts","../src/vendor/scoring/scoring.ts"],"sourcesContent":["// The files a project keeps under `.stuntdouble/`: their schemas, where they\n// live, and writes that never leave a half-written file behind.\n//\n// Actors and checklists are the hosted MCP's `create_actor` and\n// `create_checklist` inputs with slugs in place of ids, so the same files push\n// to the cloud unchanged, except `browser_config`, which the hosted inputs do\n// not take yet. They are strict: a misspelt key is an error naming the field\n// rather than a setting silently ignored.\n\nimport { randomUUID } from 'node:crypto';\nimport { appendFile, lstat, mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join, sep } from 'node:path';\nimport { z } from 'zod';\n\nimport { CHECKLIST_RUN_CONDITIONS } from './vendor/scoring/conditions.ts';\nimport { CHECKLIST_FAILURE_ACTIONS } from './vendor/scoring/failure.ts';\nimport {\n CHECKLIST_CHECK_TYPES,\n CHECKLIST_SCORE_TYPES,\n MAX_CHECK_WEIGHT,\n MAX_POINTS_SCORE,\n MAX_SCORE_BAND_LABEL_LENGTH,\n MAX_SCORE_BANDS,\n MAX_SCORE_UNIT_LENGTH,\n MIN_CHECK_WEIGHT,\n} from './vendor/scoring/scoring.ts';\n\nexport const projectDir = (): string => process.env.STUNTDOUBLE_PROJECT_DIR || process.cwd();\nexport const sdDir = (project: string): string => join(project, '.stuntdouble');\n\nconst slug = z\n .string()\n .regex(\n /^[a-z0-9][a-z0-9-]*$/,\n 'Use lowercase letters, digits and hyphens, e.g. \"guest-checkout\"'\n );\n/** Ids written back after a push to the cloud. Never read locally. */\nconst cloud = z.record(z.string(), z.unknown()).nullable().optional();\n\nexport const ConfigSchema = z.strictObject({\n format: z.literal('stuntdouble/1'),\n /** Local runs swap the checklist URL's origin for this one. */\n local_base_url: z\n .url({ protocol: /^https?$/ })\n .nullable()\n .optional(),\n headless: z.boolean().default(false),\n /** Whether screenshots go to the model on every action or only from `screenshot`. */\n model_screenshots: z.enum(['on_request', 'every_action']).default('on_request'),\n /** `third_party`: no paying, signing up or secrets on hosts other than the app under test. */\n safety: z.enum(['third_party', 'off']).default('third_party'),\n cloud,\n});\nexport type Config = z.infer<typeof ConfigSchema>;\n\n// The hosted update_actor and add_actor_knowledge limits, so a push is never\n// refused for a file that runs locally.\nconst ACTOR_INSTRUCTIONS_MAX_LENGTH = 1000;\nconst ACTOR_KNOWLEDGE_TITLE_MAX_LENGTH = 120;\nconst ACTOR_KNOWLEDGE_CONTENT_MAX_LENGTH = 8000;\n\nexport const ActorSchema = z.strictObject({\n name: z.string().min(1),\n description: z.string().optional(),\n system_prompt: z.string().max(ACTOR_INSTRUCTIONS_MAX_LENGTH).optional(),\n knowledge: z\n .array(\n z.strictObject({\n kind: z.enum(['expertise', 'process', 'reference', 'account']).default('reference'),\n title: z.string().min(1).max(ACTOR_KNOWLEDGE_TITLE_MAX_LENGTH),\n content: z.string().min(1).max(ACTOR_KNOWLEDGE_CONTENT_MAX_LENGTH),\n })\n )\n .default([]),\n /** Keep this actor's cookies between runs, in ~/.stuntdouble/profiles/<actor>. */\n persist_login: z.boolean().default(false),\n cloud,\n});\nexport type Actor = z.infer<typeof ActorSchema>;\n\nconst CheckSchema = z.strictObject({\n key: slug,\n description: z.string().min(1),\n type: z.enum(CHECKLIST_CHECK_TYPES).default('pass_fail'),\n weight: z.number().min(MIN_CHECK_WEIGHT).max(MAX_CHECK_WEIGHT).optional(),\n run_condition: z.enum(CHECKLIST_RUN_CONDITIONS).default('always'),\n on_failure: z.enum(CHECKLIST_FAILURE_ACTIONS).default('continue'),\n});\nexport type Check = z.infer<typeof CheckSchema>;\n\nexport const ChecklistSchema = z.strictObject({\n name: z.string().min(1).max(200),\n actor: slug,\n url: z.url({ protocol: /^https?$/ }),\n instructions: z.string().min(1),\n checks: z\n .array(CheckSchema)\n .min(1)\n .refine((checks) => new Set(checks.map((c) => c.key)).size === checks.length, {\n message: 'Every check needs its own key',\n }),\n pass_threshold: z.number().min(0).max(MAX_POINTS_SCORE).optional(),\n score_type: z.enum(CHECKLIST_SCORE_TYPES).default('percent'),\n score_unit: z.string().max(MAX_SCORE_UNIT_LENGTH).nullable().optional(),\n score_bands: z\n .array(\n z.strictObject({\n label: z.string().min(1).max(MAX_SCORE_BAND_LABEL_LENGTH),\n min: z.number().min(0).max(MAX_POINTS_SCORE),\n })\n )\n .max(MAX_SCORE_BANDS)\n .optional(),\n browser_config: z\n .strictObject({\n preset: z.enum(['desktop', 'tablet', 'mobile']).optional(),\n viewport: z\n .strictObject({\n width: z.int().min(320).max(2560),\n height: z.int().min(320).max(2560),\n })\n .optional(),\n touch: z.boolean().optional(),\n locale: z.string().optional(),\n })\n .optional(),\n // Cloud run settings, accepted so the file stays a valid hosted input. Unused locally.\n record_session: z.boolean().optional(),\n run_mode: z.enum(['code', 'agentic']).optional(),\n cloud,\n});\nexport type Checklist = z.infer<typeof ChecklistSchema>;\n\nexport const RUN_STATUSES = ['running', 'completed', 'stopped', 'interrupted', 'failed'] as const;\nexport const RESULT_STATUSES = ['passed', 'failed', 'informational', 'error', 'skipped'] as const;\n\nconst LocatorSchema = z.strictObject({\n text: z.string().optional(),\n selector: z.string().optional(),\n});\nexport type Locator = z.infer<typeof LocatorSchema>;\n\nconst ResultSchema = z.object({\n check: z.string(),\n status: z.enum(RESULT_STATUSES),\n evidence: z.string(),\n locator: LocatorSchema.optional(),\n screenshot: z.string().optional(),\n at: z.string().optional(),\n});\nexport type Result = z.infer<typeof ResultSchema>;\n\nexport const RunSchema = z.object({\n format: z.literal('stuntdouble.run/1'),\n id: z.string(),\n runner: z.object({\n kind: z.enum(['local', 'cloud']),\n package: z.string().optional(),\n /** False for a local run: the actor judged its own verdicts. */\n judged: z.boolean(),\n run_id: z.string().optional(),\n url: z.string().optional(),\n }),\n status: z.enum(RUN_STATUSES),\n pid: z.number().optional(),\n url: z.string(),\n started_at: z.string(),\n finished_at: z.string().nullable(),\n checklist: z.string(),\n actor: z.string(),\n checklist_snapshot: z.object({ checks: z.array(CheckSchema) }).loose(),\n results: z.array(ResultSchema),\n score: z\n .object({\n score: z.number().nullable(),\n outcome: z.enum(['passed', 'failed', 'inconclusive']),\n // A local run writes all three; a cloud run written by hand may leave them out.\n unit: z.string().nullish(),\n pass_threshold: z.number().nullish(),\n grade: z.string().nullish(),\n })\n .nullable(),\n summary: z.string().nullable(),\n cloud: z.unknown().nullable(),\n});\nexport type Run = z.infer<typeof RunSchema>;\n\n/** Every problem in a file, one per line, as `file: field.path: message`. */\nexport function describeIssues(file: string, error: z.ZodError): string {\n return error.issues\n .map((issue) => `${file}: ${issue.path.join('.') || '(top level)'}: ${issue.message}`)\n .join('\\n');\n}\n\n/** Read and validate a JSON file, or throw an error that says exactly what to fix. */\nexport async function readJson<S extends z.ZodType>(\n path: string,\n schema: S,\n label = path\n): Promise<z.infer<S>> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf8');\n } catch {\n throw new Error(`${label}: file not found`);\n }\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n throw new Error(`${label}: not valid JSON (${(err as Error).message})`, { cause: err });\n }\n const parsed = schema.safeParse(data);\n if (!parsed.success) throw new Error(describeIssues(label, parsed.error));\n return parsed.data;\n}\n\n/** Write to a temporary file, then rename over the target, so a reader never sees half a file. */\nexport async function writeJsonAtomic(path: string, data: unknown): Promise<void> {\n await writeFileAtomic(path, `${JSON.stringify(data, null, 2)}\\n`);\n}\n\nexport async function writeFileAtomic(path: string, data: string | Uint8Array): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await refuseLinks(path);\n // Unique per write: two writes to one path at once must not share a temp file.\n const tmp = `${path}.${randomUUID()}.tmp`;\n await writeFile(tmp, data, { mode: 0o600 });\n await rename(tmp, path);\n}\n\nexport async function appendJsonl(path: string, line: unknown): Promise<void> {\n await refuseLinks(path);\n await appendFile(path, `${JSON.stringify(line)}\\n`);\n}\n\n/**\n * Refuse a write through a symlink inside `.stuntdouble/`. Run folders are\n * committed and cloned, so a repo could ship `activity.jsonl -> ~/.ssh/...`.\n */\n// ponytail: checks the segments after the last `.stuntdouble`; a path without one is not checked.\nasync function refuseLinks(path: string): Promise<void> {\n const parts = path.split(sep);\n const from = parts.lastIndexOf('.stuntdouble');\n if (from < 0) return;\n for (let i = from + 1; i <= parts.length; i++) {\n const info = await lstat(parts.slice(0, i).join(sep)).catch(() => null);\n if (info?.isSymbolicLink())\n throw new Error(`Refusing to write through a symlink: ${parts.slice(0, i).join(sep)}`);\n }\n}\n\n/** Whether a process is still running. EPERM means it is, under another user. */\nexport function isAlive(pid: number | undefined): boolean {\n if (!pid) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** A run left `running` by a process that has gone is an interrupted run. */\nexport const effectiveStatus = (run: Run): Run['status'] =>\n run.status === 'running' && !isAlive(run.pid) ? 'interrupted' : run.status;\n\nexport type RunEntry = { id: string; run: Run } | { id: string; error: string };\n\n/** Every run folder under `runs/`, newest first. A run.json that does not parse says why. */\nexport async function readRuns(root: string): Promise<RunEntry[]> {\n const ids = await readdir(join(root, 'runs')).catch(() => [] as string[]);\n const entries = await Promise.all(\n ids\n .filter((id) => !id.startsWith('.'))\n .map(async (id): Promise<RunEntry> => {\n try {\n return { id, run: await readJson(join(root, 'runs', id, 'run.json'), RunSchema) };\n } catch (err) {\n return { id, error: (err as Error).message };\n }\n })\n );\n return entries.sort((a, b) => (a.id < b.id ? 1 : -1));\n}\n","// Vendored verbatim from the Stunt Double monorepo, packages/platform/src/checklists/conditions.ts\n// at ee0ccb43c, so a local run is scored by exactly the code that scores a cloud run. Do not\n// edit it here: change the source and copy it again. ponytail: a copy until the scoring is\n// published as @stunt-double/checklist, which both sides can then import.\n\n/**\n * Whether a check runs at all, given how the check before it turned out.\n *\n * Every check in a checklist used to run, always. That is the wrong shape for\n * the two things people actually write down:\n *\n * - \"the order confirmation shows the right total\" only means anything once\n * \"the payment completes\" passed. Running it regardless files a second\n * failure that is really the first one reported twice, and a report full of\n * knock-on failures buries the one that matters.\n * - \"the error message explains what to fix\" is only reachable when the step\n * before it failed. Running it on a healthy product fails a check for the\n * crime of the product working.\n *\n * So a check carries a `run_condition`, judged against the result of the check\n * immediately before it in `sort_order`. A check whose condition is unmet is\n * recorded as `skipped`: not passed, not failed, and deliberately not an error,\n * because nothing went wrong and nothing was learned.\n *\n * **The decisive rule is what counts as unmet.** Only a verdict that\n * contradicts the condition skips a check. An antecedent that errored, or that\n * is informational and therefore has no verdict at all, leaves the condition\n * undecidable, and an undecidable condition **runs** the check. Skipping there\n * would silently drop coverage on the strength of an authoring mistake or an\n * infrastructure blip, and silently testing less is the one outcome a testing\n * product must never produce. A skipped antecedent is different: it is a\n * definite \"did not pass\", so the chain propagates and a dead branch stays dead.\n *\n * Pure module: no runtime imports, so `node --test` (which resolves exact paths\n * only) can load it directly.\n */\n\nimport type { CheckResultStatus } from './scoring.ts';\n\nexport const CHECKLIST_RUN_CONDITIONS = [\n 'always',\n 'if_previous_passed',\n 'if_previous_failed',\n] as const;\n\nexport type ChecklistRunCondition = (typeof CHECKLIST_RUN_CONDITIONS)[number];\n\nexport function isChecklistRunCondition(value: unknown): value is ChecklistRunCondition {\n return (\n typeof value === 'string' && (CHECKLIST_RUN_CONDITIONS as readonly string[]).includes(value)\n );\n}\n\n/**\n * Falls back to `always` for null, an unknown value, or a lagging schema cache,\n * which is what every check did before conditions existed. A row that cannot be\n * read must keep behaving the way it always has, and \"always\" is the reading\n * that never drops a check.\n */\nexport function resolveRunCondition(value: unknown): ChecklistRunCondition {\n return isChecklistRunCondition(value) ? value : 'always';\n}\n\n/** Short labels, so the editor, the report and the run agree on the words. */\nexport const RUN_CONDITION_LABELS: Record<ChecklistRunCondition, string> = {\n always: 'Always',\n if_previous_passed: 'If previous passed',\n if_previous_failed: 'If previous failed',\n};\n\nexport const RUN_CONDITION_DESCRIPTIONS: Record<ChecklistRunCondition, string> = {\n always: 'Runs every time, whatever the check before it found.',\n if_previous_passed: 'Runs only when the check before it passed. Otherwise it is skipped.',\n if_previous_failed: 'Runs only when the check before it failed. Otherwise it is skipped.',\n};\n\n/** A check as this module needs to see it, in `sort_order`. */\nexport type ConditionalCheck = {\n id: string;\n description: string;\n runCondition: ChecklistRunCondition;\n};\n\nexport type SkippedCheck = {\n checkId: string;\n /** Stored as the result's evidence, so the report says why nothing ran. */\n reason: string;\n};\n\n/** Quotes a check so the reason reads as a sentence rather than a log line. */\nfunction quote(description: string): string {\n const trimmed = description.trim();\n return trimmed ? `\"${trimmed}\"` : 'the check before it';\n}\n\n/**\n * Whether one check is skipped, given the effective status of the check before\n * it. `previousStatus` is undefined when the previous check has no result yet,\n * which is undecided rather than unmet.\n */\nexport function checkRunIsSkipped(\n check: ConditionalCheck,\n previous: { description: string; status: CheckResultStatus | undefined } | null\n): SkippedCheck | null {\n if (check.runCondition === 'always') return null;\n // The first check has nothing before it to depend on. A condition here is an\n // authoring leftover (usually from a reorder), and running the check is the\n // reading that tests more rather than less.\n if (!previous) return null;\n\n const status = previous.status;\n if (status === undefined || status === 'error' || status === 'informational') return null;\n\n if (check.runCondition === 'if_previous_passed') {\n if (status === 'passed') return null;\n return {\n checkId: check.id,\n reason:\n status === 'skipped'\n ? `Skipped: this check runs only when ${quote(previous.description)} passes, and that check was skipped too.`\n : `Skipped: this check runs only when ${quote(previous.description)} passes, and it failed.`,\n };\n }\n\n if (status === 'failed') return null;\n return {\n checkId: check.id,\n reason:\n status === 'skipped'\n ? `Skipped: this check runs only when ${quote(previous.description)} fails, and that check was skipped.`\n : `Skipped: this check runs only when ${quote(previous.description)} fails, and it passed.`,\n };\n}\n\n/**\n * Every check that should be skipped given the results recorded so far.\n *\n * Walks the checks in run order and treats a check this pass has just skipped\n * as skipped for the check after it, so one unmet condition takes its whole\n * dependent chain with it. Checks that already have a result are left alone:\n * this only ever decides about checks nothing has judged yet.\n *\n * It is called twice by a run, on purpose. Once after each verdict, so a check\n * that can no longer run is settled immediately and never handed to the agent;\n * and once at the end over whatever is left, so the same rule decides the\n * checks a run never reached rather than a second copy of it.\n */\nexport function resolveSkippedChecks(input: {\n /** The checklist's checks in `sort_order`. */\n checks: ConditionalCheck[];\n /** Status per check id, for the checks that have a result. */\n statuses: ReadonlyMap<string, CheckResultStatus>;\n}): SkippedCheck[] {\n const effective = new Map(input.statuses);\n const skipped: SkippedCheck[] = [];\n\n input.checks.forEach((check, index) => {\n if (effective.has(check.id)) return;\n const previousCheck = index > 0 ? input.checks[index - 1] : null;\n const decision = checkRunIsSkipped(\n check,\n previousCheck\n ? { description: previousCheck.description, status: effective.get(previousCheck.id) }\n : null\n );\n if (!decision) return;\n skipped.push(decision);\n effective.set(check.id, 'skipped');\n });\n\n return skipped;\n}\n\n/**\n * Whether a checklist has any check whose condition can never be judged.\n *\n * A condition on the first check, or one pointing at an informational check\n * (which has no verdict by definition), is an authoring mistake rather than a\n * rule: at run time it resolves to \"run the check\", so nothing breaks, but the\n * author has written something that does not do what it says. The editor says\n * so instead of leaving them to find out from a report.\n */\nexport function unjudgeableConditions(\n checks: Array<{ id: string; runCondition: ChecklistRunCondition; checkType: string }>\n): string[] {\n const ids: string[] = [];\n checks.forEach((check, index) => {\n if (check.runCondition === 'always') return;\n const previous = index > 0 ? checks[index - 1] : null;\n if (!previous || previous.checkType === 'informational') ids.push(check.id);\n });\n return ids;\n}\n","// Vendored verbatim from the Stunt Double monorepo, packages/platform/src/checklists/failure.ts\n// at ee0ccb43c, so a local run is scored by exactly the code that scores a cloud run. Do not\n// edit it here: change the source and copy it again. ponytail: a copy until the scoring is\n// published as @stunt-double/checklist, which both sides can then import.\n\n/**\n * What a run does when a check fails.\n *\n * A failure never stopped anything: the actor recorded the verdict and carried\n * on through every remaining check. That is the right behaviour for a checklist\n * of independent observations, and the wrong one for a flow. If \"the payment\n * completes\" fails, everything after it is being judged against a page nobody\n * can reach, so the report fills with knock-on failures that are all the first\n * one wearing different words, and the check that actually broke is buried in\n * them.\n *\n * `run_condition` (see `conditions.ts`) already covers the narrow version of\n * this: one check that only applies when the one before it passed. It cannot\n * express \"and nothing after this is worth attempting\", which is the common\n * case for a flow, because it would have to be repeated on every remaining\n * check and rewired every time the list is reordered.\n *\n * So a check carries an action for its own failure:\n *\n * - `continue` record the failure and carry on. The default, and what every\n * check has always done.\n * - `stop` record the failure and end the run there. Every check that\n * has not been evaluated is recorded as skipped, naming the\n * check that stopped the run.\n *\n * Two decisions are worth stating, because both go the other way at first\n * glance:\n *\n * 1. **The remaining checks are skipped, not errored.** A stop is the\n * checklist working as its author wrote it, exactly like an unmet run\n * condition, so the same status carries it: out of the score's denominator,\n * out of the blocking count, and reported as a deliberate omission rather\n * than as a run that went wrong.\n * 2. **A `stop` failure always fails the run**, whatever the check's type. A\n * weighted check set to stop is an author saying the run cannot meaningfully\n * continue past it; a halted run that reported a passing score off the\n * handful of checks it reached before stopping would be the most misleading\n * report this product can produce. So `scoreChecklistRun` treats it as\n * blocking regardless of how it is scored.\n *\n * Pure module: no runtime imports (the `CheckResultStatus` import is types\n * only, so nothing is loaded at runtime and there is no cycle with\n * `scoring.ts`), so `node --test`, which resolves exact paths only, can load it\n * directly.\n */\n\nimport type { CheckResultStatus } from './scoring.ts';\n\nexport const CHECKLIST_FAILURE_ACTIONS = ['continue', 'stop'] as const;\n\nexport type ChecklistFailureAction = (typeof CHECKLIST_FAILURE_ACTIONS)[number];\n\nexport function isChecklistFailureAction(value: unknown): value is ChecklistFailureAction {\n return (\n typeof value === 'string' && (CHECKLIST_FAILURE_ACTIONS as readonly string[]).includes(value)\n );\n}\n\n/**\n * Falls back to `continue` for null, an unknown value, or a lagging schema\n * cache. That is what every check did before this column existed, and it is the\n * reading that evaluates more rather than less: a row that cannot be read must\n * not be able to cut a run short.\n */\nexport function resolveFailureAction(value: unknown): ChecklistFailureAction {\n return isChecklistFailureAction(value) ? value : 'continue';\n}\n\n/** Short labels, so the editor, the report and the run agree on the words. */\nexport const FAILURE_ACTION_LABELS: Record<ChecklistFailureAction, string> = {\n continue: 'Fail and continue',\n stop: 'Fail and stop',\n};\n\nexport const FAILURE_ACTION_DESCRIPTIONS: Record<ChecklistFailureAction, string> = {\n continue: 'The failure is recorded and the run carries on with the remaining checks.',\n stop: 'The failure is recorded and the run ends. Every check it had not reached is skipped.',\n};\n\n/** A check as this module needs to see it, in `sort_order`. */\nexport type HaltingCheck = {\n id: string;\n description: string;\n onFailure: ChecklistFailureAction;\n};\n\n/**\n * The check that stopped the run, if one did.\n *\n * The first `stop` check with a failed verdict, in run order. First rather than\n * last because a run only ever gets one: everything after it is skipped, so a\n * second one can only appear if a caller passes results a run could not have\n * produced.\n */\nexport function findHaltingCheck<T extends HaltingCheck>(input: {\n checks: T[];\n statuses: ReadonlyMap<string, CheckResultStatus>;\n}): T | null {\n return (\n input.checks.find(\n (check) => check.onFailure === 'stop' && input.statuses.get(check.id) === 'failed'\n ) ?? null\n );\n}\n\n/** Quotes a check so the reason reads as a sentence rather than a log line. */\nfunction quote(description: string): string {\n const trimmed = description.trim();\n return trimmed ? `\"${trimmed}\"` : 'a required check';\n}\n\n/** The evidence a skipped check carries when a stop is what skipped it. */\nexport function haltedReason(halting: { description: string }): string {\n return `Skipped: the run stopped when ${quote(halting.description)} failed.`;\n}\n\n/**\n * Every check a halt leaves unevaluated, with the reason to record against it.\n *\n * Everything without a result is skipped, not only the checks after the halting\n * one: an agent does not always work through a checklist in order, so a check it\n * had passed over is just as unreachable once the run stops. Checks that already\n * have a result are left exactly as they are, halt or no halt.\n *\n * Shaped like `resolveSkippedChecks` in `conditions.ts` on purpose: the run task\n * writes both through one path, so a check skipped by a stop and a check skipped\n * by a condition produce the same kind of row.\n */\nexport function resolveHaltedChecks(input: {\n /** The checklist's checks in `sort_order`. */\n checks: HaltingCheck[];\n /** Status per check id, for the checks that have a result. */\n statuses: ReadonlyMap<string, CheckResultStatus>;\n}): Array<{ checkId: string; reason: string }> {\n const halting = findHaltingCheck(input);\n if (!halting) return [];\n\n const reason = haltedReason(halting);\n return input.checks\n .filter((check) => !input.statuses.has(check.id))\n .map((check) => ({ checkId: check.id, reason }));\n}\n","// Vendored verbatim from the Stunt Double monorepo, packages/platform/src/checklists/scoring.ts\n// at ee0ccb43c, so a local run is scored by exactly the code that scores a cloud run. Do not\n// edit it here: change the source and copy it again. ponytail: a copy until the scoring is\n// published as @stunt-double/checklist, which both sides can then import.\n\n/**\n * Check types, weights, and the verdict a checklist run reaches.\n *\n * Every check used to be the same kind of thing: a binary assertion, with a run\n * judged good when none of them failed. Three kinds are now distinguished,\n * because that single shape was quietly wrong for two of them:\n *\n * - `pass_fail` the original. Binary, and **blocking**: one failure\n * fails the run whatever else scored.\n * - `weighted` contributes `weight` to a score, so \"checkout completes\"\n * can outweigh \"the footer year is current\". The run's\n * verdict comes from the weighted pass rate against the\n * checklist's `pass_threshold`. A **negative** weight makes\n * the check a penalty: it adds nothing when it passes and\n * deducts its size from the points earned when it fails,\n * without ever raising the maximum (see below).\n * - `informational` has no verdict at all. The actor records what it observed\n * (\"the headline price is $29/month\") and that text goes in\n * the report. Counting it as a pass or a failure would be a\n * lie in either direction, so it is scored as neither.\n *\n * The blocking/scored split is the load-bearing decision here. The obvious\n * alternative, folding pass/fail checks into the score with an implicit weight,\n * makes \"the user cannot check out\" survivable as long as enough cosmetic checks\n * pass, which is precisely the report nobody should be able to produce. So a\n * mixed checklist has two independent hurdles: no blocking check may fail, and\n * the weighted score must reach the threshold.\n *\n * Penalties exist because a scoring rubric is often written as \"award points\n * for each of these, and deduct points for that\". Modelling the deduction as a\n * positive check (\"no generic bank icon is used\") would put its weight into\n * the maximum, so a product that merely avoided the mistake would score higher\n * than one that did everything right, and the \"out of 80\" the rubric's author\n * wrote would silently become \"out of 110\". A penalty check is still phrased\n * as something that should be true (the actor passes it when the product\n * avoids the problem), so pass and fail keep meaning good and bad everywhere.\n *\n * A checklist can also name **score bands** (`score_bands`): labelled ranges\n * of the score, so a run reads as \"Optimal\" or \"Partial adoption\" rather than\n * only as passed or failed. The bands are a reading of the score and do not\n * move the verdict: `pass_threshold` alone decides passed or failed, which is\n * what every alert and every list filters on.\n *\n * The score itself comes in one of two **score types** (`score_type`): a\n * percentage of the points available (the default, which keeps its meaning as\n * weights change), or the points earned as they are, for a rubric written in\n * its own units (\"pass at 60 marks\"). The threshold and band mins are in the\n * same unit as the score, and a run records the unit it was scored in so its\n * number never reads in the wrong one after the checklist is switched.\n *\n * Errors are the third state and are treated as **inconclusive**, not as\n * failures: a check the run never reached says nothing about the product. They\n * stay out of the score's denominator and do not fail the run on their own,\n * which is the behaviour that already existed when `error_count` was reported\n * beside the pass count rather than folded into it. `inconclusiveCount` is\n * carried on the result so a caller can say so rather than implying the score\n * covered everything.\n *\n * A check also carries what the run does when it fails (`failure.ts`). That is\n * an execution rule rather than a scoring one, but it lands here in one place:\n * a check set to stop the run is blocking whatever its type, because a halted\n * run must never report a passing score off the few checks it reached before\n * stopping.\n *\n * Pure module: its one import is a sibling pure module, so `node --test` (which\n * resolves exact paths only) can load it directly.\n */\n\nimport { RUN_FAILURE_CAUSE_LABELS, resolveRunFailureCause } from './failure-cause.ts';\nimport { resolveFailureAction } from './failure.ts';\n\n// ---------------------------------------------------------------------------\n// Check types\n// ---------------------------------------------------------------------------\n\nexport const CHECKLIST_CHECK_TYPES = ['pass_fail', 'weighted', 'informational'] as const;\nexport type ChecklistCheckType = (typeof CHECKLIST_CHECK_TYPES)[number];\n\nexport function isChecklistCheckType(value: unknown): value is ChecklistCheckType {\n return typeof value === 'string' && (CHECKLIST_CHECK_TYPES as readonly string[]).includes(value);\n}\n\n/**\n * Falls back to 'pass_fail' for null, unknown values, or a lagging schema\n * cache, which is what every check was before types existed. A row that cannot\n * be read must keep behaving the way it always has.\n */\nexport function resolveCheckType(value: unknown): ChecklistCheckType {\n return isChecklistCheckType(value) ? value : 'pass_fail';\n}\n\n/** Short labels, so the dashboard, the MCP tools and the report agree. */\nexport const CHECK_TYPE_LABELS: Record<ChecklistCheckType, string> = {\n pass_fail: 'Pass / fail',\n weighted: 'Weighted',\n informational: 'Informational',\n};\n\nexport const CHECK_TYPE_DESCRIPTIONS: Record<ChecklistCheckType, string> = {\n pass_fail: 'Must pass. One failure fails the whole run.',\n weighted:\n 'Contributes its weight to the run score. A negative weight is a penalty, deducted when the check fails.',\n informational: 'Reported as an observation. Never passes or fails.',\n};\n\n/** What a penalty check does, for the surfaces that badge one. */\nexport const PENALTY_CHECK_DESCRIPTION =\n 'Penalty. Adds nothing when it passes and deducts its weight from the score when it fails, without raising the maximum.';\n\n// ---------------------------------------------------------------------------\n// Score types\n// ---------------------------------------------------------------------------\n\n/**\n * What a weighted score is measured in. `percent` is the points earned over\n * the points available; `points` is the points earned, as they are, named by\n * the checklist's `score_unit`.\n */\nexport const CHECKLIST_SCORE_TYPES = ['percent', 'points'] as const;\nexport type ChecklistScoreType = (typeof CHECKLIST_SCORE_TYPES)[number];\n\nexport function isChecklistScoreType(value: unknown): value is ChecklistScoreType {\n return typeof value === 'string' && (CHECKLIST_SCORE_TYPES as readonly string[]).includes(value);\n}\n\n/** Falls back to 'percent', which is what every score was before types existed. */\nexport function resolveScoreType(value: unknown): ChecklistScoreType {\n return isChecklistScoreType(value) ? value : 'percent';\n}\n\nexport const SCORE_TYPE_LABELS: Record<ChecklistScoreType, string> = {\n percent: 'Percentage',\n points: 'Points',\n};\n\nexport const SCORE_TYPE_DESCRIPTIONS: Record<ChecklistScoreType, string> = {\n percent: 'The share of the available points earned, 0 to 100%.',\n points: 'The points earned, as they are. The threshold and bands are in points.',\n};\n\nexport const DEFAULT_SCORE_UNIT = 'points';\n/** Matches the column's CHECK constraint. */\nexport const MAX_SCORE_UNIT_LENGTH = 24;\n\n/** A points label as stored: trimmed and capped, or null for the default. */\nexport function resolveScoreUnit(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim().slice(0, MAX_SCORE_UNIT_LENGTH);\n return trimmed || null;\n}\n\n/**\n * The unit a run's score is recorded in (`checklist_runs.score_unit`): null for\n * a percentage, otherwise the points label. One nullable value rather than a\n * type and a label, because a run only ever needs to know how to print its\n * number, and null is exactly how every run before score types reads.\n */\nexport function runScoreUnit(scoreType: unknown, scoreUnit: unknown): string | null {\n return resolveScoreType(scoreType) === 'points'\n ? (resolveScoreUnit(scoreUnit) ?? DEFAULT_SCORE_UNIT)\n : null;\n}\n\n/**\n * A score, threshold or band min in its unit: \"75%\" for a percentage (a null\n * unit), \"62 marks\" for points. Every surface that prints a score goes through\n * here so none of them assumes a percentage.\n */\nexport function formatScoreValue(value: number, unit: string | null | undefined): string {\n if (unit == null) return `${formatScore(value)}%`;\n // \"1 points\" reads as a typo; a unit ending in a single s drops it for one.\n const label = Math.abs(value) === 1 && /[^s]s$/i.test(unit) ? unit.slice(0, -1) : unit;\n return `${formatScore(value)} ${label}`;\n}\n\n// ---------------------------------------------------------------------------\n// Weights and thresholds\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_CHECK_WEIGHT = 1;\n/**\n * Matches the column's CHECK constraint, so the UI rejects what the database\n * would. The floor is negative because a weight below zero is a penalty.\n */\nexport const MIN_CHECK_WEIGHT = -100;\nexport const MAX_CHECK_WEIGHT = 100;\n\n/**\n * A usable weight for a check. A weight of zero is legal (it parks a check\n * without deleting it), so the fallback only applies to values that are not\n * finite numbers at all; anything out of range is clamped rather than rejected,\n * because a weight is an editor's judgement and not a correctness boundary.\n */\nexport function resolveCheckWeight(value: unknown): number {\n const parsed = typeof value === 'string' ? Number(value) : value;\n if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return DEFAULT_CHECK_WEIGHT;\n return Math.min(MAX_CHECK_WEIGHT, Math.max(MIN_CHECK_WEIGHT, parsed));\n}\n\n/** A weighted check whose weight is below zero is a penalty. */\nexport function isPenaltyWeight(weight: unknown): boolean {\n return resolveCheckWeight(weight) < 0;\n}\n\n/**\n * The most points a checklist can earn: the weights of its positively weighted\n * checks. Penalties are left out because they only ever take points away, and\n * the other two types are not scored. What an editor needs when it converts a\n * percentage threshold into the points its author was thinking in.\n */\nexport function checklistPossibleWeight(\n // A bare string is a check submitted as a description alone: pass/fail.\n checks: Array<string | { checkType?: unknown; weight?: unknown }>\n): number {\n let total = 0;\n for (const check of checks) {\n if (typeof check === 'string' || resolveCheckType(check.checkType) !== 'weighted') continue;\n const weight = resolveCheckWeight(check.weight);\n if (weight > 0) total += weight;\n }\n return total;\n}\n\n/**\n * The weighted score a run must reach to pass, in the checklist's score type.\n *\n * 100 is the default and is what keeps every checklist that predates weighting\n * behaving exactly as it did: at 100%, one failed weighted check fails the run,\n * which is the old rule. It only becomes interesting once an author lowers it.\n * A points checklist's equivalent is full marks: see `defaultPassThreshold`.\n */\nexport const DEFAULT_PASS_THRESHOLD = 100;\n\n/** The ceiling for a points threshold or band min. Matches the CHECK constraint. */\nexport const MAX_POINTS_SCORE = 100000;\n\nfunction scoreCeiling(scoreType: unknown): number {\n return resolveScoreType(scoreType) === 'points' ? MAX_POINTS_SCORE : 100;\n}\n\nexport function resolvePassThreshold(value: unknown, scoreType?: unknown): number {\n const parsed = typeof value === 'string' ? Number(value) : value;\n if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return DEFAULT_PASS_THRESHOLD;\n return Math.min(scoreCeiling(scoreType), Math.max(0, parsed));\n}\n\n/**\n * The threshold a checklist starts with when its author has not set one: every\n * point. 100% for a percentage, and the points available for points, so either\n * type begins with the all-or-nothing rule.\n */\nexport function defaultPassThreshold(scoreType: unknown, possibleWeight: number): number {\n return resolveScoreType(scoreType) === 'points' ? possibleWeight : DEFAULT_PASS_THRESHOLD;\n}\n\n/** A checklist's scoring settings as submitted by any surface that writes one. */\nexport type ChecklistScoringInput = {\n scoreType?: unknown;\n scoreUnit?: unknown;\n passThreshold?: unknown;\n scoreBands?: unknown;\n};\n\n/** The scoring columns of `checklists`, resolved for a write. */\nexport type ChecklistScoringColumns = {\n score_type: ChecklistScoreType;\n score_unit: string | null;\n pass_threshold: number;\n score_bands: ScoreBand[] | null;\n};\n\n/**\n * The scoring columns for a new checklist. Shared by the four surfaces that\n * create one (the server action, MCP, the assistant), so a points checklist\n * created without a threshold starts at full marks everywhere rather than at\n * an arbitrary 100 points.\n */\nexport function newChecklistScoring(\n input: ChecklistScoringInput & {\n checks: Array<string | { checkType?: unknown; weight?: unknown }>;\n }\n): ChecklistScoringColumns {\n const scoreType = resolveScoreType(input.scoreType);\n return {\n score_type: scoreType,\n score_unit: resolveScoreUnit(input.scoreUnit),\n pass_threshold:\n input.passThreshold == null\n ? resolvePassThreshold(\n defaultPassThreshold(scoreType, checklistPossibleWeight(input.checks)),\n scoreType\n )\n : resolvePassThreshold(input.passThreshold, scoreType),\n score_bands: resolveScoreBands(input.scoreBands, scoreType),\n };\n}\n\n/**\n * The scoring columns an edit changes, given what is stored. Undefined fields\n * leave their column alone, except that a change of score type re-reads the\n * stored threshold and bands in the new unit, since a 300-point threshold is\n * not a legal percentage and the column's CHECK would refuse the write. An\n * editor converts them itself (`convertScoreValue`) and sends them with the\n * type; this is the backstop for a caller that sends the type alone.\n */\nexport function updatedChecklistScoring(\n stored: { score_type?: unknown; pass_threshold?: unknown; score_bands?: unknown },\n patch: ChecklistScoringInput\n): Partial<ChecklistScoringColumns> {\n const storedType = resolveScoreType(stored.score_type);\n const scoreType = patch.scoreType === undefined ? storedType : resolveScoreType(patch.scoreType);\n const typeChanged = scoreType !== storedType;\n const updates: Partial<ChecklistScoringColumns> = {};\n if (patch.scoreType !== undefined) updates.score_type = scoreType;\n if (patch.scoreUnit !== undefined) updates.score_unit = resolveScoreUnit(patch.scoreUnit);\n if (patch.passThreshold !== undefined || typeChanged) {\n updates.pass_threshold = resolvePassThreshold(\n patch.passThreshold ?? stored.pass_threshold,\n scoreType\n );\n }\n if (patch.scoreBands !== undefined || typeChanged) {\n updates.score_bands = resolveScoreBands(\n patch.scoreBands === undefined ? stored.score_bands : patch.scoreBands,\n scoreType\n );\n }\n return updates;\n}\n\n/**\n * A score in the other unit, so either reading is to hand: \"75%\" means nothing\n * to somebody who wrote a rubric out of 80, and \"60 marks\" alone hides how\n * much of the checklist that is. Empty when the checklist has nothing positive\n * to earn yet.\n */\nexport function scoreEquivalent(\n value: number,\n scoreType: ChecklistScoreType,\n possibleWeight: number,\n scoreUnit?: string | null\n): string {\n if (!(possibleWeight > 0)) return '';\n const unit = scoreUnit || DEFAULT_SCORE_UNIT;\n return scoreType === 'points'\n ? `${formatScore(convertScoreValue(value, 'percent', possibleWeight))}% of ${formatScoreValue(possibleWeight, unit)}`\n : `${formatScore(convertScoreValue(value, 'points', possibleWeight))} of ${formatScoreValue(possibleWeight, unit)}`;\n}\n\n/**\n * The scoring fields of a run payload, from a checklist row as selected. One\n * helper for the six places that queue a run, so none of them can send the\n * threshold without the unit it is in.\n */\nexport function runPayloadScoring(checklist: {\n pass_threshold?: unknown;\n score_bands?: unknown;\n score_type?: unknown;\n score_unit?: unknown;\n}): {\n passThreshold: number;\n scoreBands: ScoreBand[] | null;\n scoreType: ChecklistScoreType;\n scoreUnit: string | null;\n} {\n return {\n passThreshold: resolvePassThreshold(checklist.pass_threshold, checklist.score_type),\n scoreBands: resolveScoreBands(checklist.score_bands, checklist.score_type),\n scoreType: resolveScoreType(checklist.score_type),\n scoreUnit: resolveScoreUnit(checklist.score_unit),\n };\n}\n\n/**\n * The same bar in the other unit, at the given points available, for an editor\n * switching a checklist's score type: 75% of 80 points is 60 points, and back.\n * Rounded to one decimal place. With no points available there is nothing to\n * convert against, so the value is returned clamped to its new unit.\n */\nexport function convertScoreValue(\n value: number,\n to: ChecklistScoreType,\n possibleWeight: number\n): number {\n if (possibleWeight <= 0) return resolvePassThreshold(value, to);\n const converted =\n to === 'points' ? (value / 100) * possibleWeight : (value / possibleWeight) * 100;\n return resolvePassThreshold(Math.round(converted * 10) / 10, to);\n}\n\n// ---------------------------------------------------------------------------\n// Score bands\n// ---------------------------------------------------------------------------\n\n/**\n * One labelled range of the score. `min` is the lowest score (in the\n * checklist's score type) that earns the label; the range runs up to the next\n * band's `min`.\n */\nexport type ScoreBand = {\n label: string;\n min: number;\n};\n\n/** The grade a run reached: its band, and where that band sits among them. */\nexport type ScoreGrade = {\n label: string;\n /** 0 is the best band. */\n rank: number;\n /** How many bands there are, so a caller can tell the worst from the middle. */\n count: number;\n};\n\nexport const MAX_SCORE_BANDS = 6;\nexport const MAX_SCORE_BAND_LABEL_LENGTH = 40;\n\n/**\n * The bands as stored on `checklists.score_bands`, or null when the checklist\n * has none. Tolerant on purpose: the column is JSON written by four surfaces,\n * so an entry that is not a `{ label, min }` pair is dropped rather than\n * failing the read, a label is trimmed and capped, a `min` is clamped to the\n * score type's range, and the result is ordered best band first with one band\n * per `min`. Anything that leaves no usable band at all resolves to null, which\n * is exactly what a checklist without bands looks like.\n */\nexport function resolveScoreBands(value: unknown, scoreType?: unknown): ScoreBand[] | null {\n if (!Array.isArray(value)) return null;\n const bands: ScoreBand[] = [];\n const seen = new Set<number>();\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue;\n const { label, min } = entry as { label?: unknown; min?: unknown };\n if (typeof label !== 'string') continue;\n const trimmed = label.trim().slice(0, MAX_SCORE_BAND_LABEL_LENGTH);\n if (!trimmed) continue;\n const parsed = typeof min === 'string' ? Number(min) : min;\n if (typeof parsed !== 'number' || !Number.isFinite(parsed)) continue;\n const clamped = Math.min(scoreCeiling(scoreType), Math.max(0, parsed));\n if (seen.has(clamped)) continue;\n seen.add(clamped);\n bands.push({ label: trimmed, min: clamped });\n if (bands.length === MAX_SCORE_BANDS) break;\n }\n if (bands.length === 0) return null;\n bands.sort((a, b) => b.min - a.min);\n return bands;\n}\n\n/**\n * Which band a score falls in: the best band whose `min` the score reaches.\n * The last band is a catch-all, so a score below every `min` still reads as\n * the worst grade rather than as ungraded; an author who writes \"Pushed back:\n * below 30\" means everything below 30, including a score the penalties drove\n * to zero. Null when there is no score or no bands.\n */\nexport function gradeForScore(\n bands: ScoreBand[] | null | undefined,\n score: number | null | undefined\n): ScoreGrade | null {\n if (score == null || !bands || bands.length === 0) return null;\n const rank = bands.findIndex((band) => score >= band.min);\n const index = rank === -1 ? bands.length - 1 : rank;\n return { label: bands[index]!.label, rank: index, count: bands.length };\n}\n\n/**\n * A stored run's grade under its checklist's current bands. Null when the run\n * was scored in the other score type from the one the bands are now written\n * in: a 75% run read against bands in points would grade by accident.\n */\nexport function gradeForRun(\n checklist: { score_bands?: unknown; score_type?: unknown },\n run: { score?: unknown; score_unit?: unknown }\n): ScoreGrade | null {\n if (typeof run.score !== 'number') return null;\n const scoreType = resolveScoreType(checklist.score_type);\n if ((run.score_unit == null) !== (scoreType === 'percent')) return null;\n return gradeForScore(resolveScoreBands(checklist.score_bands, scoreType), run.score);\n}\n\n// ---------------------------------------------------------------------------\n// Scoring\n// ---------------------------------------------------------------------------\n\n/**\n * A result status as stored on `checklist_run_results.status`.\n *\n * `skipped` is a check the run deliberately did not evaluate because its\n * `run_condition` was unmet (see `conditions.ts`). It is neither a verdict nor\n * an error: nothing went wrong, and nothing was learned about the product.\n */\nexport type CheckResultStatus = 'passed' | 'failed' | 'error' | 'informational' | 'skipped';\n\nexport type ScoreableCheck = {\n id: string;\n checkType: ChecklistCheckType;\n weight: number;\n /**\n * What the run does when this check fails. Optional because a caller built\n * before the column existed does not carry it, and absent resolves to\n * `continue`, which is what every check did. A `stop` check that failed is\n * blocking whatever its type: see `failure.ts`.\n */\n onFailure?: unknown;\n};\n\nexport type ScoreableResult = {\n checkId: string;\n status: CheckResultStatus;\n};\n\n/**\n * A run's verdict, as stored in `checklist_runs.outcome`.\n *\n * Two of these judge the product; the third judges the run. `inconclusive`\n * means every check errored, or a weighted checklist reached none of its\n * weighted checks, so the run produced no score to judge. It\n * exists because the alternative was reporting such a run as `passed`: an\n * errored check is deliberately left out of the weighted denominator, so a run\n * where all of them errored scored null against no blocking failures and\n * cleared both hurdles by default.\n */\nexport type ChecklistRunOutcome = 'passed' | 'failed' | 'inconclusive';\n\nexport type ChecklistRunScore = {\n /**\n * The weighted score in `scoreUnit`, rounded to one decimal place, or null\n * when the checklist has no weighted check with a verdict. Null is not zero:\n * \"nothing to score\" and \"scored nothing\" are different reports.\n */\n score: number | null;\n /**\n * What `score` is measured in, as `checklist_runs.score_unit` stores it: null\n * for a percentage, otherwise the points label.\n */\n scoreUnit: string | null;\n /**\n * Points earned: the weights of the weighted checks that passed, less every\n * penalty that applied. Can be negative, which the percentage never is.\n */\n earnedWeight: number;\n /**\n * Points available: the weights of the positively weighted checks that\n * reached a verdict either way. Penalties are not in it, so a rubric written\n * as \"out of 80\" stays out of 80 however many deductions it lists.\n */\n possibleWeight: number;\n /** The total deducted by penalty checks that failed. */\n penaltyWeight: number;\n /** The label of the score band the run landed in, or null when ungraded. */\n grade: string | null;\n /**\n * The verdict on the experience, as opposed to whether the run finished.\n *\n * `passed` and `failed` are statements about the product. `inconclusive` is\n * not: it means the run decided nothing, or reached none of a weighted\n * checklist's weighted checks, so there is no statement to make. Readers must not fold it into either verdict, and in particular must\n * not render it as a failure, which would send someone hunting a regression\n * that was never observed.\n */\n outcome: ChecklistRunOutcome;\n /**\n * Checks that failed and are blocking: every failed `pass_fail` check, plus\n * any check of any type that was set to stop the run on failure. Any one of\n * these fails the run.\n */\n blockingFailures: number;\n /** Blocking checks that passed. */\n blockingPasses: number;\n /** Checks of any scored type that errored, so nothing was learned from them. */\n inconclusiveCount: number;\n /** Informational checks that recorded an observation. */\n informationalCount: number;\n /**\n * Checks whose run condition was unmet, so the run never evaluated them.\n * Counted separately from `inconclusiveCount` because a skip is the\n * checklist working as written, not something that went wrong.\n */\n skippedCount: number;\n /** True when the checklist has at least one weighted check. */\n weighted: boolean;\n /** The threshold the score was judged against. */\n passThreshold: number;\n /**\n * The check that ended the run, when one was set to stop on failure and did.\n * Null on every other run. Carried so a caller can say the run stopped early\n * rather than leaving its skipped checks to be read as an unfinished run.\n */\n stoppedByCheckId: string | null;\n};\n\n/**\n * Judge a run from its checks and the results it recorded.\n *\n * The single source of truth for \"did this run pass?\": the run task writes its\n * answer to `checklist_runs.outcome` and `score`, and every read surface (the\n * report, the runs list, the shared page, the email) renders that same answer\n * rather than re-deriving one from the counts. Two surfaces disagreeing about\n * whether a run passed is the failure this exists to prevent.\n *\n * A check with no result at all is treated as an error, which is what the run\n * task itself records for a check it never reached.\n */\nexport function scoreChecklistRun(input: {\n checks: ScoreableCheck[];\n results: ScoreableResult[];\n passThreshold?: number | null;\n /** The checklist's score bands, as stored. Resolved here, so pass the raw column. */\n scoreBands?: unknown;\n /** The checklist's `score_type` and `score_unit`, as stored. Absent is a percentage. */\n scoreType?: unknown;\n scoreUnit?: unknown;\n}): ChecklistRunScore {\n const scoreType = resolveScoreType(input.scoreType);\n const passThreshold = resolvePassThreshold(\n input.passThreshold ?? DEFAULT_PASS_THRESHOLD,\n scoreType\n );\n const scoreBands = resolveScoreBands(input.scoreBands, scoreType);\n const byCheckId = new Map<string, CheckResultStatus>();\n for (const result of input.results) {\n // First verdict wins. A run records one result per check, but a caller\n // handing us a duplicated join should not silently double-count a weight.\n if (!byCheckId.has(result.checkId)) byCheckId.set(result.checkId, result.status);\n }\n\n let earnedWeight = 0;\n let possibleWeight = 0;\n let penaltyWeight = 0;\n let blockingFailures = 0;\n let blockingPasses = 0;\n let inconclusiveCount = 0;\n let informationalCount = 0;\n let skippedCount = 0;\n let weighted = false;\n let weightedErrored = 0;\n let stoppedByCheckId: string | null = null;\n\n for (const check of input.checks) {\n const status = byCheckId.get(check.id) ?? 'error';\n\n // A skipped check is out of the reckoning entirely, whatever its type: it\n // did not fail, and it is not inconclusive either. Counting a skip as an\n // error would let a condition the author wrote on purpose read as a run\n // that went wrong, and counting it in the weighted denominator would push\n // the score down for a check nobody asked to have evaluated.\n if (status === 'skipped') {\n skippedCount++;\n continue;\n }\n\n if (check.checkType === 'informational') {\n // An informational check is never scored, whatever status was stored\n // against it (an older run may have recorded a verdict before the check\n // was reclassified). It counts as reported when it produced anything.\n if (status !== 'error') informationalCount++;\n continue;\n }\n\n // A check set to stop the run is blocking by definition, whatever else it\n // is. Its weight still counts (an author who weighted it meant the number),\n // but the run cannot come out passed on the strength of the few checks it\n // reached before stopping, which is what leaving this to the threshold\n // would allow: the checks that would have dragged the score down are\n // exactly the ones the halt skipped.\n const halted = status === 'failed' && resolveFailureAction(check.onFailure) === 'stop';\n if (halted && !stoppedByCheckId) stoppedByCheckId = check.id;\n\n if (check.checkType === 'weighted') {\n weighted = true;\n if (status === 'error' || status === 'informational') {\n inconclusiveCount++;\n weightedErrored++;\n continue;\n }\n const weight = resolveCheckWeight(check.weight);\n if (weight < 0) {\n // A penalty: nothing to earn, only something to lose. It stays out of\n // the maximum so that avoiding a mistake is not rewarded over and\n // above doing the rest right.\n if (status === 'failed') {\n penaltyWeight += -weight;\n earnedWeight += weight;\n }\n } else {\n possibleWeight += weight;\n if (status === 'passed') earnedWeight += weight;\n }\n if (halted) blockingFailures++;\n continue;\n }\n\n // pass_fail, the blocking kind.\n if (status === 'passed') blockingPasses++;\n else if (status === 'failed') blockingFailures++;\n else inconclusiveCount++;\n }\n\n // The percentage is floored at zero: a penalty can take the points below\n // nothing, and the points say so, but \"minus 37%\" is not a pass rate anyone\n // can read. A run with nothing to earn and a penalty applied scores zero\n // rather than null, because something was judged and it was judged badly.\n // A points score is the points, negative included: that is the number its\n // author asked for.\n const nothingToScore = possibleWeight === 0 && penaltyWeight === 0;\n const score = nothingToScore\n ? null\n : scoreType === 'points'\n ? Math.round(earnedWeight * 10) / 10\n : possibleWeight > 0\n ? Math.max(0, Math.round((earnedWeight / possibleWeight) * 1000) / 10)\n : 0;\n\n // Nothing was decided: every scored check errored, so there is no score to\n // judge and no blocking check to have failed. Both hurdles below are cleared\n // by default in that state, which is how a run that evaluated none of its\n // checks used to be stored as `passed`.\n //\n // Deliberately narrow. Observations and skips are excluded by construction\n // (neither increments `inconclusiveCount`), so a checklist made entirely of\n // observations is a normal pass rather than a fault. One verdict of any kind\n // is enough to take the run out of this state: a single failing blocking\n // check among five errors is a failure, because we did observe something and\n // it was bad, and a failed penalty already scores zero rather than null.\n // A weighted checklist has a second way in: see `scoreUnreached` below.\n //\n // `blockingFailures` is listed even though the ternary below tests it first,\n // so this reads as a standalone claim about the run rather than one that is\n // only correct in its current position.\n const nothingJudged =\n possibleWeight === 0 &&\n penaltyWeight === 0 &&\n blockingPasses === 0 &&\n blockingFailures === 0 &&\n inconclusiveCount > 0;\n\n // The score is the verdict a weighted checklist exists to give, so a run that\n // reached none of its weighted checks has not answered the question it was\n // asked, however many blocking checks passed on the way in. A page-load check\n // passing before a bot wall errors every scored check is the case this\n // catches: without it the null score clears the threshold by default and the\n // one blocking pass reads as a clean run. A blocking failure still wins (that\n // was observed, and it was bad), and so does any score at all, a failed\n // penalty included. Keyed on the score rather than on \"some weighted check\n // reached a verdict\", because a passing penalty earns nothing and so leaves\n // the score null: counting it would let the bot-walled run through again.\n const scoreUnreached = weighted && score === null && weightedErrored > 0;\n\n // Two independent hurdles, so a weighted score can never buy off a blocking\n // failure and a clean set of blocking checks can never rescue a bad score.\n // The third answer sits between them: not a verdict on the product at all.\n const outcome: ChecklistRunOutcome =\n blockingFailures > 0 || (score !== null && score < passThreshold)\n ? 'failed'\n : nothingJudged || scoreUnreached\n ? 'inconclusive'\n : 'passed';\n\n return {\n score,\n scoreUnit: runScoreUnit(scoreType, input.scoreUnit),\n earnedWeight,\n possibleWeight,\n penaltyWeight,\n grade: gradeForScore(scoreBands, score)?.label ?? null,\n outcome,\n blockingFailures,\n blockingPasses,\n inconclusiveCount,\n informationalCount,\n skippedCount,\n weighted,\n passThreshold,\n stoppedByCheckId,\n };\n}\n\n/**\n * One line describing what a run concluded, used as `checklist_runs.summary`\n * and as the first line of every notification about the run.\n *\n * `passedCount` / `totalCount` stay in the sentence for an unweighted checklist\n * because that is the number people already read runs by; a weighted checklist\n * leads with the score, since its pass count no longer means what it looks\n * like it means.\n */\n/**\n * How a finished run should be labelled, for every surface that badges one.\n *\n * Shared rather than repeated because the four surfaces that show a verdict\n * (the report page, its OG card, the runs list and the report email) each used\n * to spell the same ternary themselves, and the ternary had a hole: written as\n * `outcome === 'failed' ? red : green`, an outcome that is neither renders as a\n * pass. Adding a third value to a chain like that is silent, so the chain lives\n * in one place now.\n *\n * `tone` is presentation-agnostic on purpose: the email speaks in its own\n * `danger`/`success` vocabulary and the OG card in hex, so each maps the tone\n * to its own palette rather than inheriting Tailwind class names from here.\n */\nexport function describeRunOutcome(input: {\n /** Whether the run itself finished, which outranks any verdict it recorded. */\n status?: string | null;\n outcome?: string | null;\n /**\n * Failed-check count, used only for runs recorded before `outcome` existed.\n * Both spellings are accepted because the callers are split: the web surfaces\n * hand over a database row (`failed_count`) and the email its own props\n * (`failedCount`), and a helper that silently ignored one of them would fall\n * through to the wrong label on exactly the legacy runs it exists to cover.\n */\n failedCount?: number | null;\n failed_count?: number | null;\n /**\n * Why a failed run failed (`checklist_runs.failure_cause`). Both spellings,\n * for the same reason as the failed count.\n */\n failureCause?: string | null;\n failure_cause?: string | null;\n}): { label: string; tone: 'positive' | 'negative' | 'neutral' } {\n if (input.status === 'failed') {\n const cause = resolveRunFailureCause(input.failureCause ?? input.failure_cause);\n // Neutral, never negative, for the same reason as an inconclusive run: a\n // site that could not be loaded, or answered with a server error, made no\n // claim about the checklist, and red would send someone hunting a\n // regression (or a bug of ours) that nobody observed.\n if (cause === 'site_unreachable' || cause === 'site_error') {\n return { label: RUN_FAILURE_CAUSE_LABELS[cause], tone: 'neutral' };\n }\n if (cause === 'service') return { label: RUN_FAILURE_CAUSE_LABELS[cause], tone: 'negative' };\n // Predates `failure_cause`: the cause was never recorded.\n return { label: 'Run failed', tone: 'negative' };\n }\n if (input.outcome === 'failed') return { label: 'Failed', tone: 'negative' };\n if (input.outcome === 'passed') return { label: 'Passed', tone: 'positive' };\n // Neutral, never negative. The run made no claim about the product, and\n // colouring it red sends someone looking for a regression nobody observed.\n if (input.outcome === 'inconclusive') return { label: 'Inconclusive', tone: 'neutral' };\n // Predates scoring: fall back to the rule the dashboard used before\n // `outcome` was stored.\n const failedCount = input.failedCount ?? input.failed_count;\n if (failedCount != null) {\n return failedCount === 0\n ? { label: 'Passed', tone: 'positive' }\n : { label: 'Failed', tone: 'negative' };\n }\n return { label: 'Run completed', tone: 'neutral' };\n}\n\nexport function describeRunScore(\n score: ChecklistRunScore,\n counts: { passedCount: number; totalCount: number }\n): string {\n // Neither an observation nor a skipped check is something that could have\n // passed, so neither belongs in the denominator people read the run by.\n const scored = counts.totalCount - score.informationalCount - score.skippedCount;\n\n if (score.weighted && score.score !== null) {\n // The grade sits with the number it reads, and a deduction is said in the\n // points the rubric's author wrote it in, since \"30 points deducted\" is\n // what they will look for and a percentage of the maximum is not.\n const graded = score.grade ? ` (${score.grade})` : '';\n const unit = score.scoreUnit;\n const parts = [\n unit == null\n ? `Scored ${formatScore(score.score)}%${graded} against a ${formatScore(score.passThreshold)}% threshold`\n : `Scored ${formatScore(score.score)} of ${formatScoreValue(score.possibleWeight, unit)}${graded} against a threshold of ${formatScore(score.passThreshold)}`,\n ];\n if (score.penaltyWeight > 0) {\n parts.push(`${formatScoreValue(score.penaltyWeight, unit ?? DEFAULT_SCORE_UNIT)} deducted`);\n }\n if (score.blockingFailures > 0) {\n parts.push(\n `${score.blockingFailures} required ${score.blockingFailures === 1 ? 'check' : 'checks'} failed`\n );\n }\n if (score.informationalCount > 0) {\n parts.push(`${score.informationalCount} informational`);\n }\n if (score.skippedCount > 0) {\n parts.push(`${score.skippedCount} skipped`);\n }\n return parts.join(', ');\n }\n\n // A ratio is the wrong sentence for a run that reached no verdict: \"0/6\n // checks passed\" is what the incident report actually said, and it reads as\n // six failures rather than as six checks nobody got to. The notes still\n // follow, so the sentence accounts for every check.\n //\n // A weighted checklist whose blocking checks passed but whose scored checks\n // all errored is inconclusive too, and \"No checks could be evaluated\" would\n // deny the checks that did pass, so it names both halves.\n const base =\n score.outcome !== 'inconclusive'\n ? `${counts.passedCount}/${scored} checks passed`\n : score.weighted && score.blockingPasses > 0\n ? `No weighted checks could be evaluated, ${score.blockingPasses} required ${score.blockingPasses === 1 ? 'check' : 'checks'} passed`\n : 'No checks could be evaluated';\n const notes: string[] = [];\n if (score.informationalCount > 0) notes.push(`${score.informationalCount} informational`);\n if (score.skippedCount > 0) notes.push(`${score.skippedCount} skipped`);\n return notes.length > 0 ? `${base}, ${notes.join(', ')}` : base;\n}\n\n/** Trims the trailing `.0` a whole-number score would otherwise carry. */\nexport function formatScore(value: number): string {\n return Number.isInteger(value) ? String(value) : value.toFixed(1);\n}\n\n/**\n * Whether a checklist has to be driven by the agent rather than replayed from\n * its recorded script.\n *\n * An informational check produces prose about what the page currently says, and\n * prose is exactly what a recorded script cannot produce: a replay evaluates\n * fixed assertions with no model in the loop. A code-mode checklist containing\n * one would therefore replay its scored checks and file \"the recorded script\n * does not cover this check\" against every observation, which is both useless\n * and the opposite of the point (the observation is the thing that is supposed\n * to be re-read every run so drift shows up).\n *\n * So one informational check sends the whole run down the agent path. That is a\n * real cost, and it is the honest one: there is no cheaper way to get a fresh\n * reading of what the page says.\n */\nexport function checklistNeedsAgent(checks: Array<{ checkType: ChecklistCheckType }>): boolean {\n return checks.some((check) => check.checkType === 'informational');\n}\n"],"mappings":"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","names":["quote"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui-server.ts"],"sourcesContent":["// The local viewer: one HTML page over the files in `.stuntdouble/`, with live\n// updates while a run is going and a Stop button.\n//\n// It listens on loopback only, and still trusts nothing that can reach\n// loopback: a page in the person's browser can, and so can a DNS rebinding\n// attack. So every request must name this server in its Host header, and\n// everything but the identity probe needs the per-start key, which the printed\n// link carries once and an HttpOnly, SameSite=Strict cookie carries after\n// that. No CORS headers are ever sent, and files are served from inside\n// `.stuntdouble/` only.\n\nimport { randomBytes, timingSafeEqual } from 'node:crypto';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { readdir, readFile, realpath } from 'node:fs/promises';\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport { basename, extname, join, resolve, sep } from 'node:path';\n\nimport {\n ActorSchema,\n ChecklistSchema,\n effectiveStatus,\n readJson,\n readRuns,\n writeFileAtomic,\n} from './files.ts';\n\nexport const FIRST_PORT = 4747;\nconst PORT_TRIES = 20;\n\nexport type UiServer = {\n port: number;\n token: string;\n /** An address that signs the browser in, opening at `path`. */\n link(path?: string): string;\n close(): Promise<void>;\n};\n\nconst TYPES: Record<string, string> = {\n '.png': 'image/png',\n '.json': 'application/json',\n '.jsonl': 'text/plain; charset=utf-8',\n};\n\nconst PAGE_HEADERS = {\n 'Content-Security-Policy':\n \"default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'; form-action 'none'\",\n};\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: string | Uint8Array,\n type = 'text/plain; charset=utf-8',\n headers: Record<string, string> = {}\n): void {\n res.writeHead(status, {\n 'Content-Type': type,\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n 'Referrer-Policy': 'no-referrer',\n ...headers,\n });\n res.end(body);\n}\n\nconst json = (res: ServerResponse, data: unknown) =>\n send(res, 200, JSON.stringify(data), 'application/json');\n\nfunction sameSecret(given: string, token: string): boolean {\n const a = Buffer.from(given);\n const b = Buffer.from(token);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nfunction cookie(req: IncomingMessage, name: string): string | undefined {\n for (const part of (req.headers.cookie ?? '').split(';')) {\n const [key, ...value] = part.trim().split('=');\n if (key === name) return value.join('=');\n }\n return undefined;\n}\n\n/** The real path of `rel` under `root`, or null when it would leave it (`..`, a symlink). */\nasync function confined(root: string, rel: string): Promise<string | null> {\n try {\n const realRoot = await realpath(root);\n const real = await realpath(resolve(root, rel));\n return real.startsWith(realRoot + sep) ? real : null;\n } catch {\n return null;\n }\n}\n\nasync function definitions(root: string, folder: 'checklists' | 'actors') {\n const schema = folder === 'checklists' ? ChecklistSchema : ActorSchema;\n const names = await readdir(join(root, folder)).catch(() => [] as string[]);\n return Promise.all(\n names\n .filter((n) => n.endsWith('.json'))\n .sort()\n .map(async (name) => {\n const slug = basename(name, '.json');\n try {\n return { slug, data: await readJson(join(root, folder, name), schema, name) };\n } catch (err) {\n return { slug, error: (err as Error).message };\n }\n })\n );\n}\n\nasync function readBody(req: IncomingMessage): Promise<string> {\n let body = '';\n for await (const chunk of req) {\n body += chunk;\n if (body.length > 10_000) throw new Error('Body too large');\n }\n return body;\n}\n\nexport async function startUi(options: { root: string; port?: number }): Promise<UiServer> {\n const root = resolve(options.root);\n const token = randomBytes(24).toString('base64url');\n const clients = new Set<ServerResponse>();\n let port = 0;\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const host = req.headers.host ?? '';\n if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {\n return send(res, 403, 'Forbidden');\n }\n const url = new URL(req.url ?? '/', `http://${host}`);\n if (url.pathname === '/__stuntdouble') return json(res, { stuntdouble: true });\n\n // Cookies are per host, not per port, so each server names its own.\n const cookieName = `stuntdouble_${port}`;\n const key = url.searchParams.get('t');\n if (key !== null) {\n if (!sameSecret(key, token)) return send(res, 403, 'This link is out of date.');\n url.searchParams.delete('t');\n return send(res, 302, '', undefined, {\n Location: `${url.pathname}${url.search}`,\n 'Set-Cookie': `${cookieName}=${token}; HttpOnly; SameSite=Strict; Path=/`,\n });\n }\n const signedIn = cookie(req, cookieName);\n if (!signedIn || !sameSecret(signedIn, token)) {\n return send(\n res,\n 403,\n 'Open the viewer from the link Stunt Double printed: it carries the key.'\n );\n }\n\n const path = url.pathname;\n if (req.method === 'POST' && path === '/api/stop') {\n const origin = req.headers.origin;\n if (origin && origin !== `http://${host}`) return send(res, 403, 'Forbidden');\n const { run } = JSON.parse(await readBody(req)) as { run?: unknown };\n const entry = (await readRuns(root)).find((e) => e.id === run);\n if (!entry || !('run' in entry) || effectiveStatus(entry.run) !== 'running') {\n return send(res, 409, 'That run is not going.');\n }\n await writeFileAtomic(join(root, '.live', 'stop'), entry.id);\n return send(res, 204, '');\n }\n if (req.method !== 'GET') return send(res, 405, 'Method not allowed');\n\n if (path === '/' || path === '/definitions' || /^\\/runs\\/[^/]+$/.test(path)) {\n const page = await readFile(new URL('./ui.html', import.meta.url));\n return send(res, 200, page, 'text/html; charset=utf-8', PAGE_HEADERS);\n }\n if (path === '/api/runs') {\n return json(\n res,\n (await readRuns(root)).map((e) =>\n 'run' in e ? { ...e.run, status: effectiveStatus(e.run) } : e\n )\n );\n }\n const runMatch = path.match(/^\\/api\\/runs\\/([^/]+)$/);\n if (runMatch) {\n const id = decodeURIComponent(runMatch[1]!);\n const entry = (await readRuns(root)).find((e) => e.id === id);\n if (!entry) return send(res, 404, 'No such run');\n const activity = await readFile(join(root, 'runs', entry.id, 'activity.jsonl'), 'utf8')\n .then((text) =>\n text\n .split('\\n')\n .filter(Boolean)\n .map((line) => JSON.parse(line))\n )\n .catch(() => []);\n const stop = await readFile(join(root, '.live', 'stop'), 'utf8').catch(() => '');\n return json(res, {\n ...entry,\n ...('run' in entry ? { run: { ...entry.run, status: effectiveStatus(entry.run) } } : {}),\n activity,\n stop_requested: stop.trim() === entry.id,\n });\n }\n if (path === '/api/definitions') {\n return json(res, {\n checklists: await definitions(root, 'checklists'),\n actors: await definitions(root, 'actors'),\n });\n }\n if (path === '/api/events') {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n });\n res.write(': connected\\n\\n');\n clients.add(res);\n req.on('close', () => clients.delete(res));\n return;\n }\n if (path.startsWith('/files/')) {\n const rel = decodeURIComponent(path.slice('/files/'.length));\n const file = await confined(root, rel);\n if (!file) return send(res, 403, 'Outside .stuntdouble/');\n const type = TYPES[extname(file)];\n if (!type) return send(res, 404, 'Not found');\n // A verdict's screenshot never changes once written; the live frame does.\n const cache = rel.includes('/shots/') ? 'private, max-age=86400' : 'no-store';\n return send(res, 200, await readFile(file), type, { 'Cache-Control': cache });\n }\n return send(res, 404, 'Not found');\n }\n\n const server = createServer((req, res) => {\n handle(req, res).catch(() => {\n if (res.headersSent) res.end();\n else send(res, 500, 'Something went wrong reading .stuntdouble/');\n });\n });\n\n const first = options.port ?? FIRST_PORT;\n for (let candidate = first; ; candidate++) {\n try {\n await new Promise<void>((ok, fail) => {\n server.once('error', fail);\n server.listen(candidate, '127.0.0.1', () => {\n server.off('error', fail);\n ok();\n });\n });\n break;\n } catch (err) {\n const busy = (err as NodeJS.ErrnoException).code === 'EADDRINUSE';\n if (!busy || candidate >= first + PORT_TRIES) throw err;\n }\n }\n port = (server.address() as AddressInfo).port;\n\n // Every change under .stuntdouble/ is announced, and the page refetches what\n // it shows. This works the same whether the run is in this process or not.\n let watcher: FSWatcher | null = null;\n let pending = new Set<string>();\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n watcher = watch(root, { recursive: true }, (_event, name) => {\n if (!name || name.endsWith('.tmp')) return;\n pending.add(String(name).split(sep).join('/'));\n timer ??= setTimeout(() => {\n const data = `data: ${JSON.stringify({ paths: [...pending] })}\\n\\n`;\n for (const client of clients) client.write(data);\n pending = new Set();\n timer = undefined;\n }, 150);\n });\n } catch {\n // No .stuntdouble/ yet: the viewer shows the empty state without live updates.\n }\n\n return {\n port,\n token,\n link: (path = '/') => `http://127.0.0.1:${port}${path}?t=${token}`,\n close: async () => {\n watcher?.close();\n clearTimeout(timer);\n for (const client of clients) client.end();\n server.closeAllConnections();\n await new Promise<void>((ok) => server.close(() => ok()));\n },\n };\n}\n"],"mappings":"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","names":[]}
|