@sun-asterisk/sungen 3.2.15 → 3.2.16-beta.10

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 (165) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +197 -13
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/dashboard/templates/index.html +1 -1
  5. package/dist/exporters/feature-parser.d.ts +16 -1
  6. package/dist/exporters/feature-parser.d.ts.map +1 -1
  7. package/dist/exporters/feature-parser.js +21 -2
  8. package/dist/exporters/feature-parser.js.map +1 -1
  9. package/dist/exporters/matrix/build.d.ts +61 -0
  10. package/dist/exporters/matrix/build.d.ts.map +1 -0
  11. package/dist/exporters/matrix/build.js +497 -0
  12. package/dist/exporters/matrix/build.js.map +1 -0
  13. package/dist/exporters/matrix/export.d.ts +50 -0
  14. package/dist/exporters/matrix/export.d.ts.map +1 -0
  15. package/dist/exporters/matrix/export.js +152 -0
  16. package/dist/exporters/matrix/export.js.map +1 -0
  17. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  18. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  19. package/dist/exporters/matrix/fingerprint.js +83 -0
  20. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  21. package/dist/exporters/matrix/gates.d.ts +21 -0
  22. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  23. package/dist/exporters/matrix/gates.js +367 -0
  24. package/dist/exporters/matrix/gates.js.map +1 -0
  25. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  26. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  27. package/dist/exporters/matrix/map-loader.js +193 -0
  28. package/dist/exporters/matrix/map-loader.js.map +1 -0
  29. package/dist/exporters/matrix/render-csv.d.ts +9 -0
  30. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  31. package/dist/exporters/matrix/render-csv.js +92 -0
  32. package/dist/exporters/matrix/render-csv.js.map +1 -0
  33. package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
  34. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  35. package/dist/exporters/matrix/render-xlsx.js +430 -0
  36. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  37. package/dist/exporters/matrix/types.d.ts +199 -0
  38. package/dist/exporters/matrix/types.d.ts.map +1 -0
  39. package/dist/exporters/matrix/types.js +15 -0
  40. package/dist/exporters/matrix/types.js.map +1 -0
  41. package/dist/exporters/matrix/wording.d.ts +61 -0
  42. package/dist/exporters/matrix/wording.d.ts.map +1 -0
  43. package/dist/exporters/matrix/wording.js +221 -0
  44. package/dist/exporters/matrix/wording.js.map +1 -0
  45. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  46. package/dist/exporters/scenario-merger.js +2 -1
  47. package/dist/exporters/scenario-merger.js.map +1 -1
  48. package/dist/exporters/spec-parser.d.ts.map +1 -1
  49. package/dist/exporters/spec-parser.js +2 -1
  50. package/dist/exporters/spec-parser.js.map +1 -1
  51. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  52. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  53. package/dist/exporters/xlsx-report-builder.js +3 -3
  54. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  55. package/dist/harness/audit.d.ts.map +1 -1
  56. package/dist/harness/audit.js +16 -2
  57. package/dist/harness/audit.js.map +1 -1
  58. package/dist/harness/blindspot.d.ts.map +1 -1
  59. package/dist/harness/blindspot.js +2 -1
  60. package/dist/harness/blindspot.js.map +1 -1
  61. package/dist/harness/capability-plan.d.ts.map +1 -1
  62. package/dist/harness/capability-plan.js +3 -2
  63. package/dist/harness/capability-plan.js.map +1 -1
  64. package/dist/harness/capability.d.ts +25 -0
  65. package/dist/harness/capability.d.ts.map +1 -1
  66. package/dist/harness/capability.js +44 -2
  67. package/dist/harness/capability.js.map +1 -1
  68. package/dist/harness/feedback.d.ts.map +1 -1
  69. package/dist/harness/feedback.js +3 -2
  70. package/dist/harness/feedback.js.map +1 -1
  71. package/dist/harness/flow-check.d.ts.map +1 -1
  72. package/dist/harness/flow-check.js +2 -1
  73. package/dist/harness/flow-check.js.map +1 -1
  74. package/dist/harness/flow-plan.d.ts.map +1 -1
  75. package/dist/harness/flow-plan.js +3 -2
  76. package/dist/harness/flow-plan.js.map +1 -1
  77. package/dist/harness/intent.d.ts.map +1 -1
  78. package/dist/harness/intent.js +2 -1
  79. package/dist/harness/intent.js.map +1 -1
  80. package/dist/harness/journey.d.ts.map +1 -1
  81. package/dist/harness/journey.js +3 -2
  82. package/dist/harness/journey.js.map +1 -1
  83. package/dist/harness/ledger.d.ts.map +1 -1
  84. package/dist/harness/ledger.js +3 -2
  85. package/dist/harness/ledger.js.map +1 -1
  86. package/dist/harness/manifest.d.ts.map +1 -1
  87. package/dist/harness/manifest.js +4 -3
  88. package/dist/harness/manifest.js.map +1 -1
  89. package/dist/harness/parse.d.ts.map +1 -1
  90. package/dist/harness/parse.js +16 -3
  91. package/dist/harness/parse.js.map +1 -1
  92. package/dist/harness/quality-gates.d.ts.map +1 -1
  93. package/dist/harness/quality-gates.js +2 -1
  94. package/dist/harness/quality-gates.js.map +1 -1
  95. package/dist/harness/read-text.d.ts +22 -0
  96. package/dist/harness/read-text.d.ts.map +1 -0
  97. package/dist/harness/read-text.js +64 -0
  98. package/dist/harness/read-text.js.map +1 -0
  99. package/dist/harness/script-check.d.ts.map +1 -1
  100. package/dist/harness/script-check.js +3 -2
  101. package/dist/harness/script-check.js.map +1 -1
  102. package/dist/harness/sensors.d.ts.map +1 -1
  103. package/dist/harness/sensors.js +2 -10
  104. package/dist/harness/sensors.js.map +1 -1
  105. package/dist/harness/spec-coverage.d.ts +5 -0
  106. package/dist/harness/spec-coverage.d.ts.map +1 -1
  107. package/dist/harness/spec-coverage.js +17 -7
  108. package/dist/harness/spec-coverage.js.map +1 -1
  109. package/dist/harness/trace.d.ts.map +1 -1
  110. package/dist/harness/trace.js +4 -3
  111. package/dist/harness/trace.js.map +1 -1
  112. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  113. package/dist/harness/viewpoint-ledger.js +2 -1
  114. package/dist/harness/viewpoint-ledger.js.map +1 -1
  115. package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -1
  116. package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  117. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  118. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  119. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  120. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  121. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  122. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
  123. package/package.json +4 -4
  124. package/src/cli/commands/delivery.ts +227 -15
  125. package/src/dashboard/templates/index.html +1 -1
  126. package/src/exporters/feature-parser.ts +21 -2
  127. package/src/exporters/matrix/build.ts +532 -0
  128. package/src/exporters/matrix/export.ts +149 -0
  129. package/src/exporters/matrix/fingerprint.ts +49 -0
  130. package/src/exporters/matrix/gates.ts +400 -0
  131. package/src/exporters/matrix/map-loader.ts +163 -0
  132. package/src/exporters/matrix/render-csv.ts +94 -0
  133. package/src/exporters/matrix/render-xlsx.ts +420 -0
  134. package/src/exporters/matrix/types.ts +226 -0
  135. package/src/exporters/matrix/wording.ts +221 -0
  136. package/src/exporters/scenario-merger.ts +2 -1
  137. package/src/exporters/spec-parser.ts +2 -1
  138. package/src/exporters/xlsx-report-builder.ts +3 -2
  139. package/src/harness/audit.ts +17 -3
  140. package/src/harness/blindspot.ts +2 -1
  141. package/src/harness/capability-plan.ts +3 -2
  142. package/src/harness/capability.ts +53 -3
  143. package/src/harness/feedback.ts +3 -2
  144. package/src/harness/flow-check.ts +2 -1
  145. package/src/harness/flow-plan.ts +3 -2
  146. package/src/harness/intent.ts +2 -1
  147. package/src/harness/journey.ts +3 -2
  148. package/src/harness/ledger.ts +3 -2
  149. package/src/harness/manifest.ts +4 -3
  150. package/src/harness/parse.ts +17 -3
  151. package/src/harness/quality-gates.ts +2 -1
  152. package/src/harness/read-text.ts +28 -0
  153. package/src/harness/script-check.ts +3 -2
  154. package/src/harness/sensors.ts +2 -1
  155. package/src/harness/spec-coverage.ts +22 -7
  156. package/src/harness/trace.ts +4 -3
  157. package/src/harness/viewpoint-ledger.ts +2 -1
  158. package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -1
  159. package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  160. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  161. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  162. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  163. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  164. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  165. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Delivery v2 — Test Case & Coverage Matrix model (docs/spec/delivery-coverage-matrix-spec.md).
3
+ *
4
+ * Gherkin stays the source of test meaning; the matrix is a derived presentation:
5
+ * one delivery item = one target + one intent + one oracle family, containing one or
6
+ * more atomic coverage variants (scenario / @cases row). Grouping semantics live in the
7
+ * committed Delivery Map (`qa/<kind>/<unit>/delivery/<unit>.map.yaml`) — AI-proposed,
8
+ * gate-validated, QA-approved.
9
+ */
10
+
11
+ import { PlaywrightResult } from '../types';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Delivery Map (the committed grouping artifact)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export type MapCategory = 'normal' | 'abnormal' | 'security' | 'nfr';
18
+ export type ReviewState = 'proposed' | 'approved';
19
+ export type DispositionKind = 'excluded' | 'blocked' | 'covered_elsewhere' | 'accepted_risk';
20
+
21
+ export interface MapGroup {
22
+ /** Stable delivery id, unique per unit, order-independent (e.g. DI-EMAIL-FORMAT). */
23
+ id: string;
24
+ /** ONE primary target: field/component dot-path, function/flow phrase, or `METHOD /path`. */
25
+ target: string;
26
+ /** One coherent test intent (the primary behavior/rule verified). */
27
+ intent: string;
28
+ /** Oracle family in words — the shared Pass/Fail observation. */
29
+ oracle: string;
30
+ category: MapCategory;
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;
36
+ /** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
37
+ variants: string[];
38
+ }
39
+
40
+ export interface MapDisposition {
41
+ as: DispositionKind;
42
+ reason?: string;
43
+ }
44
+
45
+ export type RequirementStatus =
46
+ | 'covered' | 'partially_covered' | 'covered_elsewhere' | 'planned' | 'gap' | 'not_applicable';
47
+
48
+ export interface RequirementOverride {
49
+ status: RequirementStatus;
50
+ note?: string;
51
+ }
52
+
53
+ export interface DeliveryMap {
54
+ version: number;
55
+ unit: string;
56
+ /** ISO form number rendered in the workbook masthead (default BM-2-901-13). */
57
+ formNo?: string;
58
+ groups: MapGroup[];
59
+ /** VP-id → intentionally-not-grouped disposition. */
60
+ dispositions: Record<string, MapDisposition>;
61
+ /** Requirement-id → reviewed status override (partially_covered / not_applicable / …).
62
+ * Without an override a requirement is `covered` when a variant traces to it, else `gap`. */
63
+ requirements: Record<string, RequirementOverride>;
64
+ /** VP-id → scenario fingerprint stamped at approval time (drift detector input).
65
+ * The reserved key `__map__` holds the fingerprint of the map's own semantic
66
+ * content (groups + dispositions) so edited wording re-opens review (GAP-09). */
67
+ fingerprints: Record<string, string>;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Derived model (deterministic, computed from the .feature at build time)
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /** Execution layers a variant verifies on — cross-checked with the verification scope. */
75
+ export type MatrixLayer = 'ui' | 'api' | 'db';
76
+
77
+ /**
78
+ * One atomic coverage variant — the unit of execution, result, and traceability.
79
+ * Backed by one scenario, or one `@cases` dataset row of a scenario.
80
+ */
81
+ export interface CoverageVariant {
82
+ /** `VP-ID` or `VP-ID#label` for a dataset row. */
83
+ ref: string;
84
+ vpId: string;
85
+ /** Dataset-row label when this variant is one `@cases` row. */
86
+ caseLabel?: string;
87
+ /** Human title (scenario name without the VP prefix; row label appended for @cases). */
88
+ title: string;
89
+ /** Viewpoint category from the VP prefix (LOGIC/VAL/UI/SEC/…). */
90
+ vpCategory: string;
91
+ priority: string; // High | Normal | Low
92
+ /** 'auto' | 'manual'; manual keeps its @manual:Mx reason. */
93
+ mode: 'auto' | 'manual';
94
+ manualReason?: string;
95
+ layers: MatrixLayer[];
96
+ /** @spec:* requirement traces. */
97
+ traces: string[];
98
+ /** Variables this scenario produces at RUN TIME (`remember … as {{v}}`, capability
99
+ * bindings) — they never exist in test-data, so Gate D must not demand them. */
100
+ runtimeVars: string[];
101
+ /** Condition summary: differentiating dimension shown on the sub-row. */
102
+ condition: string;
103
+ /** Resolved data pairs (test-data values / dataset-row columns), `key: value` lines. */
104
+ data: string[];
105
+ /** Normalized trigger shape (When steps, `{{var}}` collapsed) — signature input. */
106
+ triggerShape: string[];
107
+ /** Trigger for display (When steps, vars resolved). */
108
+ trigger: string[];
109
+ /** Normalized oracle shape (Then steps / manual Observable+Oracle) — signature input. */
110
+ oracleShape: string[];
111
+ /** Oracle for display. */
112
+ oracle: string[];
113
+ /** HOW to check (manual `Oracle:` lines — tools, panes, queries). Rendered as
114
+ * "Verification method" under the expected result, never inside it. */
115
+ verification: string[];
116
+ /** Precondition profile: auth role + scenario-level Given shapes — signature input. */
117
+ preconditionProfile: string;
118
+ /** Human preconditions (auth + own Given steps). */
119
+ precondition: string[];
120
+ /** 'simple' (G*W*T*) | 'sequence' (mid-flow Given / multi round-trip) | 'manual'. */
121
+ procedureProfile: 'simple' | 'sequence' | 'manual';
122
+ /** Canonical scenario fingerprint (drift detection). */
123
+ fingerprint: string;
124
+ /** Execution result, when the run report has one for this variant. */
125
+ result?: PlaywrightResult;
126
+ }
127
+
128
+ export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run' | 'na';
129
+
130
+ /** The exact word rendered in a variant's Result cell — the single vocabulary the
131
+ * parent's roll-up formula counts (COUNTIF is exact-match). */
132
+ export type VariantState = 'Passed' | 'Failed' | 'Blocked' | 'Pending' | 'N/A';
133
+
134
+ /** One summarized row in the matrix — a group of variants sharing the signature. */
135
+ export interface DeliveryItem {
136
+ id: string;
137
+ target: string;
138
+ intent: string;
139
+ oracle: string;
140
+ /** Short coverage-dimension digest from the map (compact parent view). */
141
+ dimensions?: string;
142
+ category: MapCategory;
143
+ review: ReviewState;
144
+ /** Highest priority among the variants (per-variant priorities stay on the sub-rows). */
145
+ priority: string;
146
+ /** Execution mode is a coverage dimension, not a split — 'mixed' when auto and
147
+ * manual variants share one intent (e.g. account states needing seeded data). */
148
+ mode: 'auto' | 'manual' | 'mixed';
149
+ layers: MatrixLayer[];
150
+ /** Union of variant traces (exact per-variant traces stay on the variants). */
151
+ traces: string[];
152
+ /** Preconditions shared by EVERY variant (the intersection) — written once here. */
153
+ precondition: string[];
154
+ /** Leading trigger steps shared by every variant — written once here; each
155
+ * variant renders only its remaining steps (`triggerDeltas`). */
156
+ trigger: string[];
157
+ variants: CoverageVariant[];
158
+ /** variant ref → its precondition lines that are not on the parent. */
159
+ preconditionDeltas: Record<string, string[]>;
160
+ /** variant ref → its trigger steps after the shared prefix. */
161
+ triggerDeltas: Record<string, string[]>;
162
+ /** Derived roll-up — never entered independently (Gate F). */
163
+ result: ItemResult;
164
+ resultCounts: { passed: number; failed: number; blocked: number; notRun: number; na: number };
165
+ }
166
+
167
+ export interface MatrixDisposition {
168
+ vpId: string;
169
+ title: string;
170
+ as: DispositionKind;
171
+ reason: string;
172
+ }
173
+
174
+ /** One row of the requirement-coverage table (Coverage sheet). */
175
+ export interface RequirementCoverage {
176
+ id: string; // FR-003 / TR-001 / NFR-…
177
+ status: RequirementStatus;
178
+ /** Delivery items whose variants trace to this requirement. */
179
+ items: string[];
180
+ variantCount: number;
181
+ note: string;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Gate findings
186
+ // ---------------------------------------------------------------------------
187
+
188
+ export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G' | 'K' | 'R' | 'W';
189
+ export type FindingSeverity = 'error' | 'review' | 'warning';
190
+
191
+ export interface MatrixFinding {
192
+ gate: GateId;
193
+ severity: FindingSeverity;
194
+ /** Delivery item / VP-id the finding anchors to, when applicable. */
195
+ ref?: string;
196
+ message: string;
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // The assembled matrix
201
+ // ---------------------------------------------------------------------------
202
+
203
+ export interface MatrixManifest {
204
+ unit: string;
205
+ transformerVersion: string;
206
+ sourceFingerprint: string; // canonical hash over all scenario fingerprints
207
+ mapFingerprint: string; // canonical hash of the map content
208
+ approvalState: 'approved' | 'draft';
209
+ itemCount: number;
210
+ variantCount: number;
211
+ thresholds: { maxVariantsPerItem: number };
212
+ }
213
+
214
+ export interface MatrixModel {
215
+ unit: string;
216
+ formNo: string;
217
+ items: DeliveryItem[];
218
+ dispositions: MatrixDisposition[];
219
+ /** Requirement coverage — empty when the unit has no requirements/spec.md ids. */
220
+ requirements: RequirementCoverage[];
221
+ findings: MatrixFinding[];
222
+ manifest: MatrixManifest;
223
+ }
224
+
225
+ /** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
226
+ export const MAX_VARIANTS_PER_ITEM = 20;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Wording normalization — deterministic rendering step AFTER semantic
3
+ * normalization (review feedback §10): turn sungen DSL steps into controlled
4
+ * manual-test English without changing the target, condition, trigger,
5
+ * precondition, oracle, or trace.
6
+ *
7
+ * - Actions render in the imperative: "User fill [Email] field with X"
8
+ * → "Enter X in the Email field."
9
+ * - Expected results render as observable assertions (never tester actions):
10
+ * "User see [Jobs] page" → "The Jobs page is displayed."
11
+ * - Manual `# Tester verifies:` labels (Setup:/Action:/Observable:/Oracle:)
12
+ * become structured fields instead of prose: Setup → precondition,
13
+ * Action → action, Observable → expected, Oracle → verification method.
14
+ */
15
+
16
+ /** A step that CAPTURES a runtime value: `User remember [X] text as {{var}}`. */
17
+ export const RUNTIME_CAPTURE = /\bremember\b/i;
18
+
19
+ /**
20
+ * Variable names a scenario defines at RUN TIME rather than reading from
21
+ * test-data: values captured from the page (`remember … as {{v}}`) and values
22
+ * bound by a capability precondition tag (`@query:`, `@api:`, `@dataFactory:`).
23
+ * The compiler already treats these as "captured" (DataResolver.registerCaptured);
24
+ * the delivery matrix must agree or it demands a static value that cannot exist.
25
+ */
26
+ export function runtimeVarsOf(stepTexts: string[], tags: string[]): Set<string> {
27
+ const out = new Set<string>();
28
+ for (const text of stepTexts) {
29
+ if (!RUNTIME_CAPTURE.test(text)) continue;
30
+ const m = text.match(/\bas\s+\{\{\s*([^}]+?)\s*\}\}/i);
31
+ if (m) out.add(m[1].trim());
32
+ }
33
+ // A capability precondition binds its result under the endpoint/query/recipe
34
+ // name, so `{{<name>}}` and `{{<name>.field}}` exist only during the run.
35
+ for (const t of tags) {
36
+ const m = t.match(/^@(?:query|api|dataFactory):([^(\s]+)/);
37
+ if (m) out.add(m[1]);
38
+ }
39
+ return out;
40
+ }
41
+
42
+ // `[Email] field` → `Email field` (the visible label + its element type).
43
+ function deRef(text: string): string {
44
+ return text.replace(/\[([^\]]+)\]/g, '$1');
45
+ }
46
+
47
+ function sentence(text: string): string {
48
+ // Trim the ENDS only — internal whitespace may be the test data itself
49
+ // (a padded email, a spaces-only value); collapsing it would silently
50
+ // change what the tester types (review GAP-04).
51
+ let s = text.trim();
52
+ if (!s) return s;
53
+ s = s.charAt(0).toUpperCase() + s.slice(1);
54
+ if (!/[.!?…"]$/.test(s)) s += '.';
55
+ return s;
56
+ }
57
+
58
+ /**
59
+ * Render one action step in the imperative. Pattern table covers the common
60
+ * sungen step verbs; anything unmatched falls back to actor-stripped text —
61
+ * still readable, never a raw `User fill`.
62
+ */
63
+ export function renderAction(raw: string): string {
64
+ let s = raw.trim().replace(/^(User|The user)\s+/i, '');
65
+ // `remember … as {{v}}` DECLARES a runtime variable — the trailing token is a
66
+ // name, not a value to resolve. Quote it so no `{{brace}}` reaches the sheet
67
+ // whatever rule matches below.
68
+ if (RUNTIME_CAPTURE.test(s)) s = s.replace(/\bas\s+\{\{\s*([^}]+?)\s*\}\}/i, 'as "$1"');
69
+
70
+ const rules: Array<[RegExp, (m: RegExpMatchArray) => string]> = [
71
+ // remember [X] (text|value) as "v" — capture a value for a later comparison
72
+ [/^remembers? \[([^\]]+)\]\s*(text|value)?\s*as "([^"]+)"$/i,
73
+ (m) => `Note the ${m[1]} ${m[2] || 'text'} for later (as "${m[3]}")`],
74
+ // fill [X] field with V
75
+ [/^fills? \[([^\]]+)\][a-z ]* with (.+)$/i, (m) => `Enter ${m[2]} in the ${m[1]} field`],
76
+ // clear [X] field
77
+ [/^clears? \[([^\]]+)\](.*)$/i, (m) => `Clear the ${m[1]}${m[2] || ' field'}`],
78
+ // click [X] <type>
79
+ [/^clicks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Click the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
80
+ // press <Key> at/in [X] field
81
+ [/^press(?:es)? (.+?) (?:at|in|on|inside) \[([^\]]+)\](?: field)?$/i, (m) => `Press ${m[1]} in the ${m[2]} field`],
82
+ [/^press(?:es)? (.+)$/i, (m) => `Press ${m[1]}`],
83
+ // select V in/from [X] dropdown
84
+ [/^selects? (.+?) (?:in|from) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Select ${m[1]} in the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
85
+ // check/uncheck [X] checkbox
86
+ [/^(un)?checks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `${m[1] ? 'Uncheck' : 'Check'} the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
87
+ // hover [X]
88
+ [/^hovers? (?:over )?\[([^\]]+)\]\s*(\w+)?$/i, (m) => `Hover over the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
89
+ // upload V to [X]
90
+ [/^uploads? (.+?) (?:to|into) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Upload ${m[1]} to the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
91
+ // is on [X] page (as an action = navigate)
92
+ [/^is on \[([^\]]+)\] page(.*)$/i, (m) => `Open the ${m[1]} page${m[2] ?? ''}`],
93
+ // wait for [X] <type> (is )?visible
94
+ [/^waits? for \[([^\]]+)\]\s*(\w+)?(?: is)?(?: visible)?$/i, (m) => `Wait until the ${m[1]}${m[2] ? ` ${m[2]}` : ''} is visible`],
95
+ // scroll to [X]
96
+ [/^scrolls? (?:to|into) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Scroll to the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
97
+ ];
98
+
99
+ for (const [re, out] of rules) {
100
+ const m = s.match(re);
101
+ if (m) return sentence(deRef(out(m)));
102
+ }
103
+ return sentence(deRef(s));
104
+ }
105
+
106
+ /**
107
+ * Render one expected step as an observable assertion (no tester action, no
108
+ * `should`, no DSL `User see`).
109
+ */
110
+ export function renderExpected(raw: string): string {
111
+ // A capture step is an INSTRUCTION wherever it sits — Gherkin often chains
112
+ // `remember … / remember … / expect a is not b` inside one Then block. Render
113
+ // it as the note-it-down action, never as an assertion (and never leaving the
114
+ // declared `{{name}}` token behind).
115
+ if (RUNTIME_CAPTURE.test(raw) && /\bas\s+\{\{/.test(raw)) return renderAction(raw);
116
+ let s = raw.trim().replace(/^(User|The user)\s+/i, '');
117
+
118
+ const rules: Array<[RegExp, (m: RegExpMatchArray) => string]> = [
119
+ // expect A is (not) B — comparing two captured values (order matters: "is not" first)
120
+ [/^expects? (.+?) is not (.+)$/i, (m) => `${m[1]} differs from ${m[2]}`],
121
+ [/^expects? (.+?) is (.+)$/i, (m) => `${m[1]} equals ${m[2]}`],
122
+ // see all [X] <type> contain(s) V — every matching element, not just the first
123
+ [/^sees? all \[([^\]]+)\]\s*(\w+)? (?:contains?|match(?:es)?|belongs?) (.+)$/i,
124
+ (m) => `Every ${m[1]}${m[2] ? ` ${m[2]}` : ''} contains ${m[3]}`],
125
+ // see all [X] <type> contain [Child] <type> — structural per-item proof
126
+ [/^sees? all \[([^\]]+)\]\s*(\w+)? (?:contains?|includes?) \[([^\]]+)\]\s*(\w+)?$/i,
127
+ (m) => `Every ${m[1]}${m[2] ? ` ${m[2]}` : ''} contains a ${m[3]}${m[4] ? ` ${m[4]}` : ''}`],
128
+ // see [X] page
129
+ [/^sees? \[([^\]]+)\] page$/i, (m) => `The ${m[1]} page is displayed`],
130
+ // see [X] <type> with V
131
+ [/^sees? \[([^\]]+)\]\s*(\w+)? with (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} shows ${m[3]}`],
132
+ // see [X] <type> contains V
133
+ [/^sees? \[([^\]]+)\]\s*(\w+)? contains (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} contains ${m[3]}`],
134
+ // see [X] <type> has text V
135
+ [/^sees? \[([^\]]+)\]\s*(\w+)? has text (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} shows ${m[3]}`],
136
+ // see [X] <type> is hidden / is disabled / is enabled / …
137
+ [/^sees? \[([^\]]+)\]\s*(\w+)? is (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is ${m[3]}`],
138
+ // not see [X] <type>
139
+ [/^(?:do(?:es)? )?not sees? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is not displayed`],
140
+ // see [X] <type>
141
+ [/^sees? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is visible`],
142
+ ];
143
+
144
+ for (const [re, out] of rules) {
145
+ const m = s.match(re);
146
+ if (m) return sentence(deRef(out(m)));
147
+ }
148
+ return sentence(deRef(s));
149
+ }
150
+
151
+ /** Precondition wording: a state, not an action ("The user is signed out."). */
152
+ export function renderPrecondition(raw: string): string {
153
+ const s = raw.trim().replace(/^(User|The user)\s+/i, '');
154
+ const m = s.match(/^is on \[([^\]]+)\] page(.*)$/i);
155
+ if (m) return sentence(`The user is on the ${m[1]} page${m[2] ?? ''}`);
156
+ // "wait for [X] <type> (is) visible" is a STATE once established, not an action.
157
+ const w = s.match(/^waits? for \[([^\]]+)\]\s*(\w+)?(?: is)?(?: visible)?$/i);
158
+ if (w) return sentence(`The ${w[1]}${w[2] ? ` ${w[2]}` : ''} is visible`);
159
+ const auth = s.match(/^is (signed|logged) (in|out)(.*)$/i);
160
+ if (auth) return sentence(`The user is ${auth[1]} ${auth[2]}${auth[3] ?? ''}`);
161
+ return sentence(deRef(`The user ${s.charAt(0).toLowerCase()}${s.slice(1)}`));
162
+ }
163
+
164
+ /**
165
+ * A manual `Setup:` line is an INSTRUCTION the tester performs to establish the
166
+ * state — imperative reads correctly ("Seed the locked account."), while
167
+ * "The user seed …" is broken grammar (review GAP-08).
168
+ */
169
+ export function renderSetupInstruction(raw: string): string {
170
+ return renderAction(raw);
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Manual `# Tester verifies:` comment classification (structured, label-free)
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export interface ManualProcedure {
178
+ /** Setup/Precondition/Arrange lines — the state to establish first. */
179
+ preconditions: string[];
180
+ /** Action/unlabelled lines — the imperative procedure. */
181
+ actions: string[];
182
+ /** Observable/Expect/Result/Assert lines — the observable outcome. */
183
+ expected: string[];
184
+ /** Oracle/Verify lines — HOW to check (tools, panes, queries). */
185
+ verification: string[];
186
+ }
187
+
188
+ /**
189
+ * Split a manual scenario's numbered comment lines into the four structured
190
+ * fields. Labels are consumed (structured), never left inside the prose —
191
+ * review feedback §10.2(3). Continuation lines append to the previous item;
192
+ * pre-amble (rationale/header/dividers) is skipped.
193
+ */
194
+ export function classifyManualComments(comments: string[]): ManualProcedure {
195
+ const out: ManualProcedure = { preconditions: [], actions: [], expected: [], verification: [] };
196
+ let last: { list: string[]; idx: number } | null = null;
197
+
198
+ const bucketOf = (label: string): keyof ManualProcedure => {
199
+ if (/setup|precondition|arrange|given/i.test(label)) return 'preconditions';
200
+ if (/oracle|verify|verification|how to check/i.test(label)) return 'verification';
201
+ if (/observ|expect|result|then|assert/i.test(label)) return 'expected';
202
+ return 'actions';
203
+ };
204
+
205
+ for (const raw of comments) {
206
+ const line = raw.trim();
207
+ if (!line) continue;
208
+ if (/^[-=*_]{2,}/.test(line)) { last = null; continue; }
209
+ const m = line.match(/^\d+[.)]\s*(?:([A-Za-z][A-Za-z /]*?):\s*)?(.+)$/);
210
+ if (m) {
211
+ const label = (m[1] || '').trim();
212
+ const text = m[2].trim();
213
+ const list = out[bucketOf(label)];
214
+ list.push(text);
215
+ last = { list, idx: list.length - 1 };
216
+ } else if (last) {
217
+ last.list[last.idx] += ' ' + line;
218
+ }
219
+ }
220
+ return out;
221
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { FeatureMetadata, OrderedStep, ScenarioMetadata, SpecFileData, SpecTest } from './types';
12
12
  import { isStepsBaseScenario, isSampleScaffoldScenario, extractTestcaseType } from './feature-parser';
13
+ import { SCENARIO_ID } from './feature-parser';
13
14
 
14
15
  /**
15
16
  * Parse a @manual scenario's `# Tester verifies:` comment block into procedure vs
@@ -162,7 +163,7 @@ function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): Sp
162
163
  if (exact) return exact;
163
164
 
164
165
  // Fallback: match by VP-ID prefix (ignore trailing text differences)
165
- const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)/);
166
+ const vpMatch = scenario.name.match(SCENARIO_ID);
166
167
  if (vpMatch) {
167
168
  const vpId = vpMatch[1];
168
169
  const byVp = tests.find((t) => t.vpId === vpId);
@@ -5,6 +5,7 @@
5
5
 
6
6
  import * as fs from 'fs';
7
7
  import { SpecTest, SpecFileData } from './types';
8
+ import { SCENARIO_ID } from './feature-parser';
8
9
 
9
10
  /**
10
11
  * Extract the inner body of a test('title', async (...) => { BODY }) block.
@@ -165,7 +166,7 @@ function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation
165
166
  * Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
166
167
  */
167
168
  function extractVpId(title: string): string | undefined {
168
- const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\b/);
169
+ const match = title.match(new RegExp(SCENARIO_ID.source + '\\b'));
169
170
  return match ? match[1] : undefined;
170
171
  }
171
172
 
@@ -143,6 +143,7 @@ export function renderReportHeaderBand(
143
143
  ws: ExcelJS.Worksheet,
144
144
  titleLabel: string,
145
145
  sungenVersion?: string,
146
+ formNo: string = 'BM-2-901-13',
146
147
  ): void {
147
148
  const issueDate = (() => {
148
149
  const d = new Date();
@@ -187,9 +188,9 @@ export function renderReportHeaderBand(
187
188
  d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
188
189
  d1.border = allBordersBlack;
189
190
 
190
- // G1 — No: BM-2-901-13
191
+ // G1 — No: <form number> (the company ISO form id; configurable via the delivery map)
191
192
  const g1 = ws.getCell('G1');
192
- g1.value = 'No: BM-2-901-13';
193
+ g1.value = `No: ${formNo}`;
193
194
  g1.font = { size: 12, name: TIMES };
194
195
  g1.alignment = { vertical: 'middle' };
195
196
  g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
@@ -17,7 +17,7 @@ import {
17
17
  } from './sensors';
18
18
  import { loadFlowScenarios } from './flow-check';
19
19
  import { manualReasonMismatches, MANUAL_REASONS, buildPlan } from './capability-plan';
20
- import { readCapabilities } from './capability';
20
+ import { readCapabilities, verificationScopeFindings } from './capability';
21
21
  import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
22
22
  import { getProvenance, Provenance } from './provenance';
23
23
  import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
@@ -27,6 +27,7 @@ import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-
27
27
  import { capabilityRegistry } from '../capabilities/registry';
28
28
  import { discoverAndRegisterCapabilities } from '../capabilities/discover';
29
29
  import { contextRouter } from '../capabilities/context-router';
30
+ import { readTextFile } from './read-text';
30
31
 
31
32
  export interface AuditReport {
32
33
  screen: string;
@@ -118,7 +119,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
118
119
  const catalogScreenName = catalogIdFromScreenDir(screenDir);
119
120
 
120
121
  const specPath = path.join(screenDir, 'requirements', 'spec.md');
121
- const featureText = fs.existsSync(featurePath) ? fs.readFileSync(featurePath, 'utf-8') : '';
122
+ const featureText = fs.existsSync(featurePath) ? readTextFile(featurePath) : '';
122
123
 
123
124
  const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
124
125
  const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
@@ -129,7 +130,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
129
130
  discoverAndRegisterCapabilities();
130
131
  // The active platform (web | mobile | …) activates its own capability for scoring + sensor routing.
131
132
  // `web` has no capability of its own → scoringCap stays the default `ui` (byte-identical).
132
- const platform = readCapabilities(projectRootFromScreenDir(screenDir)).platform;
133
+ const capProfile = readCapabilities(projectRootFromScreenDir(screenDir));
134
+ const platform = capProfile.platform;
133
135
  const defaultCap = capabilityRegistry.defaultCapabilityId();
134
136
  const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap, platform);
135
137
  const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
@@ -223,6 +225,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
223
225
  for (const c of flowCredits) {
224
226
  findings.push(`COVERED-VIA-FLOW: critical theme "${c.theme}" is not covered on this screen but is deeply covered by flow "${c.flow}" — cross-screen depth correctly owned by the flow, so it is credited (not a screen gap). Verify with \`sungen flow-check\`.`);
225
227
  }
228
+ // Verification-scope (advisory): API/DB verification the project's recorded test type never asked for.
229
+ for (const v of verificationScopeFindings(featureText, fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '', capProfile)) {
230
+ findings.push(`VERIFICATION-OUT-OF-SCOPE (${v.layer}): this unit ${v.message}`);
231
+ }
226
232
  for (const g of gate.gaps) {
227
233
  if (g.status === 'shallow') {
228
234
  findings.push(`GATE: critical theme "${g.theme}" is covered only by SHALLOW scenarios (no data assertion) → deepen with \`... with {{value}}\` / \`table ... with {{value}}\` (count @manual cross-screen too).`);
@@ -284,6 +290,14 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
284
290
  for (const u of spec.uncoveredMust) {
285
291
  findings.push(`SPEC-UNCOVERED: ${u.id} (MUST) has no covering scenario — "${u.text}" → add a scenario or tag one @spec:${u.id}.`);
286
292
  }
293
+ if (spec.inferredOnly.length > 0) {
294
+ // Covered, but only by keyword inference — no scenario cites the id. Delivery's
295
+ // requirement table (and any later refactor) cannot follow an inferred link, so
296
+ // the trace has to be written down while the author still knows which scenario
297
+ // proves what.
298
+ const shown = spec.inferredOnly.slice(0, 8).join(', ');
299
+ findings.push(`SPEC-TRACE-IMPLICIT: ${spec.inferredOnly.length} requirement(s) are covered by keyword inference only (${shown}${spec.inferredOnly.length > 8 ? ', …' : ''}) — add \`@spec:<id>\` to the scenario that proves each so the trace survives edits and delivery can report it.`);
300
+ }
287
301
  if (taxonomyMismatch) {
288
302
  findings.push(`VP-TAXONOMY-MISMATCH: only ${(trace.mappedRatio * 100).toFixed(0)}% of scenarios use the viewpoint IDs declared in test-viewpoint.md — scenarios invented a generic VP-<CAT> scheme. Re-tag to the project's viewpoint IDs so the coverage matrix is accurate.`);
289
303
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
+ import { readTextFile } from './read-text';
13
14
 
14
15
  export interface Blindspot {
15
16
  ts: string;
@@ -35,7 +36,7 @@ export function addBlindspot(entry: Omit<Blindspot, 'ts'> & { ts?: string }): st
35
36
  export function listBlindspots(): Blindspot[] {
36
37
  const p = storePath();
37
38
  if (!fs.existsSync(p)) return [];
38
- return fs.readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => {
39
+ return readTextFile(p).split('\n').filter(Boolean).map((l) => {
39
40
  try { return JSON.parse(l) as Blindspot; } catch { return null; }
40
41
  }).filter(Boolean) as Blindspot[];
41
42
  }
@@ -9,6 +9,7 @@
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { loadDriverCatalog } from './capability';
12
+ import { readTextFile } from './read-text';
12
13
 
13
14
  export type ReasonClass = 'capability' | 'keep' | 'flow';
14
15
  export interface ReasonDef { code: string; label: string; cls: ReasonClass; drivers: string[] }
@@ -61,7 +62,7 @@ interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason
61
62
  /** Parse scenarios with their tags + the reason comment line above (for @manual). */
62
63
  export function parseScenarios(featurePath: string): ParsedScenario[] {
63
64
  if (!fs.existsSync(featurePath)) return [];
64
- const lines = fs.readFileSync(featurePath, 'utf-8').split('\n');
65
+ const lines = readTextFile(featurePath).split('\n');
65
66
  const out: ParsedScenario[] = [];
66
67
  for (let i = 0; i < lines.length; i++) {
67
68
  const m = lines[i].match(/^\s*Scenario:\s*(.+)$/);
@@ -123,7 +124,7 @@ export interface ReasonMismatch { scenario: string; explicit: string; inferred:
123
124
  */
124
125
  export function manualReasonMismatches(featurePath: string): ReasonMismatch[] {
125
126
  if (!fs.existsSync(featurePath)) return [];
126
- const lines = fs.readFileSync(featurePath, 'utf-8').split('\n');
127
+ const lines = readTextFile(featurePath).split('\n');
127
128
  const out: ReasonMismatch[] = [];
128
129
  for (let i = 0; i < lines.length; i++) {
129
130
  const m = lines[i].match(/^\s*Scenario:\s*(.+)$/);