@goflag/cli 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_PROFILE,
3
4
  Logger,
5
+ PROFILES,
6
+ PROFILE_NAMES,
4
7
  exitCode,
5
8
  renderSummaryTerminal,
6
9
  renderTerminal,
7
10
  runAudit,
8
11
  summarize
9
- } from "./chunk-3777NRM4.js";
12
+ } from "./chunk-6WOWKLXY.js";
10
13
 
11
14
  // src/report/diff.ts
12
15
  function collectFindings(report) {
@@ -88,11 +91,14 @@ function diffReports(baseline, current) {
88
91
  for (const [id, entry] of before) {
89
92
  if (!now.has(id)) resolved.push(entry);
90
93
  }
94
+ const baselineProfile = baseline.profile ?? DEFAULT_PROFILE;
95
+ const currentProfile = current.profile ?? DEFAULT_PROFILE;
91
96
  return {
92
- baseline: { url: baseline.url, finishedAt: baseline.finishedAt },
97
+ baseline: { url: baseline.url, finishedAt: baseline.finishedAt, profile: baselineProfile },
93
98
  added: sortEntries(added),
94
99
  resolved: sortEntries(resolved),
95
- unchanged
100
+ unchanged,
101
+ ...baselineProfile === currentProfile ? {} : { profileMismatch: { baseline: baselineProfile, current: currentProfile } }
96
102
  };
97
103
  }
98
104
  function diffExitCode(diff, failOn, debt) {
@@ -157,6 +163,14 @@ function renderDiffTerminal(diff, options = {}) {
157
163
  `baseline ${diff.baseline.url} \u2014 taken ${diff.baseline.finishedAt}` + (age === null ? "" : age === 0 ? " (today)" : ` (${age} day${age === 1 ? "" : "s"} ago)`)
158
164
  )
159
165
  );
166
+ if (diff.profileMismatch) {
167
+ lines.push(
168
+ c(
169
+ ANSI.yellow,
170
+ `note: baseline was captured under profile \`${diff.profileMismatch.baseline}\`, this run used \`${diff.profileMismatch.current}\` \u2014 the two are not like-for-like.`
171
+ )
172
+ );
173
+ }
160
174
  lines.push("");
161
175
  if (diff.added.length > 0) {
162
176
  lines.push(c(ANSI.bold, "New findings"));
@@ -271,6 +285,9 @@ Raise --start-timeout, check the command listens on that URL, or point --start-c
271
285
  }
272
286
 
273
287
  // src/cli-args.ts
288
+ var PROFILE_HELP = PROFILE_NAMES.map(
289
+ (name) => ` ${name} \u2014 ${PROFILES[name].description}`
290
+ ).join("\n");
274
291
  var HELP = `goflag \u2014 audit a site for broken links, missing translations, and SEO metadata
275
292
 
276
293
  Usage:
@@ -295,6 +312,16 @@ Options:
295
312
  --no-sitemap Do not discover the sitemap; crawl from <url> only.
296
313
  Discovery is on by default because link-only crawling
297
314
  cannot find locales a site never links to.
315
+ --profile <name> Policy overlay on the rule set \u2014 it changes how
316
+ loudly a rule fires, never what it observes:
317
+ ${PROFILE_HELP}
318
+ --conformance Report EVERY rule's status on every page (pass /
319
+ fail / warn / na), not just the violations. Answers
320
+ "where do we stand against the catalog?"; pairs with
321
+ --json.
322
+ --advisories Attach the prose rules \u2014 the judgment calls goflag
323
+ refuses to fake \u2014 each with the observed facts an
324
+ agent needs to judge it. Never affects the gate.
298
325
  --fail-on <level> Exit 1 at or above this severity: warning (default),
299
326
  error, or never.
300
327
  --regressions-only Weaken the gate: fail only on findings that are NEW
@@ -389,6 +416,12 @@ function parseArgs(argv) {
389
416
  case "--no-sitemap":
390
417
  parsed.options.noSitemap = true;
391
418
  break;
419
+ case "--conformance":
420
+ parsed.options.conformance = true;
421
+ break;
422
+ case "--advisories":
423
+ parsed.options.advisories = true;
424
+ break;
392
425
  case "--ignore-holes":
393
426
  parsed.options.ignoreHoles = [...parsed.options.ignoreHoles ?? [], next(i, arg)];
394
427
  i++;
@@ -401,6 +434,15 @@ function parseArgs(argv) {
401
434
  i++;
402
435
  break;
403
436
  }
437
+ case "--profile": {
438
+ const value = next(i, arg);
439
+ if (!PROFILE_NAMES.includes(value)) {
440
+ throw new Error(`${arg} expects one of: ${PROFILE_NAMES.join(", ")}`);
441
+ }
442
+ parsed.options.profile = value;
443
+ i++;
444
+ break;
445
+ }
404
446
  case "--fail-on": {
405
447
  const value = next(i, arg);
406
448
  if (!FAIL_ON_LEVELS.includes(value)) {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/report/diff.ts","../src/report/render-diff.ts","../src/lib/runner/dev-server.ts","../src/cli-args.ts","../src/cli.ts"],"sourcesContent":["/**\n * Baseline comparison.\n *\n * Every finding already carries a `fingerprint` id derived from the *identity*\n * of the problem rather than its transient details, and page URLs are\n * normalised to origin-independent routes — so the same defect fingerprints\n * identically on localhost, staging and production. That machinery was built\n * for exactly this and had no consumer.\n *\n * What it changes is what the tool *is*. Without a baseline the CI gate is\n * binary: any finding fails, which is unusable on a site that is not clean\n * yet, so teams either never turn it on or learn to ignore it. With one, the\n * gate becomes \"no *new* findings\" — a promise a real codebase can keep while\n * the backlog is worked down.\n *\n * Pure function of two reports. No I/O; the CLI reads the baseline file.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { GoflagReport } from \"./types\";\n\n/** Which part of the report a finding came from. */\nexport type FindingKind =\n \"brokenLink\" | \"unreachablePage\" | \"translationHole\" | \"reciprocity\" | \"seo\" | \"site\";\n\n/** One finding, flattened out of its report section for comparison. */\nexport interface DiffEntry {\n id: string;\n kind: FindingKind;\n /**\n * Severity used for the exit-code decision. Rule findings carry their own;\n * the rest are mapped to match the verdict logic in `build.ts` — a broken\n * link or an unreachable page is an error, a translation gap is a warning.\n */\n severity: Severity;\n /** One-line description, for the rendered diff. */\n summary: string;\n /** Page the finding sits on, when it has one. */\n pageUrl?: string;\n}\n\nexport interface ReportDiff {\n /** Where and when the baseline was taken, echoed for the report header. */\n baseline: { url: string; finishedAt: string };\n /** Findings present now and absent from the baseline — what should fail CI. */\n added: DiffEntry[];\n /** Findings in the baseline that are gone. Worth showing: progress is data too. */\n resolved: DiffEntry[];\n /** Findings present in both. */\n unchanged: number;\n}\n\n/** Flatten every finding collection into one comparable list. */\nexport function collectFindings(report: GoflagReport): DiffEntry[] {\n const out: DiffEntry[] = [];\n\n for (const link of report.brokenLinks) {\n out.push({\n id: link.id,\n kind: \"brokenLink\",\n severity: \"error\",\n summary: `${link.verdict} link → ${link.href}`,\n pageUrl: link.pageUrl,\n });\n }\n\n for (const page of report.unreachablePages) {\n out.push({\n id: page.id,\n kind: \"unreachablePage\",\n severity: \"error\",\n summary: `${page.status || \"network error\"} on ${page.url}`,\n pageUrl: page.url,\n });\n }\n\n for (const hole of report.missingTranslations.holes) {\n out.push({\n id: hole.id,\n kind: \"translationHole\",\n severity: \"warning\",\n summary: `${hole.route} missing ${hole.missingLocales.join(\", \")}`,\n });\n }\n\n for (const issue of report.missingTranslations.reciprocity) {\n out.push({\n id: issue.id,\n kind: \"reciprocity\",\n severity: \"warning\",\n summary: `${issue.code} on ${issue.url}`,\n pageUrl: issue.url,\n });\n }\n\n for (const issue of report.seoIssues) {\n out.push({\n id: issue.id,\n kind: \"seo\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n for (const issue of report.siteIssues) {\n out.push({\n id: issue.id,\n kind: \"site\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n return out;\n}\n\n/** Deterministic ordering so a rendered diff is stable and reviewable. */\nconst SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 };\n\nfunction sortEntries(entries: DiffEntry[]): DiffEntry[] {\n return [...entries].sort(\n (a, b) =>\n SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||\n a.kind.localeCompare(b.kind) ||\n a.summary.localeCompare(b.summary),\n );\n}\n\n/** Total findings the site currently carries, new and known alike. */\nexport function totalFindings(report: GoflagReport): number {\n return collectFindings(report).length;\n}\n\n/** Compare a fresh report against a stored baseline. */\nexport function diffReports(baseline: GoflagReport, current: GoflagReport): ReportDiff {\n const before = new Map(collectFindings(baseline).map((e) => [e.id, e]));\n const now = new Map(collectFindings(current).map((e) => [e.id, e]));\n\n const added: DiffEntry[] = [];\n let unchanged = 0;\n for (const [id, entry] of now) {\n if (before.has(id)) unchanged += 1;\n else added.push(entry);\n }\n\n const resolved: DiffEntry[] = [];\n for (const [id, entry] of before) {\n if (!now.has(id)) resolved.push(entry);\n }\n\n return {\n baseline: { url: baseline.url, finishedAt: baseline.finishedAt },\n added: sortEntries(added),\n resolved: sortEntries(resolved),\n unchanged,\n };\n}\n\n/**\n * Exit code under `--regressions-only`: fail on findings that are *new* at or\n * above the threshold, or on a debt budget being exceeded.\n *\n * Deliberately blind to the overall verdict, unlike `exitCode`. A site with a\n * hundred known problems and no new ones passes — that is what the mode is\n * for, and why it has to be asked for by name.\n *\n * `maxDebt` is the counterweight. Gating on regressions alone lets a backlog\n * sit untouched forever behind a passing build; a ceiling you lower as you fix\n * is the only part of this design that makes the debt actually go down.\n */\nexport function diffExitCode(\n diff: ReportDiff,\n failOn: \"warning\" | \"error\" | \"never\",\n debt?: { total: number; max?: number },\n): number {\n if (debt?.max !== undefined && debt.total > debt.max) return 1;\n if (failOn === \"never\") return 0;\n const threshold = failOn === \"error\" ? 0 : 1;\n const blocking = diff.added.filter((e) => SEVERITY_RANK[e.severity] <= threshold);\n return blocking.length > 0 ? 1 : 0;\n}\n","/**\n * Render a `ReportDiff` for a terminal.\n *\n * Deliberately leads with what is new, because that is what fails the build\n * and what the reader has to act on. Resolved findings come second and are\n * kept — a run that only ever shows problems gives no signal that the backlog\n * is moving, which is how a gate stops being read.\n *\n * Pure function of the diff; no I/O.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { DiffEntry, ReportDiff } from \"./diff\";\n\ninterface RenderOptions {\n color?: boolean;\n /** Injected clock, so the rendered age is testable. */\n now?: number;\n}\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nfunction paint(enabled: boolean) {\n return (code: string, text: string) => (enabled ? `${code}${text}${ANSI.reset}` : text);\n}\n\nfunction severityTag(c: (code: string, t: string) => string, severity: Severity): string {\n if (severity === \"error\") return c(ANSI.red, \"error\");\n if (severity === \"warning\") return c(ANSI.yellow, \"warn \");\n return c(ANSI.dim, \"info \");\n}\n\n/** How many entries to list before collapsing into a \"+N more\" line. */\nconst LIST_LIMIT = 20;\n\nfunction listEntries(\n entries: DiffEntry[],\n marker: string,\n markerColor: string,\n c: (code: string, t: string) => string,\n): string[] {\n const lines: string[] = [];\n for (const entry of entries.slice(0, LIST_LIMIT)) {\n // A broken link's identity is (page, target): the same dead URL referenced\n // from four pages is four findings, and without the page they render as\n // four identical lines that look like a rendering bug.\n const where =\n entry.pageUrl && !entry.summary.includes(entry.pageUrl)\n ? c(ANSI.dim, ` on ${entry.pageUrl}`)\n : \"\";\n lines.push(\n ` ${c(markerColor, marker)} ${severityTag(c, entry.severity)} ${c(ANSI.dim, entry.kind)} ${entry.summary}${where}`,\n );\n }\n if (entries.length > LIST_LIMIT) {\n lines.push(` ${c(ANSI.dim, `… +${entries.length - LIST_LIMIT} more`)}`);\n }\n return lines;\n}\n\n/** Whole days between the baseline timestamp and now, or null if unparseable. */\nfunction baselineAgeDays(finishedAt: string, now: number): number | null {\n const taken = Date.parse(finishedAt);\n if (Number.isNaN(taken)) return null;\n return Math.max(0, Math.floor((now - taken) / 86_400_000));\n}\n\nexport function renderDiffTerminal(diff: ReportDiff, options: RenderOptions = {}): string {\n const c = paint(options.color ?? false);\n const lines: string[] = [];\n const debt = diff.unchanged;\n\n // Never green, and never the word \"clean\". This mode passes builds on sites\n // with known defects; a green flag would say the opposite of what happened,\n // and a reader who learns to see green here stops reading the number beside\n // it. The debt is the headline, not a footnote.\n const verdict =\n diff.added.length > 0\n ? c(ANSI.red, \"REGRESSION\")\n : debt > 0\n ? c(ANSI.yellow, \"REGRESSION GATE\")\n : c(ANSI.green, \"CLEAN\");\n\n const age = baselineAgeDays(diff.baseline.finishedAt, options.now ?? Date.now());\n\n lines.push(\"\");\n lines.push(`${c(ANSI.bold, \"goflag\")} ${c(ANSI.dim, \"--regressions-only\")}`);\n lines.push(\n `${verdict} ${diff.added.length} new · ` +\n (debt > 0\n ? c(ANSI.bold, `${debt} known findings NOT gating this build`)\n : c(ANSI.dim, \"no known findings\")) +\n (diff.resolved.length > 0 ? c(ANSI.dim, ` · ${diff.resolved.length} resolved`) : \"\"),\n );\n lines.push(\n c(\n ANSI.dim,\n `baseline ${diff.baseline.url} — taken ${diff.baseline.finishedAt}` +\n (age === null ? \"\" : age === 0 ? \" (today)\" : ` (${age} day${age === 1 ? \"\" : \"s\"} ago)`),\n ),\n );\n lines.push(\"\");\n\n if (diff.added.length > 0) {\n lines.push(c(ANSI.bold, \"New findings\"));\n lines.push(...listEntries(diff.added, \"+\", ANSI.red, c));\n lines.push(\"\");\n }\n\n if (diff.resolved.length > 0) {\n lines.push(c(ANSI.bold, \"Resolved\"));\n lines.push(...listEntries(diff.resolved, \"-\", ANSI.green, c));\n lines.push(\"\");\n }\n\n if (diff.added.length === 0 && diff.resolved.length === 0) {\n lines.push(\n c(\n ANSI.dim,\n debt > 0\n ? `Nothing moved since the baseline — ${debt} findings still open. Run without --regressions-only to see them.`\n : \"Nothing moved since the baseline.\",\n ),\n );\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Boot-and-audit support for `--start`.\n *\n * goflag was only ever able to audit a deployed site. That makes it a post-hoc\n * auditor, and post-hoc auditors do not get run: by the time a finding appears,\n * the regression already shipped. `--start` closes that gap — boot the app,\n * wait until it answers, audit `localhost`, tear it down — so the same checks\n * can gate a merge instead of documenting a mistake.\n *\n * Deliberately dumb about readiness: rather than parsing a framework's stdout\n * for \"ready in 1.2s\" (every framework words it differently, and the wording\n * changes between minor versions), it polls the URL goflag is about to audit.\n * If that URL answers, the audit can proceed — which is the only definition of\n * \"ready\" that actually matters here.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\n\nexport interface StartedServer {\n /** Terminate the process group and resolve once it is gone. */\n stop: () => Promise<void>;\n}\n\nexport interface StartServerOptions {\n /** Shell command to run, e.g. `pnpm start`. */\n command: string;\n /**\n * Directory to run it in. Defaults to the process's own, which is right for\n * the usual `cd my-site && npx goflag …` — but not for a monorepo audited\n * from its root, where the app lives a few directories down.\n */\n cwd?: string;\n /** URL polled until it answers; normally the audit entry point. */\n url: string;\n /** Give up after this long. Defaults to 60s. */\n timeoutMs?: number;\n /** Poll interval. Defaults to 250ms. */\n intervalMs?: number;\n /** Called with each stdout/stderr chunk, for `--verbose`. */\n onOutput?: (chunk: string) => void;\n /** Accept self-signed TLS while polling (a local https dev server). */\n allowInsecureTls?: boolean;\n}\n\n/** True once the URL returns any HTTP response at all. */\nasync function answers(url: string, allowInsecureTls: boolean): Promise<boolean> {\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n if (allowInsecureTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n try {\n // Any status counts: a 404 on the entry path still proves the server is\n // up, and the audit itself is what judges the response.\n await fetch(url, { redirect: \"manual\" });\n return true;\n } catch {\n return false;\n } finally {\n if (allowInsecureTls) {\n if (previous === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n else process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n }\n }\n}\n\nfunction terminate(child: ChildProcess): Promise<void> {\n return new Promise((resolve) => {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolve();\n return;\n }\n\n // Kill the whole group: `pnpm start` spawns the real server as a child,\n // and signalling only the wrapper orphans a process holding the port.\n const killGroup = (signal: NodeJS.Signals) => {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n // Already dead, or the group is gone — nothing left to do.\n }\n };\n\n const force = setTimeout(() => killGroup(\"SIGKILL\"), 5_000);\n child.once(\"exit\", () => {\n clearTimeout(force);\n resolve();\n });\n killGroup(\"SIGTERM\");\n });\n}\n\n/**\n * Spawn `command` and resolve once `url` answers.\n *\n * Rejects — after stopping the process — when the command exits early or the\n * timeout expires. Both are hard failures: auditing a server that never came\n * up would report a site-wide outage as a hundred SEO findings.\n */\nexport async function startServer(options: StartServerOptions): Promise<StartedServer> {\n const timeoutMs = options.timeoutMs ?? 60_000;\n const intervalMs = options.intervalMs ?? 250;\n const allowInsecureTls = options.allowInsecureTls === true;\n\n const cwd = options.cwd ?? process.cwd();\n const child = spawn(options.command, {\n cwd,\n shell: true,\n // Own process group, so `terminate` can take the whole tree down.\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let exited: { code: number | null; signal: NodeJS.Signals | null } | undefined;\n child.once(\"exit\", (code, signal) => {\n exited = { code, signal };\n });\n\n const tail: string[] = [];\n const record = (chunk: Buffer) => {\n const text = chunk.toString();\n options.onOutput?.(text);\n // Keep a short tail so a boot failure can be reported with its own words\n // instead of a bare \"command exited\".\n tail.push(text);\n if (tail.length > 40) tail.shift();\n };\n child.stdout?.on(\"data\", record);\n child.stderr?.on(\"data\", record);\n\n const server: StartedServer = { stop: () => terminate(child) };\n const deadline = Date.now() + timeoutMs;\n\n for (;;) {\n if (exited) {\n await server.stop();\n const why = exited.signal ? `signal ${exited.signal}` : `exit code ${exited.code}`;\n // Naming the directory is most of the diagnosis: a command that runs\n // fine by hand and dies here has almost always been started somewhere\n // other than where its package.json lives.\n throw new Error(\n `--start command exited before ${options.url} answered (${why}).\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `${tail.join(\"\").trimEnd()}`,\n );\n }\n if (await answers(options.url, allowInsecureTls)) return server;\n if (Date.now() >= deadline) {\n await server.stop();\n throw new Error(\n `--start command did not serve ${options.url} within ${timeoutMs}ms.\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `Raise --start-timeout, check the command listens on that URL, or point ` +\n `--start-cwd at the directory it should run in.`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n}\n","/**\n * CLI argument parsing — split out from `cli.ts` so it can be unit-tested\n * without importing the module's top-level `main()` side effect.\n *\n * `parseArgs` is a pure function of `argv` (plus the ambient TTY / NO_COLOR\n * signals used only for the default color choice). It throws a plain `Error`\n * with a user-facing message on malformed input; the CLI turns that into an\n * exit-code-2 with the help text.\n */\n\nimport type { AuditOptions, FailOn } from \"./report/build\";\nimport type { LogMode } from \"./report/logger\";\n\nexport const HELP = `goflag — audit a site for broken links, missing translations, and SEO metadata\n\nUsage:\n goflag <url> [options]\n\nOptions:\n --json Print the JSON report to stdout (nothing else).\n --summary, -s Roll findings up (dedup by link/rule/code). Pairs\n with --json for a compact, agent-friendly payload.\n --report <file> Write the (full) JSON report to <file>.\n --depth <n> Crawl depth (0 = entry page only). Default: 2.\n --max-pages <n> Hard cap on pages crawled. Default: 200.\n --include <glob> Only crawl paths matching <glob> (repeatable).\n --exclude <glob> Skip paths matching <glob> (repeatable).\n --locales <list> Comma-separated locales the site serves, e.g.\n \"fr,en,pt-br\". Authoritative: overrides what the\n sitemap and crawl suggest, and makes a locale the\n site does not serve yet show up as missing.\n --ignore-holes <glob> Route (locale-free) that is deliberately not\n translated everywhere, so its gaps are not reported\n as missing translations (repeatable).\n --no-sitemap Do not discover the sitemap; crawl from <url> only.\n Discovery is on by default because link-only crawling\n cannot find locales a site never links to.\n --fail-on <level> Exit 1 at or above this severity: warning (default),\n error, or never.\n --regressions-only Weaken the gate: fail only on findings that are NEW\n relative to --baseline. Known findings stop blocking\n the build — a green run no longer means a clean site.\n Requires --baseline.\n --baseline <file> Stored report to compare against. Use with\n --regressions-only.\n --update-baseline Write this run to --baseline and exit 0, instead of\n judging against it. Use it to capture a baseline, or\n to accept findings you have decided to live with —\n it says what it accepted.\n --max-debt <n> Fail when the site carries more than <n> findings in\n total, whether new or known. Lower it as you fix, to\n stop a baseline from fossilising.\n --start <cmd> Boot <cmd>, wait for <url> to answer, audit, then\n stop it. Use to gate a merge on the built app before\n it ships.\n --start-cwd <dir> Directory to run --start in. Defaults to the current\n one; set it when auditing a monorepo package from the\n repository root.\n --start-timeout <ms> How long to wait for --start to answer. Default: 60000.\n --no-external Do not probe off-origin (external) links.\n --static Static HTML only; never launch headless Chromium.\n --allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).\n --timeout <ms> Per-request timeout in ms. Default: 8000.\n --verbose, -V Log every page as it is analyzed.\n --quiet, -q Suppress the live progress output.\n --no-color Disable coloured output.\n -h, --help Show this help.\n -v, --version Show the version.\n\nExit codes: 0 clean, 1 findings found, 2 fatal error.`;\n\nexport interface ParsedArgs {\n url?: string;\n json: boolean;\n summary: boolean;\n report?: string;\n color: boolean;\n help: boolean;\n version: boolean;\n /** Live-progress verbosity for the logger. */\n logMode: LogMode;\n /** Severity at or above which the process exits 1. */\n failOn: FailOn;\n /** Path to a stored report to compare against (`--baseline`). */\n baseline?: string;\n /** Gate on new findings only, letting known ones through. */\n regressionsOnly: boolean;\n /** Write the run to `--baseline` rather than judging against it. */\n updateBaseline: boolean;\n /** Hard ceiling on total findings, new or known. */\n maxDebt?: number;\n /** Command to boot before auditing, and stop afterwards (`--start`). */\n start?: string;\n /** How long to wait for `--start` to answer, in ms. */\n startTimeoutMs: number;\n /** Directory to run the `--start` command in. */\n startCwd?: string;\n options: AuditOptions;\n}\n\nconst FAIL_ON_LEVELS: readonly FailOn[] = [\"warning\", \"error\", \"never\"];\n\nexport function parseArgs(argv: string[]): ParsedArgs {\n const parsed: ParsedArgs = {\n json: false,\n summary: false,\n color: process.stdout.isTTY === true && !process.env.NO_COLOR,\n help: false,\n version: false,\n logMode: \"compact\",\n failOn: \"warning\",\n regressionsOnly: false,\n updateBaseline: false,\n startTimeoutMs: 60_000,\n options: { include: [], exclude: [] },\n };\n\n const next = (i: number, flag: string): string => {\n const value = argv[i + 1];\n if (value === undefined) throw new Error(`${flag} requires a value`);\n return value;\n };\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case \"-h\":\n case \"--help\":\n parsed.help = true;\n break;\n case \"-v\":\n case \"--version\":\n parsed.version = true;\n break;\n case \"--json\":\n parsed.json = true;\n break;\n case \"-s\":\n case \"--summary\":\n parsed.summary = true;\n break;\n case \"--no-color\":\n parsed.color = false;\n break;\n case \"-V\":\n case \"--verbose\":\n parsed.logMode = \"verbose\";\n break;\n case \"-q\":\n case \"--quiet\":\n parsed.logMode = \"quiet\";\n break;\n case \"--static\":\n parsed.options.static = true;\n break;\n case \"--allow-insecure-tls\":\n parsed.options.allowInsecureTls = true;\n break;\n case \"--no-external\":\n parsed.options.checkExternal = false;\n break;\n case \"--no-sitemap\":\n parsed.options.noSitemap = true;\n break;\n case \"--ignore-holes\":\n parsed.options.ignoreHoles = [...(parsed.options.ignoreHoles ?? []), next(i, arg)];\n i++;\n break;\n case \"--locales\": {\n // Split on commas so `--locales fr,en` and repeated flags both work.\n const value = next(i, arg);\n const tags = value\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n if (tags.length === 0) throw new Error(`${arg} expects at least one locale`);\n parsed.options.locales = [...(parsed.options.locales ?? []), ...tags];\n i++;\n break;\n }\n case \"--fail-on\": {\n const value = next(i, arg) as FailOn;\n if (!FAIL_ON_LEVELS.includes(value)) {\n throw new Error(`${arg} expects one of: ${FAIL_ON_LEVELS.join(\", \")}`);\n }\n parsed.failOn = value;\n i++;\n break;\n }\n case \"--start\":\n parsed.start = next(i, arg);\n i++;\n break;\n case \"--start-timeout\":\n parsed.startTimeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--start-cwd\":\n parsed.startCwd = next(i, arg);\n i++;\n break;\n case \"--report\":\n parsed.report = next(i, arg);\n i++;\n break;\n case \"--baseline\":\n parsed.baseline = next(i, arg);\n i++;\n break;\n case \"--update-baseline\":\n parsed.updateBaseline = true;\n break;\n case \"--regressions-only\":\n parsed.regressionsOnly = true;\n break;\n case \"--max-debt\":\n parsed.maxDebt = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--depth\":\n parsed.options.depth = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--max-pages\":\n parsed.options.maxPages = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--timeout\":\n parsed.options.timeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--include\":\n parsed.options.include!.push(next(i, arg));\n i++;\n break;\n case \"--exclude\":\n parsed.options.exclude!.push(next(i, arg));\n i++;\n break;\n default:\n if (arg && arg.startsWith(\"-\")) throw new Error(`unknown option: ${arg}`);\n if (arg && !parsed.url) parsed.url = arg;\n else if (arg) throw new Error(`unexpected argument: ${arg}`);\n }\n }\n\n // Each flag is meaningless without the other, and guessing which one the\n // caller meant would silently change how strict the build is.\n if (parsed.updateBaseline && !parsed.baseline) {\n throw new Error(\"--update-baseline needs a --baseline <file> to write to\");\n }\n // Capturing a baseline is not gating against one, so the explicit opt-in that\n // --regressions-only exists to force does not apply.\n if (parsed.baseline && !parsed.regressionsOnly && !parsed.updateBaseline) {\n throw new Error(\n \"--baseline weakens the gate, so it must be requested explicitly: add --regressions-only\",\n );\n }\n if (parsed.regressionsOnly && !parsed.baseline) {\n throw new Error(\"--regressions-only needs a --baseline <file> to compare against\");\n }\n\n return parsed;\n}\n\nfunction toInt(value: string, flag: string): number {\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} expects a non-negative integer`);\n return n;\n}\n","/**\n * goflag CLI.\n *\n * goflag <url> [options]\n *\n * Crawls a site and reports broken links, missing translation pages, and\n * missing/misconfigured SEO metadata. The JSON report is the source of\n * truth; the terminal output is rendered from it.\n *\n * Exit codes:\n * 0 clean (green flag)\n * 1 findings present (yellow/red flag) — useful as a CI gate\n * 2 fatal (bad URL, unexpected error)\n */\n\nimport { runAudit, exitCode } from \"./report/build\";\nimport { diffReports, diffExitCode, totalFindings } from \"./report/diff\";\nimport { renderDiffTerminal } from \"./report/render-diff\";\nimport { startServer, type StartedServer } from \"./lib/runner/dev-server\";\nimport { renderTerminal } from \"./report/render-terminal\";\nimport { renderSummaryTerminal } from \"./report/render-summary\";\nimport { summarize } from \"./report/summarize\";\nimport { Logger } from \"./report/logger\";\nimport { HELP, parseArgs, type ParsedArgs } from \"./cli-args\";\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const { fileURLToPath } = await import(\"node:url\");\n const { dirname, join } = await import(\"node:path\");\n const here = dirname(fileURLToPath(import.meta.url));\n const pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nasync function main(): Promise<number> {\n let args: ParsedArgs;\n try {\n args = parseArgs(process.argv.slice(2));\n } catch (err) {\n process.stderr.write(`goflag: ${(err as Error).message}\\n\\n${HELP}\\n`);\n return 2;\n }\n\n if (args.help) {\n process.stdout.write(`${HELP}\\n`);\n return 0;\n }\n if (args.version) {\n process.stdout.write(`${await readVersion()}\\n`);\n return 0;\n }\n if (!args.url) {\n process.stderr.write(`goflag: missing <url>\\n\\n${HELP}\\n`);\n return 2;\n }\n\n // Progress always goes to stderr, so stdout (JSON / rendered report)\n // stays clean and pipeable. Colour follows the stderr TTY.\n const logger = new Logger({\n stream: process.stderr,\n mode: args.logMode,\n color: process.stderr.isTTY === true && !process.env.NO_COLOR,\n });\n\n // `--start` owns a child process for the whole audit; it must be torn down\n // on every exit path, including a failed audit, or CI leaks a held port.\n let server: StartedServer | undefined;\n let report;\n try {\n if (args.start) {\n logger.note(`goflag: starting \\`${args.start}\\` …`);\n server = await startServer({\n command: args.start,\n cwd: args.startCwd,\n url: args.url,\n timeoutMs: args.startTimeoutMs,\n allowInsecureTls: args.options.allowInsecureTls,\n onOutput: args.logMode === \"verbose\" ? (chunk) => process.stderr.write(chunk) : undefined,\n });\n }\n logger.note(`goflag: auditing ${args.url} …`);\n report = await runAudit(args.url, { ...args.options, onProgress: logger.onProgress });\n logger.stop();\n } catch (err) {\n logger.stop();\n process.stderr.write(`goflag: ${(err as Error).message}\\n`);\n return 2;\n } finally {\n await server?.stop();\n }\n\n // A baseline changes the question from \"is this site clean?\" to \"did this\n // change make it worse?\" — the only one a site with a backlog can answer.\n if (args.baseline) {\n const { existsSync, readFileSync } = await import(\"node:fs\");\n // Capturing a baseline for the first time is the one case where the file\n // is legitimately absent. Everywhere else a missing baseline is a typo,\n // and continuing would turn it into a green build.\n if (existsSync(args.baseline) || !args.updateBaseline) {\n try {\n const baseline = JSON.parse(readFileSync(args.baseline, \"utf8\"));\n report.diff = diffReports(baseline, report);\n } catch (err) {\n process.stderr.write(\n `goflag: could not read baseline ${args.baseline}: ${(err as Error).message}\\n`,\n );\n return 2;\n }\n }\n }\n\n // Writing the baseline is accepting everything in it. The one thing this\n // must not do is accept it quietly: a counter that drops without explanation\n // reads as \"the problem went away\".\n if (args.updateBaseline && args.baseline) {\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.baseline, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n const total = totalFindings(report);\n if (report.diff) {\n const { added, resolved } = report.diff;\n process.stdout.write(\n `goflag: baseline updated — ${added.length} newly accepted, ${resolved.length} resolved, ` +\n `${total} findings now grandfathered in ${args.baseline}\\n`,\n );\n if (added.length > 0) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n }\n } else {\n process.stdout.write(\n `goflag: baseline captured — ${total} findings grandfathered in ${args.baseline}\\n`,\n );\n }\n process.stdout.write(\n `goflag: set --max-debt ${total} to stop that number growing, and lower it as you fix.\\n`,\n );\n return 0;\n }\n\n if (args.report) {\n // The report file is always the full report — the source of truth a\n // baseline/diff can rely on, regardless of the --summary view choice.\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.report, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n process.stderr.write(`goflag: report written to ${args.report}\\n`);\n }\n\n // In baseline mode the diff *is* the answer. Printing the full report first\n // buries \"nothing changed\" under a hundred known findings — the reader stops\n // looking, which is precisely the failure the baseline exists to prevent.\n // The complete report stays available via --json and --report.\n if (report.diff && !args.json) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n }\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n if (args.summary) {\n const summary = summarize(report);\n process.stdout.write(\n args.json\n ? `${JSON.stringify(summary, null, 2)}\\n`\n : `${renderSummaryTerminal(summary, { color: args.color })}\\n`,\n );\n } else {\n process.stdout.write(\n args.json\n ? `${JSON.stringify(report, null, 2)}\\n`\n : `${renderTerminal(report, { color: args.color })}\\n`,\n );\n }\n\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n return 1;\n }\n\n if (report.diff) {\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n return exitCode(report, args.failOn);\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(`goflag: unexpected error: ${(err as Error).stack ?? err}\\n`);\n process.exit(2);\n });\n"],"mappings":"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","names":["total"]}
1
+ {"version":3,"sources":["../src/report/diff.ts","../src/report/render-diff.ts","../src/lib/runner/dev-server.ts","../src/cli-args.ts","../src/cli.ts"],"sourcesContent":["/**\n * Baseline comparison.\n *\n * Every finding already carries a `fingerprint` id derived from the *identity*\n * of the problem rather than its transient details, and page URLs are\n * normalised to origin-independent routes — so the same defect fingerprints\n * identically on localhost, staging and production. That machinery was built\n * for exactly this and had no consumer.\n *\n * What it changes is what the tool *is*. Without a baseline the CI gate is\n * binary: any finding fails, which is unusable on a site that is not clean\n * yet, so teams either never turn it on or learn to ignore it. With one, the\n * gate becomes \"no *new* findings\" — a promise a real codebase can keep while\n * the backlog is worked down.\n *\n * Pure function of two reports. No I/O; the CLI reads the baseline file.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport { DEFAULT_PROFILE } from \"../lib/rules/profiles\";\nimport type { GoflagReport } from \"./types\";\n\n/** Which part of the report a finding came from. */\nexport type FindingKind =\n \"brokenLink\" | \"unreachablePage\" | \"translationHole\" | \"reciprocity\" | \"seo\" | \"site\";\n\n/** One finding, flattened out of its report section for comparison. */\nexport interface DiffEntry {\n id: string;\n kind: FindingKind;\n /**\n * Severity used for the exit-code decision. Rule findings carry their own;\n * the rest are mapped to match the verdict logic in `build.ts` — a broken\n * link or an unreachable page is an error, a translation gap is a warning.\n */\n severity: Severity;\n /** One-line description, for the rendered diff. */\n summary: string;\n /** Page the finding sits on, when it has one. */\n pageUrl?: string;\n}\n\nexport interface ReportDiff {\n /** Where, when, and under which rule profile the baseline was taken. */\n baseline: { url: string; finishedAt: string; profile: string };\n /**\n * Set when this run was judged under a different profile than the baseline\n * was captured under. Not an error — comparing `spec-only` against a\n * `default` baseline is legitimate, it is just not like-for-like, and\n * \"0 new findings\" means less than it appears. So it is reported loudly\n * and gates nothing: silently diffing across two policies is the failure\n * mode worth preventing.\n */\n profileMismatch?: { baseline: string; current: string };\n /** Findings present now and absent from the baseline — what should fail CI. */\n added: DiffEntry[];\n /** Findings in the baseline that are gone. Worth showing: progress is data too. */\n resolved: DiffEntry[];\n /** Findings present in both. */\n unchanged: number;\n}\n\n/** Flatten every finding collection into one comparable list. */\nexport function collectFindings(report: GoflagReport): DiffEntry[] {\n const out: DiffEntry[] = [];\n\n for (const link of report.brokenLinks) {\n out.push({\n id: link.id,\n kind: \"brokenLink\",\n severity: \"error\",\n summary: `${link.verdict} link → ${link.href}`,\n pageUrl: link.pageUrl,\n });\n }\n\n for (const page of report.unreachablePages) {\n out.push({\n id: page.id,\n kind: \"unreachablePage\",\n severity: \"error\",\n summary: `${page.status || \"network error\"} on ${page.url}`,\n pageUrl: page.url,\n });\n }\n\n for (const hole of report.missingTranslations.holes) {\n out.push({\n id: hole.id,\n kind: \"translationHole\",\n severity: \"warning\",\n summary: `${hole.route} missing ${hole.missingLocales.join(\", \")}`,\n });\n }\n\n for (const issue of report.missingTranslations.reciprocity) {\n out.push({\n id: issue.id,\n kind: \"reciprocity\",\n severity: \"warning\",\n summary: `${issue.code} on ${issue.url}`,\n pageUrl: issue.url,\n });\n }\n\n for (const issue of report.seoIssues) {\n out.push({\n id: issue.id,\n kind: \"seo\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n for (const issue of report.siteIssues) {\n out.push({\n id: issue.id,\n kind: \"site\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n return out;\n}\n\n/** Deterministic ordering so a rendered diff is stable and reviewable. */\nconst SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 };\n\nfunction sortEntries(entries: DiffEntry[]): DiffEntry[] {\n return [...entries].sort(\n (a, b) =>\n SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||\n a.kind.localeCompare(b.kind) ||\n a.summary.localeCompare(b.summary),\n );\n}\n\n/** Total findings the site currently carries, new and known alike. */\nexport function totalFindings(report: GoflagReport): number {\n return collectFindings(report).length;\n}\n\n/** Compare a fresh report against a stored baseline. */\nexport function diffReports(baseline: GoflagReport, current: GoflagReport): ReportDiff {\n const before = new Map(collectFindings(baseline).map((e) => [e.id, e]));\n const now = new Map(collectFindings(current).map((e) => [e.id, e]));\n\n const added: DiffEntry[] = [];\n let unchanged = 0;\n for (const [id, entry] of now) {\n if (before.has(id)) unchanged += 1;\n else added.push(entry);\n }\n\n const resolved: DiffEntry[] = [];\n for (const [id, entry] of before) {\n if (!now.has(id)) resolved.push(entry);\n }\n\n // A baseline stored before profiles existed has no `profile`; the behaviour\n // it captured is what `default` still does, so that is the honest reading.\n const baselineProfile = baseline.profile ?? DEFAULT_PROFILE;\n const currentProfile = current.profile ?? DEFAULT_PROFILE;\n\n return {\n baseline: { url: baseline.url, finishedAt: baseline.finishedAt, profile: baselineProfile },\n added: sortEntries(added),\n resolved: sortEntries(resolved),\n unchanged,\n ...(baselineProfile === currentProfile\n ? {}\n : { profileMismatch: { baseline: baselineProfile, current: currentProfile } }),\n };\n}\n\n/**\n * Exit code under `--regressions-only`: fail on findings that are *new* at or\n * above the threshold, or on a debt budget being exceeded.\n *\n * Deliberately blind to the overall verdict, unlike `exitCode`. A site with a\n * hundred known problems and no new ones passes — that is what the mode is\n * for, and why it has to be asked for by name.\n *\n * `maxDebt` is the counterweight. Gating on regressions alone lets a backlog\n * sit untouched forever behind a passing build; a ceiling you lower as you fix\n * is the only part of this design that makes the debt actually go down.\n */\nexport function diffExitCode(\n diff: ReportDiff,\n failOn: \"warning\" | \"error\" | \"never\",\n debt?: { total: number; max?: number },\n): number {\n if (debt?.max !== undefined && debt.total > debt.max) return 1;\n if (failOn === \"never\") return 0;\n const threshold = failOn === \"error\" ? 0 : 1;\n const blocking = diff.added.filter((e) => SEVERITY_RANK[e.severity] <= threshold);\n return blocking.length > 0 ? 1 : 0;\n}\n","/**\n * Render a `ReportDiff` for a terminal.\n *\n * Deliberately leads with what is new, because that is what fails the build\n * and what the reader has to act on. Resolved findings come second and are\n * kept — a run that only ever shows problems gives no signal that the backlog\n * is moving, which is how a gate stops being read.\n *\n * Pure function of the diff; no I/O.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { DiffEntry, ReportDiff } from \"./diff\";\n\ninterface RenderOptions {\n color?: boolean;\n /** Injected clock, so the rendered age is testable. */\n now?: number;\n}\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nfunction paint(enabled: boolean) {\n return (code: string, text: string) => (enabled ? `${code}${text}${ANSI.reset}` : text);\n}\n\nfunction severityTag(c: (code: string, t: string) => string, severity: Severity): string {\n if (severity === \"error\") return c(ANSI.red, \"error\");\n if (severity === \"warning\") return c(ANSI.yellow, \"warn \");\n return c(ANSI.dim, \"info \");\n}\n\n/** How many entries to list before collapsing into a \"+N more\" line. */\nconst LIST_LIMIT = 20;\n\nfunction listEntries(\n entries: DiffEntry[],\n marker: string,\n markerColor: string,\n c: (code: string, t: string) => string,\n): string[] {\n const lines: string[] = [];\n for (const entry of entries.slice(0, LIST_LIMIT)) {\n // A broken link's identity is (page, target): the same dead URL referenced\n // from four pages is four findings, and without the page they render as\n // four identical lines that look like a rendering bug.\n const where =\n entry.pageUrl && !entry.summary.includes(entry.pageUrl)\n ? c(ANSI.dim, ` on ${entry.pageUrl}`)\n : \"\";\n lines.push(\n ` ${c(markerColor, marker)} ${severityTag(c, entry.severity)} ${c(ANSI.dim, entry.kind)} ${entry.summary}${where}`,\n );\n }\n if (entries.length > LIST_LIMIT) {\n lines.push(` ${c(ANSI.dim, `… +${entries.length - LIST_LIMIT} more`)}`);\n }\n return lines;\n}\n\n/** Whole days between the baseline timestamp and now, or null if unparseable. */\nfunction baselineAgeDays(finishedAt: string, now: number): number | null {\n const taken = Date.parse(finishedAt);\n if (Number.isNaN(taken)) return null;\n return Math.max(0, Math.floor((now - taken) / 86_400_000));\n}\n\nexport function renderDiffTerminal(diff: ReportDiff, options: RenderOptions = {}): string {\n const c = paint(options.color ?? false);\n const lines: string[] = [];\n const debt = diff.unchanged;\n\n // Never green, and never the word \"clean\". This mode passes builds on sites\n // with known defects; a green flag would say the opposite of what happened,\n // and a reader who learns to see green here stops reading the number beside\n // it. The debt is the headline, not a footnote.\n const verdict =\n diff.added.length > 0\n ? c(ANSI.red, \"REGRESSION\")\n : debt > 0\n ? c(ANSI.yellow, \"REGRESSION GATE\")\n : c(ANSI.green, \"CLEAN\");\n\n const age = baselineAgeDays(diff.baseline.finishedAt, options.now ?? Date.now());\n\n lines.push(\"\");\n lines.push(`${c(ANSI.bold, \"goflag\")} ${c(ANSI.dim, \"--regressions-only\")}`);\n lines.push(\n `${verdict} ${diff.added.length} new · ` +\n (debt > 0\n ? c(ANSI.bold, `${debt} known findings NOT gating this build`)\n : c(ANSI.dim, \"no known findings\")) +\n (diff.resolved.length > 0 ? c(ANSI.dim, ` · ${diff.resolved.length} resolved`) : \"\"),\n );\n lines.push(\n c(\n ANSI.dim,\n `baseline ${diff.baseline.url} — taken ${diff.baseline.finishedAt}` +\n (age === null ? \"\" : age === 0 ? \" (today)\" : ` (${age} day${age === 1 ? \"\" : \"s\"} ago)`),\n ),\n );\n // Loud, not dim: a cross-profile diff still prints \"0 new\", and that number\n // means something different from what the reader assumes.\n if (diff.profileMismatch) {\n lines.push(\n c(\n ANSI.yellow,\n `note: baseline was captured under profile \\`${diff.profileMismatch.baseline}\\`, ` +\n `this run used \\`${diff.profileMismatch.current}\\` — the two are not like-for-like.`,\n ),\n );\n }\n lines.push(\"\");\n\n if (diff.added.length > 0) {\n lines.push(c(ANSI.bold, \"New findings\"));\n lines.push(...listEntries(diff.added, \"+\", ANSI.red, c));\n lines.push(\"\");\n }\n\n if (diff.resolved.length > 0) {\n lines.push(c(ANSI.bold, \"Resolved\"));\n lines.push(...listEntries(diff.resolved, \"-\", ANSI.green, c));\n lines.push(\"\");\n }\n\n if (diff.added.length === 0 && diff.resolved.length === 0) {\n lines.push(\n c(\n ANSI.dim,\n debt > 0\n ? `Nothing moved since the baseline — ${debt} findings still open. Run without --regressions-only to see them.`\n : \"Nothing moved since the baseline.\",\n ),\n );\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Boot-and-audit support for `--start`.\n *\n * goflag was only ever able to audit a deployed site. That makes it a post-hoc\n * auditor, and post-hoc auditors do not get run: by the time a finding appears,\n * the regression already shipped. `--start` closes that gap — boot the app,\n * wait until it answers, audit `localhost`, tear it down — so the same checks\n * can gate a merge instead of documenting a mistake.\n *\n * Deliberately dumb about readiness: rather than parsing a framework's stdout\n * for \"ready in 1.2s\" (every framework words it differently, and the wording\n * changes between minor versions), it polls the URL goflag is about to audit.\n * If that URL answers, the audit can proceed — which is the only definition of\n * \"ready\" that actually matters here.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\n\nexport interface StartedServer {\n /** Terminate the process group and resolve once it is gone. */\n stop: () => Promise<void>;\n}\n\nexport interface StartServerOptions {\n /** Shell command to run, e.g. `pnpm start`. */\n command: string;\n /**\n * Directory to run it in. Defaults to the process's own, which is right for\n * the usual `cd my-site && npx goflag …` — but not for a monorepo audited\n * from its root, where the app lives a few directories down.\n */\n cwd?: string;\n /** URL polled until it answers; normally the audit entry point. */\n url: string;\n /** Give up after this long. Defaults to 60s. */\n timeoutMs?: number;\n /** Poll interval. Defaults to 250ms. */\n intervalMs?: number;\n /** Called with each stdout/stderr chunk, for `--verbose`. */\n onOutput?: (chunk: string) => void;\n /** Accept self-signed TLS while polling (a local https dev server). */\n allowInsecureTls?: boolean;\n}\n\n/** True once the URL returns any HTTP response at all. */\nasync function answers(url: string, allowInsecureTls: boolean): Promise<boolean> {\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n if (allowInsecureTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n try {\n // Any status counts: a 404 on the entry path still proves the server is\n // up, and the audit itself is what judges the response.\n await fetch(url, { redirect: \"manual\" });\n return true;\n } catch {\n return false;\n } finally {\n if (allowInsecureTls) {\n if (previous === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n else process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n }\n }\n}\n\nfunction terminate(child: ChildProcess): Promise<void> {\n return new Promise((resolve) => {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolve();\n return;\n }\n\n // Kill the whole group: `pnpm start` spawns the real server as a child,\n // and signalling only the wrapper orphans a process holding the port.\n const killGroup = (signal: NodeJS.Signals) => {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n // Already dead, or the group is gone — nothing left to do.\n }\n };\n\n const force = setTimeout(() => killGroup(\"SIGKILL\"), 5_000);\n child.once(\"exit\", () => {\n clearTimeout(force);\n resolve();\n });\n killGroup(\"SIGTERM\");\n });\n}\n\n/**\n * Spawn `command` and resolve once `url` answers.\n *\n * Rejects — after stopping the process — when the command exits early or the\n * timeout expires. Both are hard failures: auditing a server that never came\n * up would report a site-wide outage as a hundred SEO findings.\n */\nexport async function startServer(options: StartServerOptions): Promise<StartedServer> {\n const timeoutMs = options.timeoutMs ?? 60_000;\n const intervalMs = options.intervalMs ?? 250;\n const allowInsecureTls = options.allowInsecureTls === true;\n\n const cwd = options.cwd ?? process.cwd();\n const child = spawn(options.command, {\n cwd,\n shell: true,\n // Own process group, so `terminate` can take the whole tree down.\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let exited: { code: number | null; signal: NodeJS.Signals | null } | undefined;\n child.once(\"exit\", (code, signal) => {\n exited = { code, signal };\n });\n\n const tail: string[] = [];\n const record = (chunk: Buffer) => {\n const text = chunk.toString();\n options.onOutput?.(text);\n // Keep a short tail so a boot failure can be reported with its own words\n // instead of a bare \"command exited\".\n tail.push(text);\n if (tail.length > 40) tail.shift();\n };\n child.stdout?.on(\"data\", record);\n child.stderr?.on(\"data\", record);\n\n const server: StartedServer = { stop: () => terminate(child) };\n const deadline = Date.now() + timeoutMs;\n\n for (;;) {\n if (exited) {\n await server.stop();\n const why = exited.signal ? `signal ${exited.signal}` : `exit code ${exited.code}`;\n // Naming the directory is most of the diagnosis: a command that runs\n // fine by hand and dies here has almost always been started somewhere\n // other than where its package.json lives.\n throw new Error(\n `--start command exited before ${options.url} answered (${why}).\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `${tail.join(\"\").trimEnd()}`,\n );\n }\n if (await answers(options.url, allowInsecureTls)) return server;\n if (Date.now() >= deadline) {\n await server.stop();\n throw new Error(\n `--start command did not serve ${options.url} within ${timeoutMs}ms.\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `Raise --start-timeout, check the command listens on that URL, or point ` +\n `--start-cwd at the directory it should run in.`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n}\n","/**\n * CLI argument parsing — split out from `cli.ts` so it can be unit-tested\n * without importing the module's top-level `main()` side effect.\n *\n * `parseArgs` is a pure function of `argv` (plus the ambient TTY / NO_COLOR\n * signals used only for the default color choice). It throws a plain `Error`\n * with a user-facing message on malformed input; the CLI turns that into an\n * exit-code-2 with the help text.\n */\n\nimport { PROFILE_NAMES, PROFILES } from \"./lib/rules/profiles\";\nimport type { AuditOptions, FailOn } from \"./report/build\";\nimport type { LogMode } from \"./report/logger\";\n\n/** `--profile` names and what each one means, indented for the help block. */\nconst PROFILE_HELP = PROFILE_NAMES.map(\n (name) => ` ${name} — ${PROFILES[name]!.description}`,\n).join(\"\\n\");\n\nexport const HELP = `goflag — audit a site for broken links, missing translations, and SEO metadata\n\nUsage:\n goflag <url> [options]\n\nOptions:\n --json Print the JSON report to stdout (nothing else).\n --summary, -s Roll findings up (dedup by link/rule/code). Pairs\n with --json for a compact, agent-friendly payload.\n --report <file> Write the (full) JSON report to <file>.\n --depth <n> Crawl depth (0 = entry page only). Default: 2.\n --max-pages <n> Hard cap on pages crawled. Default: 200.\n --include <glob> Only crawl paths matching <glob> (repeatable).\n --exclude <glob> Skip paths matching <glob> (repeatable).\n --locales <list> Comma-separated locales the site serves, e.g.\n \"fr,en,pt-br\". Authoritative: overrides what the\n sitemap and crawl suggest, and makes a locale the\n site does not serve yet show up as missing.\n --ignore-holes <glob> Route (locale-free) that is deliberately not\n translated everywhere, so its gaps are not reported\n as missing translations (repeatable).\n --no-sitemap Do not discover the sitemap; crawl from <url> only.\n Discovery is on by default because link-only crawling\n cannot find locales a site never links to.\n --profile <name> Policy overlay on the rule set — it changes how\n loudly a rule fires, never what it observes:\n${PROFILE_HELP}\n --conformance Report EVERY rule's status on every page (pass /\n fail / warn / na), not just the violations. Answers\n \"where do we stand against the catalog?\"; pairs with\n --json.\n --advisories Attach the prose rules — the judgment calls goflag\n refuses to fake — each with the observed facts an\n agent needs to judge it. Never affects the gate.\n --fail-on <level> Exit 1 at or above this severity: warning (default),\n error, or never.\n --regressions-only Weaken the gate: fail only on findings that are NEW\n relative to --baseline. Known findings stop blocking\n the build — a green run no longer means a clean site.\n Requires --baseline.\n --baseline <file> Stored report to compare against. Use with\n --regressions-only.\n --update-baseline Write this run to --baseline and exit 0, instead of\n judging against it. Use it to capture a baseline, or\n to accept findings you have decided to live with —\n it says what it accepted.\n --max-debt <n> Fail when the site carries more than <n> findings in\n total, whether new or known. Lower it as you fix, to\n stop a baseline from fossilising.\n --start <cmd> Boot <cmd>, wait for <url> to answer, audit, then\n stop it. Use to gate a merge on the built app before\n it ships.\n --start-cwd <dir> Directory to run --start in. Defaults to the current\n one; set it when auditing a monorepo package from the\n repository root.\n --start-timeout <ms> How long to wait for --start to answer. Default: 60000.\n --no-external Do not probe off-origin (external) links.\n --static Static HTML only; never launch headless Chromium.\n --allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).\n --timeout <ms> Per-request timeout in ms. Default: 8000.\n --verbose, -V Log every page as it is analyzed.\n --quiet, -q Suppress the live progress output.\n --no-color Disable coloured output.\n -h, --help Show this help.\n -v, --version Show the version.\n\nExit codes: 0 clean, 1 findings found, 2 fatal error.`;\n\nexport interface ParsedArgs {\n url?: string;\n json: boolean;\n summary: boolean;\n report?: string;\n color: boolean;\n help: boolean;\n version: boolean;\n /** Live-progress verbosity for the logger. */\n logMode: LogMode;\n /** Severity at or above which the process exits 1. */\n failOn: FailOn;\n /** Path to a stored report to compare against (`--baseline`). */\n baseline?: string;\n /** Gate on new findings only, letting known ones through. */\n regressionsOnly: boolean;\n /** Write the run to `--baseline` rather than judging against it. */\n updateBaseline: boolean;\n /** Hard ceiling on total findings, new or known. */\n maxDebt?: number;\n /** Command to boot before auditing, and stop afterwards (`--start`). */\n start?: string;\n /** How long to wait for `--start` to answer, in ms. */\n startTimeoutMs: number;\n /** Directory to run the `--start` command in. */\n startCwd?: string;\n options: AuditOptions;\n}\n\nconst FAIL_ON_LEVELS: readonly FailOn[] = [\"warning\", \"error\", \"never\"];\n\nexport function parseArgs(argv: string[]): ParsedArgs {\n const parsed: ParsedArgs = {\n json: false,\n summary: false,\n color: process.stdout.isTTY === true && !process.env.NO_COLOR,\n help: false,\n version: false,\n logMode: \"compact\",\n failOn: \"warning\",\n regressionsOnly: false,\n updateBaseline: false,\n startTimeoutMs: 60_000,\n options: { include: [], exclude: [] },\n };\n\n const next = (i: number, flag: string): string => {\n const value = argv[i + 1];\n if (value === undefined) throw new Error(`${flag} requires a value`);\n return value;\n };\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case \"-h\":\n case \"--help\":\n parsed.help = true;\n break;\n case \"-v\":\n case \"--version\":\n parsed.version = true;\n break;\n case \"--json\":\n parsed.json = true;\n break;\n case \"-s\":\n case \"--summary\":\n parsed.summary = true;\n break;\n case \"--no-color\":\n parsed.color = false;\n break;\n case \"-V\":\n case \"--verbose\":\n parsed.logMode = \"verbose\";\n break;\n case \"-q\":\n case \"--quiet\":\n parsed.logMode = \"quiet\";\n break;\n case \"--static\":\n parsed.options.static = true;\n break;\n case \"--allow-insecure-tls\":\n parsed.options.allowInsecureTls = true;\n break;\n case \"--no-external\":\n parsed.options.checkExternal = false;\n break;\n case \"--no-sitemap\":\n parsed.options.noSitemap = true;\n break;\n case \"--conformance\":\n parsed.options.conformance = true;\n break;\n case \"--advisories\":\n parsed.options.advisories = true;\n break;\n case \"--ignore-holes\":\n parsed.options.ignoreHoles = [...(parsed.options.ignoreHoles ?? []), next(i, arg)];\n i++;\n break;\n case \"--locales\": {\n // Split on commas so `--locales fr,en` and repeated flags both work.\n const value = next(i, arg);\n const tags = value\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n if (tags.length === 0) throw new Error(`${arg} expects at least one locale`);\n parsed.options.locales = [...(parsed.options.locales ?? []), ...tags];\n i++;\n break;\n }\n case \"--profile\": {\n const value = next(i, arg);\n // Validated here, not in `runAudit`: a mistyped policy should cost\n // an argument-parse error, not a crawl.\n if (!PROFILE_NAMES.includes(value)) {\n throw new Error(`${arg} expects one of: ${PROFILE_NAMES.join(\", \")}`);\n }\n parsed.options.profile = value;\n i++;\n break;\n }\n case \"--fail-on\": {\n const value = next(i, arg) as FailOn;\n if (!FAIL_ON_LEVELS.includes(value)) {\n throw new Error(`${arg} expects one of: ${FAIL_ON_LEVELS.join(\", \")}`);\n }\n parsed.failOn = value;\n i++;\n break;\n }\n case \"--start\":\n parsed.start = next(i, arg);\n i++;\n break;\n case \"--start-timeout\":\n parsed.startTimeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--start-cwd\":\n parsed.startCwd = next(i, arg);\n i++;\n break;\n case \"--report\":\n parsed.report = next(i, arg);\n i++;\n break;\n case \"--baseline\":\n parsed.baseline = next(i, arg);\n i++;\n break;\n case \"--update-baseline\":\n parsed.updateBaseline = true;\n break;\n case \"--regressions-only\":\n parsed.regressionsOnly = true;\n break;\n case \"--max-debt\":\n parsed.maxDebt = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--depth\":\n parsed.options.depth = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--max-pages\":\n parsed.options.maxPages = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--timeout\":\n parsed.options.timeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--include\":\n parsed.options.include!.push(next(i, arg));\n i++;\n break;\n case \"--exclude\":\n parsed.options.exclude!.push(next(i, arg));\n i++;\n break;\n default:\n if (arg && arg.startsWith(\"-\")) throw new Error(`unknown option: ${arg}`);\n if (arg && !parsed.url) parsed.url = arg;\n else if (arg) throw new Error(`unexpected argument: ${arg}`);\n }\n }\n\n // Each flag is meaningless without the other, and guessing which one the\n // caller meant would silently change how strict the build is.\n if (parsed.updateBaseline && !parsed.baseline) {\n throw new Error(\"--update-baseline needs a --baseline <file> to write to\");\n }\n // Capturing a baseline is not gating against one, so the explicit opt-in that\n // --regressions-only exists to force does not apply.\n if (parsed.baseline && !parsed.regressionsOnly && !parsed.updateBaseline) {\n throw new Error(\n \"--baseline weakens the gate, so it must be requested explicitly: add --regressions-only\",\n );\n }\n if (parsed.regressionsOnly && !parsed.baseline) {\n throw new Error(\"--regressions-only needs a --baseline <file> to compare against\");\n }\n\n return parsed;\n}\n\nfunction toInt(value: string, flag: string): number {\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} expects a non-negative integer`);\n return n;\n}\n","/**\n * goflag CLI.\n *\n * goflag <url> [options]\n *\n * Crawls a site and reports broken links, missing translation pages, and\n * missing/misconfigured SEO metadata. The JSON report is the source of\n * truth; the terminal output is rendered from it.\n *\n * Exit codes:\n * 0 clean (green flag)\n * 1 findings present (yellow/red flag) — useful as a CI gate\n * 2 fatal (bad URL, unexpected error)\n */\n\nimport { runAudit, exitCode } from \"./report/build\";\nimport { diffReports, diffExitCode, totalFindings } from \"./report/diff\";\nimport { renderDiffTerminal } from \"./report/render-diff\";\nimport { startServer, type StartedServer } from \"./lib/runner/dev-server\";\nimport { renderTerminal } from \"./report/render-terminal\";\nimport { renderSummaryTerminal } from \"./report/render-summary\";\nimport { summarize } from \"./report/summarize\";\nimport { Logger } from \"./report/logger\";\nimport { HELP, parseArgs, type ParsedArgs } from \"./cli-args\";\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const { fileURLToPath } = await import(\"node:url\");\n const { dirname, join } = await import(\"node:path\");\n const here = dirname(fileURLToPath(import.meta.url));\n const pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nasync function main(): Promise<number> {\n let args: ParsedArgs;\n try {\n args = parseArgs(process.argv.slice(2));\n } catch (err) {\n process.stderr.write(`goflag: ${(err as Error).message}\\n\\n${HELP}\\n`);\n return 2;\n }\n\n if (args.help) {\n process.stdout.write(`${HELP}\\n`);\n return 0;\n }\n if (args.version) {\n process.stdout.write(`${await readVersion()}\\n`);\n return 0;\n }\n if (!args.url) {\n process.stderr.write(`goflag: missing <url>\\n\\n${HELP}\\n`);\n return 2;\n }\n\n // Progress always goes to stderr, so stdout (JSON / rendered report)\n // stays clean and pipeable. Colour follows the stderr TTY.\n const logger = new Logger({\n stream: process.stderr,\n mode: args.logMode,\n color: process.stderr.isTTY === true && !process.env.NO_COLOR,\n });\n\n // `--start` owns a child process for the whole audit; it must be torn down\n // on every exit path, including a failed audit, or CI leaks a held port.\n let server: StartedServer | undefined;\n let report;\n try {\n if (args.start) {\n logger.note(`goflag: starting \\`${args.start}\\` …`);\n server = await startServer({\n command: args.start,\n cwd: args.startCwd,\n url: args.url,\n timeoutMs: args.startTimeoutMs,\n allowInsecureTls: args.options.allowInsecureTls,\n onOutput: args.logMode === \"verbose\" ? (chunk) => process.stderr.write(chunk) : undefined,\n });\n }\n logger.note(`goflag: auditing ${args.url} …`);\n report = await runAudit(args.url, { ...args.options, onProgress: logger.onProgress });\n logger.stop();\n } catch (err) {\n logger.stop();\n process.stderr.write(`goflag: ${(err as Error).message}\\n`);\n return 2;\n } finally {\n await server?.stop();\n }\n\n // A baseline changes the question from \"is this site clean?\" to \"did this\n // change make it worse?\" — the only one a site with a backlog can answer.\n if (args.baseline) {\n const { existsSync, readFileSync } = await import(\"node:fs\");\n // Capturing a baseline for the first time is the one case where the file\n // is legitimately absent. Everywhere else a missing baseline is a typo,\n // and continuing would turn it into a green build.\n if (existsSync(args.baseline) || !args.updateBaseline) {\n try {\n const baseline = JSON.parse(readFileSync(args.baseline, \"utf8\"));\n report.diff = diffReports(baseline, report);\n } catch (err) {\n process.stderr.write(\n `goflag: could not read baseline ${args.baseline}: ${(err as Error).message}\\n`,\n );\n return 2;\n }\n }\n }\n\n // Writing the baseline is accepting everything in it. The one thing this\n // must not do is accept it quietly: a counter that drops without explanation\n // reads as \"the problem went away\".\n if (args.updateBaseline && args.baseline) {\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.baseline, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n const total = totalFindings(report);\n if (report.diff) {\n const { added, resolved } = report.diff;\n process.stdout.write(\n `goflag: baseline updated — ${added.length} newly accepted, ${resolved.length} resolved, ` +\n `${total} findings now grandfathered in ${args.baseline}\\n`,\n );\n if (added.length > 0) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n }\n } else {\n process.stdout.write(\n `goflag: baseline captured — ${total} findings grandfathered in ${args.baseline}\\n`,\n );\n }\n process.stdout.write(\n `goflag: set --max-debt ${total} to stop that number growing, and lower it as you fix.\\n`,\n );\n return 0;\n }\n\n if (args.report) {\n // The report file is always the full report — the source of truth a\n // baseline/diff can rely on, regardless of the --summary view choice.\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.report, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n process.stderr.write(`goflag: report written to ${args.report}\\n`);\n }\n\n // In baseline mode the diff *is* the answer. Printing the full report first\n // buries \"nothing changed\" under a hundred known findings — the reader stops\n // looking, which is precisely the failure the baseline exists to prevent.\n // The complete report stays available via --json and --report.\n if (report.diff && !args.json) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n }\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n if (args.summary) {\n const summary = summarize(report);\n process.stdout.write(\n args.json\n ? `${JSON.stringify(summary, null, 2)}\\n`\n : `${renderSummaryTerminal(summary, { color: args.color })}\\n`,\n );\n } else {\n process.stdout.write(\n args.json\n ? `${JSON.stringify(report, null, 2)}\\n`\n : `${renderTerminal(report, { color: args.color })}\\n`,\n );\n }\n\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n return 1;\n }\n\n if (report.diff) {\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n return exitCode(report, args.failOn);\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(`goflag: unexpected error: ${(err as Error).stack ?? err}\\n`);\n process.exit(2);\n });\n"],"mappings":";;;;;;;;;;;;;;AA+DO,SAAS,gBAAgB,QAAmC;AACjE,QAAM,MAAmB,CAAC;AAE1B,aAAW,QAAQ,OAAO,aAAa;AACrC,QAAI,KAAK;AAAA,MACP,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS,GAAG,KAAK,OAAO,gBAAW,KAAK,IAAI;AAAA,MAC5C,SAAS,KAAK;AAAA,IAChB,CAAC;AAAA,EACH;AAEA,aAAW,QAAQ,OAAO,kBAAkB;AAC1C,QAAI,KAAK;AAAA,MACP,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS,GAAG,KAAK,UAAU,eAAe,OAAO,KAAK,GAAG;AAAA,MACzD,SAAS,KAAK;AAAA,IAChB,CAAC;AAAA,EACH;AAEA,aAAW,QAAQ,OAAO,oBAAoB,OAAO;AACnD,QAAI,KAAK;AAAA,MACP,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS,GAAG,KAAK,KAAK,YAAY,KAAK,eAAe,KAAK,IAAI,CAAC;AAAA,IAClE,CAAC;AAAA,EACH;AAEA,aAAW,SAAS,OAAO,oBAAoB,aAAa;AAC1D,QAAI,KAAK;AAAA,MACP,IAAI,MAAM;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS,GAAG,MAAM,IAAI,OAAO,MAAM,GAAG;AAAA,MACtC,SAAS,MAAM;AAAA,IACjB,CAAC;AAAA,EACH;AAEA,aAAW,SAAS,OAAO,WAAW;AACpC,QAAI,KAAK;AAAA,MACP,IAAI,MAAM;AAAA,MACV,MAAM;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,SAAS,GAAG,MAAM,MAAM,OAAO,MAAM,OAAO;AAAA,MAC5C,SAAS,MAAM;AAAA,IACjB,CAAC;AAAA,EACH;AAEA,aAAW,SAAS,OAAO,YAAY;AACrC,QAAI,KAAK;AAAA,MACP,IAAI,MAAM;AAAA,MACV,MAAM;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,SAAS,GAAG,MAAM,MAAM,OAAO,MAAM,OAAO;AAAA,MAC5C,SAAS,MAAM;AAAA,IACjB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAGA,IAAM,gBAA0C,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE;AAEhF,SAAS,YAAY,SAAmC;AACtD,SAAO,CAAC,GAAG,OAAO,EAAE;AAAA,IAClB,CAAC,GAAG,MACF,cAAc,EAAE,QAAQ,IAAI,cAAc,EAAE,QAAQ,KACpD,EAAE,KAAK,cAAc,EAAE,IAAI,KAC3B,EAAE,QAAQ,cAAc,EAAE,OAAO;AAAA,EACrC;AACF;AAGO,SAAS,cAAc,QAA8B;AAC1D,SAAO,gBAAgB,MAAM,EAAE;AACjC;AAGO,SAAS,YAAY,UAAwB,SAAmC;AACrF,QAAM,SAAS,IAAI,IAAI,gBAAgB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AACtE,QAAM,MAAM,IAAI,IAAI,gBAAgB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAElE,QAAM,QAAqB,CAAC;AAC5B,MAAI,YAAY;AAChB,aAAW,CAAC,IAAI,KAAK,KAAK,KAAK;AAC7B,QAAI,OAAO,IAAI,EAAE,EAAG,cAAa;AAAA,QAC5B,OAAM,KAAK,KAAK;AAAA,EACvB;AAEA,QAAM,WAAwB,CAAC;AAC/B,aAAW,CAAC,IAAI,KAAK,KAAK,QAAQ;AAChC,QAAI,CAAC,IAAI,IAAI,EAAE,EAAG,UAAS,KAAK,KAAK;AAAA,EACvC;AAIA,QAAM,kBAAkB,SAAS,WAAW;AAC5C,QAAM,iBAAiB,QAAQ,WAAW;AAE1C,SAAO;AAAA,IACL,UAAU,EAAE,KAAK,SAAS,KAAK,YAAY,SAAS,YAAY,SAAS,gBAAgB;AAAA,IACzF,OAAO,YAAY,KAAK;AAAA,IACxB,UAAU,YAAY,QAAQ;AAAA,IAC9B;AAAA,IACA,GAAI,oBAAoB,iBACpB,CAAC,IACD,EAAE,iBAAiB,EAAE,UAAU,iBAAiB,SAAS,eAAe,EAAE;AAAA,EAChF;AACF;AAcO,SAAS,aACd,MACA,QACA,MACQ;AACR,MAAI,MAAM,QAAQ,UAAa,KAAK,QAAQ,KAAK,IAAK,QAAO;AAC7D,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,YAAY,WAAW,UAAU,IAAI;AAC3C,QAAM,WAAW,KAAK,MAAM,OAAO,CAAC,MAAM,cAAc,EAAE,QAAQ,KAAK,SAAS;AAChF,SAAO,SAAS,SAAS,IAAI,IAAI;AACnC;;;ACpLA,IAAM,OAAO;AAAA,EACX,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAEA,SAAS,MAAM,SAAkB;AAC/B,SAAO,CAAC,MAAc,SAAkB,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,KAAK,KAAK;AACpF;AAEA,SAAS,YAAY,GAAwC,UAA4B;AACvF,MAAI,aAAa,QAAS,QAAO,EAAE,KAAK,KAAK,OAAO;AACpD,MAAI,aAAa,UAAW,QAAO,EAAE,KAAK,QAAQ,OAAO;AACzD,SAAO,EAAE,KAAK,KAAK,OAAO;AAC5B;AAGA,IAAM,aAAa;AAEnB,SAAS,YACP,SACA,QACA,aACA,GACU;AACV,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,QAAQ,MAAM,GAAG,UAAU,GAAG;AAIhD,UAAM,QACJ,MAAM,WAAW,CAAC,MAAM,QAAQ,SAAS,MAAM,OAAO,IAClD,EAAE,KAAK,KAAK,QAAQ,MAAM,OAAO,EAAE,IACnC;AACN,UAAM;AAAA,MACJ,KAAK,EAAE,aAAa,MAAM,CAAC,IAAI,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,IAAI,CAAC,KAAK,MAAM,OAAO,GAAG,KAAK;AAAA,IACpH;AAAA,EACF;AACA,MAAI,QAAQ,SAAS,YAAY;AAC/B,UAAM,KAAK,KAAK,EAAE,KAAK,KAAK,WAAM,QAAQ,SAAS,UAAU,OAAO,CAAC,EAAE;AAAA,EACzE;AACA,SAAO;AACT;AAGA,SAAS,gBAAgB,YAAoB,KAA4B;AACvE,QAAM,QAAQ,KAAK,MAAM,UAAU;AACnC,MAAI,OAAO,MAAM,KAAK,EAAG,QAAO;AAChC,SAAO,KAAK,IAAI,GAAG,KAAK,OAAO,MAAM,SAAS,KAAU,CAAC;AAC3D;AAEO,SAAS,mBAAmB,MAAkB,UAAyB,CAAC,GAAW;AACxF,QAAM,IAAI,MAAM,QAAQ,SAAS,KAAK;AACtC,QAAM,QAAkB,CAAC;AACzB,QAAM,OAAO,KAAK;AAMlB,QAAM,UACJ,KAAK,MAAM,SAAS,IAChB,EAAE,KAAK,KAAK,YAAY,IACxB,OAAO,IACL,EAAE,KAAK,QAAQ,iBAAiB,IAChC,EAAE,KAAK,OAAO,OAAO;AAE7B,QAAM,MAAM,gBAAgB,KAAK,SAAS,YAAY,QAAQ,OAAO,KAAK,IAAI,CAAC;AAE/E,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,GAAG,EAAE,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,KAAK,oBAAoB,CAAC,EAAE;AAC3E,QAAM;AAAA,IACJ,GAAG,OAAO,KAAK,KAAK,MAAM,MAAM,gBAC7B,OAAO,IACJ,EAAE,KAAK,MAAM,GAAG,IAAI,uCAAuC,IAC3D,EAAE,KAAK,KAAK,mBAAmB,MAClC,KAAK,SAAS,SAAS,IAAI,EAAE,KAAK,KAAK,SAAM,KAAK,SAAS,MAAM,WAAW,IAAI;AAAA,EACrF;AACA,QAAM;AAAA,IACJ;AAAA,MACE,KAAK;AAAA,MACL,YAAY,KAAK,SAAS,GAAG,iBAAY,KAAK,SAAS,UAAU,MAC9D,QAAQ,OAAO,KAAK,QAAQ,IAAI,aAAa,KAAK,GAAG,OAAO,QAAQ,IAAI,KAAK,GAAG;AAAA,IACrF;AAAA,EACF;AAGA,MAAI,KAAK,iBAAiB;AACxB,UAAM;AAAA,MACJ;AAAA,QACE,KAAK;AAAA,QACL,+CAA+C,KAAK,gBAAgB,QAAQ,uBACvD,KAAK,gBAAgB,OAAO;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,EAAE;AAEb,MAAI,KAAK,MAAM,SAAS,GAAG;AACzB,UAAM,KAAK,EAAE,KAAK,MAAM,cAAc,CAAC;AACvC,UAAM,KAAK,GAAG,YAAY,KAAK,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC;AACvD,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,UAAM,KAAK,EAAE,KAAK,MAAM,UAAU,CAAC;AACnC,UAAM,KAAK,GAAG,YAAY,KAAK,UAAU,KAAK,KAAK,OAAO,CAAC,CAAC;AAC5D,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,KAAK,MAAM,WAAW,KAAK,KAAK,SAAS,WAAW,GAAG;AACzD,UAAM;AAAA,MACJ;AAAA,QACE,KAAK;AAAA,QACL,OAAO,IACH,2CAAsC,IAAI,sEAC1C;AAAA,MACN;AAAA,IACF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;ACnIA,SAAS,aAAgC;AA6BzC,eAAe,QAAQ,KAAa,kBAA6C;AAC/E,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,iBAAkB,SAAQ,IAAI,+BAA+B;AACjE,MAAI;AAGF,UAAM,MAAM,KAAK,EAAE,UAAU,SAAS,CAAC;AACvC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,QAAI,kBAAkB;AACpB,UAAI,aAAa,OAAW,QAAO,QAAQ,IAAI;AAAA,UAC1C,SAAQ,IAAI,+BAA+B;AAAA,IAClD;AAAA,EACF;AACF;AAEA,SAAS,UAAU,OAAoC;AACrD,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,MAAM,aAAa,QAAQ,MAAM,eAAe,MAAM;AACxD,cAAQ;AACR;AAAA,IACF;AAIA,UAAM,YAAY,CAAC,WAA2B;AAC5C,UAAI;AACF,YAAI,MAAM,IAAK,SAAQ,KAAK,CAAC,MAAM,KAAK,MAAM;AAAA,YACzC,OAAM,KAAK,MAAM;AAAA,MACxB,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,UAAM,QAAQ,WAAW,MAAM,UAAU,SAAS,GAAG,GAAK;AAC1D,UAAM,KAAK,QAAQ,MAAM;AACvB,mBAAa,KAAK;AAClB,cAAQ;AAAA,IACV,CAAC;AACD,cAAU,SAAS;AAAA,EACrB,CAAC;AACH;AASA,eAAsB,YAAY,SAAqD;AACrF,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,mBAAmB,QAAQ,qBAAqB;AAEtD,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,QAAQ,MAAM,QAAQ,SAAS;AAAA,IACnC;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,UAAU;AAAA,IACV,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,EAClC,CAAC;AAED,MAAI;AACJ,QAAM,KAAK,QAAQ,CAAC,MAAM,WAAW;AACnC,aAAS,EAAE,MAAM,OAAO;AAAA,EAC1B,CAAC;AAED,QAAM,OAAiB,CAAC;AACxB,QAAM,SAAS,CAAC,UAAkB;AAChC,UAAM,OAAO,MAAM,SAAS;AAC5B,YAAQ,WAAW,IAAI;AAGvB,SAAK,KAAK,IAAI;AACd,QAAI,KAAK,SAAS,GAAI,MAAK,MAAM;AAAA,EACnC;AACA,QAAM,QAAQ,GAAG,QAAQ,MAAM;AAC/B,QAAM,QAAQ,GAAG,QAAQ,MAAM;AAE/B,QAAM,SAAwB,EAAE,MAAM,MAAM,UAAU,KAAK,EAAE;AAC7D,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,aAAS;AACP,QAAI,QAAQ;AACV,YAAM,OAAO,KAAK;AAClB,YAAM,MAAM,OAAO,SAAS,UAAU,OAAO,MAAM,KAAK,aAAa,OAAO,IAAI;AAIhF,YAAM,IAAI;AAAA,QACR,iCAAiC,QAAQ,GAAG,cAAc,GAAG;AAAA,aAC7C,QAAQ,OAAO;AAAA,aACf,GAAG;AAAA,EACd,KAAK,KAAK,EAAE,EAAE,QAAQ,CAAC;AAAA,MAC9B;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,QAAQ,KAAK,gBAAgB,EAAG,QAAO;AACzD,QAAI,KAAK,IAAI,KAAK,UAAU;AAC1B,YAAM,OAAO,KAAK;AAClB,YAAM,IAAI;AAAA,QACR,iCAAiC,QAAQ,GAAG,WAAW,SAAS;AAAA,aAChD,QAAQ,OAAO;AAAA,aACf,GAAG;AAAA;AAAA,MAGrB;AAAA,IACF;AACA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,UAAU,CAAC;AAAA,EAChE;AACF;;;AC/IA,IAAM,eAAe,cAAc;AAAA,EACjC,CAAC,SAAS,4BAA4B,IAAI,WAAM,SAAS,IAAI,EAAG,WAAW;AAC7E,EAAE,KAAK,IAAI;AAEJ,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BlB,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuEd,IAAM,iBAAoC,CAAC,WAAW,SAAS,OAAO;AAE/D,SAAS,UAAU,MAA4B;AACpD,QAAM,SAAqB;AAAA,IACzB,MAAM;AAAA,IACN,SAAS;AAAA,IACT,OAAO,QAAQ,OAAO,UAAU,QAAQ,CAAC,QAAQ,IAAI;AAAA,IACrD,MAAM;AAAA,IACN,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,SAAS,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC,EAAE;AAAA,EACtC;AAEA,QAAM,OAAO,CAAC,GAAW,SAAyB;AAChD,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,UAAU,OAAW,OAAM,IAAI,MAAM,GAAG,IAAI,mBAAmB;AACnE,WAAO;AAAA,EACT;AAEA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAClB,YAAQ,KAAK;AAAA,MACX,KAAK;AAAA,MACL,KAAK;AACH,eAAO,OAAO;AACd;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,eAAO,UAAU;AACjB;AAAA,MACF,KAAK;AACH,eAAO,OAAO;AACd;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,eAAO,UAAU;AACjB;AAAA,MACF,KAAK;AACH,eAAO,QAAQ;AACf;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,eAAO,UAAU;AACjB;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,eAAO,UAAU;AACjB;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,SAAS;AACxB;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,mBAAmB;AAClC;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,gBAAgB;AAC/B;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,YAAY;AAC3B;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,cAAc;AAC7B;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,aAAa;AAC5B;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,cAAc,CAAC,GAAI,OAAO,QAAQ,eAAe,CAAC,GAAI,KAAK,GAAG,GAAG,CAAC;AACjF;AACA;AAAA,MACF,KAAK,aAAa;AAEhB,cAAM,QAAQ,KAAK,GAAG,GAAG;AACzB,cAAM,OAAO,MACV,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACjB,YAAI,KAAK,WAAW,EAAG,OAAM,IAAI,MAAM,GAAG,GAAG,8BAA8B;AAC3E,eAAO,QAAQ,UAAU,CAAC,GAAI,OAAO,QAAQ,WAAW,CAAC,GAAI,GAAG,IAAI;AACpE;AACA;AAAA,MACF;AAAA,MACA,KAAK,aAAa;AAChB,cAAM,QAAQ,KAAK,GAAG,GAAG;AAGzB,YAAI,CAAC,cAAc,SAAS,KAAK,GAAG;AAClC,gBAAM,IAAI,MAAM,GAAG,GAAG,oBAAoB,cAAc,KAAK,IAAI,CAAC,EAAE;AAAA,QACtE;AACA,eAAO,QAAQ,UAAU;AACzB;AACA;AAAA,MACF;AAAA,MACA,KAAK,aAAa;AAChB,cAAM,QAAQ,KAAK,GAAG,GAAG;AACzB,YAAI,CAAC,eAAe,SAAS,KAAK,GAAG;AACnC,gBAAM,IAAI,MAAM,GAAG,GAAG,oBAAoB,eAAe,KAAK,IAAI,CAAC,EAAE;AAAA,QACvE;AACA,eAAO,SAAS;AAChB;AACA;AAAA,MACF;AAAA,MACA,KAAK;AACH,eAAO,QAAQ,KAAK,GAAG,GAAG;AAC1B;AACA;AAAA,MACF,KAAK;AACH,eAAO,iBAAiB,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG;AAC/C;AACA;AAAA,MACF,KAAK;AACH,eAAO,WAAW,KAAK,GAAG,GAAG;AAC7B;AACA;AAAA,MACF,KAAK;AACH,eAAO,SAAS,KAAK,GAAG,GAAG;AAC3B;AACA;AAAA,MACF,KAAK;AACH,eAAO,WAAW,KAAK,GAAG,GAAG;AAC7B;AACA;AAAA,MACF,KAAK;AACH,eAAO,iBAAiB;AACxB;AAAA,MACF,KAAK;AACH,eAAO,kBAAkB;AACzB;AAAA,MACF,KAAK;AACH,eAAO,UAAU,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG;AACxC;AACA;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,QAAQ,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG;AAC9C;AACA;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,WAAW,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG;AACjD;AACA;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,YAAY,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG;AAClD;AACA;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,QAAS,KAAK,KAAK,GAAG,GAAG,CAAC;AACzC;AACA;AAAA,MACF,KAAK;AACH,eAAO,QAAQ,QAAS,KAAK,KAAK,GAAG,GAAG,CAAC;AACzC;AACA;AAAA,MACF;AACE,YAAI,OAAO,IAAI,WAAW,GAAG,EAAG,OAAM,IAAI,MAAM,mBAAmB,GAAG,EAAE;AACxE,YAAI,OAAO,CAAC,OAAO,IAAK,QAAO,MAAM;AAAA,iBAC5B,IAAK,OAAM,IAAI,MAAM,wBAAwB,GAAG,EAAE;AAAA,IAC/D;AAAA,EACF;AAIA,MAAI,OAAO,kBAAkB,CAAC,OAAO,UAAU;AAC7C,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAGA,MAAI,OAAO,YAAY,CAAC,OAAO,mBAAmB,CAAC,OAAO,gBAAgB;AACxE,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,OAAO,mBAAmB,CAAC,OAAO,UAAU;AAC9C,UAAM,IAAI,MAAM,iEAAiE;AAAA,EACnF;AAEA,SAAO;AACT;AAEA,SAAS,MAAM,OAAe,MAAsB;AAClD,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,GAAG,IAAI,iCAAiC;AAC3F,SAAO;AACT;;;ACrRA,eAAe,cAA+B;AAC5C,MAAI;AACF,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,IAAS;AAC/C,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,KAAU;AACjD,UAAM,EAAE,SAAS,KAAK,IAAI,MAAM,OAAO,MAAW;AAClD,UAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,UAAM,MAAM,KAAK,MAAM,aAAa,KAAK,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAC7E,WAAO,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;AAAA,EACzD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,OAAwB;AACrC,MAAI;AACJ,MAAI;AACF,WAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAAA,EACxC,SAAS,KAAK;AACZ,YAAQ,OAAO,MAAM,WAAY,IAAc,OAAO;AAAA;AAAA,EAAO,IAAI;AAAA,CAAI;AACrE,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,MAAM;AACb,YAAQ,OAAO,MAAM,GAAG,IAAI;AAAA,CAAI;AAChC,WAAO;AAAA,EACT;AACA,MAAI,KAAK,SAAS;AAChB,YAAQ,OAAO,MAAM,GAAG,MAAM,YAAY,CAAC;AAAA,CAAI;AAC/C,WAAO;AAAA,EACT;AACA,MAAI,CAAC,KAAK,KAAK;AACb,YAAQ,OAAO,MAAM;AAAA;AAAA,EAA4B,IAAI;AAAA,CAAI;AACzD,WAAO;AAAA,EACT;AAIA,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,QAAQ,QAAQ;AAAA,IAChB,MAAM,KAAK;AAAA,IACX,OAAO,QAAQ,OAAO,UAAU,QAAQ,CAAC,QAAQ,IAAI;AAAA,EACvD,CAAC;AAID,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,QAAI,KAAK,OAAO;AACd,aAAO,KAAK,sBAAsB,KAAK,KAAK,WAAM;AAClD,eAAS,MAAM,YAAY;AAAA,QACzB,SAAS,KAAK;AAAA,QACd,KAAK,KAAK;AAAA,QACV,KAAK,KAAK;AAAA,QACV,WAAW,KAAK;AAAA,QAChB,kBAAkB,KAAK,QAAQ;AAAA,QAC/B,UAAU,KAAK,YAAY,YAAY,CAAC,UAAU,QAAQ,OAAO,MAAM,KAAK,IAAI;AAAA,MAClF,CAAC;AAAA,IACH;AACA,WAAO,KAAK,oBAAoB,KAAK,GAAG,SAAI;AAC5C,aAAS,MAAM,SAAS,KAAK,KAAK,EAAE,GAAG,KAAK,SAAS,YAAY,OAAO,WAAW,CAAC;AACpF,WAAO,KAAK;AAAA,EACd,SAAS,KAAK;AACZ,WAAO,KAAK;AACZ,YAAQ,OAAO,MAAM,WAAY,IAAc,OAAO;AAAA,CAAI;AAC1D,WAAO;AAAA,EACT,UAAE;AACA,UAAM,QAAQ,KAAK;AAAA,EACrB;AAIA,MAAI,KAAK,UAAU;AACjB,UAAM,EAAE,YAAY,aAAa,IAAI,MAAM,OAAO,IAAS;AAI3D,QAAI,WAAW,KAAK,QAAQ,KAAK,CAAC,KAAK,gBAAgB;AACrD,UAAI;AACF,cAAM,WAAW,KAAK,MAAM,aAAa,KAAK,UAAU,MAAM,CAAC;AAC/D,eAAO,OAAO,YAAY,UAAU,MAAM;AAAA,MAC5C,SAAS,KAAK;AACZ,gBAAQ,OAAO;AAAA,UACb,mCAAmC,KAAK,QAAQ,KAAM,IAAc,OAAO;AAAA;AAAA,QAC7E;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAKA,MAAI,KAAK,kBAAkB,KAAK,UAAU;AACxC,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,IAAS;AAChD,kBAAc,KAAK,UAAU,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AAC3E,UAAMA,SAAQ,cAAc,MAAM;AAClC,QAAI,OAAO,MAAM;AACf,YAAM,EAAE,OAAO,SAAS,IAAI,OAAO;AACnC,cAAQ,OAAO;AAAA,QACb,mCAA8B,MAAM,MAAM,oBAAoB,SAAS,MAAM,cACxEA,MAAK,kCAAkC,KAAK,QAAQ;AAAA;AAAA,MAC3D;AACA,UAAI,MAAM,SAAS,GAAG;AACpB,gBAAQ,OAAO,MAAM,GAAG,mBAAmB,OAAO,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,MACpF;AAAA,IACF,OAAO;AACL,cAAQ,OAAO;AAAA,QACb,oCAA+BA,MAAK,8BAA8B,KAAK,QAAQ;AAAA;AAAA,MACjF;AAAA,IACF;AACA,YAAQ,OAAO;AAAA,MACb,0BAA0BA,MAAK;AAAA;AAAA,IACjC;AACA,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,QAAQ;AAGf,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,IAAS;AAChD,kBAAc,KAAK,QAAQ,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACzE,YAAQ,OAAO,MAAM,6BAA6B,KAAK,MAAM;AAAA,CAAI;AAAA,EACnE;AAMA,MAAI,OAAO,QAAQ,CAAC,KAAK,MAAM;AAC7B,YAAQ,OAAO,MAAM,GAAG,mBAAmB,OAAO,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;AAAA,CAAI;AAClF,UAAMA,SAAQ,cAAc,MAAM;AAClC,QAAI,KAAK,YAAY,UAAaA,SAAQ,KAAK,SAAS;AACtD,cAAQ,OAAO;AAAA,QACb,WAAWA,MAAK,8CAA8C,KAAK,OAAO;AAAA;AAAA,MAC5E;AAAA,IACF;AACA,WAAO,aAAa,OAAO,MAAM,KAAK,QAAQ,EAAE,OAAAA,QAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,EAC5E;AAEA,MAAI,KAAK,SAAS;AAChB,UAAM,UAAU,UAAU,MAAM;AAChC,YAAQ,OAAO;AAAA,MACb,KAAK,OACD,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,IACnC,GAAG,sBAAsB,SAAS,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;AAAA;AAAA,IAC9D;AAAA,EACF,OAAO;AACL,YAAQ,OAAO;AAAA,MACb,KAAK,OACD,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,IAClC,GAAG,eAAe,QAAQ,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;AAAA;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,QAAQ,cAAc,MAAM;AAClC,MAAI,KAAK,YAAY,UAAa,QAAQ,KAAK,SAAS;AACtD,YAAQ,OAAO;AAAA,MACb,WAAW,KAAK,8CAA8C,KAAK,OAAO;AAAA;AAAA,IAC5E;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAM;AACf,WAAO,aAAa,OAAO,MAAM,KAAK,QAAQ,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,EAC5E;AAEA,SAAO,SAAS,QAAQ,KAAK,MAAM;AACrC;AAEA,KAAK,EACF,KAAK,CAAC,SAAS,QAAQ,KAAK,IAAI,CAAC,EACjC,MAAM,CAAC,QAAQ;AACd,UAAQ,OAAO,MAAM,6BAA8B,IAAc,SAAS,GAAG;AAAA,CAAI;AACjF,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["total"]}