@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,61 @@
1
+ import { Command } from 'commander';
2
+ import { recordEvent, buildReport } from '../../harness/ledger';
3
+
4
+ export function registerLedgerCommand(program: Command): void {
5
+ const ledger = program
6
+ .command('ledger')
7
+ .description('Usage ledger: record AI resource per step + report efficiency');
8
+
9
+ ledger
10
+ .command('record')
11
+ .description('Append a step event to the ledger')
12
+ .requiredOption('-s, --screen <name>', 'Screen or flow name')
13
+ .requiredOption('--step <name>', 'Step name (discovery | viewpoint | gherkin | audit | repair:1 ...)')
14
+ .option('--run <id>', 'Run id — groups all phases of one create-test invocation (else auto-segmented by time gap)')
15
+ .option('--model <id>', 'Model id')
16
+ .option('--tokens-in <n>', 'Input tokens', (v) => parseInt(v, 10))
17
+ .option('--tokens-out <n>', 'Output tokens', (v) => parseInt(v, 10))
18
+ .option('--ms <n>', 'Duration in ms', (v) => parseInt(v, 10))
19
+ .option('--note <text>', 'Free note')
20
+ .action((o) => {
21
+ try {
22
+ recordEvent(o.screen, {
23
+ step: o.step, runId: o.run, model: o.model, tokensIn: o.tokensIn, tokensOut: o.tokensOut, ms: o.ms, note: o.note,
24
+ });
25
+ console.log(`✓ ledger: ${o.screen} · ${o.step}`);
26
+ } catch (e) {
27
+ console.error('Error:', e instanceof Error ? e.message : e);
28
+ process.exit(1);
29
+ }
30
+ });
31
+
32
+ ledger
33
+ .command('report')
34
+ .description('Summarise ledger + efficiency verdicts (pulls audit score if present)')
35
+ .requiredOption('-s, --screen <name>', 'Screen or flow name')
36
+ .option('--all-runs', 'Aggregate ALL runs (default: latest run only)')
37
+ .option('--json', 'Output raw JSON')
38
+ .action((o) => {
39
+ try {
40
+ const r = buildReport(o.screen, { allRuns: o.allRuns });
41
+ if (o.json) { console.log(JSON.stringify(r, null, 2)); return; }
42
+ const scope = r.runScope === 'all' ? `all ${r.runs} runs` : `latest run${r.runs > 1 ? ` of ${r.runs}` : ''}`;
43
+ console.log(`\n━━━ Usage Ledger: ${r.screen} (${r.events} events · ${scope}) ━━━`);
44
+ if (r.runScope === 'latest' && r.runs > 1) console.log(` (${r.runs} runs on file — use --all-runs to aggregate)`);
45
+ console.log(` total tokens: ${r.totalTokens} total time: ${(r.totalMs / 1000).toFixed(1)}s repair rounds: ${r.repairRounds}`);
46
+ if (r.totalTokens) console.log(` repair token share: ${(r.repairTokenPct * 100).toFixed(0)}%`);
47
+ if (r.tokensPerCoveredCritical != null) console.log(` tokens / covered-critical-viewpoint: ${r.tokensPerCoveredCritical}`);
48
+ if (r.tokensPerScenario != null) console.log(` tokens / scenario: ${r.tokensPerScenario}`);
49
+ console.log(' by step:');
50
+ for (const [step, v] of Object.entries(r.byStep)) {
51
+ console.log(` ${step.padEnd(14)} tokens=${v.tokens} ms=${v.ms} events=${v.count}`);
52
+ }
53
+ console.log(' verdict:');
54
+ for (const v of r.verdicts) console.log(` • ${v}`);
55
+ console.log('');
56
+ } catch (e) {
57
+ console.error('Error:', e instanceof Error ? e.message : e);
58
+ process.exit(1);
59
+ }
60
+ });
61
+ }
@@ -0,0 +1,55 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { buildManifest, diffManifest, loadManifest, saveManifest } from '../../harness/manifest';
5
+
6
+ function findScreenDir(name: string): string | null {
7
+ const screen = path.join(process.cwd(), 'qa', 'screens', name);
8
+ if (fs.existsSync(screen)) return screen;
9
+ const flow = path.join(process.cwd(), 'qa', 'flows', name);
10
+ if (fs.existsSync(flow)) return flow;
11
+ return null;
12
+ }
13
+
14
+ export function registerManifestCommand(program: Command): void {
15
+ program
16
+ .command('manifest')
17
+ .description('Spec-fingerprint manifest: build a scenario↔spec-section map, or diff to plan keep/regenerate/retire')
18
+ .option('-s, --screen <name>', 'Screen or flow name')
19
+ .option('--diff', 'Compare current spec vs stored manifest → change plan')
20
+ .option('--json', 'Output raw JSON')
21
+ .action((options) => {
22
+ try {
23
+ const name = options.screen;
24
+ if (!name) throw new Error('Provide --screen <name>');
25
+ const dir = findScreenDir(name);
26
+ if (!dir) throw new Error(`Screen/flow not found: ${name}`);
27
+
28
+ if (options.diff) {
29
+ const manifest = loadManifest(name);
30
+ if (!manifest) throw new Error(`No manifest for "${name}". Run \`sungen manifest --screen ${name}\` first.`);
31
+ const plan = diffManifest(dir, name, manifest);
32
+ if (options.json) { console.log(JSON.stringify(plan, null, 2)); process.exit(0); }
33
+ console.log(`\n━━━ Spec-change plan: ${name} ━━━`);
34
+ console.log(` keep=${plan.summary.keep} regenerate=${plan.summary.regenerate} retire=${plan.summary.retire} newSections=${plan.summary.newSections}\n`);
35
+ for (const s of plan.scenarios.filter((x) => x.change !== 'keep')) {
36
+ console.log(` ${s.change === 'regenerate' ? '↻ REGENERATE' : '✗ RETIRE'}: ${s.scenario}`);
37
+ console.log(` ${s.reason}`);
38
+ }
39
+ if (plan.newSections.length) console.log(`\n ✚ NEW spec sections (no scenario yet): ${plan.newSections.join(', ')}`);
40
+ if (plan.summary.regenerate + plan.summary.retire + plan.summary.newSections === 0) console.log(' ✓ Spec unchanged — all scenarios still reflect the spec.');
41
+ console.log('');
42
+ return;
43
+ }
44
+
45
+ const manifest = buildManifest(dir, name);
46
+ const out = saveManifest(manifest);
47
+ if (options.json) { console.log(JSON.stringify(manifest, null, 2)); return; }
48
+ console.log(`\n✓ Manifest built: ${manifest.entries.length} scenarios across ${Object.keys(manifest.specSections).length} spec sections`);
49
+ console.log(` ${path.relative(process.cwd(), out)}\n`);
50
+ } catch (error) {
51
+ console.error('Error:', error instanceof Error ? error.message : error);
52
+ process.exit(1);
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,50 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { runScriptCheck } from '../../harness/script-check';
5
+
6
+ export function registerScriptCheckCommand(program: Command): void {
7
+ program
8
+ .command('script-check')
9
+ .description('Verify the generated Playwright spec is a faithful 1:1 of the Gherkin feature (no hand-edit / stale drift)')
10
+ .option('-s, --screen <name>', 'Screen or flow name')
11
+ .option('--json', 'Output raw JSON')
12
+ .action(async (options) => {
13
+ try {
14
+ const name = options.screen;
15
+ if (!name) throw new Error('Provide --screen <name>');
16
+ const screen = path.join(process.cwd(), 'qa', 'screens', name);
17
+ const flow = path.join(process.cwd(), 'qa', 'flows', name);
18
+ const flowMode = !fs.existsSync(screen) && fs.existsSync(flow);
19
+ const dir = fs.existsSync(screen) ? screen : (fs.existsSync(flow) ? flow : null);
20
+ if (!dir) throw new Error(`Screen/flow not found: ${name}`);
21
+
22
+ const r = await runScriptCheck(dir, name, flowMode);
23
+
24
+ const outDir = path.join(process.cwd(), '.sungen', 'reports');
25
+ fs.mkdirSync(outDir, { recursive: true });
26
+ fs.writeFileSync(path.join(outDir, `${name}-script-check.json`), JSON.stringify(r, null, 2), 'utf-8');
27
+
28
+ if (options.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.status === 'OK' ? 0 : 2); }
29
+
30
+ const L = console.log;
31
+ L('');
32
+ L(`━━━ Script-check: ${name} — Gherkin ↔ Playwright 1:1 ━━━`);
33
+ L('');
34
+ L(` status: ${r.status === 'OK' ? '✓ IN SYNC' : '✗ ' + (r.drift === 'drift' ? 'DRIFT' : 'MISMATCH')}`);
35
+ L(` scenarios: ${r.automatedScenarios} automated (+${r.manualScenarios} @manual) spec test() blocks: ${r.specTestBlocks} count-match: ${r.countMatch ? '✓' : '✗'}`);
36
+ L(` drift: ${r.drift}`);
37
+ if (r.driftHunks.length) { L(' differing lines (committed vs fresh regenerate):'); for (const h of r.driftHunks) L(h); }
38
+ L('');
39
+ if (r.findings.length) { L(' findings:'); for (const f of r.findings) L(` • ${f}`); }
40
+ else L(' ✓ The test code faithfully reflects the Gherkin (1:1).');
41
+ L('');
42
+ if (r.drift === 'drift') L(' → Fix: re-run `sungen generate --screen ' + name + '` (or /sungen:run-test) so the spec matches the feature. Never hand-edit generated specs.');
43
+ L('');
44
+ process.exit(r.status === 'OK' ? 0 : 2);
45
+ } catch (error) {
46
+ console.error('Error:', error instanceof Error ? error.message : error);
47
+ process.exit(1);
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,60 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { buildTrace } from '../../harness/trace';
5
+
6
+ export function registerTraceCommand(program: Command): void {
7
+ program
8
+ .command('trace')
9
+ .description('Visualise the executed test-design process (workflow/skill steps, repair loops), find bottlenecks, and show where to focus human review')
10
+ .option('-s, --screen <name>', 'Screen or flow name')
11
+ .option('--json', 'Output raw JSON')
12
+ .option('--mermaid', 'Print only the Mermaid flowchart')
13
+ .action((options) => {
14
+ try {
15
+ const name = options.screen;
16
+ if (!name) throw new Error('Provide --screen <name>');
17
+ const screen = path.join(process.cwd(), 'qa', 'screens', name);
18
+ const flow = path.join(process.cwd(), 'qa', 'flows', name);
19
+ const dir = fs.existsSync(screen) ? screen : (fs.existsSync(flow) ? flow : null);
20
+ if (!dir) throw new Error(`Screen/flow not found: ${name}`);
21
+
22
+ const r = buildTrace(dir, name);
23
+ if (options.json) { console.log(JSON.stringify(r, null, 2)); return; }
24
+ if (options.mermaid) { console.log(r.mermaid); return; }
25
+
26
+ const L = console.log;
27
+ L('');
28
+ L(`━━━ Process Trace: ${name} ━━━`);
29
+ L('');
30
+ L(` ① Executed process (from ledger${r.runs > 1 ? ` — latest of ${r.runs} runs` : ''})`);
31
+ if (r.ledger.length) {
32
+ for (const e of r.ledger) L(` → ${e.step.padEnd(12)} ${e.ms}ms`);
33
+ L(` repair rounds: ${r.repairRounds} total recorded: ${(r.totalMs / 1000).toFixed(1)}s`);
34
+ } else {
35
+ L(' (ledger empty — process not instrumented this run)');
36
+ }
37
+ if (r.missingSteps.length) L(` ⚠ phases not recorded: ${r.missingSteps.join(', ')}`);
38
+ L('');
39
+ L(' ② Quality signals');
40
+ if (r.audit) L(` audit score=${r.audit.score}/10 gate=${r.audit.gate} weakest=${r.audit.weakest} findings=${r.audit.findings}`);
41
+ else L(' (no audit report — run `sungen audit`)');
42
+ L(` script-check drift: ${r.drift ?? '(not run)'}`);
43
+ L('');
44
+ L(' ③ Bottlenecks / weak points');
45
+ for (const b of r.bottlenecks) L(` • ${b}`);
46
+ L('');
47
+ L(' ④ HUMAN-LOOP FOCUS — where you (the QA) must look');
48
+ for (const h of r.humanFocus) L(` ${h.startsWith(' ') ? h : '• ' + h}`);
49
+ L('');
50
+ L(' ⑤ Visual map (Mermaid — paste into mermaid.live or a Markdown viewer)');
51
+ L(' ```mermaid');
52
+ for (const line of r.mermaid.split('\n')) L(' ' + line);
53
+ L(' ```');
54
+ L('');
55
+ } catch (error) {
56
+ console.error('Error:', error instanceof Error ? error.message : error);
57
+ process.exit(1);
58
+ }
59
+ });
60
+ }
@@ -27,22 +27,35 @@ export function registerUpdateCommand(program: Command): void {
27
27
  program
28
28
  .command('update')
29
29
  .description(
30
- 'Reinstall @sun-asterisk/sungen@latest + refresh AI rules, commands, and skills',
30
+ 'Reinstall @sun-asterisk/sungen (stable by default, --beta for prerelease) + refresh AI rules, commands, skills',
31
31
  )
32
32
  .option('--dry-run', 'Show what would be updated without making changes')
33
+ .option(
34
+ '--beta',
35
+ 'Switch to the BETA channel (installs @beta — prerelease builds for testing)',
36
+ false,
37
+ )
38
+ .option(
39
+ '--tag <dist-tag>',
40
+ 'Install a specific npm dist-tag (e.g. latest, beta, next). Overrides --beta.',
41
+ )
33
42
  .option(
34
43
  '--skip-npm-install',
35
- 'Skip the `npm install -g @sun-asterisk/sungen@latest` step (refresh project AI assets only)',
44
+ 'Skip the npm install step (refresh project AI assets only)',
36
45
  false,
37
46
  )
38
- .action(async (options: { dryRun?: boolean; skipNpmInstall?: boolean }) => {
47
+ .action(async (options: { dryRun?: boolean; beta?: boolean; tag?: string; skipNpmInstall?: boolean }) => {
39
48
  try {
40
49
  const skipNpm =
41
50
  Boolean(options.skipNpmInstall) || process.env[SKIP_NPM_ENV] === '1';
42
51
 
52
+ // Channel resolution via npm dist-tags. Default `latest` = official/stable.
53
+ // `--beta` → beta channel; running plain `sungen update` later switches back to stable.
54
+ const channel = options.tag || (options.beta ? 'beta' : 'latest');
55
+
43
56
  if (!skipNpm) {
44
- reinstallLatestSungen();
45
- printCurrentVersion();
57
+ reinstallSungen(channel);
58
+ printCurrentVersion(channel);
46
59
  reExecUpdateForAIAssets(options.dryRun ?? false);
47
60
  return;
48
61
  }
@@ -57,22 +70,29 @@ export function registerUpdateCommand(program: Command): void {
57
70
  });
58
71
  }
59
72
 
60
- function reinstallLatestSungen(): void {
61
- console.log('📦 Installing @sun-asterisk/sungen@latest...');
62
- const result = spawnSync('npm', ['install', '-g', '@sun-asterisk/sungen@latest'], {
73
+ function reinstallSungen(channel: string): void {
74
+ const spec = `@sun-asterisk/sungen@${channel}`;
75
+ const label = channel === 'latest' ? 'stable (latest)' : `${channel} channel`;
76
+ console.log(`📦 Installing ${spec} → ${label}...`);
77
+ const result = spawnSync('npm', ['install', '-g', spec], {
63
78
  stdio: 'inherit',
64
79
  shell: true,
65
80
  });
66
81
  if (result.status !== 0) {
67
82
  throw new Error(
68
- 'npm install -g @sun-asterisk/sungen@latest failed. Run it manually or check your npm setup.',
83
+ `npm install -g ${spec} failed. Run it manually or check your npm setup.`,
69
84
  );
70
85
  }
71
86
  }
72
87
 
73
- function printCurrentVersion(): void {
88
+ function printCurrentVersion(channel: string): void {
74
89
  console.log('\n🔎 Installed version:');
75
90
  spawnSync('sungen', ['--version'], { stdio: 'inherit', shell: true });
91
+ if (channel === 'latest') {
92
+ console.log(' Channel: stable. To try prereleases: sungen update --beta');
93
+ } else {
94
+ console.log(` Channel: ${channel} (prerelease). To return to stable: sungen update`);
95
+ }
76
96
  console.log('');
77
97
  }
78
98
 
package/src/cli/index.ts CHANGED
@@ -14,6 +14,14 @@ import { registerDeliveryCommand } from './commands/delivery';
14
14
  import { registerFigmaCommand } from './commands/figma';
15
15
  import { registerAddFlowCommand } from './commands/add-flow';
16
16
  import { registerDashboardCommand } from './commands/dashboard';
17
+ import { registerAuditCommand } from './commands/audit';
18
+ import { registerManifestCommand } from './commands/manifest';
19
+ import { registerLedgerCommand } from './commands/ledger';
20
+ import { registerFeedbackCommand } from './commands/feedback';
21
+ import { registerScriptCheckCommand } from './commands/script-check';
22
+ import { registerTraceCommand } from './commands/trace';
23
+ import { registerChallengeCommand } from './commands/challenge';
24
+ import { registerBlindspotCommand } from './commands/blindspot';
17
25
 
18
26
  // Read version from package.json so `--version` never drifts from the released version.
19
27
  const { version } = require('../../package.json') as { version: string };
@@ -40,6 +48,14 @@ async function main() {
40
48
  registerFigmaCommand(program);
41
49
  registerAddFlowCommand(program);
42
50
  registerDashboardCommand(program);
51
+ registerAuditCommand(program);
52
+ registerManifestCommand(program);
53
+ registerLedgerCommand(program);
54
+ registerFeedbackCommand(program);
55
+ registerScriptCheckCommand(program);
56
+ registerTraceCommand(program);
57
+ registerChallengeCommand(program);
58
+ registerBlindspotCommand(program);
43
59
 
44
60
  await program.parseAsync(process.argv);
45
61
  }
@@ -0,0 +1 @@
1
+ testData.set('{{varName}}', ((await {{> locator}}.{{capture}}()) ?? '').trim());
@@ -0,0 +1,7 @@
1
+ {
2
+ const __items_{{stepCounter}} = await {{> locator}}.allInnerTexts();
3
+ expect(__items_{{stepCounter}}.length, 'Expected at least one [{{selectorRef}}]').toBeGreaterThan(0);
4
+ for (const __t_{{stepCounter}} of __items_{{stepCounter}}) {
5
+ expect(__t_{{stepCounter}}).toContain('{{expectedText}}');
6
+ }
7
+ }
@@ -0,0 +1,59 @@
1
+ import { ParsedStep } from '../../gherkin-parser';
2
+ import { StepPattern, StepTemplateData } from './types';
3
+
4
+ /**
5
+ * Capture & collection patterns (P5) — enable cross-screen data consistency and
6
+ * filter-result correctness that plain single-element assertions can't express.
7
+ *
8
+ * 1. Capture: `User remember [Product Name] text as {{selected_product_name}}`
9
+ * → stores the element's text/value into a runtime variable so a
10
+ * later step (on another screen) can assert against it.
11
+ * REQUIRES runtime data mode (default) — emits `testData.set(...)`.
12
+ *
13
+ * 2. List: `User see all [Product Name] contain {{selected_category}}`
14
+ * → asserts EVERY matching element's text contains the value
15
+ * (e.g. all products belong to the selected category/brand).
16
+ */
17
+ export const capturePatterns: StepPattern[] = [
18
+ {
19
+ name: 'capture-variable',
20
+ matcher: (step: ParsedStep) =>
21
+ /\bremember\b/i.test(step.text) && /\bas\b/i.test(step.text) && !!step.selectorRef && !!step.dataRef,
22
+ resolver: (step, context): StepTemplateData => {
23
+ const resolved = context.selectorResolver.resolveSelector(
24
+ step.selectorRef!, undefined, step.elementType, step.nth,
25
+ );
26
+ const varName = step.dataRef!;
27
+ const isValue = /\bvalue\b/i.test(step.text);
28
+ // Register so later `{{varName}}` references resolve to testData.get(varName)
29
+ // and skip YAML validation.
30
+ context.dataResolver.registerCaptured(varName);
31
+ return {
32
+ templateName: 'capture-variable',
33
+ data: { ...resolved, varName, capture: isValue ? 'inputValue' : 'textContent' },
34
+ comment: `Remember ${step.selectorRef} ${isValue ? 'value' : 'text'} as ${varName}`,
35
+ };
36
+ },
37
+ priority: 35,
38
+ },
39
+ {
40
+ name: 'all-contain-assertion',
41
+ matcher: (step: ParsedStep) =>
42
+ /\b(see|sees)\b/i.test(step.text) &&
43
+ /\ball\b/i.test(step.text) &&
44
+ /(contain|contains|match|matches|belong)/i.test(step.text) &&
45
+ !!step.selectorRef && !!(step.value || step.dataRef),
46
+ resolver: (step, context): StepTemplateData => {
47
+ const resolved = context.selectorResolver.resolveSelector(
48
+ step.selectorRef!, undefined, step.elementType, step.nth,
49
+ );
50
+ const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
51
+ return {
52
+ templateName: 'all-contain-assertion',
53
+ data: { ...resolved, expectedText, selectorRef: step.selectorRef, stepCounter: context.stepCounter },
54
+ comment: `Assert all ${step.selectorRef} contain "${step.value || step.dataRef}"`,
55
+ };
56
+ },
57
+ priority: 34,
58
+ },
59
+ ];
@@ -10,6 +10,7 @@ import { keyboardPatterns } from './keyboard-patterns';
10
10
  import { scrollPatterns } from './scroll-patterns';
11
11
  import { scopePatterns } from './scope-patterns';
12
12
  import { tablePatterns } from './table-patterns';
13
+ import { capturePatterns } from './capture-patterns';
13
14
 
14
15
  /**
15
16
  * Pattern Registry - manages all step patterns
@@ -34,6 +35,7 @@ export class PatternRegistry {
34
35
  this.patterns.push(...scrollPatterns);
35
36
  this.patterns.push(...scopePatterns);
36
37
  this.patterns.push(...tablePatterns);
38
+ this.patterns.push(...capturePatterns);
37
39
 
38
40
  // Sort by priority (higher first)
39
41
  this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
@@ -90,6 +90,7 @@ export class StepMapper {
90
90
  this.inRowScope = false;
91
91
  this.rowScopeTable = '';
92
92
  this.lastPrimaryKeyword = 'Given';
93
+ this.dataResolver.clearCaptured(); // captured vars are scenario-scoped
93
94
  this.templateEngine.resetBaseContext();
94
95
  }
95
96
 
@@ -14,6 +14,20 @@ export class DataResolver {
14
14
  private screenName?: string;
15
15
  private runtimeMode: boolean;
16
16
  private flowMode: boolean = false;
17
+ // Vars captured at runtime via `User remember [X] as {{var}}`. References to
18
+ // these skip YAML validation (they don't exist in test-data.yaml) and resolve
19
+ // to testData.get('var') — populated by the capture step's testData.set('var', …).
20
+ private capturedVars = new Set<string>();
21
+
22
+ /** Register a variable name produced by a capture step (scenario-scoped). */
23
+ registerCaptured(name: string): void {
24
+ this.capturedVars.add(name);
25
+ }
26
+
27
+ /** Clear captured vars — call at the start of each scenario. */
28
+ clearCaptured(): void {
29
+ this.capturedVars.clear();
30
+ }
17
31
 
18
32
  constructor(testDataDir?: string, screenName?: string, runtimeMode: boolean = false) {
19
33
  this.testDataDir = testDataDir || path.join(process.cwd(), 'qa', 'test-data');
@@ -36,6 +50,12 @@ export class DataResolver {
36
50
  * @returns The resolved value
37
51
  */
38
52
  resolveData(dataRef: string, featureName?: string): string {
53
+ // Captured vars exist only at runtime → emit a marker (→ testData.get) and
54
+ // skip YAML validation. Requires runtime mode (capture writes testData.set).
55
+ if (this.capturedVars.has(dataRef)) {
56
+ return DataResolver.encodeMarker(dataRef);
57
+ }
58
+
39
59
  if (this.runtimeMode) {
40
60
  this.validateDataRef(dataRef, featureName);
41
61
  return DataResolver.encodeMarker(dataRef);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Harness Audit — runs all sensors over a screen's test-design artifacts and
3
+ * produces a structured report + a business-weighted quality score.
4
+ *
5
+ * The score is INTENTIONALLY weighted toward business-critical coverage/depth
6
+ * (not breadth), so it surfaces the gaps a count-based view hides. See
7
+ * docs/orchestration-spec.md §5 and reports/sungen_home_gherkin_viewpoint_coverage_review.md.
8
+ */
9
+ import * as path from 'path';
10
+ import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
11
+ import {
12
+ loadCatalog, viewpointGate, assertionDepth, dataThemesFor, coverageBalance, duplicateClusters, traceability,
13
+ GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult,
14
+ } from './sensors';
15
+ import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
16
+
17
+ export interface AuditReport {
18
+ screen: string;
19
+ scenarioCount: number;
20
+ gate: GateResult;
21
+ depth: DepthResult;
22
+ balance: BalanceResult;
23
+ duplicates: DuplicateResult;
24
+ trace: TraceResult;
25
+ score: {
26
+ overall: number; // 0..10, business-weighted
27
+ coverage: number; // 0..1
28
+ businessDepth: number; // 0..1
29
+ balance: number; // 0..1
30
+ traceability: number; // 0..1
31
+ formula: string;
32
+ };
33
+ gateStatus: 'PASS' | 'FAIL';
34
+ findings: string[]; // human-actionable, what the Repair loop would target
35
+ intent: IntentProfile; // P3 — the intent profile that drove the thresholds
36
+ }
37
+
38
+ export function runAudit(screenDir: string, screenName: string): AuditReport {
39
+ const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
40
+ const viewpointPath = path.join(screenDir, 'requirements', 'test-viewpoint.md');
41
+
42
+ const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
43
+ const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
44
+ const catalog = loadCatalog();
45
+
46
+ const gate = viewpointGate(scenarios, viewpoints, catalog);
47
+ // P3 — intent profile from qa/context.md drives the depth threshold (focus).
48
+ const intent = readIntent(projectRootFromScreenDir(screenDir));
49
+ const depth = assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
50
+ const balance = coverageBalance(scenarios);
51
+ const duplicates = duplicateClusters(scenarios);
52
+ const trace = traceability(scenarios, viewpoints);
53
+
54
+ // Sub-scores
55
+ const coverage = gate.coverageRatio;
56
+ const businessDepth = depth.bcDepthRatio;
57
+ const balanceScore = balance.coreCount + balance.secondaryCount > 0
58
+ ? Math.min(1, balance.coreCount / Math.max(1, balance.secondaryCount))
59
+ : 1;
60
+ const traceScore = 0.5 * trace.withVpCodeRatio + 0.5 * trace.mappedRatio;
61
+
62
+ // Business-weighted overall (coverage + depth dominate)
63
+ const overall = (0.4 * coverage + 0.3 * businessDepth + 0.15 * balanceScore + 0.15 * traceScore) * 10;
64
+
65
+ const findings: string[] = [];
66
+ for (const g of gate.gaps) {
67
+ if (g.status === 'shallow') {
68
+ findings.push(`GATE: critical theme "${g.theme}" is covered only by SHALLOW scenarios (no data assertion) → deepen with \`... with {{value}}\` / \`table ... with {{value}}\` (count @manual cross-screen too).`);
69
+ } else {
70
+ findings.push(`GATE: critical theme "${g.theme}" for page-type "${gate.pageType}" has NO covering scenario → generate it (often cross-screen → consider add-flow).`);
71
+ }
72
+ }
73
+ if (depth.businessCriticalShallow > 0) {
74
+ const tag = depth.verdict === 'fail' ? 'DEPTH-FAIL' : depth.verdict === 'warn' ? 'DEPTH-WARN' : 'DEPTH';
75
+ findings.push(
76
+ `${tag}: ${depth.businessCriticalShallow}/${depth.businessCriticalTotal} data-correctness scenarios assert only visibility ` +
77
+ `(ratio ${depth.bcDepthRatio.toFixed(2)} < threshold ${depth.threshold.toFixed(2)} for focus "${depth.focus}") → ` +
78
+ `add data assertions (\`... with {{value}}\`, \`see all ... contain {{v}}\`) or, if cross-screen, defer to a flow with @manual + reason.`,
79
+ );
80
+ }
81
+ if (balance.imbalanced) {
82
+ findings.push(`BALANCE: ${balance.note} Stop expanding secondary viewpoints until business-core gaps are filled.`);
83
+ }
84
+ if (trace.mappedRatio < 0.5) {
85
+ findings.push(`TRACE: ${trace.note}`);
86
+ }
87
+ if (gate.universalGaps.length) {
88
+ findings.push(`UNIVERSAL: missing theme(s): ${gate.universalGaps.join(', ')} (low priority reminder).`);
89
+ }
90
+
91
+ // Gate now spans coverage (viewpoint themes) AND depth (data-correctness).
92
+ // A depth 'fail' (below the intent threshold) fails the gate; 'warn' does not.
93
+ const gateStatus: 'PASS' | 'FAIL' =
94
+ gate.gaps.length === 0 && depth.verdict !== 'fail' ? 'PASS' : 'FAIL';
95
+
96
+ return {
97
+ screen: screenName,
98
+ scenarioCount: scenarios.length,
99
+ gate, depth, balance, duplicates, trace,
100
+ score: {
101
+ overall: Math.round(overall * 10) / 10,
102
+ coverage: Math.round(coverage * 100) / 100,
103
+ businessDepth: Math.round(businessDepth * 100) / 100,
104
+ balance: Math.round(balanceScore * 100) / 100,
105
+ traceability: Math.round(traceScore * 100) / 100,
106
+ formula: 'overall = (0.4*coverage + 0.3*businessDepth + 0.15*balance + 0.15*traceability) * 10',
107
+ },
108
+ gateStatus,
109
+ findings,
110
+ intent,
111
+ };
112
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Blind-Spot Memory (harness-roadmap P5).
3
+ *
4
+ * A deterministic harness reliably repeats whatever its rules/viewpoints miss. When
5
+ * a QA (or the challenge critic) finds a recurring gap, don't just patch one test —
6
+ * promote it to a reusable PATTERN here. Loop 1 (generation pre-gate) and Loop 2
7
+ * (challenge critic) read these so the same blind spot doesn't come back.
8
+ *
9
+ * Store: .sungen/blindspots/blindspots.jsonl (append-only, project-local).
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ export interface Blindspot {
15
+ ts: string;
16
+ pattern: string; // short name, e.g. "add-action-no-duplicate-check"
17
+ rule: string; // the generalised rule to apply next time
18
+ example?: string; // a concrete instance that motivated it
19
+ screen?: string; // where it was first noticed
20
+ source?: string; // 'qa' | 'challenge' | 'feedback'
21
+ }
22
+
23
+ function storePath(): string {
24
+ return path.join(process.cwd(), '.sungen', 'blindspots', 'blindspots.jsonl');
25
+ }
26
+
27
+ export function addBlindspot(entry: Omit<Blindspot, 'ts'> & { ts?: string }): string {
28
+ const p = storePath();
29
+ fs.mkdirSync(path.dirname(p), { recursive: true });
30
+ const full: Blindspot = { ts: entry.ts ?? new Date().toISOString(), ...entry };
31
+ fs.appendFileSync(p, JSON.stringify(full) + '\n', 'utf-8');
32
+ return p;
33
+ }
34
+
35
+ export function listBlindspots(): Blindspot[] {
36
+ const p = storePath();
37
+ if (!fs.existsSync(p)) return [];
38
+ return fs.readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => {
39
+ try { return JSON.parse(l) as Blindspot; } catch { return null; }
40
+ }).filter(Boolean) as Blindspot[];
41
+ }
42
+
43
+ /** Compact bullet list for injecting into generator / critic prompts. */
44
+ export function blindspotsForPrompt(): string {
45
+ const all = listBlindspots();
46
+ if (!all.length) return '';
47
+ // De-dupe by pattern (latest rule wins).
48
+ const byPattern = new Map<string, Blindspot>();
49
+ for (const b of all) byPattern.set(b.pattern, b);
50
+ return [...byPattern.values()].map((b) => `- [${b.pattern}] ${b.rule}`).join('\n');
51
+ }