@mhosaic/feedback-cli 0.49.0 → 0.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/bin.js +6 -7
  2. package/dist/{build-RCLWV2WL.js → build-AHXPNO76.js} +1 -2
  3. package/dist/{check-FOJPEE4Q.js → check-UZZFPZMA.js} +3 -4
  4. package/dist/{chunk-COJ75KUD.js → chunk-45QCNACT.js} +0 -1
  5. package/dist/{chunk-AZQSQJNQ.js → chunk-CFJPCJAQ.js} +0 -1
  6. package/dist/{chunk-SSLQOK2Z.js → chunk-GJEJSB2Q.js} +2 -3
  7. package/dist/{chunk-IHJPCMYF.js → chunk-KHQBWJ4A.js} +0 -1
  8. package/dist/config-75OC57EE.js +7 -0
  9. package/dist/{doctor-CTL34U4Y.js → doctor-Z4KZBFBN.js} +1 -2
  10. package/dist/{eject-VNJVOCQM.js → eject-QFPZ7Z3P.js} +1 -2
  11. package/dist/{generate-VFRQNPOI.js → generate-CLL5W7SX.js} +3 -4
  12. package/dist/{init-RS7BKALE.js → init-BK6TPKX6.js} +1 -2
  13. package/dist/{install-skill-BIBDYLKE.js → install-skill-XSUMSGQV.js} +0 -1
  14. package/dist/{qa-YR4GCV6T.js → qa-APZ5CKN5.js} +9 -10
  15. package/dist/{sitemap-react-QTEAUKN5.js → sitemap-react-IF5PX2PM.js} +2 -3
  16. package/dist/{sitemap-vue-EJDQTCYM.js → sitemap-vue-LWK6Y4FE.js} +2 -3
  17. package/dist/{verify-LCXL3WOX.js → verify-24HTNRXV.js} +0 -1
  18. package/package.json +1 -1
  19. package/skills/chantier/SKILL.md +3 -5
  20. package/skills/feedback-close/SKILL.md +5 -3
  21. package/skills/feedback-fix/SKILL.md +8 -11
  22. package/skills/feedback-pull/SKILL.md +4 -4
  23. package/skills/feedback-watch-merges/SKILL.md +7 -7
  24. package/skills/integrate-feedback/SKILL.md +1 -1
  25. package/skills/integrate-feedback/references/operator-provision.md +5 -5
  26. package/skills/issue-pull/SKILL.md +5 -3
  27. package/dist/bin.js.map +0 -1
  28. package/dist/build-RCLWV2WL.js.map +0 -1
  29. package/dist/check-FOJPEE4Q.js.map +0 -1
  30. package/dist/chunk-AZQSQJNQ.js.map +0 -1
  31. package/dist/chunk-COJ75KUD.js.map +0 -1
  32. package/dist/chunk-IHJPCMYF.js.map +0 -1
  33. package/dist/chunk-SSLQOK2Z.js.map +0 -1
  34. package/dist/config-JE3QRZVH.js +0 -8
  35. package/dist/config-JE3QRZVH.js.map +0 -1
  36. package/dist/doctor-CTL34U4Y.js.map +0 -1
  37. package/dist/eject-VNJVOCQM.js.map +0 -1
  38. package/dist/generate-VFRQNPOI.js.map +0 -1
  39. package/dist/init-RS7BKALE.js.map +0 -1
  40. package/dist/install-skill-BIBDYLKE.js.map +0 -1
  41. package/dist/qa-YR4GCV6T.js.map +0 -1
  42. package/dist/sitemap-react-QTEAUKN5.js.map +0 -1
  43. package/dist/sitemap-vue-EJDQTCYM.js.map +0 -1
  44. package/dist/verify-LCXL3WOX.js.map +0 -1
@@ -1 +0,0 @@
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":[]}
@@ -1 +0,0 @@
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":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/detect.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nexport type Framework =\n | { kind: 'vite-react'; entry: string }\n | { kind: 'nextjs'; entry: string }\n | { kind: 'remix'; entry: string }\n | { kind: 'cra'; entry: string }\n | { kind: 'plain'; entry: null }\n | { kind: 'unknown'; entry: null }\n\nexport interface PackageShape {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n}\n\nexport function detectFromPackage(pkg: PackageShape): Framework {\n const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }\n if (deps.next) return { kind: 'nextjs', entry: 'app/layout.tsx' }\n if (deps['@remix-run/react']) return { kind: 'remix', entry: 'app/root.tsx' }\n if (deps['react-scripts']) return { kind: 'cra', entry: 'src/index.tsx' }\n if (deps.vite && (deps.react || deps['@vitejs/plugin-react'])) return { kind: 'vite-react', entry: 'src/main.tsx' }\n return { kind: 'unknown', entry: null }\n}\n\nexport async function detectFramework(cwd: string): Promise<Framework> {\n try {\n const raw = await readFile(join(cwd, 'package.json'), 'utf8')\n return detectFromPackage(JSON.parse(raw))\n } catch {\n return { kind: 'plain', entry: null }\n }\n}\n"],"mappings":";;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY;AAed,SAAS,kBAAkB,KAA8B;AAC9D,QAAM,OAAO,EAAE,GAAI,IAAI,gBAAgB,CAAC,GAAI,GAAI,IAAI,mBAAmB,CAAC,EAAG;AAC3E,MAAI,KAAK,KAAM,QAAO,EAAE,MAAM,UAAU,OAAO,iBAAiB;AAChE,MAAI,KAAK,kBAAkB,EAAG,QAAO,EAAE,MAAM,SAAS,OAAO,eAAe;AAC5E,MAAI,KAAK,eAAe,EAAG,QAAO,EAAE,MAAM,OAAO,OAAO,gBAAgB;AACxE,MAAI,KAAK,SAAS,KAAK,SAAS,KAAK,sBAAsB,GAAI,QAAO,EAAE,MAAM,cAAc,OAAO,eAAe;AAClH,SAAO,EAAE,MAAM,WAAW,OAAO,KAAK;AACxC;AAEA,eAAsB,gBAAgB,KAAiC;AACrE,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,KAAK,KAAK,cAAc,GAAG,MAAM;AAC5D,WAAO,kBAAkB,KAAK,MAAM,GAAG,CAAC;AAAA,EAC1C,QAAQ;AACN,WAAO,EAAE,MAAM,SAAS,OAAO,KAAK;AAAA,EACtC;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { isAbsolute, join, resolve } from 'node:path'\n\n/**\n * Optional sitemap block of the qa-meter config. When absent the meter runs in\n * degraded flat mode (no route rollups). Path fields are absolutized against\n * the config file's directory; `framework` is an adapter name (no path).\n */\nexport interface QaSitemapConfig {\n /** Where the sitemap.json lives (output of `qa sitemap`, or hand-provided). Absolute. */\n file: string\n /** Adapter name for `qa sitemap` (e.g. 'vue-router'); omitted if hand-provided. */\n framework?: string\n /** Directory holding the router declarations. Absolute. */\n routerDir?: string\n /** Optional locale JSON for human titles. Absolute. */\n titlesFrom?: string\n /**\n * Optional import-alias map for the adapter (e.g. `{ '@': 'src' }`). KEYS are\n * kept verbatim; VALUES are absolutized against the config dir (like other\n * path fields) so `@/router/x` resolves to a real file. Default: none.\n */\n alias?: Record<string, string>\n /**\n * react-router adapter only: the file holding the route tree (e.g.\n * `frontend/src/App.tsx` or `frontend/src/router.tsx`). Absolute.\n */\n entry?: string\n /** react-router adapter only: force a reader instead of auto-detecting. */\n routerStyle?: 'jsx' | 'data'\n}\n\n/**\n * Resolved qa-meter config. All filesystem paths are ABSOLUTE (resolved against\n * the directory containing qa-meter.config.json) except `testsGlob` and\n * `fixturesFile`/`pageObjectsDir`, which stay relative to `e2eDir` — they are\n * consumed downstream by joining onto `e2eDir`, so they are kept as-given.\n */\nexport interface QaConfig {\n /** Playwright e2e root. Absolute. */\n e2eDir: string\n /** Suites root ('authored' + 'generated/'). Absolute. */\n suitesDir: string\n /** Where the assembled artifact is written. Absolute. */\n outFile: string\n /** Glob for spec files, relative to e2eDir (kept as a glob string). */\n testsGlob: string\n /** Fixtures file, relative to e2eDir (kept as-given). */\n fixturesFile: string\n /** Page-objects dir, relative to e2eDir (kept as-given). */\n pageObjectsDir: string\n /** Annotation prefix honored in spec headers. */\n annotationPrefix: string\n /** Environment stamped into the artifact. */\n environment: string\n /** App version string, or null (resolved from --version flag downstream). */\n appVersion: string | null\n /** Optional Playwright results file. Absolute, or null. */\n resultsFile: string | null\n /** Optional dev-validations file. Absolute, or null. */\n devValidationsFile: string | null\n /** Optional sitemap block, or null when absent. */\n sitemap: QaSitemapConfig | null\n}\n\nconst CONFIG_NAME = 'qa-meter.config.json'\n\n/** Absolutize `p` against `dir` (already-absolute paths pass through). */\nfunction abs(dir: string, p: string): string {\n return isAbsolute(p) ? p : resolve(join(dir, p))\n}\n\n/** Read a required string field; throw a field-named error if missing/wrong-type. */\nfunction reqString(raw: Record<string, unknown>, field: string): string {\n const v = raw[field]\n if (v === undefined || v === null) throw new Error(`${field} is required in ${CONFIG_NAME}`)\n if (typeof v !== 'string') throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n return v\n}\n\n/** Read an optional string field with a default; throw a field-named error on wrong type. */\nfunction optString(raw: Record<string, unknown>, field: string, fallback: string): string {\n const v = raw[field]\n if (v === undefined || v === null) return fallback\n if (typeof v !== 'string') throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n return v\n}\n\n/** Read an optional path field; absolutize if present, else null. Throws on wrong type. */\nfunction optPath(raw: Record<string, unknown>, field: string, dir: string): string | null {\n const v = raw[field]\n if (v === undefined || v === null) return null\n if (typeof v !== 'string') throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n return abs(dir, v)\n}\n\n/**\n * Read an optional alias map field. KEYS pass through verbatim; VALUES are\n * absolutized against `dir` (like other path fields) so `@` → `<dir>/src`\n * becomes an absolute base for the adapter. Returns null if absent. Throws a\n * field-named error on wrong shape.\n */\nfunction optAlias(\n raw: Record<string, unknown>,\n field: string,\n dir: string,\n): Record<string, string> | null {\n const v = raw[field]\n if (v === undefined || v === null) return null\n if (typeof v !== 'object' || Array.isArray(v)) {\n throw new Error(`${field} must be an object map in ${CONFIG_NAME}`)\n }\n const out: Record<string, string> = {}\n for (const [key, val] of Object.entries(v as Record<string, unknown>)) {\n if (typeof val !== 'string') {\n throw new Error(`${field}.${key} must be a string in ${CONFIG_NAME}`)\n }\n out[key] = abs(dir, val)\n }\n return out\n}\n\n/** Read an optional `routerStyle` ('jsx' | 'data'); null if absent. Throws on bad value. */\nfunction optRouterStyle(raw: Record<string, unknown>): 'jsx' | 'data' | null {\n const v = raw.routerStyle\n if (v === undefined || v === null) return null\n if (v !== 'jsx' && v !== 'data') {\n throw new Error(`sitemap.routerStyle must be \"jsx\" or \"data\" in ${CONFIG_NAME}`)\n }\n return v\n}\n\n/**\n * Load and validate `qa-meter.config.json` from `cwd`. All paths are resolved\n * against `cwd` (the directory containing the config file). Validation is\n * hand-rolled (no schema lib) to match the CLI's zero-dependency posture.\n */\nexport function loadQaConfig(cwd: string): QaConfig {\n const file = join(cwd, CONFIG_NAME)\n if (!existsSync(file)) {\n throw new Error(`Missing ${CONFIG_NAME} in ${cwd}. Create one — see the qa-meter design spec §7.1.`)\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'))\n } catch (err) {\n throw new Error(`Failed to parse ${CONFIG_NAME}: ${err instanceof Error ? err.message : String(err)}`)\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error(`${CONFIG_NAME} must contain a JSON object`)\n }\n const raw = parsed as Record<string, unknown>\n\n // Type-check every present-but-wrong-type string field up front, so a bad\n // field surfaces a field-named error regardless of whether some *other*\n // required field happens to be missing.\n for (const field of [\n 'e2eDir',\n 'suitesDir',\n 'outFile',\n 'testsGlob',\n 'fixturesFile',\n 'pageObjectsDir',\n 'annotationPrefix',\n 'environment',\n 'resultsFile',\n 'devValidationsFile',\n ]) {\n const v = raw[field]\n if (v !== undefined && v !== null && typeof v !== 'string') {\n throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n }\n }\n\n let sitemap: QaSitemapConfig | null = null\n if (raw.sitemap !== undefined && raw.sitemap !== null) {\n if (typeof raw.sitemap !== 'object' || Array.isArray(raw.sitemap)) {\n throw new Error(`sitemap must be an object in ${CONFIG_NAME}`)\n }\n const s = raw.sitemap as Record<string, unknown>\n const routerDir = optPath(s, 'routerDir', cwd)\n const titlesFrom = optPath(s, 'titlesFrom', cwd)\n const alias = optAlias(s, 'alias', cwd)\n const entry = optPath(s, 'entry', cwd)\n const routerStyle = optRouterStyle(s)\n sitemap = {\n file: abs(cwd, reqString(s, 'file')),\n ...(s.framework !== undefined && s.framework !== null\n ? { framework: optString(s, 'framework', '') }\n : {}),\n ...(routerDir !== null ? { routerDir } : {}),\n ...(titlesFrom !== null ? { titlesFrom } : {}),\n ...(alias !== null ? { alias } : {}),\n ...(entry !== null ? { entry } : {}),\n ...(routerStyle !== null ? { routerStyle } : {}),\n }\n }\n\n return {\n e2eDir: abs(cwd, reqString(raw, 'e2eDir')),\n suitesDir: abs(cwd, reqString(raw, 'suitesDir')),\n outFile: abs(cwd, reqString(raw, 'outFile')),\n // testsGlob, fixturesFile, pageObjectsDir are kept relative to e2eDir (not absolutized).\n testsGlob: optString(raw, 'testsGlob', 'tests/**/*.spec.ts'),\n fixturesFile: optString(raw, 'fixturesFile', 'helpers/fixtures.ts'),\n pageObjectsDir: optString(raw, 'pageObjectsDir', 'pages'),\n annotationPrefix: optString(raw, 'annotationPrefix', '@mhosaic'),\n environment: optString(raw, 'environment', 'staging'),\n appVersion:\n raw.appVersion === undefined || raw.appVersion === null\n ? null\n : typeof raw.appVersion === 'string'\n ? raw.appVersion\n : (() => {\n throw new Error(`appVersion must be a string or null in ${CONFIG_NAME}`)\n })(),\n resultsFile: optPath(raw, 'resultsFile', cwd),\n devValidationsFile: optPath(raw, 'devValidationsFile', cwd),\n sitemap,\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/generate.ts"],"sourcesContent":["/**\n * `qa generate` — static analysis of Playwright specs into generated suite YAML.\n *\n * Parses (never executes) the fixtures file, the referenced page-objects, and\n * every spec via the TypeScript compiler API, then emits one generated suite per\n * test directory: one `test_case` per `test()`, pages inferred from the\n * page-object fixtures each test destructures (or overridden by a\n * `<prefix>:pages` header). Output is fully deterministic — every collection is\n * sorted lexicographically so two runs serialize identically.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport ts from 'typescript'\nimport { stringify } from 'yaml'\nimport { loadQaConfig, type QaConfig } from './config'\nimport { normalizePattern } from './sitemap-core'\nimport type { GeneratedSuiteDoc, SuitePage, SuiteTestCase } from './suite-format'\n\nconst HEADER_COMMENT =\n '# GENERATED by qa-meter — do not hand-edit. Regenerate with: mhosaic-feedback qa generate'\n\n// ───────────────────────────────────────────────────────── helpers ──────────\n\n/** Read the CLI package version (stamped into `meta.generated_by`). */\nfunction cliVersion(): string {\n const here = dirname(fileURLToPath(import.meta.url))\n // src/qa/generate.ts → ../../package.json ; dist/* → ../package.json. Walk up.\n for (let dir = here, i = 0; i < 5; i++, dir = dirname(dir)) {\n const candidate = join(dir, 'package.json')\n if (existsSync(candidate)) {\n try {\n const pkg = JSON.parse(readFileSync(candidate, 'utf8')) as { name?: string; version?: string }\n if (pkg.name === '@mhosaic/feedback-cli' && typeof pkg.version === 'string') return pkg.version\n } catch {\n // keep walking\n }\n }\n }\n return '0.0.0'\n}\n\n/** Slug a title: lowercase, strip accents, non-alphanumerics → `-`, collapse/trim. */\nfunction slug(input: string): string {\n return input\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/** Stable registry key for a route pattern (e.g. `/widgets/:widgetId/edit` → `widgets-widgetId-edit`). */\nfunction patternKey(pattern: string): string {\n const key = pattern\n .replace(/[:*]/g, '')\n .split('/')\n .filter(Boolean)\n .map((seg) => slug(seg))\n .filter(Boolean)\n .join('-')\n return key.length > 0 ? key : 'root'\n}\n\n/** Read & parse a TS source file into an AST (no Program, no type-checking). */\nfunction parseFile(file: string): ts.SourceFile {\n const text = readFileSync(file, 'utf8')\n return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, /*setParentNodes*/ true, ts.ScriptKind.TS)\n}\n\n/** Resolve a relative module specifier to an on-disk `.ts` file path. */\nfunction resolveImport(fromFile: string, spec: string): string | null {\n if (!spec.startsWith('.')) return null\n const base = resolve(dirname(fromFile), spec)\n for (const candidate of [`${base}.ts`, join(base, 'index.ts')]) {\n if (existsSync(candidate)) return candidate\n }\n return null\n}\n\n/** Map an imported identifier name → its source file, from a file's import declarations. */\nfunction importMap(sf: ts.SourceFile): Map<string, string> {\n const map = new Map<string, string>()\n for (const stmt of sf.statements) {\n if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue\n const target = resolveImport(sf.fileName, stmt.moduleSpecifier.text)\n if (!target) continue\n const bindings = stmt.importClause?.namedBindings\n if (bindings && ts.isNamedImports(bindings)) {\n for (const el of bindings.elements) map.set(el.name.text, target)\n }\n if (stmt.importClause?.name) map.set(stmt.importClause.name.text, target)\n }\n return map\n}\n\n// ─────────────────────────────────────────────── step 1: fixtures map ───────\n\n/** `fixtureName → page-object class name` from the `base.extend({...})` call. */\nfunction fixtureClasses(sf: ts.SourceFile): Map<string, string> {\n const out = new Map<string, string>()\n\n /** Is this a `…extend(...)` / `…extend<...>(...)` call? */\n const isExtendCall = (node: ts.Node): node is ts.CallExpression =>\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'extend'\n\n let objLit: ts.ObjectLiteralExpression | undefined\n const findExtend = (node: ts.Node): void => {\n if (objLit) return\n if (isExtendCall(node)) {\n const arg = node.arguments[0]\n if (arg && ts.isObjectLiteralExpression(arg)) objLit = arg\n }\n ts.forEachChild(node, findExtend)\n }\n findExtend(sf)\n if (!objLit) return out\n\n for (const prop of objLit.properties) {\n // A fixture property is `async name({ … }, use) { … }` (method) or `name: async (…) => …`.\n let name: string | undefined\n let body: ts.Node | undefined\n if (ts.isMethodDeclaration(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {\n name = prop.name.text\n body = prop.body\n } else if (\n ts.isPropertyAssignment(prop) &&\n (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&\n (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))\n ) {\n name = prop.name.text\n body = prop.initializer.body\n }\n if (!name || !body) continue\n\n // Find `use(new <Class>(...))` inside the body.\n let className: string | undefined\n const findUse = (node: ts.Node): void => {\n if (className) return\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n node.expression.text === 'use' &&\n node.arguments[0] &&\n ts.isNewExpression(node.arguments[0]) &&\n ts.isIdentifier(node.arguments[0].expression)\n ) {\n className = node.arguments[0].expression.text\n }\n ts.forEachChild(node, findUse)\n }\n findUse(body)\n if (className) out.set(name, className)\n }\n return out\n}\n\n// ──────────────────────────────────── step 2: page-object goto patterns ─────\n\n/** Extract the route pattern from a page-object class's `goto` method, or null. */\nfunction gotoPattern(sf: ts.SourceFile, className: string): string | null {\n let cls: ts.ClassDeclaration | undefined\n for (const stmt of sf.statements) {\n if (ts.isClassDeclaration(stmt) && stmt.name?.text === className) cls = stmt\n }\n if (!cls) return null\n\n let method: ts.MethodDeclaration | undefined\n for (const member of cls.members) {\n if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === 'goto') {\n method = member\n }\n }\n if (!method?.body) return null\n\n // Parameter names, in order, to resolve `${expr}` spans that aren't bare identifiers.\n const params = method.parameters\n .map((p) => (ts.isIdentifier(p.name) ? p.name.text : undefined))\n .filter((n): n is string => !!n)\n\n // Find `this.page.goto(<arg>)`.\n let arg: ts.Expression | undefined\n const findGoto = (node: ts.Node): void => {\n if (arg) return\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'goto' &&\n node.arguments.length > 0\n ) {\n arg = node.arguments[0]\n }\n ts.forEachChild(node, findGoto)\n }\n findGoto(method.body)\n if (!arg) return null\n\n if (ts.isStringLiteralLike(arg)) return normalizePattern(arg.text)\n\n if (ts.isTemplateExpression(arg)) {\n let raw = arg.head.text\n arg.templateSpans.forEach((span, i) => {\n let name: string\n if (ts.isIdentifier(span.expression)) {\n name = span.expression.text\n } else {\n name = params[i] ?? 'param'\n }\n raw += `:${name}${span.literal.text}`\n })\n return normalizePattern(raw)\n }\n return null\n}\n\n// ─────────────────────────────────────────────────── step 3: specs ──────────\n\ninterface SpecCase {\n title: string\n /** destructured fixture property keys, in source order */\n fixtures: string[]\n /** literal `page.goto('/x')` patterns found in the body */\n gotos: string[]\n}\n\ninterface HeaderAnnotations {\n pages?: string[]\n scenario?: string\n}\n\n/** Parse the leading comment block for `<prefix>:pages` / `<prefix>:scenario` (+ legacy fallback). */\nfunction parseHeader(text: string, prefix: string): HeaderAnnotations {\n const out: HeaderAnnotations = {}\n const lines = text.split(/\\r?\\n/)\n const comments: string[] = []\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed === '') continue\n if (trimmed.startsWith('//')) {\n comments.push(trimmed.replace(/^\\/\\/\\s?/, ''))\n continue\n }\n if (trimmed.startsWith('/*') || trimmed.startsWith('*')) {\n comments.push(trimmed.replace(/^\\/\\*+\\s?/, '').replace(/^\\*+\\s?/, '').replace(/\\*+\\/\\s*$/, ''))\n continue\n }\n break // first non-comment, non-blank line ends the header\n }\n\n const escaped = prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n const read = (pfx: string): void => {\n for (const c of comments) {\n const pagesMatch = c.match(new RegExp(`${pfx}:pages\\\\s+(.+)$`))\n if (pagesMatch?.[1] && out.pages === undefined) {\n out.pages = pagesMatch[1].split(',').map((p) => p.trim()).filter(Boolean)\n }\n const scenarioMatch = c.match(new RegExp(`${pfx}:scenario\\\\s+(\\\\S+)`))\n if (scenarioMatch?.[1] && out.scenario === undefined) out.scenario = scenarioMatch[1]\n }\n }\n read(escaped)\n // legacy read-only fallback when the configured prefix form is absent\n read('@testgen')\n return out\n}\n\n/** Collect destructured property KEYS from a `test()` arrow's `({ a, b: alias })` param. */\nfunction destructuredKeys(fn: ts.ArrowFunction | ts.FunctionExpression): string[] {\n const param = fn.parameters[0]\n if (!param || !ts.isObjectBindingPattern(param.name)) return []\n const keys: string[] = []\n for (const el of param.name.elements) {\n // propertyName is the source key when aliased (`b: alias`); else `name` is the key.\n const keyNode = el.propertyName ?? el.name\n if (ts.isIdentifier(keyNode)) keys.push(keyNode.text)\n else if (ts.isStringLiteral(keyNode)) keys.push(keyNode.text)\n }\n return keys\n}\n\n/** Find literal `page.goto('/x')` string patterns inside a test body. */\nfunction literalGotos(body: ts.Node): string[] {\n const out: string[] = []\n const visit = (node: ts.Node): void => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'goto' &&\n node.arguments[0] &&\n ts.isStringLiteralLike(node.arguments[0])\n ) {\n out.push(normalizePattern(node.arguments[0].text))\n }\n ts.forEachChild(node, visit)\n }\n visit(body)\n return out\n}\n\n/**\n * Is `node` a test-defining call — `test(...)`, `test.only(...)`, or\n * `test.skip(...)` — i.e. one that REGISTERS a single test? Excludes structural\n * helpers (`test.describe`, `test.beforeEach`, `test.step`, …) which do not.\n */\nfunction isTestCall(node: ts.CallExpression): boolean {\n const callee = node.expression\n if (ts.isIdentifier(callee)) return callee.text === 'test'\n if (\n ts.isPropertyAccessExpression(callee) &&\n ts.isIdentifier(callee.expression) &&\n callee.expression.text === 'test'\n ) {\n return callee.name.text === 'only' || callee.name.text === 'skip' || callee.name.text === 'fixme'\n }\n return false\n}\n\n/** Outcome of scanning a spec AST: emittable cases + reports of dynamic-title tests. */\ninterface SpecScan {\n cases: SpecCase[]\n /** Approx descriptions of `test()` calls we found but couldn't emit (non-static title). */\n dynamicTitles: string[]\n}\n\n/**\n * Walk the WHOLE spec AST and collect every `test()`/`test.only()`/`test.skip()`\n * call (so tests inside `test.describe(() => …)` blocks, loops, or other wrappers\n * are found — not just direct top-level statements). A test with a non-static\n * title (template literal or identifier) cannot yield a stable external_key /\n * test_title that matches a Playwright result, so it is NOT emitted but is\n * REPORTED via `dynamicTitles` — silent loss is never acceptable.\n */\nfunction specCases(sf: ts.SourceFile): SpecScan {\n const cases: SpecCase[] = []\n const dynamicTitles: string[] = []\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node) && isTestCall(node)) {\n const titleArg = node.arguments[0]\n const fnArg = node.arguments[1]\n if (titleArg && ts.isStringLiteralLike(titleArg)) {\n if (fnArg && (ts.isArrowFunction(fnArg) || ts.isFunctionExpression(fnArg))) {\n const fixtures = destructuredKeys(fnArg)\n const gotos = fnArg.body ? literalGotos(fnArg.body) : []\n cases.push({ title: titleArg.text, fixtures, gotos })\n } else {\n // String title but no analyzable body (e.g. a bare reference) — still emittable.\n cases.push({ title: titleArg.text, fixtures: [], gotos: [] })\n }\n } else if (titleArg) {\n // Non-static title: template literal, identifier, call, etc. Report, don't emit.\n dynamicTitles.push(approxTitle(titleArg))\n }\n }\n ts.forEachChild(node, visit)\n }\n visit(sf)\n\n // `cases` keep AST traversal order (deterministic per file); the suite build\n // re-sorts across specs. `dynamicTitles` are sorted for stable reporting.\n dynamicTitles.sort((a, b) => a.localeCompare(b))\n return { cases, dynamicTitles }\n}\n\n/** A short, human-readable approximation of a non-static `test()` title argument. */\nfunction approxTitle(node: ts.Node): string {\n const text = node.getText().replace(/\\s+/g, ' ').trim()\n const kind = ts.isTemplateExpression(node)\n ? 'template literal'\n : ts.isIdentifier(node)\n ? 'identifier'\n : 'non-static'\n return `${text.slice(0, 80)} (${kind} title)`\n}\n\n/** List spec files under `e2eDir` matching the `tests/**​/*.spec.ts`-style glob. */\nfunction specFiles(e2eDir: string, testsGlob: string): string[] {\n // The glob is `<baseDir>/**/<suffix>` — derive base dir and a filename test.\n const starIdx = testsGlob.indexOf('**')\n const baseRel = starIdx >= 0 ? testsGlob.slice(0, starIdx).replace(/\\/+$/, '') : dirname(testsGlob)\n const suffixMatch = testsGlob.match(/\\*([^*/]+)$/)\n const suffix = suffixMatch?.[1] ?? '.spec.ts'\n const baseAbs = join(e2eDir, baseRel)\n\n const out: string[] = []\n const walk = (dir: string): void => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })\n } catch {\n return\n }\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.name.endsWith(suffix)) out.push(full)\n }\n }\n walk(baseAbs)\n return out.sort((a, b) => a.localeCompare(b))\n}\n\n// ──────────────────────────────────────────────── orchestration ─────────────\n\n/**\n * Reporting attached to a generation run so that NO spec can vanish silently.\n */\nexport interface GenerateReport {\n /** Per-section generated suites, keyed by section. */\n suites: Map<string, GeneratedSuiteDoc>\n /** Emitted cases that could not be mapped to a page: `<spec> :: <title>`. */\n unmappable: string[]\n /** `test()` calls skipped because their title is not a static string literal: `<spec> :: <approx>`. */\n dynamicTitles: string[]\n /**\n * Spec files matched by the glob that produced ZERO emittable cases (e.g. a\n * factory `messageCrudTest(...)` whose `test()` lives in an imported base).\n * These are NOT in any suite, so they must be surfaced.\n */\n zeroCaseSpecs: string[]\n}\n\n/**\n * Pure generation. Besides the per-section suites, returns three reporting\n * lists so the caller can warn loudly: unmappable cases, dynamic-title tests\n * that were skipped, and spec files that yielded no extractable cases.\n */\nexport function generateSuites(cfg: QaConfig): GenerateReport {\n const version = cliVersion()\n const e2eDir = cfg.e2eDir\n const testsBase = (() => {\n const starIdx = cfg.testsGlob.indexOf('**')\n return starIdx >= 0 ? cfg.testsGlob.slice(0, starIdx).replace(/\\/+$/, '') : dirname(cfg.testsGlob)\n })()\n\n // Step 1 — fixtures map: fixtureName → page-object file path.\n const fixtureToFile = new Map<string, string>()\n const fixturesAbs = join(e2eDir, cfg.fixturesFile)\n if (existsSync(fixturesAbs)) {\n const sf = parseFile(fixturesAbs)\n const imports = importMap(sf)\n for (const [fixtureName, className] of fixtureClasses(sf)) {\n const target = imports.get(className)\n if (target) fixtureToFile.set(fixtureName, target)\n }\n }\n\n // Step 2 — fixtureName → route pattern (parse each referenced page-object once).\n const fixtureClassMap = existsSync(fixturesAbs) ? fixtureClasses(parseFile(fixturesAbs)) : new Map()\n const patternByFile = new Map<string, ts.SourceFile>()\n const fixtureToPattern = new Map<string, string>()\n for (const [fixtureName, file] of fixtureToFile) {\n const className = fixtureClassMap.get(fixtureName)\n if (!className || !existsSync(file)) continue\n let sf = patternByFile.get(file)\n if (!sf) {\n sf = parseFile(file)\n patternByFile.set(file, sf)\n }\n const pattern = gotoPattern(sf, className)\n if (pattern) fixtureToPattern.set(fixtureName, pattern)\n }\n\n const unmappable: string[] = []\n const dynamicTitles: string[] = []\n const zeroCaseSpecs: string[] = []\n // section → accumulated cases + page registry\n interface Section {\n cases: SuiteTestCase[]\n pages: Map<string, SuitePage>\n }\n const sections = new Map<string, Section>()\n\n const files = specFiles(e2eDir, cfg.testsGlob)\n for (const file of files) {\n const specRel = relative(e2eDir, file).split('\\\\').join('/')\n const specBasename = basenameNoSpec(specRel)\n const section = sectionFor(specRel, testsBase)\n const text = readFileSync(file, 'utf8')\n const header = parseHeader(text, cfg.annotationPrefix)\n const sf = parseFile(file)\n const scan = specCases(sf)\n const cases = scan.cases\n\n for (const approx of scan.dynamicTitles) dynamicTitles.push(`${specRel} :: ${approx}`)\n // A spec matched by the glob that produced no emittable cases must be surfaced\n // — its tests live elsewhere (factory/imported base) or use only dynamic titles.\n if (cases.length === 0) zeroCaseSpecs.push(specRel)\n\n if (!sections.has(section)) sections.set(section, { cases: [], pages: new Map() })\n const sec = sections.get(section)!\n\n cases.forEach((spec, index) => {\n let primary: string | undefined\n let traversed: string[] = []\n let pagesSource: 'inferred' | 'annotation' = 'inferred'\n\n if (header.pages && header.pages.length > 0) {\n // Annotation replaces inference for every case in this spec.\n const normalized = header.pages.map((p) => normalizePattern(p))\n primary = normalized[0]\n traversed = normalized.slice(1)\n pagesSource = 'annotation'\n } else {\n // Inference from destructured fixtures, then literal gotos in the body.\n const inferred: string[] = []\n for (const fx of spec.fixtures) {\n const pat = fixtureToPattern.get(fx)\n if (pat) inferred.push(pat)\n }\n for (const g of spec.gotos) inferred.push(g)\n if (inferred.length > 0) {\n primary = inferred[0]\n traversed = inferred.slice(1)\n }\n }\n\n const externalKey = header.scenario ?? `${specRel}::${slug(spec.title)}`\n\n let appliesTo: string[] = []\n if (primary) {\n const key = registerPage(sec.pages, primary, specRel, spec.title)\n appliesTo = [key]\n } else {\n unmappable.push(`${specRel} :: ${spec.title}`)\n }\n\n const testCase: SuiteTestCase = {\n id: `${specBasename}-${index}`,\n title: spec.title,\n suite: specBasename,\n status: 'covered',\n automation: { type: 'playwright', spec: specRel, test_title: spec.title },\n applies_to: appliesTo,\n ...(traversed.length > 0 ? { traverses: traversed.map((pattern) => ({ pattern })) } : {}),\n external_key: externalKey,\n pages_source: pagesSource,\n }\n sec.cases.push(testCase)\n })\n }\n\n // Build the sorted, deterministic suites.\n const suites = new Map<string, GeneratedSuiteDoc>()\n for (const section of [...sections.keys()].sort((a, b) => a.localeCompare(b))) {\n const sec = sections.get(section)!\n // A section with no emittable cases is not a suite — its specs are already\n // surfaced via zeroCaseSpecs/dynamicTitles. Don't write an empty YAML file.\n if (sec.cases.length === 0) continue\n const pages: Record<string, SuitePage> = {}\n for (const key of [...sec.pages.keys()].sort((a, b) => a.localeCompare(b))) {\n pages[key] = sec.pages.get(key)!\n }\n const test_cases = [...sec.cases].sort((a, b) => {\n const specA = a.automation?.spec ?? ''\n const specB = b.automation?.spec ?? ''\n if (specA !== specB) return specA.localeCompare(specB)\n return a.id.localeCompare(b.id)\n })\n suites.set(section, {\n meta: {\n section,\n format: 1,\n origin: 'generated',\n generated_by: `qa-meter@${version}`,\n },\n pages,\n test_cases,\n })\n }\n\n return {\n suites,\n unmappable,\n dynamicTitles: dynamicTitles.sort((a, b) => a.localeCompare(b)),\n zeroCaseSpecs: zeroCaseSpecs.sort((a, b) => a.localeCompare(b)),\n }\n}\n\n/** Register a primary pattern in the section registry; first case for a pattern wins. */\nfunction registerPage(\n registry: Map<string, SuitePage>,\n pattern: string,\n spec: string,\n testTitle: string,\n): string {\n const key = patternKey(pattern)\n if (!registry.has(key)) {\n registry.set(key, { pattern, spec, test_title: testTitle })\n }\n return key\n}\n\n/** `tests/widgets/list.spec.ts` → `list`. */\nfunction basenameNoSpec(specRel: string): string {\n const base = specRel.split('/').pop() ?? specRel\n return base.replace(/\\.spec\\.ts$/, '').replace(/\\.ts$/, '')\n}\n\n/** Section = the immediate parent dir name under the tests base (`tests/widgets/list.spec.ts` → `widgets`). */\nfunction sectionFor(specRel: string, testsBase: string): string {\n let rel = specRel\n const prefix = `${testsBase}/`\n if (rel.startsWith(prefix)) rel = rel.slice(prefix.length)\n const parts = rel.split('/')\n if (parts.length > 1 && parts[0]) return parts[0]\n return testsBase.split('/').filter(Boolean).pop() ?? 'tests'\n}\n\n// ─────────────────────────────────────────────── serialization ──────────────\n\n/** Serialize a generated suite the way `runGenerate` writes it (header + YAML). */\nexport function serializeSuite(doc: GeneratedSuiteDoc): string {\n const yaml = stringify(doc, { lineWidth: 0 })\n return `${HEADER_COMMENT}\\n${yaml}`\n}\n\n// ─────────────────────────────────────────────────── CLI entry ──────────────\n\nexport async function runGenerate(_args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n const { suites, unmappable, dynamicTitles, zeroCaseSpecs } = generateSuites(cfg)\n\n const outDir = join(cfg.suitesDir, 'generated')\n mkdirSync(outDir, { recursive: true })\n\n const sections = [...suites.keys()].sort((a, b) => a.localeCompare(b))\n for (const section of sections) {\n const doc = suites.get(section)!\n const file = join(outDir, `${section}-test-suite.yaml`)\n writeFileSync(file, serializeSuite(doc), 'utf8')\n }\n\n const totalCases = sections.reduce((n, s) => n + suites.get(s)!.test_cases.length, 0)\n process.stderr.write(\n `qa generate: ${sections.length} suite(s), ${totalCases} case(s) → ${relative(process.cwd(), outDir) || outDir}\\n`,\n )\n if (unmappable.length > 0) {\n process.stderr.write(`qa generate: ${unmappable.length} unmappable test(s):\\n`)\n for (const entry of unmappable) process.stderr.write(` warning: unmappable — ${entry}\\n`)\n }\n if (zeroCaseSpecs.length > 0) {\n process.stderr.write(\n `qa generate: ${zeroCaseSpecs.length} spec(s) produced no extractable cases ` +\n `(factory/loop/dynamic-title) — add @mhosaic:pages/@mhosaic:scenario or a string title:\\n`,\n )\n for (const spec of zeroCaseSpecs) process.stderr.write(` warning: zero-case spec — ${spec}\\n`)\n }\n if (dynamicTitles.length > 0) {\n process.stderr.write(\n `qa generate: ${dynamicTitles.length} test(s) skipped for a non-static title ` +\n `(give them a string literal title or a @mhosaic:scenario id):\\n`,\n )\n for (const entry of dynamicTitles) process.stderr.write(` warning: dynamic-title — ${entry}\\n`)\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadQaConfig
4
- } from "./chunk-IHJPCMYF.js";
5
- export {
6
- loadQaConfig
7
- };
8
- //# sourceMappingURL=config-JE3QRZVH.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport type { Dirent } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nimport kleur from 'kleur'\n\nimport { detectFramework } from '../detect'\n\n// A bare `@mhosaic/feedback` import (no subpath) pulls the WHOLE widget into\n// the host's build — freezing its version to package.json. That's the\n// \"bundled\" delivery path: Mhosaic version pins/promotions never reach the\n// app without a dependency bump + redeploy. The loader subpaths\n// (`@mhosaic/feedback/loader`, `.../loader/react`) instead resolve the\n// version at runtime from the manifest, so a pin reaches every client with\n// zero per-repo work. This matcher flags the bundled path; see\n// scanDeliveryPath below. (2026-07-07: a client was found silently bundled\n// and stuck four releases behind — doctor missed it because it only checked\n// the React provider, not vanilla `createFeedback` installs.)\nconst BUNDLED_IMPORT = /from\\s+['\"]@mhosaic\\/feedback['\"]/\nconst CODE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.vue', '.svelte', '.astro'])\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.output', '.svelte-kit'])\n\n/** Shallow-bounded walk of `src/` (and the entry) for a bare bundled import.\n * Returns the first offending repo-relative path, or null if none. Capped so\n * doctor stays fast on large repos. */\nexport function scanDeliveryPath(cwd: string, entry?: string): string | null {\n const seen = new Set<string>()\n const check = (rel: string): boolean => {\n const abs = join(cwd, rel)\n if (seen.has(abs) || !existsSync(abs)) return false\n seen.add(abs)\n try {\n return BUNDLED_IMPORT.test(readFileSync(abs, 'utf8'))\n } catch {\n return false\n }\n }\n if (entry && check(entry)) return entry\n\n let budget = 2000 // files; a backstop, real hosts are far smaller\n const walk = (dir: string): string | null => {\n let entries: Dirent[]\n try {\n entries = readdirSync(join(cwd, dir), { withFileTypes: true })\n } catch {\n return null\n }\n for (const e of entries) {\n if (budget-- <= 0) return null\n const rel = dir ? `${dir}/${e.name}` : e.name\n if (e.isDirectory()) {\n if (SKIP_DIRS.has(e.name)) continue\n const hit = walk(rel)\n if (hit) return hit\n } else if (CODE_EXT.has(e.name.slice(e.name.lastIndexOf('.')))) {\n if (check(rel)) return rel\n }\n }\n return null\n }\n return existsSync(join(cwd, 'src')) ? walk('src') : null\n}\n\n// The loader injects `<script src=\"https://cdn.jsdelivr.net/...widget.min.js\">`\n// at runtime, so a strict CSP must allow that host in `script-src` or the\n// widget silently never loads — the \"Refused to load … not in the script-src\n// directive\" failure that took a client's widget down on 2026-07-08 right\n// after it migrated to the loader. This is the companion requirement to the\n// import swap; catching it here means at migration time, not in production.\nconst CDN_HOST = 'cdn.jsdelivr.net'\n// CSP config lives in many shapes (index.html meta, _headers, nginx confs,\n// *.toml/json framework config). We only need the text.\nconst CSP_EXT = new Set(['.html', '.htm', '.conf', '.toml', '.json', '.js', '.cjs', '.mjs', '.ts'])\nconst CSP_EXTRA_NAMES = new Set(['_headers'])\n// A source token that already permits any cross-origin script host — no CDN\n// entry needed. `'unsafe-inline'`/`'unsafe-eval'` do NOT help a cross-origin\n// src, so they don't count here.\nconst PERMISSIVE_SRC = /(?:^|\\s)(?:\\*|https:)(?=\\s|;|\"|'|$)/\n\n/** The effective script-src value from one CSP string (falls back to\n * default-src), whitespace-flattened so a multi-line meta tag parses. Null\n * when neither directive is present. */\nexport function scriptSrcOf(csp: string): string | null {\n const flat = csp.replace(/\\s+/g, ' ')\n const m = /script-src([^;]*)/i.exec(flat) ?? /default-src([^;]*)/i.exec(flat)\n return m ? (m[1] ?? null) : null\n}\n\n/** Bounded repo walk for a CSP whose (effective) script-src is restrictive\n * AND omits the widget CDN — i.e. would block the loader. Returns the first\n * offending repo-relative file, or null. Permissive policies (`*`, `https:`)\n * and policies that already allow the CDN pass. */\nexport function scanCsp(cwd: string): string | null {\n let budget = 3000\n const walk = (dir: string): string | null => {\n let entries: Dirent[]\n try {\n entries = readdirSync(join(cwd, dir), { withFileTypes: true })\n } catch {\n return null\n }\n for (const e of entries) {\n if (budget-- <= 0) return null\n const rel = dir ? `${dir}/${e.name}` : e.name\n if (e.isDirectory()) {\n if (SKIP_DIRS.has(e.name)) continue\n const hit = walk(rel)\n if (hit) return hit\n continue\n }\n const ext = e.name.slice(e.name.lastIndexOf('.'))\n if (!CSP_EXT.has(ext) && !CSP_EXTRA_NAMES.has(e.name)) continue\n let text: string\n try {\n text = readFileSync(join(cwd, rel), 'utf8')\n } catch {\n continue\n }\n if (!text.includes('script-src') && !text.includes('Content-Security-Policy')) continue\n const scriptSrc = scriptSrcOf(text)\n if (scriptSrc === null) continue // CSP present but no script/default-src → not restricting scripts\n if (scriptSrc.includes(CDN_HOST) || PERMISSIVE_SRC.test(scriptSrc)) continue // already allowed\n return rel // restrictive script-src that omits the CDN → would block the loader\n }\n return null\n }\n return walk('')\n}\n\n/** Nearest `.gitignore` on the path from `cwd` up to the repo root that\n * covers `.env.local`, as a path relative to `cwd` — or null if none does.\n *\n * Only cwd's own `.gitignore` used to be read, so the very common\n * `backend/ + frontend/` layout — one `.gitignore` at the repo root, doctor\n * run from `frontend/` — reported a miss for a file git was already\n * ignoring, and sent operators off to write a second `.gitignore` they\n * didn't need. The walk stops AT the repo root because a `.gitignore` above\n * it isn't git's to apply; `.git` is a directory in a normal clone and a\n * file in a worktree, so existence is the test, not type. Substring match on\n * purpose: this is the same shallow check as before, just widened to the\n * ancestors — parsing gitignore semantics properly is not worth it to\n * answer \"did they remember the line\". */\nexport function findGitignoreCovering(cwd: string): string | null {\n let dir = cwd\n for (let depth = 0; depth < 32; depth++) {\n const gitignore = join(dir, '.gitignore')\n if (existsSync(gitignore)) {\n try {\n if (readFileSync(gitignore, 'utf8').includes('.env.local')) {\n return depth === 0 ? '.gitignore' : '../'.repeat(depth) + '.gitignore'\n }\n } catch {\n // Unreadable — treat as absent and keep walking.\n }\n }\n if (existsSync(join(dir, '.git'))) return null // repo root, stop\n const parent = dirname(dir)\n if (parent === dir) return null // filesystem root\n dir = parent\n }\n return null\n}\n\n/** How the host instantiates the widget, or null if it never does. */\nexport type WidgetWiring = 'provider' | 'factory'\n\n// Any widget import — bare or subpath. Which one it is, is the delivery\n// check's business (scanDeliveryPath), not this one's.\nconst FEEDBACK_IMPORT = /from\\s+['\"]@mhosaic\\/feedback(?:\\/[a-z/-]+)?['\"]/\nconst FACTORY_CALL = /\\bcreateFeedback\\s*\\(/\n\n/** Detect that the widget is actually instantiated somewhere.\n *\n * Two sanctioned shapes, not one. `<FeedbackProvider>` in the entry is what\n * `init` auto-writes. But `consumer-install-vite.md` tells hosts that to add\n * withErrorTracking / withReplay / withWebVitals they should REPLACE the\n * provider with a `createFeedback()` call in a small module imported for its\n * side effect — so checking only for the provider made doctor report a\n * correctly-wired host as unwired, and told it to undo what the docs had\n * just asked for. (2026-08-19: hit on a client integration doing exactly\n * that.) */\nexport function scanWidgetWiring(cwd: string, entry?: string): WidgetWiring | null {\n const read = (rel: string): string | null => {\n const abs = join(cwd, rel)\n if (!existsSync(abs)) return null\n try {\n return readFileSync(abs, 'utf8')\n } catch {\n return null\n }\n }\n\n const entrySrc = entry ? read(entry) : null\n if (\n entrySrc &&\n entrySrc.includes('<FeedbackProvider') &&\n (entrySrc.includes('@mhosaic/feedback/loader/react') ||\n entrySrc.includes('@mhosaic/feedback/react'))\n ) {\n return 'provider'\n }\n\n const instantiates = (src: string): boolean =>\n FACTORY_CALL.test(src) && FEEDBACK_IMPORT.test(src)\n\n if (entrySrc && instantiates(entrySrc)) return 'factory'\n\n // Same bounded walk + skip list as scanDeliveryPath: the factory module is\n // conventionally src/feedback.ts, but nothing enforces that.\n let budget = 2000\n const walk = (dir: string): WidgetWiring | null => {\n let entries: Dirent[]\n try {\n entries = readdirSync(join(cwd, dir), { withFileTypes: true })\n } catch {\n return null\n }\n for (const e of entries) {\n if (budget-- <= 0) return null\n const rel = dir ? `${dir}/${e.name}` : e.name\n if (e.isDirectory()) {\n if (SKIP_DIRS.has(e.name)) continue\n const hit = walk(rel)\n if (hit) return hit\n } else if (CODE_EXT.has(e.name.slice(e.name.lastIndexOf('.')))) {\n const src = read(rel)\n if (src && instantiates(src)) return 'factory'\n }\n }\n return null\n }\n return existsSync(join(cwd, 'src')) ? walk('src') : null\n}\n\n/** A host can declare it is intentionally bundled — e.g. a strict CSP that\n * forbids external CDN scripts, so the loader physically cannot run (its\n * jsDelivr <script> is refused). It opts out via package.json\n * `{\"mhosaicFeedback\": {\"delivery\": \"bundled\"}}`. This is the ONE escape\n * hatch; every host without it still gets the hard \"switch to the loader\"\n * failure, so the default stays strong. Expected to be rare (one known case:\n * a CSP-locked client whose widget updates are done manually). */\nexport function isBundledByDesign(cwd: string): boolean {\n try {\n const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))\n return pkg?.mhosaicFeedback?.delivery === 'bundled'\n } catch {\n return false\n }\n}\n\nexport async function runDoctor(argv: string[]): Promise<void> {\n const cwd = argv.includes('--cwd') ? argv[argv.indexOf('--cwd') + 1] ?? process.cwd() : process.cwd()\n\n const checks: Array<{ name: string; ok: boolean; hint?: string }> = []\n\n const framework = await detectFramework(cwd)\n checks.push({ name: `framework detected: ${framework.kind}`, ok: framework.kind !== 'unknown' && framework.kind !== 'plain' })\n\n const envPath = join(cwd, '.env.local')\n const envOk = existsSync(envPath) && readFileSync(envPath, 'utf8').includes('VITE_FEEDBACK_API_KEY=')\n checks.push({ name: '.env.local has VITE_FEEDBACK_API_KEY', ok: envOk, hint: 'run `mhosaic-feedback init`' })\n\n const giAt = findGitignoreCovering(cwd)\n checks.push({\n // Name the file when it isn't the local one, so a monorepo operator can\n // see WHICH .gitignore answered rather than wondering if we looked.\n name:\n giAt && giAt !== '.gitignore'\n ? `.gitignore ignores .env.local (${giAt})`\n : '.gitignore ignores .env.local',\n ok: giAt !== null,\n hint: 'add `.env.local` to .gitignore',\n })\n\n const wiring = scanWidgetWiring(cwd, framework.entry ?? undefined)\n checks.push({\n name:\n wiring === 'provider'\n ? 'widget wired in the app (<FeedbackProvider> in entry)'\n : wiring === 'factory'\n ? 'widget wired in the app (createFeedback module)'\n : 'widget wired in the app',\n ok: wiring !== null,\n hint: 'run `mhosaic-feedback init`, or instantiate the widget yourself with createFeedback()',\n })\n\n // Delivery path: the loader resolves the widget version at runtime, so\n // Mhosaic version pins reach this app automatically. A bare\n // `@mhosaic/feedback` import bundles a frozen version instead. This is the\n // one check that keeps \"every client is loader\" true over time — unless the\n // host has explicitly opted into bundling (isBundledByDesign), the one\n // sanctioned exception for a CSP that can't allow the CDN.\n if (isBundledByDesign(cwd)) {\n checks.push({\n name: 'delivery: bundled on purpose (mhosaicFeedback.delivery=\"bundled\") — update the version manually',\n ok: true,\n })\n // CSP check deliberately skipped: a bundled widget is served same-origin,\n // so there is no CDN host to allowlist.\n } else {\n const bundledAt = scanDeliveryPath(cwd, framework.entry ?? undefined)\n checks.push({\n name: 'loader delivery path (version pins reach this app)',\n ok: bundledAt === null,\n ...(bundledAt\n ? {\n hint: `bundled import in ${bundledAt} — change '@mhosaic/feedback' to '@mhosaic/feedback/loader' (same API) so pins land without a redeploy. If a strict CSP forbids the CDN, keep it bundled and set package.json \"mhosaicFeedback\": { \"delivery\": \"bundled\" }`,\n }\n : {}),\n })\n\n // CSP: the loader fetches the widget from the CDN, so a strict script-src\n // must allow it. Only meaningful on the loader path — a bundled app serves\n // the widget same-origin and needs no CDN entry (the check above already\n // tells it to switch). Gating on `!bundledAt` avoids a confusing double\n // warning on apps that haven't migrated yet.\n if (bundledAt === null) {\n const cspBlockedAt = scanCsp(cwd)\n checks.push({\n name: `CSP allows the widget CDN (${CDN_HOST})`,\n ok: cspBlockedAt === null,\n ...(cspBlockedAt\n ? {\n hint: `CSP in ${cspBlockedAt} omits the CDN — add 'https://${CDN_HOST}' to script-src (and the platform origin to connect-src), or if the CSP can't allow an external CDN keep the widget bundled and set package.json \"mhosaicFeedback\": { \"delivery\": \"bundled\" }`,\n }\n : {}),\n })\n }\n }\n\n let hasFailure = false\n for (const c of checks) {\n const icon = c.ok ? kleur.green('✓') : kleur.red('✗')\n process.stdout.write(`${icon} ${c.name}${!c.ok && c.hint ? kleur.gray(' — ' + c.hint) : ''}\\n`)\n if (!c.ok) hasFailure = true\n }\n if (hasFailure) process.exitCode = 1\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/eject.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport kleur from 'kleur'\n\nimport { detectFramework } from '../detect'\n\nexport async function runEject(argv: string[]): Promise<void> {\n const cwd = argv.includes('--cwd') ? argv[argv.indexOf('--cwd') + 1] ?? process.cwd() : process.cwd()\n\n const framework = await detectFramework(cwd)\n if (framework.entry) {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n const src = readFileSync(entryPath, 'utf8')\n const stripped = src\n .replace(/\\/\\/ === mhosaic-feedback:import:start ===[\\s\\S]*?\\/\\/ === mhosaic-feedback:import:end ===\\n?/g, '')\n .replace(/\\{\\/\\* === mhosaic-feedback:wrap:start === \\*\\/\\}[\\s\\S]*?\\{\\/\\* === mhosaic-feedback:wrap:end === \\*\\/\\}\\n?/g, '')\n if (stripped !== src) {\n await writeFile(entryPath, stripped)\n process.stdout.write(kleur.green(`✓ Stripped markers from ${framework.entry}\\n`))\n }\n }\n }\n\n const envPath = join(cwd, '.env.local')\n if (existsSync(envPath)) {\n const src = readFileSync(envPath, 'utf8')\n const cleaned = src.replace(/# === mhosaic-feedback ===\\n[\\s\\S]*?# === mhosaic-feedback ===\\n?/g, '').replace(/\\n{3,}/g, '\\n\\n')\n if (cleaned !== src) {\n await writeFile(envPath, cleaned)\n process.stdout.write(kleur.green('✓ Removed env block from .env.local\\n'))\n }\n }\n\n process.stdout.write(kleur.gray(\"Run `pnpm remove @mhosaic/feedback` (or npm/yarn equivalent) to uninstall the package.\\n\"))\n}\n"],"mappings":";;;;;;AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AAErB,OAAO,WAAW;AAIlB,eAAsB,SAAS,MAA+B;AAC5D,QAAM,MAAM,KAAK,SAAS,OAAO,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,CAAC,KAAK,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAEpG,QAAM,YAAY,MAAM,gBAAgB,GAAG;AAC3C,MAAI,UAAU,OAAO;AACnB,UAAM,YAAY,KAAK,KAAK,UAAU,KAAK;AAC3C,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,MAAM,aAAa,WAAW,MAAM;AAC1C,YAAM,WAAW,IACd,QAAQ,kGAAkG,EAAE,EAC5G,QAAQ,gHAAgH,EAAE;AAC7H,UAAI,aAAa,KAAK;AACpB,cAAM,UAAU,WAAW,QAAQ;AACnC,gBAAQ,OAAO,MAAM,MAAM,MAAM,gCAA2B,UAAU,KAAK;AAAA,CAAI,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,KAAK,KAAK,YAAY;AACtC,MAAI,WAAW,OAAO,GAAG;AACvB,UAAM,MAAM,aAAa,SAAS,MAAM;AACxC,UAAM,UAAU,IAAI,QAAQ,sEAAsE,EAAE,EAAE,QAAQ,WAAW,MAAM;AAC/H,QAAI,YAAY,KAAK;AACnB,YAAM,UAAU,SAAS,OAAO;AAChC,cAAQ,OAAO,MAAM,MAAM,MAAM,4CAAuC,CAAC;AAAA,IAC3E;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,MAAM,KAAK,0FAA0F,CAAC;AAC7H;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/init.ts","../src/edits/env.ts","../src/edits/gitignore.ts","../src/edits/react.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { spawnSync } from 'node:child_process'\n\nimport kleur from 'kleur'\nimport prompts from 'prompts'\n\nimport { detectFramework } from '../detect'\nimport { writeEnvLocal, writeEnvLocalExample } from '../edits/env'\nimport { ensureGitignore } from '../edits/gitignore'\nimport { wrapReactEntry } from '../edits/react'\n\ninterface InitArgs {\n apiKey?: string\n endpoint?: string\n yes: boolean\n install: boolean\n cwd: string\n}\n\nfunction parseArgs(argv: string[]): InitArgs {\n const out: InitArgs = { yes: false, install: true, cwd: process.cwd() }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--api-key') { const v = argv[++i]; if (v !== undefined) out.apiKey = v }\n else if (a === '--endpoint') { const v = argv[++i]; if (v !== undefined) out.endpoint = v }\n else if (a === '--yes' || a === '-y') out.yes = true\n else if (a === '--no-install') out.install = false\n else if (a === '--cwd') out.cwd = argv[++i] ?? process.cwd()\n }\n return out\n}\n\nfunction detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'\n if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'\n return 'npm'\n}\n\nexport async function runInit(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const { cwd } = args\n\n process.stdout.write(kleur.bold('\\n⚡ Mhosaic Feedback setup\\n\\n'))\n\n const framework = await detectFramework(cwd)\n process.stdout.write(kleur.gray(`Detected: ${framework.kind}${framework.entry ? ' (' + framework.entry + ')' : ''}\\n`))\n\n let apiKey = args.apiKey\n let endpoint = args.endpoint ?? 'http://localhost:8000'\n if (!args.yes && !apiKey) {\n const answers = await prompts([\n { type: 'text', name: 'endpoint', message: 'Backend endpoint', initial: endpoint },\n { type: 'text', name: 'apiKey', message: 'Public API key (pk_proj_…)', validate: (v: string) => v.startsWith('pk_proj_') || 'must start with pk_proj_' },\n ])\n if (!answers.apiKey) {\n process.stdout.write(kleur.red('Aborted.\\n'))\n process.exitCode = 1\n return\n }\n endpoint = answers.endpoint\n apiKey = answers.apiKey\n }\n if (!apiKey) {\n process.stderr.write('api-key is required (flag or prompt)\\n')\n process.exitCode = 1\n return\n }\n\n const pm = detectPackageManager(cwd)\n if (args.install) {\n process.stdout.write(kleur.gray(`Installing @mhosaic/feedback via ${pm}…\\n`))\n const cmd = pm === 'pnpm' ? ['pnpm', 'add', '@mhosaic/feedback'] : pm === 'yarn' ? ['yarn', 'add', '@mhosaic/feedback'] : ['npm', 'install', '@mhosaic/feedback']\n const res = spawnSync(cmd[0]!, cmd.slice(1), { cwd, stdio: 'inherit' })\n if (res.status !== 0) {\n process.stderr.write(kleur.red('install failed\\n'))\n process.exitCode = res.status ?? 1\n return\n }\n process.stdout.write(kleur.green('✓ Installed @mhosaic/feedback\\n'))\n }\n\n await writeEnvLocal(join(cwd, '.env.local'), { apiKey, endpoint })\n process.stdout.write(kleur.green('✓ Wrote .env.local\\n'))\n\n await writeEnvLocalExample(join(cwd, '.env.local.example'))\n process.stdout.write(kleur.green('✓ Wrote .env.local.example (commit this — your next teammate will thank you)\\n'))\n\n await ensureGitignore(join(cwd, '.gitignore'))\n process.stdout.write(kleur.green('✓ Updated .gitignore\\n'))\n\n if (framework.entry && framework.kind === 'vite-react') {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n await wrapReactEntry(entryPath, {\n apiKeyEnv: 'VITE_FEEDBACK_API_KEY',\n endpointEnv: 'VITE_FEEDBACK_ENDPOINT',\n })\n process.stdout.write(kleur.green(`✓ Wrapped ${framework.entry}\\n`))\n } else {\n process.stdout.write(kleur.yellow(`⚠ Expected entry ${framework.entry} not found — wire <FeedbackProvider> manually.\\n`))\n }\n } else if (framework.kind !== 'vite-react') {\n process.stdout.write(kleur.yellow(`⚠ Auto-wiring is only supported for Vite + React today. For ${framework.kind}, add <FeedbackProvider> manually.\\n`))\n }\n\n process.stdout.write(kleur.bold('\\nDone.\\n'))\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst MARK = '# === mhosaic-feedback ==='\n\nexport interface EnvEntries {\n apiKey: string\n endpoint: string\n}\n\n/**\n * Reject values that could escape the `KEY=value` line in a .env file.\n *\n * Audit R5/M3: the previous version interpolated user input verbatim,\n * so a paste containing `\\n` would inject arbitrary additional\n * variables, and `\"` would unbalance any later parser. The user owns\n * their own machine — this is a defense-in-depth guard against\n * accidental misuse (e.g. paste from a multi-line message) more than\n * an attack surface.\n */\nfunction ensureEnvSafe(field: 'apiKey' | 'endpoint', value: string): string {\n if (/[\\r\\n]/.test(value)) {\n throw new Error(`${field} contains a newline; refusing to write .env.local.`)\n }\n if (value.includes('\"')) {\n throw new Error(`${field} contains a double-quote; refusing to write .env.local.`)\n }\n if (value.includes('\\0')) {\n throw new Error(`${field} contains a NUL byte; refusing to write .env.local.`)\n }\n return value\n}\n\nfunction ensureKnownEndpoint(value: string): string {\n // http(s) only — a `javascript:` or `file:` URL in .env would silently\n // become the configured backend after `pnpm dev`.\n if (!/^https?:\\/\\/[^\\s]+$/i.test(value)) {\n throw new Error(\n `endpoint must be an http(s) URL with no whitespace; got ${JSON.stringify(value)}`,\n )\n }\n return value\n}\n\nfunction ensureApiKey(value: string): string {\n // Trim accidental leading/trailing whitespace — pasted keys frequently\n // arrive with a trailing newline that survives split-on-tab.\n const trimmed = value.trim()\n if (!trimmed.startsWith('pk_proj_')) {\n throw new Error(\n 'apiKey must look like a widget public key (starts with `pk_proj_`).',\n )\n }\n return trimmed\n}\n\nexport function renderEnv(existing: string, entries: EnvEntries): string {\n const apiKey = ensureEnvSafe('apiKey', ensureApiKey(entries.apiKey))\n const endpoint = ensureEnvSafe('endpoint', ensureKnownEndpoint(entries.endpoint))\n const lines = existing.split(/\\r?\\n/)\n const filtered: string[] = []\n let inBlock = false\n for (const line of lines) {\n if (line.trim() === MARK) { inBlock = !inBlock; continue }\n if (inBlock) continue\n if (line.startsWith('VITE_FEEDBACK_API_KEY=') || line.startsWith('VITE_FEEDBACK_ENDPOINT=')) continue\n filtered.push(line)\n }\n // Trim trailing empty lines\n while (filtered.length > 0 && filtered[filtered.length - 1] === '') filtered.pop()\n const block = [\n MARK,\n `VITE_FEEDBACK_API_KEY=${apiKey}`,\n `VITE_FEEDBACK_ENDPOINT=${endpoint}`,\n MARK,\n ].join('\\n')\n return (filtered.length > 0 ? filtered.join('\\n') + '\\n\\n' : '') + block + '\\n'\n}\n\nexport async function writeEnvLocal(path: string, entries: EnvEntries): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n await writeFile(path, renderEnv(existing, entries))\n}\n\n/**\n * Write a `.env.local.example` template alongside `.env.local`. This file\n * IS checked into git (it lives next to .env.local, but only .env.local\n * gets gitignored) — so the second developer joining the repo sees which\n * keys they need to set, with placeholder values, without having to ask.\n *\n * Idempotent: re-running `init` overwrites the marker block but leaves\n * any custom keys the team added between markers untouched.\n */\nexport async function writeEnvLocalExample(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n // Reuse renderEnv with placeholder values so the file shape exactly\n // matches `.env.local` (same marker, same key order, same encoding rules).\n const rendered = renderEnv(existing, {\n apiKey: 'pk_proj_REPLACE_ME_WITH_YOUR_PROJECT_KEY',\n endpoint: 'https://software-factory-3tbbu.ondigitalocean.app',\n })\n // Prepend a one-line header so a developer who opens the file knows\n // it's a template (and that .env.local is the real file).\n const header = '# Copy this file to .env.local and replace the placeholder.\\n# .env.local is gitignored; this template stays in git so the next\\n# developer onboarding the repo sees which keys are required.\\n'\n await writeFile(path, header + rendered)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst REQUIRED = ['.env.local']\n\nexport function renderGitignore(existing: string): string {\n const lines = new Set(existing.split(/\\r?\\n/).map((l) => l.trim()))\n const missing = REQUIRED.filter((entry) => !lines.has(entry))\n if (missing.length === 0) return existing\n const trailingNewline = existing.endsWith('\\n') || existing === ''\n return existing + (trailingNewline ? '' : '\\n') + missing.join('\\n') + '\\n'\n}\n\nexport async function ensureGitignore(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n const next = renderGitignore(existing)\n if (next !== existing) await writeFile(path, next)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\n\nconst IMPORT_MARK_START = '// === mhosaic-feedback:import:start ==='\nconst IMPORT_MARK_END = '// === mhosaic-feedback:import:end ==='\nconst WRAP_MARK_START = '{/* === mhosaic-feedback:wrap:start === */}'\nconst WRAP_MARK_END = '{/* === mhosaic-feedback:wrap:end === */}'\n\nexport interface WrapOptions {\n apiKeyEnv: string // 'VITE_FEEDBACK_API_KEY'\n endpointEnv: string // 'VITE_FEEDBACK_ENDPOINT'\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction buildImportBlock(): string {\n return [\n IMPORT_MARK_START,\n \"import { FeedbackProvider } from '@mhosaic/feedback/loader/react'\",\n IMPORT_MARK_END,\n ].join('\\n')\n}\n\nfunction buildWrapOpen(opts: WrapOptions): string {\n return [\n WRAP_MARK_START,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n ].join('\\n ')\n}\n\nfunction buildWrapClose(): string {\n return ['</FeedbackProvider>', WRAP_MARK_END].join('\\n ')\n}\n\nexport function transformReactEntry(source: string, opts: WrapOptions): string {\n let out = source\n\n // If markers are already present, do an in-place update of the FeedbackProvider\n // props (import block + wrap tag) without restructuring anything — this is what\n // makes re-runs idempotent without needing to parse/unwrap the JSX tree.\n const alreadyWrapped =\n out.includes(IMPORT_MARK_START) && out.includes(WRAP_MARK_START)\n\n if (alreadyWrapped) {\n // Update import block (replace old import line inside the markers)\n out = out.replace(\n new RegExp(`(${escapeRegex(IMPORT_MARK_START)}\\\\n)[^\\\\n]*(\\\\n${escapeRegex(IMPORT_MARK_END)})`),\n `$1import { FeedbackProvider } from '@mhosaic/feedback/loader/react'$2`,\n )\n // Update FeedbackProvider opening tag props\n out = out.replace(\n /<FeedbackProvider[^>]*>/,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n )\n return out\n }\n\n // Fresh insert: strip any partial markers that may exist (safety), then insert.\n out = out.replace(\n new RegExp(`${escapeRegex(IMPORT_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(IMPORT_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n out = out.replace(\n new RegExp(`${escapeRegex(WRAP_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(WRAP_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n\n // Insert import block after the last existing import statement.\n const importRegex = /^(import[^\\n]+\\n)+/m\n const importMatch = out.match(importRegex)\n if (importMatch) {\n const idx = importMatch.index! + importMatch[0].length\n out = out.slice(0, idx) + buildImportBlock() + '\\n\\n' + out.slice(idx)\n } else {\n out = buildImportBlock() + '\\n\\n' + out\n }\n\n // Wrap the App component inside the render call.\n // Pattern A: createRoot(...).render(<jsx>) — covers both single-line and multi-line.\n // Pattern B: ReactDOM.render(<jsx>, document.getElementById(...))\n const renderPatterns: Array<RegExp> = [\n /\\.render\\(\\s*([\\s\\S]*?)\\s*\\)\\s*[,;]?\\s*\\n/,\n /ReactDOM\\.render\\(\\s*([\\s\\S]*?),\\s*document\\.getElementById/,\n ]\n for (const re of renderPatterns) {\n const m = out.match(re)\n if (m && m.index !== undefined) {\n // Strip any trailing argument-list punctuation from the captured JSX —\n // `render(<App />,)` is valid JS but wrapping a trailing `,` inside the\n // FeedbackProvider children would render a literal \",\" as a text node.\n const jsx = m[1]!.replace(/[,;]\\s*$/, '')\n const wrapped = `\\n ${buildWrapOpen(opts)}\\n ${jsx.replace(/\\n/g, '\\n ')}\\n ${buildWrapClose()}\\n `\n const startIdx = m.index + m[0].indexOf(m[1]!)\n out = out.slice(0, startIdx) + wrapped + out.slice(startIdx + m[1]!.length)\n break\n }\n }\n\n return out\n}\n\nexport async function wrapReactEntry(path: string, opts: WrapOptions): Promise<void> {\n const source = await readFile(path, 'utf8')\n const next = transformReactEntry(source, opts)\n if (next !== source) await writeFile(path, next)\n}\n"],"mappings":"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","names":["existsSync","readFile","writeFile","existsSync","readFile","writeFile","existsSync"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/install-skill.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport kleur from 'kleur'\n\ninterface Args {\n force: boolean\n dryRun: boolean\n dest: string\n}\n\nfunction parseArgs(argv: string[]): Args {\n const out: Args = { force: false, dryRun: false, dest: join(homedir(), '.claude', 'skills') }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--force' || a === '-f') out.force = true\n else if (a === '--dry-run') out.dryRun = true\n else if (a === '--dest') out.dest = argv[++i] ?? out.dest\n }\n return out\n}\n\n/**\n * The CLI ships the skill directory inside the npm package. At build time\n * tsup emits dist/bin.js; at runtime we walk up to find the package root\n * and resolve `skills/` from there. This works for global installs (npx)\n * and for symlinked monorepo dev installs.\n */\nfunction findSkillsSource(): string | null {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin lives at dist/bin.js — package root is one level up\n const candidates = [\n join(here, '..', 'skills'),\n join(here, '..', '..', 'skills'),\n ]\n for (const c of candidates) {\n if (existsSync(c)) return c\n }\n return null\n}\n\nfunction copyRecursive(src: string, dest: string, force: boolean, dryRun: boolean): { copied: number; skipped: number } {\n let copied = 0\n let skipped = 0\n if (!dryRun && !existsSync(dest)) mkdirSync(dest, { recursive: true })\n for (const entry of readdirSync(src)) {\n const s = join(src, entry)\n const d = join(dest, entry)\n const st = statSync(s)\n if (st.isDirectory()) {\n const r = copyRecursive(s, d, force, dryRun)\n copied += r.copied\n skipped += r.skipped\n } else {\n if (existsSync(d) && !force) {\n skipped++\n continue\n }\n if (!dryRun) {\n if (!existsSync(dirname(d))) mkdirSync(dirname(d), { recursive: true })\n writeFileSync(d, readFileSync(s))\n }\n copied++\n }\n }\n return { copied, skipped }\n}\n\nexport async function runInstallSkill(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const src = findSkillsSource()\n\n process.stdout.write(kleur.bold('\\n📦 Installing Mhosaic Feedback Claude Code skill\\n\\n'))\n\n if (src === null) {\n process.stderr.write(kleur.red('Could not locate bundled skills/ directory.\\n'))\n process.stderr.write(kleur.gray('Reinstall the CLI: `npm i -g @mhosaic/feedback-cli@latest`\\n'))\n process.exitCode = 1\n return\n }\n\n process.stdout.write(kleur.gray(`Source: ${src}\\n`))\n process.stdout.write(kleur.gray(`Destination: ${args.dest}\\n`))\n if (args.dryRun) process.stdout.write(kleur.yellow('(dry run — no files will be written)\\n'))\n process.stdout.write('\\n')\n\n const { copied, skipped } = copyRecursive(src, args.dest, args.force, args.dryRun)\n\n process.stdout.write(kleur.green(`✓ ${copied} file(s) ${args.dryRun ? 'would be ' : ''}copied\\n`))\n if (skipped > 0) {\n process.stdout.write(kleur.yellow(`⚠ ${skipped} file(s) skipped (already exist; use --force to overwrite)\\n`))\n }\n process.stdout.write('\\n')\n process.stdout.write(kleur.bold('Next:\\n'))\n process.stdout.write(` 1. ${kleur.gray('Restart Claude Code if you have it open — skills are discovered at session start.')}\\n`)\n process.stdout.write(` 2. ${kleur.gray('Onboard a new host app: ')}${kleur.cyan('/integrate-feedback')}\\n`)\n process.stdout.write(` 3. ${kleur.gray('Add the QA Meter (test-coverage FAB) to a host app: ')}${kleur.cyan('/integrate-qa-meter')}\\n`)\n process.stdout.write(` 4. ${kleur.gray('Triage + fix reports: ')}${kleur.cyan('/feedback-pull')} ${kleur.gray('→')} ${kleur.cyan('/feedback-fix')} ${kleur.gray('→')} ${kleur.cyan('/feedback-watch-merges')} ${kleur.gray('→')} ${kleur.cyan('/feedback-close')}\\n`)\n process.stdout.write(` 5. ${kleur.gray('Design before development (chantiers): ')}${kleur.cyan('/chantier')}\\n`)\n process.stdout.write(` 6. ${kleur.gray(\"Record a meeting's feedback in each person's name: \")}${kleur.cyan('/feedback-from-meeting')}\\n`)\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/index.ts"],"sourcesContent":["export async function runQa(args: string[]): Promise<void> {\n const [sub, ...rest] = args\n const commands: Record<string, () => Promise<{ run: (a: string[]) => Promise<void> }>> = {\n sitemap: async () => ({ run: dispatchSitemap }),\n generate: async () => ({ run: (await import('./generate')).runGenerate }),\n build: async () => ({ run: (await import('./build')).runBuild }),\n check: async () => ({ run: (await import('./check')).runCheck }),\n refresh: async () => ({\n run: async (a) => {\n const { loadQaConfig } = await import('./config')\n const cfg = loadQaConfig(process.cwd())\n if (cfg.sitemap?.framework) await dispatchSitemap(a)\n await (await import('./generate')).runGenerate(a)\n await (await import('./build')).runBuild(a)\n },\n }),\n }\n const entry = commands[sub ?? '']\n if (!entry) {\n process.stderr.write('Usage: mhosaic-feedback qa <sitemap|generate|build|check|refresh> [--strict] [--version <app_version>]\\n')\n process.exitCode = 1\n return\n }\n await (await entry()).run(rest)\n}\n\n/** Route `qa sitemap` to the adapter named by config.sitemap.framework. */\nasync function dispatchSitemap(args: string[]): Promise<void> {\n const { loadQaConfig } = await import('./config')\n const framework = loadQaConfig(process.cwd()).sitemap?.framework\n if (framework === 'vue-router') return (await import('./sitemap-vue')).runSitemap(args)\n if (framework === 'react-router') return (await import('./sitemap-react')).runReactSitemap(args)\n process.stderr.write(\n `qa sitemap: unsupported framework ${framework ? `'${framework}'` : '(none set)'} ` +\n `(supported: vue-router, react-router). Set config.sitemap.framework.\\n`,\n )\n process.exitCode = 1\n}\n"],"mappings":";;;AAAA,eAAsB,MAAM,MAA+B;AACzD,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,QAAM,WAAmF;AAAA,IACvF,SAAS,aAAa,EAAE,KAAK,gBAAgB;AAAA,IAC7C,UAAU,aAAa,EAAE,MAAM,MAAM,OAAO,wBAAY,GAAG,YAAY;AAAA,IACvE,OAAO,aAAa,EAAE,MAAM,MAAM,OAAO,qBAAS,GAAG,SAAS;AAAA,IAC9D,OAAO,aAAa,EAAE,MAAM,MAAM,OAAO,qBAAS,GAAG,SAAS;AAAA,IAC9D,SAAS,aAAa;AAAA,MACpB,KAAK,OAAO,MAAM;AAChB,cAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sBAAU;AAChD,cAAM,MAAM,aAAa,QAAQ,IAAI,CAAC;AACtC,YAAI,IAAI,SAAS,UAAW,OAAM,gBAAgB,CAAC;AACnD,eAAO,MAAM,OAAO,wBAAY,GAAG,YAAY,CAAC;AAChD,eAAO,MAAM,OAAO,qBAAS,GAAG,SAAS,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACA,QAAM,QAAQ,SAAS,OAAO,EAAE;AAChC,MAAI,CAAC,OAAO;AACV,YAAQ,OAAO,MAAM,0GAA0G;AAC/H,YAAQ,WAAW;AACnB;AAAA,EACF;AACA,SAAO,MAAM,MAAM,GAAG,IAAI,IAAI;AAChC;AAGA,eAAe,gBAAgB,MAA+B;AAC5D,QAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sBAAU;AAChD,QAAM,YAAY,aAAa,QAAQ,IAAI,CAAC,EAAE,SAAS;AACvD,MAAI,cAAc,aAAc,SAAQ,MAAM,OAAO,2BAAe,GAAG,WAAW,IAAI;AACtF,MAAI,cAAc,eAAgB,SAAQ,MAAM,OAAO,6BAAiB,GAAG,gBAAgB,IAAI;AAC/F,UAAQ,OAAO;AAAA,IACb,qCAAqC,YAAY,IAAI,SAAS,MAAM,YAAY;AAAA;AAAA,EAElF;AACA,UAAQ,WAAW;AACrB;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/sitemap-react.ts"],"sourcesContent":["/**\n * react-router adapter for `qa sitemap` — STATIC AST extraction.\n *\n * Reads React Router v7 *declarative* route declarations straight from the\n * TypeScript syntax tree (no module evaluation, no vm, no React). Two readers:\n * - JSX: <Routes><Route path=… /> (e.g. apur/frontend App.tsx)\n * - object: createBrowserRouter([ { path, … } ]) (e.g. mhosaic-core router.tsx)\n * Both emit a normalized ReactRoute tree, mapped to the framework-agnostic\n * RawRoute tree consumed by sitemap-core.buildSitemap.\n *\n * Honest about its limits (spec 2026-06-15 §7): non-literal paths and\n * dynamically-composed object routes are WARNED, never silently dropped.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport ts from 'typescript'\nimport { buildSitemap, type RawRoute, type SitemapPayload } from './sitemap-core'\nimport { loadQaConfig } from './config'\n\n/** Normalized, framework-shaped route node produced by a reader. */\nexport interface ReactRoute {\n /** The route's OWN path token, as authored ('app', 'reports/:id', '/login'). */\n path?: string\n /** `<Route index/>` or `{ index: true }`. */\n index?: boolean\n /** The route's `element` is a root `<Navigate/>` (a redirect). */\n isNavigate?: boolean\n children?: ReactRoute[]\n /** Set when the reader saw a node it could not statically resolve. */\n unresolved?: string\n}\n\n// ───────────────────────────────── mapping (§4) ─────────────────────────────\n\n/** True for a react-router catch-all/splat path ('*' or '…/*'). */\nexport function isCatchAllPath(path: string | undefined): boolean {\n return path === '*' || path === '/*' || (path !== undefined && path.endsWith('/*'))\n}\n\n/** Last '/'-segment of a path token; '' / '/' / undefined → 'home'. */\nexport function deriveName(pathToken: string | undefined): string {\n const segs = (pathToken ?? '').split('/').filter(Boolean)\n return segs.at(-1) ?? 'home'\n}\n\n/**\n * Map a normalized ReactRoute tree to the RawRoute tree + collect honesty\n * warnings. Rules (spec §4): drop catch-all / `<Navigate>`-only; `index` → empty\n * path with the parent's derived name; pathless layout (no path, has children) →\n * no name (core marks it `structural`); real pages get a path-derived name.\n */\nexport function mapReactRoutes(roots: ReadonlyArray<ReactRoute>): {\n routes: RawRoute[]\n warnings: string[]\n} {\n const warnings: string[] = []\n\n const visit = (node: ReactRoute, parentPath: string | undefined): RawRoute | null => {\n if (node.unresolved) {\n warnings.push(\n `a route could not be statically resolved (${node.unresolved}) — its subtree is missing from the sitemap`,\n )\n return null\n }\n if (isCatchAllPath(node.path)) return null\n const hasChildren = !!node.children && node.children.length > 0\n if (node.isNavigate && !hasChildren) return null\n\n const childRaws: RawRoute[] = []\n for (const child of node.children ?? []) {\n // index children are named from the nearest pathed ancestor; pass our own\n // path down (undefined for a pathless layout).\n const mapped = visit(child, node.index ? parentPath : node.path)\n if (mapped) childRaws.push(mapped)\n }\n\n // A degenerate node (no path, not index, no surviving children) contributes nothing.\n if (!node.index && node.path === undefined && childRaws.length === 0) return null\n\n const raw: RawRoute = {}\n if (node.index) {\n raw.path = ''\n raw.name = deriveName(parentPath)\n } else if (node.path !== undefined) {\n raw.path = node.path\n raw.name = deriveName(node.path)\n }\n // else: pathless layout → leave path & name undefined (structural in core).\n if (childRaws.length) raw.children = childRaws\n return raw\n }\n\n const routes: RawRoute[] = []\n for (const node of roots) {\n const mapped = visit(node, undefined)\n if (mapped) routes.push(mapped)\n }\n return { routes, warnings }\n}\n\n// ─────────────────────────────────── AST helpers ────────────────────────────\n\ninterface RouterLocals { route?: string; routes?: string; navigate?: string }\n\n/** Local identifiers bound to react-router(-dom) `Route`/`Routes`/`Navigate`. */\nfunction routerLocalNames(sf: ts.SourceFile): RouterLocals {\n const out: RouterLocals = {}\n for (const st of sf.statements) {\n if (!ts.isImportDeclaration(st)) continue\n const spec = st.moduleSpecifier\n if (!ts.isStringLiteral(spec)) continue\n if (spec.text !== 'react-router' && spec.text !== 'react-router-dom') continue\n const named = st.importClause?.namedBindings\n if (!named || !ts.isNamedImports(named)) continue\n for (const el of named.elements) {\n const imported = (el.propertyName ?? el.name).text\n if (imported === 'Route') out.route = el.name.text\n else if (imported === 'Routes') out.routes = el.name.text\n else if (imported === 'Navigate') out.navigate = el.name.text\n }\n }\n return out\n}\n\n/** Static string value of a JSX attribute initializer, or null if non-literal. */\nfunction jsxStringAttr(init: ts.JsxAttributeValue | undefined): string | null {\n if (!init) return null\n if (ts.isStringLiteral(init)) return init.text\n if (ts.isJsxExpression(init) && init.expression) {\n const e = init.expression\n if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text\n }\n return null\n}\n\n/** Root JSX tag name of an `element={…}` attribute initializer, or null. */\nfunction jsxElementRootTag(init: ts.JsxAttributeValue | undefined, sf: ts.SourceFile): string | null {\n if (!init || !ts.isJsxExpression(init) || !init.expression) return null\n return jsxRootTag(init.expression, sf)\n}\n\n/** Root JSX tag of an expression (unwrapping parentheses); null if not JSX. */\nfunction jsxRootTag(expr: ts.Expression, sf: ts.SourceFile): string | null {\n let e: ts.Expression = expr\n while (ts.isParenthesizedExpression(e)) e = e.expression\n if (ts.isJsxElement(e)) return e.openingElement.tagName.getText(sf)\n if (ts.isJsxSelfClosingElement(e)) return e.tagName.getText(sf)\n return null\n}\n\n// ─────────────────────────────────── JSX reader ─────────────────────────────\n\n/** Parse `<Routes><Route …/>` JSX into a ReactRoute tree. */\nexport function readJsxRoutes(sf: ts.SourceFile): { roots: ReactRoute[]; warnings: string[] } {\n const warnings: string[] = []\n const local = routerLocalNames(sf)\n\n const isTag = (n: ts.Node, name: string | undefined): boolean => {\n if (!name) return false\n if (ts.isJsxElement(n)) return n.openingElement.tagName.getText(sf) === name\n if (ts.isJsxSelfClosingElement(n)) return n.tagName.getText(sf) === name\n return false\n }\n const attrsOf = (n: ts.JsxElement | ts.JsxSelfClosingElement): ts.NodeArray<ts.JsxAttributeLike> =>\n ts.isJsxElement(n) ? n.openingElement.attributes.properties : n.attributes.properties\n\n const readRoute = (n: ts.JsxElement | ts.JsxSelfClosingElement): ReactRoute => {\n const route: ReactRoute = {}\n for (const a of attrsOf(n)) {\n if (!ts.isJsxAttribute(a)) continue\n const name = a.name.getText(sf)\n if (name === 'index') route.index = true\n else if (name === 'path') {\n const v = jsxStringAttr(a.initializer)\n if (v === null) route.unresolved = 'dynamic-path'\n else route.path = v\n } else if (name === 'element') {\n if (jsxElementRootTag(a.initializer, sf) === local.navigate) route.isNavigate = true\n }\n }\n if (ts.isJsxElement(n)) {\n const kids: ReactRoute[] = []\n for (const child of n.children) {\n if (isTag(child, local.route)) kids.push(readRoute(child as ts.JsxElement | ts.JsxSelfClosingElement))\n }\n if (kids.length) route.children = kids\n }\n return route\n }\n\n const roots: ReactRoute[] = []\n const visit = (n: ts.Node): void => {\n if (isTag(n, local.routes) && ts.isJsxElement(n)) {\n for (const child of n.children) {\n if (isTag(child, local.route)) roots.push(readRoute(child as ts.JsxElement | ts.JsxSelfClosingElement))\n }\n return // consumed this <Routes> subtree\n }\n ts.forEachChild(n, visit)\n }\n visit(sf)\n\n if (!roots.length) warnings.push('no <Routes>/<Route> elements found')\n return { roots, warnings }\n}\n\n// ─────────────────────────────────── object reader ──────────────────────────\n\nconst ROUTER_CREATORS = new Set([\n 'createBrowserRouter', 'createMemoryRouter', 'createHashRouter', 'useRoutes',\n])\n\n/** Property-name text of an object-literal key. */\nfunction propName(name: ts.PropertyName, sf: ts.SourceFile): string {\n if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text\n return name.getText(sf)\n}\n\n/** Static string value of an expression, or null if non-literal. */\nfunction literalString(expr: ts.Expression): string | null {\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text\n return null\n}\n\n/** One array element → a ReactRoute (or an `unresolved` marker). */\nfunction objectToReactRoute(\n el: ts.Expression,\n navigateName: string | undefined,\n sf: ts.SourceFile,\n): ReactRoute {\n if (ts.isSpreadElement(el)) return { unresolved: 'spread' }\n if (!ts.isObjectLiteralExpression(el)) return { unresolved: 'dynamic' }\n\n const route: ReactRoute = {}\n for (const prop of el.properties) {\n if (!ts.isPropertyAssignment(prop)) continue\n const key = propName(prop.name, sf)\n if (key === 'path') {\n const v = literalString(prop.initializer)\n if (v === null) route.unresolved = 'dynamic-path'\n else route.path = v\n } else if (key === 'index') {\n if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) route.index = true\n } else if (key === 'element') {\n if (jsxRootTag(prop.initializer, sf) === navigateName) route.isNavigate = true\n } else if (key === 'children') {\n if (ts.isArrayLiteralExpression(prop.initializer)) {\n route.children = prop.initializer.elements.map((c) => objectToReactRoute(c, navigateName, sf))\n } else {\n route.unresolved = 'dynamic-children'\n }\n }\n }\n return route\n}\n\n/** Parse `createBrowserRouter([…])` / a `routes` array into a ReactRoute tree. */\nexport function readDataRoutes(sf: ts.SourceFile): { roots: ReactRoute[]; warnings: string[] } {\n const warnings: string[] = []\n const navigateName = routerLocalNames(sf).navigate\n\n let array: ts.ArrayLiteralExpression | undefined\n const findArray = (n: ts.Node): void => {\n if (array) return\n if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && ROUTER_CREATORS.has(n.expression.text)) {\n const arg0 = n.arguments[0]\n if (arg0 && ts.isArrayLiteralExpression(arg0)) {\n array = arg0\n return\n }\n }\n ts.forEachChild(n, findArray)\n }\n findArray(sf)\n\n if (!array) {\n for (const st of sf.statements) {\n if (!ts.isVariableStatement(st)) continue\n for (const d of st.declarationList.declarations) {\n const init = d.initializer\n if (ts.isIdentifier(d.name) && d.name.text === 'routes' && init && ts.isArrayLiteralExpression(init)) {\n array = init\n }\n }\n }\n }\n\n if (!array) {\n warnings.push('no createBrowserRouter([…]) call or `routes` array found')\n return { roots: [], warnings }\n }\n\n const roots = array.elements.map((el) => objectToReactRoute(el, navigateName, sf))\n return { roots, warnings }\n}\n\n// ─────────────────────────────────── extractor ──────────────────────────────\n\nexport interface ReactRouterAdapterOptions {\n /** The file holding the route tree (absolute). Preferred. */\n entry?: string\n /** Dir to scan for the entry when `entry` is omitted (absolute). */\n routerDir?: string\n /** Force a reader; otherwise auto-detect from the parsed file. */\n routerStyle?: 'jsx' | 'data'\n}\n\ninterface ExtractResult { payload: SitemapPayload; warnings: string[] }\n\nconst ROUTE_FILE_SIGNALS = /createBrowserRouter|createMemoryRouter|createHashRouter|useRoutes|<Routes\\b/\n\n/** Recursively list candidate router files under `dir` (sorted; tests excluded). */\nfunction scanForRouteFiles(dir: string): string[] {\n const out: string[] = []\n const walk = (d: string): void => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = readdirSync(d, { withFileTypes: true })\n } catch {\n return\n }\n for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n if (e.name === 'node_modules' || e.name.startsWith('.')) continue\n const full = join(d, e.name)\n if (e.isDirectory()) {\n walk(full)\n } else if (/\\.(tsx|jsx|ts)$/.test(e.name) && !/\\.(test|spec)\\.[tj]sx?$/.test(e.name)) {\n try {\n if (ROUTE_FILE_SIGNALS.test(readFileSync(full, 'utf8'))) out.push(full)\n } catch {\n // ignore unreadable files\n }\n }\n }\n }\n walk(dir)\n return out\n}\n\n/** Resolve the entry file from opts, or null (with a warning). */\nfunction resolveEntry(opts: ReactRouterAdapterOptions, warnings: string[]): string | null {\n if (opts.entry) {\n if (existsSync(opts.entry) && statSync(opts.entry).isFile()) return opts.entry\n warnings.push(`entry file not found: ${opts.entry}`)\n return null\n }\n if (!opts.routerDir) {\n warnings.push('react-router adapter needs `entry` or `routerDir` in config.sitemap')\n return null\n }\n const candidates = scanForRouteFiles(opts.routerDir)\n const first = candidates[0]\n if (!first) {\n warnings.push(`no router file (with <Routes> or createBrowserRouter) under ${opts.routerDir}`)\n return null\n }\n if (candidates.length > 1) {\n warnings.push(\n `multiple candidate router files under ${opts.routerDir}; using ${first} — set sitemap.entry to disambiguate`,\n )\n }\n return first\n}\n\n/** Auto-detect the reader from the parsed file: a creator call ⇒ 'data', else 'jsx'. */\nfunction detectStyle(sf: ts.SourceFile): 'jsx' | 'data' {\n let isData = false\n const visit = (n: ts.Node): void => {\n if (isData) return\n if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && ROUTER_CREATORS.has(n.expression.text)) {\n isData = true\n return\n }\n ts.forEachChild(n, visit)\n }\n visit(sf)\n return isData ? 'data' : 'jsx'\n}\n\n/** Internal worker: payload + honesty/IO warnings. */\nexport function extractWithWarnings(opts: ReactRouterAdapterOptions): ExtractResult {\n const warnings: string[] = []\n const entry = resolveEntry(opts, warnings)\n if (!entry) return { payload: { format: 1, pages: [] }, warnings }\n\n const source = readFileSync(entry, 'utf8')\n const sf = ts.createSourceFile(entry, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)\n\n const style = opts.routerStyle ?? detectStyle(sf)\n const read = style === 'data' ? readDataRoutes(sf) : readJsxRoutes(sf)\n warnings.push(...read.warnings)\n\n const mapped = mapReactRoutes(read.roots)\n warnings.push(...mapped.warnings)\n\n return { payload: buildSitemap(mapped.routes), warnings }\n}\n\n/** Statically extract the react-router sitemap payload (idempotent). */\nexport function extractReactRouterSitemap(opts: ReactRouterAdapterOptions): SitemapPayload {\n return extractWithWarnings(opts).payload\n}\n\n// ─────────────────────────────────── CLI entry ──────────────────────────────\n\n/** CLI: `qa sitemap` for react-router — load config, extract, write, report. */\nexport async function runReactSitemap(_args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n if (!cfg.sitemap || cfg.sitemap.framework !== 'react-router') {\n process.stderr.write(\n 'qa sitemap: config.sitemap.framework must be \"react-router\". ' +\n 'Add a `sitemap` block with `framework: \"react-router\"`, `entry` (or `routerDir`), and `file`.\\n',\n )\n process.exitCode = 1\n return\n }\n if (!cfg.sitemap.entry && !cfg.sitemap.routerDir) {\n process.stderr.write('qa sitemap: react-router adapter needs `entry` (or `routerDir`) in config.sitemap.\\n')\n process.exitCode = 1\n return\n }\n\n const { payload, warnings } = extractWithWarnings({\n ...(cfg.sitemap.entry ? { entry: cfg.sitemap.entry } : {}),\n ...(cfg.sitemap.routerDir ? { routerDir: cfg.sitemap.routerDir } : {}),\n ...(cfg.sitemap.routerStyle ? { routerStyle: cfg.sitemap.routerStyle } : {}),\n })\n\n const outFile = cfg.sitemap.file\n mkdirSync(dirname(outFile), { recursive: true })\n writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\\n`, 'utf8')\n\n process.stderr.write(`qa sitemap → ${outFile} (${payload.pages.length} pages)\\n`)\n for (const warning of warnings) process.stderr.write(` warning: ${warning}\\n`)\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/sitemap-vue.ts"],"sourcesContent":["/**\n * vue-router adapter for `qa sitemap` (Track A3, generalized from the Vigilance\n * donor `scripts/sync-sitemap.mjs`).\n *\n * Statically evaluates the router declaration files (`routes` export) without\n * booting the app, real `.vue`/store imports, or dev deps (`vite-node`/`tsx`).\n *\n * How: each source file is transpiled ESM/TS → CommonJS with `ts.transpileModule`\n * (the real TS compiler — handles default/named/re-exports, casts, multi-line\n * imports, every syntax for free), then evaluated in a `node:vm` context with a\n * `module`/`exports` sandbox and a custom `require`. The custom require:\n * 1. resolves IN-TREE relative/aliased imports (`./x`, `../x`, `@/x`) to a real\n * file under the router tree and evaluates THAT module recursively (cached by\n * absolute path; cycles return the in-progress `exports`), so sibling route\n * modules contribute their REAL exported arrays — embedded + spread survive;\n * 2. returns a harmless callable STUB Proxy for everything else (`.vue`\n * components, `vue-router`, unresolved aliases, bare/node_modules specifiers)\n * so `component:`/library references never explode.\n *\n * The honesty pass then scans the resolved `routes` tree: any top-level entry that\n * resolved to a STUB (a silently-truncated subtree) is surfaced as a warning.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\nimport vm from 'node:vm'\nimport ts from 'typescript'\nimport { buildSitemap, type RawRoute, type SitemapPayload } from './sitemap-core'\nimport { loadQaConfig } from './config'\n\nexport interface VueRouterAdapterOptions {\n /** Directory holding the router declaration file(s). Absolute. */\n routerDir: string\n /** Optional locale/title JSON; `pages.<routeName>` keys resolve human titles. Absolute. */\n titlesFrom?: string\n /**\n * Optional import-alias map (e.g. `{ '@': '/abs/path/to/src' }`). When a\n * specifier starts with an alias key + '/', it resolves to the mapped absolute\n * base before the in-tree check. Default: none (so `@/…` falls through to the\n * stub).\n */\n alias?: Record<string, string>\n}\n\n/**\n * Replacement value for any non-relevant import (`.vue` components, stores,\n * enums, `vue-router`, lodash…). Callable, constructable, indexable — and never\n * actually read by the builder, which only walks `path`/`name`/`children`.\n * Lifted verbatim from the donor.\n */\nconst STUB: unknown = new Proxy(function stub() {}, {\n get: (_t, prop) => (prop === Symbol.toPrimitive || prop === Symbol.toStringTag ? () => '' : STUB),\n apply: () => STUB,\n construct: () => ({}),\n})\n\n/** True if `v` is the shared STUB Proxy. */\nfunction isStub(v: unknown): boolean {\n return v === STUB\n}\n\n/** Transpile one TS/ESM source to CommonJS via the real compiler (no type-check). */\nfunction transpileToCjs(source: string, fileName: string): string {\n return ts.transpileModule(source, {\n fileName,\n compilerOptions: {\n module: ts.ModuleKind.CommonJS,\n target: ts.ScriptTarget.ES2020,\n esModuleInterop: true,\n isolatedModules: true,\n },\n }).outputText\n}\n\n/**\n * Resolve an import specifier to an absolute file UNDER the router tree, or null\n * if it is out-of-tree / unresolvable (→ caller stubs it).\n *\n * - relative `./x`, `../x` → resolved against the importing file's dir;\n * - aliased `@/x` (when `alias` maps `@`) → resolved against the mapped base;\n * - bare / node_modules / `.vue` / unmapped alias → null.\n *\n * Candidate extensions tried in order: as-is, `.ts`, `.mjs`, `.js`, `/index.ts`,\n * `/index.mjs`, `/index.js`. `.vue` and non-JS/TS asset specifiers (`.json`,\n * `.css`, images, …) are always treated as out-of-tree: the evaluator only\n * transpiles JS/TS, so feeding it raw JSON crashes `ts.transpileModule`\n * (\"Output generation failed\"). Routes never depend on such assets\n * structurally, so stubbing them is the honest, lossless choice.\n */\nfunction resolveInTree(\n specifier: string,\n fromDir: string,\n alias: Record<string, string> | undefined,\n confineDir: string,\n): string | null {\n if (/\\.(vue|json|css|scss|sass|less|svg|png|jpe?g|gif|webp|woff2?|ttf)$/i.test(specifier)) {\n return null\n }\n\n let base: string | null = null\n if (specifier.startsWith('./') || specifier.startsWith('../')) {\n base = resolve(fromDir, specifier)\n } else if (alias) {\n for (const [key, target] of Object.entries(alias)) {\n if (specifier === key || specifier.startsWith(`${key}/`)) {\n const rest = specifier === key ? '' : specifier.slice(key.length + 1)\n base = rest ? join(target, rest) : target\n break\n }\n }\n }\n if (base === null) return null\n\n const candidates = [\n base,\n `${base}.ts`,\n `${base}.mjs`,\n `${base}.js`,\n join(base, 'index.ts'),\n join(base, 'index.mjs'),\n join(base, 'index.js'),\n ]\n for (const cand of candidates) {\n try {\n if (existsSync(cand) && statSync(cand).isFile()) {\n // Confine the follow to the router subtree. Alias maps (`@`) point at\n // the WHOLE `src` tree, so without this an `@/stores/…` or `@/plugins/…`\n // import in a route module would drag the adapter into the app graph —\n // Vite-only code (`import.meta`), real stores, services — that crashes\n // the vm sandbox and is irrelevant to route SHAPE. Route MODULES live\n // under routerDir; everything else (components, stores, models) is a\n // leaf reference the builder never reads, so stubbing it is lossless.\n return isWithin(confineDir, cand) ? cand : null\n }\n } catch {\n // ignore and try the next candidate\n }\n }\n return null\n}\n\n/** True if absolute `child` is `dir` itself or nested under it (path-segment safe). */\nfunction isWithin(dir: string, child: string): boolean {\n const d = resolve(dir)\n const c = resolve(child)\n return c === d || c.startsWith(d.endsWith('/') ? d : `${d}/`)\n}\n\n/**\n * Evaluate a CommonJS-shaped module in a `vm` context, recursively following\n * in-tree `require`s. `cache` keys by absolute path; a module already in-flight\n * (cycle) returns its in-progress `exports`.\n */\nfunction evaluateModule(\n absPath: string,\n alias: Record<string, string> | undefined,\n cache: Map<string, Record<string, unknown>>,\n confineDir: string,\n): Record<string, unknown> {\n const cached = cache.get(absPath)\n if (cached) return cached\n\n const moduleObj: { exports: Record<string, unknown> } = { exports: {} }\n // Register BEFORE evaluating so a cycle resolves to the in-progress exports.\n cache.set(absPath, moduleObj.exports)\n\n const source = readFileSync(absPath, 'utf8')\n const cjs = transpileToCjs(source, absPath)\n const fromDir = dirname(absPath)\n\n const requireFn = (specifier: string): unknown => {\n const resolved = resolveInTree(specifier, fromDir, alias, confineDir)\n if (resolved) {\n const exp = evaluateModule(resolved, alias, cache, confineDir)\n // After evaluating, the module object may have been reassigned\n // (`module.exports = …`); read the freshest value from cache.\n return cache.get(resolved) ?? exp\n }\n return STUB\n }\n\n const context = vm.createContext({\n module: moduleObj,\n exports: moduleObj.exports,\n require: requireFn,\n console,\n })\n vm.runInContext(cjs, context, { filename: absPath })\n\n // `module.exports = …` may have replaced the object; refresh the cache entry.\n cache.set(absPath, moduleObj.exports)\n return moduleObj.exports\n}\n\n/** Locate the router-tree ENTRY file: `<dir>/index.{ts,mjs,js}`, else the first scanned file with a `routes`/`default` array. */\nfunction findEntry(dir: string): string | null {\n for (const name of ['index.ts', 'index.mjs', 'index.js']) {\n const p = join(dir, name)\n if (existsSync(p) && statSync(p).isFile()) return p\n }\n // Fallback: scan recursively, return the first file exporting routes.\n const cache = new Map<string, Record<string, unknown>>()\n for (const file of routerFiles(dir)) {\n try {\n const ns = evaluateModule(file, undefined, cache, dir)\n if (pickRoutes(ns)) return file\n } catch {\n // ignore unreadable candidates during discovery\n }\n }\n return null\n}\n\n/** List candidate router declaration files under `dir` (recursively, sorted). */\nfunction routerFiles(dir: string): string[] {\n const out: string[] = []\n const walk = (d: string): void => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = readdirSync(d, { withFileTypes: true, encoding: 'utf8' })\n } catch {\n return\n }\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const full = join(d, entry.name)\n if (entry.isDirectory()) {\n walk(full)\n } else if (/\\.(ts|mjs|js)$/.test(entry.name)) {\n out.push(full)\n }\n }\n }\n walk(dir)\n return out\n}\n\n/** Pull the `routes` array out of an evaluated namespace (named export, then default). */\nfunction pickRoutes(ns: Record<string, unknown>): ReadonlyArray<unknown> | null {\n if (Array.isArray(ns.routes)) return ns.routes as ReadonlyArray<unknown>\n if (Array.isArray(ns.default)) return ns.default as ReadonlyArray<unknown>\n return null\n}\n\n/**\n * Flatten the raw `routes` array: a sibling route MODULE imported as a single\n * embedded element resolves to an ARRAY (`routes: [ adminRoutes, … ]` where\n * `adminRoutes` is `[{…}]`), so we splice nested arrays up one level. Honesty\n * warnings are collected for any top-level entry that is the STUB (a silently\n * truncated import) or otherwise not a plain route object.\n */\nfunction flattenAndCheck(\n raw: ReadonlyArray<unknown>,\n warnings: string[],\n): RawRoute[] {\n const out: RawRoute[] = []\n for (const entry of raw) {\n if (Array.isArray(entry)) {\n // Embedded in-tree route module (its default/named export is an array).\n out.push(...flattenAndCheck(entry, warnings))\n continue\n }\n if (isStub(entry)) {\n warnings.push(\n 'a top-level route entry resolved to a STUB (an import could not be ' +\n 'followed in-tree) — its subtree is missing from the sitemap',\n )\n continue\n }\n if (entry === null || typeof entry !== 'object') {\n warnings.push(\n `a top-level route entry is not a route object (got ${entry === null ? 'null' : typeof entry}) — skipped`,\n )\n continue\n }\n out.push(entry as RawRoute)\n }\n return out\n}\n\n/** Build a `titleFor` resolver from a flat `pages.<routeName>` JSON, or undefined. */\nfunction makeTitleFor(\n titlesFrom: string | undefined,\n): ((routeName: string | undefined, pattern: string) => string | undefined) | undefined {\n if (!titlesFrom || !existsSync(titlesFrom)) return undefined\n let messages: Record<string, unknown> = {}\n try {\n const parsed = JSON.parse(readFileSync(titlesFrom, 'utf8'))\n if (parsed && typeof parsed === 'object') messages = parsed as Record<string, unknown>\n } catch {\n return undefined\n }\n const pages =\n messages.pages && typeof messages.pages === 'object'\n ? (messages.pages as Record<string, unknown>)\n : messages\n return (routeName) => {\n if (!routeName) return undefined\n const v = pages[routeName]\n return typeof v === 'string' ? v : undefined\n }\n}\n\n/** Result of a static extraction: the payload plus any honesty/IO warnings. */\ninterface ExtractResult {\n payload: SitemapPayload\n warnings: string[]\n}\n\n/**\n * Internal worker that also reports warnings (consumed by `runSitemap`).\n * Exported for tests — the public extractor (`extractVueRouterSitemap`) returns\n * only the `SitemapPayload`; this exposes the honesty warnings alongside it.\n * @internal\n */\nexport function extractWithWarnings(opts: VueRouterAdapterOptions): ExtractResult {\n const warnings: string[] = []\n const dir = isAbsolute(opts.routerDir) ? opts.routerDir : resolve(opts.routerDir)\n const entry = findEntry(dir)\n\n let rawRoutes: ReadonlyArray<unknown> | null = null\n if (!entry) {\n warnings.push(`No router entry (index.{ts,mjs,js}) found under ${dir}`)\n } else {\n try {\n const cache = new Map<string, Record<string, unknown>>()\n const ns = evaluateModule(entry, opts.alias, cache, dir)\n rawRoutes = pickRoutes(ns)\n if (!rawRoutes) {\n warnings.push(`No \\`routes\\` export found in ${entry}`)\n }\n } catch (err) {\n warnings.push(`${entry}: ${err instanceof Error ? err.message : String(err)}`)\n }\n }\n\n const routes = rawRoutes ? flattenAndCheck(rawRoutes, warnings) : []\n const titleFor = makeTitleFor(opts.titlesFrom)\n const payload = buildSitemap(routes, titleFor ? { titleFor } : {})\n return { payload, warnings }\n}\n\n/**\n * Statically evaluate the vue-router declarations under `routerDir` and return\n * the doc-03 §3.1 sitemap payload. Pure with respect to the inputs (idempotent).\n */\nexport function extractVueRouterSitemap(opts: VueRouterAdapterOptions): SitemapPayload {\n return extractWithWarnings(opts).payload\n}\n\n/** CLI entry: `qa sitemap` — load config, extract, write pretty JSON, report. */\nexport async function runSitemap(_args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n if (!cfg.sitemap || cfg.sitemap.framework !== 'vue-router') {\n process.stderr.write(\n 'qa sitemap: config.sitemap.framework must be \"vue-router\". ' +\n 'Add a `sitemap` block with `framework: \"vue-router\"`, `routerDir`, and `file` to qa-meter.config.json.\\n',\n )\n process.exitCode = 1\n return\n }\n if (!cfg.sitemap.routerDir) {\n process.stderr.write('qa sitemap: config.sitemap.routerDir is required for the vue-router adapter.\\n')\n process.exitCode = 1\n return\n }\n\n const { payload, warnings } = extractWithWarnings({\n routerDir: cfg.sitemap.routerDir,\n ...(cfg.sitemap.titlesFrom ? { titlesFrom: cfg.sitemap.titlesFrom } : {}),\n ...(cfg.sitemap.alias ? { alias: cfg.sitemap.alias } : {}),\n })\n\n const outFile = cfg.sitemap.file\n mkdirSync(dirname(outFile), { recursive: true })\n writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\\n`, 'utf8')\n\n process.stderr.write(`qa sitemap → ${outFile} (${payload.pages.length} pages)\\n`)\n for (const warning of warnings) {\n process.stderr.write(` warning: ${warning}\\n`)\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/verify.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport kleur from 'kleur'\n\ninterface VerifyArgs {\n cwd: string\n withTestReport: boolean\n origin?: string\n}\n\nfunction parseArgs(argv: string[]): VerifyArgs {\n const out: VerifyArgs = { cwd: process.cwd(), withTestReport: false }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--cwd') out.cwd = argv[++i] ?? process.cwd()\n else if (a === '--with-test-report') out.withTestReport = true\n else if (a === '--origin') { const v = argv[++i]; if (v !== undefined) out.origin = v }\n }\n return out\n}\n\ninterface EnvFields {\n apiKey: string | null\n endpoint: string | null\n}\n\nfunction readEnvLocal(cwd: string): EnvFields {\n const path = join(cwd, '.env.local')\n if (!existsSync(path)) return { apiKey: null, endpoint: null }\n const text = readFileSync(path, 'utf8')\n let apiKey: string | null = null\n let endpoint: string | null = null\n for (const line of text.split(/\\r?\\n/)) {\n const m = /^([A-Z0-9_]+)=(.*)$/.exec(line)\n if (!m) continue\n if (m[1] === 'VITE_FEEDBACK_API_KEY') apiKey = m[2]!.trim()\n else if (m[1] === 'VITE_FEEDBACK_ENDPOINT') endpoint = m[2]!.trim()\n }\n return { apiKey, endpoint }\n}\n\ninterface CheckResult {\n name: string\n ok: boolean\n detail?: string | undefined\n hint?: string | undefined\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const controller = new AbortController()\n const t = setTimeout(() => controller.abort(), timeoutMs)\n try {\n return await fetch(url, { ...init, signal: controller.signal })\n } finally {\n clearTimeout(t)\n }\n}\n\nexport async function runVerify(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const checks: CheckResult[] = []\n\n process.stdout.write(kleur.bold('\\n🔍 Mhosaic Feedback verify\\n\\n'))\n\n // 1. .env.local exists\n const env = readEnvLocal(args.cwd)\n checks.push({\n name: '.env.local exists with both keys',\n ok: env.apiKey !== null && env.endpoint !== null,\n hint: 'run `mhosaic-feedback init`',\n })\n\n if (env.apiKey === null || env.endpoint === null) {\n report(checks)\n return\n }\n\n // 2. key format\n checks.push({\n name: 'API key starts with pk_proj_',\n ok: env.apiKey.startsWith('pk_proj_') && env.apiKey.length >= 16,\n detail: env.apiKey.length < 16 ? 'suspiciously short' : undefined,\n hint: 'mint a new key in the admin SPA (Project keys page)',\n })\n\n // 3. endpoint format\n let endpointUrl: URL | null = null\n try {\n endpointUrl = new URL(env.endpoint)\n } catch {\n /* falls through */\n }\n checks.push({\n name: 'Endpoint is a valid http(s) URL',\n ok: endpointUrl !== null && /^https?:$/.test(endpointUrl.protocol),\n detail: endpointUrl ? `${endpointUrl.origin}` : undefined,\n hint: 'should look like https://your-backend.example.com',\n })\n if (endpointUrl === null || !/^https?:$/.test(endpointUrl.protocol)) {\n report(checks)\n return\n }\n\n // 4. endpoint reachable (HEAD root or GET /admin/login/ which always exists)\n let reachable = false\n let reachableDetail = ''\n try {\n const res = await fetchWithTimeout(endpointUrl.origin + '/admin/login/', { method: 'GET' }, 5000)\n reachable = res.status > 0\n reachableDetail = `HTTP ${res.status}`\n } catch (err) {\n reachableDetail = err instanceof Error ? err.message : String(err)\n }\n checks.push({\n name: 'Endpoint reachable over network',\n ok: reachable,\n detail: reachableDetail,\n hint: 'check DNS, firewall, and that the backend is running',\n })\n\n // 5. CORS preflight from the consumer's origin\n // Default origin: from --origin flag, or guessed dev server (http://localhost:5173 — Vite default)\n const origin = args.origin ?? 'http://localhost:5173'\n let corsOk = false\n let corsDetail = ''\n try {\n const res = await fetchWithTimeout(\n endpointUrl.origin + '/api/feedback/v1/reports/',\n {\n method: 'OPTIONS',\n headers: {\n 'Origin': origin,\n 'Access-Control-Request-Method': 'POST',\n 'Access-Control-Request-Headers': 'authorization,content-type',\n },\n },\n 5000,\n )\n const allowOrigin = res.headers.get('access-control-allow-origin')\n corsOk = res.status >= 200 && res.status < 300 && (allowOrigin === origin || allowOrigin === '*')\n corsDetail = allowOrigin ? `allow-origin: ${allowOrigin}` : `HTTP ${res.status}, no allow-origin header`\n } catch (err) {\n corsDetail = err instanceof Error ? err.message : String(err)\n }\n checks.push({\n name: `CORS allows POST from ${origin}`,\n ok: corsOk,\n detail: corsDetail,\n hint: 'add this origin to the project\\'s allowed_origins (admin SPA: Edit project)',\n })\n\n // 6. Optional destructive check: submit a synthetic test report\n if (args.withTestReport) {\n let testReportOk = false\n let testReportDetail = ''\n try {\n const body = new URLSearchParams({\n description: `[mhosaic-feedback verify] CLI smoke test at ${new Date().toISOString()}`,\n feedback_type: 'feature',\n severity: 'low',\n page_url: origin,\n user_agent: `mhosaic-feedback-cli`,\n env: 'dev',\n })\n const res = await fetchWithTimeout(\n endpointUrl.origin + '/api/feedback/v1/reports/',\n {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${env.apiKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Origin': origin,\n },\n body: body.toString(),\n },\n 10_000,\n )\n let json: unknown = null\n try { json = await res.json() } catch { /* non-JSON body */ }\n const reportId = json && typeof json === 'object' && 'id' in json ? (json as { id: string }).id : undefined\n testReportOk = res.status === 201 && Boolean(reportId)\n testReportDetail = testReportOk\n ? `submitted (report id: ${reportId}) — delete it from /reports if you don't want noise`\n : `HTTP ${res.status} — ${typeof json === 'object' ? JSON.stringify(json) : '(no body)'}`\n } catch (err) {\n testReportDetail = err instanceof Error ? err.message : String(err)\n }\n checks.push({\n name: 'POST /api/feedback/v1/reports/ accepts the public key',\n ok: testReportOk,\n detail: testReportDetail,\n hint: '401 = key invalid or revoked; 403 = origin not allowed; 4xx = serializer rejected payload',\n })\n }\n\n report(checks)\n}\n\nfunction report(checks: CheckResult[]): void {\n let failed = 0\n for (const c of checks) {\n const icon = c.ok ? kleur.green('✓') : kleur.red('✗')\n const detail = c.detail ? kleur.gray(` (${c.detail})`) : ''\n const hint = !c.ok && c.hint ? kleur.gray(` — ${c.hint}`) : ''\n process.stdout.write(`${icon} ${c.name}${detail}${hint}\\n`)\n if (!c.ok) failed++\n }\n if (failed > 0) {\n process.stdout.write(kleur.red(`\\n${failed} check(s) failed.\\n`))\n process.exitCode = 1\n } else {\n process.stdout.write(kleur.green('\\nAll checks passed.\\n'))\n }\n}\n"],"mappings":"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","names":[]}