@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.
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +0 -8
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +0 -7
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +0 -9
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +0 -11
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +0 -15
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +0 -2
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +0 -1
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/types.d.ts +0 -2
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +1 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +0 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +19 -27
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +0 -22
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/harness/audit.d.ts +0 -2
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +9 -79
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/flow-plan.d.ts +0 -3
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +2 -6
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/parse.d.ts +0 -5
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +1 -29
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +1 -13
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +5 -29
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +3 -41
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +0 -10
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +16 -35
- package/dist/orchestrator/templates/qa-context.md +1 -14
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +0 -8
- package/src/cli/commands/delivery.ts +0 -6
- package/src/cli/commands/trace.ts +0 -9
- package/src/exporters/matrix/export.ts +0 -11
- package/src/exporters/matrix/render-xlsx.ts +0 -15
- package/src/exporters/matrix/types.ts +0 -2
- package/src/exporters/playwright-report-parser.ts +0 -2
- package/src/exporters/types.ts +0 -2
- package/src/generators/test-generator/diagnostics.ts +1 -6
- package/src/generators/test-generator/patterns/index.ts +24 -30
- package/src/generators/test-generator/step-mapper.ts +0 -22
- package/src/harness/audit.ts +10 -82
- package/src/harness/flow-plan.ts +3 -10
- package/src/harness/parse.ts +1 -31
- package/src/harness/sensors.ts +1 -13
- package/src/harness/spec-coverage.ts +4 -26
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +3 -41
- package/src/orchestrator/templates/ai-src/commands/create-test.md +0 -10
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +16 -35
- package/src/orchestrator/templates/qa-context.md +1 -14
- package/dist/harness/flow-contract.d.ts +0 -71
- package/dist/harness/flow-contract.d.ts.map +0 -1
- package/dist/harness/flow-contract.js +0 -235
- package/dist/harness/flow-contract.js.map +0 -1
- package/dist/harness/perf.d.ts +0 -40
- package/dist/harness/perf.d.ts.map +0 -1
- package/dist/harness/perf.js +0 -136
- package/dist/harness/perf.js.map +0 -1
- package/src/harness/flow-contract.ts +0 -229
- package/src/harness/perf.ts +0 -112
package/src/harness/audit.ts
CHANGED
|
@@ -10,11 +10,10 @@ import * as path from 'path';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { loadUnitScenarios, readUnitFeatureText, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
12
12
|
import { checkViewpointBaseline, ViewpointBaseline } from './viewpoint-baseline';
|
|
13
|
-
import { flowQuality, statefulDepthFor, FlowQualityResult } from './flow-contract';
|
|
14
13
|
import { featureFilesFor } from './unit-paths';
|
|
15
14
|
import {
|
|
16
15
|
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
17
|
-
automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength,
|
|
16
|
+
automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength,
|
|
18
17
|
GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog, AutomatableManualResult, FlowDepthResult, OracleStrengthResult,
|
|
19
18
|
} from './sensors';
|
|
20
19
|
import { loadFlowScenarios } from './flow-check';
|
|
@@ -49,7 +48,6 @@ export interface AuditReport {
|
|
|
49
48
|
oracle: OracleStrengthResult; // H4 — facet-oracle strength (weak name-substring vs title/detail/API/DB)
|
|
50
49
|
ledger: LedgerResult; // atomic viewpoint-item coverage (per-bullet status)
|
|
51
50
|
viewpointBaseline: ViewpointBaseline; // is the yardstick still the accepted one? (#557)
|
|
52
|
-
flowQuality?: FlowQualityResult; // #569 — flow contract verification (flows only)
|
|
53
51
|
calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
|
|
54
52
|
axes: Record<string, number>;
|
|
55
53
|
weakest: { axis: string; value: number };
|
|
@@ -156,28 +154,15 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
156
154
|
// back to the in-core functions if the capability provides none.
|
|
157
155
|
// A capability gate may need project context (the API gate resolves endpoint methods from the
|
|
158
156
|
// catalog) + the focus depth threshold (so it scores depth with the SAME bar as the UI gate).
|
|
157
|
+
const capGate = scoringCap?.gateProvider as
|
|
158
|
+
((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
|
|
159
|
+
const provided = capGate?.({ scenarios, viewpoints, catalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
|
|
159
160
|
// Is the declaration this suite is measured against still the one a human accepted?
|
|
160
161
|
const viewpointBaseline = checkViewpointBaseline(
|
|
161
162
|
projectRootFromScreenDir(screenDir), catalogScreenName, viewpointPath);
|
|
162
163
|
const viewpointText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// flow judged as a `form` screen scored coverage 0%), so for contract-holding flows the
|
|
166
|
-
// page-type machinery only applies when the project DECLARED a page type explicitly.
|
|
167
|
-
// The stripped catalog feeds the CAPABILITY gate too — driver-ui provides the gate on
|
|
168
|
-
// real projects, and handing it the unstripped catalog re-introduced the force-fit.
|
|
169
|
-
const isUiFlowUnit = /^flows\//.test(catalogScreenName);
|
|
170
|
-
const flowQ: FlowQualityResult = isUiFlowUnit
|
|
171
|
-
? flowQuality(screenDir, scenarios)
|
|
172
|
-
: { hasContract: false, errors: [], outcomeProven: false, outcomeManualOnly: false, offGoal: [], offGoalRatio: 0, offGoalCategories: [], phases: [], phaseRatio: 1, handoffs: { total: 0, asserted: 0, ratio: 1 } };
|
|
173
|
-
const gateCatalog = (flowQ.hasContract && !declaredPageType(viewpointText, catalog))
|
|
174
|
-
? { ...catalog, page_types: {} } as Catalog
|
|
175
|
-
: catalog;
|
|
176
|
-
const capGate = scoringCap?.gateProvider as
|
|
177
|
-
((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
|
|
178
|
-
const provided = capGate?.({ scenarios, viewpoints, catalog: gateCatalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
|
|
179
|
-
const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, gateCatalog, platform === 'mobile', viewpointText);
|
|
180
|
-
const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(gateCatalog, gate.pageType), intent.focus);
|
|
164
|
+
const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog, platform === 'mobile', viewpointText);
|
|
165
|
+
const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
|
|
181
166
|
|
|
182
167
|
// TQ-4 — deferral-aware coverage credit: an inherently cross-screen theme (cart / detail /
|
|
183
168
|
// filter correctness) belongs in a FLOW, not on the screen. When a flow deeply covers a screen
|
|
@@ -215,15 +200,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
215
200
|
// collection, the regression dimensions (count/quantity proof · teardown · multi-source) cap the
|
|
216
201
|
// businessDepth headroom: it can reach 1.0 only when all three are exercised, so a present-but-
|
|
217
202
|
// shallow flow can't claim a perfect score (floor 0.5 — assertion depth still dominates).
|
|
218
|
-
const isUiFlow =
|
|
219
|
-
|
|
220
|
-
// Contract-declared statefulness generalizes the cart-hardcoded vocabulary: a flow that
|
|
221
|
-
// mutates ANY named collection (order, application, submission …) gets the same three
|
|
222
|
-
// regression dims. The legacy cart detection stays for contract-less flows (snapshots).
|
|
223
|
-
if (isUiFlow && !flowDepth.stateful && flowQ.contract?.stateful) {
|
|
224
|
-
const d = statefulDepthFor(flowQ.contract.stateful, scenarios);
|
|
225
|
-
flowDepth = { stateful: true, countProof: d.countProof, teardown: d.teardown, multiSource: d.multiSource, ratio: d.ratio, missing: d.missing };
|
|
226
|
-
}
|
|
203
|
+
const isUiFlow = /^flows\//.test(catalogScreenName);
|
|
204
|
+
const flowDepth = isUiFlow ? flowRegressionDepth(scenarios) : { stateful: false, countProof: false, teardown: false, multiSource: false, ratio: 1, missing: [] } as FlowDepthResult;
|
|
227
205
|
const FLOW_DEPTH_FLOOR = 0.5;
|
|
228
206
|
// H4 — oracle strength: a weak facet oracle (name-substring "proves" category/brand membership)
|
|
229
207
|
// caps businessDepth the same way (floor 0.5). A suite with no facet claim, or a strong oracle, is
|
|
@@ -233,14 +211,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
233
211
|
|
|
234
212
|
// Sub-scores
|
|
235
213
|
const coverage = gate.coverageRatio;
|
|
236
|
-
// #569 — handoff integrity: a flow's depth is also whether each cross-screen transition
|
|
237
|
-
// is followed by an assertion on the carried state (same floor discipline as the other caps).
|
|
238
|
-
const HANDOFF_FLOOR = 0.5;
|
|
239
214
|
const businessDepth = Math.min(
|
|
240
215
|
depth.bcDepthRatio,
|
|
241
216
|
flowDepth.stateful ? FLOW_DEPTH_FLOOR + (1 - FLOW_DEPTH_FLOOR) * flowDepth.ratio : 1,
|
|
242
217
|
oracle.weak.length ? ORACLE_FLOOR + (1 - ORACLE_FLOOR) * oracle.ratio : 1,
|
|
243
|
-
flowQ.hasContract && flowQ.handoffs.total > 0 ? HANDOFF_FLOOR + (1 - HANDOFF_FLOOR) * flowQ.handoffs.ratio : 1,
|
|
244
218
|
);
|
|
245
219
|
// When the taxonomy drifted (most scenarios unclassified), the balance axis is unreliable — cap it
|
|
246
220
|
// at 0.5 instead of awarding a vacuous 1.0 so a stale taxonomy fails loudly, not silently (H1).
|
|
@@ -272,15 +246,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
272
246
|
// what a create-test run did, silently dropping the performance viewpoint on
|
|
273
247
|
// the way. While the declaration is unconfirmed, neither axis is evidence.
|
|
274
248
|
const viewpointMoved = viewpointBaseline.status === 'changed';
|
|
275
|
-
// 4. A flow with a contract is measured on ITS coverage: journey phases (HP/ER/EH
|
|
276
|
-
// automated) — not a screen's page-type themes. Same weight, same criticality,
|
|
277
|
-
// so a flow can finally EARN the coverage axis instead of always losing it to
|
|
278
|
-
// PAGE-TYPE-UNDETERMINED (or worse, being judged against a form checklist).
|
|
279
|
-
const flowScored = flowQ.hasContract;
|
|
280
249
|
const axisDefs: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean }> = [
|
|
281
|
-
|
|
282
|
-
? { key: 'flowCoverage', value: flowQ.phaseRatio, weight: 0.22, applicable: true, critical: true }
|
|
283
|
-
: { key: 'coverage', value: coverage, weight: 0.22, applicable: !!gate.pageType && gate.themesTotal > 0, critical: true },
|
|
250
|
+
{ key: 'coverage', value: coverage, weight: 0.22, applicable: !!gate.pageType && gate.themesTotal > 0, critical: true },
|
|
284
251
|
{ key: 'specFR', value: specRatio, weight: 0.15, applicable: spec.hasSpec && spec.frTotal > 0, critical: true },
|
|
285
252
|
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true },
|
|
286
253
|
{ key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
|
|
@@ -330,40 +297,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
330
297
|
].filter(Boolean).join(', ');
|
|
331
298
|
findings.push(`VIEWPOINT-BASELINE-CHANGED: test-viewpoint.md no longer declares what it did when this unit was last accepted — ${moved || 'the declared ids were reordered or replaced'}. atomicLedger + traceability measure the suite AGAINST this file, so they are excluded from the score until the change is confirmed: a generator that rewrites the declaration scores both 100% by construction, and a viewpoint dropped from the file stops being missing from anything. Review the diff (a removed id means that coverage is now unclaimed), then run \`sungen audit --screen ${screenName} --accept-viewpoint\`.`);
|
|
332
299
|
}
|
|
333
|
-
|
|
334
|
-
if (gate.pageTypeSource === 'undetermined' && !flowScored) {
|
|
300
|
+
if (gate.pageTypeSource === 'undetermined') {
|
|
335
301
|
findings.push(`PAGE-TYPE-UNDETERMINED: no page type fits this screen with enough confidence (best ${gate.pageTypeEvidence?.hits ?? 0} keyword hit(s)), so NO critical themes were demanded and the coverage axis is excluded from the score — declare it in test-viewpoint.md (\`page-type: <id>\`) to have the theme checklist applied.`);
|
|
336
302
|
}
|
|
337
|
-
// ---- #569: flow-quality findings -----------------------------------------
|
|
338
|
-
if (isUiFlow && !flowQ.hasContract) {
|
|
339
|
-
const why = flowQ.errors.length ? ` (${flowQ.errors.join('; ')})` : '';
|
|
340
|
-
findings.push(`FLOW-CONTRACT-MISSING: this flow declares no contract${why} — without \`requirements/flow-contract.yaml\` (goal · trigger · outcome.screen · phases) the flow cannot be scored on ITS coverage (journey phases) and falls back to screen machinery. Author it with the boundary checklist: one business goal, a clear trigger, ONE observable outcome valuable to the actor, name = "Verb + outcome"; split anything that forms an independently valuable flow.`);
|
|
341
|
-
}
|
|
342
|
-
if (flowQ.hasContract) {
|
|
343
|
-
if (!flowQ.outcomeProven) {
|
|
344
|
-
findings.push(flowQ.outcomeManualOnly
|
|
345
|
-
? `FLOW-OUTCOME-UNPROVEN: the goal "${flowQ.contract!.goal}" is covered only by @manual scenarios — the flow's own outcome (\`${flowQ.contract!.outcome.screen}\`) has no AUTOMATED data assertion, so a regression run cannot prove the journey still works. Automate the happy-path proof first; it is the reason this flow exists.`
|
|
346
|
-
: `FLOW-OUTCOME-UNPROVEN: no scenario asserts data on the outcome screen \`${flowQ.contract!.outcome.screen}\` — the flow never proves its own goal ("${flowQ.contract!.goal}"). Add the happy-path scenario that ends in the declared final assertion.`);
|
|
347
|
-
}
|
|
348
|
-
if (flowQ.offGoalRatio > 0.35) {
|
|
349
|
-
findings.push(`FLOW-SCOPE-CREEP: ${flowQ.offGoal.length}/${scenarios.length} scenarios never touch the outcome screen \`${flowQ.contract!.outcome.screen}\` and are not guards/error-recovery (categories: ${flowQ.offGoalCategories.join(', ')}) — evidence of a SECOND business goal in this flow. Split them into their own flow (one flow = one goal, one observable outcome).`);
|
|
350
|
-
}
|
|
351
|
-
for (const ph of flowQ.phases.filter((p) => !p.covered || !p.automated)) {
|
|
352
|
-
findings.push(`FLOW-PHASE-${ph.phase}-MISSING: journey phase ${ph.phase} (${ph.phase === 'HP' ? 'happy path proving the outcome' : ph.phase === 'ER' ? 'error recovery — validation must not trap the journey' : 'guards — direct access / back / refresh'}) is ${ph.covered ? 'covered only by @manual' : 'not covered'} → it does not count toward flowCoverage until automated.`);
|
|
353
|
-
}
|
|
354
|
-
if (flowQ.handoffs.total > 0 && flowQ.handoffs.ratio < 0.7) {
|
|
355
|
-
findings.push(`FLOW-HANDOFF-SHALLOW: only ${flowQ.handoffs.asserted}/${flowQ.handoffs.total} cross-screen transitions are followed by an assertion in the new screen — carried state (the thing a flow exists to verify) is unproven at the handoff. businessDepth is capped until asserted.`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if (isUiFlow) {
|
|
359
|
-
// Flows defer more than screens, so an UNCODED @manual is costlier here: the capability
|
|
360
|
-
// planner cannot route it to the driver that would automate it (password-reset: 11/22
|
|
361
|
-
// manual, most of them M5-inbox — invisible while tagged bare).
|
|
362
|
-
const bareManual = (featureText.match(/@manual(?![:\w])/g) || []).length;
|
|
363
|
-
if (bareManual > 0) {
|
|
364
|
-
findings.push(`MANUAL-CODE-MISSING: ${bareManual} @manual tag(s) carry no reason code — tag @manual:M1..M9 (M5 = external dependency e.g. inbox, M3 = mock/network, M7 = environment) so the capability planner can route what a driver would automate.`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
303
|
for (const c of flowCredits) {
|
|
368
304
|
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\`.`);
|
|
369
305
|
}
|
|
@@ -440,13 +376,6 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
440
376
|
const shown = spec.inferredOnly.slice(0, 8).join(', ');
|
|
441
377
|
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.`);
|
|
442
378
|
}
|
|
443
|
-
// Safety net for the parser (#572): the feature CITES requirement ids the spec parser
|
|
444
|
-
// could not find a declaration for. Silence here would repeat the original failure —
|
|
445
|
-
// "nothing found" quietly excluding the specFR axis while the evidence sits in the file.
|
|
446
|
-
if (spec.hasSpec && spec.frTotal === 0 && /@spec:/i.test(featureText)) {
|
|
447
|
-
const cited = [...new Set([...featureText.matchAll(/@spec:([A-Za-z0-9.-]+)/g)].map((m) => m[1].toUpperCase()))];
|
|
448
|
-
findings.push(`SPEC-IDS-UNPARSED: the feature cites ${cited.length} requirement id(s) via @spec: (${cited.slice(0, 5).join(', ')}${cited.length > 5 ? ', …' : ''}) but requirements/spec.md yielded ZERO parseable requirement declarations — the spec likely declares ids in a scheme the parser cannot read. Declare each requirement on its own line as \`<ID>: <clause>\` (bullet/bold/backtick all fine; the id must end in a number) so the MUST-coverage gate and the specFR axis can run.`);
|
|
449
|
-
}
|
|
450
379
|
if (taxonomyMismatch) {
|
|
451
380
|
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.`);
|
|
452
381
|
}
|
|
@@ -556,7 +485,6 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
556
485
|
scenarioCount: scenarios.length,
|
|
557
486
|
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
558
487
|
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
|
|
559
|
-
...(isUiFlow ? { flowQuality: flowQ } : {}),
|
|
560
488
|
score: {
|
|
561
489
|
overall: Math.round(overall * 10) / 10,
|
|
562
490
|
coverage: Math.round(coverage * 100) / 100,
|
package/src/harness/flow-plan.ts
CHANGED
|
@@ -71,9 +71,6 @@ export interface FlowPlan {
|
|
|
71
71
|
legs: LegPlan[];
|
|
72
72
|
byReason: Record<string, number>;
|
|
73
73
|
capabilityManual: number;
|
|
74
|
-
/** @manual whose reason is "cross-screen → automate via flow" (class XS) — inside a flow this
|
|
75
|
-
* usually means the scenario should simply BE automated here. (#569) */
|
|
76
|
-
crossScreenManual: number;
|
|
77
74
|
judgmentManual: number;
|
|
78
75
|
contracts: Contract[];
|
|
79
76
|
readiness: 'ready' | 'not-ready';
|
|
@@ -90,18 +87,14 @@ export function buildFlowPlan(cwd: string, flow: string): FlowPlan {
|
|
|
90
87
|
// Legs = distinct screen namespaces.
|
|
91
88
|
const legMap = new Map<string, { scenarios: Set<string>; refs: Set<string>; automated: boolean }>();
|
|
92
89
|
const byReason: Record<string, number> = {};
|
|
93
|
-
let capabilityManual = 0, judgmentManual = 0
|
|
90
|
+
let capabilityManual = 0, judgmentManual = 0;
|
|
94
91
|
|
|
95
92
|
for (const sc of scenarios) {
|
|
96
93
|
if (sc.manual) {
|
|
97
94
|
const { code } = inferReasonCode(sc.tags, sc.reason);
|
|
98
95
|
byReason[code] = (byReason[code] || 0) + 1;
|
|
99
96
|
const cls = MANUAL_REASONS[code]?.cls;
|
|
100
|
-
|
|
101
|
-
// dropped from both counters, understating the plan's manual load. (#569)
|
|
102
|
-
if (cls === 'capability') capabilityManual++;
|
|
103
|
-
else if (cls === 'keep') judgmentManual++;
|
|
104
|
-
else if (cls === 'flow') crossScreenManual++;
|
|
97
|
+
if (cls === 'capability') capabilityManual++; else if (cls === 'keep') judgmentManual++;
|
|
105
98
|
}
|
|
106
99
|
for (const r of sc.refs) {
|
|
107
100
|
const leg = r.screen.toLowerCase();
|
|
@@ -140,5 +133,5 @@ export function buildFlowPlan(cwd: string, flow: string): FlowPlan {
|
|
|
140
133
|
}
|
|
141
134
|
if (readiness === 'ready') plan.unshift('Selectors present for every automated leg — ready to compile + run.');
|
|
142
135
|
|
|
143
|
-
return { flow, total: scenarios.length, legs, byReason, capabilityManual, judgmentManual,
|
|
136
|
+
return { flow, total: scenarios.length, legs, byReason, capabilityManual, judgmentManual, contracts, readiness, missingLegs, plan };
|
|
144
137
|
}
|
package/src/harness/parse.ts
CHANGED
|
@@ -38,8 +38,6 @@ export interface ScenarioInfo {
|
|
|
38
38
|
requiresCaps?: string[]; // @requires:<cap> — automation-ready but needs an opt-in driver (TQ-11)
|
|
39
39
|
deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
|
|
40
40
|
ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
|
|
41
|
-
/** Ordered steps with their resolved bucket (And/But inherit) — flow handoff analysis (#569). */
|
|
42
|
-
steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string }>;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
/** Format-tolerant: is this token an ID (project's scheme), not a prose word?
|
|
@@ -93,29 +91,6 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
// 1b) Flow-style declarations (#569). Flow viewpoint files commonly declare per-item
|
|
97
|
-
// ids at the END of a bullet ("… → **FL-HP-001**") under phase section headers
|
|
98
|
-
// ("## FL-HP — Happy Path"). Neither matched the table/group passes, so every flow
|
|
99
|
-
// audit collapsed to taxonomy=0% / traceability n-a — scenarios correctly tagged
|
|
100
|
-
// FL-HP-001 were reported as unmapped. Both forms are additive here.
|
|
101
|
-
for (const raw of lines) {
|
|
102
|
-
const line = raw.trim();
|
|
103
|
-
const section = line.match(/^##\s+([A-Z]{2,}(?:-[A-Z0-9]{2,})*)\s+[—–-]\s*(.*)$/);
|
|
104
|
-
if (section && isViewpointId(section[1] + '-0')) {
|
|
105
|
-
const id = section[1].toUpperCase();
|
|
106
|
-
if (!entries.has(id)) entries.set(id, { id, priority: 'Unknown', reason: section[2] ?? '' });
|
|
107
|
-
}
|
|
108
|
-
if (/^[-*+]\s/.test(line)) {
|
|
109
|
-
const arrow = line.match(/(?:→|->)\s*\*{0,2}([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+[a-zA-Z]?)\*{0,2}\s*$/);
|
|
110
|
-
if (arrow) {
|
|
111
|
-
const id = arrow[1].toUpperCase();
|
|
112
|
-
if (!entries.has(id)) {
|
|
113
|
-
entries.set(id, { id, priority: 'Unknown', reason: line.replace(/\s*(?:→|->).*$/, '').replace(/^[-*+]\s+/, '') });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
94
|
// 2) Viewpoint Grouping: ### Required / ### Recommended / ### Optional → bullet list
|
|
120
95
|
let group: ViewpointEntry['group'] | undefined;
|
|
121
96
|
for (const raw of lines) {
|
|
@@ -169,9 +144,7 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
169
144
|
// Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
|
|
170
145
|
// compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
|
|
171
146
|
// not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
|
|
172
|
-
|
|
173
|
-
// flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
|
|
174
|
-
const codeMatch = sc.name.match(/\b(?:VP|FL)-([A-Z]+(?:-[A-Z]+)*)-\d+/i);
|
|
147
|
+
const codeMatch = sc.name.match(/\bVP-([A-Z]+(?:-[A-Z]+)*)-\d+/i);
|
|
175
148
|
const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
|
|
176
149
|
const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
|
|
177
150
|
// Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
|
|
@@ -185,14 +158,12 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
185
158
|
const skeletonParts: string[] = [];
|
|
186
159
|
const textParts: string[] = [sc.name];
|
|
187
160
|
const stepTextParts: string[] = [];
|
|
188
|
-
const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string }> = [];
|
|
189
161
|
|
|
190
162
|
for (const step of sc.steps as ParsedStep[]) {
|
|
191
163
|
const kw = step.keyword.trim();
|
|
192
164
|
if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
|
|
193
165
|
textParts.push(step.text);
|
|
194
166
|
stepTextParts.push(step.text);
|
|
195
|
-
orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text });
|
|
196
167
|
// normalized skeleton: keep [refs] (distinct targets = distinct tests),
|
|
197
168
|
// but neutralize {{vars}} and quoted values so EP/data families collapse.
|
|
198
169
|
const skel = step.text
|
|
@@ -228,7 +199,6 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
228
199
|
stepSkeleton: skeletonParts.join(' | '),
|
|
229
200
|
haystack: textParts.join(' ').toLowerCase(),
|
|
230
201
|
stepsText: stepTextParts.join(' ').toLowerCase(),
|
|
231
|
-
steps: orderedSteps,
|
|
232
202
|
vpId,
|
|
233
203
|
casesDataset,
|
|
234
204
|
queryRefs: queryRefs.size ? [...queryRefs] : undefined,
|
package/src/harness/sensors.ts
CHANGED
|
@@ -33,22 +33,10 @@ const BUCKET_ORDER: Array<[string, string[]]> = [
|
|
|
33
33
|
];
|
|
34
34
|
const BUCKETS: Record<string, string[]> = Object.fromEntries(BUCKET_ORDER);
|
|
35
35
|
|
|
36
|
-
// Flow journey-phase categories (FL-HP-001, FL-ER-002 …). Matched on exact SEGMENTS,
|
|
37
|
-
// never by containment — 'SHOP'.includes('HP') is true, which is exactly the kind of
|
|
38
|
-
// false hit substring matching would produce for two-letter phase tokens. (#569)
|
|
39
|
-
const PHASE_BUCKETS: Record<string, string> = {
|
|
40
|
-
HP: 'business-core', // happy path = the business goal itself
|
|
41
|
-
ER: 'validation-security', // error recovery (validation must not trap the journey)
|
|
42
|
-
EH: 'validation-security', // guards & leakage (direct access, back, refresh)
|
|
43
|
-
};
|
|
44
|
-
|
|
45
36
|
/** Classify a VP category into a balance bucket by keyword containment + precedence (H1). */
|
|
46
37
|
export function bucketForCategory(category: string | undefined): string {
|
|
47
38
|
const cat = (category || '').toUpperCase();
|
|
48
39
|
if (!cat) return 'other';
|
|
49
|
-
for (const seg of cat.split('-')) {
|
|
50
|
-
if (PHASE_BUCKETS[seg]) return PHASE_BUCKETS[seg];
|
|
51
|
-
}
|
|
52
40
|
for (const [bucket, kws] of BUCKET_ORDER) {
|
|
53
41
|
if (kws.some((k) => cat.includes(k))) return bucket;
|
|
54
42
|
}
|
|
@@ -363,7 +351,7 @@ export function flowRegressionDepth(scenarios: ScenarioInfo[]): FlowDepthResult
|
|
|
363
351
|
// 1. Count/quantity proof — a row count or item quantity, not just presence of a row.
|
|
364
352
|
const countProof = any(/\b(quantity|qty|two (?:rows|lines|cart)|row count|count column|number of items|one[_ ]row|two[_ ]rows|qty[_ ])/i);
|
|
365
353
|
// 2. Teardown — removes the item and verifies the empty/zero state (the inverse operation).
|
|
366
|
-
const teardown = any(/\b(remove|delete|clear)
|
|
354
|
+
const teardown = any(/\b(remove|delete|clear)\b/i) && any(/\b(empty|no items|zero|removed|0 items)\b/i);
|
|
367
355
|
// 3. Multi-source — the cart is fed from >1 source (the main list AND a recommended/related rail).
|
|
368
356
|
const multiSource = any(/\b(recommended|related|you may also|suggest)\b/i) && addsToCart;
|
|
369
357
|
|
|
@@ -66,43 +66,21 @@ export function parseSpecClauses(specPath: string): { frs: FrClause[]; valRows:
|
|
|
66
66
|
if (!fs.existsSync(specPath)) return { frs: [], valRows: [] };
|
|
67
67
|
const lines = readTextFile(specPath).split('\n');
|
|
68
68
|
|
|
69
|
-
// Requirement ids follow the PROJECT's scheme, not ours (#572): `**FR-1**:` is one
|
|
70
|
-
// convention among many — a real spec declared ~30 MUST clauses as `` `REQ-SRCH-001`: ``
|
|
71
|
-
// and the FR-locked pattern returned zero, so the MUST-coverage gate never ran and the
|
|
72
|
-
// specFR axis was excluded "for lack of evidence" that was sitting right there. Same
|
|
73
|
-
// silent-failure class as the CRLF parsers, same id-scheme-tolerance lesson as delivery.
|
|
74
|
-
//
|
|
75
|
-
// A declaration is: line-leading (optionally bulleted / bold / backticked) `<ID>:` where
|
|
76
|
-
// the id ends in a number. Table rows are EXCLUDED — traceability tables cite requirement
|
|
77
|
-
// ids without declaring them — and so are prefixes that are never requirements
|
|
78
|
-
// (test cases, viewpoints, known-defect records, data-factory checks, flows, delivery items).
|
|
79
|
-
const NON_REQUIREMENT_PREFIX = /^(TC|VP|KD|CHK|FL|DI)-/i;
|
|
80
69
|
const frs: FrClause[] = [];
|
|
81
|
-
const seen = new Set<string>();
|
|
82
70
|
for (const line of lines) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!m || NON_REQUIREMENT_PREFIX.test(m[1])) continue;
|
|
86
|
-
const id = m[1].toUpperCase();
|
|
87
|
-
if (seen.has(id)) continue; // first declaration wins
|
|
88
|
-
seen.add(id);
|
|
89
|
-
frs.push({ id, text: m[2].replace(/\*\*/g, '').trim(), modality: modalityOf(m[2]) });
|
|
71
|
+
const m = line.match(/\*\*FR-(\d+)\*\*\s*:\s*(.+)$/);
|
|
72
|
+
if (m) frs.push({ id: `FR-${m[1]}`, text: m[2].replace(/\*\*/g, '').trim(), modality: modalityOf(m[2]) });
|
|
90
73
|
}
|
|
91
74
|
|
|
92
75
|
// Validation Rules table: a row carries a Constraint, a Trigger cell, and (often) a code.
|
|
93
|
-
// A "Trigger" column alone is NOT enough to claim the table (#578): a screen-STATES table
|
|
94
|
-
// ("State ID | Trigger | URL/heading oracle | …") uses Trigger for the user action that
|
|
95
|
-
// enters the state, and reading it as validation rows invented three gate-relevant
|
|
96
|
-
// TRIGGER-UNCOVERED gaps on a real spec. The table must also name a Constraint/Rule/
|
|
97
|
-
// Validation column — the thing a validation row is ABOUT.
|
|
98
76
|
const valRows: ValRow[] = [];
|
|
99
77
|
let cTrigger = -1, cConstraint = -1, cCode = -1, inTable = false;
|
|
100
78
|
for (const raw of lines) {
|
|
101
79
|
const line = raw.trim();
|
|
102
|
-
if (line.startsWith('|') && /\btrigger\b/i.test(line) &&
|
|
80
|
+
if (line.startsWith('|') && /\btrigger\b/i.test(line) && cTrigger < 0) {
|
|
103
81
|
const cells = line.split('|').map((c) => c.trim());
|
|
104
82
|
cTrigger = cells.findIndex((c) => /^trigger$/i.test(c));
|
|
105
|
-
cConstraint = cells.findIndex((c) => /constraint
|
|
83
|
+
cConstraint = cells.findIndex((c) => /constraint/i.test(c));
|
|
106
84
|
cCode = cells.findIndex((c) => /code/i.test(c));
|
|
107
85
|
inTable = cTrigger >= 0;
|
|
108
86
|
continue;
|
|
@@ -86,52 +86,15 @@ qa/flows/${input:flow}/
|
|
|
86
86
|
└── ui/ # Screenshots, mockups
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
### 1a.
|
|
89
|
+
### 1a. Identify the screens in the flow
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
observable, valuable outcome. Before asking for screens, walk this checklist with the user —
|
|
93
|
-
if 1, 3 or 8 fails, propose SPLITTING into separate flows:
|
|
94
|
-
|
|
95
|
-
1. Exactly **one business goal**? (cart correctness + category filtering = two flows)
|
|
96
|
-
2. A clear **trigger** and precondition?
|
|
97
|
-
3. **One observable final outcome**? (a final assertion you can write in one sentence)
|
|
98
|
-
4. Is that outcome **valuable to the actor**? (an order placed, a password reset — not "a page rendered")
|
|
99
|
-
5. Is **every step necessary** for that outcome?
|
|
100
|
-
6. Are all steps at the **same business abstraction**?
|
|
101
|
-
7. Are optional/error branches **phases of this goal** (ER/EH), not new goals?
|
|
102
|
-
8. Does **no segment** form an independently valuable flow on its own?
|
|
103
|
-
9. Can you write **a single clear final assertion**?
|
|
104
|
-
10. Can you name it "**Verb + outcome**"? (`place-order`, `reset-password` — not `cart-and-filter`)
|
|
105
|
-
|
|
106
|
-
Then ask: "Which screens does this flow visit, in order? (e.g., login → dashboard → award-form → confirmation)"
|
|
91
|
+
Ask the user: "Which screens does this flow visit, in order? (e.g., login → dashboard → award-form → confirmation)"
|
|
107
92
|
|
|
108
93
|
Record the screen list — you will need it for:
|
|
109
94
|
- Filling `spec.md` (Step 3)
|
|
110
95
|
- Suggesting `[Screen:Element]` namespace prefixes
|
|
111
96
|
- Capturing visuals per screen (Step 2)
|
|
112
97
|
|
|
113
|
-
### 1b. Author the Flow Contract (`requirements/flow-contract.yaml`)
|
|
114
|
-
|
|
115
|
-
Write the answers down as the flow's contract — `sungen audit` scores the flow **against it**
|
|
116
|
-
(the `flowCoverage` axis: HP/ER/EH journey phases; `FLOW-OUTCOME-UNPROVEN` when no automated
|
|
117
|
-
scenario asserts data on the outcome screen; `FLOW-SCOPE-CREEP` when scenarios never touch it):
|
|
118
|
-
|
|
119
|
-
```yaml
|
|
120
|
-
goal: "Place an order for a product added from home" # Verb + outcome
|
|
121
|
-
actor: user
|
|
122
|
-
trigger: "Add a product to the cart from the home featured list"
|
|
123
|
-
precondition: "A registered account; an empty cart"
|
|
124
|
-
outcome:
|
|
125
|
-
screen: checkout # the [Screen:...] namespace carrying the final proof
|
|
126
|
-
assertion: "The confirmation shows the order number and the paid total"
|
|
127
|
-
value: "The customer has paid; the shop has a new order"
|
|
128
|
-
phases: [HP, ER, EH] # journey phases (default); add UI only if the flow owns UI states
|
|
129
|
-
stateful: cart # the mutated collection, if any — enables regression-depth dims
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**A filled contract is an INPUT to generation — never an output.** Like `test-viewpoint.md`,
|
|
133
|
-
generation must not rewrite it to match what was generated; disagree → propose the diff and ask.
|
|
134
|
-
|
|
135
98
|
### 2. Capture visual source
|
|
136
99
|
|
|
137
100
|
**Mobile path** (`platform: mobile`):
|
|
@@ -224,8 +187,7 @@ If user picks `/sungen:create-test`, **you MUST use the Skill tool** to invoke i
|
|
|
224
187
|
- Test data namespaced by phase: `login.email`, `submission.nominee`
|
|
225
188
|
- `@flow` tag required at feature level
|
|
226
189
|
- `Background:` should only contain the starting navigation — the URL path (web) or the `--reach` nav recipe (mobile)
|
|
227
|
-
- Each scenario = one phase of the journey
|
|
228
|
-
- One flow = ONE business goal with ONE observable outcome (`requirements/flow-contract.yaml`) — a segment with its own value is its own flow
|
|
190
|
+
- Each scenario = one phase of the journey
|
|
229
191
|
{{#cap parallel-subagents}}
|
|
230
192
|
- Mobile flows are tagged `@platform:mobile` and run via `/sungen:run-test <flow>` (WebdriverIO, not Playwright)
|
|
231
193
|
{{/cap}}
|
|
@@ -6,16 +6,6 @@ order: 20
|
|
|
6
6
|
claude-tools: "Read, Grep, Bash, Glob, Write, AskUserQuestion, Skill, mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot"
|
|
7
7
|
copilot-tools: "[vscode, execute, read, agent, edit, search, web, browser, todo, 'playwright/*']"
|
|
8
8
|
codex-trigger: "Run when the user asks to CREATE, generate, write, or author test cases / a .feature file for a screen or flow. Step 2 (after add-screen/add-flow, before run-test). Do NOT use for executing, running, or compiling existing tests."
|
|
9
|
-
---
|
|
10
|
-
## ⛔ HARD RULE — the run's LAST action is the next-step hand-back
|
|
11
|
-
|
|
12
|
-
A create-test run is NOT finished when the files are written or the audit prints. The final
|
|
13
|
-
action of EVERY run — success, partial, or aborted — is the next-step hand-back
|
|
14
|
-
({{#cap parallel-subagents}}an `AskUserQuestion` offering the next actions{{/cap}}{{^cap parallel-subagents}}a numbered list of next-action choices{{/cap}};
|
|
15
|
-
see "Finish — always hand the next step back" at the end of this file). Ending with a prose
|
|
16
|
-
summary and no choices is a broken run: the operator is left guessing. This holds no matter
|
|
17
|
-
how long the generation/repair loop ran.
|
|
18
|
-
|
|
19
9
|
---
|
|
20
10
|
{{#cap parallel-subagents}}
|
|
21
11
|
## ⛔ HARD RULE — No Figma MCP when PAT data exists
|
|
@@ -605,40 +605,22 @@ error:
|
|
|
605
605
|
|
|
606
606
|
> **Auto-detect**: if path is `qa/flows/<name>/` → use this section. Skip Steps 1–4 above.
|
|
607
607
|
|
|
608
|
-
**Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
|
|
609
|
-
`sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
|
|
610
|
-
`FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). No contract yet → author it with the user via the
|
|
611
|
-
boundary checklist in `add-flow` (one business goal · clear trigger · ONE observable outcome
|
|
612
|
-
valuable to the actor · name = "Verb + outcome"), THEN generate. **A filled contract is an INPUT —
|
|
613
|
-
never rewrite it to match your output** (same rule as `test-viewpoint.md`).
|
|
614
|
-
|
|
615
608
|
| Aspect | Screen | Flow |
|
|
616
609
|
|---|---|---|
|
|
617
|
-
| Section focus | UI patterns per section | Journey phases
|
|
610
|
+
| Section focus | UI patterns per section | Journey phases across screens |
|
|
618
611
|
| Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
|
|
619
612
|
| Test data keys | `{{variable}}` | `{{phase.variable}}` |
|
|
620
613
|
| Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
|
|
621
|
-
|
|
|
614
|
+
| Viewpoints | VP-UI/VAL/LOGIC/SEC per section | VP-LOGIC (transitions), VP-SEC (auth persistence), VP-VAL (cross-screen data) |
|
|
622
615
|
|
|
623
|
-
**Scenarios to generate
|
|
616
|
+
**Scenarios to generate:**
|
|
624
617
|
|
|
625
|
-
|
|
|
626
|
-
|
|
627
|
-
|
|
|
628
|
-
|
|
|
629
|
-
|
|
|
630
|
-
| Cross-screen
|
|
631
|
-
| Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
|
|
632
|
-
|
|
633
|
-
**Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
|
|
634
|
-
that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
|
|
635
|
-
DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
|
|
636
|
-
Auth persistence across transitions is part of `EH` unless the project declares it its own phase.
|
|
637
|
-
|
|
638
|
-
**Manual in flows**: always `@manual:Mx` with the reason code — bare `@manual` is flagged
|
|
639
|
-
(`MANUAL-CODE-MISSING`) because the capability planner cannot route it. Typical flow deferrals:
|
|
640
|
-
inbox/mail oracle → `M5`, network-request count → `M3`, context expiry control → `M7`. A
|
|
641
|
-
cross-screen scenario inside the flow's own goal is NOT manual — automate it here.
|
|
618
|
+
| Category | What to test |
|
|
619
|
+
|---|---|
|
|
620
|
+
| Happy path | Complete flow end-to-end with valid data |
|
|
621
|
+
| Auth persistence | Auth state maintained across screen transitions |
|
|
622
|
+
| Error recovery | Invalid input mid-flow → fix → continue |
|
|
623
|
+
| Cross-screen data | Data entered on screen A visible on screen B |
|
|
642
624
|
|
|
643
625
|
```gherkin
|
|
644
626
|
@flow @auth:user
|
|
@@ -648,20 +630,19 @@ Feature: Award Submission Flow
|
|
|
648
630
|
Given User is on [Login] page
|
|
649
631
|
|
|
650
632
|
@high
|
|
651
|
-
Scenario:
|
|
633
|
+
Scenario: User logs in successfully
|
|
652
634
|
When User fill [Login:Email] field with {{login.email}}
|
|
653
635
|
And User fill [Login:Password] field with {{login.password}}
|
|
654
636
|
And User click [Login:Submit] button
|
|
655
637
|
Then User see [Dashboard] page
|
|
656
|
-
When User click [Dashboard:Awards] link
|
|
657
|
-
And User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
658
|
-
And User click [Awards:Submit] button
|
|
659
|
-
Then User see [Awards:Success Message] text with {{success_message}}
|
|
660
638
|
|
|
661
639
|
@high
|
|
662
|
-
Scenario:
|
|
663
|
-
When User
|
|
664
|
-
Then User see [
|
|
640
|
+
Scenario: User submits nomination
|
|
641
|
+
When User click [Dashboard:Awards] link
|
|
642
|
+
Then User see [Awards] page
|
|
643
|
+
When User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
644
|
+
And User click [Awards:Submit] button
|
|
645
|
+
Then User see {{success_message}} message
|
|
665
646
|
```
|
|
666
647
|
|
|
667
648
|
```yaml
|
|
@@ -45,21 +45,8 @@ Example:
|
|
|
45
45
|
|
|
46
46
|
## Testing Strategy
|
|
47
47
|
|
|
48
|
-
Machine-readable intent — `sungen audit` reads these keys (Intent Profile). Values here are
|
|
49
|
-
live even when the surrounding text changes; an invalid value silently falls back to the default.
|
|
50
|
-
|
|
51
|
-
focus: functional
|
|
52
|
-
<!-- focus: functional | e-commerce | security | smoke — drives the audit's depth threshold -->
|
|
53
|
-
|
|
54
|
-
risk_tier: normal
|
|
55
|
-
<!-- risk_tier: high | normal | low -->
|
|
56
|
-
|
|
57
|
-
To silence driver suggestions in audit findings, add a line: capability_suggestions with value off.
|
|
58
|
-
|
|
59
48
|
**Focus areas** — what to cover thoroughly:
|
|
60
|
-
<!--
|
|
61
|
-
Response-time budgets are NOT a focus value — declare them in qa/perf.yaml
|
|
62
|
-
(percentile + scenario_ms budgets; reported by `sungen delivery`). -->
|
|
49
|
+
<!-- List from: functional, security, ui, accessibility, performance -->
|
|
63
50
|
<!-- Example: functional, security -->
|
|
64
51
|
|
|
65
52
|
**Mandatory coverage:**
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { ScenarioInfo } from './parse';
|
|
2
|
-
export interface FlowContract {
|
|
3
|
-
goal: string;
|
|
4
|
-
actor?: string;
|
|
5
|
-
trigger?: string;
|
|
6
|
-
precondition?: string;
|
|
7
|
-
outcome: {
|
|
8
|
-
screen: string;
|
|
9
|
-
assertion?: string;
|
|
10
|
-
};
|
|
11
|
-
value?: string;
|
|
12
|
-
/** Journey phases this flow declares. Default [HP, ER, EH]; UI is allowed but
|
|
13
|
-
* never demanded (presentation is the balance axis's business, not coverage's). */
|
|
14
|
-
phases: string[];
|
|
15
|
-
/** The mutated collection (cart, order, application …) — enables regression dims. */
|
|
16
|
-
stateful?: string;
|
|
17
|
-
budgets?: Record<string, number>;
|
|
18
|
-
}
|
|
19
|
-
export interface FlowQualityResult {
|
|
20
|
-
hasContract: boolean;
|
|
21
|
-
contract?: FlowContract;
|
|
22
|
-
/** Parse/shape errors — a broken contract is reported, never silently ignored. */
|
|
23
|
-
errors: string[];
|
|
24
|
-
outcomeProven: boolean;
|
|
25
|
-
/** Manual-only proof: the goal is covered but not by automation. */
|
|
26
|
-
outcomeManualOnly: boolean;
|
|
27
|
-
/** Scenario names that never touch the outcome screen (guard/error phases excluded). */
|
|
28
|
-
offGoal: string[];
|
|
29
|
-
offGoalRatio: number;
|
|
30
|
-
/** Off-goal categories, for the split suggestion ("VP-FILTER-* looks like its own flow"). */
|
|
31
|
-
offGoalCategories: string[];
|
|
32
|
-
phases: {
|
|
33
|
-
phase: string;
|
|
34
|
-
covered: boolean;
|
|
35
|
-
automated: boolean;
|
|
36
|
-
}[];
|
|
37
|
-
/** Covered-and-automated phases / declared phases (UI excluded) — the flow coverage axis. */
|
|
38
|
-
phaseRatio: number;
|
|
39
|
-
/** Cross-namespace transitions followed by an assertion / all transitions. */
|
|
40
|
-
handoffs: {
|
|
41
|
-
total: number;
|
|
42
|
-
asserted: number;
|
|
43
|
-
ratio: number;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export declare function flowContractPath(unitDir: string): string;
|
|
47
|
-
/** Load + validate. Returns null when absent; a present-but-broken file returns errors. */
|
|
48
|
-
export declare function loadFlowContract(unitDir: string): {
|
|
49
|
-
contract: FlowContract | null;
|
|
50
|
-
errors: string[];
|
|
51
|
-
};
|
|
52
|
-
/** Phase of a scenario: its declared phase token (FL-HP-001 / VP-FLOW-ER-02 / MS-EH-005)
|
|
53
|
-
* when present, else vocabulary detection. */
|
|
54
|
-
export declare function phaseOf(s: ScenarioInfo, declared: string[]): string | null;
|
|
55
|
-
/**
|
|
56
|
-
* Verify the suite against the contract. Deterministic; a flow without a contract
|
|
57
|
-
* returns hasContract:false and neutral values (the audit reports the checklist).
|
|
58
|
-
*/
|
|
59
|
-
export declare function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQualityResult;
|
|
60
|
-
/**
|
|
61
|
-
* Generalized stateful regression depth: the contract names the mutated collection,
|
|
62
|
-
* so the three dims (count-proof · teardown · multi-source) stop being cart-only.
|
|
63
|
-
*/
|
|
64
|
-
export declare function statefulDepthFor(collection: string, scenarios: ScenarioInfo[]): {
|
|
65
|
-
countProof: boolean;
|
|
66
|
-
teardown: boolean;
|
|
67
|
-
multiSource: boolean;
|
|
68
|
-
missing: string[];
|
|
69
|
-
ratio: number;
|
|
70
|
-
};
|
|
71
|
-
//# sourceMappingURL=flow-contract.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"flow-contract.d.ts","sourceRoot":"","sources":["../../src/harness/flow-contract.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;wFACoF;IACpF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,kFAAkF;IAClF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,oEAAoE;IACpE,iBAAiB,EAAE,OAAO,CAAC;IAC3B,wFAAwF;IACxF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAClE,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAkCrG;AAaD;+CAC+C;AAC/C,wBAAgB,OAAO,CAAC,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAU1E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,iBAAiB,CA2EzF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAWlL"}
|