@mhosaic/feedback-cli 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +6 -1
- package/dist/bin.js.map +1 -1
- package/dist/build-RCLWV2WL.js +565 -0
- package/dist/build-RCLWV2WL.js.map +1 -0
- package/dist/check-FOJPEE4Q.js +342 -0
- package/dist/check-FOJPEE4Q.js.map +1 -0
- package/dist/chunk-AZQSQJNQ.js +83 -0
- package/dist/chunk-AZQSQJNQ.js.map +1 -0
- package/dist/chunk-IHJPCMYF.js +126 -0
- package/dist/chunk-IHJPCMYF.js.map +1 -0
- package/dist/chunk-SSLQOK2Z.js +456 -0
- package/dist/chunk-SSLQOK2Z.js.map +1 -0
- package/dist/config-JE3QRZVH.js +8 -0
- package/dist/config-JE3QRZVH.js.map +1 -0
- package/dist/generate-VFRQNPOI.js +14 -0
- package/dist/generate-VFRQNPOI.js.map +1 -0
- package/dist/{install-skill-QJ4ZDVVR.js → install-skill-PO5YSXWY.js} +4 -2
- package/dist/{install-skill-QJ4ZDVVR.js.map → install-skill-PO5YSXWY.js.map} +1 -1
- package/dist/qa-YR4GCV6T.js +43 -0
- package/dist/qa-YR4GCV6T.js.map +1 -0
- package/dist/sitemap-react-QTEAUKN5.js +330 -0
- package/dist/sitemap-react-QTEAUKN5.js.map +1 -0
- package/dist/sitemap-vue-EJDQTCYM.js +243 -0
- package/dist/sitemap-vue-EJDQTCYM.js.map +1 -0
- package/package.json +4 -3
- package/skills/integrate-feedback/SKILL.md +7 -0
- package/skills/integrate-qa-meter/SKILL.md +201 -0
- package/skills/integrate-qa-meter/references/qa-meter-config.md +168 -0
- package/skills/integrate-qa-meter/references/qa-meter-troubleshooting.md +37 -0
- package/skills/integrate-qa-meter/references/qa-meter.config.example.json +20 -0
- package/skills/integrate-qa-meter/references/qa-meter.config.example.react.json +19 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/qa/build.ts","../src/qa/color.ts","../src/qa/results.ts","../src/qa/load-suites.ts"],"sourcesContent":["/**\n * `qa build` — assemble the `qa-status.json` artifact from suites + sitemap +\n * Playwright results. HONEST by construction: lanes reflect only real evidence\n * (a scenario whose case is `covered` with a matching report entry gets that\n * run's lane; everything else is `never_run`), no client validations exist in\n * v1 (⇒ colour ceiling is `yellow`), and pages with no scenarios stay `gray`.\n *\n * Pipeline: load suites → per-pattern scenario seeds; DEDUP generated twins of\n * authored automation links (authored wins); merge automated/dev/client lanes;\n * derive scenario colour; build page-status entries from the sitemap (or a flat\n * list in degraded mode); roll the colours/counts up the sitemap subtree; and\n * compute the dual coverage/passing metric.\n *\n * The structural assembly helpers (`countsFor`, the rollup tree-walk, the dual\n * metric) are ported from the Vigilance donor `scripts/build-demo-status.mjs`\n * (every DEMO_/synthetic block discarded).\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { loadQaConfig, type QaConfig } from './config'\nimport { aggregateColor, deriveScenarioColor, isValidationStale } from './color'\nimport { readResults, type AutomatedLaneResult } from './results'\nimport { loadScenarioSeeds, type ScenarioSeed } from './load-suites'\nimport type {\n QaAutomatedLane,\n QaColor,\n QaCounts,\n QaHumanLane,\n QaMetric,\n QaPageStatus,\n QaScenario,\n QaScenarioPageRef,\n QaSitemapNode,\n QaStatusArtifact,\n} from './artifact-types'\nimport type { SitemapPage, SitemapPayload } from './sitemap-core'\n\nexport interface BuildOptions {\n /** App version stamped into the artifact (overrides cfg.appVersion). */\n appVersion?: string\n /** Injected for deterministic goldens; `runBuild` passes `new Date().toISOString()`. */\n generatedAt?: string\n}\n\nconst EMPTY_HUMAN: QaHumanLane = { validated: false, stale: false }\n/** Stable app_version fallback so the golden is reproducible when none is supplied. */\nconst FALLBACK_VERSION = '0.0.0'\n\n// ─────────────────────────────────────────────────── helpers ────────────────\n\n/** Slug-free stable node id for a pattern (ported from the donor `idFor`). */\nfunction idFor(pattern: string): string {\n return 'p' + pattern.replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+$/, '')\n}\n\nfunction humanize(str: string): string {\n if (!str) return ''\n const cleaned = str.replace(/[:?()*]/g, '').replace(/[-_/]+/g, ' ').trim()\n return cleaned ? cleaned.charAt(0).toUpperCase() + cleaned.slice(1) : ''\n}\n\nfunction lastSegment(pattern: string): string {\n const parts = pattern.split('/').filter(Boolean)\n return parts[parts.length - 1] ?? ''\n}\n\n/** Human title for a sitemap page (ported from the donor `titleFor`). */\nfunction titleForPage(page: SitemapPage): string {\n return (\n page.title ||\n humanize(page.route_name ?? '') ||\n humanize(lastSegment(page.path_pattern)) ||\n page.path_pattern\n )\n}\n\nfunction emptyCounts(): QaCounts {\n return { scenarios: 0, passing: 0, dev_validated: 0, client_validated: 0 }\n}\n\n/** Counts over a scenario list (ported from the donor `countsFor`). */\nfunction countsFor(scenarios: QaScenario[]): QaCounts {\n return {\n scenarios: scenarios.length,\n passing: scenarios.filter((s) => s.automated.status === 'passing').length,\n dev_validated: scenarios.filter((s) => s.dev.validated).length,\n client_validated: scenarios.filter((s) => s.client.validated).length,\n }\n}\n\n// ─────────────────────────────────────── dev validations (optional) ─────────\n\ninterface DevValidationEvent {\n external_key?: string\n validated?: boolean\n rejected?: boolean\n by?: string\n at?: string\n app_version?: string\n note?: string\n}\n\n/**\n * Latest dev-validation event per `external_key`, from the optional\n * `devValidationsFile` (a flat array of events). Latest = greatest `at`.\n */\nfunction loadDevValidations(file: string | null): Map<string, DevValidationEvent> {\n const out = new Map<string, DevValidationEvent>()\n if (!file || !existsSync(file)) return out\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'))\n } catch (err) {\n throw new Error(`qa build: failed to read dev validations ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n const events: DevValidationEvent[] = Array.isArray(parsed)\n ? (parsed as DevValidationEvent[])\n : Array.isArray((parsed as { events?: unknown }).events)\n ? ((parsed as { events: DevValidationEvent[] }).events)\n : []\n for (const ev of events) {\n if (!ev.external_key) continue\n const prev = out.get(ev.external_key)\n if (!prev || (ev.at ?? '') >= (prev.at ?? '')) out.set(ev.external_key, ev)\n }\n return out\n}\n\n// ─────────────────────────────────────────── scenario assembly ──────────────\n\n/**\n * Merge a seed's lanes into a finished `QaScenario`:\n * - automated: the report lane WHEN the case is `covered` AND a report entry\n * exists; else `never_run` (honesty — no invented runs);\n * - dev: latest event from the dev-validations file by `external_key`, with\n * derived staleness; else empty;\n * - client: always empty in v1.\n */\nfunction assembleScenario(\n seed: ScenarioSeed,\n results: Map<string, AutomatedLaneResult>,\n devEvents: Map<string, DevValidationEvent>,\n): QaScenario {\n let automated: QaAutomatedLane = { status: 'never_run' }\n if (seed.caseStatus === 'covered' && seed.spec_path && seed.test_title) {\n const lane = results.get(`${seed.spec_path}::${seed.test_title}`)\n if (lane) {\n automated = {\n status: lane.status,\n ...(lane.at ? { at: lane.at } : {}),\n ...(lane.provenance ? { provenance: lane.provenance } : {}),\n }\n }\n }\n\n let dev: QaHumanLane = { ...EMPTY_HUMAN }\n const ev = devEvents.get(seed.external_key)\n if (ev) {\n const stale = ev.validated\n ? isValidationStale({\n ...(ev.at ? { validatedAt: ev.at } : {}),\n automatedStatus: automated.status,\n ...(automated.at ? { automatedAt: automated.at } : {}),\n })\n : false\n dev = {\n validated: ev.validated === true,\n stale,\n ...(ev.rejected ? { rejected: true } : {}),\n ...(ev.by ? { by: ev.by } : {}),\n ...(ev.at ? { at: ev.at } : {}),\n ...(ev.app_version ? { app_version: ev.app_version } : {}),\n ...(ev.note ? { note: ev.note } : {}),\n }\n }\n\n const client: QaHumanLane = { ...EMPTY_HUMAN }\n const color = deriveScenarioColor(automated, dev, client)\n\n return {\n id: seed.id,\n external_key: seed.external_key,\n title: seed.title,\n color,\n source: seed.source,\n ...(seed.spec_path ? { spec_path: seed.spec_path } : {}),\n pages: seed.pages,\n group: seed.group,\n ...(seed.priority ? { priority: seed.priority } : {}),\n ...(seed.caseStatus ? { caseStatus: seed.caseStatus } : {}),\n ...(seed.grounding ? { grounding: seed.grounding } : {}),\n automated,\n dev,\n client,\n }\n}\n\n/**\n * DEDUP (Step 3): an authored case whose `(spec_path, test_title)` an automation\n * link names suppresses the GENERATED twin with the same key. Authored wins (it\n * carries human metadata). Returns the seeds to keep, per pattern unchanged.\n */\nfunction dedupSeeds(byPattern: Map<string, ScenarioSeed[]>): Map<string, ScenarioSeed[]> {\n // Collect every authored dedup key across all patterns.\n const authoredKeys = new Set<string>()\n for (const seeds of byPattern.values()) {\n for (const seed of seeds) {\n if (seed.origin === 'authored' && seed.dedupKey) authoredKeys.add(seed.dedupKey)\n }\n }\n const out = new Map<string, ScenarioSeed[]>()\n for (const [pattern, seeds] of byPattern) {\n const kept = seeds.filter(\n (seed) => !(seed.origin === 'generated' && seed.dedupKey && authoredKeys.has(seed.dedupKey)),\n )\n out.set(pattern, kept)\n }\n return out\n}\n\n/** Primary-scenario colours of a page (the page colour aggregates only these). */\nfunction primaryColors(scenarios: QaScenario[], pattern: string): QaColor[] {\n return scenarios\n .filter((s) => (s.pages ?? []).some((ref: QaScenarioPageRef) => ref.pattern === pattern && ref.role === 'primary'))\n .map((s) => s.color)\n}\n\n// ──────────────────────────────────────────────── artifact build ────────────\n\n/** Read & validate the sitemap payload from `file`. */\nfunction loadSitemap(file: string): SitemapPayload {\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'))\n } catch (err) {\n throw new Error(`qa build: failed to read sitemap ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n if (typeof parsed !== 'object' || parsed === null || !Array.isArray((parsed as SitemapPayload).pages)) {\n throw new Error(`qa build: sitemap ${file} is malformed (expected { format, pages: [] })`)\n }\n return parsed as SitemapPayload\n}\n\n/**\n * Assemble the full `QaStatusArtifact`. Pure given its inputs + `opts` (no\n * non-deterministic `Date` on the tested path — `generatedAt` is injectable).\n */\nexport function buildArtifact(cfg: QaConfig, opts: BuildOptions = {}): QaStatusArtifact {\n const appVersion = opts.appVersion ?? cfg.appVersion ?? FALLBACK_VERSION\n const generatedAt = opts.generatedAt ?? FALLBACK_VERSION\n\n // 1. Seeds + dedup.\n const { byPattern: rawByPattern } = loadScenarioSeeds(cfg.suitesDir)\n const byPattern = dedupSeeds(rawByPattern)\n\n // 2. Run + dev evidence.\n const results = cfg.resultsFile && existsSync(cfg.resultsFile) ? readResults(cfg.resultsFile) : new Map()\n const devEvents = loadDevValidations(cfg.devValidationsFile)\n\n // 3. Assemble scenarios per pattern.\n const scenariosByPattern = new Map<string, QaScenario[]>()\n for (const [pattern, seeds] of byPattern) {\n scenariosByPattern.set(\n pattern,\n seeds.map((seed) => assembleScenario(seed, results, devEvents)),\n )\n }\n\n if (!cfg.sitemap) {\n return buildDegraded(scenariosByPattern, cfg, appVersion, generatedAt)\n }\n return buildWithSitemap(loadSitemap(cfg.sitemap.file), scenariosByPattern, cfg, appVersion, generatedAt)\n}\n\n/** A page-status entry for one pattern from its scenarios. */\nfunction pageStatusFor(\n pattern: string,\n title: string,\n scenarios: QaScenario[],\n): QaPageStatus {\n return {\n page: { id: idFor(pattern), path_pattern: pattern, title },\n color: aggregateColor(primaryColors(scenarios, pattern)),\n counts: countsFor(scenarios),\n scenarios,\n }\n}\n\nfunction pct(num: number, den: number): number {\n return den === 0 ? 0 : Math.round((100 * num) / den)\n}\n\nfunction metricFor(pageStatuses: QaPageStatus[], navigableTotal: number): QaMetric {\n const pagesWithScenarios = pageStatuses.filter((s) => s.counts.scenarios > 0).length\n const scenariosTotal = pageStatuses.reduce((n, s) => n + s.counts.scenarios, 0)\n const scenariosPassing = pageStatuses.reduce((n, s) => n + s.counts.passing, 0)\n return {\n pages_with_scenarios_pct: pct(pagesWithScenarios, navigableTotal),\n scenarios_passing_pct: pct(scenariosPassing, scenariosTotal),\n }\n}\n\n/**\n * Sitemap mode: page-status per navigable page node, plus the sitemap emitted as\n * a FOREST — one node per input page, `parent_id` = the id of the node whose\n * `path_pattern` is this page's input `parent` (or `null` for top-level pages).\n * NO synthetic root is invented: the real sitemap is a forest of top-level roots\n * (e.g. /login, /admin, /ingredients…), and the renderer's `buildTree` already\n * iterates every `parent_id === null` node as a root. Each node's rollup is over\n * its OWN subtree, so no node spans the whole app — correct for a forest.\n */\nfunction buildWithSitemap(\n sitemap: SitemapPayload,\n scenariosByPattern: Map<string, QaScenario[]>,\n cfg: QaConfig,\n appVersion: string,\n generatedAt: string,\n): QaStatusArtifact {\n const allPages = sitemap.pages\n const navigable = allPages.filter((p) => !p.structural)\n const titleByPattern = new Map(allPages.map((p) => [p.path_pattern, titleForPage(p)]))\n\n // Page-status for every navigable sitemap page (scenarios or none → gray).\n const pageStatusByPattern = new Map<string, QaPageStatus>()\n for (const page of navigable) {\n const scenarios = scenariosByPattern.get(page.path_pattern) ?? []\n pageStatusByPattern.set(\n page.path_pattern,\n pageStatusFor(page.path_pattern, titleByPattern.get(page.path_pattern)!, scenarios),\n )\n }\n\n // Patterns with scenarios but NO sitemap node — keep them so coverage isn't\n // lost (the design says `check` reports these). They get a page entry with a\n // null sitemap node id but are NOT rolled into the tree.\n const orphanPages: QaPageStatus[] = []\n for (const [pattern, scenarios] of scenariosByPattern) {\n if (!pageStatusByPattern.has(pattern)) {\n orphanPages.push(pageStatusFor(pattern, humanize(lastSegment(pattern)) || pattern, scenarios))\n }\n }\n\n const pages: QaPageStatus[] = [...pageStatusByPattern.values(), ...orphanPages]\n\n // ── Sitemap nodes with bottom-up rollups ──────────────────────────────────\n const childrenByParent = new Map<string | null, SitemapPage[]>()\n for (const p of allPages) {\n const list = childrenByParent.get(p.parent)\n if (list) list.push(p)\n else childrenByParent.set(p.parent, [p])\n }\n const subtree = (pattern: string): string[] => {\n const out = [pattern]\n for (const child of childrenByParent.get(pattern) ?? []) out.push(...subtree(child.path_pattern))\n return out\n }\n\n // Per-node rollup over the node's OWN subtree (real, non-structural pages only).\n const rollupOver = (statuses: QaPageStatus[]) => ({\n pages_total: statuses.length,\n pages_green: statuses.filter((s) => s.color === 'green').length,\n scenarios_total: statuses.reduce((n, s) => n + s.counts.scenarios, 0),\n scenarios_green: statuses.reduce((n, s) => n + s.scenarios.filter((x) => x.color === 'green').length, 0),\n })\n\n const nodes: QaSitemapNode[] = allPages.map((page) => {\n const status = pageStatusByPattern.get(page.path_pattern)\n const ownCounts = status ? status.counts : emptyCounts()\n // Subtree = self + descendants; keep only real (navigable) pages for rollups.\n const descStatuses = subtree(page.path_pattern)\n .map((pat) => pageStatusByPattern.get(pat))\n .filter((s): s is QaPageStatus => Boolean(s))\n return {\n id: idFor(page.path_pattern),\n path_pattern: page.path_pattern,\n title: titleByPattern.get(page.path_pattern)!,\n // Map the input `parent` (a path_pattern) → that node's id; null stays a root.\n parent_id: page.parent ? idFor(page.parent) : null,\n order: page.order,\n ...(page.structural ? { structural: true } : {}),\n color: aggregateColor(descStatuses.map((s) => s.color)),\n counts: ownCounts,\n rollup: rollupOver(descStatuses),\n }\n })\n\n // Global dual metric over ALL pages app-wide (independent of any sitemap node).\n const metric = metricFor(pages, navigable.length + orphanPages.length)\n\n return {\n format: 1,\n environment: cfg.environment,\n app_version: appVersion,\n generated_at: generatedAt,\n pages,\n sitemap: nodes,\n metric,\n }\n}\n\n/**\n * Degraded mode (no sitemap): a FLAT page list synthesized from the patterns\n * that have scenarios. Every sitemap node has `parent_id: null` (no tree). One\n * node per pattern, rolled up over itself only.\n */\nfunction buildDegraded(\n scenariosByPattern: Map<string, QaScenario[]>,\n cfg: QaConfig,\n appVersion: string,\n generatedAt: string,\n): QaStatusArtifact {\n const patterns = [...scenariosByPattern.keys()].sort((a, b) => a.localeCompare(b))\n const pages: QaPageStatus[] = patterns.map((pattern) =>\n pageStatusFor(pattern, humanize(lastSegment(pattern)) || pattern, scenariosByPattern.get(pattern)!),\n )\n\n const sitemap: QaSitemapNode[] = pages.map((ps, i) => ({\n id: ps.page!.id,\n path_pattern: ps.page!.path_pattern,\n title: ps.page!.title,\n parent_id: null,\n order: i,\n color: ps.color,\n counts: ps.counts,\n rollup: {\n pages_total: 1,\n pages_green: ps.color === 'green' ? 1 : 0,\n scenarios_total: ps.counts.scenarios,\n scenarios_green: ps.scenarios.filter((x) => x.color === 'green').length,\n },\n }))\n\n return {\n format: 1,\n environment: cfg.environment,\n app_version: appVersion,\n generated_at: generatedAt,\n pages,\n sitemap,\n metric: metricFor(pages, pages.length),\n }\n}\n\n// ─────────────────────────────────────────────────── CLI entry ──────────────\n\n/** Parse `--version <v>` from the arg list. */\nfunction parseVersion(args: string[]): string | undefined {\n const i = args.indexOf('--version')\n if (i >= 0 && args[i + 1]) return args[i + 1]\n return undefined\n}\n\nexport async function runBuild(args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n const appVersion = parseVersion(args)\n let artifact: QaStatusArtifact\n try {\n artifact = buildArtifact(cfg, {\n ...(appVersion ? { appVersion } : {}),\n generatedAt: new Date().toISOString(),\n })\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exitCode = 1\n return\n }\n\n mkdirSync(dirname(cfg.outFile), { recursive: true })\n writeFileSync(cfg.outFile, `${JSON.stringify(artifact, null, 2)}\\n`, 'utf8')\n\n process.stderr.write(\n `qa build → ${cfg.outFile}\\n` +\n ` ${artifact.metric.pages_with_scenarios_pct}% pages with scenarios · ` +\n `${artifact.metric.scenarios_passing_pct}% scenarios passing\\n`,\n )\n}\n","/**\n * QA-Meter colour algebra — CLI-LOCAL MIRROR of the core package's\n * `packages/core/src/qa-meter/color.ts`. Lifted verbatim (it is tiny and\n * dependency-free) so the CLI stays self-contained: the build derives the SAME\n * colours the renderer would, from a single set of algebra rules.\n *\n * If the rules ever change, update BOTH copies — they are intentionally\n * structurally identical (doc 03 §9.2/§9.3/§9.4).\n */\n\nexport type QaColor = 'green' | 'yellow' | 'red' | 'gray'\nexport type QaAutomatedStatus = 'passing' | 'failing' | 'flaky' | 'never_run'\n\n/** Minimal shape of the automated lane the colour needs. */\nexport interface AutomatedLaneLike {\n status: QaAutomatedStatus\n}\n\n/** Minimal shape of a human lane (dev or client). */\nexport interface HumanLaneLike {\n validated: boolean\n rejected?: boolean\n /** Derived (never stored): a validation contradicted by a more-recent failing run. */\n stale?: boolean\n}\n\n/**\n * Colour of a scenario from its lane trinity (doc 03 §9.3):\n * - `red` : automated run failing OR latest human event `rejected`;\n * - `gray` : never run AND no human validation;\n * - `green` : automated `passing` AND dev validated AND client validated, nothing stale;\n * - `yellow`: everything else (incl. \"passes, awaiting validation\").\n *\n * In the temporary meter the client lane is always empty ⇒ `green` is\n * unreachable: the ceiling colour is `yellow`, by design (honesty §12).\n */\nexport function deriveScenarioColor(\n automated: AutomatedLaneLike,\n dev: HumanLaneLike,\n client: HumanLaneLike,\n): QaColor {\n const rejected = dev.rejected === true || client.rejected === true\n if (automated.status === 'failing' || rejected) {\n return 'red'\n }\n const everRun = automated.status !== 'never_run'\n const anyHuman = dev.validated || client.validated\n if (!everRun && !anyHuman) {\n return 'gray'\n }\n const fresh = dev.stale !== true && client.stale !== true\n if (automated.status === 'passing' && dev.validated && client.validated && fresh) {\n return 'green'\n }\n return 'yellow'\n}\n\n/**\n * Aggregate colour of a set: a page from its PRIMARY scenarios, or a sitemap node\n * from its subtree pages. Precedence (doc 03 §9.3): one red dominates; else all\n * green ⇒ green; empty or all gray ⇒ gray; else yellow.\n */\nexport function aggregateColor(colors: ReadonlyArray<QaColor>): QaColor {\n if (colors.length === 0) {\n return 'gray'\n }\n if (colors.includes('red')) {\n return 'red'\n }\n if (colors.every((c) => c === 'green')) {\n return 'green'\n }\n if (colors.every((c) => c === 'gray')) {\n return 'gray'\n }\n return 'yellow'\n}\n\n/**\n * Lenient staleness (§9.4) + light re-break (§9.2): a human validation is\n * \"contradicted\" when a FAILING automated run is more recent than it. We do NOT\n * stale on a mere version bump while automated passes. Timestamps = ISO 8601\n * (lexicographically comparable).\n */\nexport function isValidationStale(opts: {\n validatedAt?: string\n automatedStatus: QaAutomatedStatus\n automatedAt?: string\n}): boolean {\n if (!opts.validatedAt) {\n return false\n }\n return opts.automatedStatus === 'failing' && !!opts.automatedAt && opts.automatedAt > opts.validatedAt\n}\n","/**\n * `qa build` — read a Playwright JSON report into per-(spec, test) automated\n * lanes. HONEST: a lane reflects only what the report says. A test absent from\n * the report yields NO lane (the build maps that to `never_run`).\n *\n * The report nests: `report.suites[]` may contain `.suites[]`; each suite has\n * `.specs[]`; each spec has `.file`/`.title`/`.tests[]`; each test has\n * `.results[]` with `.status`/`.retry`/`.startTime`. We key on the spec `file`\n * EXACTLY as it appears in the report (e2eDir-relative) + the test title.\n */\n\nimport { readFileSync } from 'node:fs'\nimport type { QaAutomatedStatus } from './color'\n\n/** The automated lane derived for one (spec, title) pair from the report. */\nexport interface AutomatedLaneResult {\n status: QaAutomatedStatus\n /** Last result's `startTime` (ISO 8601), if any. */\n at?: string\n /** Provenance summary for the tooltip — e.g. \"laptop · clone · retried\". */\n provenance?: string\n}\n\ninterface RawResult {\n status?: string\n retry?: number\n startTime?: string\n}\ninterface RawTest {\n results?: RawResult[]\n}\ninterface RawSpec {\n file?: string\n title?: string\n tests?: RawTest[]\n}\ninterface RawSuite {\n file?: string\n specs?: RawSpec[]\n suites?: RawSuite[]\n}\ninterface RawReport {\n suites?: RawSuite[]\n}\n\n/** Provenance summary from runner env vars + whether the test was retried. */\nfunction provenanceFor(retried: boolean): string | undefined {\n const parts: string[] = []\n const runner = process.env.QA_RUNNER\n const database = process.env.QA_DATABASE\n const authMode = process.env.QA_AUTH_MODE\n if (runner) parts.push(runner)\n if (database) parts.push(database)\n if (authMode) parts.push(authMode)\n if (retried) parts.push('retried')\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\n/**\n * Build the automated lane for one spec test from its `results[]`. The LAST\n * result is authoritative (Playwright appends retries): passed → `passing`,\n * unless it took a retry to get there → `flaky`; otherwise `failing`.\n */\nfunction laneFor(test: RawTest): AutomatedLaneResult {\n const results = test.results ?? []\n if (results.length === 0) {\n return { status: 'never_run' }\n }\n const last = results[results.length - 1]!\n const at = last.startTime\n const retried = (last.retry ?? 0) > 0 || results.length > 1\n let status: QaAutomatedStatus\n if (last.status === 'passed') {\n status = retried ? 'flaky' : 'passing'\n } else {\n status = 'failing'\n }\n const provenance = provenanceFor(retried)\n return { status, ...(at ? { at } : {}), ...(provenance ? { provenance } : {}) }\n}\n\n/**\n * Read a Playwright JSON report into a Map keyed `\"{specFile}::{testTitle}\"`.\n * Walks nested suites recursively. The spec `file` is used verbatim.\n */\nexport function readResults(file: string): Map<string, AutomatedLaneResult> {\n const lanes = new Map<string, AutomatedLaneResult>()\n let report: RawReport\n try {\n report = JSON.parse(readFileSync(file, 'utf8')) as RawReport\n } catch (err) {\n throw new Error(`qa build: failed to read Playwright report ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n\n const walk = (suite: RawSuite): void => {\n for (const spec of suite.specs ?? []) {\n const specFile = spec.file ?? suite.file\n if (!specFile || !spec.title) continue\n for (const test of spec.tests ?? []) {\n lanes.set(`${specFile}::${spec.title}`, laneFor(test))\n }\n }\n for (const child of suite.suites ?? []) walk(child)\n }\n for (const suite of report.suites ?? []) walk(suite)\n\n return lanes\n}\n","/**\n * `qa build` — load REAL suites (`suitesDir/`) and project them into per-pattern\n * QA-Meter scenario SEEDS (one instance per case × applicable page). Ported from\n * the Vigilance donor `scripts/load-suites.mjs` to TypeScript against the shared\n * `suite-format.ts` types.\n *\n * Sources discovered (no hard-coded list — drop a file in, it is ingested):\n * - module suites: `suitesDir/modules/<section>/<section>-test-suite.yaml`\n * - generated suites: `suitesDir/generated/*.yaml` (treated as module suites)\n * - cross-module suites: `*-test-suite.yaml` at the root of `suitesDir`.\n *\n * `page_sets` and `applies_to` are expanded; `traverses` is resolved; each (case\n * × applicable page) becomes one seed attached to that page's pattern — the\n * coverage \"numerator\". HONESTY: we do NOT fabricate a run result here. A seed\n * carries the case metadata + automation LINK; the build merges the actual run\n * lane (or `never_run`) and computes colour.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parse } from 'yaml'\nimport type { SuiteDoc, SuiteTestCase, SuitePage } from './suite-format'\nimport type { QaCaseStatus, QaGrounding, QaPriority, QaScenarioPageRef } from './artifact-types'\n\n/**\n * A per-pattern scenario SEED: everything the build needs to assemble a\n * `QaScenario` EXCEPT the run-derived lanes and the colour.\n */\nexport interface ScenarioSeed {\n id: string\n external_key: string\n title: string\n source: 'reporter' | 'manual'\n /** Spec path that covers this case (for automation-lane lookup), if any. */\n spec_path?: string\n /** The spec's `test()` title used for the automation-lane lookup, if any. */\n test_title?: string\n group: { key: string; label: string }\n priority?: QaPriority\n caseStatus?: QaCaseStatus\n grounding?: QaGrounding\n /** This seed's pages: the current pattern primary + traversed refs. */\n pages: QaScenarioPageRef[]\n /** The pattern this seed is attached to (its primary page). */\n pattern: string\n /** `authored` | `generated` — origin of the donor suite (for dedup precedence). */\n origin: 'authored' | 'generated'\n /** Dedup key: `{spec_path}::{test_title}` when both are present, else undefined. */\n dedupKey?: string\n}\n\n/** Module suite, parsed into the few fields the projection needs. */\ninterface ModuleSuite {\n section: string\n pages: Record<string, SuitePage>\n pageSets: Record<string, string[]>\n labels: Record<string, string>\n cases: SuiteTestCase[]\n origin: 'authored' | 'generated'\n}\n\n/** Auto-discover module suites under `suitesDir/modules/<section>/`. */\nfunction discoverModuleSuites(suitesDir: string): string[] {\n const modulesDir = join(suitesDir, 'modules')\n if (!existsSync(modulesDir)) return []\n const out: string[] = []\n for (const name of readdirSync(modulesDir).sort()) {\n const sub = join(modulesDir, name)\n if (!statSync(sub).isDirectory()) continue\n const file = readdirSync(sub).find((f) => f.endsWith('-test-suite.yaml'))\n if (file) out.push(join(sub, file))\n }\n return out\n}\n\n/** Auto-discover generated suites under `suitesDir/generated/`. */\nfunction discoverGeneratedSuites(suitesDir: string): string[] {\n const genDir = join(suitesDir, 'generated')\n if (!existsSync(genDir)) return []\n return readdirSync(genDir)\n .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))\n .sort()\n .map((f) => join(genDir, f))\n}\n\n/** Auto-discover cross-module suites at the root of `suitesDir`. */\nfunction discoverCrossSuites(suitesDir: string): string[] {\n if (!existsSync(suitesDir)) return []\n return readdirSync(suitesDir)\n .filter((f) => f.endsWith('-test-suite.yaml'))\n .sort()\n .map((f) => join(suitesDir, f))\n}\n\n/** Parse `# ---- Suite X — label (detail) ---- ` headers → { X: 'label' }. */\nfunction parseSuiteLabels(rawText: string): Record<string, string> {\n const labels: Record<string, string> = {}\n const re = /^\\s*#\\s*-+\\s*Suite\\s+(\\S+)\\s*[—–-]\\s*(.+?)\\s*-*\\s*$/gm\n let m: RegExpExecArray | null\n while ((m = re.exec(rawText)) !== null) {\n labels[m[1]!] = m[2]!.replace(/\\s*\\([^)]*\\)\\s*$/, '').trim()\n }\n return labels\n}\n\n/** Resolve `applies_to` (page_set name | list of route keys) to a list of route keys. */\nfunction expandRoutes(value: string[] | string | undefined, pageSets: Record<string, string[]>): string[] {\n if (!value) return []\n if (typeof value === 'string') return pageSets[value] ?? [value]\n return value.flatMap((entry) => (pageSets[entry] ? pageSets[entry]! : [entry]))\n}\n\n/** A case is `covered` iff its declared status is `covered`. */\nfunction caseStatusOf(testCase: SuiteTestCase): QaCaseStatus {\n const s = testCase.status\n if (s === 'covered' || s === 'manual' || s === 'blocked' || s === 'open-question') return s\n // Generated cases default to `covered`; authored cases without status → manual.\n return 'manual'\n}\n\n/**\n * Load every discoverable suite into per-pattern scenario seeds.\n * @returns `{ byPattern, coveredPatterns }` — seeds keyed by primary pattern, and\n * the set of patterns that have at least one `covered` case.\n */\nexport function loadScenarioSeeds(suitesDir: string): {\n byPattern: Map<string, ScenarioSeed[]>\n coveredPatterns: Set<string>\n} {\n const modules: Record<string, ModuleSuite> = {}\n const byPattern = new Map<string, ScenarioSeed[]>()\n const coveredPatterns = new Set<string>()\n\n const push = (pattern: string, seed: ScenarioSeed): void => {\n if (!pattern) return\n const list = byPattern.get(pattern)\n if (list) list.push(seed)\n else byPattern.set(pattern, [seed])\n }\n\n // ── 1. Module suites (authored) + generated suites ─────────────────────────\n const moduleFiles: Array<{ path: string; origin: 'authored' | 'generated' }> = [\n ...discoverModuleSuites(suitesDir).map((path) => ({ path, origin: 'authored' as const })),\n ...discoverGeneratedSuites(suitesDir).map((path) => ({ path, origin: 'generated' as const })),\n ]\n\n for (const { path, origin } of moduleFiles) {\n const raw = readFileSync(path, 'utf8')\n const doc = parse(raw) as SuiteDoc | undefined\n if (!doc || !doc.meta) continue\n const section = doc.meta.section ?? path\n // A generated suite for an already-authored section MERGES into it (the\n // authored registry/labels win); cases accumulate so dedup can run later.\n const existing = modules[section]\n if (existing) {\n existing.cases.push(...(doc.test_cases ?? []))\n // Authored origin dominates if mixed.\n if (origin === 'authored') existing.origin = 'authored'\n } else {\n modules[section] = {\n section,\n pages: doc.pages ?? {},\n pageSets: doc.page_sets ?? {},\n labels: parseSuiteLabels(raw),\n cases: [...(doc.test_cases ?? [])],\n origin,\n }\n }\n // Merge page registry: a generated suite may register pages the authored one\n // lacks (e.g. /widgets/:widgetId/edit) — keep authored entries on conflict.\n if (existing) {\n for (const [key, page] of Object.entries(doc.pages ?? {})) {\n if (!(key in existing.pages)) existing.pages[key] = page\n }\n for (const [key, set] of Object.entries(doc.page_sets ?? {})) {\n if (!(key in existing.pageSets)) existing.pageSets[key] = set\n }\n }\n }\n\n // Track per-case origin so we can stamp seeds. A case's origin is the origin\n // of the suite it came from; but after the merge above we lost that. Re-derive\n // by re-reading: simpler — re-walk each file independently for seed emission.\n for (const { path, origin } of moduleFiles) {\n const raw = readFileSync(path, 'utf8')\n const doc = parse(raw) as SuiteDoc | undefined\n if (!doc || !doc.meta) continue\n const section = doc.meta.section ?? path\n const mod = modules[section]!\n const labels = mod.labels\n const localPages = { ...mod.pages, ...(doc.pages ?? {}) }\n const localPageSets = { ...mod.pageSets, ...(doc.page_sets ?? {}) }\n\n for (const testCase of doc.test_cases ?? []) {\n const routes = expandRoutes(testCase.applies_to, localPageSets)\n const traversed = (testCase.traverses ?? []).map((t) => t.pattern).filter(Boolean)\n const groupKey = testCase.suite ?? '?'\n const label = labels[groupKey] ?? `Suite ${groupKey}`\n const status = caseStatusOf(testCase)\n const specFallback = routes.length > 0 ? localPages[routes[0]!]?.spec : undefined\n const specPath = testCase.automation?.spec ?? specFallback\n const testTitle = testCase.automation?.test_title ?? localPages[routes[0]!]?.test_title\n const dedupKey = specPath && testTitle ? `${specPath}::${testTitle}` : undefined\n // An authored case linked to a Playwright test DISPLAYS the canonical test\n // title (the verbatim `test()` name) — it represents the SAME scenario as\n // its generated twin, which dedup will suppress. The rich human metadata\n // (priority, grounding, steps…) still comes from the authored case.\n const displayTitle =\n origin === 'authored' && testCase.automation?.type === 'playwright' && testCase.automation?.test_title\n ? testCase.automation.test_title\n : testCase.title\n\n for (const route of routes) {\n const page = localPages[route]\n const pattern = page?.pattern\n if (!pattern) continue\n if (status === 'covered') coveredPatterns.add(pattern)\n push(pattern, {\n id: `s-${section}-${testCase.id}-${route}`,\n external_key: testCase.external_key ?? `${section}#${testCase.id}`,\n title: displayTitle,\n source: testCase.automation?.type === 'playwright' ? 'reporter' : 'manual',\n ...(specPath ? { spec_path: specPath } : {}),\n ...(testTitle ? { test_title: testTitle } : {}),\n group: { key: groupKey, label },\n ...(testCase.priority ? { priority: testCase.priority as QaPriority } : {}),\n caseStatus: status,\n ...(testCase.grounding ? { grounding: testCase.grounding as QaGrounding } : {}),\n pages: [\n { pattern, role: 'primary' },\n ...traversed.map((p) => ({ pattern: p, role: 'traversed' as const })),\n ],\n pattern,\n origin,\n ...(dedupKey ? { dedupKey } : {}),\n })\n }\n }\n }\n\n // ── 2. Cross-module suites (TC-X… cases, attached to BOTH modules' pages) ───\n for (const crossPath of discoverCrossSuites(suitesDir)) {\n const raw = readFileSync(crossPath, 'utf8')\n const doc = parse(raw) as\n | (SuiteDoc & { meta?: { kind?: string; pair?: string[] }; test_cases?: CrossCase[] })\n | undefined\n if (!doc || doc.meta?.kind !== 'cross-module') continue\n const pair = doc.meta?.pair ?? []\n const crossLabel = pair.length ? `Inter-modules (${pair.join(' × ')})` : 'Inter-modules'\n for (const testCase of doc.test_cases ?? []) {\n const refs: QaScenarioPageRef[] = []\n for (const touch of testCase.touches ?? []) {\n const mod = modules[touch.module]\n if (!mod) continue\n const routes = touch.route ? [touch.route] : expandRoutes(touch.page_set, mod.pageSets)\n for (const route of routes) {\n const pattern = mod.pages[route]?.pattern\n if (pattern) refs.push({ pattern, role: touch.role === 'origin' ? 'primary' : 'traversed' })\n }\n }\n const patterns = [...new Set(refs.map((r) => r.pattern))]\n const status = caseStatusOf(testCase as unknown as SuiteTestCase)\n for (const pattern of patterns) {\n push(pattern, {\n id: `s-cross-${testCase.id}-${pattern}`,\n external_key: `cross#${testCase.id}`,\n title: testCase.title,\n source: 'manual',\n group: { key: 'X', label: crossLabel },\n ...(testCase.priority ? { priority: testCase.priority as QaPriority } : {}),\n caseStatus: status,\n ...(testCase.grounding ? { grounding: testCase.grounding as QaGrounding } : {}),\n pages: [{ pattern, role: 'primary' }, ...refs.filter((r) => r.pattern !== pattern)],\n pattern,\n origin: 'authored',\n })\n }\n }\n }\n\n return { byPattern, coveredPatterns }\n}\n\n/** A cross-module case (`touches[]` instead of `applies_to`). */\ninterface CrossCase {\n id: string\n title: string\n status?: string\n priority?: string\n grounding?: string\n touches?: Array<{ module: string; route?: string; page_set?: string; role?: string }>\n}\n"],"mappings":"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","names":["existsSync","readFileSync","readFileSync","existsSync","readFileSync"]}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
generateSuites,
|
|
4
|
+
serializeSuite
|
|
5
|
+
} from "./chunk-SSLQOK2Z.js";
|
|
6
|
+
import "./chunk-AZQSQJNQ.js";
|
|
7
|
+
import {
|
|
8
|
+
loadQaConfig
|
|
9
|
+
} from "./chunk-IHJPCMYF.js";
|
|
10
|
+
|
|
11
|
+
// src/qa/check.ts
|
|
12
|
+
import { execFileSync } from "child_process";
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
14
|
+
import { dirname, join, relative } from "path";
|
|
15
|
+
import { parse } from "yaml";
|
|
16
|
+
function parseSuiteDoc(path) {
|
|
17
|
+
try {
|
|
18
|
+
const doc = parse(readFileSync(path, "utf8"));
|
|
19
|
+
if (!doc || !doc.meta) return null;
|
|
20
|
+
return doc;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function loadSuiteDocs(suitesDir) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const modulesDir = join(suitesDir, "modules");
|
|
28
|
+
if (existsSync(modulesDir)) {
|
|
29
|
+
for (const name of readdirSync(modulesDir).sort()) {
|
|
30
|
+
const sub = join(modulesDir, name);
|
|
31
|
+
if (!statSync(sub).isDirectory()) continue;
|
|
32
|
+
const file = readdirSync(sub).find((f) => f.endsWith("-test-suite.yaml"));
|
|
33
|
+
if (!file) continue;
|
|
34
|
+
const doc = parseSuiteDoc(join(sub, file));
|
|
35
|
+
if (doc) out.push({ doc, origin: "authored", label: doc.meta.section ?? name });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const genDir = join(suitesDir, "generated");
|
|
39
|
+
if (existsSync(genDir)) {
|
|
40
|
+
for (const f of readdirSync(genDir).filter((x) => x.endsWith(".yaml") || x.endsWith(".yml")).sort()) {
|
|
41
|
+
const doc = parseSuiteDoc(join(genDir, f));
|
|
42
|
+
if (doc) out.push({ doc, origin: "generated", label: doc.meta.section ?? f });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function referencedPatterns(suite) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const page of Object.values(suite.doc.pages ?? {})) {
|
|
50
|
+
if (page?.pattern) out.push({ pattern: page.pattern, suite: suite.label });
|
|
51
|
+
}
|
|
52
|
+
for (const tc of suite.doc.test_cases ?? []) {
|
|
53
|
+
for (const t of tc.traverses ?? []) {
|
|
54
|
+
if (t?.pattern) out.push({ pattern: t.pattern, suite: suite.label });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function checkPatternsInSitemap(suites, sitemapPatterns) {
|
|
60
|
+
const findings = [];
|
|
61
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const s of suites) {
|
|
63
|
+
const loaded = "doc" in s ? s : { doc: s, origin: s.meta.origin ?? "authored", label: s.meta.section ?? "suite" };
|
|
64
|
+
for (const { pattern, suite } of referencedPatterns(loaded)) {
|
|
65
|
+
if (sitemapPatterns.has(pattern)) continue;
|
|
66
|
+
const dedup = `${suite}::${pattern}`;
|
|
67
|
+
if (seen.has(dedup)) continue;
|
|
68
|
+
seen.add(dedup);
|
|
69
|
+
findings.push({
|
|
70
|
+
rule: "pattern-not-in-sitemap",
|
|
71
|
+
level: "error",
|
|
72
|
+
message: `pattern ${pattern} (suite ${suite}) is not present in the sitemap`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return findings;
|
|
77
|
+
}
|
|
78
|
+
function loadSitemapPatterns(cfg) {
|
|
79
|
+
if (!cfg.sitemap || !cfg.sitemap.file || !existsSync(cfg.sitemap.file)) return null;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(readFileSync(cfg.sitemap.file, "utf8"));
|
|
82
|
+
const set = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const p of parsed.pages ?? []) {
|
|
84
|
+
if (typeof p.path_pattern === "string") set.add(p.path_pattern);
|
|
85
|
+
}
|
|
86
|
+
return set;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function checkGeneratedStale(cfg) {
|
|
92
|
+
const findings = [];
|
|
93
|
+
const genDir = join(cfg.suitesDir, "generated");
|
|
94
|
+
const { suites } = generateSuites(cfg);
|
|
95
|
+
const expectedFiles = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const [section, doc] of suites) {
|
|
97
|
+
expectedFiles.set(`${section}-test-suite.yaml`, serializeSuite(doc));
|
|
98
|
+
}
|
|
99
|
+
if (!existsSync(genDir)) {
|
|
100
|
+
if (expectedFiles.size > 0) {
|
|
101
|
+
findings.push({
|
|
102
|
+
rule: "generated-stale",
|
|
103
|
+
level: "error",
|
|
104
|
+
message: "generated suites out of date \u2014 `generated/` is missing; run `mhosaic-feedback qa generate`"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
}
|
|
109
|
+
for (const [name, content] of expectedFiles) {
|
|
110
|
+
const path = join(genDir, name);
|
|
111
|
+
if (!existsSync(path)) {
|
|
112
|
+
findings.push({
|
|
113
|
+
rule: "generated-stale",
|
|
114
|
+
level: "error",
|
|
115
|
+
message: `generated suites out of date \u2014 ${name} is missing; run \`mhosaic-feedback qa generate\``
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (readFileSync(path, "utf8") !== content) {
|
|
120
|
+
findings.push({
|
|
121
|
+
rule: "generated-stale",
|
|
122
|
+
level: "error",
|
|
123
|
+
message: `generated suites out of date \u2014 ${name} differs; run \`mhosaic-feedback qa generate\``
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const onDisk = readdirSync(genDir).filter((f) => f.endsWith("-test-suite.yaml"));
|
|
128
|
+
for (const name of onDisk) {
|
|
129
|
+
if (!expectedFiles.has(name)) {
|
|
130
|
+
findings.push({
|
|
131
|
+
rule: "generated-stale",
|
|
132
|
+
level: "error",
|
|
133
|
+
message: `generated suites out of date \u2014 ${name} is no longer generated; run \`mhosaic-feedback qa generate\``
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return findings;
|
|
138
|
+
}
|
|
139
|
+
function checkDedupCollisions(suites) {
|
|
140
|
+
const findings = [];
|
|
141
|
+
const seen = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const s of suites) {
|
|
143
|
+
const loaded = "doc" in s ? s : { doc: s, origin: s.meta.origin ?? "authored", label: s.meta.section ?? "suite" };
|
|
144
|
+
if (loaded.origin !== "authored") continue;
|
|
145
|
+
const pages = loaded.doc.pages ?? {};
|
|
146
|
+
for (const tc of loaded.doc.test_cases ?? []) {
|
|
147
|
+
const firstRoute = tc.applies_to?.[0];
|
|
148
|
+
const spec = tc.automation?.spec ?? (firstRoute ? pages[firstRoute]?.spec : void 0);
|
|
149
|
+
const title = tc.automation?.test_title;
|
|
150
|
+
if (!spec || !title) continue;
|
|
151
|
+
const key = `${spec}::${title}`;
|
|
152
|
+
const here = `${loaded.label}/${tc.id}`;
|
|
153
|
+
const prior = seen.get(key);
|
|
154
|
+
if (prior) {
|
|
155
|
+
findings.push({
|
|
156
|
+
rule: "dedup-collision",
|
|
157
|
+
level: "error",
|
|
158
|
+
message: `duplicate authored dedup key ${key} \u2014 cases ${prior} and ${here} collide`
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
seen.set(key, here);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
function entryIdentity(entry) {
|
|
168
|
+
if (entry && typeof entry === "object") {
|
|
169
|
+
const o = entry;
|
|
170
|
+
for (const k of ["external_key", "externalKey", "id", "key"]) {
|
|
171
|
+
if (typeof o[k] === "string") return `${k}:${o[k]}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return JSON.stringify(entry);
|
|
175
|
+
}
|
|
176
|
+
function checkDevValidationAppendOnly(cfg) {
|
|
177
|
+
const file = cfg.devValidationsFile;
|
|
178
|
+
if (!file || !existsSync(file)) {
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
rule: "dev-validation-append-only",
|
|
182
|
+
level: "warning",
|
|
183
|
+
message: "skipped \u2014 no dev-validations file configured/present"
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
let repoRoot;
|
|
188
|
+
try {
|
|
189
|
+
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
190
|
+
cwd: dirname(file),
|
|
191
|
+
encoding: "utf8"
|
|
192
|
+
}).trim();
|
|
193
|
+
} catch {
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
rule: "dev-validation-append-only",
|
|
197
|
+
level: "warning",
|
|
198
|
+
message: "skipped \u2014 not a git repository"
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
const rel = relative(repoRoot, file).split("\\").join("/");
|
|
203
|
+
let headText;
|
|
204
|
+
try {
|
|
205
|
+
headText = execFileSync("git", ["show", `HEAD:${rel}`], { cwd: repoRoot, encoding: "utf8" });
|
|
206
|
+
} catch {
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
rule: "dev-validation-append-only",
|
|
210
|
+
level: "warning",
|
|
211
|
+
message: "skipped \u2014 dev-validations file is new (not at HEAD)"
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
let headArr;
|
|
216
|
+
let workArr;
|
|
217
|
+
try {
|
|
218
|
+
headArr = JSON.parse(headText);
|
|
219
|
+
workArr = JSON.parse(readFileSync(file, "utf8"));
|
|
220
|
+
} catch {
|
|
221
|
+
return [
|
|
222
|
+
{
|
|
223
|
+
rule: "dev-validation-append-only",
|
|
224
|
+
level: "warning",
|
|
225
|
+
message: "skipped \u2014 dev-validations file is not valid JSON at HEAD or in working copy"
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
if (!Array.isArray(headArr) || !Array.isArray(workArr)) {
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
rule: "dev-validation-append-only",
|
|
233
|
+
level: "warning",
|
|
234
|
+
message: "skipped \u2014 dev-validations file is not a JSON array"
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
const work = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const e of workArr) work.set(entryIdentity(e), JSON.stringify(e));
|
|
240
|
+
const findings = [];
|
|
241
|
+
for (const headEntry of headArr) {
|
|
242
|
+
const id = entryIdentity(headEntry);
|
|
243
|
+
const headSer = JSON.stringify(headEntry);
|
|
244
|
+
if (!work.has(id)) {
|
|
245
|
+
findings.push({
|
|
246
|
+
rule: "dev-validation-append-only",
|
|
247
|
+
level: "error",
|
|
248
|
+
message: `dev-validation entry removed (not append-only): ${id}`
|
|
249
|
+
});
|
|
250
|
+
} else if (work.get(id) !== headSer) {
|
|
251
|
+
findings.push({
|
|
252
|
+
rule: "dev-validation-append-only",
|
|
253
|
+
level: "error",
|
|
254
|
+
message: `dev-validation entry mutated (not append-only): ${id}`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return findings;
|
|
259
|
+
}
|
|
260
|
+
function isInformational(f) {
|
|
261
|
+
return f.level === "warning" && f.message.startsWith("skipped");
|
|
262
|
+
}
|
|
263
|
+
function runChecks(cfg, opts = {}) {
|
|
264
|
+
const findings = [];
|
|
265
|
+
const warnLevel = opts.strict ? "error" : "warning";
|
|
266
|
+
const suites = loadSuiteDocs(cfg.suitesDir);
|
|
267
|
+
const sitemapPatterns = loadSitemapPatterns(cfg);
|
|
268
|
+
if (sitemapPatterns === null) {
|
|
269
|
+
findings.push({
|
|
270
|
+
rule: "pattern-not-in-sitemap",
|
|
271
|
+
level: "warning",
|
|
272
|
+
message: "skipped \u2014 no sitemap configured/present (meter runs in degraded flat mode)"
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
findings.push(...checkPatternsInSitemap(suites, sitemapPatterns));
|
|
276
|
+
}
|
|
277
|
+
findings.push(...checkGeneratedStale(cfg));
|
|
278
|
+
const { unmappable, zeroCaseSpecs, dynamicTitles } = generateSuites(cfg);
|
|
279
|
+
for (const entry of unmappable) {
|
|
280
|
+
findings.push({ rule: "unmappable-test", level: warnLevel, message: `unmappable \u2014 ${entry}` });
|
|
281
|
+
}
|
|
282
|
+
for (const spec of zeroCaseSpecs) {
|
|
283
|
+
findings.push({ rule: "zero-case-spec", level: warnLevel, message: `zero-case spec \u2014 ${spec}` });
|
|
284
|
+
}
|
|
285
|
+
for (const entry of dynamicTitles) {
|
|
286
|
+
findings.push({ rule: "dynamic-title-spec", level: warnLevel, message: `dynamic-title \u2014 ${entry}` });
|
|
287
|
+
}
|
|
288
|
+
findings.push(...checkDevValidationAppendOnly(cfg));
|
|
289
|
+
findings.push(...checkDedupCollisions(suites));
|
|
290
|
+
return findings;
|
|
291
|
+
}
|
|
292
|
+
async function colors() {
|
|
293
|
+
const k = (await import("kleur")).default;
|
|
294
|
+
return {
|
|
295
|
+
red: (s) => k.red(s),
|
|
296
|
+
yellow: (s) => k.yellow(s),
|
|
297
|
+
green: (s) => k.green(s),
|
|
298
|
+
gray: (s) => k.gray(s),
|
|
299
|
+
bold: (s) => k.bold(s)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function runCheck(args) {
|
|
303
|
+
const strict = args.includes("--strict");
|
|
304
|
+
const cfg = loadQaConfig(process.cwd());
|
|
305
|
+
const findings = runChecks(cfg, { strict });
|
|
306
|
+
const c = await colors();
|
|
307
|
+
if (findings.length === 0) {
|
|
308
|
+
process.stdout.write(`${c.green("\u2713")} qa check: all checks passed
|
|
309
|
+
`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
313
|
+
for (const f of findings) {
|
|
314
|
+
const list = byRule.get(f.rule);
|
|
315
|
+
if (list) list.push(f);
|
|
316
|
+
else byRule.set(f.rule, [f]);
|
|
317
|
+
}
|
|
318
|
+
let hasError = false;
|
|
319
|
+
let hasActionableWarning = false;
|
|
320
|
+
for (const rule of [...byRule.keys()].sort((a, b) => a.localeCompare(b))) {
|
|
321
|
+
process.stdout.write(`${c.bold(rule)}
|
|
322
|
+
`);
|
|
323
|
+
for (const f of byRule.get(rule)) {
|
|
324
|
+
if (f.level === "error") hasError = true;
|
|
325
|
+
else if (!isInformational(f)) hasActionableWarning = true;
|
|
326
|
+
const icon = f.level === "error" ? c.red("\u2717") : c.yellow("!");
|
|
327
|
+
process.stdout.write(` ${icon} ${f.message}
|
|
328
|
+
`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (hasError || strict && hasActionableWarning) {
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
export {
|
|
336
|
+
checkDedupCollisions,
|
|
337
|
+
checkPatternsInSitemap,
|
|
338
|
+
isInformational,
|
|
339
|
+
runCheck,
|
|
340
|
+
runChecks
|
|
341
|
+
};
|
|
342
|
+
//# sourceMappingURL=check-FOJPEE4Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/qa/check.ts"],"sourcesContent":["/**\n * `qa check` — the CI tripwire for the QA-Meter inputs (design spec §7.4).\n *\n * Validates that the hand-authored suites, the generated suites, the frozen\n * sitemap and the dev-validations ledger are mutually CONSISTENT and have not\n * DRIFTED. Five rules (§7.4):\n *\n * 1. pattern-not-in-sitemap — every referenced page pattern exists in the sitemap.\n * 2. generated-stale — on-disk generated suites match a fresh in-memory run.\n * 3. unmappable-test (+ zero-case / dynamic-title) — nothing vanishes silently.\n * 4. dev-validation-append-only — the dev-validations ledger is append-only vs HEAD.\n * 5. dedup-collision — no two AUTHORED cases share a dedup key.\n *\n * Pure rule helpers (`checkPatternsInSitemap`, `checkDedupCollisions`, …) are\n * exported so they can be unit-tested with synthetic inputs.\n */\nimport { execFileSync } from 'node:child_process'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { dirname, join, relative } from 'node:path'\nimport { parse } from 'yaml'\n\nimport { loadQaConfig, type QaConfig } from './config'\nimport { generateSuites, serializeSuite } from './generate'\nimport type { SuiteDoc } from './suite-format'\n\n/** A single check outcome. */\nexport interface Finding {\n rule: string\n level: 'error' | 'warning'\n message: string\n}\n\n/** A suite document paired with its on-disk origin. */\ninterface LoadedSuite {\n doc: SuiteDoc\n origin: 'authored' | 'generated'\n /** Human label for messages (the file or section). */\n label: string\n}\n\n// ───────────────────────────────────────────── suite discovery (read-only) ──\n\n/** Read + parse a `*-test-suite.yaml` into a `SuiteDoc`, or null if unusable. */\nfunction parseSuiteDoc(path: string): SuiteDoc | null {\n try {\n const doc = parse(readFileSync(path, 'utf8')) as SuiteDoc | undefined\n if (!doc || !doc.meta) return null\n return doc\n } catch {\n return null\n }\n}\n\n/**\n * Load every authored module suite (`suitesDir/modules/<section>/`) and every\n * generated suite (`suitesDir/generated/`). Mirrors `load-suites` discovery but\n * keeps the raw `SuiteDoc` + origin (the check needs page registries and\n * per-case automation keys, which the seed projection flattens away).\n */\nfunction loadSuiteDocs(suitesDir: string): LoadedSuite[] {\n const out: LoadedSuite[] = []\n\n const modulesDir = join(suitesDir, 'modules')\n if (existsSync(modulesDir)) {\n for (const name of readdirSync(modulesDir).sort()) {\n const sub = join(modulesDir, name)\n if (!statSync(sub).isDirectory()) continue\n const file = readdirSync(sub).find((f) => f.endsWith('-test-suite.yaml'))\n if (!file) continue\n const doc = parseSuiteDoc(join(sub, file))\n if (doc) out.push({ doc, origin: 'authored', label: doc.meta.section ?? name })\n }\n }\n\n const genDir = join(suitesDir, 'generated')\n if (existsSync(genDir)) {\n for (const f of readdirSync(genDir).filter((x) => x.endsWith('.yaml') || x.endsWith('.yml')).sort()) {\n const doc = parseSuiteDoc(join(genDir, f))\n if (doc) out.push({ doc, origin: 'generated', label: doc.meta.section ?? f })\n }\n }\n\n // Cross-module suites at the root reference module pages, not their own — they\n // don't introduce new patterns to validate here, so they are skipped.\n return out\n}\n\n/** Every (pattern, suiteLabel) pair referenced by a suite's pages registry. */\nfunction referencedPatterns(suite: LoadedSuite): Array<{ pattern: string; suite: string }> {\n const out: Array<{ pattern: string; suite: string }> = []\n for (const page of Object.values(suite.doc.pages ?? {})) {\n if (page?.pattern) out.push({ pattern: page.pattern, suite: suite.label })\n }\n for (const tc of suite.doc.test_cases ?? []) {\n for (const t of tc.traverses ?? []) {\n if (t?.pattern) out.push({ pattern: t.pattern, suite: suite.label })\n }\n }\n return out\n}\n\n// ─────────────────────────────────────────────────── rule 1: sitemap ────────\n\n/**\n * RULE 1 — every page pattern referenced by a loaded suite must be a known\n * `path_pattern` in the sitemap. Exported for unit testing with synthetic docs.\n */\nexport function checkPatternsInSitemap(\n suites: Array<SuiteDoc | LoadedSuite>,\n sitemapPatterns: Set<string>,\n): Finding[] {\n const findings: Finding[] = []\n const seen = new Set<string>()\n for (const s of suites) {\n const loaded: LoadedSuite =\n 'doc' in s ? s : { doc: s, origin: s.meta.origin ?? 'authored', label: s.meta.section ?? 'suite' }\n for (const { pattern, suite } of referencedPatterns(loaded)) {\n if (sitemapPatterns.has(pattern)) continue\n const dedup = `${suite}::${pattern}`\n if (seen.has(dedup)) continue\n seen.add(dedup)\n findings.push({\n rule: 'pattern-not-in-sitemap',\n level: 'error',\n message: `pattern ${pattern} (suite ${suite}) is not present in the sitemap`,\n })\n }\n }\n return findings\n}\n\n/** Load the configured sitemap's `path_pattern` set, or null when absent. */\nfunction loadSitemapPatterns(cfg: QaConfig): Set<string> | null {\n if (!cfg.sitemap || !cfg.sitemap.file || !existsSync(cfg.sitemap.file)) return null\n try {\n const parsed = JSON.parse(readFileSync(cfg.sitemap.file, 'utf8')) as {\n pages?: Array<{ path_pattern?: string }>\n }\n const set = new Set<string>()\n for (const p of parsed.pages ?? []) {\n if (typeof p.path_pattern === 'string') set.add(p.path_pattern)\n }\n return set\n } catch {\n return null\n }\n}\n\n// ─────────────────────────────────────────────── rule 2: generated-stale ────\n\n/**\n * RULE 2 — re-generate suites in memory and compare each against the on-disk\n * `generated/<section>-test-suite.yaml`. Any difference, missing or extra file\n * means the committed generated suites are stale.\n */\nfunction checkGeneratedStale(cfg: QaConfig): Finding[] {\n const findings: Finding[] = []\n const genDir = join(cfg.suitesDir, 'generated')\n const { suites } = generateSuites(cfg)\n\n const expectedFiles = new Map<string, string>()\n for (const [section, doc] of suites) {\n expectedFiles.set(`${section}-test-suite.yaml`, serializeSuite(doc))\n }\n\n if (!existsSync(genDir)) {\n if (expectedFiles.size > 0) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message:\n 'generated suites out of date — `generated/` is missing; run `mhosaic-feedback qa generate`',\n })\n }\n return findings\n }\n\n // Compare expected files against disk.\n for (const [name, content] of expectedFiles) {\n const path = join(genDir, name)\n if (!existsSync(path)) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message: `generated suites out of date — ${name} is missing; run \\`mhosaic-feedback qa generate\\``,\n })\n continue\n }\n if (readFileSync(path, 'utf8') !== content) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message: `generated suites out of date — ${name} differs; run \\`mhosaic-feedback qa generate\\``,\n })\n }\n }\n\n // Extra generated files that the generator no longer produces are also stale.\n const onDisk = readdirSync(genDir).filter((f) => f.endsWith('-test-suite.yaml'))\n for (const name of onDisk) {\n if (!expectedFiles.has(name)) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message: `generated suites out of date — ${name} is no longer generated; run \\`mhosaic-feedback qa generate\\``,\n })\n }\n }\n\n return findings\n}\n\n// ─────────────────────────────────────────────── rule 5: dedup-collision ────\n\n/**\n * RULE 5 — two AUTHORED cases that resolve to the SAME dedup key\n * `(automation.spec ?? pages[firstRoute].spec) :: automation.test_title` would\n * make dedup ambiguous. Generated-vs-authored matches are NORMAL dedup (not a\n * collision) — only authored-vs-authored same-key is flagged. Exported for unit\n * testing.\n */\nexport function checkDedupCollisions(suites: Array<SuiteDoc | LoadedSuite>): Finding[] {\n const findings: Finding[] = []\n const seen = new Map<string, string>() // key → first case description\n\n for (const s of suites) {\n const loaded: LoadedSuite =\n 'doc' in s ? s : { doc: s, origin: s.meta.origin ?? 'authored', label: s.meta.section ?? 'suite' }\n if (loaded.origin !== 'authored') continue\n\n const pages = loaded.doc.pages ?? {}\n for (const tc of loaded.doc.test_cases ?? []) {\n const firstRoute = tc.applies_to?.[0]\n const spec = tc.automation?.spec ?? (firstRoute ? pages[firstRoute]?.spec : undefined)\n const title = tc.automation?.test_title\n if (!spec || !title) continue // no dedup key → cannot collide\n const key = `${spec}::${title}`\n const here = `${loaded.label}/${tc.id}`\n const prior = seen.get(key)\n if (prior) {\n findings.push({\n rule: 'dedup-collision',\n level: 'error',\n message: `duplicate authored dedup key ${key} — cases ${prior} and ${here} collide`,\n })\n } else {\n seen.set(key, here)\n }\n }\n }\n return findings\n}\n\n// ─────────────────────────────────────── rule 4: dev-validation append-only ─\n\n/** Identity of a dev-validation entry (best-effort): its `external_key`/`id`, else JSON. */\nfunction entryIdentity(entry: unknown): string {\n if (entry && typeof entry === 'object') {\n const o = entry as Record<string, unknown>\n for (const k of ['external_key', 'externalKey', 'id', 'key']) {\n if (typeof o[k] === 'string') return `${k}:${o[k]}`\n }\n }\n return JSON.stringify(entry)\n}\n\n/**\n * RULE 4 — the dev-validations file (a JSON array) must be append-only relative\n * to HEAD: every entry present at HEAD must still be present AND unchanged in the\n * working copy. SKIPS (no error) when the file or git is absent, or the file is\n * newly added (not at HEAD).\n */\nfunction checkDevValidationAppendOnly(cfg: QaConfig): Finding[] {\n const file = cfg.devValidationsFile\n if (!file || !existsSync(file)) {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — no dev-validations file configured/present',\n },\n ]\n }\n\n // Locate the repo root (git toplevel) for `git show HEAD:<rel>`.\n let repoRoot: string\n try {\n repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {\n cwd: dirname(file),\n encoding: 'utf8',\n }).trim()\n } catch {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — not a git repository',\n },\n ]\n }\n\n const rel = relative(repoRoot, file).split('\\\\').join('/')\n\n let headText: string\n try {\n headText = execFileSync('git', ['show', `HEAD:${rel}`], { cwd: repoRoot, encoding: 'utf8' })\n } catch {\n // File not at HEAD = newly added = fine (append-only trivially holds).\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — dev-validations file is new (not at HEAD)',\n },\n ]\n }\n\n let headArr: unknown[]\n let workArr: unknown[]\n try {\n headArr = JSON.parse(headText)\n workArr = JSON.parse(readFileSync(file, 'utf8'))\n } catch {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — dev-validations file is not valid JSON at HEAD or in working copy',\n },\n ]\n }\n if (!Array.isArray(headArr) || !Array.isArray(workArr)) {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — dev-validations file is not a JSON array',\n },\n ]\n }\n\n // Build an identity → serialized-entry map of the working copy. Every HEAD\n // entry must still be present (by identity) AND byte-identical (no mutation).\n const work = new Map<string, string>()\n for (const e of workArr) work.set(entryIdentity(e), JSON.stringify(e))\n\n const findings: Finding[] = []\n for (const headEntry of headArr) {\n const id = entryIdentity(headEntry)\n const headSer = JSON.stringify(headEntry)\n if (!work.has(id)) {\n findings.push({\n rule: 'dev-validation-append-only',\n level: 'error',\n message: `dev-validation entry removed (not append-only): ${id}`,\n })\n } else if (work.get(id) !== headSer) {\n findings.push({\n rule: 'dev-validation-append-only',\n level: 'error',\n message: `dev-validation entry mutated (not append-only): ${id}`,\n })\n }\n }\n return findings\n}\n\n// ──────────────────────────────────────────────────── orchestration ─────────\n\n/**\n * A finding is INFORMATIONAL (a SKIP note, not a real warning) when a rule could\n * not run — e.g. no sitemap configured, no dev-validations ledger. These must\n * never fail the build, even under `--strict`.\n */\nexport function isInformational(f: Finding): boolean {\n return f.level === 'warning' && f.message.startsWith('skipped')\n}\n\n/** Run all five §7.4 rules and return every finding. */\nexport function runChecks(cfg: QaConfig, opts: { strict?: boolean } = {}): Finding[] {\n const findings: Finding[] = []\n const warnLevel: Finding['level'] = opts.strict ? 'error' : 'warning'\n\n const suites = loadSuiteDocs(cfg.suitesDir)\n\n // Rule 1 — pattern-not-in-sitemap.\n const sitemapPatterns = loadSitemapPatterns(cfg)\n if (sitemapPatterns === null) {\n findings.push({\n rule: 'pattern-not-in-sitemap',\n level: 'warning',\n message: 'skipped — no sitemap configured/present (meter runs in degraded flat mode)',\n })\n } else {\n findings.push(...checkPatternsInSitemap(suites, sitemapPatterns))\n }\n\n // Rule 2 — generated-stale.\n findings.push(...checkGeneratedStale(cfg))\n\n // Rule 3 — unmappable / zero-case / dynamic-title (warning, or error under --strict).\n const { unmappable, zeroCaseSpecs, dynamicTitles } = generateSuites(cfg)\n for (const entry of unmappable) {\n findings.push({ rule: 'unmappable-test', level: warnLevel, message: `unmappable — ${entry}` })\n }\n for (const spec of zeroCaseSpecs) {\n findings.push({ rule: 'zero-case-spec', level: warnLevel, message: `zero-case spec — ${spec}` })\n }\n for (const entry of dynamicTitles) {\n findings.push({ rule: 'dynamic-title-spec', level: warnLevel, message: `dynamic-title — ${entry}` })\n }\n\n // Rule 4 — dev-validation append-only.\n findings.push(...checkDevValidationAppendOnly(cfg))\n\n // Rule 5 — dedup-collision.\n findings.push(...checkDedupCollisions(suites))\n\n return findings\n}\n\n// ─────────────────────────────────────────────────────── CLI entry ──────────\n\n/** Lazily load `kleur` for colored output (mirrors `commands/doctor.ts`). */\nasync function colors(): Promise<{\n red: (s: string) => string\n yellow: (s: string) => string\n green: (s: string) => string\n gray: (s: string) => string\n bold: (s: string) => string\n}> {\n const k = (await import('kleur')).default\n return {\n red: (s) => k.red(s),\n yellow: (s) => k.yellow(s),\n green: (s) => k.green(s),\n gray: (s) => k.gray(s),\n bold: (s) => k.bold(s),\n }\n}\n\nexport async function runCheck(args: string[]): Promise<void> {\n const strict = args.includes('--strict')\n const cfg = loadQaConfig(process.cwd())\n const findings = runChecks(cfg, { strict })\n const c = await colors()\n\n if (findings.length === 0) {\n process.stdout.write(`${c.green('✓')} qa check: all checks passed\\n`)\n return\n }\n\n // Group by rule for a clean, scannable report.\n const byRule = new Map<string, Finding[]>()\n for (const f of findings) {\n const list = byRule.get(f.rule)\n if (list) list.push(f)\n else byRule.set(f.rule, [f])\n }\n\n let hasError = false\n let hasActionableWarning = false\n for (const rule of [...byRule.keys()].sort((a, b) => a.localeCompare(b))) {\n process.stdout.write(`${c.bold(rule)}\\n`)\n for (const f of byRule.get(rule)!) {\n if (f.level === 'error') hasError = true\n // Informational skip notes (no sitemap, no ledger, …) never fail the build.\n else if (!isInformational(f)) hasActionableWarning = true\n const icon = f.level === 'error' ? c.red('✗') : c.yellow('!')\n process.stdout.write(` ${icon} ${f.message}\\n`)\n }\n }\n\n // Exit 1 on any error, or any actionable warning under --strict.\n if (hasError || (strict && hasActionableWarning)) {\n process.exitCode = 1\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/qa/sitemap-core.ts
|
|
4
|
+
function normalizePattern(pattern) {
|
|
5
|
+
if (!pattern) {
|
|
6
|
+
return "/";
|
|
7
|
+
}
|
|
8
|
+
let normalized = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
9
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
10
|
+
if (normalized.length > 1) {
|
|
11
|
+
normalized = normalized.replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
return normalized.length > 0 ? normalized : "/";
|
|
14
|
+
}
|
|
15
|
+
function joinPattern(parentAbs, childPath) {
|
|
16
|
+
if (!childPath) {
|
|
17
|
+
return parentAbs || "/";
|
|
18
|
+
}
|
|
19
|
+
if (childPath.startsWith("/")) {
|
|
20
|
+
return normalizePattern(childPath);
|
|
21
|
+
}
|
|
22
|
+
const base = parentAbs === "/" ? "" : parentAbs;
|
|
23
|
+
return normalizePattern(`${base}/${childPath}`);
|
|
24
|
+
}
|
|
25
|
+
function isCatchAll(path) {
|
|
26
|
+
return !!path && /\(\.\*\)/.test(path);
|
|
27
|
+
}
|
|
28
|
+
function buildSitemap(routes, options = {}) {
|
|
29
|
+
const titleFor = options.titleFor ?? (() => void 0);
|
|
30
|
+
const pages = [];
|
|
31
|
+
const byPattern = /* @__PURE__ */ new Map();
|
|
32
|
+
const visit = (records, parentAbs, parentEmitted) => {
|
|
33
|
+
records.forEach((record, index) => {
|
|
34
|
+
const hasChildren = Array.isArray(record.children) && record.children.length > 0;
|
|
35
|
+
const isRedirect = record.redirect != null;
|
|
36
|
+
if (isRedirect && !hasChildren) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (isCatchAll(record.path)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const abs = joinPattern(parentAbs, record.path ?? "");
|
|
43
|
+
const routeName = typeof record.name === "string" ? record.name : void 0;
|
|
44
|
+
const existing = byPattern.get(abs);
|
|
45
|
+
if (existing) {
|
|
46
|
+
if (routeName && !existing.route_name) {
|
|
47
|
+
existing.route_name = routeName;
|
|
48
|
+
if (!existing.title) {
|
|
49
|
+
const mergedTitle = titleFor(routeName, abs);
|
|
50
|
+
if (mergedTitle) {
|
|
51
|
+
existing.title = mergedTitle;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
delete existing.structural;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const title = titleFor(routeName, abs) ?? void 0;
|
|
58
|
+
const page = { path_pattern: abs, parent: parentEmitted, order: index };
|
|
59
|
+
if (routeName) {
|
|
60
|
+
page.route_name = routeName;
|
|
61
|
+
} else {
|
|
62
|
+
page.structural = true;
|
|
63
|
+
}
|
|
64
|
+
if (title) {
|
|
65
|
+
page.title = title;
|
|
66
|
+
}
|
|
67
|
+
byPattern.set(abs, page);
|
|
68
|
+
pages.push(page);
|
|
69
|
+
}
|
|
70
|
+
if (hasChildren) {
|
|
71
|
+
visit(record.children, abs, abs);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
visit(routes, "", null);
|
|
76
|
+
return { format: 1, pages };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
normalizePattern,
|
|
81
|
+
buildSitemap
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=chunk-AZQSQJNQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/qa/sitemap-core.ts"],"sourcesContent":["/**\n * Logique pure de construction du sitemap (Track A3).\n *\n * Transforme l'arbre de routes Vue (`router.options.routes` / la constante\n * `routes`) en le payload de sync doc 03 §3.1 — agnostique de la manière dont\n * les routes ont été chargées (Vite runtime, ou lien statique des modules).\n * Aucune dépendance runtime : se teste à la table (vitest) et se réutilise tel\n * quel quand l'export alimentera le CLI plateforme `mhosaic-feedback sitemap sync`.\n */\n\n/** Forme minimale d'un `RouteRecordRaw` dont on a besoin (structurellement compatible). */\nexport interface RawRoute {\n path?: string;\n name?: string | symbol;\n redirect?: unknown;\n children?: ReadonlyArray<RawRoute>;\n}\n\n/** Un nœud de page émis (doc 03 §3.1). */\nexport interface SitemapPage {\n path_pattern: string;\n route_name?: string;\n title?: string;\n parent: string | null;\n order: number;\n structural?: boolean;\n}\n\nexport interface SitemapPayload {\n format: 1;\n pages: SitemapPage[];\n}\n\n/** Normalisation canonique : `/` initial, pas de `/` final (sauf racine), slashes dédoublés. */\nexport function normalizePattern(pattern: string | undefined | null): string {\n if (!pattern) {\n return '/';\n }\n let normalized = pattern.startsWith('/') ? pattern : `/${pattern}`;\n normalized = normalized.replace(/\\/{2,}/g, '/');\n if (normalized.length > 1) {\n normalized = normalized.replace(/\\/+$/, '');\n }\n return normalized.length > 0 ? normalized : '/';\n}\n\n/** Joint un pattern parent absolu et un segment enfant (le segment peut être absolu). */\nfunction joinPattern(parentAbs: string, childPath: string): string {\n if (!childPath) {\n return parentAbs || '/';\n }\n if (childPath.startsWith('/')) {\n return normalizePattern(childPath);\n }\n const base = parentAbs === '/' ? '' : parentAbs;\n return normalizePattern(`${base}/${childPath}`);\n}\n\n/** Une route catch-all (`/:pathMatch(.*)*`) n'est pas une page. */\nfunction isCatchAll(path: string | undefined): boolean {\n return !!path && /\\(\\.\\*\\)/.test(path);\n}\n\nexport interface BuildOptions {\n /** Résout un titre humain depuis le nom de route et/ou le pattern. */\n titleFor?: (routeName: string | undefined, pattern: string) => string | undefined;\n}\n\n/**\n * Construit le payload de sitemap.\n *\n * Règles (doc 03 §3.1) :\n * - records redirect-only (redirect sans enfants) → sautés (pur alias) ;\n * - catch-all → sauté ;\n * - segments intermédiaires sans `name` → émis comme nœuds `structural` (arbre sans orphelins) ;\n * - un enfant à `path: ''` partage le pattern du parent : on FUSIONNE (le nom/titre concret\n * l'emporte sur le wrapper structurel — c'est le patron Vue « segment + enfant nommé sans path »).\n */\nexport function buildSitemap(routes: ReadonlyArray<RawRoute>, options: BuildOptions = {}): SitemapPayload {\n const titleFor = options.titleFor ?? (() => undefined);\n const pages: SitemapPage[] = [];\n const byPattern = new Map<string, SitemapPage>();\n\n const visit = (records: ReadonlyArray<RawRoute>, parentAbs: string, parentEmitted: string | null): void => {\n records.forEach((record, index) => {\n const hasChildren = Array.isArray(record.children) && record.children.length > 0;\n const isRedirect = record.redirect != null;\n if (isRedirect && !hasChildren) {\n return; // alias de navigation pur\n }\n if (isCatchAll(record.path)) {\n return;\n }\n\n const abs = joinPattern(parentAbs, record.path ?? '');\n const routeName = typeof record.name === 'string' ? record.name : undefined;\n\n const existing = byPattern.get(abs);\n if (existing) {\n // Fusion : le nœud nommé concret écrase le wrapper structurel au même pattern.\n if (routeName && !existing.route_name) {\n existing.route_name = routeName;\n if (!existing.title) {\n const mergedTitle = titleFor(routeName, abs);\n if (mergedTitle) {\n existing.title = mergedTitle;\n }\n }\n delete existing.structural;\n }\n } else {\n const title = titleFor(routeName, abs) ?? undefined;\n const page: SitemapPage = { path_pattern: abs, parent: parentEmitted, order: index };\n if (routeName) {\n page.route_name = routeName;\n } else {\n page.structural = true;\n }\n if (title) {\n page.title = title;\n }\n byPattern.set(abs, page);\n pages.push(page);\n }\n\n if (hasChildren) {\n visit(record.children as ReadonlyArray<RawRoute>, abs, abs);\n }\n });\n };\n\n visit(routes, '', null);\n return { format: 1, pages };\n}\n\n/** Forme minimale d'un item de menu (`src/menu.ts`). */\nexport interface MenuLike {\n label?: string;\n to?: string | { name?: string };\n subItems?: ReadonlyArray<MenuLike>;\n megaMenu?: ReadonlyArray<MenuLike>;\n}\n\nfunction resolveMessage(messages: unknown, dottedKey: string): string | undefined {\n let current: unknown = messages;\n for (const part of dottedKey.split('.')) {\n if (current == null || typeof current !== 'object' || !(part in (current as Record<string, unknown>))) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return typeof current === 'string' ? current : undefined;\n}\n\n/**\n * Construit un résolveur de titres depuis le méga-menu (`menu.ts`, qui mappe les\n * routes → clés `pages.*`) et les messages (`fr.json`). Ne remplit que ce qu'il\n * trouve ; les pages sans entrée de menu restent sans titre (curées plus tard,\n * comme côté plateforme — doc 03 §1.2 : « le sync ne remplit que les titres vides »).\n */\nexport function buildTitleResolver(menuItems: ReadonlyArray<MenuLike>, messages: unknown): (routeName: string | undefined, pattern: string) => string | undefined {\n const labelByName = new Map<string, string>();\n const labelByPattern = new Map<string, string>();\n\n const walk = (items: ReadonlyArray<MenuLike>): void => {\n for (const item of items) {\n if (item.label && item.to) {\n if (typeof item.to === 'string') {\n labelByPattern.set(normalizePattern(item.to), item.label);\n } else if (item.to.name) {\n labelByName.set(item.to.name, item.label);\n }\n }\n if (item.subItems) {\n walk(item.subItems);\n }\n if (item.megaMenu) {\n walk(item.megaMenu);\n }\n }\n };\n walk(menuItems);\n\n return (routeName, pattern) => {\n const key = (routeName && labelByName.get(routeName)) || labelByPattern.get(normalizePattern(pattern));\n return key ? resolveMessage(messages, key) : undefined;\n };\n}\n"],"mappings":";;;AAkCO,SAAS,iBAAiB,SAA4C;AAC3E,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AACA,MAAI,aAAa,QAAQ,WAAW,GAAG,IAAI,UAAU,IAAI,OAAO;AAChE,eAAa,WAAW,QAAQ,WAAW,GAAG;AAC9C,MAAI,WAAW,SAAS,GAAG;AACzB,iBAAa,WAAW,QAAQ,QAAQ,EAAE;AAAA,EAC5C;AACA,SAAO,WAAW,SAAS,IAAI,aAAa;AAC9C;AAGA,SAAS,YAAY,WAAmB,WAA2B;AACjE,MAAI,CAAC,WAAW;AACd,WAAO,aAAa;AAAA,EACtB;AACA,MAAI,UAAU,WAAW,GAAG,GAAG;AAC7B,WAAO,iBAAiB,SAAS;AAAA,EACnC;AACA,QAAM,OAAO,cAAc,MAAM,KAAK;AACtC,SAAO,iBAAiB,GAAG,IAAI,IAAI,SAAS,EAAE;AAChD;AAGA,SAAS,WAAW,MAAmC;AACrD,SAAO,CAAC,CAAC,QAAQ,WAAW,KAAK,IAAI;AACvC;AAiBO,SAAS,aAAa,QAAiC,UAAwB,CAAC,GAAmB;AACxG,QAAM,WAAW,QAAQ,aAAa,MAAM;AAC5C,QAAM,QAAuB,CAAC;AAC9B,QAAM,YAAY,oBAAI,IAAyB;AAE/C,QAAM,QAAQ,CAAC,SAAkC,WAAmB,kBAAuC;AACzG,YAAQ,QAAQ,CAAC,QAAQ,UAAU;AACjC,YAAM,cAAc,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS;AAC/E,YAAM,aAAa,OAAO,YAAY;AACtC,UAAI,cAAc,CAAC,aAAa;AAC9B;AAAA,MACF;AACA,UAAI,WAAW,OAAO,IAAI,GAAG;AAC3B;AAAA,MACF;AAEA,YAAM,MAAM,YAAY,WAAW,OAAO,QAAQ,EAAE;AACpD,YAAM,YAAY,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAElE,YAAM,WAAW,UAAU,IAAI,GAAG;AAClC,UAAI,UAAU;AAEZ,YAAI,aAAa,CAAC,SAAS,YAAY;AACrC,mBAAS,aAAa;AACtB,cAAI,CAAC,SAAS,OAAO;AACnB,kBAAM,cAAc,SAAS,WAAW,GAAG;AAC3C,gBAAI,aAAa;AACf,uBAAS,QAAQ;AAAA,YACnB;AAAA,UACF;AACA,iBAAO,SAAS;AAAA,QAClB;AAAA,MACF,OAAO;AACL,cAAM,QAAQ,SAAS,WAAW,GAAG,KAAK;AAC1C,cAAM,OAAoB,EAAE,cAAc,KAAK,QAAQ,eAAe,OAAO,MAAM;AACnF,YAAI,WAAW;AACb,eAAK,aAAa;AAAA,QACpB,OAAO;AACL,eAAK,aAAa;AAAA,QACpB;AACA,YAAI,OAAO;AACT,eAAK,QAAQ;AAAA,QACf;AACA,kBAAU,IAAI,KAAK,IAAI;AACvB,cAAM,KAAK,IAAI;AAAA,MACjB;AAEA,UAAI,aAAa;AACf,cAAM,OAAO,UAAqC,KAAK,GAAG;AAAA,MAC5D;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,QAAQ,IAAI,IAAI;AACtB,SAAO,EAAE,QAAQ,GAAG,MAAM;AAC5B;","names":[]}
|