@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sungen/driver-api",
3
- "version": "3.1.2-beta.116",
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.116",
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
  }