@sun-asterisk/sungen 3.1.0 → 3.1.2-beta.100

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 (228) hide show
  1. package/README.md +4 -428
  2. package/dist/capabilities/builtins.d.ts +31 -0
  3. package/dist/capabilities/builtins.d.ts.map +1 -0
  4. package/dist/capabilities/builtins.js +84 -0
  5. package/dist/capabilities/builtins.js.map +1 -0
  6. package/dist/capabilities/context-router.d.ts +34 -0
  7. package/dist/capabilities/context-router.d.ts.map +1 -0
  8. package/dist/capabilities/context-router.js +49 -0
  9. package/dist/capabilities/context-router.js.map +1 -0
  10. package/dist/capabilities/context.d.ts +51 -0
  11. package/dist/capabilities/context.d.ts.map +1 -0
  12. package/dist/capabilities/context.js +17 -0
  13. package/dist/capabilities/context.js.map +1 -0
  14. package/dist/capabilities/discover.d.ts +2 -0
  15. package/dist/capabilities/discover.d.ts.map +1 -0
  16. package/dist/capabilities/discover.js +48 -0
  17. package/dist/capabilities/discover.js.map +1 -0
  18. package/dist/capabilities/registry.d.ts +90 -0
  19. package/dist/capabilities/registry.d.ts.map +1 -0
  20. package/dist/capabilities/registry.js +43 -0
  21. package/dist/capabilities/registry.js.map +1 -0
  22. package/dist/capabilities/sensor.d.ts +49 -0
  23. package/dist/capabilities/sensor.d.ts.map +1 -0
  24. package/dist/capabilities/sensor.js +3 -0
  25. package/dist/capabilities/sensor.js.map +1 -0
  26. package/dist/cli/commands/challenge.d.ts.map +1 -1
  27. package/dist/cli/commands/challenge.js +9 -2
  28. package/dist/cli/commands/challenge.js.map +1 -1
  29. package/dist/cli/commands/delivery.d.ts.map +1 -1
  30. package/dist/cli/commands/delivery.js +3 -2
  31. package/dist/cli/commands/delivery.js.map +1 -1
  32. package/dist/cli/commands/generate.d.ts.map +1 -1
  33. package/dist/cli/commands/generate.js +12 -0
  34. package/dist/cli/commands/generate.js.map +1 -1
  35. package/dist/cli/index.js +10 -1
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  38. package/dist/exporters/csv-exporter.js +92 -76
  39. package/dist/exporters/csv-exporter.js.map +1 -1
  40. package/dist/exporters/spec-parser.d.ts.map +1 -1
  41. package/dist/exporters/spec-parser.js +6 -1
  42. package/dist/exporters/spec-parser.js.map +1 -1
  43. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
  44. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  45. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  46. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  47. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  48. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  49. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
  50. package/dist/generators/test-generator/code-generator.d.ts +21 -4
  51. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  52. package/dist/generators/test-generator/code-generator.js +118 -57
  53. package/dist/generators/test-generator/code-generator.js.map +1 -1
  54. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +3 -0
  55. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -0
  56. package/dist/generators/test-generator/patterns/expect-patterns.js +54 -0
  57. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -0
  58. package/dist/generators/test-generator/patterns/index.d.ts +0 -10
  59. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  60. package/dist/generators/test-generator/patterns/index.js +10 -45
  61. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  62. package/dist/generators/test-generator/step-mapper.d.ts +6 -0
  63. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  64. package/dist/generators/test-generator/step-mapper.js +8 -0
  65. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  66. package/dist/generators/test-generator/template-engine.d.ts +4 -0
  67. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  68. package/dist/generators/test-generator/template-engine.js +1 -1
  69. package/dist/generators/test-generator/template-engine.js.map +1 -1
  70. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +1 -1
  71. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  72. package/dist/generators/test-generator/utils/runtime-data-transformer.js +5 -5
  73. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  74. package/dist/harness/annotation-overrides.d.ts +9 -0
  75. package/dist/harness/annotation-overrides.d.ts.map +1 -0
  76. package/dist/harness/annotation-overrides.js +36 -0
  77. package/dist/harness/annotation-overrides.js.map +1 -0
  78. package/dist/harness/audit.d.ts.map +1 -1
  79. package/dist/harness/audit.js +35 -7
  80. package/dist/harness/audit.js.map +1 -1
  81. package/dist/harness/catalog/drivers.yaml +35 -12
  82. package/dist/harness/challenge.d.ts +1 -0
  83. package/dist/harness/challenge.d.ts.map +1 -1
  84. package/dist/harness/challenge.js +49 -2
  85. package/dist/harness/challenge.js.map +1 -1
  86. package/dist/harness/data-driven-lint.d.ts +7 -0
  87. package/dist/harness/data-driven-lint.d.ts.map +1 -0
  88. package/dist/harness/data-driven-lint.js +153 -0
  89. package/dist/harness/data-driven-lint.js.map +1 -0
  90. package/dist/harness/parse.d.ts +3 -0
  91. package/dist/harness/parse.d.ts.map +1 -1
  92. package/dist/harness/parse.js +25 -0
  93. package/dist/harness/parse.js.map +1 -1
  94. package/dist/harness/query-catalog.d.ts +48 -0
  95. package/dist/harness/query-catalog.d.ts.map +1 -0
  96. package/dist/harness/query-catalog.js +0 -0
  97. package/dist/harness/query-catalog.js.map +1 -0
  98. package/dist/harness/script-check.d.ts.map +1 -1
  99. package/dist/harness/script-check.js +7 -4
  100. package/dist/harness/script-check.js.map +1 -1
  101. package/dist/index.d.ts +20 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +32 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
  106. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  107. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +41 -0
  108. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +22 -0
  109. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
  110. package/dist/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
  111. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  112. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +41 -0
  113. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +22 -0
  114. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
  115. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
  116. package/dist/orchestrator/templates/specs-api.d.ts +19 -0
  117. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
  118. package/dist/orchestrator/templates/specs-api.js +128 -0
  119. package/dist/orchestrator/templates/specs-api.js.map +1 -0
  120. package/dist/orchestrator/templates/specs-api.ts +101 -0
  121. package/dist/orchestrator/templates/specs-db.d.ts +8 -0
  122. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  123. package/dist/orchestrator/templates/specs-db.js +22 -0
  124. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  125. package/dist/orchestrator/templates/specs-db.ts +22 -0
  126. package/dist/orchestrator/templates/specs-test-data.ts +76 -15
  127. package/package.json +7 -30
  128. package/src/capabilities/builtins.ts +85 -0
  129. package/src/capabilities/context-router.ts +66 -0
  130. package/src/capabilities/context.ts +46 -0
  131. package/src/capabilities/discover.ts +42 -0
  132. package/src/capabilities/registry.ts +111 -0
  133. package/src/capabilities/sensor.ts +47 -0
  134. package/src/cli/commands/challenge.ts +6 -2
  135. package/src/cli/commands/delivery.ts +3 -2
  136. package/src/cli/commands/generate.ts +12 -0
  137. package/src/cli/index.ts +10 -1
  138. package/src/exporters/csv-exporter.ts +22 -6
  139. package/src/exporters/spec-parser.ts +6 -1
  140. package/src/generators/test-generator/adapters/adapter-interface.ts +2 -1
  141. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  142. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  143. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
  144. package/src/generators/test-generator/code-generator.ts +114 -59
  145. package/src/generators/test-generator/patterns/expect-patterns.ts +49 -0
  146. package/src/generators/test-generator/patterns/index.ts +9 -33
  147. package/src/generators/test-generator/step-mapper.ts +9 -0
  148. package/src/generators/test-generator/template-engine.ts +5 -2
  149. package/src/generators/test-generator/utils/runtime-data-transformer.ts +5 -5
  150. package/src/harness/annotation-overrides.ts +25 -0
  151. package/src/harness/audit.ts +37 -8
  152. package/src/harness/catalog/drivers.yaml +35 -12
  153. package/src/harness/challenge.ts +47 -2
  154. package/src/harness/data-driven-lint.ts +119 -0
  155. package/src/harness/parse.ts +17 -0
  156. package/src/harness/query-catalog.ts +0 -0
  157. package/src/harness/script-check.ts +8 -5
  158. package/src/index.ts +30 -0
  159. package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
  160. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  161. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +41 -0
  162. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +22 -0
  163. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
  164. package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
  165. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  166. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +41 -0
  167. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +22 -0
  168. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
  169. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
  170. package/src/orchestrator/templates/specs-api.ts +101 -0
  171. package/src/orchestrator/templates/specs-db.ts +22 -0
  172. package/src/orchestrator/templates/specs-test-data.ts +76 -15
  173. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
  174. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
  175. package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
  176. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
  177. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
  178. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
  179. package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
  180. package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
  181. package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -5
  182. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
  183. package/dist/generators/test-generator/patterns/database-patterns.js +0 -94
  184. package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
  185. package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
  186. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
  187. package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
  188. package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
  189. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
  190. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
  191. package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
  192. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
  193. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
  194. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
  195. package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
  196. package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
  197. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
  198. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
  199. package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
  200. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
  201. package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
  202. package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
  203. package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
  204. package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
  205. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
  206. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
  207. package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
  208. package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
  209. package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
  210. package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
  211. package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
  212. package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
  213. package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
  214. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
  215. package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
  216. package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
  217. package/docs/orchestration-spec.md +0 -267
  218. package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
  219. package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
  220. package/src/generators/test-generator/patterns/database-patterns.ts +0 -95
  221. package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
  222. package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
  223. package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
  224. package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
  225. package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
  226. package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
  227. package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
  228. package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
@@ -5,6 +5,8 @@ import yaml from 'yaml';
5
5
 
6
6
  export class TestDataLoader {
7
7
  private data: Record<string, any>;
8
+ // Data-driven (@cases): when set (via withRow), get() prefers this row's columns.
9
+ private row?: Record<string, any>;
8
10
 
9
11
  private constructor(data: Record<string, any>) {
10
12
  this.data = data;
@@ -41,23 +43,56 @@ export class TestDataLoader {
41
43
  }
42
44
 
43
45
  get(key: string): string {
44
- // Captured/runtime vars (set() below) are stored under their literal — possibly
45
- // dotted — key (e.g. "cart.product_name"), so check the flat key first.
46
- let current: any = this.data[key];
47
- if (current === undefined || current === null) {
48
- // Fall back to nested navigation for YAML-structured keys (e.g. "cart.qty_two").
49
- current = this.data;
50
- for (const part of key.split('.')) {
51
- if (current == null || typeof current !== 'object') {
52
- throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
53
- }
54
- current = current[part];
55
- }
56
- }
57
- if (current === undefined || current === null) {
46
+ const value = this.resolve(key);
47
+ if (value === undefined || value === null) {
58
48
  throw new Error(`Test data key not found: ${key}`);
59
49
  }
60
- return String(current);
50
+ return String(value);
51
+ }
52
+
53
+ /**
54
+ * Resolve a `{{...}}` reference to its raw value. Supports:
55
+ * - flat keys (incl. captured runtime vars stored under a literal dotted key);
56
+ * - `@cases` row columns (the current row wins);
57
+ * - structured paths over nested data AND `@query`-bound result arrays:
58
+ * `q.count` / `q.length` → number of rows
59
+ * `q.first.col` / `q.last.col` / `q[2].col` → a specific row's column
60
+ * `q.col` → shorthand for the first row's column
61
+ */
62
+ private resolve(key: string): any {
63
+ // 1. Exact flat key — captured vars (set()) live under a literal, possibly dotted, key.
64
+ if (this.row && key in this.row && this.row[key] !== undefined && this.row[key] !== null) {
65
+ return this.row[key];
66
+ }
67
+ if (this.data[key] !== undefined && this.data[key] !== null) {
68
+ return this.data[key];
69
+ }
70
+ // 2. Structured path: head from the row (cases) or shared data, then walk segments.
71
+ const tokens = String(key).replace(/\[(\d+)\]/g, '.$1').split('.');
72
+ let cur: any = (this.row && tokens[0] in this.row) ? this.row[tokens[0]] : this.data[tokens[0]];
73
+ for (let i = 1; i < tokens.length && cur != null; i++) cur = TestDataLoader.step(cur, tokens[i]);
74
+ return cur;
75
+ }
76
+
77
+ /** One navigation step over an array (with count/first/last/index/field-shorthand) or object. */
78
+ private static step(cur: any, token: string): any {
79
+ if (Array.isArray(cur)) {
80
+ if (token === 'count' || token === 'length') return cur.length;
81
+ if (token === 'first') return cur[0];
82
+ if (token === 'last') return cur[cur.length - 1];
83
+ if (/^\d+$/.test(token)) return cur[Number(token)];
84
+ return cur[0] == null ? undefined : cur[0][token]; // shorthand: first row's field
85
+ }
86
+ if (cur && typeof cur === 'object') return cur[token];
87
+ return undefined;
88
+ }
89
+
90
+ /**
91
+ * Bind a raw value (e.g. an `@query` result array) under `key` so `{{key.…}}` paths resolve.
92
+ * Unlike set(), the value is stored as-is (array/object), not coerced to a string.
93
+ */
94
+ bind(key: string, value: any): void {
95
+ this.data[key] = value;
61
96
  }
62
97
 
63
98
  /**
@@ -68,6 +103,32 @@ export class TestDataLoader {
68
103
  set(key: string, value: string): void {
69
104
  this.data[key] = value;
70
105
  }
106
+
107
+ /**
108
+ * Data-driven (@cases): return the list of rows at `key` (after env-overlay merge),
109
+ * each stamped with `__label` for the test title / report. Throws if missing or not a list.
110
+ */
111
+ cases(key: string): Array<Record<string, any>> {
112
+ const list = this.data[key];
113
+ if (!Array.isArray(list)) {
114
+ throw new Error(`@cases dataset "${key}" not found or not a list in test-data (got ${typeof list}).`);
115
+ }
116
+ return list.map((row: any, i: number) => {
117
+ const r: Record<string, any> = (row && typeof row === 'object' && !Array.isArray(row)) ? { ...row } : { value: row };
118
+ r.__label = String(r.case ?? r.name ?? r.label ?? `row ${i + 1}`);
119
+ return r;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Data-driven (@cases): a view whose get() prefers the given row's columns and falls
125
+ * back to the shared data. Used inside the per-row test() loop.
126
+ */
127
+ withRow(row: Record<string, any>): TestDataLoader {
128
+ const view = new TestDataLoader({ ...this.data }); // clone → per-row set() stays isolated
129
+ view.row = row;
130
+ return view;
131
+ }
71
132
  }
72
133
 
73
134
  function loadYamlSync(filePath: string): Record<string, any> | null {
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.1.0",
3
+ "version": "3.1.2-beta.100",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
7
  "bin": {
8
8
  "sungen": "./bin/sungen.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc && npm run copy-templates",
11
+ "build": "rm -rf dist && tsc && npm run copy-templates",
12
12
  "copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
13
- "build:dashboard": "cd dashboard && npm install --silent && npm run build && cd .. && cp dashboard/dist/index.html src/dashboard/templates/index.html",
13
+ "build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
14
14
  "dev": "tsx src/cli/index.ts",
15
- "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts",
15
+ "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/packaging/run.ts",
16
16
  "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
17
17
  "prepublishOnly": "npm run build:dashboard && npm run build"
18
18
  },
@@ -33,6 +33,7 @@
33
33
  "node": ">=18.0.0"
34
34
  },
35
35
  "dependencies": {
36
+ "@sungen/driver-ui": "3.1.2-beta.100",
36
37
  "@anthropic-ai/sdk": "^0.71.0",
37
38
  "@babel/parser": "^7.28.5",
38
39
  "@babel/traverse": "^7.28.5",
@@ -52,37 +53,13 @@
52
53
  "yaml": "^2.8.2",
53
54
  "zod": "^4.1.13"
54
55
  },
55
- "devDependencies": {
56
- "@playwright/test": "^1.57.0",
57
- "@testing-library/jest-dom": "^6.9.1",
58
- "@testing-library/react": "^16.3.0",
59
- "@types/jsdom": "^27.0.0",
60
- "@types/node": "^20",
61
- "jsdom": "^27.3.0",
62
- "react": "^19.2.3",
63
- "react-dom": "^19.2.3",
64
- "typescript": "^5"
65
- },
66
56
  "peerDependencies": {
67
57
  "@playwright/test": "^1.57.0"
68
58
  },
69
- "overrides": {
70
- "brace-expansion": "^5.0.6",
71
- "uuid": "^11.1.1",
72
- "tmp": "^0.2.6",
73
- "esbuild": "^0.28.1"
74
- },
75
- "resolutions": {
76
- "brace-expansion": "^5.0.6",
77
- "uuid": "^11.1.1",
78
- "tmp": "^0.2.6",
79
- "esbuild": "^0.28.1"
80
- },
81
59
  "files": [
82
60
  "dist",
83
61
  "bin",
84
62
  "src",
85
- "docs/orchestration-spec.md",
86
63
  "README.md",
87
64
  "LICENSE"
88
65
  ]
@@ -0,0 +1,85 @@
1
+ /**
2
+ * In-core capability registrations (Capability SPI).
3
+ *
4
+ * `ui` has moved to `@sungen/driver-ui` (R5.4); `db` and `api` still register from here via
5
+ * `LOCAL_DRIVERS` (they relocate in R5.5/R5.6), and `core` (the generic expect/lint/verification
6
+ * layer) always stays in core. Discovery (`discover.ts`) loads the external driver(s) first, then
7
+ * these, then `core` — preserving the historical pattern composition order, so compiled output is
8
+ * byte-identical (golden + audit snapshots are the proof).
9
+ */
10
+ import type { CapabilityRegistry } from './registry';
11
+ import type { Sensor, AdvisoryScanInput, GateInput } from './sensor';
12
+ import { lintDataDriven } from '../harness/data-driven-lint';
13
+ import { resolveQuery, lintCatalog } from '../harness/query-catalog';
14
+ import { expectPatterns } from '../generators/test-generator/patterns/expect-patterns';
15
+
16
+ /** Advisory @cases/@query lint, exposed through the Sensor SPI (generate-time). */
17
+ const dataDrivenLintSensor: Sensor<AdvisoryScanInput> = {
18
+ id: 'data-driven-lint',
19
+ capability: 'core',
20
+ kind: 'advisory',
21
+ run: ({ dir, cwd }) =>
22
+ lintDataDriven(dir, cwd).map((w) => ({
23
+ sensorId: 'data-driven-lint',
24
+ capability: 'core',
25
+ scenario: w.scenario,
26
+ message: w.message,
27
+ severity: 'warn' as const,
28
+ })),
29
+ };
30
+
31
+ /**
32
+ * Generic `verification` gate sensor: every referenced named query (`@query`) must resolve in its
33
+ * catalog, and the catalog must lint clean. An unresolved/invalid reference is a gate-level error.
34
+ * (On a project with no `@query` refs it yields nothing.) `query-catalog` lives in core's harness —
35
+ * shared with the data-driven advisory lint. The `@api` counterpart now lives in
36
+ * `@sungen/driver-api`'s `api-verification` gate sensor. Message prefix unchanged → audit byte-identical.
37
+ */
38
+ const verificationGateSensor: Sensor<GateInput> = {
39
+ id: 'verification',
40
+ capability: 'core',
41
+ kind: 'gate',
42
+ run: ({ screenName, scenarios, cwd }) => {
43
+ const findings = [] as ReturnType<Sensor['run']>;
44
+ const fail = (msg: string) => findings.push({ sensorId: 'verification', capability: 'core', message: `VERIFICATION-FAIL: ${msg}`, severity: 'error' });
45
+
46
+ const queryRefs = new Set<string>();
47
+ for (const s of scenarios) for (const r of s.queryRefs ?? []) queryRefs.add(r);
48
+ for (const name of queryRefs) {
49
+ try { resolveQuery(name, screenName, cwd); } catch (e: any) { fail(e?.message || `query "${name}" does not resolve`); }
50
+ }
51
+ if (queryRefs.size) {
52
+ try { for (const err of lintCatalog(screenName, null, cwd).errors) fail(err); } catch { /* no catalog */ }
53
+ }
54
+ return findings;
55
+ },
56
+ };
57
+
58
+ /**
59
+ * core — generic, capability-agnostic steps (data-vs-data `expect`) + the advisory @cases/@query
60
+ * lint and the gate-level `verification` sensor. Stays in core (not a driver) — the always-present
61
+ * generic layer. All three real capabilities (ui/db/api) now ship as `@sungen/driver-*` packages.
62
+ */
63
+ export function registerCoreCapability(registry: CapabilityRegistry): void {
64
+ registry.register({
65
+ id: 'core',
66
+ patterns: [...expectPatterns],
67
+ sensors: [dataDrivenLintSensor, verificationGateSensor],
68
+ });
69
+ }
70
+
71
+ /**
72
+ * In-core driver manifest — now empty: `ui`, `db`, and `api` all ship as `@sungen/driver-*` packages
73
+ * loaded by discovery (`discover.ts`). Kept (empty) as the seam for any future in-core capability;
74
+ * `core` itself registers separately via `registerCoreCapability`.
75
+ */
76
+ export const LOCAL_DRIVERS: ReadonlyArray<{ capability: string; register: (r: CapabilityRegistry) => void }> = [];
77
+
78
+ /**
79
+ * @deprecated Use `discoverAndRegisterCapabilities()` from `./discover`. Kept as a thin alias so
80
+ * existing call-sites/tests don't break during R5; removed once everything routes through discovery.
81
+ */
82
+ export function registerBuiltinCapabilities(): void {
83
+ // Lazy import avoids a cycle (discover imports builtins).
84
+ require('./discover').discoverAndRegisterCapabilities();
85
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ContextRouter (Capability SPI, R1 — skeleton).
3
+ *
4
+ * The overload guard: for each unit of work it decides the minimal slice of Context + the relevant
5
+ * capabilities + the gate to load, so a project with many drivers doesn't gather everything for
6
+ * every artifact (cost scales by *target*, not *repo × drivers*). See
7
+ * `docs/spec/sungen_capability_spi_spec.md` §5.
8
+ *
9
+ * R1 scope (skeleton, deterministic, not yet enforced in the pipeline): it computes the routing
10
+ * plan — which capabilities apply (the default/implicit one + any whose annotation tags appear),
11
+ * and which sensors gate the task. Scope-trimming/token-budget enforcement is wired in a later step;
12
+ * for now `scope.budget` is a placeholder (null = unbounded) and the plan is advisory.
13
+ */
14
+ import { capabilityRegistry } from './registry';
15
+
16
+ export interface RouteTask {
17
+ /** What is being worked on now. */
18
+ target: { kind: string; id: string };
19
+ /** What artifact is being produced/checked: 'feature' | 'scenario' | 'selectors' | 'apis.yaml' | … */
20
+ artifact: string;
21
+ /** Capability tags present on the target (e.g. ['@query'], ['@api'], ['@cases']). */
22
+ tags?: string[];
23
+ }
24
+
25
+ export interface RoutePlan {
26
+ /** Applicable capability ids: the default/implicit one + any whose annotations match the tags. */
27
+ capabilities: string[];
28
+ /** Gate sensors to run for this task (ids). */
29
+ gateSensorIds: string[];
30
+ /** Advisory sensors to run for this task (ids). */
31
+ advisorySensorIds: string[];
32
+ /** Scope: the target slice + a token/context budget (null = unbounded for now). */
33
+ scope: { target: RouteTask['target']; budget: number | null };
34
+ }
35
+
36
+ class ContextRouter {
37
+ /** Which capabilities a set of tags activates: the default capability + any owning a present tag. */
38
+ capabilitiesFor(tags: string[] = []): string[] {
39
+ const ids = new Set<string>();
40
+ const def = capabilityRegistry.defaultCapabilityId();
41
+ if (def) ids.add(def);
42
+ for (const cap of capabilityRegistry.all()) {
43
+ if ((cap.annotations ?? []).some((a) => tags.includes(a))) ids.add(cap.id);
44
+ }
45
+ return [...ids];
46
+ }
47
+
48
+ /** Compute the routing plan for a task (deterministic). */
49
+ route(task: RouteTask): RoutePlan {
50
+ const capabilities = this.capabilitiesFor(task.tags ?? []);
51
+ // Generic sensors (no capability, or the always-on `core`) run regardless of the task's tags;
52
+ // capability-specific sensors run only when their capability is in scope.
53
+ const inScope = (capId?: string) => capId == null || capId === 'core' || capabilities.includes(capId);
54
+ const gateSensorIds = capabilityRegistry.sensors('gate').filter((s) => inScope(s.capability)).map((s) => s.id);
55
+ const advisorySensorIds = capabilityRegistry.sensors('advisory').filter((s) => inScope(s.capability)).map((s) => s.id);
56
+ return {
57
+ capabilities,
58
+ gateSensorIds,
59
+ advisorySensorIds,
60
+ scope: { target: task.target, budget: null },
61
+ };
62
+ }
63
+ }
64
+
65
+ /** Process-wide singleton. */
66
+ export const contextRouter = new ContextRouter();
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Orchestration phase-hook SPIs (Capability SPI, R1 — phase hooks).
3
+ *
4
+ * The kernel owns the pipeline (Plan → Discover → Contextualize → Generate → Gate → Repair →
5
+ * Deliver); a capability fills the domain-specific phases via these hooks:
6
+ * - `DiscoveryProvider` — sources → a slice of the normalized `Context`.
7
+ * - `ContextMapper` — `Context` → generation units + the capability's MODES
8
+ * (ui: screen|flow; api: matrix|flow|async; db: query-catalog).
9
+ *
10
+ * R1 scope: these are **declared** (the contract) and attachable to a CapabilityDescriptor, but
11
+ * the kernel does not consume them yet — the pipeline still runs as before. R2 (driver-ui) is the
12
+ * first conformer that implements them; the orchestration is rewired to call them then.
13
+ * See `docs/spec/sungen_capability_spi_spec.md` §3–§6 and `sungen_ui_driver_spec.md`.
14
+ */
15
+
16
+ /** Normalized, capability-tagged discovery output. Lazy/sliced — never materialized whole. */
17
+ export interface Context {
18
+ target: { kind: string; id: string; capability?: string };
19
+ /** Lazy source handles (spec/viewpoint/openapi/routes/schema/liveSnapshot/figma/…). */
20
+ sources: Record<string, unknown>;
21
+ /** Capability-specific normalized facts (ui: elements/states/pageType; api: endpoints; db: tables). */
22
+ facts: Record<string, unknown>;
23
+ /** For run-time-connected capabilities (api/db): reachability probe result. */
24
+ connectivity?: { reachable: boolean; baseUrl?: string; probedAt?: string };
25
+ }
26
+
27
+ /** One unit of generation work the kernel will drive through Generate → Gate → Repair. */
28
+ export interface GenerationUnit {
29
+ /** Capability-defined mode: 'screen' | 'flow' | 'matrix' | 'async' | 'query-catalog' | … */
30
+ mode: string;
31
+ /** The slice of Context this unit covers (a screen, an endpoint group, a query set, …). */
32
+ targetSlice: unknown;
33
+ /** Band weight hint (source-aware) — the kernel supplies the band targets. */
34
+ weight?: number;
35
+ }
36
+
37
+ export interface DiscoveryProvider {
38
+ appliesTo(target: Context['target']): boolean;
39
+ /** Produce this capability's slice of the Context for the given target + requested scope. */
40
+ discover(target: Context['target'], scope: unknown): Promise<Partial<Context>>;
41
+ }
42
+
43
+ export interface ContextMapper {
44
+ /** Turn the Context into generation units (+ modes), honouring the kernel's band targets. */
45
+ decompose(ctx: Context, bands?: Record<string, number>): GenerationUnit[];
46
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Capability discovery (R5 packaging seam).
3
+ *
4
+ * Core registers capabilities without importing any driver package statically. External
5
+ * `@sungen/driver-*` packages are loaded at runtime via their `sungenDriver.register` entry point;
6
+ * the capabilities still in core (`db`, `api`) register from `LOCAL_DRIVERS`; the generic `core`
7
+ * capability registers last. Idempotent (guards on `registry.isPopulated()`).
8
+ *
9
+ * Ordering is deterministic and behaviour-preserving: external drivers (currently `@sungen/driver-ui`)
10
+ * load first, then `LOCAL_DRIVERS` (db → api), then `core` — i.e. the historical ui → db → api → core
11
+ * composition order, so pattern composition / compiled output is byte-identical.
12
+ *
13
+ * A driver that isn't installed is simply skipped (capabilities are opt-in). Errors thrown *inside* a
14
+ * present driver propagate — only a genuinely absent package is swallowed (resolve-then-require).
15
+ */
16
+ import { capabilityRegistry } from './registry';
17
+ import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
18
+
19
+ /**
20
+ * Driver packages relocated out of core, loaded before the in-core `LOCAL_DRIVERS`. As `db`/`api`
21
+ * move to their packages (R5.5/R5.6) they join this list and leave `LOCAL_DRIVERS`; eventually this
22
+ * becomes a scan of installed `@sungen/driver-*` packages (R5.7).
23
+ */
24
+ const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api'];
25
+
26
+ function loadExternalDriver(name: string): void {
27
+ try {
28
+ require.resolve(name);
29
+ } catch {
30
+ return; // not installed — capabilities are opt-in, so skip silently
31
+ }
32
+ const mod = require(name); // present: let any error thrown inside the driver propagate (don't mask)
33
+ const register: ((r: typeof capabilityRegistry) => void) | undefined = mod?.sungenDriver?.register ?? mod?.register;
34
+ if (typeof register === 'function') register(capabilityRegistry);
35
+ }
36
+
37
+ export function discoverAndRegisterCapabilities(): void {
38
+ if (capabilityRegistry.isPopulated()) return;
39
+ for (const name of EXTERNAL_DRIVERS) loadExternalDriver(name); // ui (external) — first, default capability
40
+ for (const driver of LOCAL_DRIVERS) driver.register(capabilityRegistry); // db, api (in-core for now)
41
+ registerCoreCapability(capabilityRegistry); // generic layer, always present, registered last
42
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Capability registry — the extension seam for the microkernel (Capability SPI, R1).
3
+ *
4
+ * Replaces hardcoded `this.patterns.push(...)` wiring with **registration**: each capability
5
+ * (ui · db · api · …) declares what it contributes; the kernel composes it. This is the
6
+ * "shared framework, pull in what you use" mechanism (`docs/spec/sungen_capability_spi_spec.md`).
7
+ *
8
+ * R1 scope (behaviour-preserving): only the **patterns** slice is wired through here — the set of
9
+ * registered patterns is identical to the old hardcoded list, so compiled output is unchanged
10
+ * (golden + audit snapshots are the contract). The remaining SPI slices (discovery, contextMapper,
11
+ * sensors, adapter, viewpoints, runtimeHelpers) are declared on the descriptor for the upcoming
12
+ * steps but are not consumed yet.
13
+ */
14
+ import type { StepPattern } from '../generators/test-generator/patterns/types';
15
+ import type { Sensor } from './sensor';
16
+ import type { DiscoveryProvider, ContextMapper } from './context';
17
+
18
+ export interface CapabilityDescriptor {
19
+ /** Stable id: 'ui' | 'db' | 'api' | 'core' | … */
20
+ id: string;
21
+ /** The implicit/default capability (a scenario with no capability tag). UI sets this. */
22
+ default?: boolean;
23
+ /** Annotation tags this capability owns (e.g. ['@query'], ['@api','@hybrid']). */
24
+ annotations?: string[];
25
+ /** Step patterns this capability contributes to the compiler. */
26
+ patterns?: StepPattern[];
27
+ /** Harness sensors this capability contributes (advisory and/or gate). */
28
+ sensors?: Sensor[];
29
+ /** Orchestration phase hooks — declared in R1, consumed by the pipeline from R2 onward. */
30
+ discovery?: DiscoveryProvider; // sources → Context slice
31
+ contextMapper?: ContextMapper; // Context → generation units + modes
32
+ /** Provider for this capability's viewpoint catalog (UI: the 17-pattern universal catalog). */
33
+ viewpoints?: () => unknown;
34
+ /**
35
+ * Score-bearing gate computation owned by this capability (UI: viewpoint coverage + assertion
36
+ * depth). The audit engine calls it and assembles the score from the result. Typed generically
37
+ * so the registry stays decoupled from harness internals; the caller knows the concrete shape.
38
+ */
39
+ gateProvider?: (input: unknown) => unknown;
40
+ /** Runtime helper file(s) this capability emits into specs/ when it is active for a feature
41
+ * (db → specs/db.ts). Emitted via the same sync-when-changed mechanism. */
42
+ runtimeHelpers?: Array<{ file: string; template: string }>;
43
+ /** Optional step detector: the capability is "active" for a feature if any step matches —
44
+ * for steps that carry no annotation tag (db's declarative `User see [t] row where …`). */
45
+ detectsStep?: (stepText: string) => boolean;
46
+ /**
47
+ * Precondition codegen for this capability's annotations on a scenario (db's `@query:<name>`
48
+ * → `testData.bind(name, await db.fetchQuery(...))`). Returns raw statements (the compiler
49
+ * indents) + `boundVars` (the `{{name}}` variables it binds, so the compiler registers them as
50
+ * runtime-resolved). Keeps `@query` codegen owned by `db`, not hardcoded in the compiler.
51
+ */
52
+ preconditionCodegen?: (input: { tags: string[]; screenName: string; cwd: string }) =>
53
+ Array<{ comment?: string; code: string; boundVars?: string[] }>;
54
+
55
+ /**
56
+ * CLI commands this capability contributes. The CLI calls each with the commander `program`
57
+ * (typed generically so the registry stays decoupled from commander; the driver knows the shape).
58
+ * Lets a driver own its authoring commands (api → `sungen api import`) instead of hardcoding them
59
+ * in core's CLI. Invoked once, after discovery, in `src/cli/index.ts`.
60
+ */
61
+ cliCommands?: Array<(program: unknown) => void>;
62
+
63
+ // --- Declared for later R-steps; not consumed yet (kept here so the SPI shape is visible). ---
64
+ // adapter?: string; // codegen adapter id (existing adapterRegistry)
65
+ }
66
+
67
+ export class CapabilityRegistry {
68
+ private caps = new Map<string, CapabilityDescriptor>();
69
+
70
+ /** Register (or replace, by id) a capability descriptor. Idempotent by id. */
71
+ register(descriptor: CapabilityDescriptor): void {
72
+ this.caps.set(descriptor.id, descriptor);
73
+ }
74
+
75
+ get(id: string): CapabilityDescriptor | undefined {
76
+ return this.caps.get(id);
77
+ }
78
+
79
+ /** True once any capability is registered — discovery guards on this (replaces the builtins boolean). */
80
+ isPopulated(): boolean {
81
+ return this.caps.size > 0;
82
+ }
83
+
84
+ all(): CapabilityDescriptor[] {
85
+ return [...this.caps.values()];
86
+ }
87
+
88
+ /** The id of the implicit/default capability (UI), if registered. */
89
+ defaultCapabilityId(): string | undefined {
90
+ return this.all().find((c) => c.default)?.id;
91
+ }
92
+
93
+ /** All step patterns contributed by registered capabilities (composition order = registration order). */
94
+ patterns(): StepPattern[] {
95
+ return this.all().flatMap((c) => c.patterns ?? []);
96
+ }
97
+
98
+ /** All registered sensors, optionally filtered by kind ('advisory' | 'gate'). */
99
+ sensors(kind?: Sensor['kind']): Sensor[] {
100
+ const all = this.all().flatMap((c) => c.sensors ?? []);
101
+ return kind ? all.filter((s) => s.kind === kind) : all;
102
+ }
103
+
104
+ /** Test seam: drop all registrations. */
105
+ _reset(): void {
106
+ this.caps.clear();
107
+ }
108
+ }
109
+
110
+ /** Process-wide singleton (mirrors `adapterRegistry`). */
111
+ export const capabilityRegistry = new CapabilityRegistry();
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Harness Sensor SPI (Capability SPI, R1 — Sensor step).
3
+ *
4
+ * A sensor is a deterministic harness check a capability contributes. The kernel collects all
5
+ * registered sensors and runs them; findings merge into the harness output. Two kinds:
6
+ * - `advisory` — surfaced as warnings (e.g. at `sungen generate`), never blocks.
7
+ * - `gate` — feeds the audit scorecard / pass-fail (migrated from the hardcoded `audit.ts`
8
+ * calls in the next sub-step; gate sensors carry an intent-aware threshold).
9
+ *
10
+ * Generic over the input so the same registry holds both generate-time advisory sensors
11
+ * (which scan a screen/flow dir) and audit-time gate sensors (which read parsed scenarios) —
12
+ * each sensor declares the input it consumes; callers filter by `kind` and pass the right input.
13
+ */
14
+ export interface SensorFinding {
15
+ sensorId: string;
16
+ capability?: string;
17
+ scenario?: string;
18
+ message: string;
19
+ severity?: 'info' | 'warn' | 'error';
20
+ }
21
+
22
+ export interface Sensor<I = unknown> {
23
+ id: string;
24
+ capability?: string;
25
+ kind: 'gate' | 'advisory';
26
+ run(input: I): SensorFinding[];
27
+ }
28
+
29
+ /** Input for generate-time advisory sensors that scan a screen/flow directory. */
30
+ export interface AdvisoryScanInput {
31
+ dir: string;
32
+ cwd: string;
33
+ }
34
+
35
+ /**
36
+ * Input for audit-time gate sensors. Intentionally minimal/structural (not the full ScenarioInfo
37
+ * type) so `src/capabilities` doesn't depend on harness internals — the audit caller passes the
38
+ * parsed scenarios it already has.
39
+ */
40
+ export interface GateInput {
41
+ screenName: string;
42
+ cwd: string;
43
+ featureText: string;
44
+ scenarios: Array<{ name: string; queryRefs?: string[]; apiRefs?: string[] }>;
45
+ /** UI: universal-viewpoint theme gaps the coverage gate found (generic string list). */
46
+ universalGaps?: string[];
47
+ }
@@ -38,11 +38,15 @@ export function registerChallengeCommand(program: Command): void {
38
38
  if (report.collectionClaimSingular.length) {
39
39
  for (const f of report.collectionClaimSingular) L(` ⚠ ${f.scenario}\n → ${f.suggestion}`);
40
40
  } else L(' ✓ none');
41
- L(' ② Coverageover-covered / shallow');
41
+ L(' ② Data-drivenscenarios that should be one `@cases` (collapse data-variants / cover EP classes)');
42
+ if (report.dataDriven.length) {
43
+ for (const f of report.dataDriven) L(` ⚠ ${f.scenario}\n → ${f.suggestion}`);
44
+ } else L(' ✓ none');
45
+ L(' ③ Coverage — over-covered / shallow');
42
46
  if (report.overCovered.length) for (const o2 of report.overCovered) L(` • ${o2.bucket}: ${o2.note}`);
43
47
  if (report.shallowThemes.length) L(` • shallow themes: ${report.shallowThemes.join(', ')}`);
44
48
  if (!report.overCovered.length && !report.shallowThemes.length) L(' ✓ balanced');
45
- L(' Novelty — prompts for the `sungen-challenge` agent (≤20% of official, no auto-merge)');
49
+ L(' Novelty — prompts for the `sungen-challenge` agent (≤20% of official, no auto-merge)');
46
50
  for (const p of report.noveltyPrompts) L(` • ${p}`);
47
51
  L(' ── Exploration readiness ──');
48
52
  for (const e of report.explorationReadiness) L(` • ${e}`);
@@ -200,11 +200,12 @@ function discoverLocaleVariants(cwd: string, target: DeliveryTarget): LocaleVari
200
200
  const prefix = `${target.featureBaseName}-test-result`;
201
201
  const variants: LocaleVariant[] = [];
202
202
 
203
- const basePath = path.join(genDir, `${prefix}.json`);
203
+ // Base variant: per-target result file if present, else fall back to the global
204
+ // test-results/results.json (what playwright.config writes by default) via resolveResultsPath.
204
205
  variants.push({
205
206
  locale: '',
206
207
  displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
207
- resultsPath: fs.existsSync(basePath) ? basePath : null,
208
+ resultsPath: resolveResultsPath(cwd, target),
208
209
  });
209
210
 
210
211
  if (fs.existsSync(genDir)) {
@@ -4,6 +4,8 @@ import * as fs from 'fs';
4
4
  import { CodeGenerator } from '../../generators/test-generator/code-generator';
5
5
  import { adapterRegistry } from '../../generators/test-generator/adapters';
6
6
  import { scanTestDataSecrets } from '../../harness/secret-scan';
7
+ import { capabilityRegistry } from '../../capabilities/registry';
8
+ import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
7
9
  import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
8
10
 
9
11
  /**
@@ -182,6 +184,16 @@ export function registerGenerateCommand(program: Command): void {
182
184
  console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
183
185
  }
184
186
 
187
+ // Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
188
+ // never block generation. Each capability registers its own; the kernel just runs them.
189
+ discoverAndRegisterCapabilities();
190
+ const advisorySensors = capabilityRegistry.sensors('advisory');
191
+ const ddWarnings = scanDirs.flatMap((d) => advisorySensors.flatMap((s) => s.run({ dir: d, cwd })));
192
+ if (ddWarnings.length) {
193
+ console.log(`\n⚠️ Data-driven lint (@cases / @query) — review:`);
194
+ for (const w of ddWarnings.slice(0, 20)) console.log(` ${w.scenario ? w.scenario + ': ' : ''}${w.message}`);
195
+ }
196
+
185
197
  console.log(`Next step: npx playwright test --ui\n`);
186
198
  } catch (error) {
187
199
  console.error('Error:', error instanceof Error ? error.message : error);