@sun-asterisk/sungen 2.7.0-beta.0 → 3.0.0-beta.71

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 (256) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/add.js +3 -3
  3. package/dist/cli/commands/add.js.map +1 -1
  4. package/dist/cli/commands/audit.d.ts +3 -0
  5. package/dist/cli/commands/audit.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.js +134 -0
  7. package/dist/cli/commands/audit.js.map +1 -0
  8. package/dist/cli/commands/blindspot.d.ts +3 -0
  9. package/dist/cli/commands/blindspot.d.ts.map +1 -0
  10. package/dist/cli/commands/blindspot.js +58 -0
  11. package/dist/cli/commands/blindspot.js.map +1 -0
  12. package/dist/cli/commands/challenge.d.ts +3 -0
  13. package/dist/cli/commands/challenge.d.ts.map +1 -0
  14. package/dist/cli/commands/challenge.js +102 -0
  15. package/dist/cli/commands/challenge.js.map +1 -0
  16. package/dist/cli/commands/feedback.d.ts +3 -0
  17. package/dist/cli/commands/feedback.d.ts.map +1 -0
  18. package/dist/cli/commands/feedback.js +72 -0
  19. package/dist/cli/commands/feedback.js.map +1 -0
  20. package/dist/cli/commands/generate.d.ts.map +1 -1
  21. package/dist/cli/commands/generate.js +22 -0
  22. package/dist/cli/commands/generate.js.map +1 -1
  23. package/dist/cli/commands/ledger.d.ts +3 -0
  24. package/dist/cli/commands/ledger.d.ts.map +1 -0
  25. package/dist/cli/commands/ledger.js +71 -0
  26. package/dist/cli/commands/ledger.js.map +1 -0
  27. package/dist/cli/commands/manifest.d.ts +3 -0
  28. package/dist/cli/commands/manifest.d.ts.map +1 -0
  29. package/dist/cli/commands/manifest.js +101 -0
  30. package/dist/cli/commands/manifest.js.map +1 -0
  31. package/dist/cli/commands/script-check.d.ts +3 -0
  32. package/dist/cli/commands/script-check.d.ts.map +1 -0
  33. package/dist/cli/commands/script-check.js +97 -0
  34. package/dist/cli/commands/script-check.js.map +1 -0
  35. package/dist/cli/commands/trace.d.ts +3 -0
  36. package/dist/cli/commands/trace.d.ts.map +1 -0
  37. package/dist/cli/commands/trace.js +110 -0
  38. package/dist/cli/commands/trace.js.map +1 -0
  39. package/dist/cli/commands/update.d.ts.map +1 -1
  40. package/dist/cli/commands/update.js +22 -9
  41. package/dist/cli/commands/update.js.map +1 -1
  42. package/dist/cli/index.js +16 -0
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
  45. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
  46. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +16 -0
  47. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +1 -0
  48. package/dist/generators/test-generator/patterns/capture-patterns.js +54 -0
  49. package/dist/generators/test-generator/patterns/capture-patterns.js.map +1 -0
  50. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  51. package/dist/generators/test-generator/patterns/index.js +2 -0
  52. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  53. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  54. package/dist/generators/test-generator/step-mapper.js +1 -0
  55. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  56. package/dist/generators/test-generator/utils/data-resolver.d.ts +5 -0
  57. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  58. package/dist/generators/test-generator/utils/data-resolver.js +17 -0
  59. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  60. package/dist/harness/audit.d.ts +24 -0
  61. package/dist/harness/audit.d.ts.map +1 -0
  62. package/dist/harness/audit.js +115 -0
  63. package/dist/harness/audit.js.map +1 -0
  64. package/dist/harness/blindspot.d.ts +15 -0
  65. package/dist/harness/blindspot.d.ts.map +1 -0
  66. package/dist/harness/blindspot.js +85 -0
  67. package/dist/harness/blindspot.js.map +1 -0
  68. package/dist/harness/catalog/universal-viewpoints.yaml +114 -0
  69. package/dist/harness/challenge.d.ts +21 -0
  70. package/dist/harness/challenge.d.ts.map +1 -0
  71. package/dist/harness/challenge.js +151 -0
  72. package/dist/harness/challenge.js.map +1 -0
  73. package/dist/harness/feedback.d.ts +29 -0
  74. package/dist/harness/feedback.d.ts.map +1 -0
  75. package/dist/harness/feedback.js +106 -0
  76. package/dist/harness/feedback.js.map +1 -0
  77. package/dist/harness/intent.d.ts +11 -0
  78. package/dist/harness/intent.d.ts.map +1 -0
  79. package/dist/harness/intent.js +86 -0
  80. package/dist/harness/intent.js.map +1 -0
  81. package/dist/harness/ledger.d.ts +42 -0
  82. package/dist/harness/ledger.d.ts.map +1 -0
  83. package/dist/harness/ledger.js +171 -0
  84. package/dist/harness/ledger.js.map +1 -0
  85. package/dist/harness/manifest.d.ts +42 -0
  86. package/dist/harness/manifest.d.ts.map +1 -0
  87. package/dist/harness/manifest.js +209 -0
  88. package/dist/harness/manifest.js.map +1 -0
  89. package/dist/harness/parse.d.ts +22 -0
  90. package/dist/harness/parse.d.ts.map +1 -0
  91. package/dist/harness/parse.js +163 -0
  92. package/dist/harness/parse.js.map +1 -0
  93. package/dist/harness/script-check.d.ts +16 -0
  94. package/dist/harness/script-check.d.ts.map +1 -0
  95. package/dist/harness/script-check.js +169 -0
  96. package/dist/harness/script-check.js.map +1 -0
  97. package/dist/harness/secret-scan.d.ts +8 -0
  98. package/dist/harness/secret-scan.d.ts.map +1 -0
  99. package/dist/harness/secret-scan.js +88 -0
  100. package/dist/harness/secret-scan.js.map +1 -0
  101. package/dist/harness/sensors.d.ts +88 -0
  102. package/dist/harness/sensors.d.ts.map +1 -0
  103. package/dist/harness/sensors.js +232 -0
  104. package/dist/harness/sensors.js.map +1 -0
  105. package/dist/harness/trace.d.ts +31 -0
  106. package/dist/harness/trace.d.ts.map +1 -0
  107. package/dist/harness/trace.js +173 -0
  108. package/dist/harness/trace.js.map +1 -0
  109. package/dist/orchestrator/ai-rules-updater.d.ts +1 -0
  110. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  111. package/dist/orchestrator/ai-rules-updater.js +55 -11
  112. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  113. package/dist/orchestrator/figma/spec-figma-renderer.d.ts +2 -2
  114. package/dist/orchestrator/figma/spec-figma-renderer.js +2 -2
  115. package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +1 -1
  116. package/dist/orchestrator/figma/spec-figma-section-renderers.js +1 -1
  117. package/dist/orchestrator/project-initializer.d.ts +5 -0
  118. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  119. package/dist/orchestrator/project-initializer.js +26 -6
  120. package/dist/orchestrator/project-initializer.js.map +1 -1
  121. package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
  122. package/dist/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
  123. package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
  124. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
  125. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
  126. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +45 -13
  127. package/dist/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
  128. package/dist/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
  129. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
  130. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +8 -3
  131. package/dist/orchestrator/templates/ai-instructions/claude-config.md +1 -4
  132. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
  133. package/dist/orchestrator/templates/ai-instructions/{github-skill-sungen-figma-source.md → claude-skill-capture-mode-figma-pat.md} +14 -48
  134. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
  135. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
  136. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
  137. package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
  138. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +57 -11
  139. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +41 -31
  140. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +53 -1
  141. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
  142. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
  143. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +27 -11
  144. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
  145. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
  146. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
  147. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +6 -3
  148. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
  149. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
  150. package/{src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md → dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md} +14 -48
  151. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
  152. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
  153. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
  154. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
  155. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +62 -16
  156. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +41 -31
  157. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
  158. package/dist/orchestrator/templates/qa-context.md +90 -0
  159. package/dist/orchestrator/templates/readme.md +16 -13
  160. package/dist/orchestrator/templates/specs-test-data.ts +9 -0
  161. package/dist/tools/figma/figma-auth.d.ts +5 -2
  162. package/dist/tools/figma/figma-auth.d.ts.map +1 -1
  163. package/dist/tools/figma/figma-auth.js +19 -9
  164. package/dist/tools/figma/figma-auth.js.map +1 -1
  165. package/docs/orchestration-spec.md +267 -0
  166. package/package.json +10 -6
  167. package/src/cli/commands/add.ts +3 -3
  168. package/src/cli/commands/audit.ts +92 -0
  169. package/src/cli/commands/blindspot.ts +48 -0
  170. package/src/cli/commands/challenge.ts +55 -0
  171. package/src/cli/commands/feedback.ts +65 -0
  172. package/src/cli/commands/generate.ts +19 -0
  173. package/src/cli/commands/ledger.ts +61 -0
  174. package/src/cli/commands/manifest.ts +55 -0
  175. package/src/cli/commands/script-check.ts +50 -0
  176. package/src/cli/commands/trace.ts +60 -0
  177. package/src/cli/commands/update.ts +30 -10
  178. package/src/cli/index.ts +16 -0
  179. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
  180. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
  181. package/src/generators/test-generator/patterns/capture-patterns.ts +59 -0
  182. package/src/generators/test-generator/patterns/index.ts +2 -0
  183. package/src/generators/test-generator/step-mapper.ts +1 -0
  184. package/src/generators/test-generator/utils/data-resolver.ts +20 -0
  185. package/src/harness/audit.ts +112 -0
  186. package/src/harness/blindspot.ts +51 -0
  187. package/src/harness/catalog/universal-viewpoints.yaml +114 -0
  188. package/src/harness/challenge.ts +131 -0
  189. package/src/harness/feedback.ts +84 -0
  190. package/src/harness/intent.ts +58 -0
  191. package/src/harness/ledger.ts +155 -0
  192. package/src/harness/manifest.ts +173 -0
  193. package/src/harness/parse.ts +145 -0
  194. package/src/harness/script-check.ts +149 -0
  195. package/src/harness/secret-scan.ts +51 -0
  196. package/src/harness/sensors.ts +279 -0
  197. package/src/harness/trace.ts +138 -0
  198. package/src/orchestrator/ai-rules-updater.ts +57 -10
  199. package/src/orchestrator/figma/spec-figma-renderer.ts +2 -2
  200. package/src/orchestrator/figma/spec-figma-section-renderers.ts +1 -1
  201. package/src/orchestrator/project-initializer.ts +30 -7
  202. package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
  203. package/src/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
  204. package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
  205. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
  206. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
  207. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +45 -13
  208. package/src/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
  209. package/src/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
  210. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
  211. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +8 -3
  212. package/src/orchestrator/templates/ai-instructions/claude-config.md +1 -4
  213. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
  214. package/{dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md → src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-pat.md} +14 -48
  215. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
  216. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
  217. package/src/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
  218. package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
  219. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +57 -11
  220. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +41 -31
  221. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +53 -1
  222. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
  223. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
  224. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +27 -11
  225. package/src/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
  226. package/src/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
  227. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
  228. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +6 -3
  229. package/src/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
  230. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
  231. package/{dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md → src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md} +14 -48
  232. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
  233. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
  234. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
  235. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
  236. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +62 -16
  237. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +41 -31
  238. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
  239. package/src/orchestrator/templates/qa-context.md +90 -0
  240. package/src/orchestrator/templates/readme.md +16 -13
  241. package/src/orchestrator/templates/specs-test-data.ts +9 -0
  242. package/src/tools/figma/figma-auth.ts +20 -9
  243. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
  244. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
  245. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
  246. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
  247. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
  248. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
  249. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
  250. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
  251. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
  252. package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +0 -151
  253. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
  254. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
  255. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
  256. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +0 -151
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Harness Sensors — deterministic quality measurement over test-design artifacts.
3
+ * Each sensor returns a structured finding the Orchestrator/Repair loop can act on.
4
+ *
5
+ * NOTE (honesty): the Viewpoint Gate and Duplicate sensors involve semantics, so
6
+ * they are HEURISTIC (keyword / skeleton based), not provably exhaustive. They use
7
+ * a curated seed catalog as the matching reference. See docs/orchestration-spec.md §5.2.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { parse as parseYaml } from 'yaml';
12
+ import { ScenarioInfo, ViewpointEntry } from './parse';
13
+
14
+ // Business-critical category codes (project VP-<CAT> prefixes). Configurable later.
15
+ const BUSINESS_CRITICAL_CATS = ['LIST', 'CART', 'PRODUCT', 'FILTER', 'CHECKOUT', 'ORDER'];
16
+
17
+ // Buckets for coverage-balance.
18
+ const BUCKETS: Record<string, string[]> = {
19
+ 'business-core': BUSINESS_CRITICAL_CATS,
20
+ 'presentation': ['UI'],
21
+ 'validation-security': ['VAL', 'SEC', 'SUB'],
22
+ 'behavior': ['LOGIC'],
23
+ 'navigation': ['NAV'],
24
+ };
25
+
26
+ export interface ThemeDepth {
27
+ requires: string; // 'data-assertion' → scenarios on this theme must assert DATA
28
+ cross_screen?: boolean; // genuine depth needs another screen → flow / @manual-deferred
29
+ keywords?: string[]; // precise data-noun keywords for depth matching
30
+ template?: string; // the deep step the generator should emit by default
31
+ }
32
+ export interface CatalogTheme { theme: string; keywords: string[]; depth?: ThemeDepth }
33
+ export interface Catalog {
34
+ page_types: Record<string, { detect_keywords: string[]; must_cover: CatalogTheme[] }>;
35
+ universal: CatalogTheme[];
36
+ }
37
+
38
+ export function loadCatalog(): Catalog {
39
+ const p = path.join(__dirname, 'catalog', 'universal-viewpoints.yaml');
40
+ return parseYaml(fs.readFileSync(p, 'utf-8')) as Catalog;
41
+ }
42
+
43
+ const has = (haystacks: string[], kw: string) => {
44
+ const k = kw.toLowerCase();
45
+ return haystacks.some((h) => h.includes(k));
46
+ };
47
+
48
+ // ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
49
+
50
+ export interface GateResult {
51
+ pageType: string | null;
52
+ themesTotal: number;
53
+ themesCovered: number; // deeply covered (has a data assertion)
54
+ coverageRatio: number;
55
+ gaps: { theme: string; keywords: string[]; status: 'missing' | 'shallow' }[];
56
+ universalGaps: string[];
57
+ }
58
+
59
+ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[], catalog: Catalog): GateResult {
60
+ const haystacks = [
61
+ ...scenarios.map((s) => s.haystack),
62
+ ...viewpoints.map((v) => `${v.id} ${v.reason}`.toLowerCase()),
63
+ ];
64
+
65
+ // Detect page-type by detect_keywords hit count
66
+ let pageType: string | null = null;
67
+ let best = 0;
68
+ for (const [pt, def] of Object.entries(catalog.page_types)) {
69
+ const hits = def.detect_keywords.filter((k) => has(haystacks, k)).length;
70
+ if (hits > best) { best = hits; pageType = pt; }
71
+ }
72
+
73
+ const gaps: GateResult['gaps'] = [];
74
+ let total = 0, covered = 0;
75
+ if (pageType) {
76
+ for (const t of catalog.page_types[pageType].must_cover) {
77
+ total++;
78
+ // Scenarios that cover this theme by keyword (include @manual — a manual
79
+ // scenario with a real assertion still covers the viewpoint for design).
80
+ const coverers = scenarios.filter((s) => t.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
81
+ const deep = coverers.some((s) => s.hasDataAssertion);
82
+ if (deep) covered++;
83
+ else if (coverers.length > 0) gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
84
+ else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' });
85
+ }
86
+ }
87
+
88
+ const universalGaps = catalog.universal
89
+ .filter((t) => !t.keywords.some((k) => has(scenarios.map((s) => s.haystack), k)))
90
+ .map((t) => t.theme);
91
+
92
+ return {
93
+ pageType,
94
+ themesTotal: total,
95
+ themesCovered: covered,
96
+ coverageRatio: total ? covered / total : 1,
97
+ gaps,
98
+ universalGaps,
99
+ };
100
+ }
101
+
102
+ // ---------- Sensor 2: Assertion depth ----------
103
+
104
+ export type DepthVerdict = 'pass' | 'warn' | 'fail';
105
+
106
+ export interface DepthResult {
107
+ total: number;
108
+ shallowTotal: number;
109
+ shallowRatio: number;
110
+ businessCriticalTotal: number; // = depth-required scenarios (catalog data-themes)
111
+ businessCriticalShallow: number; // = depth-required scenarios that are shallow
112
+ bcDepthRatio: number; // fraction of depth-required scenarios with a real data assertion
113
+ shallowBusinessCritical: { name: string; category?: string }[];
114
+ // Depth-as-Gate (harness-roadmap P1)
115
+ focus: string; // intent focus driving the threshold
116
+ threshold: number; // required bcDepthRatio for this focus
117
+ verdict: DepthVerdict; // pass | warn | fail
118
+ }
119
+
120
+ // Intent → required depth ratio + whether a miss WARNs (smoke) or FAILs the gate.
121
+ // P3 will read `focus` from qa/context.md; P1 uses 'functional' by default.
122
+ const DEPTH_THRESHOLDS: Record<string, number> = {
123
+ functional: 0.7, 'e-commerce': 0.7, security: 0.8, smoke: 0.4,
124
+ };
125
+ const WARN_ONLY_FOCUS = new Set(['smoke']);
126
+
127
+ /**
128
+ * Depth = do DATA-correctness scenarios actually assert DATA (not just visibility)?
129
+ * "Depth-required" is CATALOG-DRIVEN: only scenarios matching a theme whose
130
+ * `depth.requires === 'data-assertion'` are measured. Navigation viewpoints
131
+ * (category NAV) are excluded — landing on a page IS their correct assertion.
132
+ * This avoids the old keyword-based false-positives (e.g. "API list page").
133
+ */
134
+ export function assertionDepth(
135
+ scenarios: ScenarioInfo[],
136
+ dataThemes: CatalogTheme[] = [],
137
+ focus = 'functional',
138
+ ): DepthResult {
139
+ const nonManual = scenarios.filter((s) => !s.manual);
140
+ const shallow = nonManual.filter((s) => s.shallow);
141
+
142
+ // Precise depth keywords come from each data-theme's depth.keywords (fallback: theme keywords).
143
+ const depthKeywords = dataThemes.flatMap((t) => (t.depth?.keywords?.length ? t.depth.keywords : t.keywords));
144
+ // Dismiss/close behaviors ("Continue Shopping closes the modal", "Escape dismisses the
145
+ // dialog") assert a HIDDEN state — that IS the correct, data-light assertion for the
146
+ // behavior. They only match a data keyword incidentally (e.g. "added"), so exclude them.
147
+ const isDismissBehavior = (s: ScenarioInfo) =>
148
+ /\bis hidden\b|\bare hidden\b|\bdismiss|\bdisappear|\bno longer (visible|shown|present|displayed)\b|\bcloses? the (modal|dialog|popup|overlay|panel|menu)\b/.test(s.haystack);
149
+ const isDepthRequired = (s: ScenarioInfo) =>
150
+ s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => s.haystack.includes(k.toLowerCase()));
151
+
152
+ const required = nonManual.filter(isDepthRequired);
153
+ const reqShallow = required.filter((s) => s.shallow);
154
+ // No data-theme scenarios on this screen → depth is not the binding constraint
155
+ // (the viewpoint gate already flags missing data themes). Don't double-penalize.
156
+ const ratio = required.length ? 1 - reqShallow.length / required.length : 1;
157
+
158
+ const threshold = DEPTH_THRESHOLDS[focus] ?? DEPTH_THRESHOLDS.functional;
159
+ let verdict: DepthVerdict = 'pass';
160
+ if (ratio < threshold) verdict = WARN_ONLY_FOCUS.has(focus) ? 'warn' : 'fail';
161
+
162
+ return {
163
+ total: nonManual.length,
164
+ shallowTotal: shallow.length,
165
+ shallowRatio: nonManual.length ? shallow.length / nonManual.length : 0,
166
+ businessCriticalTotal: required.length,
167
+ businessCriticalShallow: reqShallow.length,
168
+ bcDepthRatio: ratio,
169
+ shallowBusinessCritical: reqShallow.map((s) => ({ name: s.name, category: s.category })),
170
+ focus,
171
+ threshold,
172
+ verdict,
173
+ };
174
+ }
175
+
176
+ /** Collect data-correctness themes (depth.requires) for a page-type + universal. */
177
+ export function dataThemesFor(catalog: Catalog, pageType: string | null): CatalogTheme[] {
178
+ const themes: CatalogTheme[] = [];
179
+ if (pageType && catalog.page_types[pageType]) themes.push(...catalog.page_types[pageType].must_cover);
180
+ themes.push(...catalog.universal);
181
+ return themes.filter((t) => t.depth?.requires === 'data-assertion');
182
+ }
183
+
184
+ // ---------- Sensor 3: Coverage balance ----------
185
+
186
+ export interface BalanceResult {
187
+ byBucket: Record<string, number>;
188
+ byCategory: Record<string, number>;
189
+ coreCount: number;
190
+ secondaryCount: number;
191
+ imbalanced: boolean;
192
+ note: string;
193
+ }
194
+
195
+ export function coverageBalance(scenarios: ScenarioInfo[]): BalanceResult {
196
+ const byCategory: Record<string, number> = {};
197
+ const byBucket: Record<string, number> = {};
198
+ for (const b of Object.keys(BUCKETS)) byBucket[b] = 0;
199
+ byBucket['other'] = 0;
200
+
201
+ for (const s of scenarios) {
202
+ const cat = s.category || 'NONE';
203
+ byCategory[cat] = (byCategory[cat] || 0) + 1;
204
+ const bucket = Object.entries(BUCKETS).find(([, cats]) => cats.includes(cat))?.[0] || 'other';
205
+ byBucket[bucket]++;
206
+ }
207
+
208
+ const core = byBucket['business-core'];
209
+ const secondary = byBucket['presentation'] + byBucket['validation-security'];
210
+ const imbalanced = secondary > core * 1.5 && core > 0;
211
+ return {
212
+ byBucket,
213
+ byCategory,
214
+ coreCount: core,
215
+ secondaryCount: secondary,
216
+ imbalanced,
217
+ note: imbalanced
218
+ ? `Secondary viewpoints (presentation+validation/security = ${secondary}) outweigh business-core (${core}) by >1.5x.`
219
+ : 'Balanced.',
220
+ };
221
+ }
222
+
223
+ // ---------- Sensor 4: Duplicate clusters ----------
224
+
225
+ export interface DuplicateResult {
226
+ clusters: { skeleton: string; scenarios: string[]; sameDataLikely: boolean }[];
227
+ exactDuplicateCount: number;
228
+ sameShapeCount: number;
229
+ }
230
+
231
+ export function duplicateClusters(scenarios: ScenarioInfo[]): DuplicateResult {
232
+ const map = new Map<string, ScenarioInfo[]>();
233
+ for (const s of scenarios) {
234
+ const arr = map.get(s.stepSkeleton) || [];
235
+ arr.push(s);
236
+ map.set(s.stepSkeleton, arr);
237
+ }
238
+ const clusters = [...map.entries()]
239
+ .filter(([, arr]) => arr.length > 1)
240
+ .map(([skeleton, arr]) => ({
241
+ skeleton: skeleton.length > 120 ? skeleton.slice(0, 117) + '...' : skeleton,
242
+ scenarios: arr.map((s) => s.name),
243
+ // Same skeleton with data placeholders → likely an EP/data family (intentional), not a true dup.
244
+ sameDataLikely: !skeleton.includes('{}'),
245
+ }));
246
+ return {
247
+ clusters,
248
+ exactDuplicateCount: clusters.filter((c) => c.sameDataLikely).reduce((n, c) => n + (c.scenarios.length - 1), 0),
249
+ sameShapeCount: clusters.reduce((n, c) => n + c.scenarios.length, 0),
250
+ };
251
+ }
252
+
253
+ // ---------- Sensor 5: Traceability ----------
254
+
255
+ export interface TraceResult {
256
+ total: number;
257
+ withVpCode: number;
258
+ mappedToOverview: number;
259
+ withVpCodeRatio: number;
260
+ mappedRatio: number;
261
+ note: string;
262
+ }
263
+
264
+ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
265
+ const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
266
+ const withCode = scenarios.filter((s) => s.vpCode);
267
+ // A scenario maps to overview if its full VP code OR its category-derived id exists in overview.
268
+ const mapped = withCode.filter((s) => overviewIds.has(s.vpCode!) || [...overviewIds].some((id) => id.includes(s.category || '###')));
269
+ return {
270
+ total: scenarios.length,
271
+ withVpCode: withCode.length,
272
+ mappedToOverview: mapped.length,
273
+ withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
274
+ mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
275
+ note: mapped.length < withCode.length * 0.5
276
+ ? 'Scenarios use ad-hoc VP-<CAT>-NNN codes not linked to viewpoint-overview ids (weak traceability — see review Gate 4).'
277
+ : 'Traceable.',
278
+ };
279
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Trace — visualise the whole executed test-design process and tell the end-user
3
+ * where to focus human-in-the-loop review.
4
+ *
5
+ * Aggregates the artifacts the harness already produces:
6
+ * - .sungen/ledger/<name>.jsonl → what ran, in what order, how many repair loops, time
7
+ * - .sungen/reports/<name>-audit.json → gate result, sub-scores, weak sensor (bottleneck)
8
+ * - .sungen/reports/<name>-script-check.json → Gherkin↔script drift
9
+ * - features/<name>.feature → @manual scenarios (where humans MUST verify)
10
+ *
11
+ * Outputs: a text process map + a Mermaid flowchart (paste into any viewer) +
12
+ * bottleneck analysis + a ranked "human-loop focus" list.
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { segmentRuns, latestRunEvents, LedgerEvent } from './ledger';
17
+
18
+ interface ManualItem { scenario: string; reason: string }
19
+
20
+ function readJson(p: string): any | null {
21
+ try { return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : null; } catch { return null; }
22
+ }
23
+
24
+ function readLedger(screen: string): any[] {
25
+ const p = path.join(process.cwd(), '.sungen', 'ledger', `${screen}.jsonl`);
26
+ if (!fs.existsSync(p)) return [];
27
+ return fs.readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
28
+ }
29
+
30
+ /** Parse @manual scenarios + the explanatory comment line above each. */
31
+ function parseManual(featurePath: string): ManualItem[] {
32
+ if (!fs.existsSync(featurePath)) return [];
33
+ const lines = fs.readFileSync(featurePath, 'utf-8').split('\n');
34
+ const out: ManualItem[] = [];
35
+ for (let i = 0; i < lines.length; i++) {
36
+ const m = lines[i].match(/^\s*Scenario:\s*(.+)$/);
37
+ if (!m) continue;
38
+ // look back for tag line + a reason comment
39
+ let manual = false; let reason = '';
40
+ for (let j = i - 1; j >= 0 && j >= i - 4; j--) {
41
+ const l = lines[j].trim();
42
+ if (/^@/.test(l) && /@manual/.test(l)) manual = true;
43
+ else if (/^#/.test(l) && !reason) reason = l.replace(/^#+\s*/, '');
44
+ else if (l === '' || /^Scenario:/.test(l)) break;
45
+ }
46
+ if (manual) out.push({ scenario: m[1].trim(), reason: reason || '(no reason noted)' });
47
+ }
48
+ return out;
49
+ }
50
+
51
+ export interface TraceReport {
52
+ screen: string;
53
+ runs: number; // total runs on file (process map shows the latest)
54
+ ledger: { step: string; ms: number }[];
55
+ repairRounds: number;
56
+ totalMs: number;
57
+ recordedSteps: string[];
58
+ missingSteps: string[]; // expected phases not recorded in ledger
59
+ audit: { score?: number; gate?: string; weakest?: string; findings?: number; gaps?: string[] } | null;
60
+ drift: string | null;
61
+ manual: ManualItem[];
62
+ bottlenecks: string[];
63
+ humanFocus: string[];
64
+ mermaid: string;
65
+ }
66
+
67
+ const EXPECTED_PHASES = ['discovery', 'viewpoint', 'gherkin', 'audit', 'repair'];
68
+
69
+ export function buildTrace(screenDir: string, screenName: string): TraceReport {
70
+ const allEvents = readLedger(screenName) as LedgerEvent[];
71
+ const runs = segmentRuns(allEvents).length;
72
+ const events = latestRunEvents(allEvents); // scope to the most recent run (P2)
73
+ const ledger = events.map((e) => ({ step: e.step as string, ms: (e.ms as number) || 0 }));
74
+ const repairRounds = new Set(events.filter((e) => /^repair/i.test(e.step)).map((e) => e.step)).size;
75
+ const totalMs = ledger.reduce((n, e) => n + e.ms, 0);
76
+ const recordedSteps = [...new Set(ledger.map((e) => e.step.replace(/:\d+$/, '')))];
77
+ const missingSteps = EXPECTED_PHASES.filter((p) => !recordedSteps.includes(p));
78
+
79
+ const auditRaw = readJson(path.join(process.cwd(), '.sungen', 'reports', `${screenName}-audit.json`));
80
+ let audit: TraceReport['audit'] = null;
81
+ if (auditRaw) {
82
+ const subs: Record<string, number> = {
83
+ coverage: auditRaw.score?.coverage, businessDepth: auditRaw.score?.businessDepth,
84
+ balance: auditRaw.score?.balance, traceability: auditRaw.score?.traceability,
85
+ };
86
+ const weakest = Object.entries(subs).filter(([, v]) => typeof v === 'number').sort((a, b) => a[1] - b[1])[0]?.[0];
87
+ audit = {
88
+ score: auditRaw.score?.overall, gate: auditRaw.gateStatus, weakest,
89
+ findings: (auditRaw.findings || []).length,
90
+ gaps: (auditRaw.gate?.gaps || []).map((g: any) => `${g.theme} (${g.status})`),
91
+ };
92
+ }
93
+
94
+ const scRaw = readJson(path.join(process.cwd(), '.sungen', 'reports', `${screenName}-script-check.json`));
95
+ const drift = scRaw ? scRaw.drift : null;
96
+
97
+ const manual = parseManual(path.join(screenDir, 'features', `${screenName}.feature`));
98
+
99
+ // Bottlenecks
100
+ const bottlenecks: string[] = [];
101
+ if (repairRounds >= 3) bottlenecks.push(`Repair loop hit ${repairRounds} rounds (near/over budget) → first-pass generation (Guide) is the bottleneck.`);
102
+ if (audit?.weakest) bottlenecks.push(`Lowest audit dimension: ${audit.weakest} → focus engine improvement there.`);
103
+ if (audit?.gaps?.length) bottlenecks.push(`Gate gaps: ${audit.gaps.join(', ')}.`);
104
+ if (drift === 'drift') bottlenecks.push('Spec DRIFT vs Gherkin → regenerate (never hand-edit specs).');
105
+ if (missingSteps.length) bottlenecks.push(`Ledger missing phases: ${missingSteps.join(', ')} → instrument \`sungen ledger record\` at each phase for full observability.`);
106
+ if (bottlenecks.length === 0) bottlenecks.push('No bottleneck detected from available signals.');
107
+
108
+ // Human-loop focus (where the user must look)
109
+ const humanFocus: string[] = [];
110
+ if (manual.length) humanFocus.push(`${manual.length} @manual scenario(s) need HUMAN verification (not automated):`);
111
+ for (const m of manual.slice(0, 12)) humanFocus.push(` • ${m.scenario} — ${m.reason}`);
112
+ if (audit?.gaps?.length) humanFocus.push(`Critical gaps deferred/uncovered: ${audit.gaps.join(', ')} — decide flow vs accept.`);
113
+ if (humanFocus.length === 0) humanFocus.push('No manual/uncovered items — automation covers the critical viewpoints.');
114
+
115
+ // Mermaid flowchart of the executed process
116
+ const phaseSeq = ledger.length
117
+ ? ledger.map((e) => e.step)
118
+ : ['(no ledger — process not instrumented)'];
119
+ const nodes = phaseSeq.map((s, i) => ` S${i}["${s}${ledger[i] ? ` (${ledger[i].ms}ms)` : ''}"]`).join('\n');
120
+ const edges = phaseSeq.map((_, i) => (i < phaseSeq.length - 1 ? ` S${i} --> S${i + 1}` : '')).filter(Boolean).join('\n');
121
+ const mermaid = [
122
+ 'flowchart TD',
123
+ ' IN["spec / figma / ui / live"] --> DISC[discovery]',
124
+ ' DISC --> VP[viewpoint overview]',
125
+ ' VP --> GEN[generate Gherkin]',
126
+ ' GEN --> AUD{sungen audit gate}',
127
+ repairRounds ? ` AUD -- FAIL --> REP["repair ×${repairRounds}"]` : '',
128
+ repairRounds ? ' REP --> AUD' : '',
129
+ ` AUD -- ${audit?.gate === 'PASS' ? 'PASS' : 'still failing'} --> DONE[converge]`,
130
+ ' DONE --> SC{script-check 1:1}',
131
+ ` SC -- ${drift === 'drift' ? 'DRIFT → regenerate' : 'in-sync'} --> OUT[generate → run-test → delivery]`,
132
+ ].filter(Boolean).join('\n');
133
+
134
+ return {
135
+ screen: screenName, runs, ledger, repairRounds, totalMs, recordedSteps, missingSteps,
136
+ audit, drift, manual, bottlenecks, humanFocus, mermaid,
137
+ };
138
+ }
@@ -18,21 +18,25 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
18
18
  ['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
19
19
  ['claude-cmd-add-flow.md', '.claude/commands/sungen/add-flow.md'],
20
20
  ['claude-cmd-create-test.md', '.claude/commands/sungen/create-test.md'],
21
+ ['claude-cmd-design.md', '.claude/commands/sungen/design.md'],
21
22
  ['claude-cmd-run-test.md', '.claude/commands/sungen/run-test.md'],
22
23
  ['claude-cmd-review.md', '.claude/commands/sungen/review.md'],
23
24
  ['claude-cmd-delivery.md', '.claude/commands/sungen/delivery.md'],
24
25
  ['claude-cmd-dashboard.md', '.claude/commands/sungen/dashboard.md'],
25
26
  ['claude-cmd-locale.md', '.claude/commands/sungen/locale.md'],
27
+ ['claude-cmd-feedback.md', '.claude/commands/sungen/feedback.md'],
26
28
 
27
29
  // Commands — GitHub Copilot
28
30
  ['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
29
31
  ['copilot-cmd-add-flow.md', '.github/prompts/sungen-add-flow.prompt.md'],
30
32
  ['copilot-cmd-create-test.md', '.github/prompts/sungen-create-test.prompt.md'],
33
+ ['copilot-cmd-design.md', '.github/prompts/sungen-design.prompt.md'],
31
34
  ['copilot-cmd-run-test.md', '.github/prompts/sungen-run-test.prompt.md'],
32
35
  ['copilot-cmd-review.md', '.github/prompts/sungen-review.prompt.md'],
33
36
  ['copilot-cmd-delivery.md', '.github/prompts/sungen-delivery.prompt.md'],
34
37
  ['copilot-cmd-dashboard.md', '.github/prompts/sungen-dashboard.prompt.md'],
35
38
  ['copilot-cmd-locale.md', '.github/prompts/sungen-locale.prompt.md'],
39
+ ['copilot-cmd-feedback.md', '.github/prompts/sungen-feedback.prompt.md'],
36
40
 
37
41
  // Skills — Claude Code
38
42
  ['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
@@ -42,6 +46,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
42
46
  ['claude-skill-test-design-techniques.md', '.claude/skills/sungen-test-design-techniques/SKILL.md'],
43
47
  ['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
44
48
  ['claude-skill-tc-review.md', '.claude/skills/sungen-tc-review/SKILL.md'],
49
+ ['claude-skill-harness-audit.md', '.claude/skills/sungen-harness-audit/SKILL.md'],
45
50
  ['claude-skill-viewpoint.md', '.claude/skills/sungen-viewpoint/SKILL.md'],
46
51
  ['claude-skill-viewpoint-group-a-data-entry.md', '.claude/skills/sungen-viewpoint/group-a-data-entry.md'],
47
52
  ['claude-skill-viewpoint-group-b-data-ops.md', '.claude/skills/sungen-viewpoint/group-b-data-ops.md'],
@@ -50,14 +55,19 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
50
55
  ['claude-skill-viewpoint-group-e-identity.md', '.claude/skills/sungen-viewpoint/group-e-identity.md'],
51
56
  ['claude-skill-delivery.md', '.claude/skills/sungen-delivery/SKILL.md'],
52
57
  ['claude-skill-dashboard.md', '.claude/skills/sungen-dashboard/SKILL.md'],
53
- ['claude-skill-capture-figma.md', '.claude/skills/sungen-capture-figma/SKILL.md'],
54
- ['claude-skill-capture-local.md', '.claude/skills/sungen-capture-local/SKILL.md'],
55
- ['claude-skill-capture-live.md', '.claude/skills/sungen-capture-live/SKILL.md'],
56
- ['claude-skill-figma-source.md', '.claude/skills/sungen-figma-source/SKILL.md'],
58
+ // sungen-capture — router skill (1 SKILL.md + 4 mode files). Replaces the former
59
+ // capture-figma / capture-live / capture-local / figma-source standalone skills.
60
+ ['claude-skill-capture.md', '.claude/skills/sungen-capture/SKILL.md'],
61
+ ['claude-skill-capture-mode-figma-mcp.md', '.claude/skills/sungen-capture/mode-figma-mcp.md'],
62
+ ['claude-skill-capture-mode-figma-pat.md', '.claude/skills/sungen-capture/mode-figma-pat.md'],
63
+ ['claude-skill-capture-mode-live.md', '.claude/skills/sungen-capture/mode-live.md'],
64
+ ['claude-skill-capture-mode-local.md', '.claude/skills/sungen-capture/mode-local.md'],
57
65
  ['claude-skill-locale.md', '.claude/skills/sungen-locale/SKILL.md'],
58
66
 
59
- // SkillsCopilot (prompt-based)
60
- ['copilot-skill-figma-source.md', '.github/prompts/sungen-figma-source.prompt.md'],
67
+ // AgentsClaude Code sub-agents (isolated context). Copilot runs these inline.
68
+ ['claude-agent-reviewer.md', '.claude/agents/sungen-reviewer.md'],
69
+ ['claude-agent-discovery.md', '.claude/agents/sungen-discovery.md'],
70
+ ['claude-agent-challenge.md', '.claude/agents/sungen-challenge.md'],
61
71
 
62
72
  // Skills — GitHub Copilot
63
73
  ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
@@ -67,6 +77,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
67
77
  ['github-skill-sungen-test-design-techniques.md', '.github/skills/sungen-test-design-techniques/SKILL.md'],
68
78
  ['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
69
79
  ['github-skill-sungen-tc-review.md', '.github/skills/sungen-tc-review/SKILL.md'],
80
+ ['github-skill-sungen-harness-audit.md', '.github/skills/sungen-harness-audit/SKILL.md'],
70
81
  ['github-skill-sungen-viewpoint.md', '.github/skills/sungen-viewpoint/SKILL.md'],
71
82
  ['github-skill-sungen-viewpoint-group-a-data-entry.md', '.github/skills/sungen-viewpoint/group-a-data-entry.md'],
72
83
  ['github-skill-sungen-viewpoint-group-b-data-ops.md', '.github/skills/sungen-viewpoint/group-b-data-ops.md'],
@@ -75,13 +86,29 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
75
86
  ['github-skill-sungen-viewpoint-group-e-identity.md', '.github/skills/sungen-viewpoint/group-e-identity.md'],
76
87
  ['github-skill-sungen-delivery.md', '.github/skills/sungen-delivery/SKILL.md'],
77
88
  ['github-skill-sungen-dashboard.md', '.github/skills/sungen-dashboard/SKILL.md'],
78
- ['github-skill-sungen-capture-figma.md', '.github/skills/sungen-capture-figma/SKILL.md'],
79
- ['github-skill-sungen-capture-local.md', '.github/skills/sungen-capture-local/SKILL.md'],
80
- ['github-skill-sungen-capture-live.md', '.github/skills/sungen-capture-live/SKILL.md'],
81
- ['github-skill-sungen-figma-source.md', '.github/skills/sungen-figma-source/SKILL.md'],
89
+ ['github-skill-sungen-capture.md', '.github/skills/sungen-capture/SKILL.md'],
90
+ ['github-skill-sungen-capture-mode-figma-mcp.md', '.github/skills/sungen-capture/mode-figma-mcp.md'],
91
+ ['github-skill-sungen-capture-mode-figma-pat.md', '.github/skills/sungen-capture/mode-figma-pat.md'],
92
+ ['github-skill-sungen-capture-mode-live.md', '.github/skills/sungen-capture/mode-live.md'],
93
+ ['github-skill-sungen-capture-mode-local.md', '.github/skills/sungen-capture/mode-local.md'],
82
94
  ['github-skill-sungen-locale.md', '.github/skills/sungen-locale/SKILL.md'],
83
95
  ];
84
96
 
97
+ // Skill/asset directories retired in a previous refactor. `sungen update` removes
98
+ // these so already-initialized projects don't keep loading stale skill descriptions.
99
+ // (v3.0: the four capture/figma skills were merged into the `sungen-capture` router.)
100
+ export const STALE_AI_RULES_PATHS: string[] = [
101
+ '.claude/skills/sungen-capture-figma',
102
+ '.claude/skills/sungen-capture-live',
103
+ '.claude/skills/sungen-capture-local',
104
+ '.claude/skills/sungen-figma-source',
105
+ '.github/skills/sungen-capture-figma',
106
+ '.github/skills/sungen-capture-live',
107
+ '.github/skills/sungen-capture-local',
108
+ '.github/skills/sungen-figma-source',
109
+ '.github/prompts/sungen-figma-source.prompt.md',
110
+ ];
111
+
85
112
  export class AIRulesUpdater {
86
113
  private cwd: string;
87
114
  private aiTemplateDir: string;
@@ -133,11 +160,31 @@ export class AIRulesUpdater {
133
160
  }
134
161
  }
135
162
 
163
+ // Remove retired skills/assets so stale descriptions stop loading.
164
+ const removed: string[] = [];
165
+ for (const staleRelPath of STALE_AI_RULES_PATHS) {
166
+ const stalePath = path.join(this.cwd, staleRelPath);
167
+ if (fs.existsSync(stalePath)) {
168
+ if (!dryRun) {
169
+ fs.rmSync(stalePath, { recursive: true, force: true });
170
+ }
171
+ removed.push(staleRelPath);
172
+ }
173
+ }
174
+
136
175
  // Print results
137
176
  if (dryRun) {
138
177
  console.log('📋 Dry run — no files changed\n');
139
178
  }
140
179
 
180
+ if (removed.length > 0) {
181
+ console.log(`🗑️ Removed retired assets (${removed.length}):`);
182
+ for (const f of removed) {
183
+ console.log(` ${f}`);
184
+ }
185
+ console.log();
186
+ }
187
+
141
188
  if (updated.length > 0) {
142
189
  console.log(`✏️ Updated (${updated.length}):`);
143
190
  for (const f of updated) {
@@ -5,7 +5,7 @@
5
5
  * auto-gen banner, Frame metadata, Screenshots, and a SYNTHESIS marker.
6
6
  * Narrative sections (Purpose, ASCII Layout, Regions, Actions, Form Fields,
7
7
  * Data Columns, Navigation) are appended BELOW the marker by the
8
- * sungen-figma-source skill, which reads the raw cached node JSON.
8
+ * sungen-capture skill (mode figma-pat), which reads the raw cached node JSON.
9
9
  *
10
10
  * Section renderers live in spec-figma-section-renderers.ts.
11
11
  *
@@ -55,7 +55,7 @@ export interface RenderSpecFigmaInput {
55
55
 
56
56
  /**
57
57
  * Render the deterministic envelope for spec_figma.md.
58
- * The LLM synthesis step (sungen-figma-source skill) appends the narrative
58
+ * The LLM synthesis step (sungen-capture skill (mode figma-pat)) appends the narrative
59
59
  * sections AFTER the SYNTHESIS_MARKER comment. Re-synthesis replaces
60
60
  * everything from the marker to EOF.
61
61
  *
@@ -5,7 +5,7 @@
5
5
  * needs: frontmatter, Frame metadata, Screenshots, and the SYNTHESIS marker.
6
6
  * All prose sections (Purpose, ASCII Layout, Regions, Actions, Form Fields,
7
7
  * Data Columns, Navigation) are appended below the marker by the
8
- * sungen-figma-source skill.
8
+ * sungen-capture skill (mode figma-pat).
9
9
  *
10
10
  * Pure — no I/O.
11
11
  */
@@ -39,6 +39,9 @@ export class ProjectInitializer {
39
39
  // Create directories
40
40
  this.createDirectories();
41
41
 
42
+ // Create qa/context.md for QA lead to fill project-wide context
43
+ this.createContext();
44
+
42
45
  // Ensure package.json and install Playwright
43
46
  await this.setupDependencies();
44
47
 
@@ -147,10 +150,12 @@ export class ProjectInitializer {
147
150
  const gitignorePath = path.join(this.cwd, '.gitignore');
148
151
  const sungenEntries = [
149
152
  '# Sungen generated files',
150
- 'specs/.auth/',
153
+ 'specs/.auth/', // session storage state = cookies/tokens (sensitive)
151
154
  'test-results/',
152
155
  'playwright-report/',
153
- '.playwright-mcp/'
156
+ '.playwright-mcp/',
157
+ '.sungen/figma-cache/', // raw Figma node JSON (design IP)
158
+ '.sungen/feedback/', // local QA feedback (may contain product-sensitive notes)
154
159
  ];
155
160
 
156
161
  if (!fs.existsSync(gitignorePath)) {
@@ -159,12 +164,13 @@ export class ProjectInitializer {
159
164
  fs.writeFileSync(gitignorePath, content, 'utf-8');
160
165
  this.createdItems.push('.gitignore');
161
166
  } else {
162
- // Append Sungen entries if not already present
167
+ // Append any Sungen entries that are missing (idempotent — covers projects
168
+ // created before new sensitive paths were added).
163
169
  let content = fs.readFileSync(gitignorePath, 'utf-8');
164
- const needsUpdate = !content.includes('specs/.auth/');
165
-
166
- if (needsUpdate) {
167
- content += '\n' + sungenEntries.join('\n') + '\n';
170
+ const missing = sungenEntries.filter((e) => e.startsWith('#') ? false : !content.includes(e));
171
+ if (missing.length) {
172
+ const block = content.includes('# Sungen generated files') ? missing : ['# Sungen generated files', ...missing];
173
+ content += '\n' + block.join('\n') + '\n';
168
174
  fs.writeFileSync(gitignorePath, content, 'utf-8');
169
175
  this.createdItems.push('.gitignore (updated)');
170
176
  } else {
@@ -363,6 +369,23 @@ export class ProjectInitializer {
363
369
 
364
370
  }
365
371
 
372
+ /**
373
+ * Create qa/context.md for the QA lead to fill project-wide context
374
+ * (roles, testing strategy, global rules, error patterns).
375
+ */
376
+ private createContext(): void {
377
+ const contextPath = path.join(this.cwd, 'qa', 'context.md');
378
+
379
+ if (fs.existsSync(contextPath)) {
380
+ this.skippedItems.push('qa/context.md');
381
+ return;
382
+ }
383
+
384
+ const content = this.readTemplate('qa-context.md');
385
+ fs.writeFileSync(contextPath, content, 'utf-8');
386
+ this.createdItems.push('qa/context.md');
387
+ }
388
+
366
389
  /**
367
390
  * Create specs/base.ts for shared browser context
368
391
  */