@sun-asterisk/sungen 3.1.2-beta.93 → 3.1.2-beta.98
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/README.md +4 -428
- package/dist/capabilities/builtins.d.ts +31 -0
- package/dist/capabilities/builtins.d.ts.map +1 -0
- package/dist/capabilities/builtins.js +84 -0
- package/dist/capabilities/builtins.js.map +1 -0
- package/dist/capabilities/context-router.d.ts +34 -0
- package/dist/capabilities/context-router.d.ts.map +1 -0
- package/dist/capabilities/context-router.js +49 -0
- package/dist/capabilities/context-router.js.map +1 -0
- package/dist/capabilities/context.d.ts +51 -0
- package/dist/capabilities/context.d.ts.map +1 -0
- package/dist/capabilities/context.js +17 -0
- package/dist/capabilities/context.js.map +1 -0
- package/dist/capabilities/discover.d.ts +2 -0
- package/dist/capabilities/discover.d.ts.map +1 -0
- package/dist/capabilities/discover.js +48 -0
- package/dist/capabilities/discover.js.map +1 -0
- package/dist/capabilities/registry.d.ts +90 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +43 -0
- package/dist/capabilities/registry.js.map +1 -0
- package/dist/capabilities/sensor.d.ts +49 -0
- package/dist/capabilities/sensor.d.ts.map +1 -0
- package/dist/capabilities/sensor.js +3 -0
- package/dist/capabilities/sensor.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +7 -3
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +10 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/code-generator.d.ts +11 -9
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +53 -76
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +0 -10
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +10 -47
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +9 -0
- package/dist/harness/annotation-overrides.d.ts.map +1 -0
- package/dist/harness/annotation-overrides.js +36 -0
- package/dist/harness/annotation-overrides.js.map +1 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +35 -7
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +35 -12
- package/dist/harness/parse.d.ts +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +13 -4
- package/dist/harness/parse.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
- package/dist/orchestrator/templates/specs-api.d.ts +19 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-api.js +128 -0
- package/dist/orchestrator/templates/specs-api.js.map +1 -0
- package/dist/orchestrator/templates/specs-api.ts +101 -0
- package/package.json +7 -30
- package/src/capabilities/builtins.ts +85 -0
- package/src/capabilities/context-router.ts +66 -0
- package/src/capabilities/context.ts +46 -0
- package/src/capabilities/discover.ts +42 -0
- package/src/capabilities/registry.ts +111 -0
- package/src/capabilities/sensor.ts +47 -0
- package/src/cli/commands/generate.ts +7 -3
- package/src/cli/index.ts +10 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/code-generator.ts +51 -74
- package/src/generators/test-generator/patterns/index.ts +9 -35
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/harness/annotation-overrides.ts +25 -0
- package/src/harness/audit.ts +37 -8
- package/src/harness/catalog/drivers.yaml +35 -12
- package/src/harness/parse.ts +7 -2
- package/src/index.ts +30 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
- package/src/orchestrator/templates/specs-api.ts +101 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
- package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/database-patterns.js +0 -95
- package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
- package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
- package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
- package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
- package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
- package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
- package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
- package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
- package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
- package/docs/orchestration-spec.md +0 -267
- package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
- package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
- package/src/generators/test-generator/patterns/database-patterns.ts +0 -96
- package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
- package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
- package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
- package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
- package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
- package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
- package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
- package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Sungen API Driver — runtime helper (auto-generated into specs/api.ts).
|
|
4
|
+
*
|
|
5
|
+
* Runs a catalog-defined HTTP request and returns { status, ok, body, headers } — bound to a
|
|
6
|
+
* `{{name}}` variable by the `@api:<name>` annotation, asserted with `expect {{name.status}} …` /
|
|
7
|
+
* `{{name.body.<path>}}`. Base URL + auth come from a `kind: api` datasource in datasources.yaml,
|
|
8
|
+
* with `${VAR}` resolved from .env.qa / process.env — never inline.
|
|
9
|
+
*
|
|
10
|
+
* Safety: a datasource flagged `env: production` is refused unless SUNGEN_ALLOW_PROD=1.
|
|
11
|
+
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
interface ApiDataSource {
|
|
17
|
+
kind?: string;
|
|
18
|
+
base_url?: string;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
env?: string;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
timeout_ms?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadEnvQa(): void {
|
|
26
|
+
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
27
|
+
const p = path.join(process.cwd(), name);
|
|
28
|
+
if (!name.endsWith('.') && fs.existsSync(p)) {
|
|
29
|
+
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
|
|
30
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
31
|
+
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadConfig(): Record<string, ApiDataSource> {
|
|
38
|
+
loadEnvQa();
|
|
39
|
+
const file = [path.join(process.cwd(), 'datasources.yaml'), path.join(process.cwd(), 'qa', 'datasources.yaml')].find((f) => fs.existsSync(f));
|
|
40
|
+
if (!file) throw new Error('API Driver: no datasources.yaml found (project root or qa/).');
|
|
41
|
+
const raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
|
|
42
|
+
const { parse } = require('yaml');
|
|
43
|
+
const doc = parse(raw) || {};
|
|
44
|
+
return doc.datasources || {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function substitute(text: string, params: Record<string, any>): string {
|
|
48
|
+
return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class ApiClient {
|
|
52
|
+
private configs: Record<string, ApiDataSource> | null = null;
|
|
53
|
+
|
|
54
|
+
private cfg(name?: string): { key: string; conf: ApiDataSource } {
|
|
55
|
+
if (!this.configs) this.configs = loadConfig();
|
|
56
|
+
const key = name || Object.keys(this.configs).find((k) => (this.configs![k].kind || 'api') === 'api') || Object.keys(this.configs)[0];
|
|
57
|
+
const conf = this.configs[key];
|
|
58
|
+
if (!conf) throw new Error(`API Driver: datasource "${key}" not found in datasources.yaml`);
|
|
59
|
+
if (conf.env === 'production' && process.env.SUNGEN_ALLOW_PROD !== '1') {
|
|
60
|
+
throw new Error(`API Driver: datasource "${key}" is env: production — refused (set SUNGEN_ALLOW_PROD=1 to override).`);
|
|
61
|
+
}
|
|
62
|
+
return { key, conf };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run a catalog request and return the response. `req` is embedded at compile time; `params` bind at runtime. */
|
|
66
|
+
async call(
|
|
67
|
+
label: string,
|
|
68
|
+
req: { method: string; path: string; body?: unknown; datasource?: string },
|
|
69
|
+
params: Record<string, any> = {},
|
|
70
|
+
): Promise<{ status: number; ok: boolean; body: any; headers: Record<string, string> }> {
|
|
71
|
+
const { conf } = this.cfg(req.datasource);
|
|
72
|
+
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
73
|
+
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
74
|
+
const url = base + substitute(req.path, params);
|
|
75
|
+
|
|
76
|
+
let body: string | undefined;
|
|
77
|
+
const headers: Record<string, string> = { ...(conf.headers || {}) };
|
|
78
|
+
if (req.body !== undefined && req.body !== null) {
|
|
79
|
+
const resolved = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
80
|
+
body = JSON.stringify(resolved);
|
|
81
|
+
if (!headers['content-type'] && !headers['Content-Type']) headers['content-type'] = 'application/json';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const timer = setTimeout(() => controller.abort(), conf.timeout_ms ?? 15000);
|
|
86
|
+
let res: Response;
|
|
87
|
+
try {
|
|
88
|
+
res = await fetch(url, { method: req.method, headers, body, signal: controller.signal });
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
let parsed: any = text;
|
|
94
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
95
|
+
const outHeaders: Record<string, string> = {};
|
|
96
|
+
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
97
|
+
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const api = new ApiClient();
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.1.2-beta.
|
|
3
|
+
"version": "3.1.2-beta.98",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
|
-
"main": "
|
|
6
|
-
"types": "
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
7
|
"bin": {
|
|
8
8
|
"sungen": "./bin/sungen.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc && npm run copy-templates",
|
|
11
|
+
"build": "rm -rf dist && tsc && npm run copy-templates",
|
|
12
12
|
"copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
|
|
13
|
-
"build:dashboard": "cd dashboard && npm install --silent && npm run build && cd
|
|
13
|
+
"build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
|
|
14
14
|
"dev": "tsx src/cli/index.ts",
|
|
15
|
-
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts",
|
|
15
|
+
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/packaging/run.ts",
|
|
16
16
|
"test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
|
|
17
17
|
"prepublishOnly": "npm run build:dashboard && npm run build"
|
|
18
18
|
},
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"node": ">=18.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@sungen/driver-ui": "3.1.2-beta.98",
|
|
36
37
|
"@anthropic-ai/sdk": "^0.71.0",
|
|
37
38
|
"@babel/parser": "^7.28.5",
|
|
38
39
|
"@babel/traverse": "^7.28.5",
|
|
@@ -52,37 +53,13 @@
|
|
|
52
53
|
"yaml": "^2.8.2",
|
|
53
54
|
"zod": "^4.1.13"
|
|
54
55
|
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@playwright/test": "^1.57.0",
|
|
57
|
-
"@testing-library/jest-dom": "^6.9.1",
|
|
58
|
-
"@testing-library/react": "^16.3.0",
|
|
59
|
-
"@types/jsdom": "^27.0.0",
|
|
60
|
-
"@types/node": "^20",
|
|
61
|
-
"jsdom": "^27.3.0",
|
|
62
|
-
"react": "^19.2.3",
|
|
63
|
-
"react-dom": "^19.2.3",
|
|
64
|
-
"typescript": "^5"
|
|
65
|
-
},
|
|
66
56
|
"peerDependencies": {
|
|
67
57
|
"@playwright/test": "^1.57.0"
|
|
68
58
|
},
|
|
69
|
-
"overrides": {
|
|
70
|
-
"brace-expansion": "^5.0.6",
|
|
71
|
-
"uuid": "^11.1.1",
|
|
72
|
-
"tmp": "^0.2.6",
|
|
73
|
-
"esbuild": "^0.28.1"
|
|
74
|
-
},
|
|
75
|
-
"resolutions": {
|
|
76
|
-
"brace-expansion": "^5.0.6",
|
|
77
|
-
"uuid": "^11.1.1",
|
|
78
|
-
"tmp": "^0.2.6",
|
|
79
|
-
"esbuild": "^0.28.1"
|
|
80
|
-
},
|
|
81
59
|
"files": [
|
|
82
60
|
"dist",
|
|
83
61
|
"bin",
|
|
84
62
|
"src",
|
|
85
|
-
"docs/orchestration-spec.md",
|
|
86
63
|
"README.md",
|
|
87
64
|
"LICENSE"
|
|
88
65
|
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-core capability registrations (Capability SPI).
|
|
3
|
+
*
|
|
4
|
+
* `ui` has moved to `@sungen/driver-ui` (R5.4); `db` and `api` still register from here via
|
|
5
|
+
* `LOCAL_DRIVERS` (they relocate in R5.5/R5.6), and `core` (the generic expect/lint/verification
|
|
6
|
+
* layer) always stays in core. Discovery (`discover.ts`) loads the external driver(s) first, then
|
|
7
|
+
* these, then `core` — preserving the historical pattern composition order, so compiled output is
|
|
8
|
+
* byte-identical (golden + audit snapshots are the proof).
|
|
9
|
+
*/
|
|
10
|
+
import type { CapabilityRegistry } from './registry';
|
|
11
|
+
import type { Sensor, AdvisoryScanInput, GateInput } from './sensor';
|
|
12
|
+
import { lintDataDriven } from '../harness/data-driven-lint';
|
|
13
|
+
import { resolveQuery, lintCatalog } from '../harness/query-catalog';
|
|
14
|
+
import { expectPatterns } from '../generators/test-generator/patterns/expect-patterns';
|
|
15
|
+
|
|
16
|
+
/** Advisory @cases/@query lint, exposed through the Sensor SPI (generate-time). */
|
|
17
|
+
const dataDrivenLintSensor: Sensor<AdvisoryScanInput> = {
|
|
18
|
+
id: 'data-driven-lint',
|
|
19
|
+
capability: 'core',
|
|
20
|
+
kind: 'advisory',
|
|
21
|
+
run: ({ dir, cwd }) =>
|
|
22
|
+
lintDataDriven(dir, cwd).map((w) => ({
|
|
23
|
+
sensorId: 'data-driven-lint',
|
|
24
|
+
capability: 'core',
|
|
25
|
+
scenario: w.scenario,
|
|
26
|
+
message: w.message,
|
|
27
|
+
severity: 'warn' as const,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generic `verification` gate sensor: every referenced named query (`@query`) must resolve in its
|
|
33
|
+
* catalog, and the catalog must lint clean. An unresolved/invalid reference is a gate-level error.
|
|
34
|
+
* (On a project with no `@query` refs it yields nothing.) `query-catalog` lives in core's harness —
|
|
35
|
+
* shared with the data-driven advisory lint. The `@api` counterpart now lives in
|
|
36
|
+
* `@sungen/driver-api`'s `api-verification` gate sensor. Message prefix unchanged → audit byte-identical.
|
|
37
|
+
*/
|
|
38
|
+
const verificationGateSensor: Sensor<GateInput> = {
|
|
39
|
+
id: 'verification',
|
|
40
|
+
capability: 'core',
|
|
41
|
+
kind: 'gate',
|
|
42
|
+
run: ({ screenName, scenarios, cwd }) => {
|
|
43
|
+
const findings = [] as ReturnType<Sensor['run']>;
|
|
44
|
+
const fail = (msg: string) => findings.push({ sensorId: 'verification', capability: 'core', message: `VERIFICATION-FAIL: ${msg}`, severity: 'error' });
|
|
45
|
+
|
|
46
|
+
const queryRefs = new Set<string>();
|
|
47
|
+
for (const s of scenarios) for (const r of s.queryRefs ?? []) queryRefs.add(r);
|
|
48
|
+
for (const name of queryRefs) {
|
|
49
|
+
try { resolveQuery(name, screenName, cwd); } catch (e: any) { fail(e?.message || `query "${name}" does not resolve`); }
|
|
50
|
+
}
|
|
51
|
+
if (queryRefs.size) {
|
|
52
|
+
try { for (const err of lintCatalog(screenName, null, cwd).errors) fail(err); } catch { /* no catalog */ }
|
|
53
|
+
}
|
|
54
|
+
return findings;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* core — generic, capability-agnostic steps (data-vs-data `expect`) + the advisory @cases/@query
|
|
60
|
+
* lint and the gate-level `verification` sensor. Stays in core (not a driver) — the always-present
|
|
61
|
+
* generic layer. All three real capabilities (ui/db/api) now ship as `@sungen/driver-*` packages.
|
|
62
|
+
*/
|
|
63
|
+
export function registerCoreCapability(registry: CapabilityRegistry): void {
|
|
64
|
+
registry.register({
|
|
65
|
+
id: 'core',
|
|
66
|
+
patterns: [...expectPatterns],
|
|
67
|
+
sensors: [dataDrivenLintSensor, verificationGateSensor],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* In-core driver manifest — now empty: `ui`, `db`, and `api` all ship as `@sungen/driver-*` packages
|
|
73
|
+
* loaded by discovery (`discover.ts`). Kept (empty) as the seam for any future in-core capability;
|
|
74
|
+
* `core` itself registers separately via `registerCoreCapability`.
|
|
75
|
+
*/
|
|
76
|
+
export const LOCAL_DRIVERS: ReadonlyArray<{ capability: string; register: (r: CapabilityRegistry) => void }> = [];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated Use `discoverAndRegisterCapabilities()` from `./discover`. Kept as a thin alias so
|
|
80
|
+
* existing call-sites/tests don't break during R5; removed once everything routes through discovery.
|
|
81
|
+
*/
|
|
82
|
+
export function registerBuiltinCapabilities(): void {
|
|
83
|
+
// Lazy import avoids a cycle (discover imports builtins).
|
|
84
|
+
require('./discover').discoverAndRegisterCapabilities();
|
|
85
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextRouter (Capability SPI, R1 — skeleton).
|
|
3
|
+
*
|
|
4
|
+
* The overload guard: for each unit of work it decides the minimal slice of Context + the relevant
|
|
5
|
+
* capabilities + the gate to load, so a project with many drivers doesn't gather everything for
|
|
6
|
+
* every artifact (cost scales by *target*, not *repo × drivers*). See
|
|
7
|
+
* `docs/spec/sungen_capability_spi_spec.md` §5.
|
|
8
|
+
*
|
|
9
|
+
* R1 scope (skeleton, deterministic, not yet enforced in the pipeline): it computes the routing
|
|
10
|
+
* plan — which capabilities apply (the default/implicit one + any whose annotation tags appear),
|
|
11
|
+
* and which sensors gate the task. Scope-trimming/token-budget enforcement is wired in a later step;
|
|
12
|
+
* for now `scope.budget` is a placeholder (null = unbounded) and the plan is advisory.
|
|
13
|
+
*/
|
|
14
|
+
import { capabilityRegistry } from './registry';
|
|
15
|
+
|
|
16
|
+
export interface RouteTask {
|
|
17
|
+
/** What is being worked on now. */
|
|
18
|
+
target: { kind: string; id: string };
|
|
19
|
+
/** What artifact is being produced/checked: 'feature' | 'scenario' | 'selectors' | 'apis.yaml' | … */
|
|
20
|
+
artifact: string;
|
|
21
|
+
/** Capability tags present on the target (e.g. ['@query'], ['@api'], ['@cases']). */
|
|
22
|
+
tags?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RoutePlan {
|
|
26
|
+
/** Applicable capability ids: the default/implicit one + any whose annotations match the tags. */
|
|
27
|
+
capabilities: string[];
|
|
28
|
+
/** Gate sensors to run for this task (ids). */
|
|
29
|
+
gateSensorIds: string[];
|
|
30
|
+
/** Advisory sensors to run for this task (ids). */
|
|
31
|
+
advisorySensorIds: string[];
|
|
32
|
+
/** Scope: the target slice + a token/context budget (null = unbounded for now). */
|
|
33
|
+
scope: { target: RouteTask['target']; budget: number | null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ContextRouter {
|
|
37
|
+
/** Which capabilities a set of tags activates: the default capability + any owning a present tag. */
|
|
38
|
+
capabilitiesFor(tags: string[] = []): string[] {
|
|
39
|
+
const ids = new Set<string>();
|
|
40
|
+
const def = capabilityRegistry.defaultCapabilityId();
|
|
41
|
+
if (def) ids.add(def);
|
|
42
|
+
for (const cap of capabilityRegistry.all()) {
|
|
43
|
+
if ((cap.annotations ?? []).some((a) => tags.includes(a))) ids.add(cap.id);
|
|
44
|
+
}
|
|
45
|
+
return [...ids];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Compute the routing plan for a task (deterministic). */
|
|
49
|
+
route(task: RouteTask): RoutePlan {
|
|
50
|
+
const capabilities = this.capabilitiesFor(task.tags ?? []);
|
|
51
|
+
// Generic sensors (no capability, or the always-on `core`) run regardless of the task's tags;
|
|
52
|
+
// capability-specific sensors run only when their capability is in scope.
|
|
53
|
+
const inScope = (capId?: string) => capId == null || capId === 'core' || capabilities.includes(capId);
|
|
54
|
+
const gateSensorIds = capabilityRegistry.sensors('gate').filter((s) => inScope(s.capability)).map((s) => s.id);
|
|
55
|
+
const advisorySensorIds = capabilityRegistry.sensors('advisory').filter((s) => inScope(s.capability)).map((s) => s.id);
|
|
56
|
+
return {
|
|
57
|
+
capabilities,
|
|
58
|
+
gateSensorIds,
|
|
59
|
+
advisorySensorIds,
|
|
60
|
+
scope: { target: task.target, budget: null },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Process-wide singleton. */
|
|
66
|
+
export const contextRouter = new ContextRouter();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration phase-hook SPIs (Capability SPI, R1 — phase hooks).
|
|
3
|
+
*
|
|
4
|
+
* The kernel owns the pipeline (Plan → Discover → Contextualize → Generate → Gate → Repair →
|
|
5
|
+
* Deliver); a capability fills the domain-specific phases via these hooks:
|
|
6
|
+
* - `DiscoveryProvider` — sources → a slice of the normalized `Context`.
|
|
7
|
+
* - `ContextMapper` — `Context` → generation units + the capability's MODES
|
|
8
|
+
* (ui: screen|flow; api: matrix|flow|async; db: query-catalog).
|
|
9
|
+
*
|
|
10
|
+
* R1 scope: these are **declared** (the contract) and attachable to a CapabilityDescriptor, but
|
|
11
|
+
* the kernel does not consume them yet — the pipeline still runs as before. R2 (driver-ui) is the
|
|
12
|
+
* first conformer that implements them; the orchestration is rewired to call them then.
|
|
13
|
+
* See `docs/spec/sungen_capability_spi_spec.md` §3–§6 and `sungen_ui_driver_spec.md`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Normalized, capability-tagged discovery output. Lazy/sliced — never materialized whole. */
|
|
17
|
+
export interface Context {
|
|
18
|
+
target: { kind: string; id: string; capability?: string };
|
|
19
|
+
/** Lazy source handles (spec/viewpoint/openapi/routes/schema/liveSnapshot/figma/…). */
|
|
20
|
+
sources: Record<string, unknown>;
|
|
21
|
+
/** Capability-specific normalized facts (ui: elements/states/pageType; api: endpoints; db: tables). */
|
|
22
|
+
facts: Record<string, unknown>;
|
|
23
|
+
/** For run-time-connected capabilities (api/db): reachability probe result. */
|
|
24
|
+
connectivity?: { reachable: boolean; baseUrl?: string; probedAt?: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One unit of generation work the kernel will drive through Generate → Gate → Repair. */
|
|
28
|
+
export interface GenerationUnit {
|
|
29
|
+
/** Capability-defined mode: 'screen' | 'flow' | 'matrix' | 'async' | 'query-catalog' | … */
|
|
30
|
+
mode: string;
|
|
31
|
+
/** The slice of Context this unit covers (a screen, an endpoint group, a query set, …). */
|
|
32
|
+
targetSlice: unknown;
|
|
33
|
+
/** Band weight hint (source-aware) — the kernel supplies the band targets. */
|
|
34
|
+
weight?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DiscoveryProvider {
|
|
38
|
+
appliesTo(target: Context['target']): boolean;
|
|
39
|
+
/** Produce this capability's slice of the Context for the given target + requested scope. */
|
|
40
|
+
discover(target: Context['target'], scope: unknown): Promise<Partial<Context>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ContextMapper {
|
|
44
|
+
/** Turn the Context into generation units (+ modes), honouring the kernel's band targets. */
|
|
45
|
+
decompose(ctx: Context, bands?: Record<string, number>): GenerationUnit[];
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability discovery (R5 packaging seam).
|
|
3
|
+
*
|
|
4
|
+
* Core registers capabilities without importing any driver package statically. External
|
|
5
|
+
* `@sungen/driver-*` packages are loaded at runtime via their `sungenDriver.register` entry point;
|
|
6
|
+
* the capabilities still in core (`db`, `api`) register from `LOCAL_DRIVERS`; the generic `core`
|
|
7
|
+
* capability registers last. Idempotent (guards on `registry.isPopulated()`).
|
|
8
|
+
*
|
|
9
|
+
* Ordering is deterministic and behaviour-preserving: external drivers (currently `@sungen/driver-ui`)
|
|
10
|
+
* load first, then `LOCAL_DRIVERS` (db → api), then `core` — i.e. the historical ui → db → api → core
|
|
11
|
+
* composition order, so pattern composition / compiled output is byte-identical.
|
|
12
|
+
*
|
|
13
|
+
* A driver that isn't installed is simply skipped (capabilities are opt-in). Errors thrown *inside* a
|
|
14
|
+
* present driver propagate — only a genuinely absent package is swallowed (resolve-then-require).
|
|
15
|
+
*/
|
|
16
|
+
import { capabilityRegistry } from './registry';
|
|
17
|
+
import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Driver packages relocated out of core, loaded before the in-core `LOCAL_DRIVERS`. As `db`/`api`
|
|
21
|
+
* move to their packages (R5.5/R5.6) they join this list and leave `LOCAL_DRIVERS`; eventually this
|
|
22
|
+
* becomes a scan of installed `@sungen/driver-*` packages (R5.7).
|
|
23
|
+
*/
|
|
24
|
+
const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api'];
|
|
25
|
+
|
|
26
|
+
function loadExternalDriver(name: string): void {
|
|
27
|
+
try {
|
|
28
|
+
require.resolve(name);
|
|
29
|
+
} catch {
|
|
30
|
+
return; // not installed — capabilities are opt-in, so skip silently
|
|
31
|
+
}
|
|
32
|
+
const mod = require(name); // present: let any error thrown inside the driver propagate (don't mask)
|
|
33
|
+
const register: ((r: typeof capabilityRegistry) => void) | undefined = mod?.sungenDriver?.register ?? mod?.register;
|
|
34
|
+
if (typeof register === 'function') register(capabilityRegistry);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function discoverAndRegisterCapabilities(): void {
|
|
38
|
+
if (capabilityRegistry.isPopulated()) return;
|
|
39
|
+
for (const name of EXTERNAL_DRIVERS) loadExternalDriver(name); // ui (external) — first, default capability
|
|
40
|
+
for (const driver of LOCAL_DRIVERS) driver.register(capabilityRegistry); // db, api (in-core for now)
|
|
41
|
+
registerCoreCapability(capabilityRegistry); // generic layer, always present, registered last
|
|
42
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability registry — the extension seam for the microkernel (Capability SPI, R1).
|
|
3
|
+
*
|
|
4
|
+
* Replaces hardcoded `this.patterns.push(...)` wiring with **registration**: each capability
|
|
5
|
+
* (ui · db · api · …) declares what it contributes; the kernel composes it. This is the
|
|
6
|
+
* "shared framework, pull in what you use" mechanism (`docs/spec/sungen_capability_spi_spec.md`).
|
|
7
|
+
*
|
|
8
|
+
* R1 scope (behaviour-preserving): only the **patterns** slice is wired through here — the set of
|
|
9
|
+
* registered patterns is identical to the old hardcoded list, so compiled output is unchanged
|
|
10
|
+
* (golden + audit snapshots are the contract). The remaining SPI slices (discovery, contextMapper,
|
|
11
|
+
* sensors, adapter, viewpoints, runtimeHelpers) are declared on the descriptor for the upcoming
|
|
12
|
+
* steps but are not consumed yet.
|
|
13
|
+
*/
|
|
14
|
+
import type { StepPattern } from '../generators/test-generator/patterns/types';
|
|
15
|
+
import type { Sensor } from './sensor';
|
|
16
|
+
import type { DiscoveryProvider, ContextMapper } from './context';
|
|
17
|
+
|
|
18
|
+
export interface CapabilityDescriptor {
|
|
19
|
+
/** Stable id: 'ui' | 'db' | 'api' | 'core' | … */
|
|
20
|
+
id: string;
|
|
21
|
+
/** The implicit/default capability (a scenario with no capability tag). UI sets this. */
|
|
22
|
+
default?: boolean;
|
|
23
|
+
/** Annotation tags this capability owns (e.g. ['@query'], ['@api','@hybrid']). */
|
|
24
|
+
annotations?: string[];
|
|
25
|
+
/** Step patterns this capability contributes to the compiler. */
|
|
26
|
+
patterns?: StepPattern[];
|
|
27
|
+
/** Harness sensors this capability contributes (advisory and/or gate). */
|
|
28
|
+
sensors?: Sensor[];
|
|
29
|
+
/** Orchestration phase hooks — declared in R1, consumed by the pipeline from R2 onward. */
|
|
30
|
+
discovery?: DiscoveryProvider; // sources → Context slice
|
|
31
|
+
contextMapper?: ContextMapper; // Context → generation units + modes
|
|
32
|
+
/** Provider for this capability's viewpoint catalog (UI: the 17-pattern universal catalog). */
|
|
33
|
+
viewpoints?: () => unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Score-bearing gate computation owned by this capability (UI: viewpoint coverage + assertion
|
|
36
|
+
* depth). The audit engine calls it and assembles the score from the result. Typed generically
|
|
37
|
+
* so the registry stays decoupled from harness internals; the caller knows the concrete shape.
|
|
38
|
+
*/
|
|
39
|
+
gateProvider?: (input: unknown) => unknown;
|
|
40
|
+
/** Runtime helper file(s) this capability emits into specs/ when it is active for a feature
|
|
41
|
+
* (db → specs/db.ts). Emitted via the same sync-when-changed mechanism. */
|
|
42
|
+
runtimeHelpers?: Array<{ file: string; template: string }>;
|
|
43
|
+
/** Optional step detector: the capability is "active" for a feature if any step matches —
|
|
44
|
+
* for steps that carry no annotation tag (db's declarative `User see [t] row where …`). */
|
|
45
|
+
detectsStep?: (stepText: string) => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Precondition codegen for this capability's annotations on a scenario (db's `@query:<name>`
|
|
48
|
+
* → `testData.bind(name, await db.fetchQuery(...))`). Returns raw statements (the compiler
|
|
49
|
+
* indents) + `boundVars` (the `{{name}}` variables it binds, so the compiler registers them as
|
|
50
|
+
* runtime-resolved). Keeps `@query` codegen owned by `db`, not hardcoded in the compiler.
|
|
51
|
+
*/
|
|
52
|
+
preconditionCodegen?: (input: { tags: string[]; screenName: string; cwd: string }) =>
|
|
53
|
+
Array<{ comment?: string; code: string; boundVars?: string[] }>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* CLI commands this capability contributes. The CLI calls each with the commander `program`
|
|
57
|
+
* (typed generically so the registry stays decoupled from commander; the driver knows the shape).
|
|
58
|
+
* Lets a driver own its authoring commands (api → `sungen api import`) instead of hardcoding them
|
|
59
|
+
* in core's CLI. Invoked once, after discovery, in `src/cli/index.ts`.
|
|
60
|
+
*/
|
|
61
|
+
cliCommands?: Array<(program: unknown) => void>;
|
|
62
|
+
|
|
63
|
+
// --- Declared for later R-steps; not consumed yet (kept here so the SPI shape is visible). ---
|
|
64
|
+
// adapter?: string; // codegen adapter id (existing adapterRegistry)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class CapabilityRegistry {
|
|
68
|
+
private caps = new Map<string, CapabilityDescriptor>();
|
|
69
|
+
|
|
70
|
+
/** Register (or replace, by id) a capability descriptor. Idempotent by id. */
|
|
71
|
+
register(descriptor: CapabilityDescriptor): void {
|
|
72
|
+
this.caps.set(descriptor.id, descriptor);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get(id: string): CapabilityDescriptor | undefined {
|
|
76
|
+
return this.caps.get(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True once any capability is registered — discovery guards on this (replaces the builtins boolean). */
|
|
80
|
+
isPopulated(): boolean {
|
|
81
|
+
return this.caps.size > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
all(): CapabilityDescriptor[] {
|
|
85
|
+
return [...this.caps.values()];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The id of the implicit/default capability (UI), if registered. */
|
|
89
|
+
defaultCapabilityId(): string | undefined {
|
|
90
|
+
return this.all().find((c) => c.default)?.id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** All step patterns contributed by registered capabilities (composition order = registration order). */
|
|
94
|
+
patterns(): StepPattern[] {
|
|
95
|
+
return this.all().flatMap((c) => c.patterns ?? []);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** All registered sensors, optionally filtered by kind ('advisory' | 'gate'). */
|
|
99
|
+
sensors(kind?: Sensor['kind']): Sensor[] {
|
|
100
|
+
const all = this.all().flatMap((c) => c.sensors ?? []);
|
|
101
|
+
return kind ? all.filter((s) => s.kind === kind) : all;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Test seam: drop all registrations. */
|
|
105
|
+
_reset(): void {
|
|
106
|
+
this.caps.clear();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Process-wide singleton (mirrors `adapterRegistry`). */
|
|
111
|
+
export const capabilityRegistry = new CapabilityRegistry();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Sensor SPI (Capability SPI, R1 — Sensor step).
|
|
3
|
+
*
|
|
4
|
+
* A sensor is a deterministic harness check a capability contributes. The kernel collects all
|
|
5
|
+
* registered sensors and runs them; findings merge into the harness output. Two kinds:
|
|
6
|
+
* - `advisory` — surfaced as warnings (e.g. at `sungen generate`), never blocks.
|
|
7
|
+
* - `gate` — feeds the audit scorecard / pass-fail (migrated from the hardcoded `audit.ts`
|
|
8
|
+
* calls in the next sub-step; gate sensors carry an intent-aware threshold).
|
|
9
|
+
*
|
|
10
|
+
* Generic over the input so the same registry holds both generate-time advisory sensors
|
|
11
|
+
* (which scan a screen/flow dir) and audit-time gate sensors (which read parsed scenarios) —
|
|
12
|
+
* each sensor declares the input it consumes; callers filter by `kind` and pass the right input.
|
|
13
|
+
*/
|
|
14
|
+
export interface SensorFinding {
|
|
15
|
+
sensorId: string;
|
|
16
|
+
capability?: string;
|
|
17
|
+
scenario?: string;
|
|
18
|
+
message: string;
|
|
19
|
+
severity?: 'info' | 'warn' | 'error';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Sensor<I = unknown> {
|
|
23
|
+
id: string;
|
|
24
|
+
capability?: string;
|
|
25
|
+
kind: 'gate' | 'advisory';
|
|
26
|
+
run(input: I): SensorFinding[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Input for generate-time advisory sensors that scan a screen/flow directory. */
|
|
30
|
+
export interface AdvisoryScanInput {
|
|
31
|
+
dir: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Input for audit-time gate sensors. Intentionally minimal/structural (not the full ScenarioInfo
|
|
37
|
+
* type) so `src/capabilities` doesn't depend on harness internals — the audit caller passes the
|
|
38
|
+
* parsed scenarios it already has.
|
|
39
|
+
*/
|
|
40
|
+
export interface GateInput {
|
|
41
|
+
screenName: string;
|
|
42
|
+
cwd: string;
|
|
43
|
+
featureText: string;
|
|
44
|
+
scenarios: Array<{ name: string; queryRefs?: string[]; apiRefs?: string[] }>;
|
|
45
|
+
/** UI: universal-viewpoint theme gaps the coverage gate found (generic string list). */
|
|
46
|
+
universalGaps?: string[];
|
|
47
|
+
}
|
|
@@ -4,7 +4,8 @@ import * as fs from 'fs';
|
|
|
4
4
|
import { CodeGenerator } from '../../generators/test-generator/code-generator';
|
|
5
5
|
import { adapterRegistry } from '../../generators/test-generator/adapters';
|
|
6
6
|
import { scanTestDataSecrets } from '../../harness/secret-scan';
|
|
7
|
-
import {
|
|
7
|
+
import { capabilityRegistry } from '../../capabilities/registry';
|
|
8
|
+
import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
|
|
8
9
|
import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -183,8 +184,11 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
183
184
|
console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
//
|
|
187
|
-
|
|
187
|
+
// Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
|
|
188
|
+
// never block generation. Each capability registers its own; the kernel just runs them.
|
|
189
|
+
discoverAndRegisterCapabilities();
|
|
190
|
+
const advisorySensors = capabilityRegistry.sensors('advisory');
|
|
191
|
+
const ddWarnings = scanDirs.flatMap((d) => advisorySensors.flatMap((s) => s.run({ dir: d, cwd })));
|
|
188
192
|
if (ddWarnings.length) {
|
|
189
193
|
console.log(`\n⚠️ Data-driven lint (@cases / @query) — review:`);
|
|
190
194
|
for (const w of ddWarnings.slice(0, 20)) console.log(` ${w.scenario ? w.scenario + ': ' : ''}${w.message}`);
|
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { registerChallengeCommand } from './commands/challenge';
|
|
|
26
26
|
import { registerBlindspotCommand } from './commands/blindspot';
|
|
27
27
|
import { registerCapabilityCommand } from './commands/capability';
|
|
28
28
|
import { registerFlowCheckCommand } from './commands/flow-check';
|
|
29
|
+
import { capabilityRegistry } from '../capabilities/registry';
|
|
30
|
+
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
29
31
|
|
|
30
32
|
// Read version from package.json so `--version` never drifts from the released version.
|
|
31
33
|
const { version } = require('../../package.json') as { version: string };
|
|
@@ -42,7 +44,7 @@ async function main() {
|
|
|
42
44
|
program
|
|
43
45
|
.option('-v, --verbose', 'Enable verbose logging');
|
|
44
46
|
|
|
45
|
-
// Register commands
|
|
47
|
+
// Register commands
|
|
46
48
|
registerInitCommand(program);
|
|
47
49
|
registerAddCommand(program);
|
|
48
50
|
registerGenerateCommand(program);
|
|
@@ -65,6 +67,13 @@ async function main() {
|
|
|
65
67
|
registerIngestCommand(program);
|
|
66
68
|
registerEvalCommand(program);
|
|
67
69
|
|
|
70
|
+
// Capability-contributed CLI commands (Capability SPI): drivers own their authoring commands
|
|
71
|
+
// (e.g. @sungen/driver-api → `sungen api import`). Discover, then register each.
|
|
72
|
+
discoverAndRegisterCapabilities();
|
|
73
|
+
for (const cap of capabilityRegistry.all()) {
|
|
74
|
+
for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
await program.parseAsync(process.argv);
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -64,7 +64,7 @@ export interface TestGeneratorAdapter {
|
|
|
64
64
|
// Template rendering methods
|
|
65
65
|
renderTestFile(data: TestFileData): string;
|
|
66
66
|
renderScenario(data: ScenarioData): string;
|
|
67
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string;
|
|
67
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string;
|
|
68
68
|
renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
69
69
|
renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
70
70
|
renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
|
|
|
26
26
|
return this.templateEngine.renderScenario(data);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
29
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|