@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.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +16 -5
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +6 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +32 -8
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +5 -4
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +15 -6
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/gates.d.ts.map +1 -1
- package/dist/exporters/matrix/gates.js +23 -2
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +11 -6
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts +2 -0
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +73 -25
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +9 -4
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js +2 -2
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +6 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +22 -3
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +33 -18
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +30 -11
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +19 -6
- package/src/exporters/matrix/build.ts +31 -9
- package/src/exporters/matrix/export.ts +21 -6
- package/src/exporters/matrix/gates.ts +23 -2
- package/src/exporters/matrix/render-csv.ts +12 -7
- package/src/exporters/matrix/render-xlsx.ts +73 -25
- package/src/exporters/matrix/types.ts +9 -4
- package/src/exporters/matrix/wording.ts +20 -3
- package/src/orchestrator/templates/ai-src/commands/delivery.md +33 -18
- 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
|
|
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:
|
|
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
|
-
//
|
|
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: ${
|
|
154
|
-
|
|
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,
|
|
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
|
|
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
|
|
206
|
-
//
|
|
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")&"
|
|
211
|
-
`IF(COUNTIF(${rng},"Blocked")>0,"Blocked",` +
|
|
212
|
-
`IF(
|
|
213
|
-
`IF(COUNTIF(${rng},"Passed")
|
|
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
|
-
|
|
254
|
+
// No placeholder text: when triggers differ the sub-rows carry them (GAP-01).
|
|
255
|
+
commonTrigger ? numbered(commonTrigger) : '',
|
|
224
256
|
item.oracle,
|
|
225
|
-
item
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 `--
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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)
|
|
170
|
-
(flat, `Level` column `item|variant`)
|
|
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 (
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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).
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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`),
|