@sun-asterisk/sungen 3.2.21-beta.1 → 3.2.21

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 (86) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +0 -8
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/delivery.d.ts.map +1 -1
  5. package/dist/cli/commands/delivery.js +0 -7
  6. package/dist/cli/commands/delivery.js.map +1 -1
  7. package/dist/cli/commands/trace.d.ts.map +1 -1
  8. package/dist/cli/commands/trace.js +0 -9
  9. package/dist/cli/commands/trace.js.map +1 -1
  10. package/dist/exporters/matrix/export.d.ts.map +1 -1
  11. package/dist/exporters/matrix/export.js +0 -11
  12. package/dist/exporters/matrix/export.js.map +1 -1
  13. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  14. package/dist/exporters/matrix/render-xlsx.js +0 -15
  15. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  16. package/dist/exporters/matrix/types.d.ts +0 -2
  17. package/dist/exporters/matrix/types.d.ts.map +1 -1
  18. package/dist/exporters/matrix/types.js.map +1 -1
  19. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  20. package/dist/exporters/playwright-report-parser.js +0 -1
  21. package/dist/exporters/playwright-report-parser.js.map +1 -1
  22. package/dist/exporters/types.d.ts +0 -2
  23. package/dist/exporters/types.d.ts.map +1 -1
  24. package/dist/generators/test-generator/diagnostics.d.ts +1 -6
  25. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  26. package/dist/generators/test-generator/diagnostics.js +0 -5
  27. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  28. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  29. package/dist/generators/test-generator/patterns/index.js +19 -27
  30. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  31. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  32. package/dist/generators/test-generator/step-mapper.js +0 -22
  33. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  34. package/dist/harness/audit.d.ts +0 -2
  35. package/dist/harness/audit.d.ts.map +1 -1
  36. package/dist/harness/audit.js +9 -79
  37. package/dist/harness/audit.js.map +1 -1
  38. package/dist/harness/flow-plan.d.ts +0 -3
  39. package/dist/harness/flow-plan.d.ts.map +1 -1
  40. package/dist/harness/flow-plan.js +2 -6
  41. package/dist/harness/flow-plan.js.map +1 -1
  42. package/dist/harness/parse.d.ts +0 -5
  43. package/dist/harness/parse.d.ts.map +1 -1
  44. package/dist/harness/parse.js +1 -29
  45. package/dist/harness/parse.js.map +1 -1
  46. package/dist/harness/sensors.d.ts.map +1 -1
  47. package/dist/harness/sensors.js +1 -13
  48. package/dist/harness/sensors.js.map +1 -1
  49. package/dist/harness/spec-coverage.d.ts.map +1 -1
  50. package/dist/harness/spec-coverage.js +5 -29
  51. package/dist/harness/spec-coverage.js.map +1 -1
  52. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +3 -41
  53. package/dist/orchestrator/templates/ai-src/commands/create-test.md +0 -10
  54. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +16 -35
  55. package/dist/orchestrator/templates/qa-context.md +1 -14
  56. package/package.json +3 -3
  57. package/src/cli/commands/audit.ts +0 -8
  58. package/src/cli/commands/delivery.ts +0 -6
  59. package/src/cli/commands/trace.ts +0 -9
  60. package/src/exporters/matrix/export.ts +0 -11
  61. package/src/exporters/matrix/render-xlsx.ts +0 -15
  62. package/src/exporters/matrix/types.ts +0 -2
  63. package/src/exporters/playwright-report-parser.ts +0 -2
  64. package/src/exporters/types.ts +0 -2
  65. package/src/generators/test-generator/diagnostics.ts +1 -6
  66. package/src/generators/test-generator/patterns/index.ts +24 -30
  67. package/src/generators/test-generator/step-mapper.ts +0 -22
  68. package/src/harness/audit.ts +10 -82
  69. package/src/harness/flow-plan.ts +3 -10
  70. package/src/harness/parse.ts +1 -31
  71. package/src/harness/sensors.ts +1 -13
  72. package/src/harness/spec-coverage.ts +4 -26
  73. package/src/orchestrator/templates/ai-src/commands/add-flow.md +3 -41
  74. package/src/orchestrator/templates/ai-src/commands/create-test.md +0 -10
  75. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +16 -35
  76. package/src/orchestrator/templates/qa-context.md +1 -14
  77. package/dist/harness/flow-contract.d.ts +0 -71
  78. package/dist/harness/flow-contract.d.ts.map +0 -1
  79. package/dist/harness/flow-contract.js +0 -235
  80. package/dist/harness/flow-contract.js.map +0 -1
  81. package/dist/harness/perf.d.ts +0 -40
  82. package/dist/harness/perf.d.ts.map +0 -1
  83. package/dist/harness/perf.js +0 -136
  84. package/dist/harness/perf.js.map +0 -1
  85. package/src/harness/flow-contract.ts +0 -229
  86. package/src/harness/perf.ts +0 -112
@@ -1,229 +0,0 @@
1
- /**
2
- * Flow Contract — the declared boundary of a flow, and the sensors that verify
3
- * the suite against it. (#569, docs/spec/sungen-flow-quality-spec.md)
4
- *
5
- * A flow is the SMALLEST complete business action chain: one clear trigger ending
6
- * in ONE observable, valuable outcome. Nothing enforced that: `add-flow` asked only
7
- * "which screens, in order?", so a real example mixed three business goals (cart,
8
- * filter, product-detail) in one "flow", and the harness scored flows with screen
9
- * machinery — a registration flow was judged against the `form` page-type checklist
10
- * (coverage 0%), and every flow audit showed taxonomy=0% because flow phase ids
11
- * (FL-HP-001) don't even parse as categories.
12
- *
13
- * The contract is a declaration the QA owns (AI proposes at add-flow; a filled
14
- * contract is an INPUT to generation, never an output — same rule as
15
- * test-viewpoint.md). These sensors are deterministic checks against it:
16
- *
17
- * - outcome proof — the flow proves its own goal with an automated data assertion
18
- * - scope creep — scenarios that never touch the outcome are a second goal
19
- * - phase coverage — HP / ER / EH journey phases, the flow's coverage axis
20
- * - handoff — a cross-screen transition is followed by an assertion
21
- * - stateful depth — generalizes the cart-hardcoded regression dims to any
22
- * declared collection (order, application, submission …)
23
- */
24
- import * as fs from 'fs';
25
- import * as path from 'path';
26
- import { parse as parseYaml } from 'yaml';
27
- import { ScenarioInfo } from './parse';
28
- import { readTextFile } from './read-text';
29
-
30
- export interface FlowContract {
31
- goal: string;
32
- actor?: string;
33
- trigger?: string;
34
- precondition?: string;
35
- outcome: { screen: string; assertion?: string };
36
- value?: string;
37
- /** Journey phases this flow declares. Default [HP, ER, EH]; UI is allowed but
38
- * never demanded (presentation is the balance axis's business, not coverage's). */
39
- phases: string[];
40
- /** The mutated collection (cart, order, application …) — enables regression dims. */
41
- stateful?: string;
42
- budgets?: Record<string, number>;
43
- }
44
-
45
- export interface FlowQualityResult {
46
- hasContract: boolean;
47
- contract?: FlowContract;
48
- /** Parse/shape errors — a broken contract is reported, never silently ignored. */
49
- errors: string[];
50
- outcomeProven: boolean;
51
- /** Manual-only proof: the goal is covered but not by automation. */
52
- outcomeManualOnly: boolean;
53
- /** Scenario names that never touch the outcome screen (guard/error phases excluded). */
54
- offGoal: string[];
55
- offGoalRatio: number;
56
- /** Off-goal categories, for the split suggestion ("VP-FILTER-* looks like its own flow"). */
57
- offGoalCategories: string[];
58
- phases: { phase: string; covered: boolean; automated: boolean }[];
59
- /** Covered-and-automated phases / declared phases (UI excluded) — the flow coverage axis. */
60
- phaseRatio: number;
61
- /** Cross-namespace transitions followed by an assertion / all transitions. */
62
- handoffs: { total: number; asserted: number; ratio: number };
63
- }
64
-
65
- const DEFAULT_PHASES = ['HP', 'ER', 'EH'];
66
-
67
- export function flowContractPath(unitDir: string): string {
68
- return path.join(unitDir, 'requirements', 'flow-contract.yaml');
69
- }
70
-
71
- /** Load + validate. Returns null when absent; a present-but-broken file returns errors. */
72
- export function loadFlowContract(unitDir: string): { contract: FlowContract | null; errors: string[] } {
73
- const p = flowContractPath(unitDir);
74
- if (!fs.existsSync(p)) return { contract: null, errors: [] };
75
- let raw: Record<string, unknown>;
76
- try {
77
- raw = parseYaml(readTextFile(p)) as Record<string, unknown>;
78
- } catch (e) {
79
- return { contract: null, errors: [`flow-contract.yaml does not parse: ${(e as Error).message}`] };
80
- }
81
- if (!raw || typeof raw !== 'object') return { contract: null, errors: ['flow-contract.yaml is empty'] };
82
- const errors: string[] = [];
83
- if (!raw.goal || typeof raw.goal !== 'string') errors.push('missing `goal:` (Verb + outcome, e.g. "Place an order for a product added from home")');
84
- const outcome = raw.outcome as { screen?: unknown; assertion?: unknown } | undefined;
85
- if (!outcome || typeof outcome.screen !== 'string' || !outcome.screen.trim()) {
86
- errors.push('missing `outcome.screen:` — the screen namespace that carries the final proof');
87
- }
88
- if (errors.length > 0) return { contract: null, errors };
89
- const phases = Array.isArray(raw.phases) && raw.phases.length > 0
90
- ? (raw.phases as unknown[]).map((x) => String(x).toUpperCase())
91
- : DEFAULT_PHASES;
92
- return {
93
- contract: {
94
- goal: String(raw.goal),
95
- actor: raw.actor !== undefined ? String(raw.actor) : undefined,
96
- trigger: raw.trigger !== undefined ? String(raw.trigger) : undefined,
97
- precondition: raw.precondition !== undefined ? String(raw.precondition) : undefined,
98
- outcome: { screen: String(outcome!.screen).toLowerCase(), assertion: outcome!.assertion !== undefined ? String(outcome!.assertion) : undefined },
99
- value: raw.value !== undefined ? String(raw.value) : undefined,
100
- phases,
101
- stateful: raw.stateful !== undefined ? String(raw.stateful).toLowerCase() : undefined,
102
- budgets: (raw.budgets && typeof raw.budgets === 'object') ? raw.budgets as Record<string, number> : undefined,
103
- },
104
- errors: [],
105
- };
106
- }
107
-
108
- /** `[screen:element]` namespaces referenced by a scenario's steps, in step order. */
109
- function namespacesInOrder(s: ScenarioInfo): string[] {
110
- const out: string[] = [];
111
- for (const m of s.stepsText.matchAll(/\[([a-z0-9_.-]+):/g)) out.push(m[1]);
112
- return out;
113
- }
114
-
115
- function touchesOutcome(s: ScenarioInfo, outcomeScreen: string): boolean {
116
- return namespacesInOrder(s).includes(outcomeScreen);
117
- }
118
-
119
- /** Phase of a scenario: its declared phase token (FL-HP-001 / VP-FLOW-ER-02 / MS-EH-005)
120
- * when present, else vocabulary detection. */
121
- export function phaseOf(s: ScenarioInfo, declared: string[]): string | null {
122
- const id = (s.vpId ?? '').toUpperCase();
123
- for (const ph of declared) {
124
- if (new RegExp(`(^|-)${ph}(-|$)`).test(id)) return ph;
125
- }
126
- const hay = s.haystack;
127
- if (declared.includes('EH') && /\b(direct access|without (a |the )?(submit|login)|browser back|refresh|expired|tamper|unauthoriz|redirect(ed)? (back )?to|guard)\b/.test(hay)) return 'EH';
128
- if (declared.includes('ER') && /\b(invalid|error|required|validation|malformed|blocked|then correct|recover)\b/.test(hay)) return 'ER';
129
- if (declared.includes('HP') && s.hasDataAssertion) return 'HP';
130
- return null;
131
- }
132
-
133
- /**
134
- * Verify the suite against the contract. Deterministic; a flow without a contract
135
- * returns hasContract:false and neutral values (the audit reports the checklist).
136
- */
137
- export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQualityResult {
138
- const { contract, errors } = loadFlowContract(unitDir);
139
- const neutral: FlowQualityResult = {
140
- hasContract: false, errors, outcomeProven: false, outcomeManualOnly: false,
141
- offGoal: [], offGoalRatio: 0, offGoalCategories: [],
142
- phases: [], phaseRatio: 1, handoffs: { total: 0, asserted: 0, ratio: 1 },
143
- };
144
- if (!contract) return neutral;
145
-
146
- const outcomeScreen = contract.outcome.screen;
147
-
148
- // --- Outcome proof: the flow proves its own goal, by automation -------------
149
- const proofs = scenarios.filter((s) => touchesOutcome(s, outcomeScreen) && s.hasDataAssertion);
150
- const outcomeProven = proofs.some((s) => !s.manual);
151
- const outcomeManualOnly = !outcomeProven && proofs.length > 0;
152
-
153
- // --- Scope creep: a scenario that never touches the outcome and is not a ----
154
- // guard/error phase is evidence of a SECOND business goal in this flow.
155
- const declaredPhases = contract.phases;
156
- const offGoalScenarios = scenarios.filter((s) => {
157
- if (touchesOutcome(s, outcomeScreen)) return false;
158
- const ph = phaseOf(s, declaredPhases);
159
- return ph !== 'EH' && ph !== 'ER'; // guards/error-recovery legitimately stop early
160
- });
161
- const offGoalRatio = scenarios.length ? offGoalScenarios.length / scenarios.length : 0;
162
- const offGoalCategories = Array.from(new Set(
163
- offGoalScenarios.map((s) => s.category ?? s.vpId?.replace(/-\d+.*$/, '') ?? '?')));
164
-
165
- // --- Phase coverage: the flow's coverage axis (UI never demanded) -----------
166
- const demanded = declaredPhases.filter((p) => p !== 'UI');
167
- const phases = demanded.map((phase) => {
168
- const inPhase = scenarios.filter((s) => phaseOf(s, declaredPhases) === phase);
169
- // HP must additionally prove the outcome — a data assertion elsewhere is not the goal.
170
- const relevant = phase === 'HP' ? inPhase.filter((s) => touchesOutcome(s, outcomeScreen)) : inPhase;
171
- return {
172
- phase,
173
- covered: relevant.length > 0,
174
- automated: relevant.some((s) => !s.manual),
175
- };
176
- });
177
- const phaseRatio = demanded.length
178
- ? phases.filter((p) => p.covered && p.automated).length / demanded.length
179
- : 1;
180
-
181
- // --- Handoff integrity: no blind tail after a cross-namespace transition. ---
182
- // A transition counts as asserted when ANY assertion follows it — in the entered
183
- // namespace or later. Demanding the assertion in the entered namespace itself
184
- // flagged two legitimate shapes: a guard that asserts the REDIRECT target
185
- // ("go to [Checkout] → see [Home] page"), and a passthrough click en route
186
- // ("click [Cart:Checkout]" asserting on the next screen). What the sensor
187
- // actually guards against is a flow that clicks through screens and ends blind.
188
- let total = 0; let asserted = 0;
189
- for (const s of scenarios) {
190
- if (s.manual) continue;
191
- const steps = s.steps ?? [];
192
- let current: string | null = null;
193
- for (let i = 0; i < steps.length; i++) {
194
- const ns = (steps[i].text.match(/\[([A-Za-z0-9_.-]+):/) || [])[1]?.toLowerCase() ?? null;
195
- if (!ns) continue;
196
- if (current !== null && ns !== current) {
197
- total++;
198
- if (steps.slice(i).some((st) => st.bucket === 'then')) asserted++;
199
- }
200
- current = ns;
201
- }
202
- }
203
- const handoffs = { total, asserted, ratio: total ? asserted / total : 1 };
204
-
205
- return {
206
- hasContract: true, contract, errors: [],
207
- outcomeProven, outcomeManualOnly,
208
- offGoal: offGoalScenarios.map((s) => s.name.slice(0, 80)),
209
- offGoalRatio, offGoalCategories,
210
- phases, phaseRatio, handoffs,
211
- };
212
- }
213
-
214
- /**
215
- * Generalized stateful regression depth: the contract names the mutated collection,
216
- * so the three dims (count-proof · teardown · multi-source) stop being cart-only.
217
- */
218
- export function statefulDepthFor(collection: string, scenarios: ScenarioInfo[]): { countProof: boolean; teardown: boolean; multiSource: boolean; missing: string[]; ratio: number } {
219
- const hay = scenarios.map((s) => s.haystack);
220
- const any = (re: RegExp) => hay.some((h) => re.test(h));
221
- const noun = collection.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
222
- const countProof = any(new RegExp(`\\b(quantity|qty|row count|count|number of|two (rows|lines|items)|\\d+ (rows|lines|items))\\b`)) ;
223
- const teardown = any(/\b(remove|delete|clear|cancel|withdraw)(?:s|d|ed|ing|n)?\b/) && any(new RegExp(`\\b(empty|emptied|no items|zero|removed|cleared|cancelled|withdrawn|0 items)\\b|empty[- ]${noun}`));
224
- const adds = hay.filter((h) => new RegExp(`\\b(add|submit|create|place).{0,40}${noun}|${noun}.{0,40}\\b(add|submit|create|place)`).test(h));
225
- const multiSource = any(/\b(recommended|related|you may also|another source|both sources|second (list|source))\b/) && adds.length > 0;
226
- const dims: Array<[string, boolean]> = [['count-proof', countProof], ['teardown', teardown], ['multi-source', multiSource]];
227
- const missing = dims.filter(([, v]) => !v).map(([k]) => k);
228
- return { countProof, teardown, multiSource, missing, ratio: (dims.length - missing.length) / dims.length };
229
- }
@@ -1,112 +0,0 @@
1
- /**
2
- * Performance budgets — config + percentile math + the per-unit verdict. (#569)
3
- *
4
- * A flow's regression value includes "still fast enough": after a lib/framework
5
- * upgrade the main journeys must not only pass but hold their response-time
6
- * budget. Sungen had no perf concept at all — the Playwright JSON parser even
7
- * dropped the `duration` field Playwright already emits on every result.
8
- *
9
- * Scope discipline:
10
- * - This is config + measurement + report over runs sungen already makes.
11
- * Real load tests stay @manual:M8 → a dedicated tool.
12
- * - The AUDIT never reads it: the quality score is documented as a pure
13
- * function of the design artifacts ("reads no test-results, live page, or
14
- * clock"). Perf reports where runs are already read — `sungen delivery`
15
- * and the dashboard. Advisory: a blown budget never fails the design gate.
16
- *
17
- * Config: qa/perf.yaml
18
- * percentile: p75 # default p75 — "≥75% of runs meet the budget"
19
- * defaults:
20
- * scenario_ms: 30000 # whole-scenario wall clock (Playwright duration)
21
- * page_load_ms: 3000 # Phase B — needs per-transition runtime timing
22
- * transition_ms: 2000 # Phase B
23
- * units:
24
- * place-order: { scenario_ms: 20000 }
25
- */
26
- import * as fs from 'fs';
27
- import * as path from 'path';
28
- import { parse as parseYaml } from 'yaml';
29
- import { readTextFile } from './read-text';
30
-
31
- export interface PerfConfig {
32
- /** 0..100 — e.g. 75 for p75. */
33
- percentile: number;
34
- defaults: Record<string, number>;
35
- units: Record<string, Record<string, number>>;
36
- }
37
-
38
- export interface PerfVerdict {
39
- unit: string;
40
- metric: string; // 'scenario_ms' today; page_load_ms/transition_ms in Phase B
41
- percentile: number; // 75
42
- budgetMs: number;
43
- measuredMs: number; // the pXX of the observed durations
44
- samples: number;
45
- pass: boolean;
46
- /** Titles of the slowest offenders (only when failing), for the report. */
47
- slowest: Array<{ title: string; ms: number }>;
48
- }
49
-
50
- export function perfConfigPath(projectRoot: string): string {
51
- return path.join(projectRoot, 'qa', 'perf.yaml');
52
- }
53
-
54
- /** Absent file → null (perf reporting is opt-in; nothing changes until configured). */
55
- export function loadPerfConfig(projectRoot: string): PerfConfig | null {
56
- const p = perfConfigPath(projectRoot);
57
- if (!fs.existsSync(p)) return null;
58
- let raw: Record<string, unknown>;
59
- try { raw = parseYaml(readTextFile(p)) as Record<string, unknown>; } catch { return null; }
60
- if (!raw || typeof raw !== 'object') return null;
61
- const pctRaw = String(raw.percentile ?? 'p75').toLowerCase().replace(/^p/, '');
62
- const percentile = Math.min(100, Math.max(1, Number(pctRaw) || 75));
63
- const num = (o: unknown): Record<string, number> => {
64
- const out: Record<string, number> = {};
65
- if (o && typeof o === 'object') {
66
- for (const [k, v] of Object.entries(o as Record<string, unknown>)) {
67
- const n = Number(v);
68
- if (Number.isFinite(n) && n > 0) out[k] = n;
69
- }
70
- }
71
- return out;
72
- };
73
- const units: Record<string, Record<string, number>> = {};
74
- if (raw.units && typeof raw.units === 'object') {
75
- for (const [u, o] of Object.entries(raw.units as Record<string, unknown>)) units[u] = num(o);
76
- }
77
- return { percentile, defaults: num(raw.defaults), units };
78
- }
79
-
80
- /**
81
- * Nearest-rank percentile (ceil), the standard "≥pXX of samples meet the budget"
82
- * reading: p75 of [a…] is the value at ceil(0.75·n) in the sorted list. One
83
- * sample → that sample. Deterministic, no interpolation.
84
- */
85
- export function percentileOf(p: number, values: number[]): number {
86
- if (values.length === 0) return 0;
87
- const sorted = [...values].sort((a, b) => a - b);
88
- const rank = Math.min(sorted.length, Math.max(1, Math.ceil((p / 100) * sorted.length)));
89
- return sorted[rank - 1];
90
- }
91
-
92
- /** Budget for a metric on a unit: per-unit override, else defaults, else none. */
93
- export function budgetFor(config: PerfConfig, unit: string, metric: string): number | undefined {
94
- return config.units[unit]?.[metric] ?? config.defaults[metric];
95
- }
96
-
97
- /**
98
- * The scenario_ms verdict for one unit's run. `durations` = per-test wall-clock ms
99
- * (a @cases scenario contributes one sample per row-test — each is a real run).
100
- */
101
- export function perfVerdict(
102
- config: PerfConfig,
103
- unit: string,
104
- samples: Array<{ title: string; ms: number }>,
105
- ): PerfVerdict | null {
106
- const budgetMs = budgetFor(config, unit, 'scenario_ms');
107
- if (budgetMs === undefined || samples.length === 0) return null;
108
- const measuredMs = percentileOf(config.percentile, samples.map((s) => s.ms));
109
- const pass = measuredMs <= budgetMs;
110
- const slowest = pass ? [] : [...samples].sort((a, b) => b.ms - a.ms).slice(0, 3);
111
- return { unit, metric: 'scenario_ms', percentile: config.percentile, budgetMs, measuredMs, samples: samples.length, pass, slowest };
112
- }