@sun-asterisk/sungen 3.2.34 → 3.2.35

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 (218) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +9 -4
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +5 -1
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/defect.d.ts +7 -0
  8. package/dist/cli/commands/defect.d.ts.map +1 -0
  9. package/dist/cli/commands/defect.js +68 -0
  10. package/dist/cli/commands/defect.js.map +1 -0
  11. package/dist/cli/commands/delivery.d.ts.map +1 -1
  12. package/dist/cli/commands/delivery.js +35 -7
  13. package/dist/cli/commands/delivery.js.map +1 -1
  14. package/dist/cli/commands/triage.d.ts +7 -0
  15. package/dist/cli/commands/triage.d.ts.map +1 -0
  16. package/dist/cli/commands/triage.js +98 -0
  17. package/dist/cli/commands/triage.js.map +1 -0
  18. package/dist/cli/index.js +4 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/exporters/csv-exporter.d.ts +4 -0
  21. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  22. package/dist/exporters/csv-exporter.js +1 -1
  23. package/dist/exporters/csv-exporter.js.map +1 -1
  24. package/dist/exporters/feature-parser.d.ts +8 -0
  25. package/dist/exporters/feature-parser.d.ts.map +1 -1
  26. package/dist/exporters/feature-parser.js +12 -0
  27. package/dist/exporters/feature-parser.js.map +1 -1
  28. package/dist/exporters/matrix/build.d.ts.map +1 -1
  29. package/dist/exporters/matrix/build.js +49 -10
  30. package/dist/exporters/matrix/build.js.map +1 -1
  31. package/dist/exporters/matrix/carry-manual-results.d.ts +19 -0
  32. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -1
  33. package/dist/exporters/matrix/carry-manual-results.js +76 -6
  34. package/dist/exporters/matrix/carry-manual-results.js.map +1 -1
  35. package/dist/exporters/matrix/export.d.ts +5 -2
  36. package/dist/exporters/matrix/export.d.ts.map +1 -1
  37. package/dist/exporters/matrix/export.js +14 -3
  38. package/dist/exporters/matrix/export.js.map +1 -1
  39. package/dist/exporters/matrix/render-csv.js +1 -1
  40. package/dist/exporters/matrix/render-csv.js.map +1 -1
  41. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  42. package/dist/exporters/matrix/render-xlsx.js +3 -0
  43. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  44. package/dist/exporters/matrix/types.d.ts +5 -0
  45. package/dist/exporters/matrix/types.d.ts.map +1 -1
  46. package/dist/exporters/matrix/types.js.map +1 -1
  47. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  48. package/dist/exporters/playwright-report-parser.js +1 -0
  49. package/dist/exporters/playwright-report-parser.js.map +1 -1
  50. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  51. package/dist/exporters/scenario-merger.js +2 -0
  52. package/dist/exporters/scenario-merger.js.map +1 -1
  53. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  54. package/dist/exporters/test-data-resolver.js +12 -2
  55. package/dist/exporters/test-data-resolver.js.map +1 -1
  56. package/dist/exporters/types.d.ts +5 -0
  57. package/dist/exporters/types.d.ts.map +1 -1
  58. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  59. package/dist/generators/gherkin-parser/index.js +1 -0
  60. package/dist/generators/gherkin-parser/index.js.map +1 -1
  61. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
  62. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
  63. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
  64. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
  65. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
  66. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
  67. package/dist/generators/test-generator/code-generator.js +4 -5
  68. package/dist/generators/test-generator/code-generator.js.map +1 -1
  69. package/dist/generators/test-generator/diagnostics.d.ts +9 -3
  70. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  71. package/dist/generators/test-generator/diagnostics.js +9 -3
  72. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  73. package/dist/generators/test-generator/patterns/types.d.ts +1 -1
  74. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  75. package/dist/generators/test-generator/step-mapper.d.ts +8 -0
  76. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  77. package/dist/generators/test-generator/step-mapper.js +52 -8
  78. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  79. package/dist/generators/test-generator/utils/data-resolver.d.ts +16 -0
  80. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  81. package/dist/generators/test-generator/utils/data-resolver.js +59 -0
  82. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  83. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  84. package/dist/generators/test-generator/utils/selector-resolver.js +10 -2
  85. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  86. package/dist/harness/audit.d.ts.map +1 -1
  87. package/dist/harness/audit.js +18 -5
  88. package/dist/harness/audit.js.map +1 -1
  89. package/dist/harness/counted-assertions.d.ts +2 -0
  90. package/dist/harness/counted-assertions.d.ts.map +1 -0
  91. package/dist/harness/counted-assertions.js +38 -0
  92. package/dist/harness/counted-assertions.js.map +1 -0
  93. package/dist/harness/defect-draft.d.ts +14 -0
  94. package/dist/harness/defect-draft.d.ts.map +1 -0
  95. package/dist/harness/defect-draft.js +143 -0
  96. package/dist/harness/defect-draft.js.map +1 -0
  97. package/dist/harness/defects.d.ts +24 -0
  98. package/dist/harness/defects.d.ts.map +1 -0
  99. package/dist/harness/defects.js +118 -0
  100. package/dist/harness/defects.js.map +1 -0
  101. package/dist/harness/manifest.d.ts.map +1 -1
  102. package/dist/harness/manifest.js +2 -2
  103. package/dist/harness/manifest.js.map +1 -1
  104. package/dist/harness/next-step.d.ts +0 -4
  105. package/dist/harness/next-step.d.ts.map +1 -1
  106. package/dist/harness/next-step.js +34 -4
  107. package/dist/harness/next-step.js.map +1 -1
  108. package/dist/harness/parse.d.ts +18 -0
  109. package/dist/harness/parse.d.ts.map +1 -1
  110. package/dist/harness/parse.js +20 -11
  111. package/dist/harness/parse.js.map +1 -1
  112. package/dist/harness/script-check.d.ts.map +1 -1
  113. package/dist/harness/script-check.js +7 -1
  114. package/dist/harness/script-check.js.map +1 -1
  115. package/dist/harness/sensors.d.ts.map +1 -1
  116. package/dist/harness/sensors.js +23 -6
  117. package/dist/harness/sensors.js.map +1 -1
  118. package/dist/harness/triage-signals.d.ts +20 -0
  119. package/dist/harness/triage-signals.d.ts.map +1 -0
  120. package/dist/harness/triage-signals.js +30 -0
  121. package/dist/harness/triage-signals.js.map +1 -0
  122. package/dist/harness/triage-unit.d.ts +23 -0
  123. package/dist/harness/triage-unit.d.ts.map +1 -0
  124. package/dist/harness/triage-unit.js +142 -0
  125. package/dist/harness/triage-unit.js.map +1 -0
  126. package/dist/harness/triage.d.ts +28 -0
  127. package/dist/harness/triage.d.ts.map +1 -0
  128. package/dist/harness/triage.js +99 -0
  129. package/dist/harness/triage.js.map +1 -0
  130. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  131. package/dist/harness/viewpoint-ledger.js +2 -1
  132. package/dist/harness/viewpoint-ledger.js.map +1 -1
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +2 -1
  135. package/dist/index.js.map +1 -1
  136. package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -1
  137. package/dist/orchestrator/ai-skills/commands.js +5 -3
  138. package/dist/orchestrator/ai-skills/commands.js.map +1 -1
  139. package/dist/orchestrator/ai-skills/golden-skills.test.js +29 -0
  140. package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -1
  141. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  142. package/dist/orchestrator/screen-manager.js +4 -2
  143. package/dist/orchestrator/screen-manager.js.map +1 -1
  144. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
  145. package/dist/orchestrator/templates/ai-src/commands/run-test.md +8 -2
  146. package/dist/orchestrator/templates/ai-src/config/claude.md +20 -7
  147. package/dist/orchestrator/templates/ai-src/config/copilot.md +20 -7
  148. package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
  149. package/dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
  150. package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
  151. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
  152. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +7 -4
  153. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +25 -9
  154. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +2 -0
  155. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +4 -3
  156. package/dist/orchestrator/templates/specs-test-data.ts +22 -4
  157. package/package.json +3 -3
  158. package/src/cli/commands/audit.ts +9 -4
  159. package/src/cli/commands/capability.ts +3 -1
  160. package/src/cli/commands/defect.ts +49 -0
  161. package/src/cli/commands/delivery.ts +33 -10
  162. package/src/cli/commands/triage.ts +57 -0
  163. package/src/cli/index.ts +4 -0
  164. package/src/exporters/csv-exporter.ts +5 -1
  165. package/src/exporters/feature-parser.ts +12 -0
  166. package/src/exporters/matrix/build.ts +51 -9
  167. package/src/exporters/matrix/carry-manual-results.ts +70 -7
  168. package/src/exporters/matrix/export.ts +12 -2
  169. package/src/exporters/matrix/render-csv.ts +1 -1
  170. package/src/exporters/matrix/render-xlsx.ts +3 -0
  171. package/src/exporters/matrix/types.ts +5 -0
  172. package/src/exporters/playwright-report-parser.ts +1 -0
  173. package/src/exporters/scenario-merger.ts +2 -1
  174. package/src/exporters/test-data-resolver.ts +10 -2
  175. package/src/exporters/types.ts +5 -0
  176. package/src/generators/gherkin-parser/index.ts +1 -0
  177. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
  178. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
  179. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
  180. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
  181. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
  182. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
  183. package/src/generators/test-generator/code-generator.ts +4 -4
  184. package/src/generators/test-generator/diagnostics.ts +9 -3
  185. package/src/generators/test-generator/patterns/types.ts +2 -0
  186. package/src/generators/test-generator/step-mapper.ts +56 -9
  187. package/src/generators/test-generator/utils/data-resolver.ts +53 -0
  188. package/src/generators/test-generator/utils/selector-resolver.ts +10 -2
  189. package/src/harness/audit.ts +19 -6
  190. package/src/harness/counted-assertions.ts +33 -0
  191. package/src/harness/defect-draft.ts +115 -0
  192. package/src/harness/defects.ts +94 -0
  193. package/src/harness/manifest.ts +2 -2
  194. package/src/harness/next-step.ts +29 -4
  195. package/src/harness/parse.ts +20 -11
  196. package/src/harness/script-check.ts +7 -1
  197. package/src/harness/sensors.ts +26 -7
  198. package/src/harness/triage-signals.ts +46 -0
  199. package/src/harness/triage-unit.ts +113 -0
  200. package/src/harness/triage.ts +127 -0
  201. package/src/harness/viewpoint-ledger.ts +2 -2
  202. package/src/index.ts +2 -1
  203. package/src/orchestrator/ai-skills/commands.ts +5 -3
  204. package/src/orchestrator/ai-skills/golden-skills.test.ts +23 -0
  205. package/src/orchestrator/screen-manager.ts +3 -1
  206. package/src/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
  207. package/src/orchestrator/templates/ai-src/commands/run-test.md +8 -2
  208. package/src/orchestrator/templates/ai-src/config/claude.md +20 -7
  209. package/src/orchestrator/templates/ai-src/config/copilot.md +20 -7
  210. package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
  211. package/src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
  212. package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
  213. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
  214. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +7 -4
  215. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +25 -9
  216. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +2 -0
  217. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +4 -3
  218. package/src/orchestrator/templates/specs-test-data.ts +22 -4
@@ -63,13 +63,15 @@ export function parseCommandSource(text: string, id: string): CommandSource {
63
63
  }
64
64
 
65
65
  const unquote = (s: string) => s.replace(/^['"]|['"]$/g, '');
66
+ /** A YAML single-quoted scalar: an apostrophe is written twice, or it ends the value early (#899). */
67
+ const sq = (s: string) => `'${s.replace(/'/g, "''")}'`;
66
68
 
67
69
  /** Claude command file — MUST reproduce the exact existing format (byte-identical gate). */
68
70
  export function renderClaudeCommand(c: CommandSource): string {
69
71
  const fm = [
70
72
  '---',
71
73
  ...(c.name ? [`name: ${c.name}`] : []), // some commands (locale) omit name
72
- `description: '${c.description}'`,
74
+ `description: ${sq(c.description)}`,
73
75
  `argument-hint: ${c.argumentHint}`,
74
76
  `allowed-tools: ${c.claudeTools}`,
75
77
  '---',
@@ -82,8 +84,8 @@ export function renderCopilotCommand(c: CommandSource): string {
82
84
  const fm = [
83
85
  '---',
84
86
  `name: sungen-${c.id}`,
85
- `description: '${c.description}'`,
86
- `argument-hint: '${unquote(c.argumentHint)}'`,
87
+ `description: ${sq(c.description)}`,
88
+ `argument-hint: ${sq(unquote(c.argumentHint))}`,
87
89
  `agent: 'agent'`,
88
90
  `tools: ${c.copilotTools}`,
89
91
  '---',
@@ -16,12 +16,14 @@
16
16
  */
17
17
  import * as fs from 'fs';
18
18
  import * as path from 'path';
19
+ import { parseDocument } from 'yaml';
19
20
  import { buildOutputs } from './build';
20
21
  import { parseCommandSource } from './commands';
21
22
 
22
23
  const FIX = path.resolve(__dirname, '..', '..', '..', 'tests', 'golden', 'ai-skills', 'fixtures');
23
24
  const SRC = path.resolve(__dirname, '..', 'templates', 'ai-src', 'skills');
24
25
  const SRC_CMD = path.resolve(__dirname, '..', 'templates', 'ai-src', 'commands');
26
+ const SRC_CFG = path.resolve(__dirname, '..', 'templates', 'ai-src', 'config');
25
27
  const UPDATE = process.argv.includes('--update');
26
28
 
27
29
  function lint(): string[] {
@@ -47,6 +49,18 @@ function lint(): string[] {
47
49
  if (!c.description) errs.push(`commands/${f}: missing description`);
48
50
  if (c.order != null) { const k = String(c.order); if (seen.has(k)) { /* allowed: parallel entrypoints */ } else seen.set(k, f); }
49
51
  }
52
+ // The CLAUDE.md / copilot-instructions tables are the always-loaded index of what ships, so
53
+ // they must list every skill and command, with the right count (#899).
54
+ const skills = fs.readdirSync(SRC).filter((d) => fs.existsSync(path.join(SRC, d, 'SKILL.md')));
55
+ const cmds = fs.readdirSync(SRC_CMD).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
56
+ for (const [cfg, prefix] of [['claude.md', '/sungen:'], ['copilot.md', '/sungen-']] as const) {
57
+ const t = fs.readFileSync(path.join(SRC_CFG, cfg), 'utf8');
58
+ for (const s of skills) if (!t.includes(`| \`${s}\` |`)) errs.push(`config/${cfg}: skills table misses ${s}`);
59
+ const listed = (c: string) => [' ', '`'].some((end) => t.includes(`| \`${prefix}${c}${end}`));
60
+ for (const c of cmds) if (!listed(c)) errs.push(`config/${cfg}: workflow table misses ${prefix}${c}`);
61
+ const n = t.match(/## Workflow \((\d+) AI commands\)/)?.[1];
62
+ if (Number(n) !== cmds.length) errs.push(`config/${cfg}: says ${n} AI commands, ${cmds.length} ship`);
63
+ }
50
64
  return errs;
51
65
  }
52
66
 
@@ -77,6 +91,15 @@ function run() {
77
91
  else codexNames.set(m[1], f.outputPath);
78
92
  }
79
93
 
94
+ // The key regex in lint() accepts frontmatter a real YAML parser rejects (an unquoted ": ", an
95
+ // unescaped apostrophe inside '...'), and assistants then drop every field silently (#899).
96
+ // Parse what ships — renderers re-quote command fields, so a valid source is not enough.
97
+ for (const f of outputs) {
98
+ if (!f.content.startsWith('---')) continue;
99
+ const doc = parseDocument(f.content.slice(3, f.content.indexOf('\n---', 3)));
100
+ for (const e of doc.errors) lintErrs.push(`${f.outputPath}: frontmatter is not valid YAML — ${e.message.split('\n')[0]}`);
101
+ }
102
+
80
103
  if (UPDATE) {
81
104
  fs.rmSync(FIX, { recursive: true, force: true });
82
105
  for (const f of outputs) {
@@ -5,7 +5,6 @@
5
5
 
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
- import { chromium } from '@playwright/test';
9
8
  import { readCapabilities } from '../harness/capability';
10
9
  import { testDataHeader, ensureTestDataGuideFiles } from './test-data-guide';
11
10
  import { resolveWebBaseUrl } from '../exporters/env-qa';
@@ -264,6 +263,9 @@ export class ScreenManager {
264
263
 
265
264
  let browser;
266
265
  try {
266
+ // Loaded here, not at module top: @playwright/test is a peerDependency a mobile project never
267
+ // installs, and `sungen add` loads this module on every platform (#865).
268
+ const { chromium } = await import('@playwright/test');
267
269
  browser = await chromium.launch();
268
270
  const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
269
271
  await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
@@ -119,7 +119,7 @@ if 1, 3 or 8 fails, propose SPLITTING into separate flows:
119
119
  4. Is that outcome **valuable to the actor**? (an order placed, a password reset — not "a page rendered")
120
120
  5. Is **every step necessary** for that outcome?
121
121
  6. Are all steps at the **same business abstraction**?
122
- 7. Are optional/error branches **phases of this goal** (ER/EH), not new goals?
122
+ 7. Are optional/error branches **flows of this goal** (Alternate / Exception flows), not new goals?
123
123
  8. Does **no segment** form an independently valuable flow on its own?
124
124
  9. Can you write **a single clear final assertion**?
125
125
  10. Can you name it "**Verb + outcome**"? (`place-order`, `reset-password` — not `cart-and-filter`)
@@ -134,8 +134,11 @@ Record the screen list — you will need it for:
134
134
  ### 1b. Author the Flow Contract (`requirements/flow-contract.yaml`)
135
135
 
136
136
  Write the answers down as the flow's contract — `sungen audit` scores the flow **against it**
137
- (the `flowCoverage` axis: HP/ER/EH journey phases; `FLOW-OUTCOME-UNPROVEN` when no automated
138
- scenario asserts data on the outcome screen; `FLOW-SCOPE-CREEP` when scenarios never touch it):
137
+ (the `flowCoverage` axis, per declared flow; `FLOW-OUTCOME-UNPROVEN` when no automated scenario
138
+ asserts data on the outcome screen; `FLOW-SCOPE-CREEP` when scenarios never touch it). The audit
139
+ also requires **both guarantees** and the **`flows:` inventory** on every contract —
140
+ `FLOW-GUARANTEE-MISSING` / `FLOW-INVENTORY-MISSING` otherwise, and the top mark is withheld — so
141
+ write them now rather than meet them after the spec and viewpoint are reviewed:
139
142
 
140
143
  ```yaml
141
144
  goal: "Place an order for a product added from home" # Verb + outcome
@@ -146,7 +149,22 @@ outcome:
146
149
  screen: checkout # the [Screen:...] namespace carrying the final proof
147
150
  assertion: "The confirmation shows the order number and the paid total"
148
151
  value: "The customer has paid; the shop has a new order"
149
- phases: [HP, ER, EH] # journey phases (default); add UI only if the flow owns UI states
152
+ successGuarantee: "An order exists with the cart's lines and total; the cart is empty"
153
+ minimalGuarantee: "No order and no charge without a confirmation; the cart is unchanged" # what Exception flows assert
154
+ phases: [BF, AF, EF] # Basic / Alternate / Exception flows (default) — the first one must reach `outcome`
155
+ flows: # the inventory: every flow of the use case, each with a status
156
+ - id: BF
157
+ outcome: "The successGuarantee — order confirmed with its number and total"
158
+ status: covered
159
+ - id: AF01
160
+ branchFrom: "BF step 3, the user edits the quantity in the cart"
161
+ outcome: "Re-enters BF at step 3 with the new total"
162
+ status: covered
163
+ - id: EF01
164
+ branchFrom: "BF step 4, the payment is declined"
165
+ outcome: "The minimalGuarantee — no order, cart kept, an error shown"
166
+ status: pending-clarification # covered | deferred | pending-clarification | out-of-scope
167
+ reason: "The decline message is not in the spec — asked the BA"
150
168
  stateful: cart # the mutated collection, if any — enables regression-depth dims
151
169
  golden: true # optional — release-critical: Final Inspection expects @golden scenarios here
152
170
  external: # optional — legs owned by another team/vendor (System INTEGRATION Testing)
@@ -155,6 +173,10 @@ external: # optional — legs owned by another team/vendor (
155
173
  screens: [payment] # the flow namespaces that leg passes through
156
174
  ```
157
175
 
176
+ `phases: [HP, ER, EH]` (happy path / error recovery / guards) is the lighter vocabulary for a short
177
+ single-outcome flow — it still needs both guarantees and a `flows:` inventory. Enumerating the
178
+ flows (the step × risk matrix) is described in the `sungen-tc-generation` skill, flow section.
179
+
158
180
  **A filled contract is an INPUT to generation — never an output.** Like `test-viewpoint.md`,
159
181
  generation must not rewrite it to match what was generated; disagree → propose the diff and ask.
160
182
 
@@ -79,7 +79,9 @@ Stop here for mobile — do not continue to the web path.
79
79
  > ⛔ **The `.feature` Gherkin is QA-approved and FROZEN.** During auto-fix you may write
80
80
  > **only** `selectors.yaml`, `test-data.yaml` (and other `*.yaml` data), plus the **one narrow
81
81
  > `.feature` edit** allowed: the **element-type token** on a `[Reference]` (e.g. `field`→`textbox`,
82
- > `button`→`link` — selector resolution, not test logic). You must **NOT** change the Gherkin's
82
+ > `button`→`link` — selector resolution, not test logic). A second, equally narrow exception:
83
+ > adding **`@known-defect:DEF-NNN`** to a scenario's tag line once its defect record exists (step 8)
84
+ > — that tag only, the tag line only. You must **NOT** change the Gherkin's
83
85
  > content or logic: no rewriting/adding/removing steps, no `wait for` steps, no changed expected
84
86
  > values, no weakened assertions. A failure that would need any of those is a **CANDIDATE BUG /
85
87
  > QA finding → report it, let it FAIL** (it may be a real bug). The live page is NOT the oracle;
@@ -270,7 +272,7 @@ question already.
270
272
  5. **Phase 1 — Smoke Check**: Run first 5 `@high` scenarios only. If failures → diagnose, fix fundamentals (page selector, auth, base @steps), re-run. Max 2 attempts. If still broken → ask user.
271
273
  6. **Phase 2 — Priority Wave**: Run all `@high` scenarios. Fix only failures from this wave. Max 2 attempts. Shared selectors fixed here cascade to later phases.
272
274
  7. **Phase 3 — Full Run**: Run all tests. Fix only **new** failures (elements unique to `@normal`/`@low`). Max 1 attempt. Don't loop on low-priority failures.
273
- 8. **Phase 4 — Regression**: One final full run. Report results. No more fix loops.
275
+ 8. **Phase 4 — Regression**: One final full run. No more fix loops. Then run `sungen triage --screen <name>` and render its table as the results summary: every red or flaky scenario already sits in one class (`product-candidate`, `unclassified`, `test-defect`, `environment`, `blocked-capability`, `known-defect`, `flaky`) with the rule and one evidence line. You may move an `unclassified` row into a class only with a written evidence line of your own. You may never turn a `product-candidate` into a `test-defect` by editing the `.feature` or the expected value — the Gherkin is frozen; report it as a candidate bug. For each `product-candidate` you judge to be a product bug, run `sungen defect draft --screen <name> --scenario "<title>"`, fill the record's Summary, check its Expected against `spec.md`, then add `@known-defect:DEF-NNN` to that scenario's tag line (the tag-only exception above). The severity in the draft is `proposed` — confirm it with QA (`AskUserQuestion`) before the hand-back and set `severity_source: qa`. A row you judge is not a product bug stays in the summary with your reason, and gets no record. A `bare-timeout` row (`unclassified`) is never re-run inside this command — Phase 4 was the final run; offer the re-run in Next steps.
274
276
  9. **Phase 8 — Serial-cascade risk check** (#414): If any scenario has many steps, a skipped test may cause downstream steps to be incorrectly skipped due to the serial-cascade sensor detecting false "not reached" conditions. When you see "test skipped" messages during Phase 3–4 runs, check if the skipped scenario is a **blockers-exist** dependency in the test ledger (`sungen ledger query --screen <name>`). If the scenario had high step count and cascade fired, offer: *"Scenarios with deep steps can trigger serial-cascade skipping. Consider parallelizing this spec with `@concurrent` tags or breaking into smaller scenarios."* Let the user decide whether to refactor (do not auto-apply parallelization).
275
277
  {{#cap parallel-subagents}}
276
278
  9. **Integrity check & trace (always run after the final run).**
@@ -478,3 +480,7 @@ If tests **failed** (after retries):
478
480
  - **`/sungen-create-test <name>`** — Revise test cases
479
481
  {{/cap}}
480
482
  - **Done for now** — I'll fix manually later
483
+
484
+ If the triage table has `bare-timeout` rows, put this option FIRST and mark it Recommended: **Re-run the N timed-out scenario(s) once** — it tells a slow environment from a hanging page (name them). Ask; never re-run them on your own: a scenario may create data or spend an OTP/rate limit on a shared environment. On a yes, re-run only those scenarios with the subset-merge flow (§ Playwright command guidelines — never overwrite the result file), then `sungen triage` again:
485
+ - green now → move the row to `flaky` (or `environment`) with that as the evidence line;
486
+ - the same timeout again → read the trace's last action: stuck after a submit or a navigation → candidate bug, handled as in Phase 4 step 8 (`sungen defect draft`, QA confirms the severity); stuck in a wait the test itself defines → `test-defect`.
@@ -12,32 +12,45 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
12
12
  | `sungen-tc-generation` | Test case generation strategy, output format |
13
13
  | `sungen-test-design-techniques` | EP, BVA, Decision Table, State Transition — systematic scenario generation |
14
14
  | `sungen-tc-review` | Review scoring, quality rules, checklist |
15
- | `sungen-viewpoint` | 17 UI patterns x 4 viewpoints — coverage checklists |
15
+ | `sungen-viewpoint` | 32 UI patterns (17 web + 15 mobile-native) x 4 viewpoints — coverage checklists |
16
16
  | `sungen-selector-keys` | YAML key generation from `[Reference]` names, suffixes, lookup priority |
17
17
  | `sungen-selector-fix` | Selector generation from live page, auto-fix strategy |
18
- | `sungen-delivery` | Export Gherkin + Playwright results → CSV test case deliverable |
18
+ | `sungen-delivery` | Export the Test Case & Coverage Matrix (or the legacy per-scenario CSV) from Gherkin + Playwright results |
19
19
  | `sungen-capture` | Acquire visual/design context — one skill, 4 modes: figma-mcp (Dev Mode MCP), figma-pat (URL → spec_figma.md), live (Playwright MCP scan), local (images in `requirements/ui/`) |
20
20
  | `sungen-locale` | Bootstrap i18n — audit selectors, detect locale switch mechanism, generate test-data overlay |
21
21
  | `sungen-data-factory` | Standardized test-data — field-type/security catalog, 4-source method, `.overwrite`, blind-spot prompts |
22
+ | `sungen-harness-audit` | How to read `sungen audit` output and repair test-design findings (the create-test quality loop) |
23
+ | `sungen-capture-mobile` | Capture a live mobile screen via Appium MCP — locator tree + screenshot (`@platform:mobile/android/ios`) |
24
+ | `sungen-mobile-gestures` | Mobile gesture patterns (swipe, long-press, scroll-to, pinch, pull-to-refresh) — Gherkin + Appium MCP mapping |
25
+ | `sungen-selector-fix-mobile` | Mobile selector fixing — Appium MCP exploration, accessibility-id-first strategy, run via wdio |
26
+ | `sungen-api-design` | API-first design loop for an api unit — catalog, API viewpoints, `@api`/`@cases`/flow scenarios, audit gate + repair |
27
+ | `sungen-api-coverage-model` | Per-endpoint API coverage model — tiered case set expanded from the catalog's `fields:` metadata |
28
+ | `sungen-ingest-legacy` | Import a legacy manual test-case suite (Google Sheets or local file) via `sungen ingest` |
29
+ | `sungen-dashboard` | Single-file HTML dashboard of test cases + results (`sungen dashboard`) |
22
30
 
23
- ## Workflow (8 AI commands)
31
+ ## Workflow (12 AI commands)
24
32
 
25
33
  | Command | What it does |
26
34
  |---|---|
27
35
  | `/sungen:add-screen <name> <path>` | Scaffold `qa/screens/<name>/` directories |
28
36
  | `/sungen:add-flow <name> [--path <url>]` | Scaffold `qa/flows/<name>/` directories for E2E cross-screen testing |
29
- | `/sungen:create-test <name>` | Generate `.feature` + `test-data.yaml` (auto-detects screen or flow) |
37
+ | `/sungen:add-api <name> [--base-url <url>] [--flow <name>] [--import <spec>]` | Scaffold an API area (`qa/api/<name>/`), or an ordered `@api` chain with `--flow` (`qa/api/flows/<name>/`); `--base-url` is needed only until the project has an API datasource; `--import` an OpenAPI/CSV spec to populate the endpoint catalog |
38
+ | `/sungen:create-test <name>` | Generate `.feature` + `test-data.yaml`, then run the quality harness (auto-detects screen / flow / API) |
39
+ | `/sungen:design <name>` | Alias of `create-test` |
30
40
  | `/sungen:create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
31
41
  | `/sungen:review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
32
- | `/sungen:run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
33
- | `/sungen:delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all screens if no arg) |
42
+ | `/sungen:run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen / flow / API) |
43
+ | `/sungen:delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all units if no arg) |
44
+ | `/sungen:dashboard [name...]` | Build the single-file HTML dashboard (`qa/dashboard/index.html`) — results, trends, run compare, CSV/XLSX export |
34
45
  | `/sungen:locale <name> <locale>` | Bootstrap i18n for a screen — audit selectors, detect locale switch, generate overlay (run before `/sungen:run-test --env <locale>`) |
46
+ | `/sungen:feedback [message]` | Record QA feedback locally (`.sungen/feedback/`) with context attached |
35
47
 
36
48
  **Screen path:** add-screen → create-test → review → run-test → delivery.
37
49
  **Flow path:** add-flow → create-test → review → run-test → delivery.
50
+ **API path:** add-api (`--import <openapi|csv>` for accuracy) → create-test → run-test → delivery. API units carry no selectors and no visual capture; create-test runs the audit + reviewer loop itself.
38
51
  **i18n path:** (after run-test passes for base locale) → locale → run-test --env <locale> → delivery --env <locale>.
39
52
 
40
- `create-test`, `review`, and `run-test` auto-detect context: if `qa/flows/<name>/` exists → flow mode, else `qa/screens/<name>/` → screen mode.
53
+ `create-test` and `run-test` auto-detect context: if `qa/api/<name>/` (or `qa/api/flows/<name>/`) exists → API mode, else if `qa/flows/<name>/` → flow mode, else `qa/screens/<name>/` → screen mode. `review` covers screen and flow units.
41
54
 
42
55
  After each command completes, use `AskUserQuestion` to present the next actions as selectable options. Never just print text — always give clickable choices so the user can continue the workflow seamlessly.
43
56
 
@@ -12,32 +12,45 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
12
12
  | `sungen-tc-generation` | Test case generation strategy, output format |
13
13
  | `sungen-test-design-techniques` | EP, BVA, Decision Table, State Transition — systematic scenario generation |
14
14
  | `sungen-tc-review` | Review scoring, quality rules, checklist |
15
- | `sungen-viewpoint` | 17 UI patterns x 4 viewpoints — coverage checklists |
15
+ | `sungen-viewpoint` | 32 UI patterns (17 web + 15 mobile-native) x 4 viewpoints — coverage checklists |
16
16
  | `sungen-selector-keys` | YAML key generation from `[Reference]` names, suffixes, lookup priority |
17
17
  | `sungen-selector-fix` | Selector generation from live page, auto-fix strategy |
18
- | `sungen-delivery` | Export Gherkin + Playwright results → CSV test case deliverable |
18
+ | `sungen-delivery` | Export the Test Case & Coverage Matrix (or the legacy per-scenario CSV) from Gherkin + Playwright results |
19
19
  | `sungen-capture` | Acquire visual/design context — one skill, 4 modes: figma-mcp (Dev Mode MCP), figma-pat (URL → spec_figma.md), live (Playwright MCP scan), local (images in `requirements/ui/`) |
20
20
  | `sungen-locale` | Bootstrap i18n — audit selectors, detect locale switch mechanism, generate test-data overlay |
21
21
  | `sungen-data-factory` | Standardized test-data — field-type/security catalog, 4-source method, `.overwrite`, blind-spot prompts |
22
+ | `sungen-harness-audit` | How to read `sungen audit` output and repair test-design findings (the create-test quality loop) |
23
+ | `sungen-capture-mobile` | Capture a live mobile screen via Appium MCP — locator tree + screenshot (`@platform:mobile/android/ios`) |
24
+ | `sungen-mobile-gestures` | Mobile gesture patterns (swipe, long-press, scroll-to, pinch, pull-to-refresh) — Gherkin + Appium MCP mapping |
25
+ | `sungen-selector-fix-mobile` | Mobile selector fixing — Appium MCP exploration, accessibility-id-first strategy, run via wdio |
26
+ | `sungen-api-design` | API-first design loop for an api unit — catalog, API viewpoints, `@api`/`@cases`/flow scenarios, audit gate + repair |
27
+ | `sungen-api-coverage-model` | Per-endpoint API coverage model — tiered case set expanded from the catalog's `fields:` metadata |
28
+ | `sungen-ingest-legacy` | Import a legacy manual test-case suite (Google Sheets or local file) via `sungen ingest` |
29
+ | `sungen-dashboard` | Single-file HTML dashboard of test cases + results (`sungen dashboard`) |
22
30
 
23
- ## Workflow (8 AI commands)
31
+ ## Workflow (12 AI commands)
24
32
 
25
33
  | Command | What it does |
26
34
  |---|---|
27
35
  | `/sungen-add-screen <name> <path>` | Scaffold `qa/screens/<name>/` directories |
28
36
  | `/sungen-add-flow <name> [--path <url>]` | Scaffold `qa/flows/<name>/` directories for E2E cross-screen testing |
29
- | `/sungen-create-test <name>` | Generate `.feature` + `test-data.yaml` (auto-detects screen or flow) |
37
+ | `/sungen-add-api <name> [--base-url <url>] [--flow <name>] [--import <spec>]` | Scaffold an API area (`qa/api/<name>/`), or an ordered `@api` chain with `--flow` (`qa/api/flows/<name>/`); `--base-url` is needed only until the project has an API datasource; `--import` an OpenAPI/CSV spec to populate the endpoint catalog |
38
+ | `/sungen-create-test <name>` | Generate `.feature` + `test-data.yaml`, then run the quality harness (auto-detects screen / flow / API) |
39
+ | `/sungen-design <name>` | Alias of `create-test` |
30
40
  | `/sungen-create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
31
41
  | `/sungen-review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
32
- | `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
33
- | `/sungen-delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all screens if no arg) |
42
+ | `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen / flow / API) |
43
+ | `/sungen-delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all units if no arg) |
44
+ | `/sungen-dashboard [name...]` | Build the single-file HTML dashboard (`qa/dashboard/index.html`) — results, trends, run compare, CSV/XLSX export |
34
45
  | `/sungen-locale <name> <locale>` | Bootstrap i18n for a screen — audit selectors, detect locale switch, generate overlay (run before `/sungen-run-test --env <locale>`) |
46
+ | `/sungen-feedback [message]` | Record QA feedback locally (`.sungen/feedback/`) with context attached |
35
47
 
36
48
  **Screen path:** add-screen → create-test → review → run-test → delivery.
37
49
  **Flow path:** add-flow → create-test → review → run-test → delivery.
50
+ **API path:** add-api (`--import <openapi|csv>` for accuracy) → create-test → run-test → delivery. API units carry no selectors and no visual capture; create-test runs the audit + reviewer loop itself.
38
51
  **i18n path:** (after run-test passes for base locale) → locale → run-test --env <locale> → delivery --env <locale>.
39
52
 
40
- `create-test`, `review`, and `run-test` auto-detect context: if `qa/flows/<name>/` exists → flow mode, else `qa/screens/<name>/` → screen mode.
53
+ `create-test` and `run-test` auto-detect context: if `qa/api/<name>/` (or `qa/api/flows/<name>/`) exists → API mode, else if `qa/flows/<name>/` → flow mode, else `qa/screens/<name>/` → screen mode. `review` covers screen and flow units.
41
54
 
42
55
  After each command completes, present the next actions as selectable options. Never just print text — always give clickable choices so the user can continue the workflow seamlessly.
43
56
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sungen-api-coverage-model
3
- description: The per-endpoint API coverage model — the systematic case set to enumerate for each endpoint (read / mutating / flow), keyed to the catalog's fields: metadata, tiered so a first pass is complete (not "happy path + 1 error"). Load this in API unit mode alongside sungen-api-design; it turns endpoints × field constraints into a concrete case list, mechanically, instead of guessing.
3
+ description: 'The per-endpoint API coverage model — the systematic case set to enumerate for each endpoint (read / mutating / flow), keyed to the catalog''s fields: metadata, tiered so a first pass is complete (not "happy path + 1 error"). Load this in API unit mode alongside sungen-api-design; it turns endpoints × field constraints into a concrete case list, mechanically, instead of guessing.'
4
4
  ---
5
5
 
6
6
  # API coverage model (what to test per endpoint)
@@ -6,7 +6,7 @@ user-invocable: false
6
6
 
7
7
  ## Purpose
8
8
 
9
- Launch a mobile app on a device/emulator, capture **one locator tree** (`generate_locators`) and **one screenshot**, and save them as visual context for test generation. The mobile analogue of `sungen-capture-live` — same job, Appium MCP instead of Playwright MCP.
9
+ Launch a mobile app on a device/emulator, capture **one locator tree** (`generate_locators`) and **one screenshot**, and save them as visual context for test generation. The mobile analogue of `sungen-capture` mode `live` — same job, Appium MCP instead of Playwright MCP.
10
10
 
11
11
  Use when the target is a native/Flutter/React-Native app (`@platform:android` or `@platform:ios`) running on a connected device or emulator.
12
12
 
@@ -188,8 +188,8 @@ Flutter/RN expose the same Semantics on each, so one exploration covers most of
188
188
 
189
189
  ## Relationship to other capture skills
190
190
 
191
- - `sungen-capture-figma` / `sungen-capture-local` — design/image sources (platform-agnostic)
192
- - `sungen-capture-live` — web (Playwright MCP)
191
+ - `sungen-capture` (modes `figma-mcp` / `figma-pat` / `local`) — design/image sources (platform-agnostic)
192
+ - `sungen-capture` (mode `live`) — web (Playwright MCP)
193
193
  - `sungen-capture-mobile` — this skill, mobile (Appium MCP)
194
194
 
195
195
  All write to `requirements/ui/` and report back to the caller.
@@ -69,6 +69,15 @@ Rules:
69
69
  catalog defaults).
70
70
  - Map every `errorRef` placeholder (`M13`, `M39`, …) to the spec's real code via `errorMap`. Leave
71
71
  one unmapped **only** if the spec has none — the lint flags it as an open point to confirm.
72
+ - Some catalog constraints are only placeholders (string `minLength: 1` / `maxLength: 255`, phone
73
+ `8..15`, …). A field that does not set its own value is warned **`PLACEHOLDER_CONSTRAINT`** — the
74
+ boundary cases are built from that number, so take it from the spec.
75
+ - An **empty value** is judged by `required`, not by `minLength`: `required: false` → `""` is valid
76
+ (no "below minimum" case for it); `required: true` **or unset** → `""` is invalid. `minLength: 0`
77
+ has no min-1 case. So an optional field must say `required: false`.
78
+ - To drop **one** generated case for **one** field, list its id under the field's **`disable:`**
79
+ (e.g. `disable: [CHK-STR-I2]` drops min-1 and keeps max+1 `CHK-STR-I2a`). It is reported as
80
+ not applicable. The overwrite's `disable:` drops a case for every field of the type.
72
81
  - **Complex forms:** a **group** field takes nested **`fields:`** (recursive; no `type` on the group → renders a nested object like `address.city`). A field whose rule depends on another takes **`dependsOn: { field, value }`** — recorded as a `_depends_on` note (the generator doesn't resolve the runtime condition; weave it into the scenario/`@cases`).
73
82
  - Set **`unique: true`** on a field that must be fresh every run (register email/username, anything
74
83
  that would clash with a record a previous run left behind). Its valid value then carries a runtime
@@ -126,7 +126,8 @@ exist in test-data, so Gate D does not ask for them: the capture step renders as
126
126
  are resolved by TestDataLoader at execution time — including when nested inside a test-data value
127
127
  (`seed: "Summary intro {{$timestamp}}"`). They have no static value to print, so the report names
128
128
  them instead: `<unique timestamp>`, `<unique id>`, `<random 1-100>`, `<today's date>`,
129
- `<current date-time>`. An unknown `{{$foo}}` is left raw on purpose — it is a typo and Gate D says so.
129
+ `<current date-time>`, and `{{$date:+1d}}` reads `<today's date + 1 day>`. An unknown `{{$foo}}` (or an
130
+ argument the runtime does not support, e.g. `{{$date:tz=…}}`) is left raw on purpose — it is a typo and Gate D says so.
130
131
 
131
132
  **Data fidelity**: invisible test data is made visible, never normalized — `''` → `(empty)`,
132
133
  whitespace-only → `(5 spaces)`, padded → `" value "` quoted verbatim. A trim/collapse here would
@@ -73,6 +73,8 @@ reason comment); the Given/When/Then stay **exactly** as QA approved them:
73
73
 
74
74
  > **Cardinal sin (do NOT do this):** a `password > 8 chars` rule fails on a 6-char input → "fix" it to `>= 6` so the test passes. The logic is now meaningless. A failing assertion is a **finding**, not a chore.
75
75
 
76
+ **`sungen triage` makes the first pass for you.** Its classes are this section's split: `test-defect` = selector-resolution / test-data failure (auto-fix scope), `product-candidate` = assertion-value failure with an Expected and a Received (apply the spec question above), `environment` = the run itself (fix the setup, not the test), `unclassified` = yours to judge — an assertion on an element that never appeared lands here, because a wrong selector and a missing message look the same.
77
+
76
78
  **Auto-fix loop scope:** the run-test auto-fix loop engages ONLY on **selector-resolution** failures. On an assertion-value failure where the app contradicts the spec → **HALT and report**, do not loop it into passing.
77
79
 
78
80
  **Never hand-edit the generated `.spec.ts`** (e.g. inserting `page.evaluate`/`fetch` to bypass a broken control). `sungen script-check` regenerates the spec from `.feature` and flags any edit as DRIFT — regenerate, don't patch.
@@ -122,9 +124,9 @@ reason comment); the Given/When/Then stay **exactly** as QA approved them:
122
124
  | `SG-W012` | A mock-install step written AFTER a navigation step in the same block — `page.route()` registered after `goto()` misses every request fired during page load | Move the mock-install step before the navigation, or into `Background` |
123
125
  | `SG-W013` | A page step (`see [X] page` / `is on [X] page` / `wait for [X] page`) whose `[Ref]` has no `type: page` selector entry (or collides with a non-page entry) — the step falls back to the feature's own path (or `/<ref>/`) instead of `X`'s real URL, so the anchored match can never reach it: the assertion always fails, the wait can only time out | Declare a `type: page` entry for `[Ref]` with its real URL; if the key collides with another type, disambiguate with a `--type` suffix (`sungen-selector-keys` § Collision rule) |
124
126
  | `SG-W014` | `[X] page with {{v}}` where `{{v}}`'s base test-data value carries no query and no fragment — the step checks the PATH only, asserting less than it reads as | Informational — pass a value like `?q=…` if you meant to assert a query, or drop `with {{v}}` for a bare page |
125
- | `SG-W015` | `see [X] card/item with {{n}}` where `{{n}}`'s test-data value is a **bare number** — this compiles to a TEXT assertion (`toHaveText('3')`), NOT a count. An author reaching for "3 cards are shown" gets an assertion that can only pass if the card's TEXT is literally the digit: a wrong test with no error. Scoped to container-ish element types (`card`/`item`/`option` + plurals); a text element asserting a numeric readout (`[Row Quantity] text with 4`) stays silent | For a COUNT use a form that has count semantics: `see [Ref] row with {{n}}` / `see [T] table with {{n}}`, or a list `should have count`. If the number really is the element's text, assert it on a **text/label** element instead |
127
+ | `SG-W015` | `see [X] card/item with {{n}}` where `{{n}}`'s test-data value is a **bare number** — this compiles to a TEXT assertion (`toHaveText('3')`), NOT a count. An author reaching for "3 cards are shown" gets an assertion that can only pass if the card's TEXT is literally the digit: a wrong test with no error. Scoped to container-ish element types (`card`/`item`/`option` + plurals); a text element asserting a numeric readout (`[Row Quantity] text with 4`) stays silent. In a `@cases` scenario the value is judged on every dataset row; it also fires on `see [X] row with {{n}}` whose rows MIX numbers and text (a row count needs every row numeric, so the step stays a text assertion) | For mixed `@cases` rows: put the counting rows in their own `@cases` scenario. For a COUNT use a form that has count semantics: `see [Ref] row with {{n}}` / `see [T] table with {{n}}`, or a list `should have count`. If the number really is the element's text, assert it on a **text/label** element instead |
126
128
  | `SG-W016` | A table step on a table located by CSS/testid whose value does not mention `table` and that declares no `rows:` — the table templates find rows by `getByRole('row')`; on a list built from `<div>`s that is 0 rows, so `is hidden`/`is empty` PASS without checking anything and counts/clicks time out (issue #688). A heuristic — the compiler cannot see the DOM — so it warns once per table | Declare the row shape on the TABLE entry (`rows: 'div.job-row'`, relative to the table) — it drives all seven table steps — or locate the table by `role: table` if it really is one. Do not put the shape on a row label's entry: that key positions ONE row |
127
- | `SG-W017` | A step needs a **selector-entry field the entry does not declare**, so there is nothing honest to compile — today, a table paginate step whose table entry declares no `pagination.next`/`.prev` (#712). The mirror of `SG-W004`: that one reports a field written that nothing reads, this one a field a consumer needs that nobody wrote. The step compiles to a loud runtime `throw` rather than guessing a locator, which is the #690 failure mode (a locator no page has, failing far from its cause) | Add the missing field to the entry in selectors.yaml — for pagination: `pagination:` with `next:`/`prev:` locators. This is a selectors.yaml-only fix, allowed inside the run-test auto-fix loop |
129
+ | `SG-W017` | A step needs a **selector-entry field the entry does not declare**, so there is nothing honest to compile — today: a table paginate step whose table entry declares no `pagination.next`/`.prev` (#712); a web `fill [X] date-picker` whose entry locates a `<button>` (a calendar trigger) and names no fill recipe (#907). The mirror of `SG-W004`: that one reports a field written that nothing reads, this one a field a consumer needs that nobody wrote. The step compiles to a loud runtime `throw` rather than guessing a locator, which is the #690 failure mode (a locator no page has, failing far from its cause) | Add the missing field to the entry in selectors.yaml — for pagination: `pagination:` with `next:`/`prev:` locators; for a button date-picker: `variant: <recipe>` naming a `qa/app.yaml` recipe with a `fill` intent (and `assertValue` to read it back). This is a selectors.yaml-only fix, allowed inside the run-test auto-fix loop |
128
130
  | `SG-W018` | A selectors.yaml entry declares an **`nth:` outside the contract** — `nth: -2`, `nth: 'last'`, `nth: 1.5`. `nth:` was the one entry field whose VALUE nothing checked (issue #720), so the web adapter silently DROPPED the filter (identical to deleting the line — the step then hits `strict mode violation: resolved to N elements`, which names the DOM and never the YAML line) while the mobile adapter emitted `$$('~x')[-1]` / `[last]` — `undefined` indexing and a bare identifier. The value is now ignored and reported once per entry | Write an integer `>= 0` for a zero-based index, or `-1` for the last match (`.last()` on web, `.at(-1)` on mobile). There is no keyword form: `'last'` is a string, not an index. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
129
131
  | `SG-E020` | A step matched a pattern, but the **active adapter ships no template** for it (e.g. a web-only step compiled under `platform: mobile`). The feature file still generates — that one step compiles to `throw new Error("[sungen] …")` naming the step, feature, pattern, template and adapter, so the failure is loud and traceable rather than a crashed build | Rephrase to a step the target adapter actually ships (see `sungen-gherkin-syntax` Platform Support section / `sungen-mobile-gestures`), or tag the scenario `@manual` with the platform reason |
130
132
  | `SG-E021` | The step carries a trailing **state clause** (`… is hidden`) that the pattern which matched it does not assert — the clause would be SILENTLY DROPPED. Worst shape in this table: it does not weaken an assertion, it can INVERT one. `see [X] page is hidden` matched `page-assertion` (priority 13) ahead of `is-hidden` (11), so the step compiled to `toHaveURL(...)` — asserting the page IS displayed, and passing. Also fires on `wait for [X] is checked` (a wait honours only `hidden`/`visible`/`disabled`/`enabled`; everything else silently fell back to `visible`) and on `row`/`column in [T] table … is <state>` for any state but `hidden` | Move the state onto the element that has it: `Then User see [X] checkbox is checked`. For a page, assert the page you expect (`Then User see [Other] page`) or a marker element (`Then User see [Banner] is hidden`). If sungen cannot express the check, tag the scenario `@manual`. The diagnostic names the pattern that swallowed the clause — that name tells you which form you actually hit |
@@ -137,7 +139,7 @@ reason comment); the Given/When/Then stay **exactly** as QA approved them:
137
139
  | `SG-W028` | A trailing **scope clause was DROPPED**, so the step runs against more of the page than its sentence names. Two causes, one code: (1) the word after `in [X]` is not a container type — `in [Job Card] widget`, or a leaf element used as a parent — so the clause matched nothing and the first-`[ref]`/first-`{{data}}` extractors took only the head of the sentence; (2) the by-content form `in [X] <type> with {{v}}` on the **mobile** adapter, where a parent is chained with `$()` and has no content filter. Before this code, both compiled clean and silently widened the assertion to the WHOLE screen — in a list of look-alike cards the test then passes for the wrong reason, findable only by diffing the generated `.spec.ts` against the Gherkin (#667) | For (1) use a real container type: `table` `list` `list-item` `section` `dialog` `modal` `drawer` `overlay` `form` `card` `item` `row` `group` `panel` `region` `container` `tab` `menu`. For (2) give the container an accessibility-id that identifies it and scope by that, or assert on the element the value belongs to |
138
140
  | `SG-W029` | A **mobile feature with no usable app-package `Path:`** — the line is missing, or it holds a web-style route (`Path: /applied-jobs`). The compiler then cannot name the app to reset between scenarios, and before this code that was SILENT: the per-scenario `beforeEach` was not emitted at all, so Background fell to Mocha's `before()` — once per FILE. Every scenario shared one continuous app session, so a tab switch or a scroll in one stranded elements off-screen for every later one, and the `@reset:` tier ladder was dropped with it, leaving the tag inert (#669). The spec now always emits the reset machinery and resolves the app id from the session capability instead, throwing a named error at run time if nothing supplies one | Add the package to the feature: `Path: com.example.app/.MainActivity`, or `Path: <androidPkg>/<activity> | <iosBundleId>` for a dual-id feature. A `Path:` starting with `/` is a web route and will not be read as a package. Failing that, set `APP_PACKAGE` (Android) / `IOS_BUNDLE_ID` (iOS) so `wdio.conf` puts the id on the session |
139
141
  | `SG-E022` | The **generated spec file itself** is not parseable JS/TS, so NOTHING in it runs — Playwright reports one `SyntaxError` and every scenario in the unit is lost. A compiler self-check (`@babel/parser`) on the emitted code, added after a field report where the runtime-data marker post-processor mis-paired quote delimiters across two literals and the user only found out at `npx playwright test`. Reported, never thrown — the file is still written so you keep the artifact to inspect. **Unlike every other code in this table this one blames the COMPILER, not your `.feature`** | Nothing to fix in the Gherkin — this should be impossible. **Report it** with the `.feature` (and the step) the file was generated from, attaching the unparseable `.spec.ts`. Check first that you are running the project-local compiler (`npx sungen`), not an older global one |
140
- | `SG-E023` | The step carries an **assertion clause** (`matches "…"`, `contains`, `has text`) but the generated code is a BARE VISIBILITY check — the clause was swallowed by a catch-all and the test would pass whatever the element says. The sibling of `SG-E021`, for the clauses that are not states: `SG-E021` closed the state case declaratively, but a dropped `matches`/`contains`/`has text` still slid through (reproduced live: a `matches "<format>"` step compiled by an older sungen lost the assertion entirely, emitted no warning, and left a `toBeVisible()` that passed whatever the label said). Four separate defects arrived this way, so the step now compiles to a runtime `throw`. Deliberately narrow — fires only on the bare-visibility shape, so a pattern that genuinely consumed the clause (`is-hidden`, `contains`, `matches-format`, the state family, or a `filter({ hasText })` on a list member) is untouched | Either the phrasing is not the supported one for that clause — check the exact form in `sungen-gherkin-syntax` — or this sungen predates the step: verify you are running the project-local compiler (`npx sungen`), not an older global one |
142
+ | `SG-E023` | The step carries an **assertion clause** (`matches "…"`, `contains`, `has text`) but the generated code is a BARE VISIBILITY check — the clause was swallowed by a catch-all and the test would pass whatever the element says. The sibling of `SG-E021`, for the clauses that are not states: `SG-E021` closed the state case declaratively, but a dropped `matches`/`contains`/`has text` still slid through (reproduced live: a `matches "<format>"` step compiled by an older sungen lost the assertion entirely, emitted no warning, and left a `toBeVisible()` that passed whatever the label said). Four separate defects arrived this way, so the step now compiles to a runtime `throw`. Deliberately narrow — fires only on the bare-visibility shape, so a pattern that genuinely consumed the clause (`is-hidden`, `contains`, `matches-format`, the state family, or a `filter({ hasText })` on a list member) is untouched. Also fires on a trailing `is <word>` / `is not <word>` that is no state (`is on`, `is open`, `is active`, `is not visible`) — only `is visible`/`displayed`/`shown`/`present` and `is not hidden` are what a visibility check asserts | For an `is <word>` ending: use a state from the `sungen-gherkin-syntax` States list (`is expanded`/`is collapsed` for a disclosure), or `@manual`. Otherwise, either the phrasing is not the supported one for that clause — check the exact form in `sungen-gherkin-syntax` — or this sungen predates the step: verify you are running the project-local compiler (`npx sungen`), not an older global one |
141
143
  | `SG-E024` | A mobile **app-lifecycle** step (`User restart the app` / `User reinstall the app`) in a feature that declares no `Path:` app id. The lifecycle templates resolve the target app through `__resolveAppId()`, a helper the appium test-file template emits ONLY when the id is present, so the spec used to compile clean and then die at runtime with `ReferenceError: __resolveAppId is not defined` — taking the WHOLE spec down, not the one step, because the failure lands in a before hook. (Field case: a mobile unit scaffolded with the nav recipe in `--path` instead of `--reach`, so `Path` never carried an app id.) Now refused at compile time as a `throw` naming the missing line | Add the app id under the `Feature` line: `Path: com.example.app/.MainActivity \| com.example.app`. For a mobile unit `--path` carries the **APP ID** and `--reach` carries the navigation recipe — if the nav recipe ended up in `--path`, that is the mistake |
142
144
  | `SG-E025` | A feature tagged `@platform:ios` uses a step the Appium adapter does **not** serve on iOS — the template either throws deep inside a device run (`set-connectivity`, `location-services`, `open-notifications`) or silently does nothing (`hide-keyboard`, a guarded `if (!driver.isIOS …)`). Derived from the template body via the per-OS axis of the step-support matrix, so it cannot disagree with the adapter. Only an explicit `@platform:ios` is gated (`@platform:mobile` picks its OS at run time). Compiles to a loud `throw` | Use a step the matrix marks ✅ for iOS (see `docs/spec/step-support-matrix.md`), tag the feature `@platform:mobile` if it should run on either OS, or tag the scenario `@manual` |
143
145
  | `SG-E026` | A **click step names a position** (`at position "top-left"`, `at offset x=…, y=…`) that would not be honoured: the keyword is misspelt or unquoted, a value is outside the grammar (not a number, negative, above 100%, a missing axis, an unknown preset), text follows the position, or another click form claimed the step first (`try to click`, a `with {{v}}` text filter, `… and accept [OK] alert`, `double click`). Each of those would compile to a click on the element's CENTRE and pass — for a backdrop-dismiss overlay that is the one outcome that proves nothing — so the step compiles to a loud `throw` | Write `click [X] <type> at position "top-left\|top-right\|bottom-left\|bottom-right"` or `… at offset x=<n>px\|%, y=<n>px\|%` as the LAST thing in the step, on a plain `click` (no `try to`, no `with {{v}}`, no alert clause). Web only — on mobile tap the control the position targets |
@@ -173,7 +175,8 @@ The scenario uses a SECRET test-data key whose `QA_*` variable is missing in thi
173
175
 
174
176
  Sungen picks assertion based on element type:
175
177
  - **Input** (`field`, `textarea`, `search`, `slider`) → `toHaveValue()`
176
- - **Dropdown** (`dropdown`, `select`, `combobox`) → runtime tag probe: `toHaveValue()` on a native `<select>`/input, `toHaveText()` on a `<button role="combobox">` trigger (Radix/shadcn, MUI). Pin one branch with `variant: native` or an `assertValue` recipe in `qa/app.yaml`.
178
+ - **Dropdown / date-picker** (`dropdown`, `select`, `combobox`, `date-picker`) → the `assertValue` recipe when the entry's `variant:` (or `widgets.select` / `widgets.datepicker`) names one; otherwise a runtime tag probe: `toHaveValue()` on a native `<select>`/input, `toHaveText()` on a `<button role="combobox">` trigger (Radix/shadcn, MUI) or a `<button>` that opens a calendar popover. Pin one branch with `variant: native` or an `assertValue` recipe in `qa/app.yaml`. Any other element with a `variant:` uses its recipe's `assertValue` too.
179
+ - **`SG-W017` on a date-picker fill** (the entry is provably a button: `role: button`, or a css/locator ending in a `button` tag) and, at run time, **`[sungen] [X] is not a fillable element`** (a trigger the compiler could not prove is a button) → the fill target is a calendar / popup trigger. Declare a recipe with `fill` (and `assertValue`) intents in `qa/app.yaml` and set `variant: <recipe>` on the entry, or `widgets.datepicker` for every date-picker. Do not change the step to a click.
177
180
  - **Text** (everything else: `message`, `heading`, `label`, `row`) → `toHaveText()`
178
181
  - **Partial** (`contains` keyword) → `toContainText()`
179
182
 
@@ -37,7 +37,7 @@ AND → inherits from preceding keyword
37
37
 
38
38
  ```
39
39
  User is on [T] page | page with {{v}} | dialog # {{v}} = query/fragment ("?id=200", "#tab") APPENDED to the page value — a bare "200" onto "/x?id=199" gives "?id=199200"
40
- User fill [T] field | textarea | search | slider | date-picker with {{v}} # date-picker: web only unless qa/app.yaml declares widgets.datepicker — appium refuses it
40
+ User fill [T] field | textarea | search | slider | date-picker with {{v}} # date-picker: web only unless qa/app.yaml declares widgets.datepicker — appium refuses it; on web a <button> trigger (calendar popover) needs a fill recipe (variant: / widgets.datepicker) or it is refused (SG-W017)
41
41
  User fill [T] uploader with {{f}} [web]
42
42
  User clear [T] field
43
43
  User check [T] checkbox | toggle | radio
@@ -145,14 +145,20 @@ two content-filtered queries can hit different rows if the table re-renders in b
145
145
  ```
146
146
  # 1. Visibility: User see [T] type (NEVER add "is visible") | is hidden
147
147
  # 2. Text (toHaveText): User see [T] message | header | label with {{v}}
148
- # `label` forks on the entry: declared by IDENTITY (xpath/accessibility-id/testid/id/css)
149
- # → asserts THAT element contains {{v}} (add `exact: true` for a strict match); no entry,
150
- # or `type: text` → matches the caption+value text pair anywhere on screen
148
+ # `label` forks on the entry: declared by IDENTITY (xpath/accessibility-id/testid/id/css/locator)
149
+ # → toContainText on THAT element; `exact: true` → toHaveText, and an EMPTY {{v}} asserts the
150
+ # element shows no text (contains '' would pass on anything); no entry, or `type: text` → matches
151
+ # the caption+value text pair anywhere on screen
151
152
  # 3. Partial (toContainText): User see [T] text contains {{v}}
152
153
  # 3b. Format (shape, not value): User see [T] label matches "yyyy/mm/dd ~ yyyy/mm/dd"
153
- # 4. Input (toHaveValue): User see [T] field | date-picker | search | slider with {{v}}
154
- # dropdown | select | combobox → tag probed at runtime (toHaveValue native, toHaveText custom trigger)
154
+ # 4. Input (toHaveValue): User see [T] field | search | slider with {{v}}
155
+ # dropdown | select | combobox | date-picker → the entry's `assertValue` recipe if it has one
156
+ # (variant: / widgets.select / widgets.datepicker), else the tag is probed at runtime
157
+ # (toHaveValue on a native input/select, toHaveText on a custom trigger such as a <button>).
158
+ # Any element whose entry has a `variant:` reads back through that recipe's `assertValue`
155
159
  # 5. State: User see [T] button is disabled | checkbox is checked | dialog with {{v}} is hidden
160
+ # open/closed: User see [T] button | section is expanded | is collapsed [web — reads `expanded:`, else aria-expanded;
161
+ # an entry with `expanded: { state: none }` has no state to read → refused, SG-W017]
156
162
  # 6. Attribute (toHaveAttribute): User see [T] image | link with {{v}}
157
163
  # WHICH attribute comes from the entry's `attribute:` — any name, not just href/src
158
164
  # 7. Count: User see [T] row with {{count}}
@@ -501,7 +507,11 @@ zero-match assertion failure, not a platform gap.
501
507
 
502
508
  ### States
503
509
 
504
- `hidden` `visible` `disabled` `enabled` `checked` `unchecked` `focused` `empty` `loading` `selected` `sorted ascending` `sorted descending`
510
+ `hidden` `visible` `disabled` `enabled` `checked` `unchecked` `focused` `empty` `loading` `selected` `expanded` `collapsed` `sorted ascending` `sorted descending`
511
+
512
+ Any other trailing `is <word>` / `is not <word>` (`is on`, `is active`, `is open`, `is not visible`) is
513
+ **not** a state: the compiler refuses it (SG-E023) rather than check only visibility. `is displayed` /
514
+ `is shown` / `is present` read as `is visible`, and `is not hidden` as visible too.
505
515
 
506
516
  ### Element Types
507
517
 
@@ -539,12 +549,18 @@ Use `{{$var}}` in test-data YAML for values that must be unique per test run. Re
539
549
 
540
550
  | Variable | Example | Output |
541
551
  |---|---|---|
542
- | `{{$timestamp}}` | `"User-{{$timestamp}}"` | `"User-1714000000"` |
552
+ | `{{$timestamp}}` | `"User-{{$timestamp}}"` | `"User-1714000000000"` (milliseconds) |
543
553
  | `{{$uuid}}` | `"{{$uuid}}"` | `"a1b2c3d4-..."` |
544
554
  | `{{$random:min:max}}` | `"{{$random:1:100}}"` | `"42"` |
545
555
  | `{{$date}}` | `"{{$date}}"` | `"2026-04-24"` |
556
+ | `{{$date:+Nd}}` / `{{$date:-Nd}}` | `"{{$date:+1d}}"` | `"2026-04-25"` — today shifted by N days |
546
557
  | `{{$datetime}}` | `"{{$datetime}}"` | `"2026-04-24T10:30:00.000Z"` |
547
558
 
559
+ `{{$date}}` is the LOCAL calendar date of the machine running the tests — the "today" the browser shows
560
+ when `playwright.config.ts` sets no `timezoneId`. If the config pins `timezoneId`, export the same zone as
561
+ `TZ` (e.g. `TZ=Asia/Tokyo npx playwright test`) so both agree. `{{$datetime}}` is a UTC instant (`…Z`). Any `{{$date:…}}` argument other than `±Nd`
562
+ (`tz=…`, a format) and any `{{$datetime:…}}` argument are not supported and stay literal.
563
+
548
564
  **Rules:**
549
565
  - `$timestamp` and `$uuid` → same value across all keys in one `load()` call (stable within a test file)
550
566
  - `$random` → unique per occurrence (each key gets a different random)
@@ -631,7 +647,7 @@ on mobile a dialog-scoped ref still resolves against the whole screen (`SG-W021`
631
647
  | `@spec:<id>` | **Traceability**: this scenario proves requirement `<id>` from `requirements/spec.md` (`@spec:FR-003`). Repeatable — a scenario may prove several. Read by `sungen audit` (requirement coverage) and by delivery (the Coverage sheet's requirement table). Without it the link is only inferred from wording and cannot survive an edit. |
632
648
  | `@ui:<UI-NNN>` | **Design traceability**: this scenario proves design item `UI-NNN` from `requirements/ui-inventory.md` (`@ui:UI-014`). Repeatable. Read by `sungen audit` (`UI-ITEM-UNCOVERED`). An item deliberately not tested is recorded as a comment instead: `# UI-DEFERRED: UI-002..013 — <reason>`. Rules: `sungen-tc-generation` § Design Inventory. |
633
649
  | `@manual` | Skip in generation |
634
- | `@known-defect` | The scenario is EXPECTED to be red — it asserts a live defect. It still runs and still reports as failed (never skipped, never "fixed"), and it stays a pass-through tag so `--grep @known-defect` selects it. **`[mobile]`** Its failure does NOT escalate the next scenario's `@reset:` tier: a red that was designed in is not evidence of leftover state |
650
+ | `@known-defect` | The scenario is EXPECTED to be red — it asserts a live defect. It still runs and still reports as failed (never skipped, never "fixed"), and it stays a pass-through tag so `--grep @known-defect` selects it. `@known-defect:DEF-NNN` names the record in `qa/defects/DEF-NNN-*.md` (`sungen defect draft` writes it); `sungen audit` reports `DEFECT-REF-MISSING` when that file does not exist. **`[mobile]`** Its failure does NOT escalate the next scenario's `@reset:` tier: a red that was designed in is not evidence of leftover state |
635
651
  | `@auth:role` | Use auth storage state for role |
636
652
  | `@no-auth` | Disable inherited auth |
637
653
  | `@steps:name` | Define reusable step block (base scenario) |
@@ -35,6 +35,8 @@ user-invocable: false
35
35
  | **BALANCE** | secondary viewpoints (UI/validation/security) outweigh business-core | **Stop expanding** secondary viewpoints; generate the missing business-core scenarios first. Do not add more subscription/UI variants while core is thin. |
36
36
  | **TRACE** | scenarios use ad-hoc `VP-<CAT>-NNN` codes not linked to the viewpoint-overview | Make each scenario map to a viewpoint-overview id (align category codes, or add a mapping comment). |
37
37
  | **ORACLE-URL-ONLY** | every `Then` in the scenario is a page/URL assertion — it proves a path was pushed, not that the page rendered; on a permission/guard scenario a late 403 redirect turns it falsely green | Add `anchor:` to the page selector (one line per screen; every `see [X] page` on it becomes a content oracle) or a content assertion after the page step. Never delete the page step to silence it. |
38
+ | **DEFECT-REF-MISSING** | a `@known-defect:DEF-NNN` tag names no file in `qa/defects/` | Draft the record (`sungen defect draft --screen <name> --scenario "<title>"`) or correct the id. Never drop the tag to silence it — the scenario still asserts a live defect. |
39
+ | **DEFECT-ORPHAN** (info) | an open defect record names a scenario this unit no longer has | Update the record's `scenarios:` to the renamed title, or close the record if the scenario was removed on purpose. Nothing in the suite to repair. |
38
40
  | **UNIVERSAL** | a universal theme (error/empty-state, accessibility) is absent | Low priority — add if in scope; otherwise note as out-of-scope with reason. |
39
41
 
40
42
  ## P5 steps for deep cross-screen / list coverage
@@ -773,8 +773,9 @@ those namespaces are the **System INTEGRATION** group — keep their oracles abo
773
773
  > **Auto-detect**: if path is `qa/flows/<name>/` → use this section. Skip Steps 1–4 above.
774
774
 
775
775
  **Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
776
- `sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
777
- `FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). **A filled contract is an INPUT — never rewrite it
776
+ `sungen audit` scores the flow against** (`flowCoverage` axis = the declared flows / phases
777
+ automated; `FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`; `FLOW-GUARANTEE-MISSING`;
778
+ `FLOW-INVENTORY-MISSING`). **A filled contract is an INPUT — never rewrite it
778
779
  to match your output** (same rule as `test-viewpoint.md`).
779
780
 
780
781
  > **HARD RULE — the contract must be AUTHORED, never merely cited.** If
@@ -796,7 +797,7 @@ to match your output** (same rule as `test-viewpoint.md`).
796
797
  | Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
797
798
  | Test data keys | `{{variable}}` | `{{phase.variable}}` |
798
799
  | Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
799
- | 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 |
800
+ | 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 — the contract still needs both guarantees and a `flows:` inventory either way |
800
801
 
801
802
  **Scenarios to generate — every phase demanded by the contract, automated:**
802
803
 
@@ -329,7 +329,19 @@ function resolveDynamicVars(data: Record<string, any>): Record<string, any> {
329
329
  const ts = String(Date.now());
330
330
  const uid = crypto.randomUUID();
331
331
  const now = new Date();
332
- const date = now.toISOString().split('T')[0];
332
+ // `{{$date}}` is the runner's LOCAL calendar date — the "today" the browser sees when
333
+ // playwright.config sets no `timezoneId`. toISOString() is UTC, so east of Greenwich it named
334
+ // yesterday until the offset passed (00:00–06:59 at UTC+7). Node reads the `TZ` env var, so a
335
+ // config that pins `timezoneId` pins this too by exporting the same zone as `TZ`.
336
+ // `{{$datetime}}` stays an ISO-8601 UTC instant: it names a moment, not a calendar day.
337
+ const pad = (n: number): string => String(n).padStart(2, '0');
338
+ // `{{$date:+1d}}` / `{{$date:-7d}}` shift by whole days; built from the local Y/M/D, so it stays
339
+ // the right calendar day across a DST change.
340
+ const localDate = (offsetDays: number): string => {
341
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() + offsetDays);
342
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
343
+ };
344
+ const date = localDate(0);
333
345
  const datetime = now.toISOString();
334
346
 
335
347
  function resolveValue(value: any): any {
@@ -344,10 +356,16 @@ function resolveDynamicVars(data: Record<string, any>): Record<string, any> {
344
356
  const [min, max] = (args || '1:9999').split(':').map(Number);
345
357
  return String(Math.floor(Math.random() * (max - min + 1)) + min);
346
358
  }
347
- case 'date':
348
- return date;
359
+ case 'date': {
360
+ // An argument this does not understand stays literal, like an unknown variable — it used
361
+ // to be ignored, so `{{$date:+1d}}` silently produced today. Up to 5 digits (~270 years):
362
+ // a typo like +99999999999d would build an Invalid Date.
363
+ if (!args) return date;
364
+ const off = /^([+-]\d{1,5})d$/.exec(args.trim());
365
+ return off ? localDate(Number(off[1])) : match;
366
+ }
349
367
  case 'datetime':
350
- return datetime;
368
+ return args ? match : datetime;
351
369
  default:
352
370
  return match;
353
371
  }