@sun-asterisk/sungen 3.2.1-beta.1 → 3.2.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +31 -0
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/exporters/feature-parser.d.ts +25 -0
  5. package/dist/exporters/feature-parser.d.ts.map +1 -1
  6. package/dist/exporters/feature-parser.js +59 -0
  7. package/dist/exporters/feature-parser.js.map +1 -1
  8. package/dist/exporters/types.d.ts +38 -0
  9. package/dist/exporters/types.d.ts.map +1 -1
  10. package/dist/exporters/xlsx-exporter.d.ts +31 -2
  11. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  12. package/dist/exporters/xlsx-exporter.js +144 -1
  13. package/dist/exporters/xlsx-exporter.js.map +1 -1
  14. package/dist/harness/parse.d.ts.map +1 -1
  15. package/dist/harness/parse.js +4 -1
  16. package/dist/harness/parse.js.map +1 -1
  17. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  18. package/dist/orchestrator/ai-rules-updater.js +1 -0
  19. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  20. package/dist/orchestrator/templates/ai-instructions/claude-agent-generator.md +44 -0
  21. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +18 -1
  22. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +27 -0
  23. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
  24. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +2 -0
  25. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +2 -1
  26. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +27 -0
  27. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
  28. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +2 -0
  29. package/dist/orchestrator/templates/specs-api.d.ts +7 -0
  30. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  31. package/dist/orchestrator/templates/specs-api.js +13 -2
  32. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  33. package/dist/orchestrator/templates/specs-api.ts +13 -2
  34. package/package.json +3 -3
  35. package/src/cli/commands/delivery.ts +32 -2
  36. package/src/exporters/feature-parser.ts +57 -0
  37. package/src/exporters/types.ts +38 -0
  38. package/src/exporters/xlsx-exporter.ts +176 -2
  39. package/src/harness/parse.ts +4 -1
  40. package/src/orchestrator/ai-rules-updater.ts +1 -0
  41. package/src/orchestrator/templates/ai-instructions/claude-agent-generator.md +44 -0
  42. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +18 -1
  43. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +27 -0
  44. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
  45. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +2 -0
  46. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +2 -1
  47. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +27 -0
  48. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
  49. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +2 -0
  50. package/src/orchestrator/templates/specs-api.ts +13 -2
@@ -20,8 +20,8 @@ import {
20
20
  renderCsv,
21
21
  writeCsv,
22
22
  } from '../../exporters/csv-exporter';
23
- import { renderXlsxMultiSheet, writeXlsx } from '../../exporters/xlsx-exporter';
24
- import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow } from '../../exporters/types';
23
+ import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
24
+ import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
25
25
 
26
26
  const COLOR = {
27
27
  reset: '\x1b[0m',
@@ -135,6 +135,24 @@ function qaDir(cwd: string, target: DeliveryTarget): string {
135
135
  return path.join(cwd, 'qa', qaParent(target.kind), target.screen);
136
136
  }
137
137
 
138
+ /**
139
+ * Load the apis.yaml catalog for an api-kind unit.
140
+ * The catalog lives at qa/api/<screen>/api/apis.yaml.
141
+ * Returns an empty object when the file is absent (allows graceful degradation).
142
+ */
143
+ function loadApiCatalog(cwd: string, target: DeliveryTarget): Record<string, ApiCatalogEntry> {
144
+ if (target.kind !== 'api') return {};
145
+ const catalogPath = path.join(cwd, 'qa', 'api', target.screen, 'api', 'apis.yaml');
146
+ if (!fs.existsSync(catalogPath)) return {};
147
+ try {
148
+ const parsed = parseYaml(fs.readFileSync(catalogPath, 'utf-8'));
149
+ if (parsed && typeof parsed === 'object') {
150
+ return parsed as Record<string, ApiCatalogEntry>;
151
+ }
152
+ } catch { /* malformed yaml — skip */ }
153
+ return {};
154
+ }
155
+
138
156
  function generatedDir(cwd: string, target: DeliveryTarget): string {
139
157
  const sub = target.kind === 'flow' ? path.join('flows', target.screen) : target.kind === 'api' ? path.join('api', target.screen) : target.screen;
140
158
  return path.join(cwd, 'specs', 'generated', sub);
@@ -411,6 +429,10 @@ async function exportTarget(
411
429
  const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : '';
412
430
  const explicitEnv = process.env.SUNGEN_ENV;
413
431
 
432
+ // For api-kind units, load the endpoint catalog so we can add the API detail sheet.
433
+ const apiCatalog = loadApiCatalog(cwd, target);
434
+ const hasApiCatalog = target.kind === 'api' && Object.keys(apiCatalog).length > 0;
435
+
414
436
  // Decide between single-locale and aggregated multi-locale flows.
415
437
  // • SUNGEN_ENV set → single locale (existing behaviour, no change)
416
438
  // • SUNGEN_ENV unset → discover every *-test-result*.json variant.
@@ -439,6 +461,10 @@ async function exportTarget(
439
461
  { sheetName: 'Auto', summary: buildSummary(label, autoRows, ''), rows: autoRows, specLink },
440
462
  { sheetName: 'Manual', summary: buildSummary(label, manualRows, ''), rows: manualRows, specLink },
441
463
  ]);
464
+ // Append the API detail sheet when the unit has a catalog (api-kind only; no-op otherwise).
465
+ if (hasApiCatalog) {
466
+ addApiDetailSheet(wb, buildApiDetailRows(apiCatalog, feature.scenarios));
467
+ }
442
468
  await writeXlsx(cwd, target.featureBaseName, wb);
443
469
  return buildSummary(label, rows, path.relative(cwd, csvPath));
444
470
  }
@@ -496,6 +522,10 @@ async function exportTarget(
496
522
  { sheetName: 'Manual', summary: buildSummary(label, manualRows, ''), rows: manualRows, specLink },
497
523
  ];
498
524
  const wb = renderXlsxMultiSheet(sheets);
525
+ // Append the API detail sheet when the unit has a catalog (api-kind only; no-op otherwise).
526
+ if (hasApiCatalog) {
527
+ addApiDetailSheet(wb, buildApiDetailRows(apiCatalog, feature.scenarios));
528
+ }
499
529
  await writeXlsx(cwd, target.featureBaseName, wb);
500
530
 
501
531
  return primarySummary ?? buildSummary(label, (autoSheets[0]?.rows ?? []).concat(manualRows), primaryCsvPath);
@@ -164,6 +164,9 @@ export function splitVpAndName(scenarioName: string): { vpId?: string; category1
164
164
  * Map VP prefix to Category 2.
165
165
  * XSS/Injection security tests are input-validation tests → Function.
166
166
  * All other VP-SEC tests (auth, RBAC, access control) → Accessing.
167
+ * API auth viewpoints (missing/invalid/insufficient credentials) are access-control
168
+ * tests → Accessing; every other API viewpoint (contract, error, idempotency, flow,
169
+ * async, and the numbered baseline ids) is functional → Function.
167
170
  */
168
171
  export function mapVpToCategory2(vpId: string | undefined, scenarioName?: string): string {
169
172
  if (!vpId) return 'Function';
@@ -171,12 +174,66 @@ export function mapVpToCategory2(vpId: string | undefined, scenarioName?: string
171
174
  if (scenarioName && /xss|injection/i.test(scenarioName)) return 'Function';
172
175
  return 'Accessing';
173
176
  }
177
+ if (vpId.startsWith('VP-API-')) {
178
+ if (vpId.startsWith('VP-API-AUTH')) return 'Accessing';
179
+ return 'Function';
180
+ }
174
181
  if (vpId.startsWith('VP-UI-')) return 'GUI';
175
182
  if (vpId.startsWith('VP-VAL-')) return 'Function';
176
183
  if (vpId.startsWith('VP-LOGIC-')) return 'Function';
177
184
  return 'Function';
178
185
  }
179
186
 
187
+ // ---------------------------------------------------------------------------
188
+ // API annotation helpers
189
+ // Used by the XLSX API-detail sheet only — no effect on CSV/BM-2-901-13 path.
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /**
193
+ * Extract ordered @api:<name> call sequence from feature-level or scenario tags.
194
+ * Tags may contain call arguments: @api:register(name={{x}},email={{y}}) — we strip
195
+ * the argument parens and keep only the endpoint name.
196
+ *
197
+ * Example: ["@api:register(name={{n}})", "@api:count_users(email={{e}})"]
198
+ * → ["register", "count_users"]
199
+ */
200
+ export function extractApiCallOrder(tags: string[]): string[] {
201
+ return tags
202
+ .filter((t) => t.startsWith('@api:'))
203
+ .map((t) => {
204
+ const body = t.slice('@api:'.length);
205
+ const parenIdx = body.indexOf('(');
206
+ return parenIdx >= 0 ? body.slice(0, parenIdx) : body;
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Extract the @cases:<dataset> name from tags (returns the first one found, or null).
212
+ * Used to label the "cases" annotation so the detail sheet can note which scenarios
213
+ * exercise a given endpoint with a matrix of input/status pairs.
214
+ */
215
+ export function extractCasesDataset(tags: string[]): string | null {
216
+ const tag = tags.find((t) => t.startsWith('@cases:'));
217
+ return tag ? tag.slice('@cases:'.length) : null;
218
+ }
219
+
220
+ /**
221
+ * Extract the concurrency invariant text from @concurrent:<N> and @query:<oracle> tags.
222
+ * N is the number of parallel fires; the invariant the band proves is exactly-one success
223
+ * (regardless of N), cross-checked by the @query DB oracle. Returns e.g.
224
+ * "2× parallel → exactly-one; @query user_count", or '' when absent.
225
+ */
226
+ export function extractConcurrencyInvariant(tags: string[]): string {
227
+ const concurrentTag = tags.find((t) => t.startsWith('@concurrent:'));
228
+ if (!concurrentTag) return '';
229
+ const n = concurrentTag.slice('@concurrent:'.length);
230
+ const queryTag = tags.find((t) => t.startsWith('@query:'));
231
+ const oracle = queryTag ? queryTag.slice('@query:'.length).split('(')[0] : '';
232
+ const parts = [`${n}× parallel → exactly-one`];
233
+ if (oracle) parts.push(`@query ${oracle}`);
234
+ return parts.join('; ');
235
+ }
236
+
180
237
  /**
181
238
  * Generate TC ID, namespaced by screen/flow so it is globally unique across the
182
239
  * whole project. This matters because the dashboard tracks each test case by its
@@ -128,3 +128,41 @@ export interface EnvironmentInfo {
128
128
  projectName: string;
129
129
  executor: string;
130
130
  }
131
+
132
+ /**
133
+ * One row in the "API detail" worksheet — one row per catalog endpoint.
134
+ * Populated from the apis.yaml catalog + scenario annotations (@cases, @api, @concurrent).
135
+ * Only emitted for api-kind units; never touches the BM-2-901-13 Testcases sheet.
136
+ */
137
+ export interface ApiDetailRow {
138
+ /** Endpoint path from catalog (e.g. /register, /users/count?email=:email) */
139
+ endpoint: string;
140
+ /** HTTP method (GET, POST, …) */
141
+ method: string;
142
+ /** Auth / datasource string composed from catalog datasource + any @auth tag */
143
+ authDatasource: string;
144
+ /** Request shape: body fields / params / encoding from the catalog entry */
145
+ requestShape: string;
146
+ /** Expected-status matrix: the catalog expect.status plus a pointer to any @cases dataset
147
+ * that drives this endpoint, e.g. "201; @cases:register_cases". */
148
+ expectedStatusMatrix: string;
149
+ /** Ordered @api:<name> call sequence for flow scenarios referencing this endpoint */
150
+ flowSteps: string;
151
+ /** Concurrency invariant for @concurrent scenarios, e.g. "2× parallel → exactly-one; @query <oracle>" */
152
+ concurrencyInvariant: string;
153
+ }
154
+
155
+ /**
156
+ * Catalog entry as parsed from apis.yaml.
157
+ * All fields are typed loosely (unknown) because the yaml structure may vary — callers
158
+ * must guard before use.
159
+ */
160
+ export interface ApiCatalogEntry {
161
+ method?: string;
162
+ path?: string;
163
+ datasource?: string;
164
+ description?: string;
165
+ body?: unknown;
166
+ params?: unknown;
167
+ expect?: { status?: number | string };
168
+ }
@@ -13,10 +13,15 @@ import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import ExcelJS from 'exceljs';
15
15
  import JSZip from 'jszip';
16
- import { ScreenSummary, TestCaseRow } from './types';
16
+ import { ApiCatalogEntry, ApiDetailRow, ScreenSummary, TestCaseRow } from './types';
17
17
  import { getPackageVersion } from './package-info';
18
18
  import { SUN_LOGO_PNG_BASE64 } from './sun-logo';
19
19
  import { deliverableBasename } from './csv-exporter';
20
+ import {
21
+ extractApiCallOrder,
22
+ extractCasesDataset,
23
+ extractConcurrencyInvariant,
24
+ } from './feature-parser';
20
25
 
21
26
  const COL_COUNT = 16;
22
27
  const HEADER_FILL = 'FFD9D2E9'; // lavender — matches the summary-header band on row 6
@@ -37,15 +42,31 @@ function applyBorder(cell: AnyCell): void {
37
42
  };
38
43
  }
39
44
 
45
+ /**
46
+ * Optional context for the supplementary "API detail" worksheet.
47
+ * Passed only when the unit is kind:api. When omitted, only the standard
48
+ * Testcases sheet is emitted (non-api delivery stays byte-identical).
49
+ */
50
+ export interface ApiDetailContext {
51
+ /** Parsed apis.yaml catalog keyed by endpoint name */
52
+ catalog: Record<string, ApiCatalogEntry>;
53
+ /** Pre-built detail rows (one per catalog endpoint) */
54
+ rows: ApiDetailRow[];
55
+ }
56
+
40
57
  export function renderXlsx(
41
58
  summary: ScreenSummary,
42
59
  rows: TestCaseRow[],
43
- specLink: string
60
+ specLink: string,
61
+ apiDetail?: ApiDetailContext,
44
62
  ): ExcelJS.Workbook {
45
63
  const wb = new ExcelJS.Workbook();
46
64
  wb.creator = 'sungen delivery';
47
65
  wb.created = new Date();
48
66
  addTestcaseSheet(wb, 'Testcases', summary, rows, specLink);
67
+ if (apiDetail) {
68
+ addApiDetailSheet(wb, apiDetail.rows);
69
+ }
49
70
  return wb;
50
71
  }
51
72
 
@@ -410,6 +431,159 @@ function addTestcaseSheet(
410
431
  };
411
432
  }
412
433
 
434
+ // ---------------------------------------------------------------------------
435
+ // API detail sheet (api-kind units only)
436
+ // Second worksheet appended after Testcases — never alters the Testcases sheet.
437
+ // ---------------------------------------------------------------------------
438
+
439
+ const API_DETAIL_HEADER_FILL = 'FF2E5984'; // dark blue header for differentiation
440
+ const API_DETAIL_HEADER_FONT = 'FFFFFFFF'; // white text on dark header
441
+
442
+ /**
443
+ * Build ApiDetailRow[] from the apis.yaml catalog + feature-level annotations.
444
+ * Called once per feature file for api-kind units in the delivery pipeline.
445
+ *
446
+ * @param catalog Parsed apis.yaml keyed by endpoint name
447
+ * @param scenarios Scenario-level tag arrays from parseFeatureMetadata().scenarios
448
+ */
449
+ export function buildApiDetailRows(
450
+ catalog: Record<string, ApiCatalogEntry>,
451
+ scenarios: Array<{ tags: string[] }>,
452
+ ): ApiDetailRow[] {
453
+ const rows: ApiDetailRow[] = [];
454
+
455
+ for (const [endpointName, entry] of Object.entries(catalog)) {
456
+ const method = (entry.method ?? '').toUpperCase();
457
+ const endpoint = entry.path ?? endpointName;
458
+ const datasource = entry.datasource ?? '';
459
+
460
+ // Auth: look for @auth: tag in any scenario that calls this endpoint.
461
+ const authTags = scenarios.flatMap((s) => {
462
+ const calls = extractApiCallOrder(s.tags);
463
+ if (!calls.includes(endpointName)) return [];
464
+ return s.tags.filter((t) => t.startsWith('@auth:'));
465
+ });
466
+ const uniqueAuth = [...new Set(authTags.map((t) => t.slice('@auth:'.length)))];
467
+ const authDatasource = [datasource, ...uniqueAuth].filter(Boolean).join('; ');
468
+
469
+ // Request shape: compose from body + params + encoding.
470
+ const bodyStr = entry.body
471
+ ? `body: ${typeof entry.body === 'string' ? entry.body : JSON.stringify(entry.body)}`
472
+ : '';
473
+ const paramsArr = Array.isArray(entry.params) ? entry.params as string[] : [];
474
+ const paramsStr = paramsArr.length > 0 ? `params: [${paramsArr.join(', ')}]` : '';
475
+ const requestShape = [bodyStr, paramsStr].filter(Boolean).join('; ') || '—';
476
+
477
+ // Expected-status matrix: aggregate @cases dataset labels + expected status
478
+ // from scenarios that call this endpoint. Fall back to catalog expect.status.
479
+ const statusEntries: string[] = [];
480
+ for (const sc of scenarios) {
481
+ const calls = extractApiCallOrder(sc.tags);
482
+ if (!calls.includes(endpointName)) continue;
483
+ const dataset = extractCasesDataset(sc.tags);
484
+ if (dataset) {
485
+ // @cases dataset name as label — actual per-row statuses live in test-data.yaml
486
+ statusEntries.push(`@cases:${dataset}`);
487
+ }
488
+ }
489
+ // Show the catalog baseline status plus a pointer to any @cases matrix dataset (the per-row
490
+ // statuses live in test-data; resolving them into this cell is a later enrichment).
491
+ const catalogStatus = entry.expect?.status != null ? String(entry.expect.status) : '';
492
+ const expectedStatusMatrix =
493
+ [catalogStatus, ...new Set(statusEntries)].filter(Boolean).join('; ') || '—';
494
+
495
+ // Flow steps: ordered @api names from flow-tagged scenarios referencing this endpoint.
496
+ const flowStepsSet = new Set<string>();
497
+ for (const sc of scenarios) {
498
+ const calls = extractApiCallOrder(sc.tags);
499
+ if (!calls.includes(endpointName)) continue;
500
+ // All scenarios show their call order; flow scenarios show multi-step chains.
501
+ if (calls.length > 1) {
502
+ flowStepsSet.add(calls.join(' → '));
503
+ }
504
+ }
505
+ const flowSteps = [...flowStepsSet].join('; ') || '—';
506
+
507
+ // Concurrency invariant: from @concurrent scenarios calling this endpoint.
508
+ const concurrencyParts: string[] = [];
509
+ for (const sc of scenarios) {
510
+ const calls = extractApiCallOrder(sc.tags);
511
+ if (!calls.includes(endpointName)) continue;
512
+ const inv = extractConcurrencyInvariant(sc.tags);
513
+ if (inv) concurrencyParts.push(inv);
514
+ }
515
+ const concurrencyInvariant = concurrencyParts.join('; ') || '—';
516
+
517
+ rows.push({
518
+ endpoint,
519
+ method,
520
+ authDatasource,
521
+ requestShape,
522
+ expectedStatusMatrix,
523
+ flowSteps,
524
+ concurrencyInvariant,
525
+ });
526
+ }
527
+
528
+ return rows;
529
+ }
530
+
531
+ /**
532
+ * Append a second "API detail" worksheet to the workbook.
533
+ * Called only for api-kind units; no effect on the Testcases sheet or other sheets.
534
+ *
535
+ * Columns: Endpoint · Method · Auth/Datasource · Request shape ·
536
+ * Expected-status matrix · Flow steps · Concurrency invariant
537
+ */
538
+ export function addApiDetailSheet(wb: ExcelJS.Workbook, detailRows: ApiDetailRow[]): void {
539
+ const ws = wb.addWorksheet('API detail');
540
+ const ARIAL_FONT = 'Arial';
541
+
542
+ ws.columns = [
543
+ { header: 'Endpoint', width: 35 },
544
+ { header: 'Method', width: 10 },
545
+ { header: 'Auth / Datasource', width: 22 },
546
+ { header: 'Request shape', width: 40 },
547
+ { header: 'Expected-status matrix', width: 30 },
548
+ { header: 'Flow steps', width: 40 },
549
+ { header: 'Concurrency invariant', width: 35 },
550
+ ];
551
+
552
+ // Style the auto-generated header row (row 1).
553
+ const headerRow = ws.getRow(1);
554
+ headerRow.height = 30;
555
+ headerRow.eachCell((cell) => {
556
+ cell.font = { bold: true, color: { argb: API_DETAIL_HEADER_FONT }, name: ARIAL_FONT };
557
+ cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: API_DETAIL_HEADER_FILL } };
558
+ cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
559
+ applyBorder(cell);
560
+ });
561
+
562
+ ws.views = [{ state: 'frozen', ySplit: 1 }];
563
+
564
+ for (const r of detailRows) {
565
+ const row = ws.addRow([
566
+ r.endpoint,
567
+ r.method,
568
+ r.authDatasource,
569
+ r.requestShape,
570
+ r.expectedStatusMatrix,
571
+ r.flowSteps,
572
+ r.concurrencyInvariant,
573
+ ]);
574
+ row.alignment = { vertical: 'top', wrapText: true };
575
+ row.eachCell({ includeEmpty: true }, (cell) => {
576
+ applyBorder(cell);
577
+ cell.font = { name: ARIAL_FONT };
578
+ });
579
+ }
580
+
581
+ ws.autoFilter = {
582
+ from: { row: 1, column: 1 },
583
+ to: { row: ws.rowCount, column: 7 },
584
+ };
585
+ }
586
+
413
587
  /**
414
588
  * Write the workbook to `qa/deliverables/<screen>-testcases[.env].xlsx`.
415
589
  * When `SUNGEN_ENV` is set, the env name is appended so locale exports don't
@@ -106,7 +106,10 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
106
106
  const deferredToFlow = tags.includes('@deferred:flow');
107
107
  const ownedByFlow = (tags.find((t: string) => /^@owned-by:/i.test(t)) || '').slice('@owned-by:'.length) || undefined;
108
108
  // @deferred:flow is owned by a flow → not automated on this screen, so it accounts like @manual (H6).
109
- const manual = tags.includes('@manual') || deferredToFlow;
109
+ // Recognize both bare `@manual` and the reason-coded `@manual:Mx` convention (what the generator emits);
110
+ // must match capability-plan.ts's detection, or `@manual:Mx` scenarios stay in the businessDepth
111
+ // denominator and silently suppress the ratio (#386).
112
+ const manual = tags.some((t) => /^@manual\b/i.test(t)) || deferredToFlow;
110
113
  const casesTag = tags.find((t) => t.startsWith('@cases:'));
111
114
  const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
112
115
  // Named-query references: @query:<name>[(overrides)] tags + inline `query [name]` step refs.
@@ -74,6 +74,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
74
74
  ['claude-agent-reviewer.md', '.claude/agents/sungen-reviewer.md'],
75
75
  ['claude-agent-discovery.md', '.claude/agents/sungen-discovery.md'],
76
76
  ['claude-agent-challenge.md', '.claude/agents/sungen-challenge.md'],
77
+ ['claude-agent-generator.md', '.claude/agents/sungen-generator.md'],
77
78
 
78
79
  // Skills — GitHub Copilot
79
80
  ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: sungen-generator
3
+ description: Generates Gherkin scenarios for ONE shard (a viewpoint theme or a spec section) in an isolated context and writes a self-contained fragment — so create-test can fan out many generators in parallel and the orchestrator stays lean. Each shard owns a disjoint VP-prefix namespace, so fragments merge without renumbering. Invoked by create-test/design during parallel generation.
4
+ tools: Read, Grep, Glob, Bash, Write, Edit, Skill
5
+ ---
6
+
7
+ You are a **single-shard test-case generator**. You run in an **isolated context** and produce the scenarios for **exactly one shard** — never the whole screen. The orchestrator runs several of you in parallel, then merges the fragments. Keeping each fragment small is also what keeps every generator under the output-token cap.
8
+
9
+ ## What a shard is
10
+ A shard is one **coverage unit**, sized for real parallelism (not the 5 coarse viewpoint-router groups — a screen loads only 1–2 of those). It is **one of**:
11
+ - a **viewpoint theme** — a `VP-` prefix from the viewpoint overview (e.g. `VP-SEC`, `VP-ERROR-EMPTY-STATE`, `VP-CAROUSEL`), or
12
+ - a **spec section** — one `spec.md` section per the `sungen-tc-generation` Mapping Contract (Table 1).
13
+
14
+ Your shard owns its `VP-` prefix, so your ids never collide with sibling shards.
15
+
16
+ ## Inputs (passed by the orchestrator)
17
+ - **Your shard**: the theme/section name + its viewpoint items (the slice).
18
+ - **The `sungen-discovery` report** (Step 3): condensed facts — use it instead of re-reading every source.
19
+ - **Relevant context**: only the `spec.md` section(s) your shard maps to, and **which** `sungen-viewpoint` group file holds your shard's patterns (load only that one).
20
+ - **Unit context**: screen vs flow, the unit name, the chosen tier (1 / 2 / 3 / full), and your fragment paths.
21
+
22
+ ## Generate (your shard ONLY)
23
+ 1. Load **only** the skills you need: `sungen-tc-generation` (output format + mapping), `sungen-gherkin-syntax` (step patterns), and the **one** `sungen-viewpoint` group file your shard belongs to. Do not load the others.
24
+ 2. Produce the scenarios for your shard's viewpoint items at the requested tier, following the skill's mapping contract. Keep every `VP-` id under **your shard's prefix** so it stays in a disjoint namespace.
25
+ 3. **Flows**: use `[Screen:Element]` namespace refs, namespace test-data by phase, add the `@flow` tag per the skill.
26
+ 4. Tag `@manual:Mx` (with a reason) only for true judgment / missing-capability items, per the skill.
27
+
28
+ ## Write your fragment (do NOT write the final feature)
29
+ Write two self-contained fragment files (the orchestrator merges them):
30
+ - `.sungen/fragments/<unit>/<shard>.feature` — a **headerless** block: just your `@tag`-decorated `Scenario:` / `Scenario Outline:` blocks, no `Feature:` line (the orchestrator owns the single Feature header).
31
+ - `.sungen/fragments/<unit>/<shard>.test-data.yaml` — only the `{{variables}}` your scenarios introduce.
32
+
33
+ Distinct paths per shard ⇒ no write conflict with sibling generators.
34
+
35
+ ## Return (compact — your only message back)
36
+ ```
37
+ SHARD: <theme-or-section>
38
+ SCENARIOS: <n> (VP ids: <VP-...-001..NNN>)
39
+ TEST-DATA KEYS: <keys you added>
40
+ SPEC SECTIONS COVERED: <list>
41
+ ASSUMPTIONS / DEFERRED: <items you marked @manual or could not source>
42
+ FRAGMENT: .sungen/fragments/<unit>/<shard>.feature
43
+ ```
44
+ Keep it tight. Do not audit, do not merge, do not touch other shards' fragments or the final `.feature`.
@@ -71,12 +71,29 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
71
71
  Summarize what you found in requirements and present to the user.
72
72
 
73
73
  4. Follow the `sungen-tc-generation` skill for section identification, viewpoint generation, and output format. **Viewpoint loading discipline:** `sungen-viewpoint` is a **router** — from the page-type (form / list / detail / auth / dashboard …) read **only the matching group file(s)** (e.g. a login screen → group-e-identity; a product list → group-c-data-explore), never all five groups. This keeps the generation context lean. **For flows**, use the "Flow Test Generation" section in the skill. When requirements exist, use the "Requirements-Driven Generation" strategy. **For Tier 1**, apply the **Lightweight Guard** — verify required fields, validation rules, business rules, security checks, and key state transitions all have TCs after generation. **For Tier 2+**, **MUST** apply the full **Mapping Contract** — walk every `spec.md` section top-to-bottom and produce the indicated TCs per Table 1; handle `test-viewpoint.md` per Table 2. Do not silently skip sections.
74
- 5. Generate or update `.feature` + `test-data.yaml` following `sungen-gherkin-syntax` and `sungen-tc-generation` skills. **For flows**: use `[Screen:Element]` namespace format, namespace test-data by phase, add `@flow` tag.
74
+ 5. Generate `.feature` + `test-data.yaml` following `sungen-gherkin-syntax` and `sungen-tc-generation`. **Partition the work into shards and generate them in parallel** when there are ≥2.
75
+
76
+ **5a. Decide the shards.** A shard is one **coverage unit** sized for parallelism — NOT the 5 coarse viewpoint-router groups (a screen loads only 1–2 of those). Use **either**:
77
+ - one **viewpoint theme** per shard — a `VP-` prefix from the viewpoint overview (`VP-SEC`, `VP-ERROR-EMPTY-STATE`, `VP-CAROUSEL`, …) — preferred when the viewpoint overview is rich (test-2/home had 47 items across many themes); **or**
78
+ - one **`spec.md` section** per shard (the Mapping Contract walk, Table 1) — preferred when generating from spec.
79
+ Each shard owns a disjoint `VP-` prefix ⇒ ids never collide. One shard → skip to 5c (no fan-out gain).
80
+
81
+ **5b. Parallel fan-out (Claude Code).** Spawn one **`sungen-generator`** sub-agent **per shard** (Task tool, `subagent_type: sungen-generator`) — issue all the Task calls **in a single message** so they run concurrently. Pass each: its shard (theme/section) + viewpoint slice, the **`sungen-discovery` report** (Step 3), only the `spec.md` section(s) it maps to, which one `sungen-viewpoint` group file holds its patterns, the unit (screen/flow) + name + tier, and its fragment paths `.sungen/fragments/<name>/<shard>.{feature,test-data.yaml}`. Each writes a **headerless** fragment + a test-data fragment and returns a compact summary. Small fragments also keep every generator under the output-token cap (the reason the single-pass path writes incrementally).
82
+
83
+ **5c. Merge (orchestrator — barrier; only after all generators return).**
84
+ - Write the final `qa/<screens|flows>/<name>/features/<name>.feature`: one `Feature:` header (+ `@flow` for flows), then concatenate the fragments in **stable order** — spec-section order top-to-bottom (or theme order from the viewpoint overview) — so output is coherent and reproducible across runs.
85
+ - **Dedup** cross-shard scenarios with near-identical titles (a generic "navigation works" from two shards): keep the earlier shard's, drop the duplicate, note it. No id renumber needed — prefixes are disjoint by construction.
86
+ - **Union** the test-data fragments into `test-data.yaml`; dedup keys, and **flag** any key two shards define with different values.
87
+ - Delete `.sungen/fragments/<name>/` once merged.
88
+ - Guarantees a **coherent** suite (no dup, valid ids, passes `audit`), not a byte-identical one — generation is AI-authored; the determinism asset lives downstream in the Gherkin→`.spec.ts` compiler.
89
+
90
+ **5d. Sequential fallback.** Use the single-context incremental path (Step 2: tier-by-tier `Write`/`Edit` batches) when: only **one** shard applies, **Copilot / no sub-agents**, or a constrained setup. Same output, just no speedup. **For flows**: `[Screen:Element]` namespace refs, test-data namespaced by phase, `@flow` tag.
75
91
 
76
92
  5.5. **Quality gate & repair (harness — always run, do NOT skip).** Follow the `sungen-harness-audit` skill:
77
93
  - Run `sungen audit --screen <name>` (Bash) and read `gateStatus` + `findings` (deterministic, structural).
78
94
  - **Independent semantic review.** **Claude Code:** spawn the **`sungen-reviewer`** sub-agent (Task tool, `subagent_type: sungen-reviewer`) — it judges what the gate can't (does each scenario's steps PROVE its title/viewpoint, observable Thens, business-critical assertion depth) and returns `VERDICT` + `ISSUES` with concrete fixes. **Merge its NEEDS-REPAIR issues with the audit findings.** (Copilot / no sub-agents: run the same review inline using the `sungen-reviewer` criteria.)
79
95
  - Repair **both** the audit findings and the reviewer issues (budget 3 rounds), then re-audit:
96
+ - **Repair runs single-agent by default** (it edits the one `.feature` — concurrent edits to the same file conflict, and BALANCE/dedup needs whole-suite context). **Exception:** a finding that is purely **additive new coverage** (GATE missing-theme → generate scenarios for an uncovered theme) is just more shards — fan it out as `sungen-generator` sub-agent(s) (new disjoint `VP-` prefix) and merge, exactly like Step 5b. Findings that **edit existing** scenarios (DEPTH/BALANCE/TRACE) stay serial.
80
97
  - If the gate FAILs or there are findings, **repair** (budget 3 rounds), then re-audit:
81
98
  - **GATE** missing critical theme → generate scenarios for it. If it is **cross-screen** (cart-correctness, product-detail-consistency, filter-result-correctness): **automate it in the flow** (`/sungen:add-flow` if none exists) with observable data assertions (`... with {{value}}`, `see all ... contain {{v}}`) — a single home→target journey runs as one Playwright test. Do **not** write a full `@manual` duplicate of it on the screen (that is a non-running dead copy — `sungen audit` flags it `MANUAL-AUTOMATABLE`), and do **not** fake a shallow single-screen pass. Reserve `@manual` for true judgment / missing-capability, tagged `@manual:Mx`.
82
99
  - **DEPTH** → replace `see [X] page/section` on business-critical scenarios with data assertions.
@@ -88,6 +88,33 @@ Multi-locale (no `SUNGEN_ENV`): one **`<LOCALE> Auto`** sheet per locale + a sin
88
88
 
89
89
  ---
90
90
 
91
+ ## API delivery — extra worksheet
92
+
93
+ For **api-kind units** (`qa/api/<area>/`), the `.xlsx` gains a third worksheet **`API detail`** (appended after Auto/Manual). The main BM-2-901-13 Testcases layout is unchanged. The CSV is unchanged (16-column, no extra sheet).
94
+
95
+ ### Required sources (API detail sheet only)
96
+
97
+ | Source | Path | Created by |
98
+ |--------|------|------------|
99
+ | Endpoint catalog | `qa/api/<area>/api/apis.yaml` | `sungen add --api` or `sungen api import` |
100
+ | Scenario annotations | `qa/api/<area>/features/<feature>.feature` | `create-test` |
101
+
102
+ ### API detail column mapping
103
+
104
+ | Column | Source |
105
+ |--------|--------|
106
+ | Endpoint | `path` from `apis.yaml` catalog entry |
107
+ | Method | `method` from catalog entry (uppercased) |
108
+ | Auth / Datasource | catalog `datasource` + any `@auth:<role>` tag from scenarios calling this endpoint |
109
+ | Request shape | catalog `body` + `params` fields composed as `body: {…}; params: [a, b]` |
110
+ | Expected-status matrix | `@cases:<dataset>` label for data-driven scenarios; catalog `expect.status` as fallback |
111
+ | Flow steps | Ordered `@api:<name>` call chain from multi-call scenarios (e.g. `register → count_users`) |
112
+ | Concurrency invariant | `@concurrent:<N>` + `@query:<oracle>` from concurrent scenarios (e.g. `ok_count=2; @query user_count`) |
113
+
114
+ **Sources are catalog + annotations only** — Field Metadata (FM) is not required for this sheet.
115
+
116
+ ---
117
+
91
118
  ## Excluded from CSV
92
119
 
93
120
  - `@steps:<name>` **base** scenarios — these are setup-only, inlined into `@extend:...` scenarios at compile time
@@ -214,6 +214,8 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
214
214
  | `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
215
215
  | `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
216
216
  | `@api:name` | API: run the named request from `api/apis.yaml` (precondition) and bind the response to `{{name}}`; assert with `expect {{name.status}} …` + path access (`{{name.body.<path>}}`). Override params `@api:name(p={{v}})`. Repeatable. (Optional API Driver) |
217
+ | `@concurrent:N` | API idempotency: fire the bound `@api` request N times in parallel, then bind aggregates on the `@api` name — `{{name.ok_count}}` (2xx count) and `{{name.status_counts}}` (status→count map). Assert the exactly-once invariant (`expect {{name.ok_count}} is 1`); pair with `@query` as a DB oracle. Tag order = run order: `@api` (mutate) before `@query` (verify). (Optional API Driver) |
218
+ | `@hybrid` | One unit, two capabilities: a signed-in browser session (UI) authorizes the `@api` call — the API request reuses the UI `storageState`. (Optional API + UI Drivers) |
217
219
 
218
220
  ### Data-driven scenarios (`@cases`)
219
221
 
@@ -9,6 +9,8 @@ user-invocable: false
9
9
  - **Write incrementally — never emit the whole suite in one response.** Build the `.feature` in batches via successive `Write`/`Edit` (≈10–15 scenarios per call). For **Full coverage**, write tier-by-tier: `Write` Tier 1 → `Edit` append Tier 2 → `Edit` append Tier 3.
10
10
  → One huge `Write` can exceed the model's output-token cap → `API Error: Claude's response exceeded the N output token maximum`. Single-pass full coverage only fits when `CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`; otherwise batch. Batching also lets the audit/reviewer run per batch — higher quality.
11
11
 
12
+ - **Sharded (parallel) generation — keep each shard self-contained.** When `create-test` fans out one `sungen-generator` sub-agent per shard (a viewpoint theme like `VP-SEC`, or a `spec.md` section — see create-test Steps 5a–5c), you are generating **only your shard**: emit your scenarios under **your own `VP-` prefix** (disjoint namespace, so ids never collide), as a **headerless fragment** (no `Feature:` line — the orchestrator owns the single header). Do not reference or renumber other shards. The orchestrator concatenates fragments in stable order (spec-section / theme order), dedups by title, and unions test-data. Small fragments also stay under the output-token cap by construction.
13
+
12
14
  - `spec_figma.md` exists → read file only, **NEVER** call `mcp__figma__*`
13
15
  → PAT auth flow already done by `sungen-capture` (mode figma-pat); re-calling fails or duplicates work.
14
16
 
@@ -64,7 +64,8 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
64
64
  Summarize what you found in requirements and present to the user.
65
65
 
66
66
  4. Follow the `sungen-tc-generation` skill for section identification, viewpoint generation, and output format. **For flows**, use the "Flow Test Generation" section in the skill. When requirements exist, use the "Requirements-Driven Generation" strategy. **For Tier 1**, apply the **Lightweight Guard** — verify required fields, validation rules, business rules, security checks, and key state transitions all have TCs after generation. **For Tier 2+**, **MUST** apply the full **Mapping Contract** — walk every `spec.md` section top-to-bottom and produce the indicated TCs per Table 1; handle `test-viewpoint.md` per Table 2. Do not silently skip sections. Present sections as a numbered list and let user pick.
67
- 5. Generate or update `.feature` + `test-data.yaml` following `sungen-gherkin-syntax` and `sungen-tc-generation` skills. **For flows**: use `[Screen:Element]` namespace format, namespace test-data by phase, add `@flow` tag.
67
+ 5. Generate or update `.feature` + `test-data.yaml` following `sungen-gherkin-syntax` and `sungen-tc-generation` skills. Generate **group-by-group** (one viewpoint group at a time, tier-by-tier `Write`/`Edit` batches) to stay under the output-token cap. **For flows**: use `[Screen:Element]` namespace format, namespace test-data by phase, add `@flow` tag.
68
+ > **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.
68
69
  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.
69
70
  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.
70
71
  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:
@@ -88,6 +88,33 @@ Multi-locale (no `SUNGEN_ENV`): one **`<LOCALE> Auto`** sheet per locale + a sin
88
88
 
89
89
  ---
90
90
 
91
+ ## API delivery — extra worksheet
92
+
93
+ For **api-kind units** (`qa/api/<area>/`), the `.xlsx` gains a third worksheet **`API detail`** (appended after Auto/Manual). The main BM-2-901-13 Testcases layout is unchanged. The CSV is unchanged (16-column, no extra sheet).
94
+
95
+ ### Required sources (API detail sheet only)
96
+
97
+ | Source | Path | Created by |
98
+ |--------|------|------------|
99
+ | Endpoint catalog | `qa/api/<area>/api/apis.yaml` | `sungen add --api` or `sungen api import` |
100
+ | Scenario annotations | `qa/api/<area>/features/<feature>.feature` | `create-test` |
101
+
102
+ ### API detail column mapping
103
+
104
+ | Column | Source |
105
+ |--------|--------|
106
+ | Endpoint | `path` from `apis.yaml` catalog entry |
107
+ | Method | `method` from catalog entry (uppercased) |
108
+ | Auth / Datasource | catalog `datasource` + any `@auth:<role>` tag from scenarios calling this endpoint |
109
+ | Request shape | catalog `body` + `params` fields composed as `body: {…}; params: [a, b]` |
110
+ | Expected-status matrix | `@cases:<dataset>` label for data-driven scenarios; catalog `expect.status` as fallback |
111
+ | Flow steps | Ordered `@api:<name>` call chain from multi-call scenarios (e.g. `register → count_users`) |
112
+ | Concurrency invariant | `@concurrent:<N>` + `@query:<oracle>` from concurrent scenarios (e.g. `ok_count=2; @query user_count`) |
113
+
114
+ **Sources are catalog + annotations only** — Field Metadata (FM) is not required for this sheet.
115
+
116
+ ---
117
+
91
118
  ## Excluded from CSV
92
119
 
93
120
  - `@steps:<name>` **base** scenarios — these are setup-only, inlined into `@extend:...` scenarios at compile time
@@ -214,6 +214,8 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
214
214
  | `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
215
215
  | `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
216
216
  | `@api:name` | API: run the named request from `api/apis.yaml` (precondition) and bind the response to `{{name}}`; assert with `expect {{name.status}} …` + path access (`{{name.body.<path>}}`). Override params `@api:name(p={{v}})`. Repeatable. (Optional API Driver) |
217
+ | `@concurrent:N` | API idempotency: fire the bound `@api` request N times in parallel, then bind aggregates on the `@api` name — `{{name.ok_count}}` (2xx count) and `{{name.status_counts}}` (status→count map). Assert the exactly-once invariant (`expect {{name.ok_count}} is 1`); pair with `@query` as a DB oracle. Tag order = run order: `@api` (mutate) before `@query` (verify). (Optional API Driver) |
218
+ | `@hybrid` | One unit, two capabilities: a signed-in browser session (UI) authorizes the `@api` call — the API request reuses the UI `storageState`. (Optional API + UI Drivers) |
217
219
 
218
220
  ### Data-driven scenarios (`@cases`)
219
221
 
@@ -9,6 +9,8 @@ user-invocable: false
9
9
  - **Write incrementally — never emit the whole suite in one response.** Build the `.feature` in batches via successive `Write`/`Edit` (≈10–15 scenarios per call). For **Full coverage**, write tier-by-tier: `Write` Tier 1 → `Edit` append Tier 2 → `Edit` append Tier 3.
10
10
  → One huge `Write` can exceed the model's output-token cap → `API Error: Claude's response exceeded the N output token maximum`. Single-pass full coverage only fits when `CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`; otherwise batch. Batching also lets the audit/reviewer run per batch — higher quality.
11
11
 
12
+ - **Generate group-by-group (sequential here).** Copilot has no sub-agents, so generate one viewpoint group/theme at a time, tier-by-tier, keeping each `VP-` theme in its own id prefix. (The Claude Code variant fans these out as parallel `sungen-generator` shards and merges — same output shape, just no speedup. Keep each theme self-contained so it would merge cleanly either way.)
13
+
12
14
  - `spec_figma.md` exists → read file only, **NEVER** call `mcp__figma__*`
13
15
  → PAT auth flow already done by `sungen-capture` (mode figma-pat); re-calling fails or duplicates work.
14
16