@sun-asterisk/sungen 3.1.1 → 3.1.2-beta.101
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/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +4 -1
- package/dist/exporters/spec-parser.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 +18 -9
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +76 -119
- 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-cmd-create-test.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -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 +4 -3
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -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/exporters/spec-parser.ts +4 -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 +71 -118
- 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-cmd-create-test.md +4 -3
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -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 +4 -3
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -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,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.api = void 0;
|
|
37
|
+
/* eslint-disable */
|
|
38
|
+
/**
|
|
39
|
+
* Sungen API Driver — runtime helper (auto-generated into specs/api.ts).
|
|
40
|
+
*
|
|
41
|
+
* Runs a catalog-defined HTTP request and returns { status, ok, body, headers } — bound to a
|
|
42
|
+
* `{{name}}` variable by the `@api:<name>` annotation, asserted with `expect {{name.status}} …` /
|
|
43
|
+
* `{{name.body.<path>}}`. Base URL + auth come from a `kind: api` datasource in datasources.yaml,
|
|
44
|
+
* with `${VAR}` resolved from .env.qa / process.env — never inline.
|
|
45
|
+
*
|
|
46
|
+
* Safety: a datasource flagged `env: production` is refused unless SUNGEN_ALLOW_PROD=1.
|
|
47
|
+
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
48
|
+
*/
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
function loadEnvQa() {
|
|
52
|
+
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
53
|
+
const p = path.join(process.cwd(), name);
|
|
54
|
+
if (!name.endsWith('.') && fs.existsSync(p)) {
|
|
55
|
+
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
|
|
56
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
57
|
+
if (m && process.env[m[1]] === undefined)
|
|
58
|
+
process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function loadConfig() {
|
|
64
|
+
loadEnvQa();
|
|
65
|
+
const file = [path.join(process.cwd(), 'datasources.yaml'), path.join(process.cwd(), 'qa', 'datasources.yaml')].find((f) => fs.existsSync(f));
|
|
66
|
+
if (!file)
|
|
67
|
+
throw new Error('API Driver: no datasources.yaml found (project root or qa/).');
|
|
68
|
+
const raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
|
|
69
|
+
const { parse } = require('yaml');
|
|
70
|
+
const doc = parse(raw) || {};
|
|
71
|
+
return doc.datasources || {};
|
|
72
|
+
}
|
|
73
|
+
function substitute(text, params) {
|
|
74
|
+
return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
|
|
75
|
+
}
|
|
76
|
+
class ApiClient {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.configs = null;
|
|
79
|
+
}
|
|
80
|
+
cfg(name) {
|
|
81
|
+
if (!this.configs)
|
|
82
|
+
this.configs = loadConfig();
|
|
83
|
+
const key = name || Object.keys(this.configs).find((k) => (this.configs[k].kind || 'api') === 'api') || Object.keys(this.configs)[0];
|
|
84
|
+
const conf = this.configs[key];
|
|
85
|
+
if (!conf)
|
|
86
|
+
throw new Error(`API Driver: datasource "${key}" not found in datasources.yaml`);
|
|
87
|
+
if (conf.env === 'production' && process.env.SUNGEN_ALLOW_PROD !== '1') {
|
|
88
|
+
throw new Error(`API Driver: datasource "${key}" is env: production — refused (set SUNGEN_ALLOW_PROD=1 to override).`);
|
|
89
|
+
}
|
|
90
|
+
return { key, conf };
|
|
91
|
+
}
|
|
92
|
+
/** Run a catalog request and return the response. `req` is embedded at compile time; `params` bind at runtime. */
|
|
93
|
+
async call(label, req, params = {}) {
|
|
94
|
+
const { conf } = this.cfg(req.datasource);
|
|
95
|
+
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
96
|
+
if (!base)
|
|
97
|
+
throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
98
|
+
const url = base + substitute(req.path, params);
|
|
99
|
+
let body;
|
|
100
|
+
const headers = { ...(conf.headers || {}) };
|
|
101
|
+
if (req.body !== undefined && req.body !== null) {
|
|
102
|
+
const resolved = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
103
|
+
body = JSON.stringify(resolved);
|
|
104
|
+
if (!headers['content-type'] && !headers['Content-Type'])
|
|
105
|
+
headers['content-type'] = 'application/json';
|
|
106
|
+
}
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), conf.timeout_ms ?? 15000);
|
|
109
|
+
let res;
|
|
110
|
+
try {
|
|
111
|
+
res = await fetch(url, { method: req.method, headers, body, signal: controller.signal });
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
const text = await res.text();
|
|
117
|
+
let parsed = text;
|
|
118
|
+
try {
|
|
119
|
+
parsed = text ? JSON.parse(text) : null;
|
|
120
|
+
}
|
|
121
|
+
catch { /* non-JSON → keep text */ }
|
|
122
|
+
const outHeaders = {};
|
|
123
|
+
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
124
|
+
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.api = new ApiClient();
|
|
128
|
+
//# sourceMappingURL=specs-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.
|
|
3
|
+
"version": "3.1.2-beta.101",
|
|
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",
|
|
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.101",
|
|
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
|
+
}
|