@sun-asterisk/sungen 3.2.26 → 3.2.27
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/next.js +2 -2
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +2 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.js +3 -3
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/flow-contract.d.ts +0 -14
- package/dist/harness/flow-contract.d.ts.map +1 -1
- package/dist/harness/flow-contract.js +44 -9
- package/dist/harness/flow-contract.js.map +1 -1
- package/dist/harness/next-step.d.ts +8 -2
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +2 -2
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +9 -1
- package/dist/harness/sensors.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +17 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +11 -1
- package/dist/utils/ref-key.d.ts +30 -0
- package/dist/utils/ref-key.d.ts.map +1 -0
- package/dist/utils/ref-key.js +48 -0
- package/dist/utils/ref-key.js.map +1 -0
- package/package.json +3 -3
- package/src/cli/commands/next.ts +2 -2
- package/src/generators/test-generator/utils/selector-resolver.ts +2 -1
- package/src/harness/audit.ts +3 -3
- package/src/harness/flow-contract.ts +41 -8
- package/src/harness/next-step.ts +10 -4
- package/src/harness/sensors.ts +9 -1
- package/src/orchestrator/templates/ai-src/commands/create-test.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +17 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +11 -1
- package/src/utils/ref-key.ts +43 -0
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* declared collection (order, application, submission …)
|
|
23
23
|
*/
|
|
24
24
|
import * as fs from 'fs';
|
|
25
|
+
import { refNamespaces, screenKey } from '../utils/ref-key';
|
|
25
26
|
import * as path from 'path';
|
|
26
27
|
import { parse as parseYaml } from 'yaml';
|
|
27
28
|
import { ScenarioInfo } from './parse';
|
|
@@ -179,7 +180,7 @@ export function loadFlowContract(unitDir: string): { contract: FlowContract | nu
|
|
|
179
180
|
actor: raw.actor !== undefined ? String(raw.actor) : undefined,
|
|
180
181
|
trigger: raw.trigger !== undefined ? String(raw.trigger) : undefined,
|
|
181
182
|
precondition: raw.precondition !== undefined ? String(raw.precondition) : undefined,
|
|
182
|
-
outcome: { screen: String(outcome!.screen)
|
|
183
|
+
outcome: { screen: screenKey(String(outcome!.screen)), assertion: outcome!.assertion !== undefined ? String(outcome!.assertion) : undefined },
|
|
183
184
|
value: raw.value !== undefined ? String(raw.value) : undefined,
|
|
184
185
|
successGuarantee: raw.successGuarantee !== undefined ? String(raw.successGuarantee) : (raw.success_guarantee !== undefined ? String(raw.success_guarantee) : undefined),
|
|
185
186
|
minimalGuarantee: raw.minimalGuarantee !== undefined ? String(raw.minimalGuarantee) : (raw.minimal_guarantee !== undefined ? String(raw.minimal_guarantee) : undefined),
|
|
@@ -210,7 +211,7 @@ export function loadFlowContract(unitDir: string): { contract: FlowContract | nu
|
|
|
210
211
|
.map((e) => ({
|
|
211
212
|
name: String(e.name),
|
|
212
213
|
owner: e.owner !== undefined ? String(e.owner) : undefined,
|
|
213
|
-
screens: Array.isArray(e.screens) ? (e.screens as unknown[]).map((x) => String(x)
|
|
214
|
+
screens: Array.isArray(e.screens) ? (e.screens as unknown[]).map((x) => screenKey(String(x))) : [],
|
|
214
215
|
}))
|
|
215
216
|
: undefined,
|
|
216
217
|
golden: raw.golden === true,
|
|
@@ -223,13 +224,12 @@ export function loadFlowContract(unitDir: string): { contract: FlowContract | nu
|
|
|
223
224
|
|
|
224
225
|
/** `[screen:element]` namespaces referenced by a scenario's steps, in step order. */
|
|
225
226
|
function namespacesInOrder(s: ScenarioInfo): string[] {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return out;
|
|
227
|
+
// Through the shared vocabulary — a private regex here once dropped every multi-word screen (#660).
|
|
228
|
+
return refNamespaces(s.stepsText);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
function touchesOutcome(s: ScenarioInfo, outcomeScreen: string): boolean {
|
|
232
|
-
return namespacesInOrder(s).includes(outcomeScreen);
|
|
232
|
+
return namespacesInOrder(s).includes(screenKey(outcomeScreen));
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
@@ -244,7 +244,8 @@ function reachesOutcome(s: ScenarioInfo, outcomeScreen: string): boolean {
|
|
|
244
244
|
// anywhere in the scenario would suppress every positive assertion in it.
|
|
245
245
|
const steps = (s.steps ?? []).map((st) => st.text.toLowerCase());
|
|
246
246
|
if (steps.length === 0) return touchesOutcome(s, outcomeScreen);
|
|
247
|
-
|
|
247
|
+
const key = screenKey(outcomeScreen);
|
|
248
|
+
return steps.some((t) => refNamespaces(t, { bare: true }).includes(key)
|
|
248
249
|
&& !/\b(is hidden|is not visible|does not exist|is absent|no longer)\b/.test(t));
|
|
249
250
|
}
|
|
250
251
|
|
|
@@ -366,15 +367,39 @@ export function flowInventory(contract: FlowContract, scenarios: ScenarioInfo[])
|
|
|
366
367
|
* Both inflate the flow count while adding no branch coverage, which is exactly what makes a
|
|
367
368
|
* suite look complete to the harness and thin to a reviewer.
|
|
368
369
|
*/
|
|
370
|
+
/** An interaction, whichever keyword the author put in front of it — `Then User click` exists. */
|
|
371
|
+
const INTERACTION_VERB = /\buser\s+(?:click|clicks|tap|taps|fill|fills|type|types|select|selects|press|presses|check|checks|uncheck|unchecks|upload|uploads|submit|submits|enter|enters|choose|chooses|toggle|toggles|drag|drags|swipe|swipes|scroll|scrolls|hover|hovers|clear|clears)\b/i;
|
|
372
|
+
|
|
369
373
|
export function misfiledPhases(
|
|
370
374
|
contract: FlowContract, scenarios: ScenarioInfo[], basicPhase: string,
|
|
371
375
|
): Array<{ scenario: string; id: string; why: string }> {
|
|
372
376
|
const out: Array<{ scenario: string; id: string; why: string }> = [];
|
|
373
377
|
const outcome = contract.outcome.screen;
|
|
378
|
+
// A branch point is a STRUCTURAL fact before it is a vocabulary one. The check used to demand a
|
|
379
|
+
// branch WORD (back / cancel / retry…), so a precondition guard — `Given` already authenticated,
|
|
380
|
+
// or no originating session → `Then` redirected — had no way to satisfy it: such a scenario has
|
|
381
|
+
// no interaction step at all, by construction, and its branch is in the `Given`, not in a verb.
|
|
382
|
+
// Reported under AF and EF alike on a real project (#660). Two structural signals now count:
|
|
383
|
+
// no `When` at all (a guard by construction), or a `Given` the basic flow never establishes
|
|
384
|
+
// (a precondition branch). Vocabulary remains as the fallback for scenarios without step data.
|
|
385
|
+
const basicGivens = new Set(scenarios
|
|
386
|
+
.filter((sc) => phaseToken(flowIdOf(sc, contract.phases) ?? '') === phaseToken(basicPhase))
|
|
387
|
+
.flatMap((sc) => (sc.steps ?? []).filter((st) => st.bucket === 'given').map((st) => st.text.toLowerCase().trim())));
|
|
388
|
+
const branchesStructurally = (sc: ScenarioInfo): boolean => {
|
|
389
|
+
const steps = sc.steps ?? [];
|
|
390
|
+
if (steps.length === 0) return false;
|
|
391
|
+
// No interaction AND the journey does not complete: a guard by construction (the redirect or
|
|
392
|
+
// the blocked screen IS the branch). No interaction but the outcome IS reached is a different
|
|
393
|
+
// animal — a postcondition on the basic path — and stays subject to the checks below.
|
|
394
|
+
const interacts = steps.some((st) => st.bucket === 'when' || INTERACTION_VERB.test(st.text));
|
|
395
|
+
if (!interacts && !reachesOutcome(sc, outcome)) return true;
|
|
396
|
+
return steps.some((st) => st.bucket === 'given' && !basicGivens.has(st.text.toLowerCase().trim()));
|
|
397
|
+
};
|
|
374
398
|
for (const s of scenarios) {
|
|
375
399
|
const id = flowIdOf(s, contract.phases);
|
|
376
400
|
if (!id || phaseToken(id) === phaseToken(basicPhase)) continue;
|
|
377
401
|
const ph = phaseToken(id);
|
|
402
|
+
if (branchesStructurally(s)) continue;
|
|
378
403
|
if (ph === 'EF' && reachesOutcome(s, outcome)) {
|
|
379
404
|
// An error-then-recover flow legitimately ends at the outcome — it is the recovery that
|
|
380
405
|
// is being proven, and a guard flow is blocked rather than "failed". Only a scenario with
|
|
@@ -415,8 +440,16 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
415
440
|
// guard/error phase is evidence of a SECOND business goal in this flow.
|
|
416
441
|
const declaredPhases = contract.phases;
|
|
417
442
|
const basic = declaredPhases.filter((p) => p !== 'UI')[0];
|
|
443
|
+
// A flow the inventory declares WITH ITS OWN outcome is measured against that outcome, never
|
|
444
|
+
// against the use case's. A spec that keeps an entry variant deliberately light ("prove the
|
|
445
|
+
// entry reaches the action bar; do not repeat the full assertion") could only satisfy the old
|
|
446
|
+
// check by widening the scenario — which the human reviewer then called over-testing. The gate
|
|
447
|
+
// and the reviewer were optimising different things; the declaration is where they agree (#660).
|
|
448
|
+
const ownOutcome = new Set((contract.flows ?? []).filter((f) => f.outcome).map((f) => f.id));
|
|
418
449
|
const offGoalScenarios = scenarios.filter((s) => {
|
|
419
450
|
if (touchesOutcome(s, outcomeScreen)) return false;
|
|
451
|
+
const fid = flowIdOf(s, declaredPhases);
|
|
452
|
+
if (fid && ownOutcome.has(fid)) return false;
|
|
420
453
|
const ph = phaseOf(s, declaredPhases, contract.phaseDetails);
|
|
421
454
|
// Any NON-basic declared phase (guards, error recovery, alternate branches) legitimately
|
|
422
455
|
// stops before the outcome — that is what a branch IS. Only an unclassified scenario that
|
|
@@ -470,7 +503,7 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
470
503
|
const steps = s.steps ?? [];
|
|
471
504
|
let current: string | null = null;
|
|
472
505
|
for (let i = 0; i < steps.length; i++) {
|
|
473
|
-
const ns = (steps[i].text
|
|
506
|
+
const ns = refNamespaces(steps[i].text)[0] ?? null;
|
|
474
507
|
if (!ns) continue;
|
|
475
508
|
if (current !== null && ns !== current) {
|
|
476
509
|
total++;
|
package/src/harness/next-step.ts
CHANGED
|
@@ -25,8 +25,14 @@ export interface NextStep {
|
|
|
25
25
|
command: string;
|
|
26
26
|
/** Why it is next — stated so an agent can weigh it, not just obey it. */
|
|
27
27
|
because: string;
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* `blocked` steps are not runnable yet; they name what to resolve first. `decide` steps are a
|
|
30
|
+
* HUMAN'S to run: an agent presents them and stops. Accepting the viewpoint baseline is the
|
|
31
|
+
* case — offered as a plain `do`, an agent that had just rewritten `test-viewpoint.md` from the
|
|
32
|
+
* spec ran it, and `atomicLedger`/`traceability` read 100% against a checklist it wrote itself
|
|
33
|
+
* in the same session (#660).
|
|
34
|
+
*/
|
|
35
|
+
kind: 'do' | 'blocked' | 'optional' | 'decide';
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export interface UnitState {
|
|
@@ -228,9 +234,9 @@ export function deriveSteps(s: UnitState): NextStep[] {
|
|
|
228
234
|
}
|
|
229
235
|
if (s.audit.viewpointBaselineStatus === 'changed') {
|
|
230
236
|
out.push({
|
|
231
|
-
kind: '
|
|
237
|
+
kind: 'decide',
|
|
232
238
|
command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
|
|
233
|
-
because: 'test-viewpoint.md changed since the accepted baseline, so the ledger and traceability axes are not evidence until
|
|
239
|
+
because: 'test-viewpoint.md changed since the accepted baseline, so the ledger and traceability axes are not evidence until a QA confirms the new declaration. A decision, not a step: an agent presents it and does not run it — accepting the yardstick you just wrote makes both axes read 100% by construction',
|
|
234
240
|
});
|
|
235
241
|
}
|
|
236
242
|
const repair = s.audit.findings
|
package/src/harness/sensors.ts
CHANGED
|
@@ -659,7 +659,15 @@ const CLAIM_RULES: ClaimRule[] = [
|
|
|
659
659
|
// category behind "browser back does not re-submit", "does not re-charge the card",
|
|
660
660
|
// "double-click does not create two orders" — not a per-feature keyword.
|
|
661
661
|
claim: 'no-side-effect/no-duplicate',
|
|
662
|
-
|
|
662
|
+
// The negation must attach to an EFFECT — something created, sent, stored, charged, fired
|
|
663
|
+
// twice — not to the INPUT being refused. "duplicate address is rejected" is an observable
|
|
664
|
+
// rejection, proven by the error assertion in front of the user; "no duplicate record is
|
|
665
|
+
// created" is an absence nothing on the page can show. The rule used to fire on any action
|
|
666
|
+
// word beside any negation word, so ordinary rejection titles were told to go find a DB count,
|
|
667
|
+
// and renaming around the trigger words became the workflow (#660). Two shapes now qualify:
|
|
668
|
+
// (a) a negation followed by an effect token, (b) a repeat token (twice / again / double- or
|
|
669
|
+
// re-submit / second …) beside a negation, "only once" or a disabled control.
|
|
670
|
+
title: /(?:\b(?:no|not|n['’]t|never|without|cannot|không|chưa)\b[^.;]{0,40}?\b(?:creat(?:e|es|ed|ion)|record(?:s|ed)?|rows?|entr(?:y|ies)|sen[dt]|resen[dt]|charg(?:e|es|ed)|order(?:s|ed)?|payment|paid|insert(?:s|ed)?|submit(?:s|ted)?|submission|stor(?:e|es|ed)|sav(?:e|es|ed)|persist(?:s|ed)?|fir(?:e|es|ed)|issu(?:e|es|ed)|post(?:s|ed)?|email(?:s|ed)?|request(?:s|ed)?|\botp\b|twice|again|second|double|duplicate[sd]?|re-?submit(?:s|ted)?|tạo|gửi|lưu)\b)|(?=.*\b(?:double[- ]?(?:submit|click|tap|activation)|re-?submi(?:t|ts|tted|ssion)|twice|again|second (?:submit|submission|request|click|tap|order|charge|email|record))\b)(?=.*\b(?:no|not|n['’]t|never|without|cannot|prevent(?:s|ed)?|block(?:s|ed)?|avoid(?:s|ed)?|only once|exactly one|single|idempotent|disabled|không|chưa)\b)/i,
|
|
663
671
|
// `is disabled` counts: when the spec's own mechanism against a repeat is "the control
|
|
664
672
|
// is disabled immediately" (FR-014-style), asserting the disabled state IS the contrast —
|
|
665
673
|
// the second activation cannot occur. Without it the canonical double-submit proof shape
|
|
@@ -96,7 +96,7 @@ dropped along the way stops being missing from anything. `sungen audit` now repo
|
|
|
96
96
|
`VIEWPOINT-ADOPTED-POST-HOC` on the first sighting of a viewpoint alongside an existing suite and
|
|
97
97
|
holds `atomicLedger` + `traceability` as unverified until a human accepts it — so the honest move
|
|
98
98
|
is to write the claims from `spec.md` and the contract, mark plainly that they are a draft for QA
|
|
99
|
-
review, and let the QA revise before `--accept-viewpoint`.
|
|
99
|
+
review, and let the QA revise before `--accept-viewpoint`. **You never run `--accept-viewpoint` yourself** — `sungen next` lists it as a `⚖ decide` step for exactly this reason: an agent accepting the yardstick it wrote in the same session makes `atomicLedger` and `traceability` read 100% against its own checklist, and the Exception Flow it forgot stops being missing from anything. Present the acceptance to the QA (`AskUserQuestion`) with what the file declares, and stop.
|
|
100
100
|
|
|
101
101
|
**When you give a claim an id, a scenario must carry that id.** The id is the contract: a claim
|
|
102
102
|
whose id no scenario carries is reported MISSING, and word overlap will not rescue it. Several
|
|
@@ -68,6 +68,23 @@ Use these when repairing GATE/DEPTH findings for the hard viewpoints (cart/detai
|
|
|
68
68
|
4. **EP/data families are OK.** A `duplicates` cluster with `sameDataLikely=false` is an intentional equivalence-partition family (e.g. many invalid-email cases) — keep it; only collapse `sameDataLikely=true` exact duplicates.
|
|
69
69
|
5. **Advisory findings — surface, don't gate.** `MANUAL-REASON-MISMATCH` → fix the scenario's `@manual:Mx` code (so the planner recommends the right driver) during repair. `CAPABILITY-SUGGESTION` → **present it to the user as a next-step option** (e.g. "N @manual could be automated — `sungen capability add api db`?"), **recommend-only — never auto-install**. `VERIFICATION-OUT-OF-SCOPE (api|db)` → the unit uses `@api`/`@query`/`@requires:api|db` verification the project's recorded **test type** never opted into (E2E/UI-only per `capabilities.yaml`, and the viewpoint doesn't ask for it). During repair: **rewrite the oracle to be UI-observable** (or downgrade the deep check to an `(optional deep check: …)` note); only keep the API/DB verification if the project genuinely tests that layer — then record it (`verification: [ui, api]` / `sungen capability add`). Keep API/DB-in-E2E to the ≤20% band. Neither of these fails the gate.
|
|
70
70
|
|
|
71
|
+
6. **A gate that contradicts the spec's intent is a question, not a repair.** When the only way to
|
|
72
|
+
satisfy a finding is to change what the spec deliberately asked for — widening an entry variant the
|
|
73
|
+
spec keeps light so `FLOW-SCOPE-CREEP` stops firing, renaming a title around a trigger word so a
|
|
74
|
+
claim rule stops firing, adding an assertion the spec never demands — **stop and put the conflict
|
|
75
|
+
to the user** (`AskUserQuestion`: the finding, the spec passage it collides with, the two ways out).
|
|
76
|
+
A score reached by editing toward the gate is not a measurement of the suite; it is a measurement
|
|
77
|
+
of how well the gate was gamed, and the human reviewer will read it as over-testing or a false
|
|
78
|
+
claim. Where the harness provides a declaration route — `flows:` with a per-flow `outcome:`, a
|
|
79
|
+
`status: deferred` with a reason, `@manual` naming the gesture — that route is the fix, never the
|
|
80
|
+
scenario.
|
|
81
|
+
7. **`--accept-viewpoint` is a QA decision. Never run it.** `atomicLedger` and `traceability` are
|
|
82
|
+
evidence only while `test-viewpoint.md` is independent of what was generated. `sungen next` lists
|
|
83
|
+
the acceptance as a `⚖ decide` step: present it (the diff of declared ids, what was added, what was
|
|
84
|
+
dropped) and let the QA run the command. An agent that accepts the yardstick it just wrote makes
|
|
85
|
+
both axes read 100% by construction — and the spec's Exception Flow it forgot stops being missing
|
|
86
|
+
from anything.
|
|
87
|
+
|
|
71
88
|
## Discovery / fallback tree (when input is limited)
|
|
72
89
|
|
|
73
90
|
```
|
|
@@ -340,6 +340,9 @@ Security: [S1 – admin only]
|
|
|
340
340
|
`sungen audit` enforces these. Generate compliant output up front:
|
|
341
341
|
|
|
342
342
|
1. **Taxonomy-match** (`VP-TAXONOMY-MISMATCH`, gate-FAIL) — when `test-viewpoint.md` declares its own viewpoint IDs (e.g. `VP0`, `VP1`, … `VP12`, `MS-HP-001`, `MS-EH-001`), **reuse those IDs verbatim as the scenario codes**. Do NOT invent a generic `VP-UI / VP-LOGIC / VP-VAL` scheme — that breaks the coverage matrix. Only fall back to `VP-<CATEGORY>-<NNN>` when the viewpoint file declares no IDs.
|
|
343
|
+
- **`--accept-viewpoint` is the QA's command, not yours.** `sungen next` shows it as a `⚖ decide`
|
|
344
|
+
step. Present the declared-id diff and stop; a yardstick accepted by the process it measures
|
|
345
|
+
reads 100% by construction.
|
|
343
346
|
- **Match the scenarios to the file — never the file to the scenarios.** A filled `test-viewpoint.md` is an input; do not rewrite its declarations to fit what you generated. That is not compliance, it is moving the yardstick: `traceability` + `atomicLedger` then read 100% by construction and a dropped viewpoint stops being missing from anything. Disagree with the taxonomy → propose the diff and ask. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and excludes both axes until a human accepts the change (`sungen audit --screen <name> --accept-viewpoint`).
|
|
344
347
|
2. **Spec-coverage triggers** (`TRIGGER-UNCOVERED`, gate-FAIL) — the Validation-Rules table lists a **trigger** per constraint (e.g. `blur, submit`). Generate one scenario **per (constraint × trigger)** — a `format` rule validating *on blur AND on submit* needs BOTH a blur scenario (`press Tab`) and a submit scenario (`click [Submit]` / `press Enter`). Never collapse the trigger × input matrix to one representative case.
|
|
345
348
|
3. **Claim-Proof** (`CLAIM-UNPROVEN`) — a title claiming `all`/`only`/`every`/`single`/`correct`/`same`/`changes`/`hidden`/`cleared`/`restored`/`independent`/`sanitized`/`announces` MUST have the matching assertion (`see all …`, count, `remember`+compare, `is hidden`, return-and-assert-empty, etc.). If the title promises it, the steps must prove it.
|
|
@@ -821,6 +824,13 @@ is a gap to REPORT, not to quietly absorb.
|
|
|
821
824
|
that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
|
|
822
825
|
DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
|
|
823
826
|
Auth persistence across transitions is part of `EH` unless the project declares it its own phase.
|
|
827
|
+
**The exception is declared, not widened:** when the spec KEEPS a variant deliberately light — an
|
|
828
|
+
entry variant that only proves the alternative entry reaches the action bar, without repeating the
|
|
829
|
+
full assertion the basic flow already carries — declare that flow in `flows:` with its own
|
|
830
|
+
`outcome:` and the audit judges it against that outcome. Never widen the scenario to satisfy the
|
|
831
|
+
gate; a reviewer reads the result as over-testing, and the finding was the thing that was wrong.
|
|
832
|
+
When a gate and the spec's stated intent collide and no declaration route exists, stop and put the
|
|
833
|
+
conflict to the user — do not edit toward the score.
|
|
824
834
|
|
|
825
835
|
**Manual in flows**: always `@manual:Mx` with the reason code — bare `@manual` is flagged
|
|
826
836
|
(`MANUAL-CODE-MISSING`) because the capability planner cannot route it. Typical flow deferrals:
|
|
@@ -847,7 +857,7 @@ Feature: Award Submission Flow
|
|
|
847
857
|
|
|
848
858
|
@high
|
|
849
859
|
Scenario: FL-EH-001 Direct access to the award form without login redirects to login
|
|
850
|
-
|
|
860
|
+
Given User is on [Awards] page
|
|
851
861
|
Then User see [Login] page
|
|
852
862
|
```
|
|
853
863
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE vocabulary for what a `[Reference]` means.
|
|
3
|
+
*
|
|
4
|
+
* The compiler's documented key rule is "the lowercase of the `[Reference]` text, spaces
|
|
5
|
+
* preserved" (`SelectorResolver.generateKey`). The harness re-implemented that rule with its own
|
|
6
|
+
* regex — `/\[([a-z0-9_.-]+):/` — which has no space in it, so a multi-word screen such as
|
|
7
|
+
* `[Basic Info:Email]` compiled, ran and passed, and was NEVER seen by `touchesOutcome`,
|
|
8
|
+
* `reachesOutcome`, phase coverage, handoffs or the navigation-target check. A real project
|
|
9
|
+
* hyphenated every namespace with zero functional change and its audit went 6.9 → 8.9 (#660).
|
|
10
|
+
*
|
|
11
|
+
* Two rules, one place:
|
|
12
|
+
* - `normalizeRefLabel` IS the compiler's key rule. The compiler delegates here.
|
|
13
|
+
* - `screenKey` is the COMPARISON form the harness uses for a screen/namespace: separators are
|
|
14
|
+
* equivalent (`basic info` ≡ `basic-info` ≡ `basic_info`), because a contract author writes
|
|
15
|
+
* `outcome.screen: basic-info` as readily as the feature writes `[Basic Info:…]`, and the
|
|
16
|
+
* harness only ever asks "is this the same screen?" — never "which YAML key resolves?".
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The compiler's selector-key rule: NFC, lowercase, trimmed, inner whitespace collapsed. */
|
|
20
|
+
export function normalizeRefLabel(label: string): string {
|
|
21
|
+
return label.normalize('NFC').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Comparison form for a screen / namespace name — separator-insensitive. */
|
|
25
|
+
export function screenKey(label: string): string {
|
|
26
|
+
return normalizeRefLabel(label).replace(/[\s_-]+/g, ' ').trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every namespace a piece of step text references, in order, as `screenKey`s.
|
|
31
|
+
* `[Screen:Element]` → `screen`; a bare `[Screen]` page reference is included too when `bare`
|
|
32
|
+
* is set (a `see [Done] page` assertion names the screen without an element).
|
|
33
|
+
*/
|
|
34
|
+
export function refNamespaces(text: string, opts: { bare?: boolean } = {}): string[] {
|
|
35
|
+
const out: string[] = [];
|
|
36
|
+
for (const m of text.matchAll(/\[([^\]]+)\]/g)) {
|
|
37
|
+
const inner = m[1];
|
|
38
|
+
const colon = inner.indexOf(':');
|
|
39
|
+
if (colon !== -1) out.push(screenKey(inner.slice(0, colon)));
|
|
40
|
+
else if (opts.bare) out.push(screenKey(inner));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|