@sun-asterisk/sungen 3.2.1-beta.1 → 3.2.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +31 -0
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/depth-lint.d.ts +3 -0
- package/dist/cli/commands/depth-lint.d.ts.map +1 -0
- package/dist/cli/commands/depth-lint.js +88 -0
- package/dist/cli/commands/depth-lint.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +25 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +59 -0
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/types.d.ts +38 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +31 -2
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +144 -1
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/harness/depth-lint.d.ts +25 -0
- package/dist/harness/depth-lint.d.ts.map +1 -0
- package/dist/harness/depth-lint.js +118 -0
- package/dist/harness/depth-lint.js.map +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +4 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +1 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-generator.md +44 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +27 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +3 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +3 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +27 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -0
- package/dist/orchestrator/templates/specs-api.d.ts +7 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +13 -2
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +13 -2
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +32 -2
- package/src/cli/commands/depth-lint.ts +51 -0
- package/src/cli/index.ts +2 -0
- package/src/exporters/feature-parser.ts +57 -0
- package/src/exporters/types.ts +38 -0
- package/src/exporters/xlsx-exporter.ts +176 -2
- package/src/harness/depth-lint.ts +122 -0
- package/src/harness/parse.ts +4 -1
- package/src/orchestrator/ai-rules-updater.ts +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-generator.md +44 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +27 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +3 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +3 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +27 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -0
- package/src/orchestrator/templates/specs-api.ts +13 -2
|
@@ -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
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depth lint (issue #384) — a deterministic, generation-time depth self-check.
|
|
3
|
+
*
|
|
4
|
+
* The audit's `assertionDepth` sensor decides WHICH business-critical scenarios are shallow
|
|
5
|
+
* (the authoritative set). This lint adds the missing half: for each shallow business-critical
|
|
6
|
+
* scenario it classifies the *fix* using the catalog's per-theme `depth` metadata —
|
|
7
|
+
* • cross_screen:false → DEEPEN in place (emit the theme's `depth.template` value assertion)
|
|
8
|
+
* • cross_screen:true → DEFER (flow-own, or @manual:Mx with a reason) — leaves the depth denominator
|
|
9
|
+
* so a generator (or the create-test repair step) can act mechanically BEFORE the first audit,
|
|
10
|
+
* instead of churning the 3-round repair budget on scenarios that can't be deepened on-screen.
|
|
11
|
+
*
|
|
12
|
+
* Reuses the audit plumbing verbatim (parse + catalog + assertionDepth) → same verdict as `sungen audit`.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
17
|
+
import { loadCatalog, viewpointGate, assertionDepth, dataThemesFor, CatalogTheme } from './sensors';
|
|
18
|
+
|
|
19
|
+
export type DepthAction = 'deepen' | 'defer';
|
|
20
|
+
|
|
21
|
+
export interface DepthLintItem {
|
|
22
|
+
scenario: string;
|
|
23
|
+
theme: string | null;
|
|
24
|
+
action: DepthAction;
|
|
25
|
+
/** the exact deep step to emit (deepen) or the routing hint (defer) */
|
|
26
|
+
fix: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DepthLintReport {
|
|
30
|
+
screen: string;
|
|
31
|
+
pageType: string | null;
|
|
32
|
+
focus: string;
|
|
33
|
+
threshold: number;
|
|
34
|
+
bcDepthRatio: number;
|
|
35
|
+
verdict: 'pass' | 'warn' | 'fail';
|
|
36
|
+
businessCriticalTotal: number;
|
|
37
|
+
shallowTotal: number;
|
|
38
|
+
/** shallow business-critical scenarios that CAN be deepened on-screen (actionable now) */
|
|
39
|
+
deepen: DepthLintItem[];
|
|
40
|
+
/** shallow business-critical scenarios that are cross-screen → route to a flow / @manual */
|
|
41
|
+
defer: DepthLintItem[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Find the data-theme a scenario belongs to (precise depth.keywords, fallback theme.keywords). */
|
|
45
|
+
function matchTheme(s: ScenarioInfo, dataThemes: CatalogTheme[]): CatalogTheme | undefined {
|
|
46
|
+
return dataThemes.find((t) => {
|
|
47
|
+
const kws = t.depth?.keywords?.length ? t.depth.keywords : t.keywords;
|
|
48
|
+
return kws.some((k) => s.haystack.includes(k.toLowerCase()));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function runDepthLint(screenDir: string, screenName: string, focus = 'functional'): DepthLintReport {
|
|
53
|
+
const last = screenName.split('/').pop() || screenName;
|
|
54
|
+
const featurePath = path.join(screenDir, 'features', `${last}.feature`);
|
|
55
|
+
const viewpointPath = path.join(screenDir, 'requirements', 'test-viewpoint.md');
|
|
56
|
+
|
|
57
|
+
const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
|
|
58
|
+
const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
|
|
59
|
+
const catalog = loadCatalog();
|
|
60
|
+
const gate = viewpointGate(scenarios, viewpoints, catalog);
|
|
61
|
+
const dataThemes = dataThemesFor(catalog, gate.pageType);
|
|
62
|
+
const depth = assertionDepth(scenarios, dataThemes, focus);
|
|
63
|
+
|
|
64
|
+
const byName = new Map(scenarios.map((s) => [s.name, s]));
|
|
65
|
+
const deepen: DepthLintItem[] = [];
|
|
66
|
+
const defer: DepthLintItem[] = [];
|
|
67
|
+
|
|
68
|
+
for (const sb of depth.shallowBusinessCritical) {
|
|
69
|
+
const s = byName.get(sb.name);
|
|
70
|
+
const theme = s ? matchTheme(s, dataThemes) : undefined;
|
|
71
|
+
const crossScreen = theme?.depth?.cross_screen ?? false;
|
|
72
|
+
if (crossScreen) {
|
|
73
|
+
defer.push({
|
|
74
|
+
scenario: sb.name,
|
|
75
|
+
theme: theme?.theme ?? null,
|
|
76
|
+
action: 'defer',
|
|
77
|
+
fix: `cross-screen — own it in a flow (sungen add-flow) or tag @manual:Mx with a reason; do not fake an on-screen data assertion`,
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
deepen.push({
|
|
81
|
+
scenario: sb.name,
|
|
82
|
+
theme: theme?.theme ?? null,
|
|
83
|
+
action: 'deepen',
|
|
84
|
+
fix: theme?.depth?.template ?? `add a data assertion (\`... with {{value}}\` or \`see all ... contain {{v}}\`)`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
screen: screenName,
|
|
91
|
+
pageType: gate.pageType,
|
|
92
|
+
focus,
|
|
93
|
+
threshold: depth.threshold,
|
|
94
|
+
bcDepthRatio: depth.bcDepthRatio,
|
|
95
|
+
verdict: depth.verdict,
|
|
96
|
+
businessCriticalTotal: depth.businessCriticalTotal,
|
|
97
|
+
shallowTotal: depth.businessCriticalShallow,
|
|
98
|
+
deepen,
|
|
99
|
+
defer,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function renderDepthLint(r: DepthLintReport): void {
|
|
104
|
+
const pct = (n: number) => `${Math.round(n * 100)}%`;
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(`━━━ Depth lint: ${r.screen} (page-type ${r.pageType ?? 'unknown'}) ━━━`);
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(` businessDepth ${pct(r.bcDepthRatio)} (threshold ${pct(r.threshold)} · focus ${r.focus}) → ${r.verdict.toUpperCase()}`);
|
|
109
|
+
console.log(` ${r.businessCriticalTotal} business-critical · ${r.shallowTotal} shallow → ${r.deepen.length} deepen-in-place · ${r.defer.length} cross-screen`);
|
|
110
|
+
if (r.deepen.length) {
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(' ── DEEPEN IN PLACE (fix before audit) ──');
|
|
113
|
+
for (const d of r.deepen) console.log(` • ${d.scenario}\n [${d.theme}] → ${d.fix}`);
|
|
114
|
+
}
|
|
115
|
+
if (r.defer.length) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(' ── CROSS-SCREEN (route to flow / @manual:Mx) ──');
|
|
118
|
+
for (const d of r.defer) console.log(` • ${d.scenario} [${d.theme}]`);
|
|
119
|
+
}
|
|
120
|
+
if (!r.deepen.length && !r.defer.length) console.log(' ✓ no shallow business-critical scenarios');
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
package/src/harness/parse.ts
CHANGED
|
@@ -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
|
-
|
|
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,34 @@ 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
|
|
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.
|
|
91
|
+
|
|
92
|
+
5.4. **Depth self-check (deterministic — run BEFORE the audit).** Run `sungen depth-lint --screen <name>` (Bash). It reuses the audit's businessDepth classifier and splits every shallow business-critical scenario into two actionable buckets — act on them now so the audit/repair loop doesn't burn rounds on depth:
|
|
93
|
+
- **DEEPEN IN PLACE** — add a real value assertion to each (`User see all [X] contain {{v}}`, `User remember [X] as {{v}}` + `… with {{v}}`). The printed `template` is a **hint** keyed off the theme — apply judgment to the scenario's actual claim; do NOT paste a value assertion that doesn't fit (e.g. a carousel-visibility scenario should assert the product SET, not a price). If a flagged scenario is genuinely visibility/behavior (not data-correctness), that's an over-count — leave it and note it, never fake an assertion.
|
|
94
|
+
- **CROSS-SCREEN** — route to a flow (`/sungen:add-flow`) or tag `@manual:Mx` + reason; do NOT fake an on-screen data assertion. This removes it from the screen's depth denominator honestly.
|
|
95
|
+
Re-run `sungen depth-lint` until `deepen` is empty (or only honest over-counts remain), THEN proceed to the gate. This lifts first-pass `businessDepth` mechanically instead of via 2–3 organic repair rounds.
|
|
75
96
|
|
|
76
97
|
5.5. **Quality gate & repair (harness — always run, do NOT skip).** Follow the `sungen-harness-audit` skill:
|
|
77
98
|
- Run `sungen audit --screen <name>` (Bash) and read `gateStatus` + `findings` (deterministic, structural).
|
|
78
99
|
- **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
100
|
- Repair **both** the audit findings and the reviewer issues (budget 3 rounds), then re-audit:
|
|
101
|
+
- **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
102
|
- If the gate FAILs or there are findings, **repair** (budget 3 rounds), then re-audit:
|
|
81
103
|
- **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
104
|
- **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
|
|
|
@@ -273,6 +275,7 @@ Security: [S1 – admin only]
|
|
|
273
275
|
|
|
274
276
|
**Depth is a GATE dimension (harness-roadmap P1) — self-raise, never silently go shallow:**
|
|
275
277
|
- For every data-correctness theme the catalog marks `depth.requires: data-assertion`, emit its `depth.template` shape by **default** — don't wait for the repair loop. `sungen audit` measures `businessDepth` (ratio of these scenarios that assert data) against an intent threshold (functional ≥ 0.70); below it the **gate FAILs**.
|
|
278
|
+
- **Verify depth deterministically before the gate:** run `sungen depth-lint --screen <name>`. It classifies every shallow business-critical scenario into **deepen-in-place** (add the theme's value assertion — the printed `template` is a hint, fit it to the actual claim) vs **cross-screen** (route to a flow / `@manual:Mx`). Clear the `deepen` list first — this is the mechanical way to hit `businessDepth` on the first pass instead of churning repair rounds. Never fake a value assertion onto a visibility/behavior scenario the lint over-counts; leave it and note the over-count.
|
|
276
279
|
- `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape as an **automated flow scenario** (in the flow — do NOT leave a full-step `@manual` duplicate on the screen). `@manual` is **only** for genuine judgment (M6 visual/UX · M8 not-worth · M9 human) or a missing capability (M1–M5/M7), and it **must** carry a reason code (`@manual:Mx`, or a reason comment the planner can infer). A `@manual` scenario that still has full automatable steps (a data assertion, no visual/mock/a11y judgment) is now flagged by `sungen audit` as `MANUAL-AUTOMATABLE`, and business-critical scenarios you defer to `@manual` are reported as `DEPTH-DEFERRED` (they do NOT silently inflate `businessDepth`). Deferring automatable work to `@manual` lowers quality — automate it in the flow instead.
|
|
277
280
|
- **Pick the right `@manual:Mx` code — it decides which driver can later automate the case** (`sungen audit` flags a code↔reason mismatch). Tag the code that matches the **oracle the reason describes**:
|
|
278
281
|
|
|
@@ -64,7 +64,9 @@ 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.
|
|
69
|
+
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.
|
|
68
70
|
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
71
|
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
72
|
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
|
|
|
@@ -273,6 +275,7 @@ Security: [S1 – admin only]
|
|
|
273
275
|
|
|
274
276
|
**Depth is a GATE dimension (harness-roadmap P1) — self-raise, never silently go shallow:**
|
|
275
277
|
- For every data-correctness theme the catalog marks `depth.requires: data-assertion`, emit its `depth.template` shape by **default** — don't wait for the repair loop. `sungen audit` measures `businessDepth` (ratio of these scenarios that assert data) against an intent threshold (functional ≥ 0.70); below it the **gate FAILs**.
|
|
278
|
+
- **Verify depth deterministically before the gate:** run `sungen depth-lint --screen <name>`. It classifies every shallow business-critical scenario into **deepen-in-place** (add the theme's value assertion — the printed `template` is a hint, fit it to the actual claim) vs **cross-screen** (route to a flow / `@manual:Mx`). Clear the `deepen` list first — this is the mechanical way to hit `businessDepth` on the first pass instead of churning repair rounds. Never fake a value assertion onto a visibility/behavior scenario the lint over-counts; leave it and note the over-count.
|
|
276
279
|
- `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape as an **automated flow scenario** (in the flow — do NOT leave a full-step `@manual` duplicate on the screen). `@manual` is **only** for genuine judgment (M6 visual/UX · M8 not-worth · M9 human) or a missing capability (M1–M5/M7), and it **must** carry a reason code (`@manual:Mx`, or a reason comment the planner can infer). A `@manual` scenario that still has full automatable steps (a data assertion, no visual/mock/a11y judgment) is now flagged by `sungen audit` as `MANUAL-AUTOMATABLE`, and business-critical scenarios you defer to `@manual` are reported as `DEPTH-DEFERRED` (they do NOT silently inflate `businessDepth`). Deferring automatable work to `@manual` lowers quality — automate it in the flow instead.
|
|
277
280
|
- **Pick the right `@manual:Mx` code — it decides which driver can later automate the case** (`sungen audit` flags a code↔reason mismatch). Tag the code that matches the **oracle the reason describes**:
|
|
278
281
|
|
|
@@ -49,6 +49,17 @@ function substitute(text: string, params: Record<string, any>): string {
|
|
|
49
49
|
return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Join a datasource base URL with a catalog path. Concatenate rather than rely on Playwright's
|
|
54
|
+
* baseURL resolution: an absolute path (`/user/1`) resolves against the base ORIGIN and would drop
|
|
55
|
+
* a base path component (`/api/v3`). Most APIs are mounted under such a prefix, so the full URL must
|
|
56
|
+
* be built explicitly.
|
|
57
|
+
*/
|
|
58
|
+
export function joinApiUrl(base: string, urlPath: string): string {
|
|
59
|
+
const b = base.replace(/\/$/, '');
|
|
60
|
+
return urlPath.startsWith('/') ? b + urlPath : `${b}/${urlPath}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
class ApiClient {
|
|
53
64
|
private configs: Record<string, ApiDataSource> | null = null;
|
|
54
65
|
|
|
@@ -103,13 +114,13 @@ class ApiClient {
|
|
|
103
114
|
// `storageState` (the @auth role's saved session) so the request shares the browser's
|
|
104
115
|
// authenticated cookies. Disposed per call so no request context lingers and hangs the process.
|
|
105
116
|
const ctx: APIRequestContext = await request.newContext({
|
|
106
|
-
baseURL: base,
|
|
107
117
|
extraHTTPHeaders: headers,
|
|
108
118
|
timeout: conf.timeout_ms ?? 15000,
|
|
109
119
|
...(opts.storageState ? { storageState: opts.storageState } : {}),
|
|
110
120
|
});
|
|
111
121
|
try {
|
|
112
|
-
|
|
122
|
+
// Full URL (not a baseURL-relative path) so a base path component like /api/v3 is preserved.
|
|
123
|
+
const res = await ctx.fetch(joinApiUrl(base, urlPath), { method: req.method, ...bodyOpt });
|
|
113
124
|
const text = await res.text();
|
|
114
125
|
let parsed: any = text;
|
|
115
126
|
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|