import { parseJsonOr } from "@intentic/base/json"; import { isRecord } from "@intentic/base/escape"; import type { Advisory, Bundle, DeadCode, Duplication, MutationScore, OutdatedPackage, ProbeFacts, ProbeId, UiScan } from "../schemas/maintenance.js"; import type { IdiomRule } from "./stack.js"; import { BYPASS_PATTERN, COMPONENT_GLOBS, IDIOM_RULES, MARKUP_GLOBS, normalizePath, SCAN_IGNORES, UI_FRAMEWORKS, TAILWIND_PACKAGES, } from "./stack.js"; import { WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG, WORKSPACE_ROOT_RG_EXCLUDE_ARG } from "./workspace-scope.js"; // Probe specs (command + parser) shared by the daemon and the panel so both agree on what each measurement means. // Parsers return `undefined` rather than throw on an unrecognized shape, since a tool's JSON output can drift between // versions; the runner records that as a failed probe, not a clean repo. // Tier sets refresh cadence by cost (tier 1 daily, tier 2 weekly); a non-zero `available` means unmeasured, never a // false-clean result. const HOUR_MS = 3_600_000; const DAY_MS = 24 * HOUR_MS; export interface ProbeSpec { readonly id: ProbeId; readonly title: string; // Shown to the reader in the panel, next to the probe's age. readonly measures: string; readonly tier: 1 | 2; // How long a result stays fresh; the background runner refreshes past it, manual refresh ignores it. readonly ttlMs: number; readonly timeoutMs: number; // Exit 0 means this repo can be measured; runs in the repo's own directory, like `command`. readonly available: string; // What's missing, as a bare clause; must never imply the repo has zero findings (unmeasured is not clean). readonly unavailable: string; // Runs via `sh -c` in the repo dir; non-zero exit isn't failure, the runner judges by whether parse succeeded. readonly command: string; readonly parse: (stdout: string) => ProbeFacts | undefined; } // Parses the first `{...}` found as JSON, or returns undefined; tolerates leading warnings or empty stdout without // throwing. const asObject = (text: string): Record | undefined => { const start = text.indexOf(`{`); if (start === -1) { return undefined; } const parsed = parseJsonOr(text.slice(start), undefined); return isRecord(parsed) ? parsed : undefined; }; const asString = (value: unknown): string | undefined => (typeof value === `string` && value !== `` ? value : undefined); const countOf = (value: unknown): number => (Array.isArray(value) ? value.length : 0); // Leading-integer parts of a version, compared positionally rather than via full semver; prerelease/build metadata // can't change which position differs first. Not the contract's compareVersions: this asks which part an upgrade steps // (major, minor or patch), not which version is newer, and it reads whatever pnpm prints, `1.2.3(peer@1)` included, // where the semver reader would refuse the whole string. const versionParts = (version: string): number[] => version .replace(/^[^\d]*/, ``) .split(`.`) .map((part) => Number.parseInt(part, 10) || 0); const semverKind = (current: string, latest: string): OutdatedPackage["kind"] => { const [currentMajor = 0, currentMinor = 0] = versionParts(current); const [latestMajor = 0, latestMinor = 0] = versionParts(latest); if (latestMajor !== currentMajor) { return `major`; } return latestMinor !== currentMinor ? `minor` : `patch`; }; // ---- packages the workspace leaves out ------------------------------------------------------------------------- // `pnpm outdated -r` and `pnpm audit` read the workspace's own tree, so a package the repo deliberately keeps OUT of // it (the iOS shell, whose dependencies Xcode consumes and which is installed with npm on the Mac that builds it) // was measured by neither, while Dependabot reads every manifest: GitHub reported a critical Capacitor advisory // this panel never saw. Every literal `!dir` under `packages:` in pnpm-workspace.yaml naming a folder with its own // package.json counts. A glob negation (`!**/fixtures/**`) is how fixtures are left out; it never names a folder // that exists, so the package.json test drops it without a rule of its own. const STANDALONE_DIRS = `sed -n '/^packages:/,/^[^[:space:]#-]/p' pnpm-workspace.yaml 2>/dev/null | tr -d "\\"'" | ` + `sed -n -e 's|/\\*\\*[[:space:]]*$||' -e 's|^[[:space:]]*-[[:space:]]*!\\([^[:space:]#]*\\).*$|\\1|p'`; // The line that opens one standalone package's report, as JSON so a guard can still read the stream with `jq -s`. const STANDALONE_LINE = /^\{"standalone":"([^"]*)"\}$/; // Runs `measure` against each standalone package in a scratch copy, never in place: `--lockfile-only` would write // a pnpm lockfile into a folder whose whole point is that pnpm does not manage it. A lockfile the folder already // has is honoured (an npm one through `pnpm import`); without one the tree is what a fresh install there resolves // today, which for a folder that commits none is exactly what its next build installs. A copy that cannot be // resolved prints nothing after its marker, and the parser turns that into a failed probe, never a clean package. const standaloneCommand = (measure: string): string => `${STANDALONE_DIRS} | while IFS= read -r dir; do ` + `[ -f "$dir/package.json" ] || continue; ` + `printf '{"standalone":"%s"}\\n' "$dir"; ` + `scratch=$(mktemp -d) || continue; ` + `for file in package.json pnpm-lock.yaml package-lock.json .npmrc; do [ -f "$dir/$file" ] && cp "$dir/$file" "$scratch/"; done; ` + `( cd "$scratch" && { [ -f pnpm-lock.yaml ] || { [ -f package-lock.json ] && pnpm import >/dev/null 2>&1; } || ` + `pnpm install --lockfile-only --ignore-workspace --ignore-scripts >/dev/null 2>&1; } && ${measure} ); ` + `rm -rf "$scratch"; ` + `done`; // The workspace's own report, then one per standalone package; `true` at the end because pnpm exits non-zero when // it has findings, and the runner judges by whether the output parses. const withStandalone = (measure: string, standaloneMeasure: string): string => `${measure}; ${standaloneCommand(standaloneMeasure)}; true`; interface Section { readonly standalone?: string; readonly text: string; } // Splits a probe's output at each standalone marker; the first section, before any marker, is the workspace's own. const sectionsOf = (stdout: string): Section[] => { const sections: { standalone?: string; lines: string[] }[] = [{ lines: [] }]; for (const line of stdout.split(`\n`)) { const marker = STANDALONE_LINE.exec(line.trim()); if (marker === null) { sections.at(-1)?.lines.push(line); } else { sections.push({ standalone: marker[1] ?? ``, lines: [] }); } } return sections.map(({ standalone, lines }) => ({ ...(standalone === undefined ? {} : { standalone }), text: lines.join(`\n`) })); }; // Every section has to parse: a standalone package that could not be measured is not a clean one, so one unreadable // report fails the whole probe rather than vanishing from it. const parseSections = (stdout: string, parse: (section: Section) => readonly T[] | undefined): T[] | undefined => { const found: T[] = []; for (const section of sectionsOf(stdout)) { const parsed = parse(section); if (parsed === undefined) { return undefined; } found.push(...parsed); } return found; }; const standaloneOf = (section: Section): { standalone?: string } => (section.standalone === undefined ? {} : { standalone: section.standalone }); // pnpm 12 keys a package that appears more than once in the workspace (two versions, or both a dependency and a // devDependency somewhere) as `name@version` and `name@version (dev)`; taken as the name, that listed mermaid twice // under a name no registry knows. const DISAMBIGUATED_KEY = /^(@?[^@\s]+)@\S+(?:\s+\([^)]*\))?$/; const packageNameOf = (key: string): string => DISAMBIGUATED_KEY.exec(key)?.[1] ?? key; // What ships outranks what only builds, so the merged row says `dependencies` if any of its copies does. const SECTION_RANK = [`dependencies`, `optionalDependencies`, `peerDependencies`, `devDependencies`]; const rankOf = (section: string): number => { const rank = SECTION_RANK.indexOf(section); return rank === -1 ? SECTION_RANK.length : rank; }; // `pnpm outdated --json` prints a name→{current,latest,dependencyType} map; the recursive workspace form merges into // the same shape. // Entries missing `current` or `latest` are skipped: pnpm could not resolve them against the registry. const parseOutdatedSection = (section: Section): OutdatedPackage[] | undefined => { const root = asObject(section.text); if (root === undefined) { return undefined; } const packages = new Map(); for (const [key, raw] of Object.entries(root)) { if (typeof raw !== `object` || raw === null) { continue; } const entry = raw as Record; const current = asString(entry[`current`]); const latest = asString(entry[`latest`]); if (current === undefined || latest === undefined || current === latest) { continue; } const name = packageNameOf(key); const row: OutdatedPackage = { name, current, latest, kind: semverKind(current, latest), section: asString(entry[`dependencyType`]) ?? `dependencies`, ...standaloneOf(section), }; // One row per version pair: the same upgrade seen from a dependency and a devDependency is one upgrade. const identity = `${name}@${current}→${latest}`; const seen = packages.get(identity); if (seen === undefined || rankOf(row.section) < rankOf(seen.section)) { packages.set(identity, row); } } return [...packages.values()]; }; const parseOutdated = (stdout: string): ProbeFacts | undefined => { const packages = parseSections(stdout, parseOutdatedSection); return packages === undefined ? undefined : { id: `outdated`, packages }; }; const SEVERITIES = new Set([`critical`, `high`, `moderate`, `low`, `info`]); // `pnpm audit --json` prints {advisories, metadata}; metadata's tally is ignored since the chore needs which package // and whether a patch exists. // `dev` is read from the findings' own flag: a build-time-only advisory is a different risk than one in a running // service. const parseAuditSection = (section: Section): Advisory[] | undefined => { const root = asObject(section.text); if (root === undefined) { return undefined; } const raw = root[`advisories`]; // No `advisories` key is pnpm's clean report (empty list), not an unrecognized shape. if (raw === undefined) { return []; } if (typeof raw !== `object` || raw === null) { return undefined; } const advisories: Advisory[] = []; for (const value of Object.values(raw as Record)) { if (typeof value !== `object` || value === null) { continue; } const entry = value as Record; const name = asString(entry[`module_name`]); const severity = asString(entry[`severity`]); if (name === undefined || severity === undefined || !SEVERITIES.has(severity)) { continue; } const patched = asString(entry[`patched_versions`]); const findings = Array.isArray(entry[`findings`]) ? (entry[`findings`] as Record[]) : []; advisories.push({ name, severity: severity as Advisory["severity"], title: asString(entry[`title`]) ?? name, // `<0.0.0` is npm's spelling of "no patch exists"; treated as absent rather than a fixable range. ...(patched === undefined || patched === `<0.0.0` ? {} : { patched }), dev: findings.length > 0 && findings.every((finding) => finding[`dev`] === true), ...standaloneOf(section), }); } return advisories; }; const parseAudit = (stdout: string): ProbeFacts | undefined => { const advisories = parseSections(stdout, parseAuditSection); return advisories === undefined ? undefined : { id: `audit`, advisories }; }; // Shared with the security chore's nightly guard, so the automation wakes on exactly what the panel shows. The output // is a stream of JSON documents (`jq -s` reads it whole): pnpm's report, then a `{"standalone": dir}` line and that // package's report for each standalone package. export const AUDIT_COMMAND = withStandalone(`pnpm audit --json 2>/dev/null`, `pnpm audit --json --ignore-workspace 2>/dev/null`); // Sample of paths only; the agent re-runs knip live, so a full list would already be stale. const DEAD_CODE_SAMPLE = 8; const parseKnip = (stdout: string): ProbeFacts | undefined => { const raw = asObject(stdout)?.[`issues`]; // `issues` is the whole report; its absence means this isn't knip's output at all. if (!Array.isArray(raw)) { return undefined; } const issues = raw.filter((issue): issue is Record => typeof issue === `object` && issue !== null); const sum = (key: string): number => issues.reduce((total, issue) => total + countOf(issue[key]), 0); const deadCode: DeadCode = { files: sum(`files`), exports: sum(`exports`), types: sum(`types`), dependencies: sum(`dependencies`), devDependencies: sum(`devDependencies`), sample: issues.flatMap((issue) => (countOf(issue[`files`]) === 0 ? [] : (asString(issue[`file`]) ?? []))).slice(0, DEAD_CODE_SAMPLE), }; return { id: `knip`, deadCode }; }; // jscpd writes JSON to a file, not stdout; percentage of scanned lines is stable, unlike a raw clone count. const DUPLICATION_SAMPLE = 5; const parseJscpd = (stdout: string): ProbeFacts | undefined => { const root = asObject(stdout); const statistics = root?.[`statistics`]; if (typeof statistics !== `object` || statistics === null) { return undefined; } const total = (statistics as Record)[`total`]; const percentage = typeof total === `object` && total !== null ? (total as Record)[`percentage`] : undefined; const duplicates = Array.isArray(root?.[`duplicates`]) ? (root[`duplicates`] as Record[]) : []; const pathOf = (side: unknown): string => typeof side === `object` && side !== null ? (asString((side as Record)[`name`]) ?? `?`) : `?`; const duplication: Duplication = { percentage: typeof percentage === `number` ? percentage : 0, clones: duplicates.length, top: duplicates .map((clone) => ({ lines: typeof clone[`lines`] === `number` ? clone[`lines`] : 0, first: pathOf(clone[`firstFile`]), second: pathOf(clone[`secondFile`]), })) .toSorted((left, right) => right.lines - left.lines) .slice(0, DUPLICATION_SAMPLE), }; return { id: `jscpd`, duplication }; }; // Timeout counts as DETECTED, NoCoverage as UNDETECTED; Pending is inconclusive, not dropped either way. const SURVIVOR_SAMPLE = 8; const DETECTED = new Set([`Killed`, `Timeout`]); const UNDETECTED = new Set([`Survived`, `NoCoverage`]); type Survivor = MutationScore["survivors"][number]; // One file's mutant list, or empty for any shape that doesn't match; a malformed entry doesn't invalidate the rest of // the report. const mutantsOf = (raw: unknown): readonly Record[] => { if (typeof raw !== `object` || raw === null) { return []; } const mutants = (raw as Record)[`mutants`]; return Array.isArray(mutants) ? mutants.filter((entry): entry is Record => typeof entry === `object` && entry !== null) : []; }; const lineOf = (mutant: Record): number => { const location = mutant[`location`]; const start = typeof location === `object` && location !== null ? (location as Record)[`start`] : undefined; const line = typeof start === `object` && start !== null ? (start as Record)[`line`] : undefined; return typeof line === `number` ? line : 0; }; const survivorOf = (file: string, mutant: Record): Survivor => ({ file, line: lineOf(mutant), mutator: asString(mutant[`mutatorName`]) ?? `?`, // Empty replacement means the mutator deleted the expression; rendered as `(removed)` rather than blank. replacement: asString(mutant[`replacement`]) ?? `(removed)`, }); interface Tally { killed: number; survived: number; inconclusive: number; readonly survivors: Survivor[]; } const tallyMutants = (files: Record): Tally => { const tally: Tally = { killed: 0, survived: 0, inconclusive: 0, survivors: [] }; for (const [file, raw] of Object.entries(files)) { for (const mutant of mutantsOf(raw)) { const status = asString(mutant[`status`]) ?? ``; if (DETECTED.has(status)) { tally.killed++; continue; } // Neither detected nor undetected got a verdict; it didn't compile or was ignored, and doesn't move the // score. if (!UNDETECTED.has(status)) { tally.inconclusive++; continue; } tally.survived++; tally.survivors.push(survivorOf(file, mutant)); } } return tally; }; const parseMutation = (stdout: string): ProbeFacts | undefined => { const files = asObject(stdout)?.[`files`]; // Missing `files` means this isn't a mutation report; an empty one is honest (nothing was mutated). if (typeof files !== `object` || files === null || Array.isArray(files)) { return undefined; } const tally = tallyMutants(files as Record); const valid = tally.killed + tally.survived; const mutation: MutationScore = { // Nothing to mutate scores 100, not NaN: no undetected fault exists, and it avoids dividing by zero. score: valid === 0 ? 100 : Math.round((tally.killed / valid) * 100), killed: tally.killed, survived: tally.survived, inconclusive: tally.inconclusive, survivors: tally.survivors.slice(0, SURVIVOR_SAMPLE), }; return { id: `mutation`, mutation }; }; // First output line is always literal marker `UI`: distinguishes a clean sweep from one that never ran. const UI_MARKER = `UI`; // Caps apply after `sort`: truncation is alphabetical and stable between runs; anything past the cap is dropped. const COMPONENT_LIMIT = 2000; const RULE_FILE_LIMIT = 500; // Required: without a path, ripgrep reads stdin instead of the tree when run non-interactively, as here. const SCAN_ROOT = `.`; const globArgs = (globs: readonly string[]): string => [...[...globs, ...SCAN_IGNORES].map((glob) => `-g '${glob}'`), WORKSPACE_ROOT_RG_EXCLUDE_ARG].join(` `); // `path:count` from `rg --count-matches`; splits at the last colon since a path may contain one but the count is always // trailing digits. const splitCount = (text: string): { path: string; count: number } | undefined => { const at = text.lastIndexOf(`:`); if (at <= 0) { return undefined; } const count = Number.parseInt(text.slice(at + 1), 10); return Number.isNaN(count) || count <= 0 ? undefined : { path: normalizePath(text.slice(0, at)), count }; }; // Emits a bare path per match: `-l` for a present rule, `--files-without-match` for an absent one; no count is ever // needed downstream. const idiomCommand = (rule: IdiomRule): string => `rg --no-messages ${rule.absent === undefined ? `-l` : `--files-without-match`} -e '${rule.pattern}' ${globArgs(rule.globs)} ${SCAN_ROOT} 2>/dev/null ` + `| sort | head -n ${RULE_FILE_LIMIT} | awk '{print "IDIOM\\t${rule.id}\\t" $0}'`; const scanCommand = (): string => [ `echo ${UI_MARKER}`, `rg --files ${globArgs(COMPONENT_GLOBS)} ${SCAN_ROOT} 2>/dev/null | sort | head -n ${COMPONENT_LIMIT} | awk '{print "COMPONENT\\t" $0}'`, `rg --no-messages --count-matches -e '${BYPASS_PATTERN}' ${globArgs(MARKUP_GLOBS)} ${SCAN_ROOT} 2>/dev/null ` + `| sort | head -n ${RULE_FILE_LIMIT} | awk '{print "BYPASS\\t" $0}'`, ...IDIOM_RULES.map(idiomCommand), // Resets exit status to 0: an rg above exiting 1 (no matches) is the healthy case, not a broken command. `true`, ].join(`; `); const parseUi = (stdout: string): ProbeFacts | undefined => { const lines = stdout.split(`\n`).map((line) => line.trim()); if (lines.find((line) => line !== ``) !== UI_MARKER) { return undefined; } const components: string[] = []; const bypasses: UiScan["bypasses"] = []; const byIdiom = new Map(); for (const line of lines) { const [label, ...rest] = line.split(`\t`); if (label === `COMPONENT` && rest[0] !== undefined) { components.push(normalizePath(rest[0])); } else if (label === `BYPASS`) { const hit = splitCount(rest.join(`\t`)); if (hit !== undefined) { bypasses.push(hit); } } else if (label === `IDIOM` && rest[0] !== undefined) { const path = normalizePath(rest.slice(1).join(`\t`)); if (path !== ``) { byIdiom.set(rest[0], [...(byIdiom.get(rest[0]) ?? []), path]); } } } const scan: UiScan = { components, bypasses, idioms: [...byIdiom].map(([id, files]) => ({ id, files })) }; return { id: `ui`, scan }; }; // Checks disk, not the toolchain: a CI-only build reports unavailable, not a wrong number. const BUILD_DIRS = [`dist`, `build`, `out`, `public/build`]; const BUNDLE_MARKER = `DIR`; // Ranking cutoff; past this are long-tail lazy chunks not worth the badge route's cost to carry. const ASSET_LIMIT = 40; const bundleCommand = (): string => [ `dir=""`, `for d in ${BUILD_DIRS.join(` `)}; do if [ -d "$d" ]; then dir="$d"; break; fi; done`, `[ -n "$dir" ] || exit 0`, `printf '${BUNDLE_MARKER}\\t%s\\n' "$dir"`, // `-exec ... {} +`, not a `for` loop: an asset path can contain a space; sorted by bytes so head is the // ranking. `find "$dir" -type f \\( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.css' \\) ` + `-exec sh -c 'for f; do printf "ASSET\\t%s\\t%s\\t%s\\n" "$(wc -c <"$f")" "$(gzip -c "$f" | wc -c)" "$f"; done' _ {} + ` + `2>/dev/null | sort -k2 -rn | head -n ${ASSET_LIMIT}`, ].join(`; `); const parseBundle = (stdout: string): ProbeFacts | undefined => { const lines = stdout.split(`\n`).map((line) => line.trim()); const dirLine = lines.find((line) => line.startsWith(`${BUNDLE_MARKER}\t`)); if (dirLine === undefined) { return undefined; } const assets: Bundle["assets"] = []; for (const line of lines) { const [label, rawBytes, rawGzip, ...path] = line.split(`\t`); if (label !== `ASSET` || path.length === 0) { continue; } const bytes = Number.parseInt(rawBytes ?? ``, 10); const gzip = Number.parseInt(rawGzip ?? ``, 10); if (Number.isNaN(bytes) || Number.isNaN(gzip)) { continue; } assets.push({ path: path.join(`\t`), bytes, gzip }); } const bundle: Bundle = { dir: dirLine.slice(BUNDLE_MARKER.length + 1), // Sum of the assets carried (the ranking), not the whole build; avoids a second walk to total the rest. totalBytes: assets.reduce((sum, asset) => sum + asset.bytes, 0), totalGzip: assets.reduce((sum, asset) => sum + asset.gzip, 0), assets, }; return { id: `bundle`, bundle }; }; // jscpd report dir under /tmp, read immediately then discarded; same path chores.ts uses. const JSCPD_DIR = `/tmp/intentic-chore-jscpd`; // Stryker's own default json-report path, not configurable per run; read from the repo dir the probe runs in. const MUTATION_REPORT = `reports/mutation/mutation.json`; export const PROBES: readonly ProbeSpec[] = [ { id: `outdated`, title: `Dependency versions`, measures: `how far behind the registry each dependency is`, tier: 1, ttlMs: DAY_MS, timeoutMs: 5 * 60_000, available: `test -f package.json`, unavailable: `no package.json`, // `-r` reports every workspace package, not just root, and resolves `catalog:` through pnpm-workspace.yaml, // so the catalog is what gets measured. It also honours `minimumReleaseAge`: a release younger than the // floor is not reported as latest, by the same policy that would refuse to install it. command: withStandalone(`pnpm outdated -r --json 2>/dev/null`, `pnpm outdated --json --ignore-workspace 2>/dev/null`), parse: parseOutdated, }, { id: `audit`, title: `Security advisories`, measures: `published advisories against this dependency tree`, tier: 1, ttlMs: DAY_MS, timeoutMs: 5 * 60_000, // A lockfile, not package.json: audit resolves the installed tree, and without one pnpm has nothing to check. available: `test -f pnpm-lock.yaml || test -f package-lock.json`, unavailable: `no lockfile`, command: AUDIT_COMMAND, parse: parseAudit, }, { id: `knip`, title: `Unreachable code`, measures: `files, exports and dependencies nothing references`, tier: 2, ttlMs: 7 * DAY_MS, timeoutMs: 15 * 60_000, // The repo's own knip, never a floating one: a mismatched version can call its own live API dead code. available: `pnpm exec knip --version >/dev/null 2>&1`, unavailable: `knip is not a devDependency`, command: `pnpm exec knip --reporter json --no-exit-code 2>/dev/null || true`, parse: parseKnip, }, { id: `jscpd`, title: `Copy-paste`, measures: `how much of the tree is duplicated elsewhere in it`, tier: 2, ttlMs: 7 * DAY_MS, timeoutMs: 20 * 60_000, available: `test -f package.json`, unavailable: `no package.json`, // `--threshold 100` stops jscpd failing on its own opinion; the chore judges duplication from the percentage. command: `pnpm dlx jscpd ${WORKSPACE_ROOT_JSCPD_EXCLUDE_ARG} --reporters json --output ${JSCPD_DIR} --min-lines 12 --threshold 100 . >/dev/null 2>&1; ` + `cat ${JSCPD_DIR}/jscpd-report.json 2>/dev/null`, parse: parseJscpd, }, { id: `mutation`, title: `Test strength`, measures: `how much of the code could break with every test still passing`, // `--incremental` reruns only changed mutants; the 30-day TTL targets that costly first full run. tier: 2, ttlMs: 30 * DAY_MS, timeoutMs: 90 * 60_000, // Config presence means the repo opted in, not just having Stryker; must use the repo's own stryker, not `dlx`. available: `{ test -f stryker.conf.mjs || test -f stryker.conf.json; } && pnpm exec stryker --version >/dev/null 2>&1`, unavailable: `no stryker config in this repo`, // `|| true`: Stryker exits non-zero under its own score threshold; the parser decides if the run was valid. command: `pnpm exec stryker run --reporters json --incremental >/dev/null 2>&1 || true; cat ${MUTATION_REPORT} 2>/dev/null`, parse: parseMutation, }, { id: `ui`, title: `Front-end source`, measures: `components, hard-coded styles and idioms the framework has replaced`, // Tier 1 despite scanning the tree: ripgrep walks cost seconds, and findings move daily as components change. tier: 1, ttlMs: DAY_MS, timeoutMs: 5 * 60_000, // Any manifest declaring a framework or Tailwind, not just root's; monorepos often keep React in an app // package. available: `rg -l --no-messages -g '**/package.json' -g '!**/node_modules/**' ${WORKSPACE_ROOT_RG_EXCLUDE_ARG} ` + `-e '[\\x22](${[...UI_FRAMEWORKS.flatMap((framework) => framework.packages), ...TAILWIND_PACKAGES].join(`|`)})[\\x22]\\s*:' . >/dev/null`, unavailable: `no package here declares a UI framework or Tailwind`, command: scanCommand(), parse: parseUi, }, { id: `bundle`, title: `Build output`, measures: `what the last build put on disk for a browser to download`, tier: 1, ttlMs: DAY_MS, timeoutMs: 5 * 60_000, // Must contain an actual asset file: `-d` alone would pass an empty `dist/` left by a cleaned checkout. available: `find ${BUILD_DIRS.join(` `)} -maxdepth 4 -type f \\( -name '*.js' -o -name '*.mjs' -o -name '*.css' \\) 2>/dev/null | head -n 1 | grep -q .`, // States it never builds, only reads the last one: "no output" would otherwise read as "the build failed". unavailable: `no build output on disk, this reads the last build, it never runs one`, command: bundleCommand(), parse: parseBundle, }, ]; export const probeSpec = (id: ProbeId): ProbeSpec => { const spec = PROBES.find((probe) => probe.id === id); if (spec === undefined) { throw new Error(`chores: no probe named "${id}"`); } return spec; };