@sun-asterisk/sungen 2.7.0-beta.1 → 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 (245) 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.map +1 -1
  118. package/dist/orchestrator/project-initializer.js +10 -6
  119. package/dist/orchestrator/project-initializer.js.map +1 -1
  120. package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
  121. package/dist/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
  122. package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
  123. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
  124. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
  125. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +36 -12
  126. package/dist/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
  127. package/dist/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
  128. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
  129. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +4 -1
  130. package/dist/orchestrator/templates/ai-instructions/claude-config.md +1 -4
  131. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
  132. package/dist/orchestrator/templates/ai-instructions/{github-skill-sungen-figma-source.md → claude-skill-capture-mode-figma-pat.md} +14 -48
  133. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
  134. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
  135. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
  136. package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
  137. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -1
  138. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
  139. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
  140. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +18 -10
  141. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
  142. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
  143. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
  144. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +2 -1
  145. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
  146. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
  147. 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
  148. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
  149. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
  150. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
  151. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
  152. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -1
  153. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
  154. package/dist/orchestrator/templates/specs-test-data.ts +9 -0
  155. package/dist/tools/figma/figma-auth.d.ts +5 -2
  156. package/dist/tools/figma/figma-auth.d.ts.map +1 -1
  157. package/dist/tools/figma/figma-auth.js +19 -9
  158. package/dist/tools/figma/figma-auth.js.map +1 -1
  159. package/docs/orchestration-spec.md +267 -0
  160. package/package.json +10 -6
  161. package/src/cli/commands/add.ts +3 -3
  162. package/src/cli/commands/audit.ts +92 -0
  163. package/src/cli/commands/blindspot.ts +48 -0
  164. package/src/cli/commands/challenge.ts +55 -0
  165. package/src/cli/commands/feedback.ts +65 -0
  166. package/src/cli/commands/generate.ts +19 -0
  167. package/src/cli/commands/ledger.ts +61 -0
  168. package/src/cli/commands/manifest.ts +55 -0
  169. package/src/cli/commands/script-check.ts +50 -0
  170. package/src/cli/commands/trace.ts +60 -0
  171. package/src/cli/commands/update.ts +30 -10
  172. package/src/cli/index.ts +16 -0
  173. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
  174. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
  175. package/src/generators/test-generator/patterns/capture-patterns.ts +59 -0
  176. package/src/generators/test-generator/patterns/index.ts +2 -0
  177. package/src/generators/test-generator/step-mapper.ts +1 -0
  178. package/src/generators/test-generator/utils/data-resolver.ts +20 -0
  179. package/src/harness/audit.ts +112 -0
  180. package/src/harness/blindspot.ts +51 -0
  181. package/src/harness/catalog/universal-viewpoints.yaml +114 -0
  182. package/src/harness/challenge.ts +131 -0
  183. package/src/harness/feedback.ts +84 -0
  184. package/src/harness/intent.ts +58 -0
  185. package/src/harness/ledger.ts +155 -0
  186. package/src/harness/manifest.ts +173 -0
  187. package/src/harness/parse.ts +145 -0
  188. package/src/harness/script-check.ts +149 -0
  189. package/src/harness/secret-scan.ts +51 -0
  190. package/src/harness/sensors.ts +279 -0
  191. package/src/harness/trace.ts +138 -0
  192. package/src/orchestrator/ai-rules-updater.ts +57 -10
  193. package/src/orchestrator/figma/spec-figma-renderer.ts +2 -2
  194. package/src/orchestrator/figma/spec-figma-section-renderers.ts +1 -1
  195. package/src/orchestrator/project-initializer.ts +10 -7
  196. package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
  197. package/src/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
  198. package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
  199. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
  200. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
  201. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +36 -12
  202. package/src/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
  203. package/src/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
  204. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
  205. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +4 -1
  206. package/src/orchestrator/templates/ai-instructions/claude-config.md +1 -4
  207. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
  208. 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
  209. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
  210. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
  211. package/src/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
  212. package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
  213. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -1
  214. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
  215. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
  216. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +18 -10
  217. package/src/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
  218. package/src/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
  219. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
  220. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +2 -1
  221. package/src/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
  222. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
  223. 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
  224. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
  225. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
  226. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
  227. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
  228. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -1
  229. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
  230. package/src/orchestrator/templates/specs-test-data.ts +9 -0
  231. package/src/tools/figma/figma-auth.ts +20 -9
  232. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
  233. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
  234. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
  235. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
  236. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
  237. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
  238. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
  239. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
  240. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
  241. package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +0 -151
  242. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
  243. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
  244. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
  245. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +0 -151
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Manifest + spec-fingerprint — make testcases reflect the SOFTWARE, not chase it.
3
+ *
4
+ * Records, per scenario, which spec section it was derived from + a hash of that
5
+ * section. On re-run, diff current spec hashes vs the manifest to classify each
6
+ * scenario: keep / regenerate / retire — so the orchestrator regenerates only the
7
+ * parts whose spec changed (stable + cheap), and never silently drifts.
8
+ *
9
+ * Deterministic. Manifest lives at .sungen/manifest/<screen>.json.
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { createHash } from 'crypto';
14
+
15
+ export interface SpecSection { name: string; hash: string }
16
+ export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
17
+ export interface Manifest {
18
+ screen: string;
19
+ builtAt: string;
20
+ specSections: Record<string, string>; // section name → hash (snapshot at build time)
21
+ entries: ManifestEntry[]; // scenario ↔ section ↔ hash
22
+ }
23
+
24
+ export type ChangeKind = 'keep' | 'regenerate' | 'retire';
25
+ export interface ChangePlan {
26
+ screen: string;
27
+ scenarios: { scenario: string; section: string; change: ChangeKind; reason: string }[];
28
+ newSections: string[]; // spec sections with no scenario yet → generate
29
+ removedSections: string[]; // sections in manifest no longer in spec
30
+ summary: { keep: number; regenerate: number; retire: number; newSections: number };
31
+ }
32
+
33
+ function norm(s: string): string {
34
+ return s.replace(/\(Tier[^)]*\)/i, '').replace(/[#*-]/g, '').trim().toLowerCase().replace(/\s+/g, ' ');
35
+ }
36
+ function shortHash(s: string): string {
37
+ return createHash('sha1').update(s).digest('hex').slice(0, 12);
38
+ }
39
+
40
+ const NO_SPEC = '(no-spec-link)';
41
+
42
+ /**
43
+ * Match a feature section name to a spec section name. Feature comments often
44
+ * combine spec sections (e.g. "features items / product cards" → spec "features
45
+ * items"), so fall back to longest substring match in either direction.
46
+ */
47
+ function matchSection(featureSection: string, specNames: string[]): string | null {
48
+ if (specNames.includes(featureSection)) return featureSection;
49
+ let best: string | null = null;
50
+ for (const s of specNames) {
51
+ if (featureSection.includes(s) || s.includes(featureSection)) {
52
+ if (!best || s.length > best.length) best = s;
53
+ }
54
+ }
55
+ return best;
56
+ }
57
+
58
+ /** Parse spec.md into hashable sections (## and ### headings; "Section:" prefix stripped). */
59
+ export function parseSpecSections(specPath: string): SpecSection[] {
60
+ if (!fs.existsSync(specPath)) return [];
61
+ const lines = fs.readFileSync(specPath, 'utf-8').split('\n');
62
+ const sections: { name: string; body: string[] }[] = [];
63
+ let cur: { name: string; body: string[] } | null = null;
64
+ for (const line of lines) {
65
+ const h = line.match(/^#{2,3}\s+(.*)$/);
66
+ if (h) {
67
+ if (cur) sections.push(cur);
68
+ const name = h[1].replace(/^Section:\s*/i, '').trim();
69
+ cur = { name, body: [] };
70
+ } else if (cur) {
71
+ cur.body.push(line);
72
+ }
73
+ }
74
+ if (cur) sections.push(cur);
75
+ return sections.map((s) => ({ name: norm(s.name), hash: shortHash(s.body.join('\n').trim()) }));
76
+ }
77
+
78
+ /** Parse feature into scenario → section mapping using `# --- Section: X ---` comments. */
79
+ function parseFeatureSections(featurePath: string): { scenario: string; vpCode?: string; section: string }[] {
80
+ if (!fs.existsSync(featurePath)) return [];
81
+ const lines = fs.readFileSync(featurePath, 'utf-8').split('\n');
82
+ const out: { scenario: string; vpCode?: string; section: string }[] = [];
83
+ let section = '(unsectioned)';
84
+ for (const raw of lines) {
85
+ const line = raw.trim();
86
+ const sc = line.match(/^#\s*-{2,}\s*Section:\s*(.*?)\s*-{2,}\s*$/i);
87
+ if (sc) { section = norm(sc[1]); continue; }
88
+ const s = line.match(/^Scenario:\s*(.*)$/);
89
+ if (s) {
90
+ const name = s[1].trim();
91
+ const code = name.match(/\bVP-[A-Z]+-\d+/i)?.[0];
92
+ out.push({ scenario: name, vpCode: code, section });
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+
98
+ export function buildManifest(screenDir: string, screenName: string): Manifest {
99
+ const specPath = path.join(screenDir, 'requirements', 'spec.md');
100
+ const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
101
+ const specSections = parseSpecSections(specPath);
102
+ const specMap: Record<string, string> = {};
103
+ for (const s of specSections) specMap[s.name] = s.hash;
104
+
105
+ const specNames = Object.keys(specMap);
106
+ const featScenarios = parseFeatureSections(featurePath);
107
+ const entries: ManifestEntry[] = featScenarios.map((f) => {
108
+ const matched = matchSection(f.section, specNames);
109
+ return {
110
+ scenario: f.scenario,
111
+ vpCode: f.vpCode,
112
+ section: matched ?? f.section, // canonical spec section when matched
113
+ specHash: matched ? specMap[matched] : NO_SPEC,
114
+ };
115
+ });
116
+
117
+ return {
118
+ screen: screenName,
119
+ builtAt: new Date().toISOString(),
120
+ specSections: specMap,
121
+ entries,
122
+ };
123
+ }
124
+
125
+ export function diffManifest(screenDir: string, screenName: string, manifest: Manifest): ChangePlan {
126
+ const specPath = path.join(screenDir, 'requirements', 'spec.md');
127
+ const current = parseSpecSections(specPath);
128
+ const currentMap: Record<string, string> = {};
129
+ for (const s of current) currentMap[s.name] = s.hash;
130
+
131
+ const scenarios = manifest.entries.map((e) => {
132
+ let change: ChangeKind; let reason: string;
133
+ if (e.specHash === NO_SPEC) { change = 'keep'; reason = 'not linked to a spec section (manual/cross-cutting)'; }
134
+ else {
135
+ const now = currentMap[e.section];
136
+ if (now === undefined) { change = 'retire'; reason = `spec section "${e.section}" no longer exists`; }
137
+ else if (now !== e.specHash) { change = 'regenerate'; reason = `spec section "${e.section}" changed (${e.specHash} → ${now})`; }
138
+ else { change = 'keep'; reason = 'spec section unchanged'; }
139
+ }
140
+ return { scenario: e.scenario, section: e.section, change, reason };
141
+ });
142
+
143
+ const manifestSections = new Set(manifest.entries.map((e) => e.section));
144
+ const newSections = Object.keys(currentMap).filter((s) => !manifestSections.has(s) && !manifest.specSections[s]);
145
+ const removedSections = Object.keys(manifest.specSections).filter((s) => currentMap[s] === undefined);
146
+
147
+ return {
148
+ screen: screenName,
149
+ scenarios,
150
+ newSections,
151
+ removedSections,
152
+ summary: {
153
+ keep: scenarios.filter((s) => s.change === 'keep').length,
154
+ regenerate: scenarios.filter((s) => s.change === 'regenerate').length,
155
+ retire: scenarios.filter((s) => s.change === 'retire').length,
156
+ newSections: newSections.length,
157
+ },
158
+ };
159
+ }
160
+
161
+ export function manifestPath(screenName: string): string {
162
+ return path.join(process.cwd(), '.sungen', 'manifest', `${screenName}.json`);
163
+ }
164
+ export function loadManifest(screenName: string): Manifest | null {
165
+ const p = manifestPath(screenName);
166
+ return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : null;
167
+ }
168
+ export function saveManifest(m: Manifest): string {
169
+ const p = manifestPath(m.screen);
170
+ fs.mkdirSync(path.dirname(p), { recursive: true });
171
+ fs.writeFileSync(p, JSON.stringify(m, null, 2), 'utf-8');
172
+ return p;
173
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Harness parsers — load the two test-design artifacts the sensors operate on:
3
+ * 1. requirements/test-viewpoint.md → viewpoint-overview (id, priority, group)
4
+ * 2. features/<screen>.feature → scenarios (vp code, priority, Then-shape)
5
+ *
6
+ * Reuses the existing GherkinParser (framework-agnostic) for the .feature file.
7
+ */
8
+ import * as fs from 'fs';
9
+ import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
10
+
11
+ export type Priority = 'high' | 'normal' | 'low' | 'unknown';
12
+
13
+ export interface ViewpointEntry {
14
+ id: string; // e.g. VP-DATA-CONSISTENCY
15
+ priority: 'High' | 'Medium' | 'Low' | 'Unknown';
16
+ reason: string;
17
+ group?: 'Required' | 'Recommended' | 'Optional';
18
+ }
19
+
20
+ export interface ScenarioInfo {
21
+ name: string;
22
+ vpCode?: string; // VP-CART-001 style code in the scenario title
23
+ category?: string; // CART, LIST, VAL, ... (from the code)
24
+ priority: Priority;
25
+ manual: boolean;
26
+ thenCount: number;
27
+ hasDataAssertion: boolean; // a Then asserts {{data}} / contains / match data
28
+ shallow: boolean; // asserts only visibility/navigation (no data)
29
+ stepSkeleton: string; // normalized steps for duplicate clustering
30
+ haystack: string; // lowercase name + steps text (for keyword coverage)
31
+ }
32
+
33
+ // ---------- test-viewpoint.md ----------
34
+
35
+ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
36
+ if (!fs.existsSync(filePath)) return [];
37
+ const text = fs.readFileSync(filePath, 'utf-8');
38
+ const lines = text.split('\n');
39
+
40
+ const entries = new Map<string, ViewpointEntry>();
41
+
42
+ // 1) Priority Viewpoints table: | VP | Priority | Reason |
43
+ let inPriorityTable = false;
44
+ for (const raw of lines) {
45
+ const line = raw.trim();
46
+ if (/^##\s+Priority Viewpoints/i.test(line)) { inPriorityTable = true; continue; }
47
+ if (inPriorityTable && /^##\s/.test(line)) { inPriorityTable = false; }
48
+ if (inPriorityTable && line.startsWith('|')) {
49
+ const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
50
+ if (cells.length >= 3) {
51
+ const id = cells[0];
52
+ if (/^VP[-A-Z0-9]/i.test(id) && !/^vp$/i.test(id) && !/^-+$/.test(cells[1])) {
53
+ const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
54
+ entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ // 2) Viewpoint Grouping: ### Required / ### Recommended / ### Optional → bullet list
61
+ let group: ViewpointEntry['group'] | undefined;
62
+ for (const raw of lines) {
63
+ const line = raw.trim();
64
+ const g = line.match(/^###\s+(Required|Recommended|Optional)/i);
65
+ if (g) { group = (g[1][0].toUpperCase() + g[1].slice(1).toLowerCase()) as any; continue; }
66
+ if (/^##\s/.test(line)) { group = undefined; }
67
+ if (group) {
68
+ const m = line.match(/^-\s+(VP[-A-Z0-9]+)/i);
69
+ if (m) {
70
+ const id = m[1].toUpperCase();
71
+ const existing = entries.get(id);
72
+ if (existing) existing.group = group;
73
+ else entries.set(id, { id, priority: 'Unknown', reason: '', group });
74
+ }
75
+ }
76
+ }
77
+
78
+ return [...entries.values()];
79
+ }
80
+
81
+ // ---------- .feature ----------
82
+
83
+ const PRIORITY_TAGS: Record<string, Priority> = { '@high': 'high', '@normal': 'normal', '@low': 'low' };
84
+
85
+ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
86
+ const tags = sc.tags || [];
87
+ const manual = tags.includes('@manual');
88
+ let priority: Priority = 'unknown';
89
+ for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
90
+
91
+ const codeMatch = sc.name.match(/\bVP-([A-Z]+)-\d+/i);
92
+ const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
93
+ const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
94
+
95
+ // Then-phase detection (And/But inherit previous primary keyword)
96
+ let last = 'Given';
97
+ let thenCount = 0;
98
+ let hasData = false;
99
+ const skeletonParts: string[] = [];
100
+ const textParts: string[] = [sc.name];
101
+
102
+ for (const step of sc.steps as ParsedStep[]) {
103
+ const kw = step.keyword.trim();
104
+ if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
105
+ textParts.push(step.text);
106
+ // normalized skeleton: keep [refs] (distinct targets = distinct tests),
107
+ // but neutralize {{vars}} and quoted values so EP/data families collapse.
108
+ const skel = step.text
109
+ .replace(/\{\{[^}]*\}\}/g, '{}')
110
+ .replace(/"[^"]*"/g, '""')
111
+ .replace(/\s+/g, ' ')
112
+ .trim()
113
+ .toLowerCase();
114
+ skeletonParts.push(`${kw === 'And' || kw === 'But' ? last : kw}:${skel}`);
115
+
116
+ if (last === 'Then') {
117
+ thenCount++;
118
+ if (/\{\{|contains|match data|toHaveText/i.test(step.text)) hasData = true;
119
+ }
120
+ }
121
+
122
+ const shallow = thenCount > 0 && !hasData;
123
+
124
+ return {
125
+ name: sc.name,
126
+ vpCode,
127
+ category,
128
+ priority,
129
+ manual,
130
+ thenCount,
131
+ hasDataAssertion: hasData,
132
+ shallow,
133
+ stepSkeleton: skeletonParts.join(' | '),
134
+ haystack: textParts.join(' ').toLowerCase(),
135
+ };
136
+ }
137
+
138
+ export function loadScenarios(featurePath: string): ScenarioInfo[] {
139
+ if (!fs.existsSync(featurePath)) return [];
140
+ const parser = new GherkinParser();
141
+ const feature = parser.parseFeatureFile(featurePath);
142
+ return (feature.scenarios || [])
143
+ .filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
144
+ .map(classifyScenario);
145
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Script-check — verify the generated Playwright spec is a faithful 1:1 of the
3
+ * Gherkin feature, i.e. "the testcase and the test code are not two different things".
4
+ *
5
+ * Two deterministic checks:
6
+ * A. Structural 1:1 — every non-@manual / non-@steps scenario has exactly one
7
+ * matching `test('<title>')` block in the committed spec (and no extras).
8
+ * B. Drift — regenerate the spec from the SAME .feature + selectors + test-data
9
+ * into a temp dir and diff against the committed spec. Any difference means
10
+ * the committed spec was hand-edited or is stale (feature changed without a
11
+ * regenerate) → the script no longer reflects the testcase.
12
+ *
13
+ * Pure-deterministic (reuses the compiler). No AI.
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import * as os from 'os';
18
+ import { loadScenarios } from './parse';
19
+
20
+ export interface ScriptCheckResult {
21
+ screen: string;
22
+ specPath: string | null;
23
+ automatedScenarios: number; // non-manual, non-steps
24
+ manualScenarios: number;
25
+ specTestBlocks: number;
26
+ countMatch: boolean;
27
+ missingInSpec: string[]; // scenario titles with no test() block
28
+ extraInSpec: string[]; // test() titles with no scenario
29
+ drift: 'in-sync' | 'drift' | 'no-spec';
30
+ driftHunks: string[]; // sample differing lines (committed vs regenerated)
31
+ status: 'OK' | 'FAIL';
32
+ findings: string[];
33
+ }
34
+
35
+ function extractTestTitles(specSrc: string): string[] {
36
+ // Count real test cases only: test(...), test.only/.skip/.fixme(...).
37
+ // Exclude test.describe / test.beforeAll / hooks (not test cases).
38
+ const titles: string[] = [];
39
+ const re = /\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/g;
40
+ let m: RegExpExecArray | null;
41
+ while ((m = re.exec(specSrc))) titles.push(m[2].trim());
42
+ return titles;
43
+ }
44
+
45
+ function normalize(src: string): string {
46
+ return src
47
+ .split('\n')
48
+ .map((l) => l.replace(/\s+$/, ''))
49
+ .join('\n')
50
+ .replace(/\n{3,}/g, '\n\n')
51
+ .trim();
52
+ }
53
+
54
+ function findSpec(dir: string, screen: string): string | null {
55
+ // generated spec: <dir>/<screen>/<feature>.spec.ts (or nested)
56
+ const hits: string[] = [];
57
+ const walk = (d: string) => {
58
+ if (!fs.existsSync(d)) return;
59
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
60
+ const p = path.join(d, e.name);
61
+ if (e.isDirectory()) walk(p);
62
+ else if (e.name.endsWith('.spec.ts')) hits.push(p);
63
+ }
64
+ };
65
+ walk(dir);
66
+ return hits[0] ?? null;
67
+ }
68
+
69
+ export async function runScriptCheck(screenDir: string, screenName: string, flowMode: boolean): Promise<ScriptCheckResult> {
70
+ const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
71
+ const scenarios = loadScenarios(featurePath);
72
+ const automated = scenarios.filter((s) => !s.manual);
73
+ const manual = scenarios.filter((s) => s.manual);
74
+
75
+ const committedSpec = findSpec(path.join(process.cwd(), 'specs', 'generated'), screenName);
76
+
77
+ const findings: string[] = [];
78
+ let specTitles: string[] = [];
79
+ let specSrc = '';
80
+ if (committedSpec) {
81
+ specSrc = fs.readFileSync(committedSpec, 'utf-8');
82
+ specTitles = extractTestTitles(specSrc);
83
+ } else {
84
+ findings.push('No generated spec found under specs/generated/ — run `sungen generate` / `/sungen:run-test` first.');
85
+ }
86
+
87
+ // A. Structural 1:1
88
+ const specTitleSet = new Set(specTitles);
89
+ const scenTitleSet = new Set(automated.map((s) => s.name));
90
+ const missingInSpec = automated.filter((s) => !specTitleSet.has(s.name)).map((s) => s.name);
91
+ const extraInSpec = specTitles.filter((t) => !scenTitleSet.has(t));
92
+ const countMatch = committedSpec ? automated.length === specTitles.length : false;
93
+ if (committedSpec && !countMatch) {
94
+ findings.push(`Count mismatch: ${automated.length} automated scenarios vs ${specTitles.length} test() blocks.`);
95
+ }
96
+ for (const t of missingInSpec) findings.push(`MISSING in spec: scenario "${t}" has no test() block (stale spec — regenerate).`);
97
+ for (const t of extraInSpec) findings.push(`EXTRA in spec: test "${t}" has no matching scenario (hand-edited spec).`);
98
+
99
+ // B. Drift — regenerate to temp and diff
100
+ let drift: ScriptCheckResult['drift'] = committedSpec ? 'in-sync' : 'no-spec';
101
+ const driftHunks: string[] = [];
102
+ if (committedSpec) {
103
+ try {
104
+ const { CodeGenerator } = require('../generators/test-generator/code-generator');
105
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sungen-scriptcheck-'));
106
+ const qaSourceDir = path.join(process.cwd(), 'qa', flowMode ? 'flows' : 'screens');
107
+ const gen = new CodeGenerator({ framework: 'playwright', screenName, runtimeData: true, flowMode });
108
+ await gen.generateAllTests(qaSourceDir, tmp, [featurePath]);
109
+ const fresh = findSpec(tmp, screenName);
110
+ if (fresh) {
111
+ const a = normalize(specSrc);
112
+ const b = normalize(fs.readFileSync(fresh, 'utf-8'));
113
+ if (a !== b) {
114
+ drift = 'drift';
115
+ // collect a few differing lines
116
+ const al = a.split('\n'), bl = b.split('\n');
117
+ const max = Math.max(al.length, bl.length);
118
+ for (let i = 0, shown = 0; i < max && shown < 6; i++) {
119
+ if (al[i] !== bl[i]) {
120
+ driftHunks.push(` L${i + 1}\n committed: ${(al[i] ?? '∅').trim().slice(0, 100)}\n expected : ${(bl[i] ?? '∅').trim().slice(0, 100)}`);
121
+ shown++;
122
+ }
123
+ }
124
+ findings.push('DRIFT: committed spec differs from a fresh regenerate → spec was hand-edited or the .feature changed without `sungen generate`. The test code no longer reflects the Gherkin.');
125
+ }
126
+ }
127
+ fs.rmSync(tmp, { recursive: true, force: true });
128
+ } catch (e) {
129
+ findings.push(`Drift check skipped (regenerate failed): ${e instanceof Error ? e.message : e}`);
130
+ }
131
+ }
132
+
133
+ const ok = !!committedSpec && countMatch && missingInSpec.length === 0 && extraInSpec.length === 0 && drift === 'in-sync';
134
+
135
+ return {
136
+ screen: screenName,
137
+ specPath: committedSpec,
138
+ automatedScenarios: automated.length,
139
+ manualScenarios: manual.length,
140
+ specTestBlocks: specTitles.length,
141
+ countMatch,
142
+ missingInSpec,
143
+ extraInSpec,
144
+ drift,
145
+ driftHunks,
146
+ status: ok ? 'OK' : 'FAIL',
147
+ findings,
148
+ };
149
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Secret scanner (security S0) — warn (never block) when test-data appears to
3
+ * contain a REAL secret rather than a safe placeholder/test value.
4
+ *
5
+ * test-data/*.yaml is committed by design, so a real API key / prod token there is
6
+ * a leak waiting to happen. We flag high-confidence signals only, to avoid nagging
7
+ * about ordinary test passwords ("Test@123") or `{{vars}}`.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ export interface SecretHit { file: string; line: number; reason: string }
13
+
14
+ // High-confidence vendor token prefixes (very low false-positive rate).
15
+ const VENDOR_PREFIXES = /\b(figd_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})/;
16
+ // A secret-named key assigned a long, non-placeholder value.
17
+ const SECRET_KEY = /\b(password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\b\s*:\s*(.+)$/i;
18
+ const PLACEHOLDERish = /\{\{|<.*>|changeme|example|dummy|test\b|placeholder|xxxx|\*\*\*/i;
19
+
20
+ function scanText(text: string, file: string): SecretHit[] {
21
+ const hits: SecretHit[] = [];
22
+ text.split('\n').forEach((raw, i) => {
23
+ const line = i + 1;
24
+ if (VENDOR_PREFIXES.test(raw)) {
25
+ hits.push({ file, line, reason: 'looks like a real vendor API token/key' });
26
+ return;
27
+ }
28
+ const m = raw.match(SECRET_KEY);
29
+ if (m) {
30
+ const val = m[2].trim().replace(/^["']|["']$/g, '');
31
+ // Long, high-entropy-ish, not an obvious placeholder/test value.
32
+ if (val.length >= 20 && !PLACEHOLDERish.test(raw) && /[A-Za-z]/.test(val) && /[0-9]/.test(val)) {
33
+ hits.push({ file, line, reason: `secret-named key "${m[1]}" with a long literal value` });
34
+ }
35
+ }
36
+ });
37
+ return hits;
38
+ }
39
+
40
+ /** Scan a screen/flow dir's test-data/*.yaml for likely real secrets. */
41
+ export function scanTestDataSecrets(baseDir: string): SecretHit[] {
42
+ const tdDir = path.join(baseDir, 'test-data');
43
+ if (!fs.existsSync(tdDir)) return [];
44
+ const hits: SecretHit[] = [];
45
+ for (const f of fs.readdirSync(tdDir)) {
46
+ if (!/\.ya?ml$/i.test(f)) continue;
47
+ const p = path.join(tdDir, f);
48
+ try { hits.push(...scanText(fs.readFileSync(p, 'utf-8'), path.join('test-data', f))); } catch { /* ignore */ }
49
+ }
50
+ return hits;
51
+ }