@sun-asterisk/sungen 3.2.16-beta.1 → 3.2.16-beta.3
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 +1 -0
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +8 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +166 -24
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +2 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +7 -0
- 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 +40 -2
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
- package/dist/exporters/matrix/map-loader.js +18 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts +3 -2
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +49 -29
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts +18 -8
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +125 -52
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +29 -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 +45 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -0
- package/dist/exporters/matrix/wording.js +150 -0
- package/dist/exporters/matrix/wording.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +45 -9
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +36 -11
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +1 -0
- package/src/exporters/matrix/build.ts +168 -24
- package/src/exporters/matrix/export.ts +10 -0
- package/src/exporters/matrix/gates.ts +44 -2
- package/src/exporters/matrix/map-loader.ts +20 -1
- package/src/exporters/matrix/render-csv.ts +50 -30
- package/src/exporters/matrix/render-xlsx.ts +131 -55
- package/src/exporters/matrix/types.ts +33 -4
- package/src/exporters/matrix/wording.ts +157 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +45 -9
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +36 -11
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Wording normalization — deterministic rendering step AFTER semantic
|
|
4
|
+
* normalization (review feedback §10): turn sungen DSL steps into controlled
|
|
5
|
+
* manual-test English without changing the target, condition, trigger,
|
|
6
|
+
* precondition, oracle, or trace.
|
|
7
|
+
*
|
|
8
|
+
* - Actions render in the imperative: "User fill [Email] field with X"
|
|
9
|
+
* → "Enter X in the Email field."
|
|
10
|
+
* - Expected results render as observable assertions (never tester actions):
|
|
11
|
+
* "User see [Jobs] page" → "The Jobs page is displayed."
|
|
12
|
+
* - Manual `# Tester verifies:` labels (Setup:/Action:/Observable:/Oracle:)
|
|
13
|
+
* become structured fields instead of prose: Setup → precondition,
|
|
14
|
+
* Action → action, Observable → expected, Oracle → verification method.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.renderAction = renderAction;
|
|
18
|
+
exports.renderExpected = renderExpected;
|
|
19
|
+
exports.renderPrecondition = renderPrecondition;
|
|
20
|
+
exports.classifyManualComments = classifyManualComments;
|
|
21
|
+
// `[Email] field` → `Email field` (the visible label + its element type).
|
|
22
|
+
function deRef(text) {
|
|
23
|
+
return text.replace(/\[([^\]]+)\]/g, '$1');
|
|
24
|
+
}
|
|
25
|
+
function sentence(text) {
|
|
26
|
+
let s = text.trim().replace(/\s+/g, ' ');
|
|
27
|
+
if (!s)
|
|
28
|
+
return s;
|
|
29
|
+
s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
30
|
+
if (!/[.!?…]$/.test(s))
|
|
31
|
+
s += '.';
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Render one action step in the imperative. Pattern table covers the common
|
|
36
|
+
* sungen step verbs; anything unmatched falls back to actor-stripped text —
|
|
37
|
+
* still readable, never a raw `User fill`.
|
|
38
|
+
*/
|
|
39
|
+
function renderAction(raw) {
|
|
40
|
+
let s = raw.trim().replace(/^(User|The user)\s+/i, '');
|
|
41
|
+
const rules = [
|
|
42
|
+
// fill [X] field with V
|
|
43
|
+
[/^fills? \[([^\]]+)\][a-z ]* with (.+)$/i, (m) => `Enter ${m[2]} in the ${m[1]} field`],
|
|
44
|
+
// clear [X] field
|
|
45
|
+
[/^clears? \[([^\]]+)\](.*)$/i, (m) => `Clear the ${m[1]}${m[2] || ' field'}`],
|
|
46
|
+
// click [X] <type>
|
|
47
|
+
[/^clicks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Click the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
|
|
48
|
+
// press <Key> at/in [X] field
|
|
49
|
+
[/^press(?:es)? (.+?) (?:at|in|inside) \[([^\]]+)\](?: field)?$/i, (m) => `Press ${m[1]} in the ${m[2]} field`],
|
|
50
|
+
[/^press(?:es)? (.+)$/i, (m) => `Press ${m[1]}`],
|
|
51
|
+
// select V in/from [X] dropdown
|
|
52
|
+
[/^selects? (.+?) (?:in|from) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Select ${m[1]} in the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
|
|
53
|
+
// check/uncheck [X] checkbox
|
|
54
|
+
[/^(un)?checks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `${m[1] ? 'Uncheck' : 'Check'} the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
|
|
55
|
+
// hover [X]
|
|
56
|
+
[/^hovers? (?:over )?\[([^\]]+)\]\s*(\w+)?$/i, (m) => `Hover over the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
|
|
57
|
+
// upload V to [X]
|
|
58
|
+
[/^uploads? (.+?) (?:to|into) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Upload ${m[1]} to the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
|
|
59
|
+
// is on [X] page (as an action = navigate)
|
|
60
|
+
[/^is on \[([^\]]+)\] page(.*)$/i, (m) => `Open the ${m[1]} page${m[2] ?? ''}`],
|
|
61
|
+
// wait for [X] <type> (is )?visible
|
|
62
|
+
[/^waits? for \[([^\]]+)\]\s*(\w+)?(?: is)?(?: visible)?$/i, (m) => `Wait until the ${m[1]}${m[2] ? ` ${m[2]}` : ''} is visible`],
|
|
63
|
+
// scroll to [X]
|
|
64
|
+
[/^scrolls? (?:to|into) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Scroll to the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
|
|
65
|
+
];
|
|
66
|
+
for (const [re, out] of rules) {
|
|
67
|
+
const m = s.match(re);
|
|
68
|
+
if (m)
|
|
69
|
+
return sentence(deRef(out(m)));
|
|
70
|
+
}
|
|
71
|
+
return sentence(deRef(s));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render one expected step as an observable assertion (no tester action, no
|
|
75
|
+
* `should`, no DSL `User see`).
|
|
76
|
+
*/
|
|
77
|
+
function renderExpected(raw) {
|
|
78
|
+
let s = raw.trim().replace(/^(User|The user)\s+/i, '');
|
|
79
|
+
const rules = [
|
|
80
|
+
// see [X] page
|
|
81
|
+
[/^sees? \[([^\]]+)\] page$/i, (m) => `The ${m[1]} page is displayed`],
|
|
82
|
+
// see [X] <type> with V
|
|
83
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? with (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} shows ${m[3]}`],
|
|
84
|
+
// see [X] <type> contains V
|
|
85
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? contains (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} contains ${m[3]}`],
|
|
86
|
+
// see [X] <type> has text V
|
|
87
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? has text (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} shows ${m[3]}`],
|
|
88
|
+
// see [X] <type> is hidden / is disabled / is enabled / …
|
|
89
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? is (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is ${m[3]}`],
|
|
90
|
+
// not see [X] <type>
|
|
91
|
+
[/^(?:do(?:es)? )?not sees? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is not displayed`],
|
|
92
|
+
// see [X] <type>
|
|
93
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is visible`],
|
|
94
|
+
];
|
|
95
|
+
for (const [re, out] of rules) {
|
|
96
|
+
const m = s.match(re);
|
|
97
|
+
if (m)
|
|
98
|
+
return sentence(deRef(out(m)));
|
|
99
|
+
}
|
|
100
|
+
return sentence(deRef(s));
|
|
101
|
+
}
|
|
102
|
+
/** Precondition wording: a state, not an action ("The user is signed out."). */
|
|
103
|
+
function renderPrecondition(raw) {
|
|
104
|
+
const s = raw.trim().replace(/^(User|The user)\s+/i, '');
|
|
105
|
+
const m = s.match(/^is on \[([^\]]+)\] page(.*)$/i);
|
|
106
|
+
if (m)
|
|
107
|
+
return sentence(`The user is on the ${m[1]} page${m[2] ?? ''}`);
|
|
108
|
+
return sentence(deRef(`The user ${s.charAt(0).toLowerCase()}${s.slice(1)}`));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Split a manual scenario's numbered comment lines into the four structured
|
|
112
|
+
* fields. Labels are consumed (structured), never left inside the prose —
|
|
113
|
+
* review feedback §10.2(3). Continuation lines append to the previous item;
|
|
114
|
+
* pre-amble (rationale/header/dividers) is skipped.
|
|
115
|
+
*/
|
|
116
|
+
function classifyManualComments(comments) {
|
|
117
|
+
const out = { preconditions: [], actions: [], expected: [], verification: [] };
|
|
118
|
+
let last = null;
|
|
119
|
+
const bucketOf = (label) => {
|
|
120
|
+
if (/setup|precondition|arrange|given/i.test(label))
|
|
121
|
+
return 'preconditions';
|
|
122
|
+
if (/oracle|verify|verification|how to check/i.test(label))
|
|
123
|
+
return 'verification';
|
|
124
|
+
if (/observ|expect|result|then|assert/i.test(label))
|
|
125
|
+
return 'expected';
|
|
126
|
+
return 'actions';
|
|
127
|
+
};
|
|
128
|
+
for (const raw of comments) {
|
|
129
|
+
const line = raw.trim();
|
|
130
|
+
if (!line)
|
|
131
|
+
continue;
|
|
132
|
+
if (/^[-=*_]{2,}/.test(line)) {
|
|
133
|
+
last = null;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const m = line.match(/^\d+[.)]\s*(?:([A-Za-z][A-Za-z /]*?):\s*)?(.+)$/);
|
|
137
|
+
if (m) {
|
|
138
|
+
const label = (m[1] || '').trim();
|
|
139
|
+
const text = m[2].trim();
|
|
140
|
+
const list = out[bucketOf(label)];
|
|
141
|
+
list.push(text);
|
|
142
|
+
last = { list, idx: list.length - 1 };
|
|
143
|
+
}
|
|
144
|
+
else if (last) {
|
|
145
|
+
last.list[last.idx] += ' ' + line;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=wording.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wording.js","sourceRoot":"","sources":["../../../src/exporters/matrix/wording.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AAoBH,oCAkCC;AAMD,wCAyBC;AAGD,gDAKC;AAuBD,wDA2BC;AA7ID,0EAA0E;AAC1E,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACjB,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,CAAC,IAAI,GAAG,CAAC;IACjC,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,GAAW;IACtC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;IAEvD,MAAM,KAAK,GAAqD;QAC9D,wBAAwB;QACxB,CAAC,yCAAyC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACxF,kBAAkB;QAClB,CAAC,6BAA6B,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC;QAC9E,mBAAmB;QACnB,CAAC,kCAAkC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACzF,8BAA8B;QAC9B,CAAC,gEAAgE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC/G,CAAC,sBAAsB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,gCAAgC;QAChC,CAAC,qDAAqD,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACxH,6BAA6B;QAC7B,CAAC,uCAAuC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACtH,YAAY;QACZ,CAAC,4CAA4C,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACxG,kBAAkB;QAClB,CAAC,qDAAqD,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACxH,2CAA2C;QAC3C,CAAC,gCAAgC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/E,oCAAoC;QACpC,CAAC,0DAA0D,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC;QACjI,gBAAgB;QAChB,CAAC,+CAA+C,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;KAC3G,CAAC;IAEF,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtB,IAAI,CAAC;YAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;IAEvD,MAAM,KAAK,GAAqD;QAC9D,eAAe;QACf,CAAC,4BAA4B,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC;QACtE,wBAAwB;QACxB,CAAC,0CAA0C,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzG,4BAA4B;QAC5B,CAAC,8CAA8C,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChH,4BAA4B;QAC5B,CAAC,8CAA8C,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7G,0DAA0D;QAC1D,CAAC,wCAAwC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpG,qBAAqB;QACrB,CAAC,mDAAmD,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,mBAAmB,CAAC;QACrH,iBAAiB;QACjB,CAAC,gCAAgC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC;KAC7F,CAAC;IAEF,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtB,IAAI,CAAC;YAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED,gFAAgF;AAChF,SAAgB,kBAAkB,CAAC,GAAW;IAC5C,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACvE,OAAO,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/E,CAAC;AAiBD;;;;;GAKG;AACH,SAAgB,sBAAsB,CAAC,QAAkB;IACvD,MAAM,GAAG,GAAoB,EAAE,aAAa,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAChG,IAAI,IAAI,GAA2C,IAAI,CAAC;IAExD,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAyB,EAAE;QACxD,IAAI,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,eAAe,CAAC;QAC5E,IAAI,0CAA0C,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,cAAc,CAAC;QAClF,IAAI,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;QACvE,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAAC,IAAI,GAAG,IAAI,CAAC;YAAC,SAAS;QAAC,CAAC;QACxD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACxE,IAAI,CAAC,EAAE,CAAC;YACN,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChB,IAAI,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxC,CAAC;aAAM,IAAI,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,IAAI,CAAC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -66,19 +66,55 @@ dispositions: # scenarios intentionally NOT delivered as te
|
|
|
66
66
|
# as: excluded | blocked | covered_elsewhere | accepted_risk
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
**Grouping rules (the aggregation signature)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
**Grouping rules (the aggregation signature) — group COMPACTLY.** The matrix exists to be
|
|
70
|
+
substantially shorter than the scenario list, so a reviewer can see missing viewpoints at a
|
|
71
|
+
glance. Merge whenever the cases share ALL of: target · test intent/business rule ·
|
|
72
|
+
precondition/condition · trigger or procedure shape · **the way the expected result is
|
|
73
|
+
determined** (its oracle *family*, not its exact message).
|
|
74
|
+
|
|
75
|
+
- **Oracle family = the determination method, parameterized.** All validation branches of ONE
|
|
76
|
+
field belong to ONE item — required, format, length, character-class are *expected branches*
|
|
77
|
+
(parameters) of "the field shows the validation message defined for the violated rule", shown
|
|
78
|
+
per variant, never separate items.
|
|
79
|
+
- MAY vary inside one item (coverage dimensions, visible on the sub-rows): data values, boundary
|
|
80
|
+
points, **account states** (a seeded/locked/deleted account next to a wrong-password case),
|
|
81
|
+
provider/browser/locale, `@cases` rows, a different trigger with the same oracle (blur vs
|
|
82
|
+
submit), **execution mode** (auto + manual mix — the parent shows `Auto n · Manual m`), and
|
|
83
|
+
**priority** (the item takes the highest; per-variant priorities stay visible).
|
|
84
|
+
- MUST split: different target, different intent/business rule, different way of determining the
|
|
85
|
+
expected result (a field-error family ≠ a session-established family), different test layer
|
|
86
|
+
(`@api`/`@query`), materially different precondition, or a different procedure shape —
|
|
87
|
+
sequence-sensitive flows (re-Given/When after a Then) stay solo. **Different risk classes never
|
|
88
|
+
merge**: XSS and SQL injection are separate items (different risk and determination), even on
|
|
89
|
+
the same field.
|
|
90
|
+
- When unsure, keep items separate — the gates and QA decide, never guess-merge.
|
|
78
91
|
- Every scenario must land in exactly one group **or** one disposition (Gate B enforces 100%
|
|
79
92
|
disposition). Data-setup blocks (`@manual:data-setup`) → `excluded`; SPEC-GAP placeholders →
|
|
80
93
|
`blocked`.
|
|
81
94
|
|
|
95
|
+
**Wording rules for `intent`/`oracle` (customer-facing — Gate W lints these):**
|
|
96
|
+
- Plain product language, present simple, ~10–20 words, one behavior:
|
|
97
|
+
"A user can sign in with valid credentials and is redirected to the Jobs page."
|
|
98
|
+
- Oracle = the observable outcome as a definite assertion ("The Jobs page is displayed and the
|
|
99
|
+
Logout link is visible.") — no `should`, no tester actions.
|
|
100
|
+
- NEVER: `{{tokens}}`, `[Selector]` references, DSL phrasing (`User fill/click/see`), generator
|
|
101
|
+
labels (`Setup:`/`Observable:`/`Oracle:`), or vague verbs (`handles`, `surfaces`) when a precise
|
|
102
|
+
behavior exists. Use the visible UI label (the Login button, the Email field).
|
|
103
|
+
- **Preserve the source meaning exactly** — never strengthen, weaken, or reinterpret an oracle
|
|
104
|
+
(a security assertion especially: if the source says "the password appears ONLY in the HTTPS
|
|
105
|
+
POST body", do not write "no plaintext password on the network").
|
|
106
|
+
|
|
107
|
+
**Requirement coverage (`requirements:` section, optional):** `sungen delivery` scans
|
|
108
|
+
`requirements/spec.md` for FR-/TR-/NFR- ids; ids traced by `@spec:` tags are `covered`, the rest
|
|
109
|
+
are `gap` (Gate R warning). Record the reviewed status for genuine non-gaps:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
requirements:
|
|
113
|
+
TR-007: { status: planned, note: Performance needs Lighthouse-style tooling }
|
|
114
|
+
TR-004: { status: partially_covered, note: client-side covered by VP-SEC-003; hashing needs DB verify }
|
|
115
|
+
# status: covered | partially_covered | covered_elsewhere | planned | gap | not_applicable
|
|
116
|
+
```
|
|
117
|
+
|
|
82
118
|
Then validate and fix any ERROR findings:
|
|
83
119
|
|
|
84
120
|
```bash
|
|
@@ -29,19 +29,44 @@ 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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+
|
|
37
|
+
data · trigger · oracle all renderable; every `{{var}}` resolves; **no template token may survive
|
|
38
|
+
into a rendered cell** — test-data cross-references are resolved for display) · E drift
|
|
39
|
+
(fingerprint mismatch → back to review) · G review state (proposed groups block the official
|
|
40
|
+
render; `--preview` renders a DRAFT watermark) · R requirement coverage (spec FR/TR/NFR ids with
|
|
41
|
+
no trace and no `requirements:` status → warning) · W wording lint (map intent/oracle containing
|
|
42
|
+
tokens, `[Selector]` refs, DSL phrasing, or generator labels → warning).
|
|
43
|
+
|
|
44
|
+
**Wording normalization (deterministic, after semantic normalization):** DSL steps render as
|
|
45
|
+
controlled manual-test English without changing meaning — actions in the imperative (`User fill
|
|
46
|
+
[Email] field with X` → `Enter X in the Email field.`), expected results as observable assertions
|
|
47
|
+
(`User see [Jobs] page` → `The Jobs page is displayed.`), preconditions as states (`The user is
|
|
48
|
+
signed out.`). Manual `# Tester verifies:` labels become structured fields: `Setup:` →
|
|
49
|
+
Precondition, `Action:` → Action, `Observable:` → Expected Result, `Oracle:` → a separate
|
|
50
|
+
`Verification method:` line. Sequence-sensitive flows keep event order: actions numbered with
|
|
51
|
+
mid-flow assertions inline as `Verify: …`; only the final Then block is the Expected Result.
|
|
52
|
+
Empty test values render as `(empty)`.
|
|
53
|
+
|
|
54
|
+
**Workbook**: `Testcases` sheet — parent rows + outline-level-1 variant sub-rows for **every**
|
|
55
|
+
item (single-variant included: the sub-row carries the source VP-id, resolved data, and the
|
|
56
|
+
result/evidence entry). Collapse outline for the customer view, expand to execute. Variant Result
|
|
57
|
+
cells have a dropdown (Passed/Failed/Blocked/Pending/N/A) and the parent Result is a **live Excel
|
|
58
|
+
formula** over its children (failed→blocked→pending→partial→passed, e.g. `2/3 Passed · 1 Failed`)
|
|
59
|
+
— a parent can never contradict its variants, even after manual edits. ID + Target columns are
|
|
60
|
+
frozen; dates are ISO (`2026-08-04`). `Coverage` sheet — requirement coverage table (every FR/TR/
|
|
61
|
+
NFR id with an explicit status), target × category grid with explicit `—` gaps, dispositions,
|
|
62
|
+
manifest. CSV is flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
|
|
43
63
|
`delivery_item_count` ≠ progress — variants are the execution metric.
|
|
44
64
|
|
|
65
|
+
**Authoring guidance the matrix rewards** (create-test side): payload/provider matrices (SQLi
|
|
66
|
+
payload lists, OAuth provider sets) belong in `@cases` datasets so each case is an atomic,
|
|
67
|
+
independently-reportable variant; keep dataset `case:` labels short and stable (`CHK-EMAIL-I1`),
|
|
68
|
+
with descriptions in other columns — the label is part of the variant's identity.
|
|
69
|
+
|
|
45
70
|
---
|
|
46
71
|
|
|
47
72
|
## Legacy mode (--legacy / --full)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.16-beta.
|
|
3
|
+
"version": "3.2.16-beta.3",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@babel/types": "^7.28.5",
|
|
40
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
41
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
-
"@sungen/driver-data-factory": "3.2.16-beta.
|
|
43
|
-
"@sungen/driver-ui": "3.2.16-beta.
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.16-beta.3",
|
|
43
|
+
"@sungen/driver-ui": "3.2.16-beta.3",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -621,6 +621,7 @@ function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths
|
|
|
621
621
|
featureFile: path.join(base, 'features', `${target.featureBaseName}.feature`),
|
|
622
622
|
testDataFile: resolveTestDataPathForTarget(cwd, target),
|
|
623
623
|
specFile: path.join(genBase, `${target.featureBaseName}.spec.ts`),
|
|
624
|
+
specMdFile: path.join(base, 'requirements', 'spec.md'),
|
|
624
625
|
resultsPath: resolveResultsPath(cwd, target),
|
|
625
626
|
mapFile: mapFilePath(base, target.featureBaseName),
|
|
626
627
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { FeatureMetadata, PlaywrightResult } from '../types';
|
|
9
|
-
import { MergedScenario
|
|
9
|
+
import { MergedScenario } from '../scenario-merger';
|
|
10
10
|
import {
|
|
11
11
|
extractAuthRole,
|
|
12
12
|
extractPriority,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
splitVpAndName,
|
|
15
15
|
} from '../feature-parser';
|
|
16
16
|
import { getCasesDatasetRows, resolveResultVariants } from '../result-variants';
|
|
17
|
-
import {
|
|
17
|
+
import { classifyManualComments, renderAction, renderExpected, renderPrecondition } from './wording';
|
|
18
18
|
import { scenarioFingerprint, combinedFingerprint, mapContentFingerprint } from './fingerprint';
|
|
19
19
|
import {
|
|
20
20
|
CoverageVariant,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
MatrixDisposition,
|
|
25
25
|
MatrixLayer,
|
|
26
26
|
MatrixModel,
|
|
27
|
+
RequirementCoverage,
|
|
27
28
|
MAX_VARIANTS_PER_ITEM,
|
|
28
29
|
} from './types';
|
|
29
30
|
import { runGates } from './gates';
|
|
@@ -89,6 +90,8 @@ export interface BuildInputs {
|
|
|
89
90
|
results: Map<string, PlaywrightResult> | null;
|
|
90
91
|
map: DeliveryMap;
|
|
91
92
|
transformerVersion: string;
|
|
93
|
+
/** requirements/spec.md content — source of the requirement-id inventory (FR/TR/NFR). */
|
|
94
|
+
specText?: string;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
/**
|
|
@@ -97,7 +100,10 @@ export interface BuildInputs {
|
|
|
97
100
|
* support tooling see the same universe the builder does.
|
|
98
101
|
*/
|
|
99
102
|
export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' | 'testData' | 'results'>): CoverageVariant[] {
|
|
100
|
-
const { merged,
|
|
103
|
+
const { merged, results } = inputs;
|
|
104
|
+
// Test-data values may cross-reference other keys (email_padded: " {{valid_email}} ") —
|
|
105
|
+
// resolve one level so display cells never leak a template token (review B-04).
|
|
106
|
+
const testData = resolveCrossRefs(inputs.testData);
|
|
101
107
|
const variants: CoverageVariant[] = [];
|
|
102
108
|
|
|
103
109
|
for (const m of merged) {
|
|
@@ -107,26 +113,36 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
107
113
|
const mode = extractTestcaseType(tags) === 'Manual' ? 'manual' : 'auto';
|
|
108
114
|
const authRole = extractAuthRole(tags);
|
|
109
115
|
const vpCategory = vpId.replace(/^VP-/, '').replace(/-\d+[a-zA-Z]?$/, '');
|
|
116
|
+
const procedureProfile = deriveProcedureProfile(m);
|
|
110
117
|
|
|
111
|
-
// Manual
|
|
118
|
+
// Manual scenarios: the `# Tester verifies:` block is classified into structured
|
|
119
|
+
// fields — Setup → precondition (review B-02), Action → trigger, Observable →
|
|
120
|
+
// expected, Oracle → verification method. Labels never remain inside prose.
|
|
112
121
|
const manual = mode === 'manual' && m.feature.comments?.length
|
|
113
|
-
?
|
|
122
|
+
? classifyManualComments(m.feature.comments)
|
|
114
123
|
: null;
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
const rawTrigger = manual ? manual.
|
|
118
|
-
const
|
|
119
|
-
|
|
124
|
+
|
|
125
|
+
// Raw (pre-wording) step texts — these feed shapes + Gate C, never the cells.
|
|
126
|
+
const rawTrigger = manual ? manual.actions : m.feature.rawWhenSteps;
|
|
127
|
+
const rawOracle = manual
|
|
128
|
+
? (manual.expected.length > 0 ? manual.expected : m.feature.rawThenSteps)
|
|
129
|
+
: m.resolvedExpected.filter((s) => s.bucket === 'then').map((s) => s.text);
|
|
120
130
|
|
|
121
131
|
const preconditionProfile = [
|
|
122
132
|
authRole ?? '-',
|
|
123
133
|
m.feature.extendsName ?? '-',
|
|
124
134
|
...m.feature.rawGivenSteps.map(normalizeShape),
|
|
135
|
+
...(manual?.preconditions ?? []).map(normalizeShape),
|
|
125
136
|
].join(' | ');
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
// Display precondition = auth state + Background Given (shared start state) +
|
|
138
|
+
// the scenario's own Given steps + manual Setup lines — deduplicated (a manual
|
|
139
|
+
// scenario often repeats the Background navigation as its own Given).
|
|
140
|
+
const precondition = Array.from(new Set([
|
|
141
|
+
...(authRole ? [authRole === 'no-auth' ? 'The user is signed out.' : `The user is signed in as ${authRole}.`] : []),
|
|
142
|
+
...inputs.feature.backgroundGivenSteps.map(renderPrecondition),
|
|
143
|
+
...m.feature.rawGivenSteps.map(renderPrecondition),
|
|
144
|
+
...(manual?.preconditions ?? []).map((t) => renderPrecondition(t)),
|
|
145
|
+
]));
|
|
130
146
|
|
|
131
147
|
const base = {
|
|
132
148
|
vpId,
|
|
@@ -136,11 +152,46 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
136
152
|
manualReason: manualReason(tags),
|
|
137
153
|
layers: deriveLayers(tags),
|
|
138
154
|
traces: tags.filter((t) => t.startsWith('@spec:')).map((t) => t.slice('@spec:'.length)),
|
|
139
|
-
triggerShape:
|
|
140
|
-
oracleShape:
|
|
155
|
+
triggerShape: rawTrigger.map(normalizeShape),
|
|
156
|
+
oracleShape: rawOracle.map(normalizeShape),
|
|
141
157
|
preconditionProfile,
|
|
142
158
|
precondition,
|
|
143
|
-
procedureProfile
|
|
159
|
+
procedureProfile,
|
|
160
|
+
verification: manual?.verification.map((t) => sentenceOf(t)) ?? [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** Render the display cells for one data context (vars resolved first). */
|
|
164
|
+
const renderCells = (vars: Record<string, string>): { trigger: string[]; oracle: string[]; precondition: string[]; verification: string[] } => {
|
|
165
|
+
const sub = (s: string): string => substituteDisplayVars(s, vars);
|
|
166
|
+
const preconditionOut = precondition.map(sub);
|
|
167
|
+
const verificationOut = base.verification.map(sub);
|
|
168
|
+
if (procedureProfile === 'sequence' && !manual) {
|
|
169
|
+
// Ordered flow (review B-03): keep the event order — actions stay numbered in
|
|
170
|
+
// the trigger, mid-flow assertions render inline as "Verify:", and only the
|
|
171
|
+
// FINAL Then block becomes the expected result.
|
|
172
|
+
const steps = m.feature.orderedSteps;
|
|
173
|
+
let lastActionIdx = -1;
|
|
174
|
+
steps.forEach((s, i) => { if (s.bucket !== 'then') lastActionIdx = i; });
|
|
175
|
+
const trigger: string[] = [];
|
|
176
|
+
const oracle: string[] = [];
|
|
177
|
+
steps.forEach((s, i) => {
|
|
178
|
+
if (i > lastActionIdx) oracle.push(renderExpected(sub(s.text))); // trailing Then block
|
|
179
|
+
else if (s.bucket === 'then') trigger.push(`Verify: ${renderExpected(sub(s.text))}`);
|
|
180
|
+
else if (s.bucket === 'when') trigger.push(renderAction(sub(s.text)));
|
|
181
|
+
// leading given steps already live in the precondition
|
|
182
|
+
});
|
|
183
|
+
return { trigger, oracle, precondition: preconditionOut, verification: verificationOut };
|
|
184
|
+
}
|
|
185
|
+
const trigger = rawTrigger.map((s) => renderAction(sub(s)));
|
|
186
|
+
const oracle = rawOracle.map((s) => renderExpected(sub(s)));
|
|
187
|
+
return {
|
|
188
|
+
// A pure-render check (Given + Then only) is checked on page load — say so
|
|
189
|
+
// in words instead of a placeholder token (review §10.3).
|
|
190
|
+
trigger: trigger.length > 0 ? trigger : ['No action — the state is checked on page load.'],
|
|
191
|
+
oracle,
|
|
192
|
+
precondition: preconditionOut,
|
|
193
|
+
verification: verificationOut,
|
|
194
|
+
};
|
|
144
195
|
};
|
|
145
196
|
|
|
146
197
|
const rows = getCasesDatasetRows(m, testData ?? undefined);
|
|
@@ -150,6 +201,7 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
150
201
|
const resultByLabel = new Map(resultVariants.map((rv) => [rv.nameSuffix.replace(/^ — /, ''), rv.result]));
|
|
151
202
|
rows.forEach((row, i) => {
|
|
152
203
|
const label = String(row.case ?? row.name ?? row.label ?? `row ${i + 1}`);
|
|
204
|
+
const cells = renderCells({ ...(testData ?? {}), ...rowAsStrings(row) });
|
|
153
205
|
variants.push({
|
|
154
206
|
...base,
|
|
155
207
|
ref: `${vpId}#${label}`,
|
|
@@ -157,21 +209,26 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
157
209
|
title: `${category1} — ${label}`,
|
|
158
210
|
condition: label,
|
|
159
211
|
data: resolveDataPairs(m.feature.referencedVars, testData, row),
|
|
160
|
-
trigger:
|
|
161
|
-
oracle:
|
|
212
|
+
trigger: cells.trigger,
|
|
213
|
+
oracle: cells.oracle,
|
|
214
|
+
precondition: cells.precondition,
|
|
215
|
+
verification: cells.verification,
|
|
162
216
|
fingerprint: scenarioFingerprint(m.feature, row),
|
|
163
217
|
result: resultByLabel.get(label),
|
|
164
218
|
});
|
|
165
219
|
});
|
|
166
220
|
} else {
|
|
221
|
+
const cells = renderCells(testData ?? {});
|
|
167
222
|
variants.push({
|
|
168
223
|
...base,
|
|
169
224
|
ref: vpId,
|
|
170
225
|
title: category1,
|
|
171
226
|
condition: category1,
|
|
172
227
|
data: resolveDataPairs(m.feature.referencedVars, testData),
|
|
173
|
-
trigger:
|
|
174
|
-
oracle:
|
|
228
|
+
trigger: cells.trigger,
|
|
229
|
+
oracle: cells.oracle,
|
|
230
|
+
precondition: cells.precondition,
|
|
231
|
+
verification: cells.verification,
|
|
175
232
|
fingerprint: scenarioFingerprint(m.feature),
|
|
176
233
|
result: resolveResultVariants(m, results ?? null, testData ?? undefined)[0]?.result,
|
|
177
234
|
});
|
|
@@ -180,9 +237,77 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
180
237
|
return variants;
|
|
181
238
|
}
|
|
182
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Display substitution for matrix cells. Unlike the legacy exporter's
|
|
242
|
+
* substituteTestDataVars (which keeps EMPTY values literal for the Steps
|
|
243
|
+
* column), an empty value is a legitimate test input here (an intentionally
|
|
244
|
+
* empty field) — it renders as `(empty)` so the tester sees the intent and no
|
|
245
|
+
* `{{token}}` survives into the deliverable (Gate D would reject it).
|
|
246
|
+
*/
|
|
247
|
+
function substituteDisplayVars(text: string, vars: Record<string, string>): string {
|
|
248
|
+
return text.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (m, key: string) => {
|
|
249
|
+
if (!(key in vars)) return m; // unknown stays literal → Gate D flags it
|
|
250
|
+
const v = vars[key];
|
|
251
|
+
return v === '' ? '(empty)' : v;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sentenceOf(text: string): string {
|
|
256
|
+
const s = text.trim();
|
|
257
|
+
if (!s) return s;
|
|
258
|
+
const cap = s.charAt(0).toUpperCase() + s.slice(1);
|
|
259
|
+
return /[.!?…]$/.test(cap) ? cap : `${cap}.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Requirement inventory ∪ traces → coverage table. Ids are scanned from the spec
|
|
264
|
+
* text (FR-/TR-/NFR-<n>); map overrides win over the derived covered/gap status.
|
|
265
|
+
*/
|
|
266
|
+
export function computeRequirements(
|
|
267
|
+
specText: string,
|
|
268
|
+
map: DeliveryMap,
|
|
269
|
+
items: DeliveryItem[],
|
|
270
|
+
): RequirementCoverage[] {
|
|
271
|
+
const ids: string[] = [];
|
|
272
|
+
const seen = new Set<string>();
|
|
273
|
+
for (const m of specText.matchAll(/\b(?:FR|TR|NFR)-\d+\b/g)) {
|
|
274
|
+
if (!seen.has(m[0])) { seen.add(m[0]); ids.push(m[0]); }
|
|
275
|
+
}
|
|
276
|
+
// Overrides may reference ids the spec text doesn't list (e.g. a shared catalog).
|
|
277
|
+
for (const id of Object.keys(map.requirements ?? {})) {
|
|
278
|
+
if (!seen.has(id)) { seen.add(id); ids.push(id); }
|
|
279
|
+
}
|
|
280
|
+
if (ids.length === 0) return [];
|
|
281
|
+
|
|
282
|
+
return ids.map((id) => {
|
|
283
|
+
const traced = items.filter((it) => it.variants.some((v) => v.traces.includes(id)));
|
|
284
|
+
const override = (map.requirements ?? {})[id];
|
|
285
|
+
return {
|
|
286
|
+
id,
|
|
287
|
+
status: override?.status ?? (traced.length > 0 ? 'covered' : 'gap'),
|
|
288
|
+
items: traced.map((it) => it.id),
|
|
289
|
+
variantCount: traced.reduce((a, it) => a + it.variants.filter((v) => v.traces.includes(id)).length, 0),
|
|
290
|
+
note: override?.note ?? '',
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Resolve one level of {{key}} cross-references inside test-data VALUES. */
|
|
296
|
+
function resolveCrossRefs(testData: Record<string, string> | null): Record<string, string> | null {
|
|
297
|
+
if (!testData) return null;
|
|
298
|
+
const out: Record<string, string> = {};
|
|
299
|
+
for (const [k, v] of Object.entries(testData)) {
|
|
300
|
+
out[k] = typeof v === 'string' && v.includes('{{') ? substituteDisplayVars(v, testData) : v;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
183
305
|
function rowAsStrings(row: Record<string, unknown>): Record<string, string> {
|
|
184
306
|
const out: Record<string, string> = {};
|
|
185
|
-
for (const [k, v] of Object.entries(row))
|
|
307
|
+
for (const [k, v] of Object.entries(row)) {
|
|
308
|
+
out[k] = String(v);
|
|
309
|
+
out[`row.${k}`] = String(v); // steps reference dataset columns as {{row.<col>}}
|
|
310
|
+
}
|
|
186
311
|
return out;
|
|
187
312
|
}
|
|
188
313
|
|
|
@@ -243,6 +368,12 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
243
368
|
const { result, counts } = rollUp(groupVariants);
|
|
244
369
|
const first = groupVariants[0];
|
|
245
370
|
const triggerShapes = new Set(groupVariants.map((v) => v.triggerShape.join(' ; ')));
|
|
371
|
+
const modes = new Set(groupVariants.map((v) => v.mode));
|
|
372
|
+
// Item priority = highest variant priority (per-variant priorities stay visible).
|
|
373
|
+
const priorityRank: Record<string, number> = { High: 0, Normal: 1, Low: 2 };
|
|
374
|
+
const priority = groupVariants
|
|
375
|
+
.map((v) => v.priority)
|
|
376
|
+
.sort((a, b) => (priorityRank[a] ?? 1) - (priorityRank[b] ?? 1))[0] ?? 'Normal';
|
|
246
377
|
return {
|
|
247
378
|
id: g.id,
|
|
248
379
|
target: g.target,
|
|
@@ -251,8 +382,8 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
251
382
|
category: g.category,
|
|
252
383
|
// A stale (drifted) group presents as proposed regardless of its stored state.
|
|
253
384
|
review: stale.has(g.id) ? 'proposed' : g.review,
|
|
254
|
-
priority
|
|
255
|
-
mode: first?.mode ?? 'auto',
|
|
385
|
+
priority,
|
|
386
|
+
mode: modes.size > 1 ? 'mixed' : (first?.mode ?? 'auto'),
|
|
256
387
|
layers: Array.from(new Set(groupVariants.flatMap((v) => v.layers))),
|
|
257
388
|
traces: Array.from(new Set(groupVariants.flatMap((v) => v.traces))),
|
|
258
389
|
precondition: first?.precondition ?? [],
|
|
@@ -270,6 +401,18 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
270
401
|
reason: d.reason ?? '',
|
|
271
402
|
}));
|
|
272
403
|
|
|
404
|
+
// Requirement coverage (review §6): the requirement-id inventory comes from
|
|
405
|
+
// requirements/spec.md; @spec: traces mark `covered`; the map's `requirements:`
|
|
406
|
+
// section carries the reviewed overrides (partially_covered / not_applicable / …).
|
|
407
|
+
const requirements = computeRequirements(inputs.specText ?? '', map, items);
|
|
408
|
+
const gaps = requirements.filter((r) => r.status === 'gap');
|
|
409
|
+
if (gaps.length > 0) {
|
|
410
|
+
findings.push({
|
|
411
|
+
gate: 'R', severity: 'warning',
|
|
412
|
+
message: `${gaps.length} requirement id(s) with no coverage or disposition: ${gaps.map((g) => g.id).join(', ')} — trace them, or record a status in the map \`requirements:\` section`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
273
416
|
const variantCount = items.reduce((a, i) => a + i.variants.length, 0);
|
|
274
417
|
const approved = items.every((i) => i.review === 'approved')
|
|
275
418
|
&& !findings.some((f) => f.severity === 'error' || f.severity === 'review');
|
|
@@ -279,6 +422,7 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
279
422
|
formNo: map.formNo ?? 'BM-2-901-13',
|
|
280
423
|
items,
|
|
281
424
|
dispositions,
|
|
425
|
+
requirements,
|
|
282
426
|
findings,
|
|
283
427
|
manifest: {
|
|
284
428
|
unit,
|