@sun-asterisk/sungen 3.2.23 → 3.2.24-beta.1

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 (57) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +21 -6
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/dashboard/templates/index.html +1 -1
  5. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  6. package/dist/exporters/csv-exporter.js +3 -1
  7. package/dist/exporters/csv-exporter.js.map +1 -1
  8. package/dist/exporters/feature-parser.d.ts +6 -0
  9. package/dist/exporters/feature-parser.d.ts.map +1 -1
  10. package/dist/exporters/feature-parser.js +21 -0
  11. package/dist/exporters/feature-parser.js.map +1 -1
  12. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  13. package/dist/exporters/xlsx-report-builder.js +3 -1
  14. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  15. package/dist/harness/audit.d.ts +13 -0
  16. package/dist/harness/audit.d.ts.map +1 -1
  17. package/dist/harness/audit.js +41 -10
  18. package/dist/harness/audit.js.map +1 -1
  19. package/dist/harness/flow-contract.d.ts +29 -0
  20. package/dist/harness/flow-contract.d.ts.map +1 -1
  21. package/dist/harness/flow-contract.js +77 -11
  22. package/dist/harness/flow-contract.js.map +1 -1
  23. package/dist/harness/parse.d.ts.map +1 -1
  24. package/dist/harness/parse.js +22 -3
  25. package/dist/harness/parse.js.map +1 -1
  26. package/dist/harness/quality-gates.d.ts +2 -1
  27. package/dist/harness/quality-gates.d.ts.map +1 -1
  28. package/dist/harness/quality-gates.js +47 -5
  29. package/dist/harness/quality-gates.js.map +1 -1
  30. package/dist/harness/sensors.d.ts.map +1 -1
  31. package/dist/harness/sensors.js +48 -5
  32. package/dist/harness/sensors.js.map +1 -1
  33. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  34. package/dist/harness/viewpoint-ledger.js +17 -1
  35. package/dist/harness/viewpoint-ledger.js.map +1 -1
  36. package/dist/orchestrator/templates/ai-src/commands/create-test.md +17 -0
  37. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +62 -5
  38. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-e-identity.md +1 -1
  39. package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
  40. package/dist/orchestrator/test-data-guide.js +8 -0
  41. package/dist/orchestrator/test-data-guide.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/cli/commands/audit.ts +18 -6
  44. package/src/dashboard/templates/index.html +1 -1
  45. package/src/exporters/csv-exporter.ts +3 -1
  46. package/src/exporters/feature-parser.ts +16 -0
  47. package/src/exporters/xlsx-report-builder.ts +3 -1
  48. package/src/harness/audit.ts +49 -11
  49. package/src/harness/flow-contract.ts +100 -11
  50. package/src/harness/parse.ts +22 -3
  51. package/src/harness/quality-gates.ts +48 -5
  52. package/src/harness/sensors.ts +47 -4
  53. package/src/harness/viewpoint-ledger.ts +16 -1
  54. package/src/orchestrator/templates/ai-src/commands/create-test.md +17 -0
  55. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +62 -5
  56. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-e-identity.md +1 -1
  57. package/src/orchestrator/test-data-guide.ts +8 -0
@@ -636,10 +636,21 @@ those namespaces are the **System INTEGRATION** group — keep their oracles abo
636
636
 
637
637
  **Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
638
638
  `sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
639
- `FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). No contract yet author it with the user via the
640
- boundary checklist in `add-flow` (one business goal · clear trigger · ONE observable outcome
641
- valuable to the actor · name = "Verb + outcome"), THEN generate. **A filled contract is an INPUT —
642
- never rewrite it to match your output** (same rule as `test-viewpoint.md`).
639
+ `FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). **A filled contract is an INPUT never rewrite it
640
+ to match your output** (same rule as `test-viewpoint.md`).
641
+
642
+ > **HARD RULE the contract must be AUTHORED, never merely cited.** If
643
+ > `requirements/flow-contract.yaml` is absent, WRITE it (with the user, via the boundary checklist
644
+ > in `add-flow`: one business goal · clear trigger · ONE observable outcome valuable to the actor ·
645
+ > name = "Verb + outcome") **before** generating scenarios. Citing a contract you did not create
646
+ > leaves the flow scored as a generic screen — `flowCoverage` never applies, `coverage` falls back
647
+ > to page-type themes that do not fit a journey, and the audit reports `FLOW-CONTRACT-MISSING`.
648
+ > A contract also needs **both** guarantees, not just the happy one:
649
+ >
650
+ > - `successGuarantee` — everything true once the goal IS reached (what the Basic Flow proves).
651
+ > - `minimalGuarantee` — what must still hold when the journey **FAILS** (no duplicate record, no
652
+ > mail on a rejected submit, no half-written state). Without it an Exception Flow has nothing to
653
+ > assert against beyond "an error appeared", and `FLOW-GUARANTEE-MISSING` is reported.
643
654
 
644
655
  | Aspect | Screen | Flow |
645
656
  |---|---|---|
@@ -647,7 +658,7 @@ never rewrite it to match your output** (same rule as `test-viewpoint.md`).
647
658
  | Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
648
659
  | Test data keys | `{{variable}}` | `{{phase.variable}}` |
649
660
  | Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
650
- | Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — phases: `HP` (happy path), `ER` (error recovery), `EH` (guards), `UI` (journey UI states, optional) |
661
+ | Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — use-case phases `BF`/`AF0n`/`EF0n` (see below), or the flat `HP` (happy path) / `ER` (error recovery) / `EH` (guards) / `UI` for a short single-outcome flow |
651
662
 
652
663
  **Scenarios to generate — every phase demanded by the contract, automated:**
653
664
 
@@ -659,6 +670,52 @@ never rewrite it to match your output** (same rule as `test-viewpoint.md`).
659
670
  | Cross-screen handoff | After every screen transition, assert the CARRIED state on the new screen (the added product's name in the cart, the email echoed on the sent screen). | blind tails cap `businessDepth` (`FLOW-HANDOFF-SHALLOW`) |
660
671
  | Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
661
672
 
673
+ ### Use-case decomposition — how a flow becomes 15 scenarios instead of 3
674
+
675
+ `HP`/`ER`/`EH` are phase *buckets*; they do not tell you how many flows a use case HAS. The
676
+ use-case framework does, and it is the default decomposition for a multi-screen journey:
677
+
678
+ **1 Use Case = 1 Basic Flow + N Alternate Flows + N Exception Flows.**
679
+
680
+ | Phase | Meaning | Test |
681
+ |---|---|---|
682
+ | `BF` Basic Flow | **Exactly one.** The path where nothing goes wrong, end to end. | Reaches `outcome.assertion` and satisfies `successGuarantee`. |
683
+ | `AF` Alternate Flow | A **designed-for branch** — the user does something else legitimate. NOT an error. | Its own outcome: either re-enters the BF at a named step, or ends elsewhere on purpose. |
684
+ | `EF` Exception Flow | **Blocked.** The goal is not reached. | Asserts `minimalGuarantee` — what must STILL be true now that this failed. |
685
+
686
+ `AF` is the bucket most generators skip entirely, and it is where the interesting bugs live
687
+ (going back and finding the form re-hydrated, two tabs, an escape hatch out of the journey).
688
+ Use `FL-BF-NNN` / `FL-AF0n-NNN` / `FL-EF0n-NNN` ids — `sungen audit` buckets them (BF→business
689
+ core, AF→behaviour, EF→validation/security) and traces them to the viewpoint's `FL-BF`/`FL-AF`/
690
+ `FL-EF` priority rows. Declare `phases: [BF, AF, EF]` in the contract; the FIRST phase is the
691
+ Basic Flow and is the one that must reach `outcome`.
692
+
693
+ **Declare once, then declare only the differences.** Actor · Trigger · Goal · Precondition ·
694
+ `successGuarantee` · `minimalGuarantee` are use-case-level: they live in `flow-contract.yaml`
695
+ and are never repeated per flow. Each flow in `test-viewpoint.md` then states only three things:
696
+ **where it branches from · its own steps · its own outcome.** Repeating the precondition in
697
+ fifteen rows is noise; the branch point is the information.
698
+
699
+ **Enumerate flows with the step × risk matrix.** Walk every BF step and ask each column. A hit
700
+ with a basis in the spec becomes a flow; a hit with no basis becomes an **open question** — never
701
+ an invented behaviour:
702
+
703
+ | Risk family | The question at this step |
704
+ |---|---|
705
+ | Double submit | Is the control tapped twice guarded? One request, one record? (Spec silent → open question, not an assumed guard.) |
706
+ | Client-side buffer | Is state held client-side between this step and the next? Then test **both** directions — it survives a legitimate back, and it is **LOST** on reload. A suite proving only the surviving side has tested half the mechanism (`CONTINUITY-ONE-SIDED`). |
707
+ | Concurrency | Two tabs / two devices at this step — whose state wins? |
708
+ | Server error | This step's submit fails server-side: what is kept, what is retryable, is anything half-written? |
709
+ | Abandonment / TTL | The user stops here. Does the partial state lapse, expire, or linger forever? |
710
+ | Direct access | This step's URL opened cold, without the preceding state → refused, and the screen never renders. |
711
+ | Auth transition | Where exactly does the session begin? Before that point the actor is unauthenticated — prove it. |
712
+ | Escape hatch | Is there a documented way OUT of the journey here (back to top, cancel)? That is an `AF`, and its outcome is "goal deliberately not reached". |
713
+
714
+ **Do NOT re-derive field validation.** Per-field equivalence and boundary values belong to the
715
+ OWNING SCREEN's suite. A flow takes representative inputs (see the system-test rule above). State
716
+ that exclusion in the viewpoint's Design Decisions — and if the screen suite does not exist, that
717
+ is a gap to REPORT, not to quietly absorb.
718
+
662
719
  **Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
663
720
  that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
664
721
  DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
@@ -107,7 +107,7 @@ See `SKILL.md` for the 4 Viewpoints, Shared Checks, and Security Tag Rules.
107
107
 
108
108
  **[VP-VAL] Edge cases**
109
109
 
110
- - [@normal] Register with a sub-address email (user+tag@gmail.com) → treated as a unique email, creation succeeds
110
+ - [@normal] Register with a sub-address email (user+tag@sun-asterisk.com) → treated as a unique email, creation succeeds
111
111
  - [@low] Browser autofill fills the fields → the form receives the correct values, no conflict with a custom input component
112
112
 
113
113
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"test-data-guide.d.ts","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":"AAaA,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBvD;AA2GD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAoB1D"}
1
+ {"version":3,"file":"test-data-guide.d.ts","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":"AAaA,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBvD;AAmHD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAoB1D"}
@@ -85,6 +85,14 @@ payload \`<script>alert(1)</script>\`; the exact expected error message from the
85
85
  **Self-check:** *"If I swapped this for another value of the same kind, would the
86
86
  test still mean the same thing?"* → yes = INVENTED.
87
87
 
88
+ **Email domains — never a live mailbox provider.** An invented address still gets
89
+ mail sent to it: a registration or password-reset test submits it and the system
90
+ dispatches for real. \`@gmail.com\`, \`@yahoo.com\`, \`@outlook.com\` and friends all
91
+ resolve, so that mail reaches whoever owns the address — a stranger, or someone's
92
+ personal account. Use the project domain \`@sun-asterisk.com\`, or \`@example.com\`
93
+ (RFC 2606 — reserved, guaranteed never to deliver). Make each run unique with
94
+ \`{{$timestamp}}\` in the local part: \`"signup+{{\$timestamp}}@sun-asterisk.com"\`.
95
+
88
96
  ## 2) ENV-BOUND — real records of the test environment (not secret)
89
97
 
90
98
  **What it is:** the value must EXIST in the environment for the test to work — a
@@ -1 +1 @@
1
- {"version":3,"file":"test-data-guide.js","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,wCAgBC;AA+GD,4DAoBC;AAjKD;;;;;;;;;GASG;AACH,uCAAyB;AACzB,2CAA6B;AAE7B,oEAAoE;AACpE,SAAgB,cAAc,CAAC,QAAgB;IAC7C,OAAO;QACL,KAAK,QAAQ,+DAA+D;QAC5E,+EAA+E;QAC/E,sBAAsB;QACtB,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,iCAAiC;QACjC,8EAA8E;QAC9E,yEAAyE;QACzE,8EAA8E;QAC9E,0EAA0E;QAC1E,+EAA+E;QAC/E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FhB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAC;AAEF;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,GAAW;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,6EAA6E;QAC7E,oEAAoE;QACpE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1F,EAAE,CAAC,cAAc,CAAC,aAAa,EAAE,iFAAiF,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,+EAA+E,EAAE,OAAO,CAAC,CAAC;IAC5H,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"test-data-guide.js","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,wCAgBC;AAuHD,4DAoBC;AAzKD;;;;;;;;;GASG;AACH,uCAAyB;AACzB,2CAA6B;AAE7B,oEAAoE;AACpE,SAAgB,cAAc,CAAC,QAAgB;IAC7C,OAAO;QACL,KAAK,QAAQ,+DAA+D;QAC5E,+EAA+E;QAC/E,sBAAsB;QACtB,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,iCAAiC;QACjC,8EAA8E;QAC9E,yEAAyE;QACzE,8EAA8E;QAC9E,0EAA0E;QAC1E,+EAA+E;QAC/E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoGhB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAC;AAEF;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,GAAW;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,6EAA6E;QAC7E,oEAAoE;QACpE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1F,EAAE,CAAC,cAAc,CAAC,aAAa,EAAE,iFAAiF,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,+EAA+E,EAAE,OAAO,CAAC,CAAC;IAC5H,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.23",
3
+ "version": "3.2.24-beta.1",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -39,8 +39,8 @@
39
39
  "@babel/types": "^7.28.5",
40
40
  "@cucumber/gherkin": "^37.0.0",
41
41
  "@cucumber/messages": "^31.0.0",
42
- "@sungen/driver-data-factory": "3.2.23",
43
- "@sungen/driver-ui": "3.2.23",
42
+ "@sungen/driver-data-factory": "3.2.24-beta.1",
43
+ "@sungen/driver-ui": "3.2.24-beta.1",
44
44
  "chalk": "^5.6.2",
45
45
  "commander": "^14.0.2",
46
46
  "dotenv": "^17.2.3",
@@ -29,11 +29,19 @@ function render(r: AuditReport): void {
29
29
  L(` Quality score (business-weighted): ${r.score.overall}/10 [Viewpoint Gate: ${r.gateStatus}]`);
30
30
  L(` Intent: focus=${r.intent.focus} · depth threshold ${r.depth.threshold.toFixed(2)} (${r.intent.source})`);
31
31
  L(` Provenance: sungen ${r.provenance.sungenVersion} · catalog ${r.provenance.catalogHash} (same here on every machine → same score)`);
32
- L(` coverage ${bar(r.score.coverage)} ${(r.score.coverage * 100).toFixed(0)}%`);
33
- L(` businessDepth ${bar(r.score.businessDepth)} ${(r.score.businessDepth * 100).toFixed(0)}% [${r.depth.verdict.toUpperCase()}]`);
34
- L(` claimProof ${bar(r.claim.ratio)} ${(r.claim.ratio * 100).toFixed(0)}% [${r.claim.verdict.toUpperCase()}]`);
35
- L(` balance ${bar(r.score.balance)} ${(r.score.balance * 100).toFixed(0)}%`);
36
- L(` traceability ${bar(r.score.traceability)} ${(r.score.traceability * 100).toFixed(0)}%`);
32
+ // Print the axes the score was ACTUALLY built from — a fixed five-line block omitted a flow's
33
+ // `flowCoverage`, `specFR` and `atomicLedger` while showing `balance`, which a contract flow
34
+ // does not score on.
35
+ const verdictOf: Record<string, string> = {
36
+ businessDepth: ` [${r.depth.verdict.toUpperCase()}]`,
37
+ claimProof: ` [${r.claim.verdict.toUpperCase()}]`,
38
+ };
39
+ const width = Math.max(...r.score.axes.map((a) => a.key.length));
40
+ for (const a of r.score.axes) {
41
+ const label = a.key.padEnd(width);
42
+ if (!a.applicable) { L(` ${label} ${' '.repeat(20)} n/a — no evidence`); continue; }
43
+ L(` ${label} ${bar(a.value)} ${(a.value * 100).toFixed(0)}%${verdictOf[a.key] ?? ''}`);
44
+ }
37
45
  L(` (${r.score.formula})`);
38
46
  L('');
39
47
  L(` ① Viewpoint Gate — page-type: ${r.gate.pageType ?? 'unknown'} — themes ${r.gate.themesCovered}/${r.gate.themesTotal} covered`);
@@ -60,7 +68,11 @@ function render(r: AuditReport): void {
60
68
  L(` ③·5 Taxonomy — ${r.taxonomy.mislabeled.length === 0 ? '✓ VP codes match scenario semantics' : `⚠ ${r.taxonomy.mislabeled.length} mislabel(s)`}`);
61
69
  for (const m of r.taxonomy.mislabeled.slice(0, 6)) L(` ⚠ ${m.name} → VP-${m.current} reads as ${m.suggested} ("${m.signal}")`);
62
70
  L('');
63
- L(` ④ Coverage balance ${r.balance.imbalanced ? '⚠ IMBALANCED' : '✓ balanced'}`);
71
+ // A use-case decomposition is lopsided BY CONSTRUCTION (1 basic flow, N exceptions), so the
72
+ // verdict is withheld where the axis carries no weight — printing "IMBALANCED" there told
73
+ // authors to pad the suite to fix a number that was never being scored.
74
+ const balanceScored = r.score.axes.find((a) => a.key === 'balance')?.applicable !== false;
75
+ L(` ④ Coverage balance — ${balanceScored ? (r.balance.imbalanced ? '⚠ IMBALANCED' : '✓ balanced') : 'not scored for a contract flow (the phases are)'}`);
64
76
  L(` buckets: ${Object.entries(r.balance.byBucket).map(([k, v]) => `${k}=${v}`).join(' ')}`);
65
77
  L('');
66
78
  L(` ⑤ Duplicate — ${r.duplicates.clusters.length} same-shape cluster(s), ${r.duplicates.exactDuplicateCount} likely exact dup(s)`);
@@ -117,7 +117,7 @@ use chrome, FireFox or Internet Explorer 11`)}var a=e(`safe-buffer`),o=e(`random
117
117
  \r -퟿-�𐀀-􏿿`,n.S=` \r
118
118
  `,n.NAME_START_CHAR=`:A-Z_a-zÀ-ÖØ-öø-˿Ͱ-ͽͿ-῿‌‍⁰-↏Ⰰ-⿯、-퟿豈-﷏ﷰ-�𐀀-󯿿`,n.NAME_CHAR=`-`+n.NAME_START_CHAR+`.0-9·̀-ͯ‿-⁀`,n.CHAR_RE=RegExp(`^[`+n.CHAR+`]$`,`u`),n.S_RE=RegExp(`^[`+n.S+`]+$`,`u`),n.NAME_START_CHAR_RE=RegExp(`^[`+n.NAME_START_CHAR+`]$`,`u`),n.NAME_CHAR_RE=RegExp(`^[`+n.NAME_CHAR+`]$`,`u`),n.NAME_RE=RegExp(`^[`+n.NAME_START_CHAR+`][`+n.NAME_CHAR+`]*$`,`u`),n.NMTOKEN_RE=RegExp(`^[`+n.NAME_CHAR+`]+$`,`u`);function r(e){return e>=65&&e<=90||e>=97&&e<=122||e===58||e===95||e===8204||e===8205||e>=192&&e<=214||e>=216&&e<=246||e>=248&&e<=767||e>=880&&e<=893||e>=895&&e<=8191||e>=8304&&e<=8591||e>=11264&&e<=12271||e>=12289&&e<=55295||e>=63744&&e<=64975||e>=65008&&e<=65533||e>=65536&&e<=983039}n.S_LIST=[32,10,13,9],n.isChar=function(e){return e>=32&&e<=55295||e===10||e===13||e===9||e>=57344&&e<=65533||e>=65536&&e<=1114111},n.isS=function(e){return e===32||e===10||e===13||e===9},n.isNameStartChar=r,n.isNameChar=function(e){return r(e)||e>=48&&e<=57||e===45||e===46||e===183||e>=768&&e<=879||e>=8255&&e<=8256}},{}],544:[function(e,t,n){Object.defineProperty(n,"__esModule",{value:!0}),n.CHAR=`-퟿-�𐀀-􏿿`,n.RESTRICTED_CHAR=`-\b\v\f--„†-Ÿ`,n.S=` \r
119
119
  `,n.NAME_START_CHAR=`:A-Z_a-zÀ-ÖØ-öø-˿Ͱ-ͽͿ-῿‌‍⁰-↏Ⰰ-⿯、-퟿豈-﷏ﷰ-�𐀀-󯿿`,n.NAME_CHAR=`-`+n.NAME_START_CHAR+`.0-9·̀-ͯ‿-⁀`,n.CHAR_RE=RegExp(`^[`+n.CHAR+`]$`,`u`),n.RESTRICTED_CHAR_RE=RegExp(`^[`+n.RESTRICTED_CHAR+`]$`,`u`),n.S_RE=RegExp(`^[`+n.S+`]+$`,`u`),n.NAME_START_CHAR_RE=RegExp(`^[`+n.NAME_START_CHAR+`]$`,`u`),n.NAME_CHAR_RE=RegExp(`^[`+n.NAME_CHAR+`]$`,`u`),n.NAME_RE=RegExp(`^[`+n.NAME_START_CHAR+`][`+n.NAME_CHAR+`]*$`,`u`),n.NMTOKEN_RE=RegExp(`^[`+n.NAME_CHAR+`]+$`,`u`);function r(e){return e>=65&&e<=90||e>=97&&e<=122||e===58||e===95||e===8204||e===8205||e>=192&&e<=214||e>=216&&e<=246||e>=248&&e<=767||e>=880&&e<=893||e>=895&&e<=8191||e>=8304&&e<=8591||e>=11264&&e<=12271||e>=12289&&e<=55295||e>=63744&&e<=64975||e>=65008&&e<=65533||e>=65536&&e<=983039}n.S_LIST=[32,10,13,9],n.isChar=function(e){return e>=1&&e<=55295||e>=57344&&e<=65533||e>=65536&&e<=1114111},n.isRestrictedChar=function(e){return e>=1&&e<=8||e===11||e===12||e>=14&&e<=31||e>=127&&e<=132||e>=134&&e<=159},n.isCharAndNotRestricted=function(e){return e===9||e===10||e===13||e>31&&e<127||e===133||e>159&&e<=55295||e>=57344&&e<=65533||e>=65536&&e<=1114111},n.isS=function(e){return e===32||e===10||e===13||e===9},n.isNameStartChar=r,n.isNameChar=function(e){return r(e)||e>=48&&e<=57||e===45||e===46||e===183||e>=768&&e<=879||e>=8255&&e<=8256}},{}],545:[function(e,t,n){function r(e){return e>=65&&e<=90||e===95||e>=97&&e<=122||e>=192&&e<=214||e>=216&&e<=246||e>=248&&e<=767||e>=880&&e<=893||e>=895&&e<=8191||e>=8204&&e<=8205||e>=8304&&e<=8591||e>=11264&&e<=12271||e>=12289&&e<=55295||e>=63744&&e<=64975||e>=65008&&e<=65533||e>=65536&&e<=983039}Object.defineProperty(n,"__esModule",{value:!0}),n.NC_NAME_START_CHAR=`A-Z_a-zÀ-ÖØ-öø-˿Ͱ-ͽͿ-῿‌-‍⁰-↏Ⰰ-⿯、-퟿豈-﷏ﷰ-�𐀀-󯿿`,n.NC_NAME_CHAR=`-`+n.NC_NAME_START_CHAR+`.0-9·̀-ͯ‿-⁀`,n.NC_NAME_START_CHAR_RE=RegExp(`^[`+n.NC_NAME_START_CHAR+`]$`,`u`),n.NC_NAME_CHAR_RE=RegExp(`^[`+n.NC_NAME_CHAR+`]$`,`u`),n.NC_NAME_RE=RegExp(`^[`+n.NC_NAME_START_CHAR+`][`+n.NC_NAME_CHAR+`]*$`,`u`),n.isNCNameStartChar=r,n.isNCNameChar=function(e){return r(e)||e===45||e===46||e>=48&&e<=57||e===183||e>=768&&e<=879||e>=8255&&e<=8256}},{}]},{},[15])(15)}))})),aN=l(rN(),1),oN=l(iN()),sN=`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`,cN=`FFD9D2E9`,lN=`FF000000`,uN=`FFD9D2E9`,dN=`FFBFBFBF`,fN=`Times New Roman`,pN=`Arial`,mN={argb:`FF000000`},hN={argb:`FFFFFFFF`},gN={argb:`FFD9D2E9`},_N={style:`thin`,color:mN},vN={top:_N,left:_N,bottom:_N,right:_N};function yN(e){e.border={top:{style:`thin`,color:{argb:dN}},left:{style:`thin`,color:{argb:dN}},bottom:{style:`thin`,color:{argb:dN}},right:{style:`thin`,color:{argb:dN}}}}function bN(e,t,n,r,i=`BM-2-901-13`){let a=(()=>{let e=new Date;return`${String(e.getDate()).padStart(2,`0`)}/${String(e.getMonth()+1).padStart(2,`0`)}/${e.getFullYear()}`})();for(let e=1;e<=4;e++)t.getRow(e);t.mergeCells(`A1:C3`),t.mergeCells(`D1:F3`),t.mergeCells(`G1:H1`),t.mergeCells(`G2:H2`),t.mergeCells(`G3:H3`),t.mergeCells(`A4:F4`),t.mergeCells(`G4:H4`);let o=t.getCell(`A1`);o.alignment={horizontal:`center`,vertical:`middle`},o.border=vN;try{let n=e.addImage({base64:sN,extension:`png`});t.addImage(n,{tl:{nativeCol:0,nativeColOff:1237500,nativeRow:0,nativeRowOff:42862},ext:{width:90,height:51},editAs:`oneCell`})}catch{}let s=t.getCell(`D1`);s.value=n,s.font={bold:!0,size:18,name:fN},s.alignment={horizontal:`center`,vertical:`middle`,wrapText:!0},s.border=vN;let c=t.getCell(`G1`);c.value=`No: ${i}`,c.font={size:12,name:fN},c.alignment={vertical:`middle`},c.border={top:_N,left:_N,right:_N};let l=t.getCell(`G2`);l.value=`Version: ${r??`unknown`}`,l.font={size:12,name:fN},l.alignment={vertical:`middle`},l.border={left:_N,right:_N};let u=t.getCell(`G3`);u.value=`Issue Date: ${a}`,u.font={size:12,name:fN},u.alignment={vertical:`middle`},u.border={left:_N,right:_N,bottom:_N};let d=t.getCell(`A4`);d.value=`SUN ASTERISK VIETNAM CO., LTD`,d.font={bold:!0,size:10,name:fN},d.alignment={vertical:`middle`},d.border=vN;let f=t.getCell(`G4`);f.value=`ISO/IEC 27001:2022 & ISO 9001:2015`,f.font={bold:!0,size:10,name:fN},f.alignment={horizontal:`right`,vertical:`middle`,wrapText:!0},f.border=vN}var xN=[`#`,`Sheet name`,`Passed`,`Failed`,`Pending`,`N/A`,`Remain`,`Total`];function SN(e,t,n){let r=e.addWorksheet(`Overview`);r.columns=[{width:4},{width:6},{width:26},{width:13},{width:13},{width:13},{width:13},{width:13},{width:13}],bN(e,r,`${(t[0]?.summary.screen??``).toUpperCase()} TESTCASE`,n);let i=r.getRow(7);i.height=20,xN.forEach((e,t)=>{let n=i.getCell(2+t);n.value=e,n.font={bold:!0,size:10,color:mN,name:pN},n.fill={type:`pattern`,pattern:`solid`,fgColor:gN},n.alignment={horizontal:`center`,vertical:`middle`,wrapText:!0},n.border=vN});let a={passed:0,failed:0,pending:0,na:0,remain:0,total:0},o=(e,t,n)=>{let i=r.getRow(e);t.forEach((e,t)=>{let r=i.getCell(2+t);r.value=e,r.font={bold:n,size:10,color:mN,name:pN},r.alignment={horizontal:t<=1?`left`:`center`,vertical:`middle`},r.border=vN,n&&(r.fill={type:`pattern`,pattern:`solid`,fgColor:gN})})},s=e=>e.replace(/'/g,`''`),c=(e,t,n)=>({formula:`'${s(e)}'!${t}`,result:n}),l=8,u=l;t.forEach((e,t)=>{let n=e.summary,r=Math.max(0,n.total-n.passed-n.na);o(l,[t+1,e.sheetName,c(e.sheetName,`D7`,n.passed),c(e.sheetName,`E7`,n.failed),c(e.sheetName,`F7`,n.pending),c(e.sheetName,`G7`,n.na),c(e.sheetName,`H7`,r),c(e.sheetName,`C7`,n.total)],!1),a.passed+=n.passed,a.failed+=n.failed,a.pending+=n.pending,a.na+=n.na,a.remain+=r,a.total+=n.total,l+=1});let d=l-1,f=(e,t)=>({formula:`SUM(${e}${u}:${e}${d})`,result:t});o(l,[``,`Total`,f(`D`,a.passed),f(`E`,a.failed),f(`F`,a.pending),f(`G`,a.na),f(`H`,a.remain),f(`I`,a.total)],!0)}function CN(e,t,n,r,i,a){let o=e.addWorksheet(t,{views:[{state:`frozen`,ySplit:0,xSplit:0}]});o.columns=[{width:20},{width:12.5},{width:15.38},{width:16.25},{width:17},{width:25.5},{width:24.88},{width:50},{width:41},{width:12},{width:13.5},{width:15.88},{width:13.5},{width:16.5},{width:23},{width:29}],bN(e,o,`${n.screen.toUpperCase()} TESTCASE`,a);for(let e=5;e<=8;e++)o.getRow(e);for(let[e,t]of Object.entries({C:`Total TCs`,D:`Passed`,E:`Failed`,F:`Pending`,G:`N/A`,H:`Remaining`})){let n=o.getCell(`${e}6`);n.value=t,n.font={bold:!0,size:10,color:mN,name:pN},n.fill={type:`pattern`,pattern:`solid`,fgColor:gN},n.alignment={horizontal:`center`,vertical:`top`},n.border=vN}let s=1e4,c={C:{formula:`COUNTA(A14:A${s})`,result:n.total},D:{formula:`COUNTIF(L14:L${s},"Passed")`,result:n.passed},E:{formula:`COUNTIF(L14:L${s},"Failed")`,result:n.failed},F:{formula:`COUNTIF(L14:L${s},"Pending")`,result:n.pending},G:{formula:`COUNTIF(L14:L${s},"N/A")`,result:n.na},H:{formula:`C7-D7-G7`,result:n.failed+n.pending}};for(let[e,t]of Object.entries(c)){let n=o.getCell(`${e}7`);n.value=t,n.font={size:10,color:mN,name:pN},n.fill={type:`pattern`,pattern:`solid`,fgColor:hN},n.alignment={horizontal:`center`,vertical:`top`},n.border=vN,e===`H`&&(n.numFmt=`#,##0`)}let l=o.getCell(`C8`);l.font={size:10,color:mN,name:pN},l.fill={type:`pattern`,pattern:`solid`,fgColor:hN},l.alignment={horizontal:`center`,vertical:`top`},l.border=vN;let u={D:{formula:`IFERROR(D7/(C7-G7),0)`,result:n.passed/Math.max(1,n.total-n.na)},E:{formula:`IFERROR(E7/(C7-G7),0)`,result:n.failed/Math.max(1,n.total-n.na)},F:{formula:`IFERROR(F7/(C7-G7),0)`,result:n.pending/Math.max(1,n.total-n.na)},G:{formula:`IFERROR(G7/(C7-G7),0)`,result:n.na/Math.max(1,n.total-n.na)},H:{formula:`IFERROR(H7/(C7-G7),0)`,result:(n.failed+n.pending)/Math.max(1,n.total-n.na)}};for(let[e,t]of Object.entries(u)){let n=o.getCell(`${e}8`);n.value=t,n.font={size:10,color:mN,name:pN},n.fill={type:`pattern`,pattern:`solid`,fgColor:hN},n.alignment={horizontal:`center`,vertical:`top`},n.border=vN,n.numFmt=`0%`}for(;o.rowCount<9;)o.addRow([]);let d=o.addRow([`Spec/Design link:`,i]);d.getCell(1).font={bold:!0},d.getCell(2).font={color:{argb:`FF1F4E78`},underline:!0},o.addRow([]);let f=o.addRow([`*: Mandatory`]);f.getCell(1).font={italic:!0,color:{argb:`FF808080`}};let p=o.addRow([`TC ID*`,`Screen/Function`,`Big item`,`Medium item`,`Test Object`,`Pre-condition`,`Test Data`,`Steps*`,`Expected results*`,`Priority`,`Testcase type`,`Test Result*`,`Executed Date*`,`Test Executor*`,`Test Environment`,`Note
120
- (Test evidence, DefectID, Actual result)`]);p.height=38,p.eachCell(e=>{e.font={bold:!0,color:{argb:lN}},e.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:cN}},e.alignment={horizontal:`center`,vertical:`middle`,wrapText:!0},yN(e)}),o.views=[{state:`normal`}];let m=[`Accessing`,`GUI`,`Function`],h=new Map;for(let e of r){let t=h.get(e.category2)||[];t.push(e),h.set(e.category2,t)}let g=new Set;function _(e,t){let n=o.addRow([]);n.getCell(2).value=e,n.getCell(2).font={bold:!0},n.eachCell({includeEmpty:!1},e=>{e.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:uN}}});for(let e=1;e<=16;e++){let t=n.getCell(e);t.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:uN}},yN(t)}for(let e of t){let t=o.addRow([e.tcId,``,``,``,e.category1,e.precondition,e.testData,e.steps,e.expectedResults,e.priority,e.testcaseType,e.testResult,e.executedDate,e.testExecutor,e.testEnvironment,e.note]);t.alignment={vertical:`top`,wrapText:!0},t.eachCell({includeEmpty:!0},(e,t)=>{if(yN(e),t===1&&(e.font={bold:!0}),t===12){let t=String(e.value||``);t===`Passed`?e.font={color:{argb:`FF107C41`},bold:!0}:t===`Failed`?e.font={color:{argb:`FFC00000`},bold:!0}:t===`Pending`?e.font={color:{argb:`FF996D00`},bold:!0}:t===`N/A`&&(e.font={color:{argb:`FF808080`}})}})}}for(let e of m){let t=h.get(e);!t||t.length===0||(g.add(e),_(e,t))}for(let[e,t]of h.entries())g.has(e)||_(e,t);o.autoFilter={from:{row:p.number,column:1},to:{row:o.rowCount,column:16}},o.sheetProtection={sheet:!0,objects:!1,scenarios:!1}}var wN=`FF2E5984`,TN=`FFFFFFFF`;function EN(e,t){let n=e.addWorksheet(`API detail`),r=`Arial`;n.columns=[{header:`Endpoint`,width:35},{header:`Method`,width:10},{header:`Auth / Datasource`,width:22},{header:`Request shape`,width:40},{header:`Expected-status matrix`,width:30},{header:`Flow steps`,width:40},{header:`Concurrency invariant`,width:35}];let i=n.getRow(1);i.height=30,i.eachCell(e=>{e.font={bold:!0,color:{argb:TN},name:r},e.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:wN}},e.alignment={horizontal:`center`,vertical:`middle`,wrapText:!0},yN(e)}),n.views=[{state:`frozen`,ySplit:1}];for(let e of t){let t=n.addRow([e.endpoint,e.method,e.authDatasource,e.requestShape,e.expectedStatusMatrix,e.flowSteps,e.concurrencyInvariant]);t.alignment={vertical:`top`,wrapText:!0},t.eachCell({includeEmpty:!0},e=>{yN(e),e.font={name:r}})}n.autoFilter={from:{row:1,column:1},to:{row:n.rowCount,column:7}}}function DN(e,t){if(e.length===0)throw Error(`renderXlsxMultiSheet requires at least one locale sheet`);let n=new oN.default.Workbook;n.creator=`sungen delivery`,n.created=new Date,SN(n,e,t?.sungenVersion);for(let r of e)CN(n,r.sheetName,r.summary,r.rows,r.specLink,t?.sungenVersion);return n}function ON(e){let t=e.features??[],n=e=>e.platform?`${e.featureBaseName}.${e.platform}`:e.featureBaseName;if(t.length>1)return t.map(t=>({basename:n(t),featureName:t.featureName,specLink:t.specLink??e.specLink,summary:t.summary,scenarios:t.scenarios,locales:t.locales,apiDetail:t.apiDetail}));let r=t[0];return[{basename:r?n(r):e.name,featureName:r?.featureName??e.featureName,specLink:r?.specLink??e.specLink,summary:r?.summary??e.summary,scenarios:r?.scenarios??e.scenarios,locales:r?.locales,apiDetail:r?.apiDetail}]}var kN=420*60*1e3;function AN(e,t){let n=IN(e,t.screens),r=VN();for(let t of n)for(let n of ON(t)){let t=n.locales&&n.locales.length>=2?n.locales.map(e=>({locale:e.locale,summary:e.summary,scenarios:e.scenarios})):[{locale:``,summary:n.summary,scenarios:n.scenarios}];for(let i of t){let t=jN(n,e.environment.sungenVersion,i.summary,i.scenarios),a=i.locale?`.${i.locale}`:``;WN(`${n.basename}-testcases${a}_${r}.csv`,t,`text/csv;charset=utf-8`)}}}function jN(e,t,n,r){let i=BN(),a=n??e.summary,o=r??e.scenarios,s=a.total||1,c=e=>`${Math.round(e/s*100)}%`,l=`${e.basename.toUpperCase()} TESTCASE`,u=e.specLink??``,d=[];d.push(UN([``,``,``,l,``,``,`No: BM-2-901-13`,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,`Version: ${t}`,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,`Issue Date: ${i}`,``,``,``,``,``,``,``,``,``])),d.push(UN([`SUN ASTERISK VIETNAM CO., LTD`,``,``,``,``,``,`ISO/IEC 27001:2022 & ISO 9001:2015`,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,`Total TCs`,`Passed`,`Failed`,`Pending`,`N/A`,`Remaining`,``,``,``,``,``,``,``,``])),d.push(UN([``,``,a.total,a.passed,a.failed,a.pending,a.na,a.pending+a.na,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,c(a.passed),c(a.failed),c(a.pending),c(a.na),c(a.pending+a.na),``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([`Spec/Design link:`,u,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([`*: Mandatory`,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([`TC ID*`,`Screen/Function`,`Big item`,`Medium item`,`Test Object`,`Pre-condition`,`Test Data`,`Steps*`,`Expected results*`,`Priority`,`Testcase type`,`Test Result*`,`Executed Date*`,`Test Executor*`,`Test Environment`,`Note
120
+ (Test evidence, DefectID, Actual result)`]);p.height=38,p.eachCell(e=>{e.font={bold:!0,color:{argb:lN}},e.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:cN}},e.alignment={horizontal:`center`,vertical:`middle`,wrapText:!0},yN(e)}),o.views=[{state:`normal`}];let m=[`Basic Flow`,`Alternate Flow`,`Exception Flow`,`Accessing`,`GUI`,`Function`],h=new Map;for(let e of r){let t=h.get(e.category2)||[];t.push(e),h.set(e.category2,t)}let g=new Set;function _(e,t){let n=o.addRow([]);n.getCell(2).value=e,n.getCell(2).font={bold:!0},n.eachCell({includeEmpty:!1},e=>{e.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:uN}}});for(let e=1;e<=16;e++){let t=n.getCell(e);t.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:uN}},yN(t)}for(let e of t){let t=o.addRow([e.tcId,``,``,``,e.category1,e.precondition,e.testData,e.steps,e.expectedResults,e.priority,e.testcaseType,e.testResult,e.executedDate,e.testExecutor,e.testEnvironment,e.note]);t.alignment={vertical:`top`,wrapText:!0},t.eachCell({includeEmpty:!0},(e,t)=>{if(yN(e),t===1&&(e.font={bold:!0}),t===12){let t=String(e.value||``);t===`Passed`?e.font={color:{argb:`FF107C41`},bold:!0}:t===`Failed`?e.font={color:{argb:`FFC00000`},bold:!0}:t===`Pending`?e.font={color:{argb:`FF996D00`},bold:!0}:t===`N/A`&&(e.font={color:{argb:`FF808080`}})}})}}for(let e of m){let t=h.get(e);!t||t.length===0||(g.add(e),_(e,t))}for(let[e,t]of h.entries())g.has(e)||_(e,t);o.autoFilter={from:{row:p.number,column:1},to:{row:o.rowCount,column:16}},o.sheetProtection={sheet:!0,objects:!1,scenarios:!1}}var wN=`FF2E5984`,TN=`FFFFFFFF`;function EN(e,t){let n=e.addWorksheet(`API detail`),r=`Arial`;n.columns=[{header:`Endpoint`,width:35},{header:`Method`,width:10},{header:`Auth / Datasource`,width:22},{header:`Request shape`,width:40},{header:`Expected-status matrix`,width:30},{header:`Flow steps`,width:40},{header:`Concurrency invariant`,width:35}];let i=n.getRow(1);i.height=30,i.eachCell(e=>{e.font={bold:!0,color:{argb:TN},name:r},e.fill={type:`pattern`,pattern:`solid`,fgColor:{argb:wN}},e.alignment={horizontal:`center`,vertical:`middle`,wrapText:!0},yN(e)}),n.views=[{state:`frozen`,ySplit:1}];for(let e of t){let t=n.addRow([e.endpoint,e.method,e.authDatasource,e.requestShape,e.expectedStatusMatrix,e.flowSteps,e.concurrencyInvariant]);t.alignment={vertical:`top`,wrapText:!0},t.eachCell({includeEmpty:!0},e=>{yN(e),e.font={name:r}})}n.autoFilter={from:{row:1,column:1},to:{row:n.rowCount,column:7}}}function DN(e,t){if(e.length===0)throw Error(`renderXlsxMultiSheet requires at least one locale sheet`);let n=new oN.default.Workbook;n.creator=`sungen delivery`,n.created=new Date,SN(n,e,t?.sungenVersion);for(let r of e)CN(n,r.sheetName,r.summary,r.rows,r.specLink,t?.sungenVersion);return n}function ON(e){let t=e.features??[],n=e=>e.platform?`${e.featureBaseName}.${e.platform}`:e.featureBaseName;if(t.length>1)return t.map(t=>({basename:n(t),featureName:t.featureName,specLink:t.specLink??e.specLink,summary:t.summary,scenarios:t.scenarios,locales:t.locales,apiDetail:t.apiDetail}));let r=t[0];return[{basename:r?n(r):e.name,featureName:r?.featureName??e.featureName,specLink:r?.specLink??e.specLink,summary:r?.summary??e.summary,scenarios:r?.scenarios??e.scenarios,locales:r?.locales,apiDetail:r?.apiDetail}]}var kN=420*60*1e3;function AN(e,t){let n=IN(e,t.screens),r=VN();for(let t of n)for(let n of ON(t)){let t=n.locales&&n.locales.length>=2?n.locales.map(e=>({locale:e.locale,summary:e.summary,scenarios:e.scenarios})):[{locale:``,summary:n.summary,scenarios:n.scenarios}];for(let i of t){let t=jN(n,e.environment.sungenVersion,i.summary,i.scenarios),a=i.locale?`.${i.locale}`:``;WN(`${n.basename}-testcases${a}_${r}.csv`,t,`text/csv;charset=utf-8`)}}}function jN(e,t,n,r){let i=BN(),a=n??e.summary,o=r??e.scenarios,s=a.total||1,c=e=>`${Math.round(e/s*100)}%`,l=`${e.basename.toUpperCase()} TESTCASE`,u=e.specLink??``,d=[];d.push(UN([``,``,``,l,``,``,`No: BM-2-901-13`,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,`Version: ${t}`,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,`Issue Date: ${i}`,``,``,``,``,``,``,``,``,``])),d.push(UN([`SUN ASTERISK VIETNAM CO., LTD`,``,``,``,``,``,`ISO/IEC 27001:2022 & ISO 9001:2015`,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,`Total TCs`,`Passed`,`Failed`,`Pending`,`N/A`,`Remaining`,``,``,``,``,``,``,``,``])),d.push(UN([``,``,a.total,a.passed,a.failed,a.pending,a.na,a.pending+a.na,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,c(a.passed),c(a.failed),c(a.pending),c(a.na),c(a.pending+a.na),``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([`Spec/Design link:`,u,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([`*: Mandatory`,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``])),d.push(UN([`TC ID*`,`Screen/Function`,`Big item`,`Medium item`,`Test Object`,`Pre-condition`,`Test Data`,`Steps*`,`Expected results*`,`Priority`,`Testcase type`,`Test Result*`,`Executed Date*`,`Test Executor*`,`Test Environment`,`Note
121
121
  (Test evidence, DefectID, Actual result)`]));let f=[`Accessing`,`GUI`,`Function`],p=new Map;for(let e of o){let t=p.get(e.category2)||[];t.push(e),p.set(e.category2,t)}let m=new Set,h=(e,t)=>{d.push(UN([``,e,``,``,``,``,``,``,``,``,``,``,``,``,``,``]));for(let e of t)d.push(UN(LN(e)))};for(let e of f){let t=p.get(e);!t||t.length===0||(m.add(e),h(e,t))}for(let[e,t]of p.entries())m.has(e)||h(e,t);return``+d.join(`
122
122
  `)+`
123
123
  `}async function MN(e,t){let n=IN(e,t.screens),r=VN();for(let t of n)for(let n of ON(t)){let t=DN(FN(n),{sungenVersion:e.environment.sungenVersion});n.apiDetail&&n.apiDetail.length>0&&EN(t,n.apiDetail);let i=await KN(await t.xlsx.writeBuffer()),a=new Blob([i.buffer],{type:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`}),o=URL.createObjectURL(a);GN(`${n.basename}-testcases_${r}.xlsx`,o),setTimeout(()=>URL.revokeObjectURL(o),1e3)}}function NN(e){let t={total:e.length,passed:0,failed:0,pending:0,na:0,notCompiled:0,passRate:0};for(let n of e)n.status===`Passed`?t.passed++:n.status===`Failed`?t.failed++:n.status===`Pending`?t.pending++:n.status===`N/A`&&t.na++,n.type===`Not compiled`&&t.notCompiled++;let n=t.passed+t.failed;return t.passRate=n>0?t.passed/n:0,t}function PN(e){return{tcId:e.tcId,category1:e.category1,category2:e.category2,precondition:e.precondition,testData:e.testData,steps:RN(e.steps),expectedResults:RN(e.expectedResults),priority:e.priority,testcaseType:e.type,testResult:e.status,executedDate:e.executedDate??``,testExecutor:e.testExecutor??``,testEnvironment:e.testEnvironment??``,note:zN(e)}}function FN(e){let t=e.specLink??``,n=(n,r)=>{let i=NN(r);return{sheetName:n,summary:{screen:e.basename,total:i.total,passed:i.passed,failed:i.failed,pending:i.pending,na:i.na},rows:r.map(PN),specLink:t}},r=[];if(e.locales&&e.locales.length>=2){let t=[];for(let i of e.locales){let e=i.scenarios.filter(e=>e.type!==`Manual`);r.push(n(`${i.displayCode} Auto`,e)),i.locale===``&&(t=i.scenarios.filter(e=>e.type===`Manual`))}t.length===0&&e.locales[0]&&(t=e.locales[0].scenarios.filter(e=>e.type===`Manual`)),r.push(n(`Manual`,t))}else{let t=e.scenarios.filter(e=>e.type!==`Manual`),i=e.scenarios.filter(e=>e.type===`Manual`);r.push(n(`Auto`,t)),r.push(n(`Manual`,i))}return r}function IN(e,t){if(!t||t.length===0)return e.screens;let n=new Set(t);return e.screens.filter(e=>n.has(e.name))}function LN(e){return[e.tcId,``,``,``,e.category1,e.precondition,e.testData,RN(e.steps),RN(e.expectedResults),e.priority,e.type,e.status,e.executedDate??``,e.testExecutor??``,e.testEnvironment??``,zN(e)]}function RN(e){return!e||e.length===0?``:e.length===1?e[0]:e.map((e,t)=>`${t+1}. ${e}`).join(`
@@ -353,7 +353,9 @@ export function renderCsv(summary: ScreenSummary, rows: TestCaseRow[], specLink:
353
353
  ]));
354
354
 
355
355
  // Group rows by Category 2 (in stable order)
356
- const order = ['Accessing', 'GUI', 'Function'];
356
+ // Flow phases lead, in use-case order (the basic flow first, then its branches, then the
357
+ // blocked paths), so a flow deliverable reads as the journey it tests (#592).
358
+ const order = ['Basic Flow', 'Alternate Flow', 'Exception Flow', 'Accessing', 'GUI', 'Function'];
357
359
  const grouped = new Map<string, TestCaseRow[]>();
358
360
  for (const row of rows) {
359
361
  const g = grouped.get(row.category2) || [];
@@ -186,9 +186,25 @@ export function splitVpAndName(scenarioName: string): { vpId?: string; category1
186
186
  * API auth viewpoints (missing/invalid/insufficient credentials) are access-control
187
187
  * tests → Accessing; every other API viewpoint (contract, error, idempotency, flow,
188
188
  * async, and the numbered baseline ids) is functional → Function.
189
+ *
190
+ * Flow ids carry the use-case decomposition (#592) and group by it, so the deliverable shows
191
+ * the reviewer WHICH flow each case belongs to instead of collapsing a whole journey into one
192
+ * "Function" block: `FL-BF-*` → Basic Flow, `FL-AF0n-*` → Alternate Flow, `FL-EF0n-*` →
193
+ * Exception Flow. The flat scheme keeps its old mapping (`FL-HP`/`FL-ER` functional,
194
+ * `FL-EH` guards → Accessing).
189
195
  */
190
196
  export function mapVpToCategory2(vpId: string | undefined, scenarioName?: string): string {
191
197
  if (!vpId) return 'Function';
198
+ const flow = vpId.match(/^(?:VP-)?FL-([A-Z]{2})\d*(?:-|$)/i);
199
+ if (flow) {
200
+ const phase = flow[1].toUpperCase();
201
+ if (phase === 'BF') return 'Basic Flow';
202
+ if (phase === 'AF') return 'Alternate Flow';
203
+ if (phase === 'EF') return 'Exception Flow';
204
+ if (phase === 'EH') return 'Accessing';
205
+ if (phase === 'UI') return 'GUI';
206
+ return 'Function'; // HP / ER — the flat scheme
207
+ }
192
208
  // Compare on the category segment so a project scheme without the `VP-` prefix
193
209
  // (`SEC-123`) classifies the same as `VP-SEC-001`.
194
210
  vpId = /^VP-/.test(vpId) ? vpId : `VP-${vpId}`;
@@ -501,7 +501,9 @@ export function addTestcaseSheet(
501
501
  ws.views = [{ state: 'normal' }];
502
502
 
503
503
  // -- Data rows grouped by category --
504
- const order = ['Accessing', 'GUI', 'Function'];
504
+ // Flow phases lead, in use-case order (the basic flow first, then its branches, then the
505
+ // blocked paths), so a flow deliverable reads as the journey it tests (#592).
506
+ const order = ['Basic Flow', 'Alternate Flow', 'Exception Flow', 'Accessing', 'GUI', 'Function'];
505
507
  const grouped = new Map<string, BuilderRow[]>();
506
508
  for (const r of rows) {
507
509
  const g = grouped.get(r.category2) || [];
@@ -10,7 +10,7 @@ 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';
13
+ import { flowQuality, statefulDepthFor, continuityGaps, FlowQualityResult } from './flow-contract';
14
14
  import { featureFilesFor } from './unit-paths';
15
15
  import {
16
16
  loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
@@ -67,6 +67,13 @@ export interface AuditReport {
67
67
  businessDepth: number; // 0..1
68
68
  balance: number; // 0..1
69
69
  traceability: number; // 0..1
70
+ /**
71
+ * Every axis considered, in weight order, with the renormalised weight it actually
72
+ * carried. The five legacy fields above are a fixed subset that omitted `flowCoverage`,
73
+ * `specFR` and `atomicLedger` entirely — so a flow's real coverage axis was never printed
74
+ * while `balance`, which carried no weight, was.
75
+ */
76
+ axes: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean }>;
70
77
  formula: string;
71
78
  };
72
79
  gateStatus: 'PASS' | 'FAIL';
@@ -286,7 +293,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
286
293
  { key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
287
294
  { key: 'claimProof', value: claim.ratio, weight: 0.15, applicable: claim.withClaims > 0, critical: true },
288
295
  { key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false },
289
- { key: 'balance', value: balanceScore, weight: 0.06, applicable: true, critical: false },
296
+ // A use-case decomposition is EF-heavy by construction (1 basic flow, N alternates, N
297
+ // exceptions) — that is the shape of a well-decomposed use case, not neglected business
298
+ // core. `flowCoverage` already measures whether the journey's phases are covered, so
299
+ // balance carries no independent evidence for a contract flow (#592).
300
+ { key: 'balance', value: balanceScore, weight: 0.06, applicable: !flowScored, critical: false },
290
301
  ];
291
302
  const scored = axisDefs.filter((a) => a.applicable);
292
303
  const weightSum = scored.reduce((t, a) => t + a.weight, 0) || 1;
@@ -313,7 +324,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
313
324
  // axes — a stateful flow with FULL regression depth (count + teardown + multi-source), no weak
314
325
  // facet oracle, and no parallel-cart isolation risk. Otherwise the score is held just below 9, so
315
326
  // "≥9" means senior-grade, not merely "themes covered". Neutral for screens/api (no signals).
316
- const isoRisk = isolationRisk(featureText, flowDepth.stateful);
327
+ const isoRisk = isolationRisk(featureText, flowDepth.stateful, flowQ.contract?.stateful);
317
328
  const overall = seniorBandedOverall(rawOverall, { flowStateful: flowDepth.stateful, flowRatio: flowDepth.ratio, oracleWeak: oracle.weak.length, isolationRisk: isoRisk });
318
329
 
319
330
  const findings: string[] = [];
@@ -356,6 +367,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
356
367
  for (const x of flowQ.sit.filter((v) => v.mockOnly).slice(0, 4)) {
357
368
  findings.push(`SIT-MOCK-ONLY: "${x.name}" touches the external system "${x.external}" but is verified only against mocks — Pass (Mocked) says nothing about the real vendor behaviour. Re-verify on the real dependency before calling it Done (the delivery's External Dependency Tracking table holds the checklist).`);
358
369
  }
370
+ // #592 W1 — an Exception Flow asserts against the Minimal Guarantee; without the
371
+ // declaration there is no anchor and its assertions are guesswork.
372
+ if (!flowQ.contract!.minimalGuarantee) {
373
+ findings.push('FLOW-GUARANTEE-MISSING: the contract declares no `minimalGuarantee:` — the postcondition that must hold in EVERY flow, including failure ("no second account for the same email", "no verification mail on a rejected submit"). Exception/guard scenarios have nothing to assert against without it; add it (and `successGuarantee:` for the goal-reached side).');
374
+ }
359
375
  // #580 P9 — a release-critical flow must carry its Final-Inspection selection.
360
376
  if (flowQ.contract!.golden && !/@golden\b/i.test(featureText)) {
361
377
  findings.push(`GOLDEN-MISSING: the contract declares \`golden: true\` (release-critical) but no scenario is tagged @golden — Final Inspection (\`sungen inspect\`) will have nothing to run for this flow. Tag the happy-path scenario(s) that prove the outcome.`);
@@ -364,6 +380,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
364
380
  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.`);
365
381
  }
366
382
  }
383
+ // #592 W5 — a continuity mechanism proved on one side only.
384
+ if (isUiFlow) {
385
+ const scenarioTexts = scenarios.map((s) => `${s.name} ${s.stepsText}`);
386
+ for (const g of continuityGaps(parseViewpointItems(viewpointPath).map((i) => i.text), scenarioTexts).slice(0, 4)) {
387
+ findings.push(`CONTINUITY-ONE-SIDED: the viewpoint claims "${g.claim}" but the suite proves only the ${g.missing === 'loss' ? 'SURVIVING' : 'LOSING'} side — a persistence mechanism has two sides and testing one is testing half the mechanism. Add the ${g.missing === 'loss' ? 'loss' : 'persistence'} scenario (e.g. ${g.missing === 'loss' ? 'reload / clear storage between the steps and assert the values are gone' : 'return to the step and assert the values are restored'}).`);
388
+ }
389
+ }
367
390
  if (isUiFlow) {
368
391
  // Flows defer more than screens, so an UNCODED @manual is costlier here: the capability
369
392
  // planner cannot route it to the driver that would automate it (password-reset: 11/22
@@ -399,7 +422,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
399
422
  // isolation, else count/quantity asserts go flaky. Warn (advisory) when no mitigation is present
400
423
  // (@cleanup:cart / @isolate / a "Given … empty cart" background / fresh context).
401
424
  if (isoRisk) {
402
- findings.push('ISOLATION-RISK: this @parallel flow mutates the cart but has no per-scenario isolation → cart/count/quantity asserts can go flaky when scenarios share state. Add `@cleanup:cart` (or `@isolate`, or a `Given User has an empty cart` background) so each scenario starts clean.');
425
+ const what = flowQ.contract?.stateful ?? 'cart';
426
+ findings.push(`ISOLATION-RISK: this @parallel flow mutates ${what} but has no per-scenario isolation → count/quantity/state asserts can go flaky when scenarios share it. Add \`@cleanup:${what}\` (or \`@isolate\`, a fresh context, or \`{{$timestamp}}\` data) so each scenario starts clean.`);
403
427
  }
404
428
  // #414 — serial-cascade masking: an @serial suite of independent (re-navigating) scenarios hides
405
429
  // failures, because Playwright's describe.serial SKIPS everything after the first red. Isolation is
@@ -409,12 +433,19 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
409
433
  findings.push(`SERIAL-CASCADE-RISK: this \`@serial\` suite has ${cascadeN} INDEPENDENT scenarios (each re-navigates via Background) that don't need a shared-page sequence. \`describe.serial\` SKIPS every test after the first failure → one early red MASKS the rest as never-run. **Drop \`@serial\`** — the default per-scenario isolation runs every scenario and isolates failures. Keep \`@serial\` only for a genuinely stateful ordered sequence (where scenario N depends on N-1's state).`);
410
434
  }
411
435
  if (flowDepth.stateful && flowDepth.missing.length) {
436
+ // The advice names the collection the CONTRACT declared. It used to say "the cart" whatever
437
+ // the flow mutated, so a registration journey was told to "add to the cart from EVERY
438
+ // source" — advice its author could not act on (#592).
439
+ const what = flowQ.contract?.stateful ?? 'cart';
412
440
  const how: Record<string, string> = {
413
- 'count-proof': 'assert the cart ROW COUNT / item QUANTITY (e.g. `... table with {{two_rows}}`, `Quantity column with {{qty}}`), not just the row presence',
414
- 'teardown': 'add a REMOVE/clear scenario that returns the cart to its empty state (the inverse operation)',
415
- 'multi-source': 'add to the cart from EVERY source on the page (the main list AND the recommended/related rail), not just one',
441
+ 'count-proof': `assert the ${what} ROW COUNT / item QUANTITY (e.g. \`... table with {{two_rows}}\`, \`Quantity column with {{qty}}\`), not just the row presence`,
442
+ 'teardown': `prove the INVERSE operation the ${what} returning to its empty/absent state (a remove or clear for a collection; a lapse, expiry, reset or loss for a state machine)`,
443
+ 'multi-source': `exercise EVERY source that feeds the ${what} (the main list AND the recommended/related rail), not just one`,
416
444
  };
417
- findings.push(`FLOW-DEPTH: this stateful flow exercises ${3 - flowDepth.missing.length}/3 regression dimensions missing [${flowDepth.missing.join(', ')}] ${flowDepth.missing.map((m) => how[m]).join('; ')}. (businessDepth is capped until covered.)`);
445
+ // How many dimensions this flow can HAVEthe ratio is renormalised over the applicable
446
+ // ones, so "2/3" would misreport a flow that only has two to begin with.
447
+ const dims = flowDepth.ratio < 1 ? Math.round(flowDepth.missing.length / (1 - flowDepth.ratio)) : 3;
448
+ findings.push(`FLOW-DEPTH: this stateful flow (${what}) exercises ${dims - flowDepth.missing.length}/${dims} applicable regression dimensions — missing [${flowDepth.missing.join(', ')}] → ${flowDepth.missing.map((m) => how[m]).join('; ')}. (businessDepth is capped until covered.)`);
418
449
  }
419
450
  for (const w of oracle.weak) {
420
451
  findings.push(`ORACLE-WEAK: "${w.name}" — ${w.hint}`);
@@ -428,7 +459,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
428
459
  }
429
460
  if (balance.unclassifiedRatio > 0.4) {
430
461
  findings.push(`TAXONOMY-UNCLASSIFIED: ${balance.note} → align the VP-<CATEGORY> codes with the catalog (or extend the bucket keywords) so coverage-balance is meaningful.`);
431
- } else if (balance.imbalanced) {
462
+ } else if (balance.imbalanced && !flowScored) {
432
463
  findings.push(`BALANCE: ${balance.note} Stop expanding secondary viewpoints until business-core gaps are filled.`);
433
464
  }
434
465
  if (trace.mappedRatio < 0.5) {
@@ -454,7 +485,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
454
485
  // "nothing found" quietly excluding the specFR axis while the evidence sits in the file.
455
486
  if (spec.hasSpec && spec.frTotal === 0 && /@spec:/i.test(featureText)) {
456
487
  const cited = [...new Set([...featureText.matchAll(/@spec:([A-Za-z0-9.-]+)/g)].map((m) => m[1].toUpperCase()))];
457
- 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.`);
488
+ findings.push(isUiFlow
489
+ ? `SPEC-IDS-UNPARSED: the feature cites ${cited.length} requirement id(s) via @spec: (${cited.slice(0, 5).join(', ')}${cited.length > 5 ? ', …' : ''}) that this FLOW's requirements/spec.md does not declare. That is normal when the ids belong to the SCREEN specs the flow traverses — but it means nothing here can verify them: the specFR axis and the MUST-coverage gate stay off for this unit. To gate the flow on them, restate each as \`<ID>: <clause>\` in the flow's own spec (a one-line restatement per id is enough).`
490
+ : `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.`);
458
491
  }
459
492
  if (taxonomyMismatch) {
460
493
  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.`);
@@ -584,7 +617,12 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
584
617
  businessDepth: Math.round(businessDepth * 100) / 100,
585
618
  balance: Math.round(balanceScore * 100) / 100,
586
619
  traceability: Math.round(traceScore * 100) / 100,
587
- formula: 'overall = (0.4*coverage + 0.3*businessDepth + 0.15*balance + 0.15*traceability) * 10',
620
+ // Built from the axes ACTUALLY scored, with the renormalised weights — a hardcoded
621
+ // string drifted from the real calculation and printed axes (`balance` on a contract
622
+ // flow) that carried no weight at all.
623
+ axes: axisDefs.map((a) => ({ ...a, weight: a.applicable ? a.weight / weightSum : 0 })),
624
+ formula: `overall = (${scored.map((a) => `${(a.weight / weightSum).toFixed(2)}*${a.key}`).join(' + ')}) * 10`
625
+ + (missingEvidence.length ? ` — n/a (no evidence): ${missingEvidence.join(', ')}` : ''),
588
626
  },
589
627
  gateStatus,
590
628
  findings,