@sungen/driver-api 3.1.2-beta.116 → 3.1.2-beta.117
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/package.json +2 -2
- package/src/api-harness.ts +135 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sungen/driver-api",
|
|
3
|
-
"version": "3.1.2-beta.
|
|
3
|
+
"version": "3.1.2-beta.117",
|
|
4
4
|
"description": "Sungen API capability — the @api annotation, API catalog, OpenAPI import + `sungen api import`, and the specs/api.ts runtime template. Plugs into @sun-asterisk/sungen via the capability SPI.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"author": "eqe team (engineer & quality) — Sun Asterisk",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@sun-asterisk/sungen": "3.1.2-beta.
|
|
16
|
+
"@sun-asterisk/sungen": "3.1.2-beta.117",
|
|
17
17
|
"commander": "^14.0.2",
|
|
18
18
|
"yaml": "^2.8.2"
|
|
19
19
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API harness — the `api` capability's score-bearing gate + viewpoint catalog (AO-2).
|
|
3
|
+
*
|
|
4
|
+
* The audit engine is capability-routed (AO-1): for an `api/<area>` unit it calls THIS capability's
|
|
5
|
+
* `viewpoints()` + `gateProvider()` instead of the UI ones, then assembles the score with the same
|
|
6
|
+
* `0.4·coverage + 0.3·businessDepth + 0.15·balance + 0.15·trace` formula. We compute API-native
|
|
7
|
+
* coverage + depth here (not the UI keyword model), so an API area is scored on what matters for an
|
|
8
|
+
* API: contract + failure coverage, and — the headline — `businessDepth`: do the business-critical
|
|
9
|
+
* (mutating) scenarios PROVE THE EFFECT (assert body / DB side-effect / concurrency invariant), or
|
|
10
|
+
* just the status?
|
|
11
|
+
*
|
|
12
|
+
* Type-only on core (like the rest of the driver) — the depth threshold + project context arrive via
|
|
13
|
+
* the gate input, so loading the driver never forces a core runtime resolve.
|
|
14
|
+
*/
|
|
15
|
+
import type { Catalog, GateResult, DepthResult } from '@sun-asterisk/sungen';
|
|
16
|
+
import { resolveApi } from './api-catalog';
|
|
17
|
+
|
|
18
|
+
/** A scenario as the audit parses it (the subset the API gate reads). */
|
|
19
|
+
interface ApiScenario {
|
|
20
|
+
name: string;
|
|
21
|
+
apiRefs?: string[];
|
|
22
|
+
queryRefs?: string[];
|
|
23
|
+
casesDataset?: string;
|
|
24
|
+
stepsText?: string; // lowercase steps text
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ApiGateInput {
|
|
28
|
+
scenarios: ApiScenario[];
|
|
29
|
+
focus: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
screenName: string; // catalog-resolution id, e.g. `api/<area>`
|
|
32
|
+
threshold: number; // required businessDepth for the focus (from core)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MUTATING = /^(POST|PUT|PATCH|DELETE)$/;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The API viewpoint catalog (display + the project-override seam). The gate computes coverage from
|
|
39
|
+
* the live `apis.yaml`, so this is intentionally light — one universal theme per viewpoint category.
|
|
40
|
+
*/
|
|
41
|
+
export function apiViewpoints(): Catalog {
|
|
42
|
+
const theme = (t: string, keywords: string[]) => ({ theme: t, keywords });
|
|
43
|
+
return {
|
|
44
|
+
page_types: {
|
|
45
|
+
// method-profiles (the API analog of UI page-types) — documented; the gate enforces them.
|
|
46
|
+
read: { detect_keywords: ['get', 'list', 'fetch'], must_cover: [theme('contract', ['status', 'body'])] },
|
|
47
|
+
mutating: {
|
|
48
|
+
detect_keywords: ['create', 'update', 'delete', 'post', 'put', 'patch'],
|
|
49
|
+
must_cover: [theme('contract', ['status', 'body']), theme('error', ['4', '5', 'invalid', 'cases'])],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
universal: [theme('contract', ['status']), theme('error', ['4', '5'])],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Score-bearing API gate: coverage (must-cover viewpoints per endpoint, by method-profile) + depth
|
|
58
|
+
* (mutating scenarios that prove the effect). Returns the same `{gate, depth}` shape the audit
|
|
59
|
+
* assembles into the score.
|
|
60
|
+
*/
|
|
61
|
+
export function apiGateProvider(input: ApiGateInput): { gate: GateResult; depth: DepthResult } {
|
|
62
|
+
const { scenarios, focus, cwd, screenName, threshold } = input;
|
|
63
|
+
const apiScenarios = scenarios.filter((s) => (s.apiRefs ?? []).length);
|
|
64
|
+
|
|
65
|
+
// Resolve each referenced endpoint's HTTP method (best-effort; unresolved → the verification
|
|
66
|
+
// sensor already fails the gate, so we just skip it here).
|
|
67
|
+
const methodOf = new Map<string, string>();
|
|
68
|
+
for (const s of apiScenarios) {
|
|
69
|
+
for (const name of s.apiRefs ?? []) {
|
|
70
|
+
if (methodOf.has(name)) continue;
|
|
71
|
+
try { methodOf.set(name, resolveApi(name, screenName, cwd).method); } catch { /* unresolved */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const isMutating = (name: string) => MUTATING.test(methodOf.get(name) ?? '');
|
|
75
|
+
|
|
76
|
+
// ── Coverage: must-cover viewpoints per resolved endpoint ──────────────────────────────────
|
|
77
|
+
// read (GET/HEAD): {contract}; mutating: {contract, error}. (auth/idempotency/side-effect are
|
|
78
|
+
// advisory via the api-viewpoints sensor + rewarded through businessDepth, not coverage-gating.)
|
|
79
|
+
const asserts = (s: ApiScenario, endpoint: string, vp: 'contract' | 'error'): boolean => {
|
|
80
|
+
if (!(s.apiRefs ?? []).includes(endpoint)) return false;
|
|
81
|
+
const t = s.stepsText ?? '';
|
|
82
|
+
if (vp === 'contract') return new RegExp(`\\b${endpoint.toLowerCase()}\\.(status|ok|body|headers|ok_count|status_counts)\\b`).test(t);
|
|
83
|
+
return !!s.casesDataset || /\b[45]\d\d\b/.test(t); // error: a @cases matrix or an asserted 4xx/5xx
|
|
84
|
+
};
|
|
85
|
+
const gaps: GateResult['gaps'] = [];
|
|
86
|
+
let covered = 0;
|
|
87
|
+
let themesTotal = 0;
|
|
88
|
+
for (const endpoint of methodOf.keys()) {
|
|
89
|
+
const vps: ('contract' | 'error')[] = isMutating(endpoint) ? ['contract', 'error'] : ['contract'];
|
|
90
|
+
for (const vp of vps) {
|
|
91
|
+
themesTotal++;
|
|
92
|
+
if (apiScenarios.some((s) => asserts(s, endpoint, vp))) covered++;
|
|
93
|
+
else gaps.push({ theme: `${endpoint}:${vp}`, keywords: [], status: 'missing' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const coverageRatio = themesTotal ? covered / themesTotal : 0;
|
|
97
|
+
const area = screenName.replace(/^api\//, '');
|
|
98
|
+
const gate: GateResult = {
|
|
99
|
+
pageType: `api:${area}`,
|
|
100
|
+
themesTotal,
|
|
101
|
+
themesCovered: covered,
|
|
102
|
+
coverageRatio,
|
|
103
|
+
gaps,
|
|
104
|
+
universalGaps: [],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ── businessDepth: the SUCCESS path of a mutation must PROVE THE EFFECT, not just assert status ──
|
|
108
|
+
// An error scenario (a @cases matrix, or one asserting a 4xx/5xx) proves the failure CONTRACT —
|
|
109
|
+
// the status IS its correct assertion (the API analog of UI NAV), so it is not depth-required.
|
|
110
|
+
const errorLike = (s: ApiScenario) => !!s.casesDataset || /\bis\s+["']?[45]\d\d\b/.test(s.stepsText ?? '');
|
|
111
|
+
const depthRequired = apiScenarios.filter((s) => (s.apiRefs ?? []).some(isMutating) && !errorLike(s));
|
|
112
|
+
const isDeep = (s: ApiScenario): boolean => {
|
|
113
|
+
const t = s.stepsText ?? '';
|
|
114
|
+
return /\b[a-z_][a-z0-9_]*\.body\b/.test(t) // asserts a response body field
|
|
115
|
+
|| /\b[a-z_][a-z0-9_]*\.(ok_count|status_counts)\b/.test(t) // a concurrency invariant
|
|
116
|
+
|| (s.queryRefs ?? []).length > 0; // a @query DB side-effect cross-check
|
|
117
|
+
};
|
|
118
|
+
const reqShallow = depthRequired.filter((s) => !isDeep(s));
|
|
119
|
+
const bcDepthRatio = depthRequired.length ? (depthRequired.length - reqShallow.length) / depthRequired.length : 1;
|
|
120
|
+
const verdict: DepthResult['verdict'] = bcDepthRatio < threshold ? (focus === 'smoke' ? 'warn' : 'fail') : 'pass';
|
|
121
|
+
const depth: DepthResult = {
|
|
122
|
+
total: apiScenarios.length,
|
|
123
|
+
shallowTotal: reqShallow.length,
|
|
124
|
+
shallowRatio: apiScenarios.length ? reqShallow.length / apiScenarios.length : 0,
|
|
125
|
+
businessCriticalTotal: depthRequired.length,
|
|
126
|
+
businessCriticalShallow: reqShallow.length,
|
|
127
|
+
bcDepthRatio,
|
|
128
|
+
shallowBusinessCritical: reqShallow.map((s) => ({ name: s.name, category: 'API' })),
|
|
129
|
+
focus,
|
|
130
|
+
threshold,
|
|
131
|
+
verdict,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return { gate, depth };
|
|
135
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { CapabilityRegistry, Sensor, GateInput } from '@sun-asterisk/sungen';
|
|
20
20
|
import { resolveApi, apiParamNames, lintApiCatalog } from './api-catalog';
|
|
21
21
|
import { registerApiCommand } from './cli-import';
|
|
22
|
+
import { apiViewpoints, apiGateProvider } from './api-harness';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Parse `name(a={{x}},b="lit",c=3)` annotation overrides → JS expressions. Inlined from core
|
|
@@ -184,6 +185,8 @@ export function register(registry: CapabilityRegistry): void {
|
|
|
184
185
|
runtimeHelpers: [{ file: 'api.ts', template: 'specs-api.ts' }], // template resolved from core's templates dir
|
|
185
186
|
preconditionCodegen: apiPreconditionCodegen, // @api:<name> → bind {{name}}
|
|
186
187
|
sensors: [apiVerificationSensor, apiViewpointSensor],
|
|
188
|
+
viewpoints: apiViewpoints as () => unknown, // AO-2: API viewpoint catalog
|
|
189
|
+
gateProvider: apiGateProvider as (i: unknown) => unknown, // AO-2: API coverage + businessDepth
|
|
187
190
|
cliCommands: [registerApiCommand as (program: unknown) => void], // `sungen api import <openapi>`
|
|
188
191
|
});
|
|
189
192
|
}
|