@sun-asterisk/sungen 3.2.16-beta.2 → 3.2.16-beta.4

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 (42) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +16 -5
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/exporters/matrix/build.d.ts +6 -0
  5. package/dist/exporters/matrix/build.d.ts.map +1 -1
  6. package/dist/exporters/matrix/build.js +32 -8
  7. package/dist/exporters/matrix/build.js.map +1 -1
  8. package/dist/exporters/matrix/export.d.ts +5 -4
  9. package/dist/exporters/matrix/export.d.ts.map +1 -1
  10. package/dist/exporters/matrix/export.js +15 -6
  11. package/dist/exporters/matrix/export.js.map +1 -1
  12. package/dist/exporters/matrix/gates.d.ts.map +1 -1
  13. package/dist/exporters/matrix/gates.js +23 -2
  14. package/dist/exporters/matrix/gates.js.map +1 -1
  15. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  16. package/dist/exporters/matrix/render-csv.js +11 -6
  17. package/dist/exporters/matrix/render-csv.js.map +1 -1
  18. package/dist/exporters/matrix/render-xlsx.d.ts +2 -0
  19. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  20. package/dist/exporters/matrix/render-xlsx.js +73 -25
  21. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  22. package/dist/exporters/matrix/types.d.ts +9 -4
  23. package/dist/exporters/matrix/types.d.ts.map +1 -1
  24. package/dist/exporters/matrix/types.js +2 -2
  25. package/dist/exporters/matrix/types.js.map +1 -1
  26. package/dist/exporters/matrix/wording.d.ts +6 -0
  27. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  28. package/dist/exporters/matrix/wording.js +22 -3
  29. package/dist/exporters/matrix/wording.js.map +1 -1
  30. package/dist/orchestrator/templates/ai-src/commands/delivery.md +33 -18
  31. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +30 -11
  32. package/package.json +3 -3
  33. package/src/cli/commands/delivery.ts +19 -6
  34. package/src/exporters/matrix/build.ts +31 -9
  35. package/src/exporters/matrix/export.ts +21 -6
  36. package/src/exporters/matrix/gates.ts +23 -2
  37. package/src/exporters/matrix/render-csv.ts +12 -7
  38. package/src/exporters/matrix/render-xlsx.ts +73 -25
  39. package/src/exporters/matrix/types.ts +9 -4
  40. package/src/exporters/matrix/wording.ts +20 -3
  41. package/src/orchestrator/templates/ai-src/commands/delivery.md +33 -18
  42. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +30 -11
@@ -54,6 +54,15 @@ function dataCell(c: ExcelJS.Cell, value: ExcelJS.CellValue, opts?: { bold?: boo
54
54
  if (opts?.fill) c.fill = { type: 'pattern', pattern: 'solid', fgColor: opts.fill };
55
55
  }
56
56
 
57
+ /** Mode is a coverage dimension: mixed items show both counts. */
58
+ export function itemModeLabel(item: DeliveryItem): string {
59
+ if (item.mode === 'mixed') {
60
+ const auto = item.variants.filter((v) => v.mode === 'auto').length;
61
+ return `Auto ${auto} · Manual ${item.variants.length - auto}`;
62
+ }
63
+ return item.mode === 'manual' ? 'Manual' : 'Auto';
64
+ }
65
+
57
66
  export function itemResultLabel(item: DeliveryItem): string {
58
67
  const { passed, failed, blocked, notRun } = item.resultCounts;
59
68
  const total = item.variants.length;
@@ -111,7 +120,7 @@ function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
111
120
  const MATRIX_HEADERS = [
112
121
  'ID', 'Target', 'Test Intent / Condition', 'Category', 'Priority', 'Precondition',
113
122
  'Coverage / Test Data', 'Action / Trigger', 'Expected Result', 'Mode', 'Trace',
114
- 'Result', 'Executed Date', 'Executor', 'Note\n(Evidence, DefectID)',
123
+ 'Result', 'Executed Date', 'Executor', 'Note / Evidence', 'Defect ID',
115
124
  ];
116
125
  const RESULT_COL = 13; // column M
117
126
 
@@ -135,25 +144,35 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
135
144
  { width: 22 }, // M Result
136
145
  { width: 12 }, // N Executed
137
146
  { width: 13 }, // O Executor
138
- { width: 24 }, // P Note
147
+ { width: 22 }, // P Note / Evidence
148
+ { width: 12 }, // Q Defect ID
139
149
  ];
140
150
 
141
151
  renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} TEST CASE & COVERAGE MATRIX`, sungenVersion, model.formNo);
142
152
  draftBanner(ws, model);
143
153
 
144
- // Summary band (row 6) — items vs variants stay separate metrics by design (I8):
145
- // the ITEM count is reviewer workload, the VARIANT count is execution workload.
154
+ // Summary band (row 6) — items vs variants stay separate metrics by design (I8).
155
+ // The execution counters are LIVE formulas over the variant Result cells, so
156
+ // hand-entered results update the totals (review GAP-06). Variant cells hold the
157
+ // exact state words; parent cells hold composed labels — exact-match COUNTIF
158
+ // therefore counts variants only.
146
159
  const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
147
160
  const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
148
161
  const notRun = model.items.reduce((a, i) => a + i.resultCounts.notRun + i.resultCounts.blocked, 0);
162
+ const RANGE = 'M9:M10000';
149
163
  const band = ws.getRow(6);
150
- [
151
- `Delivery items: ${model.manifest.itemCount}`,
152
- `Coverage variants: ${model.manifest.variantCount}`,
153
- `Passed: ${passed}`, `Failed: ${failed}`, `Pending: ${notRun}`,
154
- ].forEach((label, i) => {
164
+ const bandCells: Array<[string, ExcelJS.CellValue]> = [
165
+ ['items', `Delivery items: ${model.manifest.itemCount}`],
166
+ ['variants', `Coverage variants: ${model.manifest.variantCount}`],
167
+ ['passed', { formula: `"Passed: "&COUNTIF(${RANGE},"Passed")`, result: `Passed: ${passed}` } as ExcelJS.CellValue],
168
+ ['failed', { formula: `"Failed: "&COUNTIF(${RANGE},"Failed")`, result: `Failed: ${failed}` } as ExcelJS.CellValue],
169
+ ['blocked', { formula: `"Blocked: "&COUNTIF(${RANGE},"Blocked")`, result: 'Blocked: 0' } as ExcelJS.CellValue],
170
+ ['pending', { formula: `"Pending: "&COUNTIF(${RANGE},"Pending")`, result: `Pending: ${notRun}` } as ExcelJS.CellValue],
171
+ ['na', { formula: `"N/A: "&COUNTIF(${RANGE},"N/A")`, result: 'N/A: 0' } as ExcelJS.CellValue],
172
+ ];
173
+ bandCells.forEach(([, v], i) => {
155
174
  const c = band.getCell(2 + i);
156
- dataCell(c, label, { bold: true, fill: SOFT_BLUE, center: true });
175
+ dataCell(c, v, { bold: true, fill: SOFT_BLUE, center: true });
157
176
  });
158
177
 
159
178
  const HEADER_ROW = 8;
@@ -169,16 +188,25 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
169
188
  // Every item gets sub-rows — the sub-row is where the source scenario id, the
170
189
  // resolved data, and the manual result/evidence entry live (review B-05).
171
190
  const firstChild = rowIdx;
191
+ // Delta-only sub-rows (review GAP-01): content identical across the whole
192
+ // item lives ONCE on the parent — a child repeats only what distinguishes it.
193
+ // The child's Expected Result is always kept: it is the precise source
194
+ // oracle (the semantic anchor), not a repetition of the parent's summary.
195
+ const commonPre = new Set(item.precondition);
196
+ const triggerTextsAll = new Set(item.variants.map((v) => v.trigger.join('\n')));
197
+ const triggerIsCommon = triggerTextsAll.size === 1;
172
198
  for (const v of item.variants) {
173
199
  const vr = ws.getRow(rowIdx++);
174
200
  vr.outlineLevel = 1;
201
+ const preDelta = v.precondition.filter((line) => !commonPre.has(line));
175
202
  const vValues: ExcelJS.CellValue[] = [
176
203
  v.ref,
177
204
  '',
178
205
  v.condition,
179
- '', '', '',
206
+ '', '',
207
+ numbered(preDelta),
180
208
  numbered(v.data),
181
- numbered(v.trigger),
209
+ triggerIsCommon ? '' : numbered(v.trigger),
182
210
  expectedWithVerification(v.oracle, v.verification),
183
211
  v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
184
212
  v.traces.join(', '),
@@ -186,6 +214,7 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
186
214
  isoDate(v.result?.startTime),
187
215
  '',
188
216
  v.result?.error ? String(v.result.error).slice(0, 200) : '',
217
+ '',
189
218
  ];
190
219
  vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
191
220
  // Manual-execution entry: constrain the Result cell to the known states.
@@ -198,19 +227,21 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
198
227
  // --- Parent row: only values that are genuinely COMMON to the whole item
199
228
  // (review M-04); everything variant-specific stays on the sub-rows.
200
229
  const pr = ws.getRow(parentRowIdx);
201
- const triggerTexts = new Set(item.variants.map((v) => v.trigger.join('\n')));
202
- const commonTrigger = triggerTexts.size === 1 ? item.variants[0].trigger : null;
230
+ const commonTrigger = triggerIsCommon ? item.variants[0].trigger : null;
203
231
  const coverage = `${item.variants.length} variant(s): ${item.variants.map((v) => v.condition).join(' · ')}`;
204
232
 
205
- // Live roll-up: derived from the child Result cells so hand-entered results
206
- // recompute the parent — precedence failed → blocked → pending → partial → passed.
233
+ // Live roll-up derived from the child Result cells precedence
234
+ // failed → blocked → pending → partial → passed. N/A variants leave the
235
+ // denominator (review GAP-06): eff = n − COUNTIF(N/A); all-N/A → "N/A".
207
236
  const rng = `M${firstChild}:M${lastChild}`;
208
237
  const n = item.variants.length;
238
+ const EFF = `(${n}-COUNTIF(${rng},"N/A"))`;
209
239
  const rollUpFormula =
210
- `IF(COUNTIF(${rng},"Failed")>0,COUNTIF(${rng},"Passed")&"/${n} Passed · "&COUNTIF(${rng},"Failed")&" Failed",` +
211
- `IF(COUNTIF(${rng},"Blocked")>0,"Blocked",` +
212
- `IF(COUNTIF(${rng},"Passed")=${n},"${n}/${n} Passed",` +
213
- `IF(COUNTIF(${rng},"Passed")=0,"Pending",COUNTIF(${rng},"Passed")&"/${n} Passed · pending"))))`;
240
+ `IF(COUNTIF(${rng},"Failed")>0,COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · "&COUNTIF(${rng},"Failed")&" Failed",` +
241
+ `IF(COUNTIF(${rng},"Blocked")>0,COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · "&COUNTIF(${rng},"Blocked")&" Blocked",` +
242
+ `IF(${EFF}=0,"N/A",` +
243
+ `IF(COUNTIF(${rng},"Passed")=${EFF},${EFF}&"/"&${EFF}&" Passed",` +
244
+ `IF(COUNTIF(${rng},"Passed")=0,"Pending",COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · pending")))))`;
214
245
 
215
246
  const values: ExcelJS.CellValue[] = [
216
247
  item.id,
@@ -220,14 +251,16 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
220
251
  item.priority,
221
252
  numbered(item.precondition),
222
253
  coverage,
223
- commonTrigger ? numbered(commonTrigger) : '(differs per variant see sub-rows)',
254
+ // No placeholder text: when triggers differ the sub-rows carry them (GAP-01).
255
+ commonTrigger ? numbered(commonTrigger) : '',
224
256
  item.oracle,
225
- item.mode === 'manual' ? 'Manual' : 'Auto',
257
+ itemModeLabel(item),
226
258
  item.traces.join(', '),
227
259
  { formula: rollUpFormula, result: itemResultLabel(item) } as ExcelJS.CellValue,
228
260
  '',
229
261
  '',
230
262
  item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
263
+ '',
231
264
  ];
232
265
  values.forEach((v, i) => dataCell(pr.getCell(2 + i), v, {
233
266
  bold: true,
@@ -237,9 +270,18 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
237
270
  }));
238
271
  }
239
272
 
240
- ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 16 } };
273
+ ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 17 } };
241
274
  // Freeze the header band AND the ID + Target columns (review §9).
242
275
  ws.views = [{ state: 'frozen', xSplit: 3, ySplit: HEADER_ROW }];
276
+ // Print: landscape, fit width only (never squash all rows into one page),
277
+ // repeat the masthead + header rows on every page (review GAP-08).
278
+ ws.pageSetup = {
279
+ orientation: 'landscape',
280
+ fitToPage: true,
281
+ fitToWidth: 1,
282
+ fitToHeight: 0,
283
+ printTitlesRow: '1:8',
284
+ };
243
285
  }
244
286
 
245
287
  // ---------------------------------------------------------------------------
@@ -261,8 +303,14 @@ function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersio
261
303
  if (model.requirements.length > 0) {
262
304
  dataCell(ws.getRow(rowIdx).getCell(2), 'Requirement coverage — every id has an explicit status', { bold: true });
263
305
  rowIdx += 1;
264
- const rh = ws.getRow(rowIdx++);
265
- ['Requirement', 'Status', 'Delivery items', 'Variants', 'Note'].forEach((l, i) => headerCell(rh.getCell(2 + i), l));
306
+ const rh = ws.getRow(rowIdx);
307
+ ws.mergeCells(rowIdx, 4, rowIdx, 6);
308
+ headerCell(rh.getCell(2), 'Requirement');
309
+ headerCell(rh.getCell(3), 'Status');
310
+ headerCell(rh.getCell(4), 'Delivery items');
311
+ headerCell(rh.getCell(7), 'Variants');
312
+ headerCell(rh.getCell(8), 'Note');
313
+ rowIdx += 1;
266
314
  for (const r of model.requirements) {
267
315
  const row = ws.getRow(rowIdx++);
268
316
  dataCell(row.getCell(2), r.id, { bold: true });
@@ -57,7 +57,9 @@ export interface DeliveryMap {
57
57
  /** Requirement-id → reviewed status override (partially_covered / not_applicable / …).
58
58
  * Without an override a requirement is `covered` when a variant traces to it, else `gap`. */
59
59
  requirements: Record<string, RequirementOverride>;
60
- /** VP-id → scenario fingerprint stamped at approval time (drift detector input). */
60
+ /** VP-id → scenario fingerprint stamped at approval time (drift detector input).
61
+ * The reserved key `__map__` holds the fingerprint of the map's own semantic
62
+ * content (groups + dispositions) so edited wording re-opens review (GAP-09). */
61
63
  fingerprints: Record<string, string>;
62
64
  }
63
65
 
@@ -126,8 +128,11 @@ export interface DeliveryItem {
126
128
  oracle: string;
127
129
  category: MapCategory;
128
130
  review: ReviewState;
131
+ /** Highest priority among the variants (per-variant priorities stay on the sub-rows). */
129
132
  priority: string;
130
- mode: 'auto' | 'manual';
133
+ /** Execution mode is a coverage dimension, not a split — 'mixed' when auto and
134
+ * manual variants share one intent (e.g. account states needing seeded data). */
135
+ mode: 'auto' | 'manual' | 'mixed';
131
136
  layers: MatrixLayer[];
132
137
  /** Union of variant traces (exact per-variant traces stay on the variants). */
133
138
  traces: string[];
@@ -198,5 +203,5 @@ export interface MatrixModel {
198
203
  manifest: MatrixManifest;
199
204
  }
200
205
 
201
- /** Complexity warning threshold (rules draft Gate I) — echoed in the manifest. */
202
- export const MAX_VARIANTS_PER_ITEM = 12;
206
+ /** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
207
+ export const MAX_VARIANTS_PER_ITEM = 20;
@@ -19,10 +19,13 @@ function deRef(text: string): string {
19
19
  }
20
20
 
21
21
  function sentence(text: string): string {
22
- let s = text.trim().replace(/\s+/g, ' ');
22
+ // Trim the ENDS only — internal whitespace may be the test data itself
23
+ // (a padded email, a spaces-only value); collapsing it would silently
24
+ // change what the tester types (review GAP-04).
25
+ let s = text.trim();
23
26
  if (!s) return s;
24
27
  s = s.charAt(0).toUpperCase() + s.slice(1);
25
- if (!/[.!?…]$/.test(s)) s += '.';
28
+ if (!/[.!?…"]$/.test(s)) s += '.';
26
29
  return s;
27
30
  }
28
31
 
@@ -42,7 +45,7 @@ export function renderAction(raw: string): string {
42
45
  // click [X] <type>
43
46
  [/^clicks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Click the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
44
47
  // press <Key> at/in [X] field
45
- [/^press(?:es)? (.+?) (?:at|in|inside) \[([^\]]+)\](?: field)?$/i, (m) => `Press ${m[1]} in the ${m[2]} field`],
48
+ [/^press(?:es)? (.+?) (?:at|in|on|inside) \[([^\]]+)\](?: field)?$/i, (m) => `Press ${m[1]} in the ${m[2]} field`],
46
49
  [/^press(?:es)? (.+)$/i, (m) => `Press ${m[1]}`],
47
50
  // select V in/from [X] dropdown
48
51
  [/^selects? (.+?) (?:in|from) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Select ${m[1]} in the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
@@ -103,9 +106,23 @@ export function renderPrecondition(raw: string): string {
103
106
  const s = raw.trim().replace(/^(User|The user)\s+/i, '');
104
107
  const m = s.match(/^is on \[([^\]]+)\] page(.*)$/i);
105
108
  if (m) return sentence(`The user is on the ${m[1]} page${m[2] ?? ''}`);
109
+ // "wait for [X] <type> (is) visible" is a STATE once established, not an action.
110
+ const w = s.match(/^waits? for \[([^\]]+)\]\s*(\w+)?(?: is)?(?: visible)?$/i);
111
+ if (w) return sentence(`The ${w[1]}${w[2] ? ` ${w[2]}` : ''} is visible`);
112
+ const auth = s.match(/^is (signed|logged) (in|out)(.*)$/i);
113
+ if (auth) return sentence(`The user is ${auth[1]} ${auth[2]}${auth[3] ?? ''}`);
106
114
  return sentence(deRef(`The user ${s.charAt(0).toLowerCase()}${s.slice(1)}`));
107
115
  }
108
116
 
117
+ /**
118
+ * A manual `Setup:` line is an INSTRUCTION the tester performs to establish the
119
+ * state — imperative reads correctly ("Seed the locked account."), while
120
+ * "The user seed …" is broken grammar (review GAP-08).
121
+ */
122
+ export function renderSetupInstruction(raw: string): string {
123
+ return renderAction(raw);
124
+ }
125
+
109
126
  // ---------------------------------------------------------------------------
110
127
  // Manual `# Tester verifies:` comment classification (structured, label-free)
111
128
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: delivery
3
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...] [--env <locale>] [--legacy] (omit names for all; --env for locale-specific export)"
4
+ argument-hint: "[name...] [--format csv] [--env <locale>] [--legacy] (omit names for all)"
5
5
  order: 50
6
6
  claude-tools: "Bash, Read, Write, AskUserQuestion"
7
7
  copilot-tools: "[read, execute, edit, vscode/askQuestions]"
@@ -27,6 +27,8 @@ review → approve → official render.
27
27
 
28
28
  Parse from `$ARGUMENTS`:
29
29
  - **names** — zero or more screen/flow/api names. Empty → all targets.
30
+ - **`--format <xlsx|csv|both>`** — output format. **Default `xlsx` — one artifact only.** Pass
31
+ `--format csv` when the user wants the flat CSV (pipelines/diffing), `--format both` for both.
30
32
  - **`--env <locale>`** — sets `SUNGEN_ENV=<locale>` for the run (accept `--locale` as alias).
31
33
  - **`--legacy` / `--full`** — pass through to the CLI and skip the map flow entirely.
32
34
 
@@ -37,8 +39,8 @@ Parse from `$ARGUMENTS`:
37
39
  ```bash
38
40
  [ -x ./bin/sungen.js ] && ./bin/sungen.js delivery <names> || npx sungen delivery <names>
39
41
  ```
40
- (prepend `SUNGEN_ENV=<locale>` when `--env` was given; append `--legacy` when requested then skip
41
- to step 5.)
42
+ (prepend `SUNGEN_ENV=<locale>` when `--env` was given; append `--format <fmt>` when the user asked
43
+ for a non-default format; append `--legacy` when requested — then skip to step 5.)
42
44
 
43
45
  Three outcomes per target:
44
46
  - **Rendered** → done, go to step 5.
@@ -66,18 +68,28 @@ dispositions: # scenarios intentionally NOT delivered as te
66
68
  # as: excluded | blocked | covered_elsewhere | accepted_risk
67
69
  ```
68
70
 
69
- **Grouping rules (the aggregation signature):**
70
- - One group = **one target + one intent + one oracle family**. When unsure, keep items separate —
71
- the gates and QA decide, never guess-merge.
72
- - MAY share a group (become coverage dimensions): equivalence partitions, boundary values,
73
- different data (`@cases` rows), a different trigger with the same oracle (blur vs submit),
74
- locales.
75
- - MUST split: different target, intent, oracle family, category, execution mode (`@manual` vs
76
- auto), test layer (`@api`/`@query`), or priority tag; sequence-sensitive flows (re-Given/When
77
- after a Then) stay solo. **Different risk classes never merge** XSS and SQL injection are
78
- separate items even though both are "injection on the same field" (different risk, action,
79
- and oracle family); a component's visibility rule and its action/revalidation rule are two
80
- intents, not one.
71
+ **Grouping rules (the aggregation signature) — group COMPACTLY.** The matrix exists to be
72
+ substantially shorter than the scenario list, so a reviewer can see missing viewpoints at a
73
+ glance. Merge whenever the cases share ALL of: target · test intent/business rule ·
74
+ precondition/condition · trigger or procedure shape · **the way the expected result is
75
+ determined** (its oracle *family*, not its exact message).
76
+
77
+ - **Oracle family = the determination method, parameterized.** All validation branches of ONE
78
+ field belong to ONE item — required, format, length, character-class are *expected branches*
79
+ (parameters) of "the field shows the validation message defined for the violated rule", shown
80
+ per variant, never separate items.
81
+ - MAY vary inside one item (coverage dimensions, visible on the sub-rows): data values, boundary
82
+ points, **account states** (a seeded/locked/deleted account next to a wrong-password case),
83
+ provider/browser/locale, `@cases` rows, a different trigger with the same oracle (blur vs
84
+ submit), **execution mode** (auto + manual mix — the parent shows `Auto n · Manual m`), and
85
+ **priority** (the item takes the highest; per-variant priorities stay visible).
86
+ - MUST split: different target, different intent/business rule, different way of determining the
87
+ expected result (a field-error family ≠ a session-established family), different test layer
88
+ (`@api`/`@query`), materially different precondition, or a different procedure shape —
89
+ sequence-sensitive flows (re-Given/When after a Then) stay solo. **Different risk classes never
90
+ merge**: XSS and SQL injection are separate items (different risk and determination), even on
91
+ the same field.
92
+ - When unsure, keep items separate — the gates and QA decide, never guess-merge.
81
93
  - Every scenario must land in exactly one group **or** one disposition (Gate B enforces 100%
82
94
  disposition). Data-setup blocks (`@manual:data-setup`) → `excluded`; SPEC-GAP placeholders →
83
95
  `blocked`.
@@ -141,6 +153,7 @@ counted in variants, never items**). Then `AskUserQuestion`:
141
153
  - **Open the workbook** — inspect `qa/deliverables/<unit>-testcases.xlsx` (Testcases sheet:
142
154
  collapse outline level 1 for the customer view; Coverage sheet: target × category grid + gaps).
143
155
  - **Run tests to refresh results** — `/sungen:run-test <unit>`, then re-run delivery.
156
+ - **Also export CSV** — `sungen delivery <unit> --format csv` (flat `item`/`variant` rows for pipelines).
144
157
  - **Export the legacy workbook too** — `sungen delivery <unit> --legacy`.
145
158
  - **Done**
146
159
 
@@ -158,7 +171,8 @@ counted in variants, never items**). Then `AskUserQuestion`:
158
171
  ## CLI reference
159
172
 
160
173
  ```
161
- sungen delivery [names...] # matrix (default; needs the map)
174
+ sungen delivery [names...] # matrix (default; needs the map) → XLSX only
175
+ --format <xlsx|csv|both> # output format; default xlsx (one artifact)
162
176
  --check # gates only — validate the map, write nothing
163
177
  --approve [DI-a,DI-b] # flip proposed→approved (+ stamp fingerprints); all groups when bare
164
178
  --preview # render despite review findings (DRAFT watermark)
@@ -166,5 +180,6 @@ sungen delivery [names...] # matrix (default; needs the map)
166
180
  --skip-preflight | --continue-on-missing | --env <env> # as before
167
181
  ```
168
182
 
169
- Outputs: `qa/deliverables/<unit>-testcases.xlsx` (Testcases + Coverage sheets) + `.csv`
170
- (flat, `Level` column `item|variant`). Legacy mode writes the classic files instead.
183
+ Outputs: `qa/deliverables/<unit>-testcases.xlsx` (Testcases + Coverage sheets) by default;
184
+ `--format csv` writes `<unit>-testcases.csv` instead (flat, `Level` column `item|variant`),
185
+ `--format both` writes both. Legacy mode writes the classic files instead.
@@ -29,12 +29,15 @@ fingerprints). Schema + grouping rules live in the delivery command instructions
29
29
  spec is `docs/spec/delivery-coverage-matrix-spec.md`.
30
30
 
31
31
  **Gates** (CLI `--check`): A source (VP-ids unique, oracle present, Background setup-only) ·
32
- B mapping (every scenario in exactly one group XOR one disposition) · C aggregation (mode/layer/
33
- priority recomputed and equal within a group — heuristic oracle-shape/precondition mismatches are
34
- review-level, silenced once approved and unchanged) · D executability (precondition · condition+
32
+ B mapping (every scenario in exactly one group XOR one disposition) · C aggregation (test layer
33
+ recomputed and equal within a group — **execution mode and priority are coverage dimensions, not
34
+ splits**: mixed items show `Auto n · Manual m` and take the highest variant priority; heuristic
35
+ oracle-shape/precondition mismatches are review-level, silenced once approved and unchanged) ·
36
+ D executability (precondition · condition+
35
37
  data · trigger · oracle all renderable; every `{{var}}` resolves; **no template token may survive
36
38
  into a rendered cell** — test-data cross-references are resolved for display) · E drift
37
- (fingerprint mismatch → back to review) · G review state (proposed groups block the official
39
+ (scenario fingerprint mismatch → back to review; the map's OWN reviewed
40
+ wording/grouping is fingerprinted as `__map__` too, so post-approval edits re-open review) · G review state (proposed groups block the official
38
41
  render; `--preview` renders a DRAFT watermark) · R requirement coverage (spec FR/TR/NFR ids with
39
42
  no trace and no `requirements:` status → warning) · W wording lint (map intent/oracle containing
40
43
  tokens, `[Selector]` refs, DSL phrasing, or generator labels → warning).
@@ -49,17 +52,33 @@ Precondition, `Action:` → Action, `Observable:` → Expected Result, `Oracle:`
49
52
  mid-flow assertions inline as `Verify: …`; only the final Then block is the Expected Result.
50
53
  Empty test values render as `(empty)`.
51
54
 
55
+ **Output format**: **XLSX only by default** (one artifact). `--format csv` writes the flat CSV
56
+ instead; `--format both` writes both.
57
+
52
58
  **Workbook**: `Testcases` sheet — parent rows + outline-level-1 variant sub-rows for **every**
53
59
  item (single-variant included: the sub-row carries the source VP-id, resolved data, and the
54
- result/evidence entry). Collapse outline for the customer view, expand to execute. Variant Result
55
- cells have a dropdown (Passed/Failed/Blocked/Pending/N/A) and the parent Result is a **live Excel
56
- formula** over its children (failed→blocked→pending→partial→passed, e.g. `2/3 Passed · 1 Failed`)
57
- a parent can never contradict its variants, even after manual edits. ID + Target columns are
58
- frozen; dates are ISO (`2026-08-04`). `Coverage` sheet — requirement coverage table (every FR/TR/
59
- NFR id with an explicit status), target × category grid with explicit `—` gaps, dispositions,
60
- manifest. CSV is flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
60
+ result/evidence entry). Sub-rows are **delta-only**: knowledge common to the whole item
61
+ (precondition, trigger) is written ONCE on the parent; a child repeats only what distinguishes it
62
+ (condition/data, precondition delta, trigger when it differs) plus its own precise oracle and
63
+ execution fields. The parent never carries placeholder text when triggers differ the cell is
64
+ simply empty and the sub-rows carry them.
65
+
66
+ Parent preconditions are the **intersection** of the variants' variant-specific setup stays on
67
+ the variant, so no variant inherits a wrong start state. Variant Result cells have a dropdown
68
+ (Passed/Failed/Blocked/Pending/N/A) and the parent Result is a **live Excel formula** over its
69
+ children (failed→blocked→pending→partial→passed; **`N/A` leaves the denominator**, all-`N/A` →
70
+ `N/A`). The summary band counts results with live formulas too. Evidence and Defect ID are
71
+ separate columns; ID + Target frozen; ISO dates; landscape print with repeated header rows.
72
+
73
+ `Coverage` sheet — requirement coverage table (every FR/TR/NFR id with an explicit status),
74
+ target × category grid with explicit `—` gaps, dispositions, manifest. CSV mirrors the same model
75
+ flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
61
76
  `delivery_item_count` ≠ progress — variants are the execution metric.
62
77
 
78
+ **Data fidelity**: invisible test data is made visible, never normalized — `''` → `(empty)`,
79
+ whitespace-only → `(5 spaces)`, padded → `" value "` quoted verbatim. A trim/collapse here would
80
+ silently break the whitespace tests it describes.
81
+
63
82
  **Authoring guidance the matrix rewards** (create-test side): payload/provider matrices (SQLi
64
83
  payload lists, OAuth provider sets) belong in `@cases` datasets so each case is an atomic,
65
84
  independently-reportable variant; keep dataset `case:` labels short and stable (`CHK-EMAIL-I1`),