@sun-asterisk/sungen 3.2.34-beta.1 → 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 (177) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +3 -29
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/index.js +0 -2
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  7. package/dist/exporters/test-data-resolver.js +12 -2
  8. package/dist/exporters/test-data-resolver.js.map +1 -1
  9. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  10. package/dist/generators/gherkin-parser/index.js +1 -0
  11. package/dist/generators/gherkin-parser/index.js.map +1 -1
  12. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
  13. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
  18. package/dist/generators/test-generator/code-generator.js +4 -5
  19. package/dist/generators/test-generator/code-generator.js.map +1 -1
  20. package/dist/generators/test-generator/diagnostics.d.ts +9 -3
  21. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  22. package/dist/generators/test-generator/diagnostics.js +9 -3
  23. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  24. package/dist/generators/test-generator/patterns/types.d.ts +1 -1
  25. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  26. package/dist/generators/test-generator/step-mapper.d.ts +8 -0
  27. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  28. package/dist/generators/test-generator/step-mapper.js +52 -8
  29. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  30. package/dist/generators/test-generator/utils/data-resolver.d.ts +16 -0
  31. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  32. package/dist/generators/test-generator/utils/data-resolver.js +59 -0
  33. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  34. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  35. package/dist/generators/test-generator/utils/selector-resolver.js +10 -2
  36. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  37. package/dist/harness/audit.d.ts.map +1 -1
  38. package/dist/harness/audit.js +2 -13
  39. package/dist/harness/audit.js.map +1 -1
  40. package/dist/harness/next-step.d.ts +0 -2
  41. package/dist/harness/next-step.d.ts.map +1 -1
  42. package/dist/harness/next-step.js +1 -15
  43. package/dist/harness/next-step.js.map +1 -1
  44. package/dist/harness/parse.d.ts +3 -6
  45. package/dist/harness/parse.d.ts.map +1 -1
  46. package/dist/harness/parse.js +4 -9
  47. package/dist/harness/parse.js.map +1 -1
  48. package/dist/harness/sensors.d.ts.map +1 -1
  49. package/dist/harness/sensors.js +9 -11
  50. package/dist/harness/sensors.js.map +1 -1
  51. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  52. package/dist/harness/viewpoint-baseline.js +1 -7
  53. package/dist/harness/viewpoint-baseline.js.map +1 -1
  54. package/dist/harness/viewpoint-ledger.d.ts +3 -13
  55. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  56. package/dist/harness/viewpoint-ledger.js +8 -38
  57. package/dist/harness/viewpoint-ledger.js.map +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -1
  62. package/dist/orchestrator/ai-skills/commands.js +5 -3
  63. package/dist/orchestrator/ai-skills/commands.js.map +1 -1
  64. package/dist/orchestrator/ai-skills/golden-skills.test.js +29 -0
  65. package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -1
  66. package/dist/orchestrator/flow-manager.d.ts +1 -0
  67. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  68. package/dist/orchestrator/flow-manager.js +52 -3
  69. package/dist/orchestrator/flow-manager.js.map +1 -1
  70. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  71. package/dist/orchestrator/screen-manager.js +52 -4
  72. package/dist/orchestrator/screen-manager.js.map +1 -1
  73. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
  74. package/dist/orchestrator/templates/ai-src/commands/create-test.md +22 -30
  75. package/dist/orchestrator/templates/ai-src/config/claude.md +20 -7
  76. package/dist/orchestrator/templates/ai-src/config/copilot.md +20 -7
  77. package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
  78. package/dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
  79. package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
  80. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
  81. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -4
  82. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +24 -8
  83. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -3
  84. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -4
  85. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +0 -18
  86. package/dist/orchestrator/templates/specs-test-data.ts +22 -4
  87. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +1 -4
  88. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -1
  89. package/dist/orchestrator/viewpoint-pattern-catalog.js +2 -5
  90. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -1
  91. package/package.json +3 -3
  92. package/src/cli/commands/audit.ts +3 -26
  93. package/src/cli/index.ts +0 -2
  94. package/src/exporters/test-data-resolver.ts +10 -2
  95. package/src/generators/gherkin-parser/index.ts +1 -0
  96. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
  97. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
  98. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
  99. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
  100. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
  101. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
  102. package/src/generators/test-generator/code-generator.ts +4 -4
  103. package/src/generators/test-generator/diagnostics.ts +9 -3
  104. package/src/generators/test-generator/patterns/types.ts +2 -0
  105. package/src/generators/test-generator/step-mapper.ts +56 -9
  106. package/src/generators/test-generator/utils/data-resolver.ts +53 -0
  107. package/src/generators/test-generator/utils/selector-resolver.ts +10 -2
  108. package/src/harness/audit.ts +2 -13
  109. package/src/harness/next-step.ts +1 -17
  110. package/src/harness/parse.ts +5 -13
  111. package/src/harness/sensors.ts +8 -9
  112. package/src/harness/viewpoint-baseline.ts +1 -7
  113. package/src/harness/viewpoint-ledger.ts +11 -52
  114. package/src/index.ts +2 -1
  115. package/src/orchestrator/ai-skills/commands.ts +5 -3
  116. package/src/orchestrator/ai-skills/golden-skills.test.ts +23 -0
  117. package/src/orchestrator/flow-manager.ts +53 -3
  118. package/src/orchestrator/screen-manager.ts +52 -4
  119. package/src/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
  120. package/src/orchestrator/templates/ai-src/commands/create-test.md +22 -30
  121. package/src/orchestrator/templates/ai-src/config/claude.md +20 -7
  122. package/src/orchestrator/templates/ai-src/config/copilot.md +20 -7
  123. package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
  124. package/src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
  125. package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
  126. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
  127. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -4
  128. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +24 -8
  129. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -3
  130. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -4
  131. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +0 -18
  132. package/src/orchestrator/templates/specs-test-data.ts +22 -4
  133. package/src/orchestrator/viewpoint-pattern-catalog.ts +2 -5
  134. package/dist/cli/commands/viewpoint.d.ts +0 -7
  135. package/dist/cli/commands/viewpoint.d.ts.map +0 -1
  136. package/dist/cli/commands/viewpoint.js +0 -70
  137. package/dist/cli/commands/viewpoint.js.map +0 -1
  138. package/dist/harness/viewpoint-draft-header.d.ts +0 -7
  139. package/dist/harness/viewpoint-draft-header.d.ts.map +0 -1
  140. package/dist/harness/viewpoint-draft-header.js +0 -100
  141. package/dist/harness/viewpoint-draft-header.js.map +0 -1
  142. package/dist/harness/viewpoint-mode.d.ts +0 -9
  143. package/dist/harness/viewpoint-mode.d.ts.map +0 -1
  144. package/dist/harness/viewpoint-mode.js +0 -52
  145. package/dist/harness/viewpoint-mode.js.map +0 -1
  146. package/dist/harness/viewpoint-priority.d.ts +0 -19
  147. package/dist/harness/viewpoint-priority.d.ts.map +0 -1
  148. package/dist/harness/viewpoint-priority.js +0 -36
  149. package/dist/harness/viewpoint-priority.js.map +0 -1
  150. package/dist/harness/viewpoint-review-open.d.ts +0 -6
  151. package/dist/harness/viewpoint-review-open.d.ts.map +0 -1
  152. package/dist/harness/viewpoint-review-open.js +0 -90
  153. package/dist/harness/viewpoint-review-open.js.map +0 -1
  154. package/dist/harness/viewpoint-scan-refs.d.ts +0 -50
  155. package/dist/harness/viewpoint-scan-refs.d.ts.map +0 -1
  156. package/dist/harness/viewpoint-scan-refs.js +0 -200
  157. package/dist/harness/viewpoint-scan-refs.js.map +0 -1
  158. package/dist/harness/viewpoint-scan.d.ts +0 -41
  159. package/dist/harness/viewpoint-scan.d.ts.map +0 -1
  160. package/dist/harness/viewpoint-scan.js +0 -118
  161. package/dist/harness/viewpoint-scan.js.map +0 -1
  162. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +0 -63
  163. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +0 -158
  164. package/dist/orchestrator/viewpoint-scaffold.d.ts +0 -3
  165. package/dist/orchestrator/viewpoint-scaffold.d.ts.map +0 -1
  166. package/dist/orchestrator/viewpoint-scaffold.js +0 -70
  167. package/dist/orchestrator/viewpoint-scaffold.js.map +0 -1
  168. package/src/cli/commands/viewpoint.ts +0 -58
  169. package/src/harness/viewpoint-draft-header.ts +0 -55
  170. package/src/harness/viewpoint-mode.ts +0 -49
  171. package/src/harness/viewpoint-priority.ts +0 -38
  172. package/src/harness/viewpoint-review-open.ts +0 -51
  173. package/src/harness/viewpoint-scan-refs.ts +0 -154
  174. package/src/harness/viewpoint-scan.ts +0 -111
  175. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +0 -63
  176. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +0 -158
  177. package/src/orchestrator/viewpoint-scaffold.ts +0 -36
@@ -58,13 +58,6 @@ function claimedCount(text: string): number | null {
58
58
  }
59
59
 
60
60
  const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.Title, VP7-002, MS-HP-001, TV-01
61
- /**
62
- * A bullet declares only the id it STARTS with (after a `[ ]` checkbox); one further along is a
63
- * citation (`… (SG-01, …)`). `- **High** VP-…` or `- Email shown — VP-…` therefore carry no id.
64
- */
65
- const BULLET_ID = new RegExp(`^[\\s(#]*(?:\\[[ xX]\\]\\s*)?\\[?${ID_RE.source}`);
66
- /** A table cell that is an id and nothing else. */
67
- const WHOLE_ID = new RegExp(`^${ID_RE.source}$`);
68
61
 
69
62
  /**
70
63
  * Lines that DECLARE something about the checklist rather than being a checklist item —
@@ -109,31 +102,17 @@ const PROCEDURE_LEAD = /^(?:how to (?:run|test|reproduce|verify)|(?:manual )?pro
109
102
  const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+/;
110
103
  const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
111
104
 
112
- /**
113
- * `decision`: a `- D4: …` line under Design Decisions. It names the spec's intent so rows can cite
114
- * it; its testable part is carried by a VP row (review check 8). No scenario ever carries `D4`, so
115
- * as a ledger item it stayed MISSING for good — "not tested" decisions included (phase-7 D5). The
116
- * ledger skips it; the continuity, source and integrity checks still read it. Only that section
117
- * declares: a QA claim labelled `D8` elsewhere stays a claim, and an id-less decision bullet stays
118
- * the prose claim it always was.
119
- */
120
- export interface ViewpointItem { id?: string; text: string; decision?: true }
121
- const DECISION_ID = /^D\d+$/;
122
- const DECISIONS_SECTION = /^(?:design decisions?|quyết định(?: thiết kế)?)\b/i;
123
-
124
105
  /** Extract atomic checklist items from a viewpoint file (format-tolerant). */
125
- export function parseViewpointItems(viewpointPath: string): ViewpointItem[] {
106
+ export function parseViewpointItems(viewpointPath: string): { id?: string; text: string }[] {
126
107
  if (!fs.existsSync(viewpointPath)) return [];
127
108
  const lines = readViewpointText(viewpointPath).split('\n');
128
- const items: ViewpointItem[] = [];
109
+ const items: { id?: string; text: string }[] = [];
129
110
  let inFence = false;
130
111
  let inDeclarationSection = false;
131
112
  // The level an excluded section was opened at. A SUBSECTION inherits it: `### Optional`
132
113
  // nested under `## 8. Traceability Index` used to re-open the section and hand its rows
133
114
  // back as claims, because exclusion was recomputed from scratch at every heading.
134
115
  let excludedAt: number | null = null;
135
- // The level the Design Decisions section was opened at, while inside it (see `decision`).
136
- let decisionsAt: number | null = null;
137
116
  // Inside the list a PROCEDURE_LEAD introduced: the indent of the lead (-1 for a paragraph lead),
138
117
  // whose list items — and their wrapped continuation lines — are steps. A heading, a table, a
139
118
  // new lead, or (after a paragraph lead) a blank line once a step was read ends it: when in
@@ -168,28 +147,21 @@ export function parseViewpointItems(viewpointPath: string): ViewpointItem[] {
168
147
  if (heading) {
169
148
  const level = heading[1].length;
170
149
  if (excludedAt !== null && level > excludedAt) continue; // still inside the excluded section
171
- if (decisionsAt !== null && level > decisionsAt) continue; // a subsection of Design Decisions
172
150
  // A heading both ends the previous section and decides whether this one declares or claims.
173
151
  // Strip an ordinal prefix ("## 5. Known Issues", "## 8. Viewpoint Grouping") before
174
152
  // testing. The regex anchors at the start, so a NUMBERED heading matched nothing and
175
153
  // every excluded section leaked its rows back in as claims — a real viewpoint numbers
176
154
  // its sections, so in practice the exclusion list was doing nothing at all (#657).
177
- // `Phần 4:` / `Part 4.` / `Mục 4.` number a section the same way.
178
- const headText = heading[2].replace(/[*`]/g, '').replace(/^\s*(?:(?:[Pp]hần|[Pp]art|[Mm]ục|[Ss]ection)\s+)?(?:\d+|[A-Z])\s*[.):]\s*/, '').trim();
155
+ const headText = heading[2].replace(/[*`]/g, '').replace(/^\s*(?:\d+|[A-Z])\s*[.):]\s*/, '').trim();
179
156
  inDeclarationSection = DECLARATION_SECTION.test(headText);
180
157
  excludedAt = inDeclarationSection ? level : null;
181
- decisionsAt = DECISIONS_SECTION.test(headText) ? level : null;
182
158
  continue;
183
159
  }
184
160
  if (inDeclarationSection) continue;
185
161
  let text = '';
186
162
  let lead = '';
187
- let declared: RegExpMatchArray | null = null;
188
163
  const bullet = line.match(/^(?:[-*+]|\d+[.)])\s+(.*)$/);
189
- if (bullet) {
190
- text = bullet[1]; lead = bullet[1];
191
- declared = lead.replace(/[*`]/g, '').match(BULLET_ID);
192
- }
164
+ if (bullet) { text = bullet[1]; lead = bullet[1]; }
193
165
  else if (line.startsWith('|')) { // table data row
194
166
  if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
195
167
  // The row right above a `|---|` separator IS the header, whatever its words (`Skill #`,
@@ -200,14 +172,7 @@ export function parseViewpointItems(viewpointPath: string): ViewpointItem[] {
200
172
  const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
201
173
  if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status|step|flow|ref|level|question|screen|actor|branches from|own steps|component|thành phần|trường|bước)$/i.test(cells[0] || '')) continue; // header
202
174
  text = cells.join(' — ');
203
- // The lead is the row's FIRST cell, empty or not: a continuation row (`| | | case | … see SG-02 |`)
204
- // declares nothing. A later cell holding an id and nothing else still declares (`| x | VP-UI-001 |`),
205
- // and so does a row number followed by a cell that starts with one (`| 1 | VP-UI-001 — … |`).
206
- const raw = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.replace(/[*`]/g, '').trim());
207
- lead = raw[0] ?? '';
208
- declared = lead.match(ID_RE)
209
- ?? (/^\d+\.?$/.test(lead) ? (raw[1] ?? '').match(BULLET_ID) : null)
210
- ?? raw.slice(1).map((c) => c.match(WHOLE_ID)).find(Boolean) ?? null;
175
+ lead = cells[0] ?? '';
211
176
  } else continue;
212
177
  text = text.replace(/[*`]/g, '').trim();
213
178
  if (!text) continue;
@@ -222,16 +187,15 @@ export function parseViewpointItems(viewpointPath: string): ViewpointItem[] {
222
187
  // under a grouping section. It declares nothing; it points at things declared elsewhere.
223
188
  const leadIds = new Set((leadClean.match(new RegExp(ID_RE.source, 'g')) || []).filter((x) => /\d/.test(x)));
224
189
  if (leadIds.size > 1) continue;
225
- const id = declared && /\d/.test(declared[1]) ? declared[1] : undefined; // require a digit so prose words aren't IDs
190
+ const idM = leadClean.match(ID_RE) ?? text.match(ID_RE);
191
+ const id = idM && /\d/.test(idM[1]) ? idM[1] : undefined; // require a digit so prose words aren't IDs
226
192
  // A row keyed by a bare GROUP prefix ("VP-LOGIC — transitions — High — why the flow exists")
227
193
  // declares a family and its priority, not a checklist item: nothing can "cover" it, and
228
194
  // demanding coverage sends the author looking for a test that was never meant to exist.
229
195
  if (/^VP-[A-Z]+\s*(?:—|-|\||$)/.test(leadClean) && !/\d/.test(leadClean)) continue;
230
196
  const words = (text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w));
231
197
  if (!id && words.length < 2) continue; // not substantive enough to track
232
- items.push(decisionsAt !== null && id && DECISION_ID.test(id)
233
- ? { id, text: text.slice(0, 100), decision: true }
234
- : { id, text: text.slice(0, 100) });
198
+ items.push({ id, text: text.slice(0, 100) });
235
199
  }
236
200
  return items;
237
201
  }
@@ -242,7 +206,6 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
242
206
  // (#622) — silently collapsing those would remove its input.
243
207
  const seen = new Set<string>();
244
208
  const items = parseViewpointItems(viewpointPath)
245
- .filter((i) => !i.decision)
246
209
  .filter((i) => { const k = `${i.id ?? ''}|${i.text}`; if (seen.has(k)) return false; seen.add(k); return true; });
247
210
  if (!fs.existsSync(viewpointPath) || items.length === 0) {
248
211
  return { hasViewpoint: fs.existsSync(viewpointPath), total: 0, covered: 0, ratio: 1, missing: [], manualOnly: [], partial: [] };
@@ -434,15 +397,11 @@ export function viewpointIntegrity(
434
397
 
435
398
  // The Priority-Viewpoints table: `| <id> | <priority> | <reason> |`.
436
399
  const vpText = readViewpointText(viewpointPath);
437
- const words = [...vpText
400
+ const rows = [...vpText
438
401
  .matchAll(new RegExp(`^\\|\\s*((?:VP|FL)[A-Z0-9._-]*)\\s*\\|\\s*(${PRIORITY_WORDS})\\s*\\|`, 'gim'))]
439
402
  .map((m) => m[2].toLowerCase());
440
- // Fold only synonyms (Medium / Med / Normal are one level). `critical` stays its own word: a
441
- // Critical / High / High table is a ranking, and folding it into `high` made it read as uniform.
442
- const level = (w: string): string => (/^(?:medium|med|normal)$/.test(w) ? 'normal' : w);
443
- const uniformPriority = words.length >= 3 && new Set(words.map(level)).size === 1
444
- ? { levels: [words[0]], rows: words.length } // the word QA wrote, not the folded key
445
- : null;
403
+ const levels = [...new Set(rows)];
404
+ const uniformPriority = rows.length >= 3 && levels.length === 1 ? { levels, rows: rows.length } : null;
446
405
 
447
406
  return { duplicateIds, danglingRefs, uniformPriority };
448
407
  }
package/src/index.ts CHANGED
@@ -20,7 +20,8 @@ export type { ResolvedAppProfile } from './generators/test-generator/app-profile
20
20
  export type { MappedStep } from './generators/test-generator/step-mapper';
21
21
  // The shared "nothing honest to compile" step body. Exported for the driver SPI: a pattern that
22
22
  // must refuse (SG-W017 and siblings) emits THIS rather than shipping a one-sided refusal template,
23
- // which would fail the adapter-parity gate and push playwright off its refusal-only floor of 0.
23
+ // which would fail the adapter-parity gate and push playwright off its refusal-only floor (1 — the
24
+ // `date-picker-unsupported` refusal both adapters ship for the resolver-based fill-input pattern, #907).
24
25
  export { unsupportedStepCode } from './generators/test-generator/patterns/unsupported-step-code';
25
26
  export type { ParsedStep } from './generators/gherkin-parser';
26
27
  export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
@@ -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) {
@@ -8,7 +8,6 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import { readCapabilities } from '../harness/capability';
10
10
  import { testDataHeader, ensureTestDataGuideFiles } from './test-data-guide';
11
- import { viewpointScaffold } from './viewpoint-scaffold';
12
11
 
13
12
  export interface FlowOptions {
14
13
  name: string;
@@ -144,7 +143,7 @@ export class FlowManager {
144
143
  fs.writeFileSync(specPath, this.generateSpecTemplate(options, flowName, isMobile), 'utf-8');
145
144
 
146
145
  const viewpointPath = path.join(requirementsDir, 'test-viewpoint.md');
147
- fs.writeFileSync(viewpointPath, viewpointScaffold(`${options.name} Flow`, isMobile), 'utf-8');
146
+ fs.writeFileSync(viewpointPath, this.generateViewpointTemplate(options), 'utf-8');
148
147
 
149
148
  console.log(`Created files:`);
150
149
  console.log(` ${path.relative(this.cwd, featurePath)}`);
@@ -218,7 +217,7 @@ export class FlowManager {
218
217
  restored.push(file);
219
218
  };
220
219
  ensure(path.join(paths.requirementsDir, 'spec.md'), () => this.generateSpecTemplate(options, flowName, isMobile));
221
- ensure(path.join(paths.requirementsDir, 'test-viewpoint.md'), () => viewpointScaffold(`${options.name} Flow`, isMobile));
220
+ ensure(path.join(paths.requirementsDir, 'test-viewpoint.md'), () => this.generateViewpointTemplate(options));
222
221
  ensure(paths.featurePath, () => this.generateFeatureTemplate(options, flowName, isMobile));
223
222
  return restored;
224
223
  }
@@ -288,6 +287,57 @@ export class FlowManager {
288
287
  `;
289
288
  }
290
289
 
290
+ private generateViewpointTemplate(options: FlowOptions): string {
291
+ return [
292
+ `# ${options.name} Flow — Test Viewpoints`,
293
+ '',
294
+ '## Edge Cases',
295
+ '',
296
+ '<!-- Sample — replace with actual edge cases for this flow:',
297
+ '- Network drops between screen transitions — should resume or show error?',
298
+ '- Session expires mid-flow — should redirect to login and preserve progress?',
299
+ '- Browser back button at step 3 — should return to step 2 with data intact?',
300
+ '- Complete flow twice rapidly — should prevent duplicate submissions',
301
+ '-->',
302
+ '',
303
+ '## Known Issues',
304
+ '',
305
+ '<!-- Sample — replace with actual known issues:',
306
+ '- [BUG-001] Form data lost if user navigates away and returns',
307
+ '- [BUG-002] Confirmation page shows stale data on slow connections',
308
+ '-->',
309
+ '',
310
+ '## Design Decisions',
311
+ '',
312
+ '<!-- Sample — replace with actual design decisions:',
313
+ '- Flow requires completing all steps in order (no skip)',
314
+ '- Draft is auto-saved at each step transition',
315
+ '- Cancel at any step discards the entire flow',
316
+ '-->',
317
+ '',
318
+ '## Cross-Screen Concerns',
319
+ '',
320
+ '<!-- Sample — replace with actual cross-screen concerns:',
321
+ '- Auth role must remain consistent across all screens',
322
+ '- Data entered in step 1 must be visible in confirmation (step 3)',
323
+ '- Error in step 2 should allow returning to step 1 without re-entering data',
324
+ '-->',
325
+ '',
326
+ '## Priority Viewpoints',
327
+ '',
328
+ '<!-- Rate importance for this flow (High / Medium / Low / Skip):',
329
+ '',
330
+ '| VP | Priority | Reason |',
331
+ '|---|----------|--------|',
332
+ '| VP-UI | Medium | Standard forms across screens |',
333
+ '| VP-VAL | High | Cross-screen validation rules |',
334
+ '| VP-LOGIC | High | Multi-step business logic, state transitions |',
335
+ '| VP-SEC | Medium | Auth must persist, role-based access at each screen |',
336
+ '-->',
337
+ '',
338
+ ].join('\n');
339
+ }
340
+
291
341
  private generateFeatureTemplate(options: FlowOptions, flowName: string, isMobile: boolean): string {
292
342
  const description = options.description || `complete the ${options.name} flow`;
293
343
 
@@ -8,7 +8,7 @@ import * as path from 'path';
8
8
  import { readCapabilities } from '../harness/capability';
9
9
  import { testDataHeader, ensureTestDataGuideFiles } from './test-data-guide';
10
10
  import { resolveWebBaseUrl } from '../exporters/env-qa';
11
- import { viewpointScaffold } from './viewpoint-scaffold';
11
+ import { uiPatternRows } from './viewpoint-pattern-catalog';
12
12
 
13
13
  export interface ScreenOptions {
14
14
  name: string;
@@ -158,11 +158,59 @@ export class ScreenManager {
158
158
  fs.writeFileSync(specPath, this.generateSpecTemplate(options, screenName), 'utf-8');
159
159
  }
160
160
 
161
- // Generate requirements test-viewpoint.md (only on first screen creation): the QA template,
162
- // samples inside comments, so an untouched file declares nothing (see viewpoint-scaffold.ts).
161
+ // Generate requirements test-viewpoint.md (only on first screen creation)
163
162
  const viewpointPath = path.join(requirementsDir, 'test-viewpoint.md');
164
163
  if (!fs.existsSync(viewpointPath)) {
165
- fs.writeFileSync(viewpointPath, viewpointScaffold(options.name, isMobile), 'utf-8');
164
+ fs.writeFileSync(viewpointPath, [
165
+ `# ${options.name} — Test Viewpoints`,
166
+ '',
167
+ '## Edge Cases',
168
+ '',
169
+ '<!-- Sample — replace with actual edge cases for this screen:',
170
+ '- Paste emoji into text fields (e.g., 🎉👋) — should accept or sanitize?',
171
+ '- Input 10,000+ characters into free text — truncate or reject?',
172
+ '- Double-click Submit rapidly — should only create 1 record',
173
+ '- Browser back button after successful submit — should not re-submit',
174
+ '-->',
175
+ '',
176
+ '## Known Issues',
177
+ '',
178
+ '<!-- Sample — replace with actual known issues:',
179
+ '- [BUG-001] Date picker does not work on Safari < 16',
180
+ '- [BUG-002] File upload silently fails for files > 20MB (no error shown)',
181
+ '- Search does not handle Vietnamese accents correctly ("Nguyên" ≠ "Nguyen")',
182
+ '-->',
183
+ '',
184
+ '## Design Decisions',
185
+ '',
186
+ '<!-- Sample — replace with actual design decisions:',
187
+ '- Show all validation errors at once (not one at a time) — per UX review',
188
+ '- Cancel button resets form completely (no draft saving)',
189
+ '- Anonymous mode hides sender name but keeps department visible',
190
+ '-->',
191
+ '',
192
+ '## UI Patterns Identified',
193
+ '',
194
+ '<!-- Check applicable patterns for this screen (numbered as in the sungen-viewpoint skill):',
195
+ '',
196
+ '| # | Pattern | Applicable? | Notes |',
197
+ '|---|---------|-------------|-------|',
198
+ ...uiPatternRows(isMobile),
199
+ '-->',
200
+ '',
201
+ '## Priority Viewpoints',
202
+ '',
203
+ '<!-- Rate importance for this screen (High / Medium / Low / Skip):',
204
+ '',
205
+ '| VP | Priority | Reason |',
206
+ '|---|----------|--------|',
207
+ '| VP-UI | High | Complex form with many states |',
208
+ '| VP-VAL | High | 14 validation rules from spec |',
209
+ '| VP-LOGIC | Medium | Standard CRUD, no complex business rules |',
210
+ '| VP-SEC | Low | Internal tool, basic auth only |',
211
+ '-->',
212
+ '',
213
+ ].join('\n'), 'utf-8');
166
214
  }
167
215
 
168
216
  // Auto-screenshot: capture page only when --capture is set and not mobile (requires --path)
@@ -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
 
@@ -64,8 +64,7 @@ restated from screen specs the project does not hold, say so: the audit reports
64
64
  `SPEC-RESTATED-UNVERIFIED` because `specFR 100%` over a hand-copied list certifies the copy, not
65
65
  the source.
66
66
 
67
- **A viewpoint you draft (mode 2, step 3) carries BOTH classification axes** — on a QA-written one
68
- (mode 1) a missing axis or guarantee line is a row in `viewpoint-review.md`, not an edit. `VP-LOGIC / VP-VAL / VP-SEC / VP-NAV` classify
67
+ **The viewpoint carries BOTH classification axes.** `VP-LOGIC / VP-VAL / VP-SEC / VP-NAV` classify
69
68
  by TECHNICAL RISK; `BF / AF / EF` classify by BUSINESS BRANCH. They answer different questions and
70
69
  neither replaces the other — a reviewer needs to see both, so use the compound id
71
70
  (`VP-SEC-EF02-001`) and give the viewpoint a section per flow as well as its priority table by
@@ -82,8 +81,8 @@ questions and only one of them is a yardstick:
82
81
  | `flow-contract.yaml` | the USE CASE — actor · trigger · goal · precondition · both guarantees · the flow inventory | `flowCoverage` |
83
82
  | `test-viewpoint.md` | the CLAIMS the suite must prove — cross-screen concerns, edge cases, design decisions, open questions, priorities | `atomicLedger` · `traceability` |
84
83
 
85
- If `requirements/test-viewpoint.md` is missing or still the scaffold, **draft it before generating**
86
- (mode 2, step 3 — `add-flow` scaffolds the template; a run that deletes or replaces it has removed the yardstick). Without it two scored
84
+ If `requirements/test-viewpoint.md` is missing, **write it before generating** (`add-flow` scaffolds
85
+ a template; a run that deletes or replaces it has removed the yardstick). Without it two scored
87
86
  axes are excluded and four sensors cannot run at all — the atomic ledger, the
88
87
  declaration-integrity checks, the browser-gesture check and the two-way continuity check — so the
89
88
  audit reports `VIEWPOINT-MISSING` and the result is *unmeasured*, not passing. And the reason the
@@ -96,8 +95,8 @@ ordering choice: a yardstick derived from the scenarios reads 100% by constructi
96
95
  dropped along the way stops being missing from anything. `sungen audit` now reports
97
96
  `VIEWPOINT-ADOPTED-POST-HOC` on the first sighting of a viewpoint alongside an existing suite and
98
97
  holds `atomicLedger` + `traceability` as unverified until a human accepts it — so the honest move
99
- is to write the claims from `spec.md` and the contract, mark the file `drafted-by: ai` (its first
100
- line), and let the QA revise before `--accept-viewpoint`. **You never run `--accept-viewpoint` yourself** — `sungen next` lists it as a `⚖ decide` step for exactly this reason: an agent accepting the yardstick it wrote in the same session makes `atomicLedger` and `traceability` read 100% against its own checklist, and the Exception Flow it forgot stops being missing from anything. Present the acceptance to the QA in the hand-back (`AskUserQuestion`) with what the file declares — never as a question mid-run.
98
+ is to write the claims from `spec.md` and the contract, mark plainly that they are a draft for QA
99
+ review, and let the QA revise before `--accept-viewpoint`. **You never run `--accept-viewpoint` yourself** — `sungen next` lists it as a `⚖ decide` step for exactly this reason: an agent accepting the yardstick it wrote in the same session makes `atomicLedger` and `traceability` read 100% against its own checklist, and the Exception Flow it forgot stops being missing from anything. Present the acceptance to the QA (`AskUserQuestion`) with what the file declares, and stop.
101
100
 
102
101
  **When you give a claim an id, a scenario must carry that id.** The id is the contract: a claim
103
102
  whose id no scenario carries is reported MISSING, and word overlap will not rescue it. Several
@@ -111,9 +110,8 @@ narrow the claim to what is actually proven.
111
110
 
112
111
  **Priority comes from the declaration.** A scenario tagged `@high` under a viewpoint row that says
113
112
  `Low` is `PRIORITY-CONTRADICTED` — a smoke run selects on the tag, so it picks up work the QA
114
- deliberately deprioritised. Inherit the declared priority; if the tag is right, the declaration is
115
- what is wrong — fix it in your own draft (mode 2), or report it in `viewpoint-review.md` for the QA
116
- (mode 1). Never leave both on record silently.
113
+ deliberately deprioritised. Inherit the declared priority; if the tag is right, fix the
114
+ declaration instead of leaving both on record.
117
115
 
118
116
  ---
119
117
 
@@ -207,11 +205,22 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
207
205
  **Screen requirements** — check `<base>/${input:name}/requirements/`:
208
206
  {{/cap}}
209
207
  - If `spec.md` exists → read it as PRIMARY source (sections, fields, validation rules, business rules, states).
210
- - `test-viewpoint.md` → **Viewpoint — review or draft** below, once every source is read. No question here.
211
208
  {{#cap parallel-subagents}}
209
+ - If `test-viewpoint.md` exists → read it. If it only contains HTML comments (scaffold template), use `AskUserQuestion` to ask:
210
+ - **Fill test-viewpoint.md first** — I'll help you identify edge cases, known issues, and design decisions for this screen before generating tests
211
+ - **Continue without it** — generate tests from spec and other sources only
212
+
213
+ **A filled `test-viewpoint.md` is an INPUT — never an output.** Do NOT rewrite, re-order, or replace its viewpoint declarations as part of generating tests. It is the yardstick two scored axes measure the suite against (`traceability`, `atomicLedger`): rewrite it to match what you just generated and both read 100% by construction, while any viewpoint you quietly dropped stops being missing from anything. If the declared taxonomy genuinely does not fit the screen, **report the mismatch and ask** — propose the diff (ids added / removed and why) and let the QA decide. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and stops scoring both axes when this file moves, so an unannounced rewrite lowers the score rather than raising it.
212
214
 
213
215
  **Context discovery (prefer an isolated agent).** Reading all sources here can flood this context. **Claude Code:** spawn the **`sungen-discovery`** sub-agent (Task tool, `subagent_type: sungen-discovery`) to read spec/figma/ui/live in isolation and return a **compact discovery report** (sources, completeness, conflicts, recommended route, key facts); use that report instead of pasting raw sources. **Copilot / no sub-agents:** do the reading inline as below.
214
216
  {{/cap}}
217
+ {{^cap parallel-subagents}}
218
+ - If `test-viewpoint.md` exists → read it. If it only contains HTML comments (scaffold template), ask:
219
+ - **1) Fill test-viewpoint.md first** — identify edge cases, known issues, and design decisions before generating tests
220
+ - **2) Continue without it** — generate tests from spec and other sources only
221
+
222
+ **A filled `test-viewpoint.md` is an INPUT — never an output.** Do NOT rewrite, re-order, or replace its viewpoint declarations while generating tests. It is the yardstick two scored axes measure the suite against (`traceability`, `atomicLedger`): rewrite it to match what you just generated and both read 100% by construction, while any viewpoint you quietly dropped stops being missing from anything. If the declared taxonomy genuinely does not fit the screen, **report the mismatch and ask** — propose the diff (ids added / removed and why) and let the QA decide. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and stops scoring both axes when this file moves.
223
+ {{/cap}}
215
224
 
216
225
  **Auto-detect visual source** — do NOT ask the user to pick a source. Instead, check what already exists and use it:
217
226
  1. If `spec_figma.md` exists → read it as Figma supplement (PAT flow already completed during `add-screen`). Do NOT call any `mcp__figma__*` tool.
@@ -235,18 +244,6 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
235
244
 
236
245
  **Cross-check + Design Inventory**: when the unit holds any design source (`ui/` images, `ui/figma-meta.md`, `spec_figma.md`), follow `sungen-tc-generation` § **Design Inventory** (reading order, one `UI-NNN` row per item, `spec+design` / `design-only` / `conflict`) and **write `requirements/ui-inventory.md`** before generating — from the discovery report's UI INVENTORY block when a sub-agent read the sources. Every `design-only` / `conflict` item must then be covered (`@ui:UI-NNN`) or deferred with a reason (`# UI-DEFERRED: UI-NNN — <reason>`) in the `.feature`; `sungen audit` reports `UI-ITEM-UNCOVERED` otherwise. Show the user the conflicts and design-only items — they are decisions, not details.
237
246
 
238
- **Viewpoint — review or draft (no question asked).** Run `sungen viewpoint scan --screen <name> --json` (a flow: `--flow <name>`). It reads ids and item numbers only. Its `mode` (1 or 2) and `modeReason` name the row below — follow them; the table explains them:
239
-
240
- | `requirements/test-viewpoint.md` | Mode | You |
241
- |---|---|---|
242
- | missing; nothing but the scaffold (headings, `<…>` placeholders, empty tables); or ONE OR MORE VP rows and every one of them lacks a `Kỳ vọng` | **2 — draft** | Write it from `sungen-viewpoint/viewpoint-template.md`, first line `drafted-by: ai`, keeping any row already there. Each row answers "test CÁI GÌ" and "kỳ vọng ra sao"; where the spec states the expected result, write it; only what the spec leaves open becomes a closed question in mục 5, never an invented expectation; every accepted-input class the spec names (whitelist, value list, conversion) gets an "accepted, no error" row, citing the list by name; a VP that needs a real device, account or time is `# MANUAL/ENV`. Then scan again. |
243
- | first line `drafted-by: ai` | **2 — re-review** | Your draft, not yet accepted: you may revise it, from the spec and the review. |
244
- | anything else — QA-written, with or without VP ids (a scaffold from an older sungen reads as this row too: delete the file to have it drafted) | **1 — review only** | Never edit it. Generate from it as written; a gap is reported, not filled. |
245
-
246
- Then review it with `sungen-viewpoint/review-checklist.md` and the scan JSON (mode 2: fix your draft until only QA decisions remain) and write `requirements/viewpoint-review.md`, overwritten each run. Mode 2 also runs `sungen audit --screen <name> --no-baseline` before generating. Keep the review's row count for the hand-back.
247
-
248
- **A filled `test-viewpoint.md` is an INPUT — never an output.** Do NOT rewrite, re-order, or replace its viewpoint declarations as part of generating tests — your own draft included: a draft is revised from the spec, never from the scenarios. It is the yardstick two scored axes measure the suite against (`traceability`, `atomicLedger`): rewrite it to match what you just generated and both read 100% by construction, while any viewpoint you quietly dropped stops being missing from anything. If the declared taxonomy genuinely does not fit the screen, **report the mismatch in `viewpoint-review.md`** — the diff (ids added / removed and why) — and let the QA decide. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and stops scoring both axes when this file moves, so an unannounced rewrite lowers the score rather than raising it.
249
-
250
247
  Summarize what you found in requirements and present to the user.
251
248
 
252
249
  {{#cap parallel-subagents}}
@@ -287,7 +284,6 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
287
284
  - **BALANCE** → stop expanding secondary viewpoints; add business-core scenarios first.
288
285
  - **TRACE** → align `VP-` ids with the viewpoint-overview.
289
286
  - Stop when the gate PASSes and findings clear, **or** the budget is exhausted → report residual gaps honestly (never fake a pass).
290
- - **Repair never edits a QA-written viewpoint (mode 1).** A finding whose fix is an edit to `test-viewpoint.md` becomes a row in `viewpoint-review.md` for the QA when it cites one of the file's VP ids (the checklist's audit rule; anything else goes in the hand-back); in a mode-2 draft, fix the draft.
291
287
 
292
288
  5.6. **Record (reuse + observability).** Build the manifest and report usage:
293
289
  - `sungen manifest --screen <name>` — fingerprints for next-run change detection. On a **re-run**, start the whole command by `sungen manifest --screen <name> --diff` and only regenerate scenarios whose spec section changed (keep/regenerate/retire).
@@ -301,7 +297,7 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
301
297
  > **No parallel fan-out here.** Copilot has no sub-agents, so generation is sequential (the Claude Code variant fans out one `sungen-generator` per viewpoint group and merges). Same output, no speedup.
302
298
  5.3. **Standardize field test-data — Data Factory (when the unit has input fields).** For each input field, author the field-map `qa/data-factory/<name>.fields.yaml` (field → catalog `type` + real `constraints` + `errorMap`), then run `sungen data lint --screen ${input:name}` and `sungen data gen --screen ${input:name}` per the **`sungen-data-factory`** skill. `data gen` **MERGES** into `test-data.yaml` (replaces only its field keys, preserves the rest), so it is safe here. Weave the standardized boundary/invalid sets into `@cases` with `CHK-*` trace. Then `… data validate` (values correct) + `… data crosscheck` (testcase↔data agree); for a stateful precondition, `… data state --scaffold <name>` to auto-draft → fill entities + confirm with QA → `… data state`, weave the `@manual:data-setup` `Background` (works spec+viewpoint only). **Same standard as `/sungen:create-data-test`, applied inline — do NOT run that command separately afterwards.** No input fields (navigation/list) → skip.
303
299
  5.4. **Depth self-check (deterministic — BEFORE the audit).** Run `sungen depth-lint --screen ${input:name}`. It splits every shallow business-critical scenario into **DEEPEN IN PLACE** (add a real value assertion — the printed `template` is a theme-keyed hint, apply judgment to the actual claim; never fake one onto a visibility/behavior scenario) and **CROSS-SCREEN** (route to a flow / tag `@manual:Mx` + reason — removes it from the depth denominator honestly). Act on both, re-run until `deepen` is empty (or only honest over-counts remain), THEN gate. Lifts first-pass `businessDepth` mechanically instead of via 2–3 repair rounds.
304
- 5.5. **Quality gate & repair (harness — always run).** Per `sungen-harness-audit`: run `sungen audit --screen ${input:name}` (structural), THEN do an **independent semantic review inline** using the `sungen-reviewer` criteria (does each scenario's steps PROVE its title/viewpoint? observable Thens? business-critical assertion depth?). Merge both sets of issues; if gate FAILs / findings exist, repair (budget 3) and re-audit — GATE missing theme → generate it (cross-screen → **automate it in the flow** via `/sungen:add-flow`, NOT a full `@manual` screen duplicate — `sungen audit` flags an automatable `@manual` as `MANUAL-AUTOMATABLE`; reserve `@manual:Mx` for true judgment/missing-capability); DEPTH → add data assertions; BALANCE → add business-core first; TRACE → align VP ids. Never fake a pass. Repair never edits a QA-written viewpoint (mode 1): a finding whose fix is an edit to `test-viewpoint.md` and cites one of its VP ids becomes a row in `viewpoint-review.md`; in a mode-2 draft, fix the draft.
300
+ 5.5. **Quality gate & repair (harness — always run).** Per `sungen-harness-audit`: run `sungen audit --screen ${input:name}` (structural), THEN do an **independent semantic review inline** using the `sungen-reviewer` criteria (does each scenario's steps PROVE its title/viewpoint? observable Thens? business-critical assertion depth?). Merge both sets of issues; if gate FAILs / findings exist, repair (budget 3) and re-audit — GATE missing theme → generate it (cross-screen → **automate it in the flow** via `/sungen:add-flow`, NOT a full `@manual` screen duplicate — `sungen audit` flags an automatable `@manual` as `MANUAL-AUTOMATABLE`; reserve `@manual:Mx` for true judgment/missing-capability); DEPTH → add data assertions; BALANCE → add business-core first; TRACE → align VP ids. Never fake a pass.
305
301
  5.6. **Record.** `sungen manifest --screen ${input:name}`. Ledger **each phase** (not just repair) — pick one `runId` at the start and pass it so `trace`/`ledger report` show THIS run, not a mix: `sungen ledger record --screen ${input:name} --run <runId> --step <discovery|viewpoint|gherkin|audit|repair:N> --ms <elapsed>`. On re-run, start with `sungen manifest --screen ${input:name} --diff` and only regenerate changed sections.
306
302
  6. **Converge — show the trace.** Run `sungen trace --screen ${input:name}` and present: process map (phases + repair rounds), bottlenecks, **HUMAN-LOOP FOCUS** (@manual to verify), audit score + gate + residual gaps. Then offer next steps based on which tier was just generated:
307
303
  {{/cap}}
@@ -367,13 +363,9 @@ Do not stop after writing the files. Close every run with, in this order:
367
363
  1. **The traceability check below** — an untagged requirement becomes an uncovered gap at delivery.
368
364
  2. **The harness result in one line** — score, gate verdict, and the axis the `SCORE-CAPPED` finding
369
365
  names (that finding is the single answer to "how do I raise the score").
370
- 3. **The viewpoint review in one line** — `Viewpoint review: N điểm cần QA xem` (N = the `[ ]` rows
371
- of `requirements/viewpoint-review.md`).
372
- 4. **`AskUserQuestion` with the next actions** — never end with prose alone. Offer
366
+ 3. **`AskUserQuestion` with the next actions** — never end with prose alone. Offer
373
367
  `/sungen:run-test <name>` (recommended once the tiers are written), re-running `create-test` to
374
- extend, "Mở viewpoint-review.md" when N > 0, and "Done for now". After a mode-2 draft also list
375
- the decision `sungen audit --screen <name> --accept-viewpoint` — for the QA to run once the draft
376
- is reviewed; you never run it yourself. A run that ends without this leaves the operator guessing.
368
+ extend, and "Done for now". A run that ends without this leaves the operator guessing.
377
369
 
378
370
  ### Requirement traceability
379
371