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

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 (30) hide show
  1. package/dist/exporters/matrix/build.d.ts +14 -2
  2. package/dist/exporters/matrix/build.d.ts.map +1 -1
  3. package/dist/exporters/matrix/build.js +55 -20
  4. package/dist/exporters/matrix/build.js.map +1 -1
  5. package/dist/exporters/matrix/gates.js +16 -1
  6. package/dist/exporters/matrix/gates.js.map +1 -1
  7. package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
  8. package/dist/exporters/matrix/map-loader.js +2 -0
  9. package/dist/exporters/matrix/map-loader.js.map +1 -1
  10. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  11. package/dist/exporters/matrix/render-csv.js +8 -10
  12. package/dist/exporters/matrix/render-csv.js.map +1 -1
  13. package/dist/exporters/matrix/render-xlsx.d.ts +14 -0
  14. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  15. package/dist/exporters/matrix/render-xlsx.js +62 -42
  16. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  17. package/dist/exporters/matrix/types.d.ts +18 -2
  18. package/dist/exporters/matrix/types.d.ts.map +1 -1
  19. package/dist/exporters/matrix/types.js.map +1 -1
  20. package/dist/orchestrator/templates/ai-src/commands/delivery.md +12 -0
  21. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +12 -6
  22. package/package.json +3 -3
  23. package/src/exporters/matrix/build.ts +56 -17
  24. package/src/exporters/matrix/gates.ts +16 -1
  25. package/src/exporters/matrix/map-loader.ts +2 -0
  26. package/src/exporters/matrix/render-csv.ts +9 -11
  27. package/src/exporters/matrix/render-xlsx.ts +59 -43
  28. package/src/exporters/matrix/types.ts +19 -3
  29. package/src/orchestrator/templates/ai-src/commands/delivery.md +12 -0
  30. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +12 -6
@@ -5,8 +5,8 @@
5
5
  * and result), so downstream pipelines can re-derive both views from one file.
6
6
  */
7
7
 
8
- import { statusToTestResult } from '../playwright-report-parser';
9
- import { itemModeLabel, itemResultLabel } from './render-xlsx';
8
+ import { variantState } from './build';
9
+ import { coverageSummary, itemModeLabel, itemResultLabel } from './render-xlsx';
10
10
  import { MatrixModel } from './types';
11
11
 
12
12
  const HEADERS = [
@@ -35,9 +35,6 @@ export function renderMatrixCsv(model: MatrixModel): string {
35
35
  lines.push(HEADERS.map(esc).join(','));
36
36
 
37
37
  for (const item of model.items) {
38
- const triggerTexts = new Set(item.variants.map((v) => v.trigger.join('\n')));
39
- const triggerIsCommon = triggerTexts.size === 1;
40
- const commonPre = new Set(item.precondition);
41
38
  lines.push([
42
39
  'item',
43
40
  item.id,
@@ -46,9 +43,9 @@ export function renderMatrixCsv(model: MatrixModel): string {
46
43
  item.category,
47
44
  item.priority,
48
45
  item.precondition.join('\n'),
49
- `${item.variants.length} variant(s): ${item.variants.map((v) => v.condition).join(' · ')}`,
50
- // No placeholder text: when triggers differ the variant rows carry them (GAP-01).
51
- triggerIsCommon ? item.variants[0].trigger.join('\n') : '',
46
+ coverageSummary(item),
47
+ // Shared leading steps only; each variant row carries its remaining steps.
48
+ item.trigger.join('\n'),
52
49
  item.oracle,
53
50
  itemModeLabel(item),
54
51
  item.traces.join(' '),
@@ -69,13 +66,14 @@ export function renderMatrixCsv(model: MatrixModel): string {
69
66
  v.condition,
70
67
  '', '',
71
68
  // Precondition DELTA — variant-specific setup only (review GAP-03).
72
- v.precondition.filter((line) => !commonPre.has(line)).join('\n'),
69
+ (item.preconditionDeltas[v.ref] ?? []).join('\n'),
73
70
  v.data.join('\n'),
74
- triggerIsCommon ? '' : v.trigger.join('\n'),
71
+ // Steps after the parent's shared prefix only.
72
+ (item.triggerDeltas[v.ref] ?? []).join('\n'),
75
73
  expected,
76
74
  v.mode === 'manual' ? 'Manual' : 'Auto',
77
75
  v.traces.join(' '),
78
- v.result ? statusToTestResult(v.result.status) : 'Pending',
76
+ variantState(v),
79
77
  isoDate(v.result?.startTime),
80
78
  v.result?.error ? String(v.result.error).slice(0, 200) : '',
81
79
  '',
@@ -27,8 +27,8 @@ import {
27
27
  BLACK,
28
28
  LAVENDER,
29
29
  } from '../xlsx-report-builder';
30
- import { statusToTestResult } from '../playwright-report-parser';
31
- import { CoverageVariant, DeliveryItem, ItemResult, MatrixModel, MapCategory } from './types';
30
+ import { variantState } from './build';
31
+ import { DeliveryItem, MatrixModel, MapCategory } from './types';
32
32
 
33
33
  const CATEGORY_ORDER: MapCategory[] = ['normal', 'abnormal', 'security', 'nfr'];
34
34
  const SOFT_BLUE = { argb: 'FFDCE6F1' };
@@ -63,22 +63,21 @@ export function itemModeLabel(item: DeliveryItem): string {
63
63
  return item.mode === 'manual' ? 'Manual' : 'Auto';
64
64
  }
65
65
 
66
+ /**
67
+ * Parent roll-up label. It must NEVER be exactly one of the variant state words
68
+ * (`Passed`/`Failed`/`Blocked`/`Pending`/`N/A`) — the summary band counts the
69
+ * Result column with exact-match COUNTIF over parent AND child rows, so a parent
70
+ * reading plain "Pending" was counted as an extra pending variant (109 instead
71
+ * of 77 on st-login).
72
+ */
66
73
  export function itemResultLabel(item: DeliveryItem): string {
67
- const { passed, failed, blocked, notRun } = item.resultCounts;
68
- const total = item.variants.length;
69
- const map: Record<ItemResult, string> = {
70
- passed: `${passed}/${total} Passed`,
71
- failed: `${passed}/${total} Passed · ${failed} Failed`,
72
- blocked: `${blocked} Blocked`,
73
- partial: `${passed}/${total} Passed · ${notRun} not run`,
74
- not_run: 'Pending',
75
- };
76
- return map[item.result];
77
- }
78
-
79
- function variantResultLabel(v: CoverageVariant): string {
80
- if (!v.result) return 'Pending';
81
- return statusToTestResult(v.result.status);
74
+ const { passed, failed, blocked, na } = item.resultCounts;
75
+ const eff = item.variants.length - na;
76
+ const suffix = na > 0 ? ` · ${na} N/A` : '';
77
+ if (item.result === 'na') return 'All N/A';
78
+ if (item.result === 'failed') return `${passed}/${eff} Passed · ${failed} Failed${suffix}`;
79
+ if (item.result === 'blocked') return `${passed}/${eff} Passed · ${blocked} Blocked${suffix}`;
80
+ return `${passed}/${eff} Passed${suffix}`;
82
81
  }
83
82
 
84
83
  /** ISO date (2026-08-04) — unambiguous across locales (review §9). */
@@ -89,10 +88,24 @@ function isoDate(startTime: string | undefined): string {
89
88
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
90
89
  }
91
90
 
92
- function numbered(lines: string[]): string {
91
+ function numbered(lines: string[], start = 1): string {
93
92
  if (lines.length === 0) return '';
94
- if (lines.length === 1) return lines[0];
95
- return lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
93
+ if (lines.length === 1 && start === 1) return lines[0];
94
+ return lines.map((l, i) => `${start + i}. ${l}`).join('\n');
95
+ }
96
+
97
+ /**
98
+ * Compact coverage summary for the parent row. A declared `dimensions:` digest
99
+ * wins ("15 variants — required ×3 · format ×9 …"); otherwise a single variant
100
+ * shows its condition and a group falls back to its variant REFS. Concatenating
101
+ * every variant's full title turned this cell into an 800-character paragraph.
102
+ */
103
+ export function coverageSummary(item: DeliveryItem): string {
104
+ const n = item.variants.length;
105
+ const head = `${n} variant${n === 1 ? '' : 's'}:`;
106
+ if (item.dimensions) return `${head} ${item.dimensions}`;
107
+ if (n === 1) return `${head} ${item.variants[0].condition}`;
108
+ return `${head} ${item.variants.map((v) => v.ref).join(' · ')}`;
96
109
  }
97
110
 
98
111
  /** Expected cell = observable outcomes; the HOW moves under a separate heading. */
@@ -154,11 +167,16 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
154
167
  // Summary band (row 6) — items vs variants stay separate metrics by design (I8).
155
168
  // The execution counters are LIVE formulas over the variant Result cells, so
156
169
  // 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.
159
- const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
160
- const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
161
- const notRun = model.items.reduce((a, i) => a + i.resultCounts.notRun + i.resultCounts.blocked, 0);
170
+ // exact state words and parent cells NEVER do (itemResultLabel + the roll-up
171
+ // formula always compose a "n/m Passed …" label), so exact-match COUNTIF over
172
+ // the whole column counts variants only.
173
+ const sum = (k: 'passed' | 'failed' | 'blocked' | 'notRun' | 'na'): number =>
174
+ model.items.reduce((a, i) => a + i.resultCounts[k], 0);
175
+ const passed = sum('passed');
176
+ const failed = sum('failed');
177
+ const blocked = sum('blocked');
178
+ const notRun = sum('notRun');
179
+ const na = sum('na');
162
180
  const RANGE = 'M9:M10000';
163
181
  const band = ws.getRow(6);
164
182
  const bandCells: Array<[string, ExcelJS.CellValue]> = [
@@ -166,9 +184,9 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
166
184
  ['variants', `Coverage variants: ${model.manifest.variantCount}`],
167
185
  ['passed', { formula: `"Passed: "&COUNTIF(${RANGE},"Passed")`, result: `Passed: ${passed}` } as ExcelJS.CellValue],
168
186
  ['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],
187
+ ['blocked', { formula: `"Blocked: "&COUNTIF(${RANGE},"Blocked")`, result: `Blocked: ${blocked}` } as ExcelJS.CellValue],
170
188
  ['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],
189
+ ['na', { formula: `"N/A: "&COUNTIF(${RANGE},"N/A")`, result: `N/A: ${na}` } as ExcelJS.CellValue],
172
190
  ];
173
191
  bandCells.forEach(([, v], i) => {
174
192
  const c = band.getCell(2 + i);
@@ -192,25 +210,23 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
192
210
  // item lives ONCE on the parent — a child repeats only what distinguishes it.
193
211
  // The child's Expected Result is always kept: it is the precise source
194
212
  // 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;
198
213
  for (const v of item.variants) {
199
214
  const vr = ws.getRow(rowIdx++);
200
215
  vr.outlineLevel = 1;
201
- const preDelta = v.precondition.filter((line) => !commonPre.has(line));
202
216
  const vValues: ExcelJS.CellValue[] = [
203
217
  v.ref,
204
218
  '',
205
219
  v.condition,
206
220
  '', '',
207
- numbered(preDelta),
221
+ numbered(item.preconditionDeltas[v.ref] ?? []),
208
222
  numbered(v.data),
209
- triggerIsCommon ? '' : numbered(v.trigger),
223
+ // Only the steps after the parent's shared prefix, numbered so the
224
+ // parent's steps + these read as one continuous procedure.
225
+ numbered(item.triggerDeltas[v.ref] ?? [], item.trigger.length + 1),
210
226
  expectedWithVerification(v.oracle, v.verification),
211
227
  v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
212
228
  v.traces.join(', '),
213
- variantResultLabel(v),
229
+ variantState(v),
214
230
  isoDate(v.result?.startTime),
215
231
  '',
216
232
  v.result?.error ? String(v.result.error).slice(0, 200) : '',
@@ -227,8 +243,7 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
227
243
  // --- Parent row: only values that are genuinely COMMON to the whole item
228
244
  // (review M-04); everything variant-specific stays on the sub-rows.
229
245
  const pr = ws.getRow(parentRowIdx);
230
- const commonTrigger = triggerIsCommon ? item.variants[0].trigger : null;
231
- const coverage = `${item.variants.length} variant(s): ${item.variants.map((v) => v.condition).join(' · ')}`;
246
+ const coverage = coverageSummary(item);
232
247
 
233
248
  // Live roll-up derived from the child Result cells — precedence
234
249
  // failed → blocked → pending → partial → passed. N/A variants leave the
@@ -236,12 +251,13 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
236
251
  const rng = `M${firstChild}:M${lastChild}`;
237
252
  const n = item.variants.length;
238
253
  const EFF = `(${n}-COUNTIF(${rng},"N/A"))`;
254
+ const NA = `IF(COUNTIF(${rng},"N/A")>0," · "&COUNTIF(${rng},"N/A")&" N/A","")`;
255
+ // Mirrors itemResultLabel exactly — and never yields a bare state word.
239
256
  const rollUpFormula =
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")))))`;
257
+ `IF(COUNTIF(${rng},"Failed")>0,COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · "&COUNTIF(${rng},"Failed")&" Failed"&${NA},` +
258
+ `IF(COUNTIF(${rng},"Blocked")>0,COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · "&COUNTIF(${rng},"Blocked")&" Blocked"&${NA},` +
259
+ `IF(${EFF}=0,"All N/A",` +
260
+ `COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed"&${NA})))`;
245
261
 
246
262
  const values: ExcelJS.CellValue[] = [
247
263
  item.id,
@@ -251,8 +267,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
251
267
  item.priority,
252
268
  numbered(item.precondition),
253
269
  coverage,
254
- // No placeholder text: when triggers differ the sub-rows carry them (GAP-01).
255
- commonTrigger ? numbered(commonTrigger) : '',
270
+ // The shared leading steps; each variant's remaining steps sit on its sub-row.
271
+ numbered(item.trigger),
256
272
  item.oracle,
257
273
  itemModeLabel(item),
258
274
  item.traces.join(', '),
@@ -29,6 +29,10 @@ export interface MapGroup {
29
29
  oracle: string;
30
30
  category: MapCategory;
31
31
  review: ReviewState;
32
+ /** Optional short digest of the coverage dimensions this item varies over
33
+ * (e.g. "required ×3 · format ×9 · full-width ×2"). Rendered on the parent row
34
+ * instead of concatenating every variant's title — the compact review view. */
35
+ dimensions?: string;
32
36
  /** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
33
37
  variants: string[];
34
38
  }
@@ -118,7 +122,11 @@ export interface CoverageVariant {
118
122
  result?: PlaywrightResult;
119
123
  }
120
124
 
121
- export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run';
125
+ export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run' | 'na';
126
+
127
+ /** The exact word rendered in a variant's Result cell — the single vocabulary the
128
+ * parent's roll-up formula counts (COUNTIF is exact-match). */
129
+ export type VariantState = 'Passed' | 'Failed' | 'Blocked' | 'Pending' | 'N/A';
122
130
 
123
131
  /** One summarized row in the matrix — a group of variants sharing the signature. */
124
132
  export interface DeliveryItem {
@@ -126,6 +134,8 @@ export interface DeliveryItem {
126
134
  target: string;
127
135
  intent: string;
128
136
  oracle: string;
137
+ /** Short coverage-dimension digest from the map (compact parent view). */
138
+ dimensions?: string;
129
139
  category: MapCategory;
130
140
  review: ReviewState;
131
141
  /** Highest priority among the variants (per-variant priorities stay on the sub-rows). */
@@ -136,13 +146,19 @@ export interface DeliveryItem {
136
146
  layers: MatrixLayer[];
137
147
  /** Union of variant traces (exact per-variant traces stay on the variants). */
138
148
  traces: string[];
149
+ /** Preconditions shared by EVERY variant (the intersection) — written once here. */
139
150
  precondition: string[];
140
- /** Common trigger when all variants share one shape, else '(per variant)'. */
151
+ /** Leading trigger steps shared by every variant written once here; each
152
+ * variant renders only its remaining steps (`triggerDeltas`). */
141
153
  trigger: string[];
142
154
  variants: CoverageVariant[];
155
+ /** variant ref → its precondition lines that are not on the parent. */
156
+ preconditionDeltas: Record<string, string[]>;
157
+ /** variant ref → its trigger steps after the shared prefix. */
158
+ triggerDeltas: Record<string, string[]>;
143
159
  /** Derived roll-up — never entered independently (Gate F). */
144
160
  result: ItemResult;
145
- resultCounts: { passed: number; failed: number; blocked: number; notRun: number };
161
+ resultCounts: { passed: number; failed: number; blocked: number; notRun: number; na: number };
146
162
  }
147
163
 
148
164
  export interface MatrixDisposition {
@@ -60,6 +60,7 @@ groups:
60
60
  target: login.email # ONE target: field/component dot-path, flow phrase, or METHOD /path
61
61
  intent: <one behavior/rule this item verifies>
62
62
  oracle: <the shared observable Pass/Fail statement>
63
+ dimensions: violated rule — required ×3 · format ×10 · full-width ×2 # see below
63
64
  category: normal | abnormal | security | nfr
64
65
  review: proposed # ALWAYS proposed — only QA approval flips it
65
66
  variants: [VP-VAL-001-B, VP-VAL-001-S] # VP-ids; bare id on a @cases scenario = all its rows
@@ -94,6 +95,17 @@ determined** (its oracle *family*, not its exact message).
94
95
  disposition). Data-setup blocks (`@manual:data-setup`) → `excluded`; SPEC-GAP placeholders →
95
96
  `blocked`.
96
97
 
98
+ **`dimensions:` — the compact coverage digest (required for items with >3 variants).**
99
+ This one short line is what the collapsed parent row shows instead of listing every variant, so a
100
+ reviewer sees *which dimensions* the item covers without expanding it. Name the dimension, then the
101
+ branches with counts:
102
+ - `violated rule — required ×3 · format ×10 · full-width ×2`
103
+ - `account state — wrong password · unregistered · locked · soft-deleted`
104
+ - `submission method — Login button · Enter in Password · Enter in Email`
105
+
106
+ Keep it ≤120 chars (Gate W warns). **YAML caveat:** a bare `: ` inside the value breaks the parse —
107
+ use ` — ` as the label separator (as above) or quote the whole string.
108
+
97
109
  **Wording rules for `intent`/`oracle` (customer-facing — Gate W lints these):**
98
110
  - Plain product language, present simple, ~10–20 words, one behavior:
99
111
  "A user can sign in with valid credentials and is redirected to the Jobs page."
@@ -63,12 +63,18 @@ result/evidence entry). Sub-rows are **delta-only**: knowledge common to the who
63
63
  execution fields. The parent never carries placeholder text — when triggers differ the cell is
64
64
  simply empty and the sub-rows carry them.
65
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.
66
+ Parent preconditions are the **intersection** of the variants' and the parent's Action holds the
67
+ **shared leading steps**; each variant renders only its remaining steps, numbered to continue the
68
+ parent's list so a shared prefix is written once, not repeated on every child. The parent's
69
+ Coverage cell is the map's short `dimensions:` digest (or the variant refs when none is declared),
70
+ never a concatenation of variant titles.
71
+
72
+ Variant Result cells hold exactly one of `Passed`/`Failed`/`Blocked`/`Pending`/`N/A` (dropdown) —
73
+ that vocabulary is the contract: the parent Result and the summary band are **live Excel formulas**
74
+ counting those words, so a parent label is always a composed `n/m Passed …` string (`All N/A` when
75
+ every variant is N/A) and never a bare state word. **`N/A` leaves the denominator.** Evidence and
76
+ Defect ID are separate columns; ID + Target frozen; ISO dates; landscape print with repeated
77
+ header rows.
72
78
 
73
79
  `Coverage` sheet — requirement coverage table (every FR/TR/NFR id with an explicit status),
74
80
  target × category grid with explicit `—` gaps, dispositions, manifest. CSV mirrors the same model