@sun-asterisk/sungen 3.2.16-beta.7 → 3.2.17

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 (95) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +13 -197
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/cli/commands/ingest.d.ts.map +1 -1
  5. package/dist/cli/commands/ingest.js +56 -4
  6. package/dist/cli/commands/ingest.js.map +1 -1
  7. package/dist/dashboard/templates/index.html +1 -1
  8. package/dist/exporters/feature-parser.js +1 -1
  9. package/dist/exporters/feature-parser.js.map +1 -1
  10. package/dist/exporters/scenario-merger.js +1 -1
  11. package/dist/exporters/scenario-merger.js.map +1 -1
  12. package/dist/exporters/spec-parser.js +1 -1
  13. package/dist/exporters/spec-parser.js.map +1 -1
  14. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  15. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  16. package/dist/exporters/xlsx-report-builder.js +3 -3
  17. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-action.hbs +1 -1
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  21. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  22. package/dist/generators/test-generator/utils/runtime-data-transformer.js +28 -17
  23. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  24. package/dist/ingest/legacy-parser.d.ts +3 -0
  25. package/dist/ingest/legacy-parser.d.ts.map +1 -1
  26. package/dist/ingest/legacy-parser.js +52 -3
  27. package/dist/ingest/legacy-parser.js.map +1 -1
  28. package/dist/orchestrator/templates/ai-src/commands/delivery.md +120 -202
  29. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  30. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  31. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +6 -92
  32. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +7 -0
  33. package/package.json +4 -4
  34. package/src/cli/commands/delivery.ts +15 -227
  35. package/src/cli/commands/ingest.ts +57 -5
  36. package/src/dashboard/templates/index.html +1 -1
  37. package/src/exporters/feature-parser.ts +1 -1
  38. package/src/exporters/scenario-merger.ts +1 -1
  39. package/src/exporters/spec-parser.ts +1 -1
  40. package/src/exporters/xlsx-report-builder.ts +2 -3
  41. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-action.hbs +1 -1
  42. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  43. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  44. package/src/generators/test-generator/utils/runtime-data-transformer.ts +29 -18
  45. package/src/ingest/legacy-parser.ts +48 -3
  46. package/src/orchestrator/templates/ai-src/commands/delivery.md +120 -202
  47. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  48. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  49. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +6 -92
  50. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +7 -0
  51. package/dist/exporters/matrix/build.d.ts +0 -61
  52. package/dist/exporters/matrix/build.d.ts.map +0 -1
  53. package/dist/exporters/matrix/build.js +0 -472
  54. package/dist/exporters/matrix/build.js.map +0 -1
  55. package/dist/exporters/matrix/export.d.ts +0 -50
  56. package/dist/exporters/matrix/export.d.ts.map +0 -1
  57. package/dist/exporters/matrix/export.js +0 -152
  58. package/dist/exporters/matrix/export.js.map +0 -1
  59. package/dist/exporters/matrix/fingerprint.d.ts +0 -20
  60. package/dist/exporters/matrix/fingerprint.d.ts.map +0 -1
  61. package/dist/exporters/matrix/fingerprint.js +0 -83
  62. package/dist/exporters/matrix/fingerprint.js.map +0 -1
  63. package/dist/exporters/matrix/gates.d.ts +0 -21
  64. package/dist/exporters/matrix/gates.d.ts.map +0 -1
  65. package/dist/exporters/matrix/gates.js +0 -358
  66. package/dist/exporters/matrix/gates.js.map +0 -1
  67. package/dist/exporters/matrix/map-loader.d.ts +0 -22
  68. package/dist/exporters/matrix/map-loader.d.ts.map +0 -1
  69. package/dist/exporters/matrix/map-loader.js +0 -193
  70. package/dist/exporters/matrix/map-loader.js.map +0 -1
  71. package/dist/exporters/matrix/render-csv.d.ts +0 -9
  72. package/dist/exporters/matrix/render-csv.d.ts.map +0 -1
  73. package/dist/exporters/matrix/render-csv.js +0 -92
  74. package/dist/exporters/matrix/render-csv.js.map +0 -1
  75. package/dist/exporters/matrix/render-xlsx.d.ts +0 -41
  76. package/dist/exporters/matrix/render-xlsx.d.ts.map +0 -1
  77. package/dist/exporters/matrix/render-xlsx.js +0 -430
  78. package/dist/exporters/matrix/render-xlsx.js.map +0 -1
  79. package/dist/exporters/matrix/types.d.ts +0 -196
  80. package/dist/exporters/matrix/types.d.ts.map +0 -1
  81. package/dist/exporters/matrix/types.js +0 -15
  82. package/dist/exporters/matrix/types.js.map +0 -1
  83. package/dist/exporters/matrix/wording.d.ts +0 -51
  84. package/dist/exporters/matrix/wording.d.ts.map +0 -1
  85. package/dist/exporters/matrix/wording.js +0 -169
  86. package/dist/exporters/matrix/wording.js.map +0 -1
  87. package/src/exporters/matrix/build.ts +0 -503
  88. package/src/exporters/matrix/export.ts +0 -149
  89. package/src/exporters/matrix/fingerprint.ts +0 -49
  90. package/src/exporters/matrix/gates.ts +0 -391
  91. package/src/exporters/matrix/map-loader.ts +0 -163
  92. package/src/exporters/matrix/render-csv.ts +0 -94
  93. package/src/exporters/matrix/render-xlsx.ts +0 -420
  94. package/src/exporters/matrix/types.ts +0 -223
  95. package/src/exporters/matrix/wording.ts +0 -174
@@ -165,7 +165,7 @@ function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation
165
165
  * Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
166
166
  */
167
167
  function extractVpId(title: string): string | undefined {
168
- const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+(?:[a-zA-Z]|-[A-Z0-9]+)?)\b/);
168
+ const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\b/);
169
169
  return match ? match[1] : undefined;
170
170
  }
171
171
 
@@ -143,7 +143,6 @@ export function renderReportHeaderBand(
143
143
  ws: ExcelJS.Worksheet,
144
144
  titleLabel: string,
145
145
  sungenVersion?: string,
146
- formNo: string = 'BM-2-901-13',
147
146
  ): void {
148
147
  const issueDate = (() => {
149
148
  const d = new Date();
@@ -188,9 +187,9 @@ export function renderReportHeaderBand(
188
187
  d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
189
188
  d1.border = allBordersBlack;
190
189
 
191
- // G1 — No: <form number> (the company ISO form id; configurable via the delivery map)
190
+ // G1 — No: BM-2-901-13
192
191
  const g1 = ws.getCell('G1');
193
- g1.value = `No: ${formNo}`;
192
+ g1.value = 'No: BM-2-901-13';
194
193
  g1.font = { size: 12, name: TIMES };
195
194
  g1.alignment = { vertical: 'middle' };
196
195
  g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
@@ -1 +1 @@
1
- await {{> locator}}.click();
1
+ await {{> locator}}.click({{#if force}}{ force: true }{{/if}});
@@ -1 +1 @@
1
- await {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact legacyTail=(and (eq strategy "text") (eq value ""))}} }){{> locator-nth}}.click();
1
+ await {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact legacyTail=(and (eq strategy "text") (eq value ""))}} }){{> locator-nth}}.click({{#if force}}{ force: true }{{/if}});
@@ -1 +1 @@
1
- await page.waitForLoadState('networkidle');
1
+ await page.waitForTimeout({{duration}});
@@ -18,30 +18,36 @@ export function transformToRuntimeData(code: string, accessor: string = 'testDat
18
18
  (_, indent, before, enc, after) => `${indent}//${before}${decodeKey(enc)}${after}`
19
19
  );
20
20
 
21
- // Pass 1: String literal context — handles both whole-string and embedded markers
22
- // 'marker' testData.get('key')
23
- // 'prefix__marker__suffix' → `prefix${testData.get('key')}suffix`
21
+ // Pass 1: String literal context — a literal may carry SEVERAL markers
22
+ // ('/{{code}}/car-search?{{prefill}}' compiles to two markers in ONE literal).
23
+ // Match the whole quoted literal that contains at least one marker, then
24
+ // interpolate EVERY marker inside it. The old shape (one marker + lazy
25
+ // prefix/suffix) swallowed trailing markers into its suffix verbatim; they
26
+ // escaped the quote context (the literal had become a backtick template) and
27
+ // Pass 3's numeric catch-all emitted them as literal
28
+ // `Number(testData.get(...))` TEXT inside the template — a compiling spec
29
+ // navigating to a garbage URL.
30
+ // 'marker' → testData.get('key')
31
+ // 'a__m1__b__m2__c' → `a${testData.get('k1')}b${testData.get('k2')}c`
24
32
  code = code.replace(
25
- /(['"])((?:(?!\1).)*?)__SUNGEN_TD_([A-Za-z0-9_]+)__((?:(?!\1).)*?)\1/g,
26
- (_, _quote, prefix, enc, suffix) => {
27
- const key = decodeKey(enc);
28
- if (!prefix && !suffix) {
29
- return `${accessor}.get('${key}')`;
30
- }
31
- return `\`${prefix}\${${accessor}.get('${key}')}${suffix}\``;
33
+ /(['"])((?:(?!\1).)*?__SUNGEN_TD_[A-Za-z0-9_]+__(?:(?!\1).)*)\1/g,
34
+ (_, _quote, body) => {
35
+ const whole = /^__SUNGEN_TD_([A-Za-z0-9_]+)__$/.exec(body);
36
+ if (whole) return `${accessor}.get('${decodeKey(whole[1])}')`;
37
+ return `\`${interpolateMarkers(body, accessor)}\``;
32
38
  }
33
39
  );
34
40
 
35
- // Pass 2: Regex literal context — /prefix__marker__suffix/ → new RegExp(`...`)
36
- // Only matches within a single line (no newlines in prefix/suffix)
41
+ // Pass 2: Regex literal context — /…__marker__…/ → new RegExp(`…`). Same
42
+ // several-markers-per-literal rule as Pass 1 (/{{min}}-{{max}} items/).
43
+ // Only matches within a single line (no newlines in the body).
37
44
  code = code.replace(
38
- /\/((?:[^/\\\n]|\\.)*?)__SUNGEN_TD_([A-Za-z0-9_]+)__((?:[^/\\\n]|\\.)*?)\/([gimsuy]*)/g,
39
- (_, prefix, enc, suffix, flags) => {
40
- const key = decodeKey(enc);
41
- const ref = `${accessor}.get('${key}')`;
45
+ /\/((?:[^/\\\n]|\\.)*?__SUNGEN_TD_[A-Za-z0-9_]+__(?:[^/\\\n]|\\.)*)\/([gimsuy]*)/g,
46
+ (_, body, flags) => {
42
47
  const flagStr = flags ? `, '${flags}'` : '';
43
- if (!prefix && !suffix) return `new RegExp(${ref}${flagStr})`;
44
- return `new RegExp(\`${prefix}\${${ref}}${suffix}\`${flagStr})`;
48
+ const whole = /^__SUNGEN_TD_([A-Za-z0-9_]+)__$/.exec(body);
49
+ if (whole) return `new RegExp(${accessor}.get('${decodeKey(whole[1])}')${flagStr})`;
50
+ return `new RegExp(\`${interpolateMarkers(body, accessor)}\`${flagStr})`;
45
51
  }
46
52
  );
47
53
 
@@ -63,3 +69,8 @@ export function hasRuntimeDataMarkers(code: string): boolean {
63
69
  function decodeKey(encoded: string): string {
64
70
  return encoded.replace(/_D_/g, '.');
65
71
  }
72
+
73
+ /** Replace every marker in a literal's body with a `${accessor.get('key')}` interpolation. */
74
+ function interpolateMarkers(body: string, accessor: string): string {
75
+ return body.replace(/__SUNGEN_TD_([A-Za-z0-9_]+)__/g, (_, enc) => `\${${accessor}.get('${decodeKey(enc)}')}`);
76
+ }
@@ -30,6 +30,9 @@ export interface LegacyInventory {
30
30
  source: { file: string };
31
31
  sheets: SheetInfo[];
32
32
  testcases: LegacyTestcase[];
33
+ /** Anti-silence guards: convention mismatches that would otherwise drop data quietly
34
+ * (near-miss headers, testcase tab with 0 rows, mandatory column empty on every row). */
35
+ warnings?: string[];
33
36
  }
34
37
 
35
38
  // ---- quote-aware CSV parser (embedded commas + newlines) ----
@@ -49,7 +52,11 @@ export function parseCSV(text: string): string[][] {
49
52
  return rows;
50
53
  }
51
54
 
52
- const norm = (s: string | undefined) => (s || '').replace(/\*/g, '').trim().toLowerCase();
55
+ // Strip the "required" decoration `(*)` BEFORE stray asterisks: legacy workbooks
56
+ // mark mandatory columns as `Steps (*)` / `TC ID (*)`. Removing only the `*` left
57
+ // `steps ()` — classification regexes still matched (/steps?|expected/), but the
58
+ // exact-key column lookups missed and every mandatory cell came back '' silently.
59
+ const norm = (s: string | undefined) => (s || '').replace(/\(\s*\*\s*\)/g, '').replace(/\*/g, '').trim().toLowerCase();
53
60
 
54
61
  function detectSheetType(rows: string[][]): { type: SheetType; headerRow: number } {
55
62
  for (let i = 0; i < Math.min(rows.length, 40); i++) {
@@ -109,6 +116,22 @@ function parseTestcaseSheet(rows: string[][], headerRow: number, sheet: string):
109
116
  return out;
110
117
  }
111
118
 
119
+ /**
120
+ * exceljs's `.text` getter can THROW on a merged cell whose master value is null
121
+ * (MergeValue.toString() on null) — a `cell.text != null` guard never runs because
122
+ * READING the property explodes. Field workbook: 25 such cells across 3 tabs
123
+ * (Google-Sheets-exported merged headers). Guard on `.value` first (safe — merge
124
+ * slaves delegate to the master's value without stringifying), and keep a
125
+ * try/catch backstop for values that are non-null yet still un-stringifiable
126
+ * (e.g. a formula cell whose result is null). Never hand-stringify `.value` —
127
+ * richText/hyperlink objects would become "[object Object]"; exceljs's own
128
+ * `.text` stays the single formatter.
129
+ */
130
+ function safeCellText(cell: any): string {
131
+ if (cell.value == null) return '';
132
+ try { return String(cell.text ?? ''); } catch { return ''; }
133
+ }
134
+
112
135
  export interface RawSheet { name: string; rows: string[][] }
113
136
 
114
137
  /** Load sheets from one input file: CSV (1 sheet), XLSX (all sheets), or a JSON
@@ -137,7 +160,7 @@ async function loadSheets(filePath: string): Promise<RawSheet[]> {
137
160
  const rows: string[][] = [];
138
161
  ws.eachRow((row: any) => {
139
162
  const cells: string[] = [];
140
- row.eachCell({ includeEmpty: true }, (cell: any) => { cells.push(cell.text != null ? String(cell.text) : ''); });
163
+ row.eachCell({ includeEmpty: true }, (cell: any) => { cells.push(safeCellText(cell)); });
141
164
  rows.push(cells);
142
165
  });
143
166
  out.push({ name: ws.name, rows });
@@ -168,7 +191,7 @@ export async function parseLegacyFile(filePaths: string | string[], onlySheets?:
168
191
 
169
192
  const inv: LegacyInventory = {
170
193
  source: { file: files.map((f) => path.basename(f)).join(', ') },
171
- sheets: [], testcases: [],
194
+ sheets: [], testcases: [], warnings: [],
172
195
  };
173
196
  for (const f of files) {
174
197
  for (const s of await loadSheets(f)) {
@@ -176,6 +199,28 @@ export async function parseLegacyFile(filePaths: string | string[], onlySheets?:
176
199
  const { type, headerRow } = detectSheetType(s.rows);
177
200
  let tcs: LegacyTestcase[] = [];
178
201
  if (type === 'testcase' && headerRow >= 0) tcs = parseTestcaseSheet(s.rows, headerRow, s.name);
202
+ // Anti-silence guards — a convention mismatch must SAY so, never quietly drop data:
203
+ // (a) near-miss: a tab that carries a TC-ID-ish header yet classified unknown;
204
+ // (b) a testcase tab whose extraction produced ZERO rows;
205
+ // (c) rows extracted but a mandatory column resolved empty on EVERY row.
206
+ if (type === 'unknown') {
207
+ const loose = (v: string) => v.replace(/[^a-z0-9]/gi, '').toLowerCase();
208
+ const nearMissRow = s.rows.slice(0, 40).find((r) => r.some((c) => /tcid/.test(loose(c))));
209
+ if (nearMissRow) {
210
+ inv.warnings!.push(`sheet "${s.name}": looks like a testcase sheet (TC-ID-like header found) but was NOT classified — header candidates: [${nearMissRow.filter(Boolean).slice(0, 10).join(' | ')}]`);
211
+ }
212
+ } else if (type === 'testcase' && headerRow >= 0) {
213
+ const header = s.rows[headerRow].filter(Boolean).slice(0, 12).join(' | ');
214
+ if (tcs.length === 0) {
215
+ inv.warnings!.push(`sheet "${s.name}": classified testcase but 0 rows extracted — column names may not follow the expected convention. Header row: [${header}]`);
216
+ } else {
217
+ for (const [field, label] of [['steps', 'Steps'], ['expected', 'Expected results']] as const) {
218
+ if (tcs.every((t) => !(t as any)[field])) {
219
+ inv.warnings!.push(`sheet "${s.name}": the "${label}" column resolved EMPTY for all ${tcs.length} extracted row(s) — check its header spelling. Header row: [${header}]`);
220
+ }
221
+ }
222
+ }
223
+ }
179
224
  inv.sheets.push({ name: s.name, type, rows: tcs.length || s.rows.length });
180
225
  inv.testcases.push(...tcs);
181
226
  }
@@ -1,228 +1,146 @@
1
1
  ---
2
2
  name: delivery
3
- description: "Export the Test Case & Coverage Matrix (review/manual/customer deliverable) from Gherkin + Playwright results. --legacy exports the classic per-scenario CSV/XLSX."
4
- argument-hint: "[name...] [--format csv] [--env <locale>] [--legacy] (omit names for all)"
3
+ description: "Export Gherkin scenarios + Playwright results to CSV test case file for QA delivery."
4
+ argument-hint: "[screen-name...] [--env <locale>] (omit screens for all; --env for locale-specific export)"
5
5
  order: 50
6
- claude-tools: "Bash, Read, Write, AskUserQuestion"
6
+ claude-tools: "Bash, Read, AskUserQuestion"
7
7
  copilot-tools: "[read, execute, edit, vscode/askQuestions]"
8
- codex-trigger: "Run when the user asks to EXPORT or deliver test cases for QA hand-off. Final step. Do NOT use for creating or running tests."
8
+ codex-trigger: "Run when the user asks to EXPORT or deliver test cases to CSV/XLSX for QA hand-off. Final step. Do NOT use for creating or running tests."
9
9
  ---
10
10
  ## Role
11
11
 
12
- You are a **QA Test Delivery Engineer**. The deterministic `sungen delivery` CLI does all parsing,
13
- gating, and rendering. Your job is (1) invoking it, (2) **proposing the Delivery Map** when a unit
14
- does not have one yet — the one semantic step the CLI cannot do — and (3) walking the user through
15
- review → approve → official render.
16
-
17
- ## The two delivery modes
18
-
19
- - **Matrix (default)** — the Test Case & Coverage Matrix: one row per *delivery item* (one target +
20
- one test intent + one oracle family), with each Gherkin scenario / `@cases` row preserved as a
21
- collapsible *coverage variant* sub-row. No numbered step dumps. This is the reviewable /
22
- manually-executable / customer-facing standard.
23
- - **Legacy (`--legacy`, alias `--full`)** — the classic per-scenario-steps workbook (BM-2-901-13).
24
- Use it when the user asks for the old format or a project is mid-engagement on it.
12
+ You are a **QA Test Delivery Engineer**. Your job is to invoke the deterministic `sungen delivery` CLI that performs all parsing and CSV export. Your role is minimal — just run the CLI and help the user if pre-flight checks fail.
25
13
 
26
14
  ## Parameters
27
15
 
28
16
  Parse from `$ARGUMENTS`:
29
- - **names** — zero or more screen/flow/api names. Empty → **a sweep of every unit**.
30
- Strictness follows the scope: a **named** unit that is not ready aborts the run (the user asked
31
- for that unit); a **sweep** exports every ready unit and skips the rest with an accounted
32
- "Not exported" list, succeeding as long as something was written. So `/sungen:delivery` with no
33
- argument is safe in a work-in-progress project scaffolded units (added but `create-test` not
34
- run) no longer block the units that are ready.
35
- - **`--format <xlsx|csv|both>`** — output format. **Default `xlsx` one artifact only.** Pass
36
- `--format csv` when the user wants the flat CSV (pipelines/diffing), `--format both` for both.
37
- - **`--env <locale>`** — sets `SUNGEN_ENV=<locale>` for the run (accept `--locale` as alias).
38
- - **`--legacy` / `--full`** — pass through to the CLI and skip the map flow entirely.
39
-
40
- ## Steps
41
-
42
- ### 1. Run the CLI once
43
-
44
- ```bash
45
- [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <names> || npx sungen delivery <names>
46
- ```
47
- (prepend `SUNGEN_ENV=<locale>` when `--env` was given; append `--format <fmt>` when the user asked
48
- for a non-default format; append `--legacy` when requested — then skip to step 5.)
49
-
50
- Three outcomes per target:
51
- - **Rendered** → done, go to step 5.
52
- - **"no delivery map"** → go to step 2 (propose it).
53
- - **Gate findings / "not approved"** → go to step 4 (review & approve).
54
-
55
- On a sweep, read the **"Not exported"** list at the end: units marked *not authored yet* need
56
- `/sungen:create-test` first, the others each printed their own reason above (missing map, gate
57
- errors, or awaiting approval). Handle them one unit at a time — do not re-run the sweep expecting
58
- a different result.
59
-
60
- ### 2. Propose the Delivery Map (only when missing or the user asks to regroup)
61
-
62
- Read the unit's `.feature` (and `requirements/spec.md` for target naming). Write
63
- `qa/<screens|flows|api>/<unit>/delivery/<unit>.map.yaml`:
64
-
65
- ```yaml
66
- version: 1
67
- unit: <unit>
68
- groups:
69
- - id: DI-<TARGET>-<THEME> # stable kebab id, never renumbered
70
- target: login.email # ONE target: field/component dot-path, flow phrase, or METHOD /path
71
- intent: <one behavior/rule this item verifies>
72
- oracle: <the shared observable Pass/Fail statement>
73
- dimensions: violated rule — required ×3 · format ×10 · full-width ×2 # see below
74
- category: normal | abnormal | security | nfr # see the rule below
75
- review: proposed # ALWAYS proposed — only QA approval flips it
76
- variants: [VP-VAL-001-B, VP-VAL-001-S] # VP-ids; bare id on a @cases scenario = all its rows
77
- dispositions: # scenarios intentionally NOT delivered as test cases
78
- VP-DATA-000: { as: excluded, reason: data-setup checklist }
79
- # as: excluded | blocked | covered_elsewhere | accepted_risk
80
- ```
81
-
82
- **Grouping rules (the aggregation signature) — group COMPACTLY.** The matrix exists to be
83
- substantially shorter than the scenario list, so a reviewer can see missing viewpoints at a
84
- glance. Merge whenever the cases share ALL of: target · test intent/business rule ·
85
- precondition/condition · trigger or procedure shape · **the way the expected result is
86
- determined** (its oracle *family*, not its exact message).
87
-
88
- - **Oracle family = the determination method, parameterized.** All validation branches of ONE
89
- field belong to ONE item — required, format, length, character-class are *expected branches*
90
- (parameters) of "the field shows the validation message defined for the violated rule", shown
91
- per variant, never separate items.
92
- - MAY vary inside one item (coverage dimensions, visible on the sub-rows): data values, boundary
93
- points, **account states** (a seeded/locked/deleted account next to a wrong-password case),
94
- provider/browser/locale, `@cases` rows, a different trigger with the same oracle (blur vs
95
- submit), **execution mode** (auto + manual mix — the parent shows `Auto n · Manual m`), and
96
- **priority** (the item takes the highest; per-variant priorities stay visible).
97
- - MUST split: different target, different intent/business rule, different way of determining the
98
- expected result (a field-error family ≠ a session-established family), different test layer
99
- (`@api`/`@query`), materially different precondition, or a different procedure shape —
100
- sequence-sensitive flows (re-Given/When after a Then) stay solo. **Different risk classes never
101
- merge**: XSS and SQL injection are separate items (different risk and determination), even on
102
- the same field.
103
- - **Look for these families before settling on a grouping** — they are where under-merging happens:
104
- | Family | Merge into one item |
105
- |---|---|
106
- | Static render | every "element X is visible/has its content on load" scenario of the screen — title, instructions, progress step, buttons present, header/footer |
107
- | Field validation branches | all rules of ONE field (required · format · length · character class · full-width) |
108
- | Account/entity states | wrong-credentials · locked · deleted · unverified for the same rejection oracle |
109
- | Provider / surface sets | the 3 OAuth providers, header+footer, the a11y surfaces of one behaviour |
110
- | Lifetime / mode pairs | checked vs unchecked, mobile vs desktop, when the oracle is one rule with two branches |
111
- On a 36-scenario screen, four separate one-variant "renders on load" items should have been one.
112
- - When unsure, keep items separate — the gates and QA decide, never guess-merge.
113
- - Every scenario must land in exactly one group **or** one disposition (Gate B enforces 100%
114
- disposition). Data-setup blocks (`@manual:data-setup`) → `excluded`; SPEC-GAP placeholders →
115
- `blocked`.
116
-
117
- **`dimensions:` — the compact coverage digest (required for items with >3 variants).**
118
- This one short line is what the collapsed parent row shows instead of listing every variant, so a
119
- reviewer sees *which dimensions* the item covers without expanding it. Name the dimension, then the
120
- branches with counts:
121
- - `violated rule — required ×3 · format ×10 · full-width ×2`
122
- - `account state — wrong password · unregistered · locked · soft-deleted`
123
- - `submission method — Login button · Enter in Password · Enter in Email`
124
-
125
- Keep it ≤120 chars (Gate W warns). **YAML caveat:** a bare `: ` inside the value breaks the parse —
126
- use ` — ` as the label separator (as above) or quote the whole string.
127
-
128
- **`category` is not free choice for two classes (Gate K checks it):** a group whose variants are
129
- `VP-SEC-*` MUST be `category: security`, and `VP-NFR-*` MUST be `nfr` — otherwise the Coverage
130
- sheet's security/nfr column renders empty and the grid reports a gap the unit does not have while
131
- hiding the work it does have. `normal` vs `abnormal` stays your judgement.
132
-
133
- **Wording rules for `intent`/`oracle` (customer-facing — Gate W lints these):**
134
- - Plain product language, present simple, ~10–20 words, one behavior:
135
- "A user can sign in with valid credentials and is redirected to the Jobs page."
136
- - Oracle = the observable outcome as a definite assertion ("The Jobs page is displayed and the
137
- Logout link is visible.") — no `should`, no tester actions.
138
- - NEVER: `{{tokens}}`, `[Selector]` references, DSL phrasing (`User fill/click/see`), generator
139
- labels (`Setup:`/`Observable:`/`Oracle:`), or vague verbs (`handles`, `surfaces`) when a precise
140
- behavior exists. Use the visible UI label (the Login button, the Email field).
141
- - **Preserve the source meaning exactly** — never strengthen, weaken, or reinterpret an oracle
142
- (a security assertion especially: if the source says "the password appears ONLY in the HTTPS
143
- POST body", do not write "no plaintext password on the network").
144
-
145
- **Requirement coverage (`requirements:` section, optional):** `sungen delivery` scans
146
- `requirements/spec.md` for FR-/TR-/NFR- ids; ids traced by `@spec:` tags are `covered`, the rest
147
- are `gap` (Gate R warning). Record the reviewed status for genuine non-gaps:
148
-
149
- ```yaml
150
- requirements:
151
- TR-007: { status: planned, note: Performance needs Lighthouse-style tooling }
152
- TR-004: { status: partially_covered, note: client-side covered by VP-SEC-003; hashing needs DB verify }
153
- # status: covered | partially_covered | covered_elsewhere | planned | gap | not_applicable
154
- ```
17
+ {{#cap parallel-subagents}}
18
+ - **screens** zero or more screen/flow names. Empty CLI processes all targets in `qa/screens/` + `qa/flows/`.
19
+ {{/cap}}
20
+ {{^cap parallel-subagents}}
21
+ - **names** zero or more screen/flow names. Empty CLI processes all targets in `qa/screens/` + `qa/flows/`.
22
+ {{/cap}}
23
+ - **`--env <locale>`** — optional. Sets `SUNGEN_ENV=<locale>` for the run so the CLI merges `<name>.<locale>.yaml` over the base test-data and writes `<name>-testcases.<locale>.csv` / `.xlsx`. Accept `--locale <locale>` as an alias.
155
24
 
156
- **Never write `status: covered` for a requirement no variant traces to** (Gate R flags it). A note
157
- saying "proven by DI-SEC-CSRF" is prose — nothing detects it when that scenario later changes. If a
158
- scenario in THIS feature proves the requirement, **add `@spec:<id>` to that scenario** so the trace
159
- is real, then drop the override (it derives as `covered` on its own). Use `covered_elsewhere` only
160
- when another suite proves it, and name that suite; `not_applicable` when the spec itself excludes
161
- the requirement.
25
+ If `--env` is passed but no value follows, ask the user which locale to use.
162
26
 
163
- Then validate and fix any ERROR findings:
164
-
165
- ```bash
166
- [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <unit> --check || npx sungen delivery <unit> --check
167
- ```
168
-
169
- ### 3. Present the grouping for review
170
-
171
- Show the user a compact summary: item count vs variant count, the groups (id · target · intent ·
172
- variant count), dispositions, and every remaining `review`-level finding (oracle-shape /
173
- precondition heuristics — these are exactly what QA must confirm). Then `AskUserQuestion`:
27
+ ## Steps
174
28
 
175
- - **Approve all groups** (Recommended when the findings look right)
176
- - **Adjust the grouping** — user names the groups to change; edit the map, re-run `--check`, re-present.
177
- - **Preview draft first** — `sungen delivery <unit> --preview` (DRAFT watermark), then re-ask.
178
- - **Cancel**
29
+ ### 1. Invoke the CLI
179
30
 
180
- ### 4. Approve + official render
31
+ {{#cap parallel-subagents}}
32
+ Run via Bash (single command, no extra parsing). Prefer the local `./bin/sungen.js` when it exists — the sungen monorepo ships local-only features the global npm package doesn't have yet (multi-sheet locale aggregation, `.<env>` filename suffix, locale-aware step rendering). Fall back to `npx sungen` in downstream projects.
33
+ {{/cap}}
34
+ {{^cap parallel-subagents}}
35
+ Run via Bash (single command, no extra parsing):
36
+ {{/cap}}
181
37
 
182
38
  ```bash
183
- [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <unit> --approve || npx sungen delivery <unit> --approve
184
- [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <unit> || npx sungen delivery <unit>
39
+ # No env local-first dispatcher:
40
+ {{#cap parallel-subagents}}
41
+ [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <screens> || npx sungen delivery <screens>
42
+ {{/cap}}
43
+ {{^cap parallel-subagents}}
44
+ [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <names> || npx sungen delivery <names>
45
+ {{/cap}}
46
+
47
+ # Locale-specific:
48
+ {{#cap parallel-subagents}}
49
+ [ -x ./bin/sungen.js ] && SUNGEN_ENV=<locale> ./bin/sungen.js delivery <screens> || SUNGEN_ENV=<locale> npx sungen delivery <screens>
50
+ {{/cap}}
51
+ {{^cap parallel-subagents}}
52
+ [ -x ./bin/sungen.js ] && SUNGEN_ENV=<locale> ./bin/sungen.js delivery <names> || SUNGEN_ENV=<locale> npx sungen delivery <names>
53
+ {{/cap}}
185
54
  ```
186
55
 
187
- `--approve` stamps scenario fingerprints into the map — if the `.feature` changes later, Gate E
188
- flips the affected groups back to review (drift). Approved + unchanged groups render without
189
- re-asking.
190
-
191
- ### 5. Show summary + offer next steps
192
-
193
- Forward the CLI summary verbatim (Items = review rows; **Variants = execution unitsprogress is
194
- counted in variants, never items**). Then `AskUserQuestion`:
195
-
196
- - **Open the workbook** — inspect `qa/deliverables/<unit>-testcases.xlsx` (Testcases sheet:
197
- collapse outline level 1 for the customer view; Coverage sheet: target × category grid + gaps).
198
- - **Run tests to refresh results** — `/sungen:run-test <unit>`, then re-run delivery.
199
- - **Also export CSV** — `sungen delivery <unit> --format csv` (flat `item`/`variant` rows for pipelines).
200
- - **Export the legacy workbook too** `sungen delivery <unit> --legacy`.
201
- - **Done**
56
+ {{#cap parallel-subagents}}
57
+ - If no screen args omit `<screens>` (CLI processes all targets).
58
+ {{/cap}}
59
+ {{^cap parallel-subagents}}
60
+ - If no name args omit `<names>` (CLI processes all targets).
61
+ {{/cap}}
62
+ - If `--env <locale>` was provided prepend `SUNGEN_ENV=<locale>` to the command. Do NOT pass `--env` to the CLI itself it's not a CLI flag, only a slash-command convenience.
63
+
64
+ The CLI handles:
65
+ {{#cap parallel-subagents}}
66
+ - Scope detection (all screens + flows vs specific)
67
+ {{/cap}}
68
+ {{^cap parallel-subagents}}
69
+ - Scope detection (all screens + flows vs specific names)
70
+ - Auto-detect: `qa/flows/<name>/` → flow, `qa/screens/<name>/` → screen
71
+ {{/cap}}
72
+ - Pre-flight source checks with colorful output
73
+ - Parsing `.feature`, `.spec.ts`, `test-data.yaml` (+ `<name>.<env>.yaml` overlay when `SUNGEN_ENV` is set), and per-target `<name>-test-result[.<env>].json`
74
+ - Generating CSV/XLSX at `qa/deliverables/<name>-testcases[.<env>].csv` / `.xlsx`
75
+ - Printing summary table
76
+
77
+ ### 2. Handle pre-flight failures (if CLI exits non-zero)
78
+
79
+ {{#cap parallel-subagents}}
80
+ If the CLI exits with blocking issues, it will have already printed a clear table showing exactly what's missing per screen.
81
+ {{/cap}}
82
+ {{^cap parallel-subagents}}
83
+ If the CLI exits with blocking issues, it will have already printed a clear table showing exactly what's missing per target.
84
+ {{/cap}}
85
+
86
+ Use `AskUserQuestion` to offer next steps:
87
+
88
+ **Options:**
89
+ - **Fix missing sources** (Recommended) — Print the suggested commands from CLI output and stop. User will run those commands manually, then re-invoke `/sungen:delivery`.
90
+ {{#cap parallel-subagents}}
91
+ - **Continue with available screens** — Re-run as `npx sungen delivery <screens> --continue-on-missing` to skip screens with blocking issues.
92
+ {{/cap}}
93
+ {{^cap parallel-subagents}}
94
+ - **Continue with available targets** — Re-run as `npx sungen delivery <names> --continue-on-missing` to skip targets with blocking issues.
95
+ {{/cap}}
96
+ - **Cancel** — Exit.
97
+
98
+ ### 3. Show summary + offer next steps (on success)
99
+
100
+ Forward the CLI's summary table to the user verbatim. Then use `AskUserQuestion`:
101
+
102
+ - **Open a specific CSV** — Help user inspect one of the exported files with Read tool.
103
+ {{#cap parallel-subagents}}
104
+ - **Run tests to refresh results** — Suggest `/sungen:run-test <screen>` to update `test-results/results.json`, then re-run delivery.
105
+ - **Export another screen** — User can run `/sungen:delivery <other-screen>`.
106
+ {{/cap}}
107
+ {{^cap parallel-subagents}}
108
+ - **Run tests to refresh results** — Suggest `/sungen-run-test <name>` to update test results, then re-run delivery.
109
+ - **Export another target** — User can run `/sungen-delivery <other-name>`.
110
+ {{/cap}}
111
+ - **Done** — Exit.
202
112
 
203
113
  ## Important notes
204
114
 
205
- - **Do NOT parse/duplicate what the CLI computes** gates recompute mode/layer/priority/oracle
206
- shapes from the `.feature`; the map only carries the semantic fields (target/intent/oracle/
207
- category/grouping).
208
- - **Never write `review: approved` yourself** — only `--approve` after the user agreed. Proposing
209
- with `review: approved` defeats the QA review this format exists for.
210
- - **Do NOT modify feature/test-data files during delivery** — read-only except the map file.
211
- - The map is committed and stable: adding a group never renumbers others; deleting scenarios →
212
- update the map (Gate B lists exactly what's unmapped/stale).
115
+ - **Do NOT parse files yourself** — the CLI is the source of truth for parsing logic. Your job is orchestration + user interaction.
116
+ - **Do NOT modify feature/spec.ts/test-data files** the delivery is read-only.
117
+ - **The CLI already respects `@manual` tags, skips `@steps:` base scenarios, groups by Category 2, and generates UTF-8 BOM CSV for Excel compatibility with Vietnamese.**
118
+ - **Pre-flight check is built into the CLI** — use `--skip-preflight` only in CI/automated pipelines where checks are done externally.
213
119
 
214
- ## CLI reference
120
+ ## CLI Reference
215
121
 
216
122
  ```
217
- sungen delivery [names...] # matrix (default; needs the map) → XLSX only
218
- --format <xlsx|csv|both> # output format; default xlsx (one artifact)
219
- --check # gates only — validate the map, write nothing
220
- --approve [DI-a,DI-b] # flip proposed→approved (+ stamp fingerprints); all groups when bare
221
- --preview # render despite review findings (DRAFT watermark)
222
- --legacy | --full # classic per-scenario workbook (no map needed)
223
- --skip-preflight | --continue-on-missing | --env <env> # as before
123
+ {{#cap parallel-subagents}}
124
+ sungen delivery [screens...]
125
+ {{/cap}}
126
+ {{^cap parallel-subagents}}
127
+ sungen delivery [names...]
128
+ {{/cap}}
129
+ [--skip-preflight] Skip pre-flight checks (not recommended)
130
+ {{#cap parallel-subagents}}
131
+ [--continue-on-missing] Skip screens with blocking misses
132
+ {{/cap}}
133
+ {{^cap parallel-subagents}}
134
+ [--continue-on-missing] Skip targets with blocking misses
135
+ {{/cap}}
136
+
137
+ # Locale-aware export (env var, not a CLI flag):
138
+ {{#cap parallel-subagents}}
139
+ SUNGEN_ENV=<locale> sungen delivery [screens...]
140
+ {{/cap}}
141
+ {{^cap parallel-subagents}}
142
+ SUNGEN_ENV=<locale> sungen delivery [names...]
143
+ {{/cap}}
144
+ → reads <name>.<locale>.yaml overlay, picks <name>-test-result.<locale>.json,
145
+ writes <name>-testcases.<locale>.csv / .xlsx
224
146
  ```
225
-
226
- Outputs: `qa/deliverables/<unit>-testcases.xlsx` (Testcases + Coverage sheets) by default;
227
- `--format csv` writes `<unit>-testcases.csv` instead (flat, `Level` column `item|variant`),
228
- `--format both` writes both. Legacy mode writes the classic files instead.
@@ -30,7 +30,7 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
30
30
  | `/sungen:create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
31
31
  | `/sungen:review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
32
32
  | `/sungen:run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
33
- | `/sungen:delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all screens if no arg) |
33
+ | `/sungen:delivery [name...]` | Export test cases CSV for QA delivery (all screens if no arg) |
34
34
  | `/sungen:locale <name> <locale>` | Bootstrap i18n for a screen — audit selectors, detect locale switch, generate overlay (run before `/sungen:run-test --env <locale>`) |
35
35
 
36
36
  **Screen path:** add-screen → create-test → review → run-test → delivery.
@@ -30,7 +30,7 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
30
30
  | `/sungen-create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
31
31
  | `/sungen-review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
32
32
  | `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
33
- | `/sungen-delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all screens if no arg) |
33
+ | `/sungen-delivery [name...]` | Export test cases CSV for QA delivery (all screens if no arg) |
34
34
  | `/sungen-locale <name> <locale>` | Bootstrap i18n for a screen — audit selectors, detect locale switch, generate overlay (run before `/sungen-run-test --env <locale>`) |
35
35
 
36
36
  **Screen path:** add-screen → create-test → review → run-test → delivery.