@sun-asterisk/sungen 3.1.2-beta.99 → 3.2.0-beta.141
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/capabilities/context.d.ts +17 -0
- package/dist/capabilities/context.d.ts.map +1 -1
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +70 -9
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/capabilities/registry.d.ts +3 -1
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/capabilities/sensor.d.ts +3 -0
- package/dist/capabilities/sensor.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +17 -11
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +57 -5
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/context.d.ts +9 -0
- package/dist/cli/commands/context.d.ts.map +1 -0
- package/dist/cli/commands/context.js +91 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +42 -30
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +28 -5
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/ledger.d.ts.map +1 -1
- package/dist/cli/commands/ledger.js +15 -5
- package/dist/cli/commands/ledger.js.map +1 -1
- package/dist/cli/commands/manifest.d.ts.map +1 -1
- package/dist/cli/commands/manifest.js +10 -9
- package/dist/cli/commands/manifest.js.map +1 -1
- package/dist/cli/commands/repair.d.ts +8 -0
- package/dist/cli/commands/repair.d.ts.map +1 -0
- package/dist/cli/commands/repair.js +97 -0
- package/dist/cli/commands/repair.js.map +1 -0
- package/dist/cli/commands/script-check.d.ts.map +1 -1
- package/dist/cli/commands/script-check.js +13 -9
- package/dist/cli/commands/script-check.js.map +1 -1
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +7 -4
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +7 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +112 -42
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +3 -1
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +3 -1
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts +9 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +114 -12
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +14 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +63 -1
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +23 -0
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts +9 -0
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +13 -6
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/intent.d.ts +6 -0
- package/dist/harness/intent.d.ts.map +1 -1
- package/dist/harness/intent.js +20 -4
- package/dist/harness/intent.js.map +1 -1
- package/dist/harness/ledger.d.ts.map +1 -1
- package/dist/harness/ledger.js +3 -2
- package/dist/harness/ledger.js.map +1 -1
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +3 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +3 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.js +1 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/query-catalog.d.ts.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/harness/repair.d.ts +20 -0
- package/dist/harness/repair.d.ts.map +1 -0
- package/dist/harness/repair.js +111 -0
- package/dist/harness/repair.js.map +1 -0
- package/dist/harness/script-check.d.ts +3 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +22 -8
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts +40 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +54 -2
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/trace.d.ts.map +1 -1
- package/dist/harness/trace.js +4 -3
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/unit-paths.d.ts +3 -0
- package/dist/harness/unit-paths.d.ts.map +1 -0
- package/dist/harness/unit-paths.js +52 -0
- package/dist/harness/unit-paths.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +2 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/context-discovery.d.ts +12 -0
- package/dist/orchestrator/context-discovery.d.ts.map +1 -0
- package/dist/orchestrator/context-discovery.js +46 -0
- package/dist/orchestrator/context-discovery.js.map +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +7 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +6 -2
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +18 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-api-design.md +62 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +2 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +16 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -2
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +11 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +62 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +2 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +16 -2
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
- package/dist/orchestrator/templates/specs-api.d.ts +38 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +65 -22
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +71 -18
- package/dist/orchestrator/templates/specs-db.d.ts +3 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +78 -1
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +78 -1
- package/dist/orchestrator/templates/specs-test-data.ts +2 -1
- package/package.json +2 -2
- package/src/capabilities/context.ts +19 -0
- package/src/capabilities/discover.ts +27 -7
- package/src/capabilities/registry.ts +3 -1
- package/src/capabilities/sensor.ts +1 -1
- package/src/cli/commands/audit.ts +15 -9
- package/src/cli/commands/capability.ts +53 -5
- package/src/cli/commands/context.ts +52 -0
- package/src/cli/commands/delivery.ts +40 -31
- package/src/cli/commands/generate.ts +30 -5
- package/src/cli/commands/ledger.ts +13 -5
- package/src/cli/commands/manifest.ts +9 -7
- package/src/cli/commands/repair.ts +57 -0
- package/src/cli/commands/script-check.ts +12 -8
- package/src/cli/commands/trace.ts +7 -4
- package/src/cli/index.ts +4 -0
- package/src/generators/test-generator/code-generator.ts +115 -40
- package/src/harness/annotation-overrides.ts +3 -1
- package/src/harness/audit.ts +115 -15
- package/src/harness/capability-plan.ts +51 -1
- package/src/harness/data-driven-lint.ts +20 -0
- package/src/harness/flow-check.ts +15 -6
- package/src/harness/intent.ts +25 -4
- package/src/harness/ledger.ts +3 -2
- package/src/harness/manifest.ts +3 -2
- package/src/harness/parse.ts +4 -0
- package/src/harness/quality-gates.ts +1 -1
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/repair.ts +75 -0
- package/src/harness/script-check.ts +25 -8
- package/src/harness/sensors.ts +71 -2
- package/src/harness/trace.ts +4 -3
- package/src/harness/unit-paths.ts +14 -0
- package/src/index.ts +4 -2
- package/src/orchestrator/ai-rules-updater.ts +2 -0
- package/src/orchestrator/context-discovery.ts +50 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +7 -1
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +6 -2
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +18 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-api-design.md +62 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +2 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +16 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -2
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +11 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +62 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +2 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +16 -2
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
- package/src/orchestrator/templates/specs-api.ts +71 -18
- package/src/orchestrator/templates/specs-db.ts +78 -1
- package/src/orchestrator/templates/specs-test-data.ts +2 -1
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* A driver that isn't installed is simply skipped (capabilities are opt-in). Errors thrown *inside* a
|
|
14
14
|
* present driver propagate — only a genuinely absent package is swallowed (resolve-then-require).
|
|
15
15
|
*/
|
|
16
|
+
import { createRequire } from 'module';
|
|
17
|
+
import * as path from 'path';
|
|
16
18
|
import { capabilityRegistry } from './registry';
|
|
17
19
|
import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
|
|
18
20
|
|
|
@@ -24,14 +26,32 @@ import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
|
|
|
24
26
|
const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api'];
|
|
25
27
|
|
|
26
28
|
function loadExternalDriver(name: string): void {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Resolve from the user's PROJECT first, then from core's own location. Opt-in drivers
|
|
30
|
+
// (`sungen capability add db|api`) install into the PROJECT's node_modules — a globally-installed
|
|
31
|
+
// core (`npm i -g`) wouldn't see them via a core-anchored `require`. The bundled @sungen/driver-ui
|
|
32
|
+
// is a core dependency (co-located with core), so the second anchor finds it.
|
|
33
|
+
for (const anchor of [path.join(process.cwd(), 'package.json'), __filename]) {
|
|
34
|
+
let req: NodeRequire;
|
|
35
|
+
try { req = createRequire(anchor); } catch { continue; }
|
|
36
|
+
let resolved: string;
|
|
37
|
+
try { resolved = req.resolve(name); } catch { continue; } // not installed at this anchor → try next
|
|
38
|
+
try {
|
|
39
|
+
const mod = req(resolved);
|
|
40
|
+
const register: ((r: typeof capabilityRegistry) => void) | undefined = mod?.sungenDriver?.register ?? mod?.register;
|
|
41
|
+
if (typeof register === 'function') register(capabilityRegistry);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// A present-but-BROKEN driver (e.g. a stale/incompatible leftover whose own core dep doesn't
|
|
44
|
+
// resolve) must never crash the whole CLI — `sungen --version`, `capability remove`, etc. must
|
|
45
|
+
// still work. Warn + skip so the user can recover.
|
|
46
|
+
console.warn(
|
|
47
|
+
`⚠ sungen: capability driver "${name}" is installed but failed to load — skipping.\n` +
|
|
48
|
+
` ${(e as Error)?.message || e}\n` +
|
|
49
|
+
` Fix: reinstall it (\`sungen capability add ${name.replace(/^@sungen\/driver-/, '')}\`) or remove it.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return; // found at this anchor (loaded or warned) — don't fall through to other anchors
|
|
31
53
|
}
|
|
32
|
-
|
|
33
|
-
const register: ((r: typeof capabilityRegistry) => void) | undefined = mod?.sungenDriver?.register ?? mod?.register;
|
|
34
|
-
if (typeof register === 'function') register(capabilityRegistry);
|
|
54
|
+
// not installed at any anchor — capabilities are opt-in, so skip silently
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
export function discoverAndRegisterCapabilities(): void {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { StepPattern } from '../generators/test-generator/patterns/types';
|
|
15
15
|
import type { Sensor } from './sensor';
|
|
16
|
-
import type { DiscoveryProvider, ContextMapper } from './context';
|
|
16
|
+
import type { DiscoveryProvider, ContextMapper, RepairProvider } from './context';
|
|
17
17
|
|
|
18
18
|
export interface CapabilityDescriptor {
|
|
19
19
|
/** Stable id: 'ui' | 'db' | 'api' | 'core' | … */
|
|
@@ -29,6 +29,8 @@ export interface CapabilityDescriptor {
|
|
|
29
29
|
/** Orchestration phase hooks — declared in R1, consumed by the pipeline from R2 onward. */
|
|
30
30
|
discovery?: DiscoveryProvider; // sources → Context slice
|
|
31
31
|
contextMapper?: ContextMapper; // Context → generation units + modes
|
|
32
|
+
/** Repair phase: the capability's deterministic fix catalog (audit/runtime finding → fix). */
|
|
33
|
+
repair?: RepairProvider;
|
|
32
34
|
/** Provider for this capability's viewpoint catalog (UI: the 17-pattern universal catalog). */
|
|
33
35
|
viewpoints?: () => unknown;
|
|
34
36
|
/**
|
|
@@ -41,7 +41,7 @@ export interface GateInput {
|
|
|
41
41
|
screenName: string;
|
|
42
42
|
cwd: string;
|
|
43
43
|
featureText: string;
|
|
44
|
-
scenarios: Array<{ name: string; queryRefs?: string[]; apiRefs?: string[] }>;
|
|
44
|
+
scenarios: Array<{ name: string; queryRefs?: string[]; apiRefs?: string[]; casesDataset?: string; stepsText?: string; manual?: boolean }>;
|
|
45
45
|
/** UI: universal-viewpoint theme gaps the coverage gate found (generic string list). */
|
|
46
46
|
universalGaps?: string[];
|
|
47
47
|
}
|
|
@@ -2,12 +2,16 @@ import { Command } from 'commander';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import { runAudit, AuditReport } from '../../harness/audit';
|
|
5
|
+
import { reportSlug } from '../../harness/unit-paths';
|
|
5
6
|
|
|
6
7
|
function findScreenDir(name: string): string | null {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// `name` may be a bare screen/flow/area, or an api unit id (`api/<area>`, `api/flows/<flow>`).
|
|
9
|
+
const candidates = [
|
|
10
|
+
path.join(process.cwd(), 'qa', 'screens', name),
|
|
11
|
+
path.join(process.cwd(), 'qa', 'flows', name),
|
|
12
|
+
path.join(process.cwd(), 'qa', 'api', name), // qa/api/<area> or qa/api/flows/<flow>
|
|
13
|
+
];
|
|
14
|
+
for (const c of candidates) if (fs.existsSync(c)) return c;
|
|
11
15
|
return null;
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -88,20 +92,22 @@ export function registerAuditCommand(program: Command): void {
|
|
|
88
92
|
.command('audit')
|
|
89
93
|
.description('Harness: measure test-design quality (viewpoint gate, depth, balance, duplicates, traceability)')
|
|
90
94
|
.option('-s, --screen <name>', 'Screen or flow name to audit')
|
|
95
|
+
.option('--api <name>', 'API-first area or api flow to audit (e.g. orders, flows/signup)')
|
|
96
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
91
97
|
.option('--json', 'Output the raw JSON report')
|
|
92
98
|
.action((options) => {
|
|
93
99
|
try {
|
|
94
|
-
const name = options.screen;
|
|
95
|
-
if (!name) throw new Error('Provide --screen <name>');
|
|
100
|
+
const name = options.screen || options.api || options.area;
|
|
101
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
96
102
|
const dir = findScreenDir(name);
|
|
97
|
-
if (!dir) throw new Error(`
|
|
103
|
+
if (!dir) throw new Error(`Not found: qa/screens/${name}, qa/flows/${name}, or qa/api/${name}`);
|
|
98
104
|
|
|
99
105
|
const report = runAudit(dir, name);
|
|
100
106
|
|
|
101
|
-
// Persist report under .sungen/reports/
|
|
107
|
+
// Persist report under .sungen/reports/ (flat slug: api flow `flows/x` → `flows-x-audit.json`).
|
|
102
108
|
const outDir = path.join(process.cwd(), '.sungen', 'reports');
|
|
103
109
|
fs.mkdirSync(outDir, { recursive: true });
|
|
104
|
-
const outPath = path.join(outDir, `${name}-audit.json`);
|
|
110
|
+
const outPath = path.join(outDir, `${reportSlug(name)}-audit.json`);
|
|
105
111
|
fs.writeFileSync(outPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
106
112
|
|
|
107
113
|
if (options.json) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import * as fs from 'fs';
|
|
4
5
|
import * as path from 'path';
|
|
5
6
|
import {
|
|
@@ -8,6 +9,42 @@ import {
|
|
|
8
9
|
import { buildPlan, MANUAL_REASONS } from '../../harness/capability-plan';
|
|
9
10
|
import { adapterRegistry } from '../../generators/test-generator/adapters';
|
|
10
11
|
|
|
12
|
+
/** The running CLI's (core) version — opt-in drivers install in lockstep with it (not `@latest`). */
|
|
13
|
+
function coreVersion(): string {
|
|
14
|
+
try { return require('../../../package.json').version || 'latest'; } catch { return 'latest'; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Verify a capability driver (db/api/…) installed into the PROJECT exposes the capability SPI entry
|
|
19
|
+
* (`sungenDriver.register`). Resolved from the project's node_modules so it works with a globally
|
|
20
|
+
* installed core. Capability drivers are NOT codegen adapters — don't route them through loadDriver.
|
|
21
|
+
*/
|
|
22
|
+
function verifyCapabilityDriver(pkg: string, cwd: string): void {
|
|
23
|
+
const req = createRequire(path.join(cwd, 'package.json'));
|
|
24
|
+
// 1) Installed? (resolve only — distinguishes "not installed" from "installed but broken")
|
|
25
|
+
try {
|
|
26
|
+
req.resolve(pkg);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(`${pkg} is not installed in this project — run \`npm install -D ${pkg}\` here (a package.json must exist in this directory).`);
|
|
29
|
+
}
|
|
30
|
+
// 2) Loadable? Surface the REAL error — a failure here is almost always a stale/wrong
|
|
31
|
+
// @sun-asterisk/sungen on the module path (e.g. leftover node_modules), NOT a missing driver.
|
|
32
|
+
let mod: any;
|
|
33
|
+
try {
|
|
34
|
+
mod = req(pkg);
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${pkg} is installed but failed to load:\n ${e?.message || e}\n` +
|
|
38
|
+
' This usually means a stale @sun-asterisk/sungen is on the module path. Run in a clean,\n' +
|
|
39
|
+
' dedicated project directory and remove stray installs (e.g. rm -rf ~/node_modules).',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const register = mod?.sungenDriver?.register ?? mod?.register;
|
|
43
|
+
if (typeof register !== 'function') {
|
|
44
|
+
throw new Error(`${pkg} is installed but is not a sungen capability driver (missing sungenDriver.register).`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
11
48
|
function findScreenDir(name: string): string | null {
|
|
12
49
|
const s = path.join(process.cwd(), 'qa', 'screens', name);
|
|
13
50
|
if (fs.existsSync(s)) return s;
|
|
@@ -111,16 +148,27 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
111
148
|
const bundled = adapterRegistry.hasAdapter(adapterName); // e.g. `web` is bundled in Phase 2a
|
|
112
149
|
|
|
113
150
|
if (!bundled && !o.skipInstall) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
151
|
+
// Pin the driver to the RUNNING core's exact version (lockstep). Without this, plain
|
|
152
|
+
// `npm install @sungen/driver-x` resolves the package's `latest` dist-tag — which on a
|
|
153
|
+
// beta channel can lag (e.g. a prerelease first-published-as-latest), pulling a driver
|
|
154
|
+
// whose core dep resolves to the wrong @sun-asterisk/sungen and fails to load.
|
|
155
|
+
const spec = `${meta.package}@${coreVersion()}`;
|
|
156
|
+
console.log(`📦 Installing ${spec} (dev dependency)...`);
|
|
157
|
+
const r = spawnSync('npm', ['install', '-D', spec], { stdio: 'inherit', shell: true });
|
|
158
|
+
if (r.status !== 0) throw new Error(`npm install -D ${spec} failed.`);
|
|
117
159
|
} else if (bundled) {
|
|
118
160
|
console.log(`✓ ${driver} is built-in (no install needed).`);
|
|
119
161
|
}
|
|
120
162
|
|
|
121
|
-
// Verify it
|
|
163
|
+
// Verify it loaded (skip for bundled — already registered). Two driver shapes:
|
|
164
|
+
// - platform driver (web/mobile): a codegen adapter, loaded via the adapter registry (mod.activate)
|
|
165
|
+
// - capability driver (db/api/…): a capability-SPI package (mod.sungenDriver.register) — NOT an adapter
|
|
122
166
|
if (!bundled) {
|
|
123
|
-
|
|
167
|
+
if (meta.kind === 'platform') {
|
|
168
|
+
adapterRegistry.loadDriver(adapterName, meta.package, cwd); // throws if not loadable
|
|
169
|
+
} else {
|
|
170
|
+
verifyCapabilityDriver(meta.package, cwd); // capability SPI entry, resolved from the project
|
|
171
|
+
}
|
|
124
172
|
}
|
|
125
173
|
|
|
126
174
|
const profile = readCapabilities(cwd);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { discoverUnitContext } from '../../orchestrator/context-discovery';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `sungen context` — run the Discover → Contextualize phase (AO-3) for a unit and emit the
|
|
8
|
+
* normalized Context + generation-unit work-list. The `/sungen:design` loop calls this first so it
|
|
9
|
+
* knows WHAT to generate (the endpoints/screens + their modes) before authoring scenarios. Pure +
|
|
10
|
+
* deterministic; writes `.sungen/reports/<unit>-context.json`.
|
|
11
|
+
*/
|
|
12
|
+
export function registerContextCommand(program: Command): void {
|
|
13
|
+
program
|
|
14
|
+
.command('context')
|
|
15
|
+
.description('Discover + contextualize a unit (screen/flow/api area) → Context + generation units')
|
|
16
|
+
.option('-s, --screen <name>', 'Screen name')
|
|
17
|
+
.option('--flow <name>', 'Flow name (resolved as flows/<name>)')
|
|
18
|
+
.option('--api <name>', 'API-first area or api flow (resolved as api/<name>)')
|
|
19
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
20
|
+
.option('--json', 'Print the raw JSON')
|
|
21
|
+
.action(async (o: { screen?: string; flow?: string; api?: string; area?: string; json?: boolean }) => {
|
|
22
|
+
try {
|
|
23
|
+
const unitId = (o.api || o.area) ? `api/${o.api || o.area}` : o.flow ? `flows/${o.flow}` : o.screen;
|
|
24
|
+
if (!unitId) throw new Error('Provide --screen <name>, --flow <name>, or --api <area>.');
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const result = await discoverUnitContext(unitId, cwd);
|
|
27
|
+
|
|
28
|
+
const outDir = path.join(cwd, '.sungen', 'reports');
|
|
29
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
30
|
+
const slug = unitId.replace(/\//g, '-');
|
|
31
|
+
const outPath = path.join(outDir, `${slug}-context.json`);
|
|
32
|
+
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
33
|
+
if (o.json) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
34
|
+
|
|
35
|
+
const L = console.log;
|
|
36
|
+
const facts = result.context.facts as { endpoints?: Array<{ name: string; method: string; path: string }> };
|
|
37
|
+
L(`\n━━━ Context: ${unitId} (capability: ${result.capability}) ━━━`);
|
|
38
|
+
const srcKeys = Object.keys(result.context.sources);
|
|
39
|
+
L(` Sources: ${srcKeys.length ? srcKeys.join(', ') : '(none)'}`);
|
|
40
|
+
if (facts.endpoints?.length) {
|
|
41
|
+
L(` Endpoints (${facts.endpoints.length}):`);
|
|
42
|
+
for (const e of facts.endpoints) L(` • ${e.method} ${e.path} (@api:${e.name})`);
|
|
43
|
+
}
|
|
44
|
+
const byMode = result.units.reduce<Record<string, number>>((m, u) => { m[u.mode] = (m[u.mode] || 0) + 1; return m; }, {});
|
|
45
|
+
L(` Generation units: ${result.units.length} — ${Object.entries(byMode).map(([m, n]) => `${m}×${n}`).join(' · ') || '(none)'}`);
|
|
46
|
+
L(`\n Report: ${path.relative(cwd, outPath)}\n`);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('Error:', e instanceof Error ? e.message : e);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -52,45 +52,54 @@ function log(msg: string): void {
|
|
|
52
52
|
* `name` is kept as an alias of `featureBaseName` so existing callers/labels
|
|
53
53
|
* (preflight table, summary) read naturally — every visible row is per-feature.
|
|
54
54
|
*/
|
|
55
|
+
type UnitKind = 'screen' | 'flow' | 'api';
|
|
56
|
+
/** qa/ subfolder for a unit kind. */
|
|
57
|
+
const qaParent = (kind: UnitKind): string => (kind === 'flow' ? 'flows' : kind === 'api' ? 'api' : 'screens');
|
|
58
|
+
|
|
55
59
|
interface DeliveryTarget {
|
|
56
60
|
screen: string;
|
|
57
61
|
featureBaseName: string;
|
|
58
62
|
/** Alias of `featureBaseName` — preserves the old `target.name` call sites. */
|
|
59
63
|
name: string;
|
|
64
|
+
kind: UnitKind;
|
|
65
|
+
/** Back-compat: flows kept distinct labels/paths before api was added. */
|
|
60
66
|
isFlow: boolean;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
function makeTarget(screen: string, featureBaseName: string,
|
|
64
|
-
return { screen, featureBaseName, name: featureBaseName, isFlow };
|
|
69
|
+
function makeTarget(screen: string, featureBaseName: string, kind: UnitKind): DeliveryTarget {
|
|
70
|
+
return { screen, featureBaseName, name: featureBaseName, kind, isFlow: kind === 'flow' };
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
|
-
* List all `.feature` files inside a screen/flow as separate targets.
|
|
74
|
+
* List all `.feature` files inside a screen/flow/api unit as separate targets.
|
|
69
75
|
* Returns empty array when the directory has no features yet.
|
|
70
76
|
*/
|
|
71
|
-
function listFeatureTargets(cwd: string, screen: string,
|
|
72
|
-
const featuresDir = path.join(cwd, 'qa',
|
|
77
|
+
function listFeatureTargets(cwd: string, screen: string, kind: UnitKind): DeliveryTarget[] {
|
|
78
|
+
const featuresDir = path.join(cwd, 'qa', qaParent(kind), screen, 'features');
|
|
73
79
|
if (!fs.existsSync(featuresDir)) return [];
|
|
74
80
|
return fs.readdirSync(featuresDir)
|
|
75
81
|
.filter((f) => f.endsWith('.feature'))
|
|
76
|
-
.map((f) => makeTarget(screen, f.slice(0, -'.feature'.length),
|
|
82
|
+
.map((f) => makeTarget(screen, f.slice(0, -'.feature'.length), kind))
|
|
77
83
|
.sort((a, b) => a.featureBaseName.localeCompare(b.featureBaseName));
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
function listAllTargets(cwd: string): DeliveryTarget[] {
|
|
81
87
|
const targets: DeliveryTarget[] = [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for (const d of fs.readdirSync(
|
|
86
|
-
if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, d.name,
|
|
88
|
+
const scan = (kind: UnitKind, skip: (n: string) => boolean = () => false) => {
|
|
89
|
+
const root = path.join(cwd, 'qa', qaParent(kind));
|
|
90
|
+
if (!fs.existsSync(root)) return;
|
|
91
|
+
for (const d of fs.readdirSync(root, { withFileTypes: true })) {
|
|
92
|
+
if (d.isDirectory() && !skip(d.name)) targets.push(...listFeatureTargets(cwd, d.name, kind));
|
|
87
93
|
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
};
|
|
95
|
+
scan('screen');
|
|
96
|
+
scan('flow');
|
|
97
|
+
scan('api', (n) => n === 'flows'); // api areas: qa/api/<area>
|
|
98
|
+
// api flows: qa/api/flows/<flow> → screen `flows/<flow>`, kind 'api' (paths compose to qa/api/flows/… + specs/generated/api/flows/…)
|
|
99
|
+
const apiFlowsRoot = path.join(cwd, 'qa', 'api', 'flows');
|
|
100
|
+
if (fs.existsSync(apiFlowsRoot)) {
|
|
101
|
+
for (const d of fs.readdirSync(apiFlowsRoot, { withFileTypes: true })) {
|
|
102
|
+
if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, path.posix.join('flows', d.name), 'api'));
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -108,28 +117,27 @@ function listAllTargets(cwd: string): DeliveryTarget[] {
|
|
|
108
117
|
* feature file with the basename across all screens & flows.
|
|
109
118
|
*/
|
|
110
119
|
function resolveTargetsFromArg(cwd: string, name: string): DeliveryTarget[] {
|
|
111
|
-
if (fs.existsSync(path.join(cwd, 'qa', 'flows', name)))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Treat as feature basename — find the parent
|
|
120
|
+
if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) return listFeatureTargets(cwd, name, 'flow');
|
|
121
|
+
if (fs.existsSync(path.join(cwd, 'qa', 'screens', name))) return listFeatureTargets(cwd, name, 'screen');
|
|
122
|
+
if (fs.existsSync(path.join(cwd, 'qa', 'api', name))) return listFeatureTargets(cwd, name, 'api');
|
|
123
|
+
// `flows/<flow>` or a bare api-flow name → qa/api/flows/<flow>
|
|
124
|
+
const apiFlow = name.startsWith('flows/') ? name : path.posix.join('flows', name);
|
|
125
|
+
if (fs.existsSync(path.join(cwd, 'qa', 'api', apiFlow))) return listFeatureTargets(cwd, apiFlow, 'api');
|
|
126
|
+
// Treat as feature basename — find the parent unit that hosts it.
|
|
118
127
|
const candidates = listAllTargets(cwd).filter((t) => t.featureBaseName === name);
|
|
119
128
|
if (candidates.length > 0) return candidates;
|
|
120
129
|
// Fallback: treat as screen name even if directory missing (lets preflight
|
|
121
130
|
// surface the "feature file missing" error with the right path).
|
|
122
|
-
return [makeTarget(name, name,
|
|
131
|
+
return [makeTarget(name, name, 'screen')];
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
function qaDir(cwd: string, target: DeliveryTarget): string {
|
|
126
|
-
return path.join(cwd, 'qa', target.
|
|
135
|
+
return path.join(cwd, 'qa', qaParent(target.kind), target.screen);
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
function generatedDir(cwd: string, target: DeliveryTarget): string {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
: path.join(cwd, 'specs', 'generated', target.screen);
|
|
139
|
+
const sub = target.kind === 'flow' ? path.join('flows', target.screen) : target.kind === 'api' ? path.join('api', target.screen) : target.screen;
|
|
140
|
+
return path.join(cwd, 'specs', 'generated', sub);
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
// ----------------------------------------------------------------------------
|
|
@@ -262,7 +270,8 @@ function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
|
|
|
262
270
|
|
|
263
271
|
const featureOk = checkFeatureReal(featureFile);
|
|
264
272
|
const testDataOk = checkTestDataHasVars(testDataFile);
|
|
265
|
-
|
|
273
|
+
// API units have no DOM → no selectors; the catalog (apis.yaml) is the contract. N/A, not missing.
|
|
274
|
+
const selectorsOk = target.kind === 'api' ? true : checkSelectorsHasEntries(selectorsFile, target.featureBaseName);
|
|
266
275
|
const specOk = fs.existsSync(specFile);
|
|
267
276
|
const resultsOk = resultsFile !== null;
|
|
268
277
|
|
|
@@ -284,7 +293,7 @@ function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
|
|
|
284
293
|
}
|
|
285
294
|
if (!specOk) {
|
|
286
295
|
missing.push(`compiled .spec.ts missing: ${path.relative(cwd, specFile)}`);
|
|
287
|
-
suggestions.push(target.
|
|
296
|
+
suggestions.push(`sungen generate --${target.kind === 'flow' ? 'flow' : target.kind === 'api' ? 'api' : 'screen'} ${target.screen}`);
|
|
288
297
|
}
|
|
289
298
|
if (!resultsOk) {
|
|
290
299
|
const env = process.env.SUNGEN_ENV;
|
|
@@ -44,12 +44,21 @@ function findFeatureFilesForFlow(flowName: string): string[] {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Find
|
|
47
|
+
* Find feature files for a specific API-first unit — an area (`orders`) or an api flow
|
|
48
|
+
* (`flows/<flow>`). Catalog/test-data live alongside under qa/api/<name>/.
|
|
49
|
+
*/
|
|
50
|
+
function findFeatureFilesForApi(name: string): string[] {
|
|
51
|
+
return findFeatureFiles(path.join(process.cwd(), 'qa', 'api', name, 'features'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find all feature files across all screens, flows, and API-first areas/flows.
|
|
48
56
|
*/
|
|
49
57
|
function findAllFeatureFiles(): string[] {
|
|
50
58
|
const allFiles: string[] = [];
|
|
51
59
|
const screensDir = path.join(process.cwd(), 'qa', 'screens');
|
|
52
60
|
const flowsDir = path.join(process.cwd(), 'qa', 'flows');
|
|
61
|
+
const apiDir = path.join(process.cwd(), 'qa', 'api');
|
|
53
62
|
|
|
54
63
|
if (fs.existsSync(screensDir)) {
|
|
55
64
|
const screenDirs = fs.readdirSync(screensDir, { withFileTypes: true })
|
|
@@ -73,6 +82,9 @@ function findAllFeatureFiles(): string[] {
|
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
// API-first: every .feature under qa/api/** (areas qa/api/<area>/features + flows qa/api/flows/<flow>/features).
|
|
86
|
+
if (fs.existsSync(apiDir)) allFiles.push(...findFeatureFiles(apiDir));
|
|
87
|
+
|
|
76
88
|
return allFiles;
|
|
77
89
|
}
|
|
78
90
|
|
|
@@ -82,19 +94,32 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
82
94
|
.description('Generate Playwright test code from Gherkin features')
|
|
83
95
|
.option('-s, --screen <name>', 'Generate tests for a specific screen')
|
|
84
96
|
.option('--flow <name>', 'Generate tests for a specific flow')
|
|
85
|
-
.option('--
|
|
97
|
+
.option('--api <name>', 'Generate tests for an API-first area or api flow (e.g. orders, flows/signup)')
|
|
98
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
99
|
+
.option('--all', 'Generate tests for all screens, flows, and API areas')
|
|
86
100
|
.option('--framework <name>', 'Override the platform driver (else read from qa/capabilities.yaml)')
|
|
87
101
|
.option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
|
|
88
102
|
.action(async (options) => {
|
|
89
103
|
try {
|
|
90
104
|
const screenName = options.screen;
|
|
91
105
|
const flowName = options.flow;
|
|
106
|
+
const apiName = options.api || options.area;
|
|
92
107
|
|
|
93
108
|
// Find feature files
|
|
94
109
|
let featureFiles: string[];
|
|
95
110
|
let qaSourceDir: string;
|
|
96
111
|
|
|
97
|
-
if (
|
|
112
|
+
if (apiName) {
|
|
113
|
+
featureFiles = findFeatureFilesForApi(apiName);
|
|
114
|
+
if (featureFiles.length === 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`No feature files found for API unit: ${apiName}\n` +
|
|
117
|
+
`Looked in: qa/api/${apiName}/features/ (scaffold with \`sungen api add --area ${apiName}\` or \`--flow\`)`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
qaSourceDir = path.join(process.cwd(), 'qa', 'api');
|
|
121
|
+
console.log(`\nGenerating tests: api/${apiName}\n`);
|
|
122
|
+
} else if (screenName) {
|
|
98
123
|
featureFiles = findFeatureFilesForScreen(screenName);
|
|
99
124
|
if (featureFiles.length === 0) {
|
|
100
125
|
throw new Error(
|
|
@@ -117,10 +142,10 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
117
142
|
} else {
|
|
118
143
|
featureFiles = findAllFeatureFiles();
|
|
119
144
|
if (featureFiles.length === 0) {
|
|
120
|
-
throw new Error('No feature files found in qa/screens/ or qa/
|
|
145
|
+
throw new Error('No feature files found in qa/screens/, qa/flows/, or qa/api/');
|
|
121
146
|
}
|
|
122
147
|
qaSourceDir = path.join(process.cwd(), 'qa');
|
|
123
|
-
console.log(`\nGenerating tests for all screens and
|
|
148
|
+
console.log(`\nGenerating tests for all screens, flows, and API areas\n`);
|
|
124
149
|
}
|
|
125
150
|
|
|
126
151
|
// Output directory
|
|
@@ -9,7 +9,9 @@ export function registerLedgerCommand(program: Command): void {
|
|
|
9
9
|
ledger
|
|
10
10
|
.command('record')
|
|
11
11
|
.description('Append a step event to the ledger')
|
|
12
|
-
.
|
|
12
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
13
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
14
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
13
15
|
.requiredOption('--step <name>', 'Step name (discovery | viewpoint | gherkin | audit | repair:1 ...)')
|
|
14
16
|
.option('--run <id>', 'Run id — groups all phases of one create-test invocation (else auto-segmented by time gap)')
|
|
15
17
|
.option('--model <id>', 'Model id')
|
|
@@ -19,10 +21,12 @@ export function registerLedgerCommand(program: Command): void {
|
|
|
19
21
|
.option('--note <text>', 'Free note')
|
|
20
22
|
.action((o) => {
|
|
21
23
|
try {
|
|
22
|
-
|
|
24
|
+
const name = o.screen || o.api || o.area;
|
|
25
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
26
|
+
recordEvent(name, {
|
|
23
27
|
step: o.step, runId: o.run, model: o.model, tokensIn: o.tokensIn, tokensOut: o.tokensOut, ms: o.ms, note: o.note,
|
|
24
28
|
});
|
|
25
|
-
console.log(`✓ ledger: ${
|
|
29
|
+
console.log(`✓ ledger: ${name} · ${o.step}`);
|
|
26
30
|
} catch (e) {
|
|
27
31
|
console.error('Error:', e instanceof Error ? e.message : e);
|
|
28
32
|
process.exit(1);
|
|
@@ -32,12 +36,16 @@ export function registerLedgerCommand(program: Command): void {
|
|
|
32
36
|
ledger
|
|
33
37
|
.command('report')
|
|
34
38
|
.description('Summarise ledger + efficiency verdicts (pulls audit score if present)')
|
|
35
|
-
.
|
|
39
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
40
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
41
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
36
42
|
.option('--all-runs', 'Aggregate ALL runs (default: latest run only)')
|
|
37
43
|
.option('--json', 'Output raw JSON')
|
|
38
44
|
.action((o) => {
|
|
39
45
|
try {
|
|
40
|
-
const
|
|
46
|
+
const name = o.screen || o.api || o.area;
|
|
47
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
48
|
+
const r = buildReport(name, { allRuns: o.allRuns });
|
|
41
49
|
if (o.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
42
50
|
const scope = r.runScope === 'all' ? `all ${r.runs} runs` : `latest run${r.runs > 1 ? ` of ${r.runs}` : ''}`;
|
|
43
51
|
console.log(`\n━━━ Usage Ledger: ${r.screen} (${r.events} events · ${scope}) ━━━`);
|
|
@@ -4,10 +4,10 @@ import * as fs from 'fs';
|
|
|
4
4
|
import { buildManifest, diffManifest, loadManifest, saveManifest } from '../../harness/manifest';
|
|
5
5
|
|
|
6
6
|
function findScreenDir(name: string): string | null {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
for (const p of ['screens', 'flows', 'api']) {
|
|
8
|
+
const d = path.join(process.cwd(), 'qa', p, name);
|
|
9
|
+
if (fs.existsSync(d)) return d;
|
|
10
|
+
}
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -16,14 +16,16 @@ export function registerManifestCommand(program: Command): void {
|
|
|
16
16
|
.command('manifest')
|
|
17
17
|
.description('Spec-fingerprint manifest: build a scenario↔spec-section map, or diff to plan keep/regenerate/retire')
|
|
18
18
|
.option('-s, --screen <name>', 'Screen or flow name')
|
|
19
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
20
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
19
21
|
.option('--diff', 'Compare current spec vs stored manifest → change plan')
|
|
20
22
|
.option('--json', 'Output raw JSON')
|
|
21
23
|
.action((options) => {
|
|
22
24
|
try {
|
|
23
|
-
const name = options.screen;
|
|
24
|
-
if (!name) throw new Error('Provide --screen <name>');
|
|
25
|
+
const name = options.screen || options.api || options.area;
|
|
26
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
25
27
|
const dir = findScreenDir(name);
|
|
26
|
-
if (!dir) throw new Error(`
|
|
28
|
+
if (!dir) throw new Error(`Not found: qa/screens|flows|api/${name}`);
|
|
27
29
|
|
|
28
30
|
if (options.diff) {
|
|
29
31
|
const manifest = loadManifest(name);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { planRepair } from '../../harness/repair';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `sungen repair` (#343) — turn a unit's audit findings + Playwright failures into a concrete fix
|
|
8
|
+
* plan, using the unit-capability's fix catalog (the `repair` SPI). Deterministic; the `/sungen:design`
|
|
9
|
+
* + `/sungen:run-test` repair loops apply the same proposals. Run `sungen audit`/the tests first.
|
|
10
|
+
*/
|
|
11
|
+
export function registerRepairCommand(program: Command): void {
|
|
12
|
+
program
|
|
13
|
+
.command('repair')
|
|
14
|
+
.description('Propose concrete fixes for a unit from its audit findings + test failures (capability fix catalog)')
|
|
15
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
16
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
17
|
+
.option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
|
|
18
|
+
.option('--json', 'Output raw JSON')
|
|
19
|
+
.action((o: { screen?: string; api?: string; area?: string; json?: boolean }) => {
|
|
20
|
+
try {
|
|
21
|
+
const name = o.screen || o.api || o.area;
|
|
22
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
// Capability-resolution id + the generated dir, per kind.
|
|
25
|
+
const kind = fs.existsSync(path.join(cwd, 'qa', 'api', name)) || o.api ? 'api'
|
|
26
|
+
: fs.existsSync(path.join(cwd, 'qa', 'flows', name)) ? 'flow' : 'screen';
|
|
27
|
+
const unitId = kind === 'api' ? `api/${name}` : kind === 'flow' ? `flows/${name}` : name;
|
|
28
|
+
const generatedDir = path.join(cwd, 'specs', 'generated', kind === 'api' ? path.join('api', name) : kind === 'flow' ? path.join('flows', name) : name);
|
|
29
|
+
|
|
30
|
+
const plan = planRepair(unitId, name, cwd, generatedDir);
|
|
31
|
+
if (o.json) { console.log(JSON.stringify(plan, null, 2)); return; }
|
|
32
|
+
|
|
33
|
+
const L = console.log;
|
|
34
|
+
L(`\n━━━ Repair plan: ${name} (capability: ${plan.capability ?? 'none'}) ━━━`);
|
|
35
|
+
if (!plan.rulesAvailable) L(` (this capability ships no repair rules)`);
|
|
36
|
+
if (!plan.proposals.length && !plan.unmatched.length) {
|
|
37
|
+
L(` ✓ Nothing to repair — run \`sungen audit --${kind === 'api' ? 'api' : 'screen'} ${name}\` + the tests first, or the unit is clean.\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (plan.proposals.length) {
|
|
41
|
+
L(`\n Proposed fixes (${plan.proposals.length}):`);
|
|
42
|
+
for (const p of plan.proposals) {
|
|
43
|
+
L(` • [${p.ruleId}] ${p.source === 'runtime' ? '(test) ' : ''}${p.signal}`);
|
|
44
|
+
L(` → ${p.fix}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (plan.unmatched.length) {
|
|
48
|
+
L(`\n No known rule (review manually):`);
|
|
49
|
+
for (const u of plan.unmatched.slice(0, 10)) L(` - ${u}`);
|
|
50
|
+
}
|
|
51
|
+
L('');
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error('Error:', e instanceof Error ? e.message : e);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|