@suluk/cockpit 0.1.16 → 0.2.0

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <h1 align="center">@suluk/cockpit</h1>
8
8
 
9
- <p align="center"><b>The pure cockpit core (cycle model · builder model · codegen · deploy planning · validate/audit/preview) shared by the vscode extension and the /superadmin web admin panel.</b></p>
9
+ <p align="center"><b>The pure cockpit core — one brain, two faces: the cycle model, the builder model, codegen, deploy planning, and the validate/audit/preview/converge helpers shared by the VS Code extension and the <code>/superadmin</code> web panel.</b></p>
10
10
 
11
11
  <p align="center">
12
12
  <em>Part of <a href="https://github.com/MahmoodKhalil57/suluk">Suluk</a> — one typed OpenAPI v4 contract projecting into every full-stack layer.</em>
@@ -24,11 +24,168 @@
24
24
  bun add @suluk/cockpit
25
25
  ```
26
26
 
27
- ## The Suluk cycle
27
+ ## What it does
28
28
 
29
- `@suluk/cockpit` is one station on the Suluk walk author one v4 source, then **validate · audit ·
30
- preview · generate · deploy** the whole stack from it. Explore the full toolchain in the
31
- [main repository](https://github.com/MahmoodKhalil57/suluk) or drive it from the [VS Code cockpit](https://marketplace.visualstudio.com/items?itemName=MahmoodKhalil.suluk-vscode).
29
+ Given **one** v4 "Suluk" document, the cockpit computes every view a developer tool needs over it as
30
+ **pure functions, no host API**. The VS Code extension (`suluk-vscode`) and the web admin panel (`@suluk/admin`)
31
+ both consume this exact core, so a projection is never reimplemented in a shell.
32
+
33
+ - **The cycle model** — `buildCycle` projects the document into a layered view (data · contract · auth · cost ·
34
+ docs · state · UI · providers · tests). Each layer is a *projection* of the same source, so you can read the
35
+ lineage at a glance. The model is also a function of the requesting principal — pass scopes and gated
36
+ operations drop out.
37
+ - **Validate / audit / preview** — `validateSource` (meta-schema), `auditSource` (doc-coverage findings), and
38
+ `previewHtml` (a self-contained Scalar or Swagger page) over raw source text.
39
+ - **The builder model + codegen** — `buildBuilderModel` walks the tiered composition (pages → sections → blocks →
40
+ components); `generateForm` / `generateTable` / `generateAppFiles` land real TSX + a shadcn registry.
41
+ - **Lifecycle & coherence** — `contractGates` + `shipSummary` (the "are you ready to ship?" checklist),
42
+ `convergeContract` (cross-cutting contradictions a clean merge leaves behind), `diffContracts` (local-vs-deployed
43
+ drift), `crossCut` (one contract refracted through every viewer), `contractToD2` (ERD / cycle / operation diagrams).
44
+ - **Deploy planning** — `deployPlan` / `deployMarkdown` turn the contract into a Cloudflare plan + a `DEPLOY.md`.
45
+ The cockpit **plans**; it never runs `wrangler` and holds no credentials.
46
+
47
+ ## When to reach for it
48
+
49
+ - You are building **another shell** over the Suluk contract (an editor integration, a CI gate, a dashboard) and
50
+ want the same brain the VS Code extension and `/superadmin` panel use — not a re-derivation.
51
+ - You need a **single, principal-aware projection** of a v4 document: what entities, operations, costs, providers
52
+ and gated surfaces it implies, plus its ship-readiness and coherence.
53
+
54
+ When **not** to: if you only need *one* projection in isolation, depend on it directly — `@suluk/scalar` /
55
+ `@suluk/swagger` for docs HTML, `@suluk/shadcn` for forms/tables, `@suluk/builder` for the app graph, `@suluk/deploy`
56
+ for the plan. The cockpit's value is **composing** them into the tool views above; reach past it when you don't need that.
57
+
58
+ ## Usage
59
+
60
+ ### The cycle model (the spine)
61
+
62
+ ```ts
63
+ import { parseDocument } from "@suluk/core";
64
+ import { buildCycle, cycleSummary } from "@suluk/cockpit";
65
+
66
+ const doc = parseDocument(source); // a v4 "Suluk" document
67
+ const model = buildCycle(doc);
68
+
69
+ model.valid; // passes the v4 meta-schema?
70
+ model.coverage; // documentation coverage 0..1
71
+ cycleSummary(model);
72
+ // → [{ layer: "Data (entities)", summary: "3 entities", status: "ok" }, … ]
73
+
74
+ // Project for a principal — scope-gated operations they can't reach drop out of every layer:
75
+ const asViewer = buildCycle(doc, { principal: { scopes: ["read:pets"] } });
76
+ ```
77
+
78
+ ### Validate · audit · preview (over raw source text)
79
+
80
+ ```ts
81
+ import { validateSource, auditSource, previewHtml } from "@suluk/cockpit";
82
+
83
+ const { ok, diagnostics } = validateSource(yamlOrJsonText);
84
+ const { findings } = auditSource(yamlOrJsonText); // under-documented routes
85
+ const { html } = previewHtml(yamlOrJsonText, "scalar"); // or "swagger" — a self-contained page
86
+ ```
87
+
88
+ ### Ship-readiness + coherence
89
+
90
+ ```ts
91
+ import { contractGates, shipSummary, convergeContract } from "@suluk/cockpit";
92
+ import type { Baseline } from "@suluk/visual";
93
+
94
+ const baseline: Baseline = {}; // the pixel-confidence baseline (empty ⇒ not yet verified)
95
+ const gates = contractGates(doc, baseline);
96
+ const { ready, line } = shipSummary(gates);
97
+ // → { ready: false, line: "1 blocker · 1 to do · 3/5 pass" }
98
+
99
+ const report = convergeContract(doc); // cross-cutting contradictions
100
+ report.clean; // true ⇒ no dangling refs / undeclared schemes / orphan scopes / empty paths
101
+ ```
102
+
103
+ ### Drift, cross-cut, diagrams
104
+
105
+ ```ts
106
+ import { diffContracts, crossCut, defaultViewers, contractToD2 } from "@suluk/cockpit";
107
+
108
+ // "what's drifted in prod" — your LOCAL contract vs the DEPLOYED /openapi.json
109
+ diffContracts(local, deployed).summary; // "1+ 0- 2~ ops · 1+ 0- 0~ schemas" | "in sync — local matches deployed"
110
+
111
+ // one contract refracted through every viewer — the scope-gated surface
112
+ crossCut(doc, defaultViewers(doc)).gated; // operations not visible to every viewer
113
+
114
+ // another projection: D2 diagram source ("erd" | "cycle" | "operations")
115
+ const d2 = contractToD2(doc, "erd"); // render with the d2 CLI / playground / kroki.io
116
+ ```
117
+
118
+ ### Codegen + deploy planning (the actions that land artifacts)
119
+
120
+ ```ts
121
+ import { generateForm, generateAppFiles, deployPlan, deployMarkdown } from "@suluk/cockpit";
122
+
123
+ const formTsx = generateForm(doc, "Pet"); // a shadcn form component (TSX) for an entity
124
+ const files = generateAppFiles(doc); // [{ path, content }] — openapi.json, components, pages, registry, diagrams
125
+
126
+ const plan = deployPlan(doc); // a Cloudflare DeployPlan
127
+ const md = deployMarkdown(plan); // a DEPLOY.md the user follows (Suluk never runs wrangler)
128
+ ```
129
+
130
+ ### Modules — install a contract fragment, re-project for free
131
+
132
+ ```ts
133
+ import { installModule, ECOMMERCE, buildCycle } from "@suluk/cockpit";
134
+
135
+ const { doc: merged } = installModule(host, ECOMMERCE); // ECOMMERCE | CRM | BILLING are first-party fragments
136
+ buildCycle(merged); // every layer now includes the module's entities, operations, cost and provider slots
137
+ ```
138
+
139
+ ### Agents (OBSERVE-only)
140
+
141
+ ```ts
142
+ import { agentsView, agentsSummary } from "@suluk/cockpit";
143
+
144
+ const view = agentsView(doc); // the x-suluk-agents tier tree, effective scope, gate findings, reachable surface
145
+ agentsSummary(view); // a one-line digest — read-only; agent execution + secrets live OUTSIDE the cockpit
146
+ ```
147
+
148
+ ## API
149
+
150
+ The package exposes a single entry point (`@suluk/cockpit`). The core groups:
151
+
152
+ | Export | What it does |
153
+ | --- | --- |
154
+ | `buildCycle`, `cycleSummary`, `docChecks` | the layered cycle model (principal-aware) + doc-level contract checks |
155
+ | `validateSource`, `auditSource`, `previewHtml`, `looksLikeV4` | validate / audit / preview over raw source text |
156
+ | `buildBuilderModel`, `builderTree`, `entitiesFromDoc`, `generateAppFiles`, `generateRegistryJson` | the tiered builder model + the app/registry generators |
157
+ | `entityNames`, `generateForm`, `generateTable`, `generateStoresModule`, `exportV4Json` | per-entity codegen + the canonical JSON export |
158
+ | `contractGates`, `shipSummary` | the contract-level ship-readiness gates + a one-line summary |
159
+ | `convergeContract` | a coherence audit (dangling refs / undeclared schemes / orphan scopes / preview backdoors) |
160
+ | `diffContracts`, `canonical` | local-vs-deployed contract drift |
161
+ | `crossCut`, `documentScopes`, `defaultViewers`, `previewRoles`, `previewAllowedRoles`, `previewLaunchUrl` | the per-viewer / role-preview security surface |
162
+ | `contractToD2`, `diagramViews` | D2 diagram source (ERD / cycle / operations) |
163
+ | `componentReport`, `approveComponents`, `primitiveCss` | UI-primitive decomposition + pixel-confidence (surfaces `@suluk/visual`) |
164
+ | `deployPlan`, `deployMarkdown`, `previewDeployPlan`, `previewDeployMarkdown` | Cloudflare deploy planning + the rendered `DEPLOY.md` |
165
+ | `agentsView`, `agentsSummary` | the `x-suluk-agents` OBSERVE view (tier tree · scope · context · model selection) |
166
+ | `installModule`, `namespaceModule`, `previewInstall`, `gradeModule`, `composeModules`, `planComposition` | install / compose contract-fragment modules into the hub doc |
167
+ | `ECOMMERCE`, `CRM`, `BILLING`, `FIRST_PARTY_REGISTRY`, `PROVIDER_CATALOG`, `STACK_TEMPLATES` | the first-party module + provider + stack-template catalogs |
168
+ | `providerFacets`, `readProviders`, `swapProvider`, `parseRegistry`, `validateModule`, `resolveTemplate` | provider-slot + registry + template helpers (re-exported from `@suluk/builder`) |
169
+ | `formatMicroUsd`, `summarize` | cost formatting (re-exported from `@suluk/cost`) for a live ledger |
170
+
171
+ Every function is pure and typed against `OpenAPIv4Document` from `@suluk/core`; the type exports (`CycleModel`,
172
+ `Gate`, `ConvergeReport`, `ContractDiff`, `CrossCut`, `AgentsView`, …) ride alongside each group.
173
+
174
+ ## Boundary
175
+
176
+ The cockpit is **L3 — it renders and generates, never hosts** (the C023 line). It is pure logic with no host API:
177
+ it returns models, strings and file lists; the *shell* writes files, fetches the deployed `/openapi.json`, opens
178
+ terminals and renders webviews. Two consequences:
179
+
180
+ - **No credentials seam.** Deploy planning emits a `DEPLOY.md` and the plan; it never runs `wrangler` or touches a
181
+ token — `wrangler login`'s OAuth happens in *your* terminal so credentials never reach Suluk (C020). Role-preview
182
+ links are computed (`previewLaunchUrl`) but the cockpit never mints or holds a session.
183
+ - **Agents are OBSERVE-only.** `agentsView` derives the static tier tree, effective scope, gate findings and a
184
+ *projection preview* (file/tool **names**). It never executes an agent, fetches a preprompt, or reads a secret.
185
+
186
+ Inject the host concerns (the bytes to fetch, the deployed document to diff against, the baseline to approve at);
187
+ keep the projections here. To contribute a new view, add a pure function over `OpenAPIv4Document` and let both
188
+ shells render it.
32
189
 
33
190
  ## License
34
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/cockpit",
3
- "version": "0.1.16",
3
+ "version": "0.2.0",
4
4
  "description": "The pure cockpit core (cycle model · builder model · codegen · deploy planning · validate/audit/preview) shared by the vscode extension and the /superadmin web admin panel. CANDIDATE tooling.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -19,16 +19,17 @@
19
19
  ".": "./src/index.ts"
20
20
  },
21
21
  "dependencies": {
22
- "@suluk/core": "^0.1.7",
23
- "@suluk/hono": "^0.1.2",
24
- "@suluk/scalar": "^0.1.2",
22
+ "@suluk/core": "^0.1.13",
23
+ "@suluk/hono": "^0.1.5",
24
+ "@suluk/scalar": "^0.8.0",
25
25
  "@suluk/swagger": "^0.1.2",
26
26
  "@suluk/shadcn": "^0.1.2",
27
27
  "@suluk/builder": "^0.1.11",
28
- "@suluk/deploy": "^0.1.3",
29
- "@suluk/cost": "^0.1.2",
28
+ "@suluk/deploy": "^0.1.4",
29
+ "@suluk/cost": "^0.2.0",
30
+ "@suluk/harden": "^0.2.0",
30
31
  "@suluk/visual": "^0.1.3",
31
- "@suluk/agents": "^0.1.0"
32
+ "@suluk/agents": "^0.1.6"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/bun": "latest"
package/src/agents.ts CHANGED
@@ -50,7 +50,7 @@ export interface AgentNodeView {
50
50
  context: AgentContextLoad;
51
51
  /** per-skill model pick (C027 × @suluk/models) — present only when agentsView is given a catalog. OBSERVE-only:
52
52
  * "why this model" (declared vs selected, top ids, deciding preference, UNKNOWN-coverage gaps). Never executes. */
53
- modelSelection?: { skill: string; from: "declared" | "selected"; ids: string[]; decidingPreference?: string; coverageGaps?: string[] }[];
53
+ modelSelection?: { skill: string; from?: "declared" | "selected"; ids?: string[]; resolve?: "pinned" | "router" | "latest"; pickPinned?: boolean; decidingPreference?: string; coverageGaps?: string[]; error?: string }[];
54
54
  }
55
55
  /** The agent-declared vs operator-effective diff + the cost three-number (cap / estimate / actual). Read-only. */
56
56
  export interface AgentGovernedView {
@@ -163,12 +163,14 @@ export function agentsView(doc: OpenAPIv4Document, opts: { catalog?: ModelCatalo
163
163
  ...(opts.catalog ? {
164
164
  modelSelection: Object.keys(a.skills ?? {}).sort().map((sk) => {
165
165
  const minWin = cr.loads.find((l) => l.agent === name)?.minWindowRequired;
166
- const r = skillModels(doc, name, sk, opts.catalog!, minWin);
167
- return {
168
- skill: sk, from: r.from, ids: r.ids.slice(0, 3),
169
- ...(r.selection?.ranked[0] ? { decidingPreference: r.selection.ranked[0].why.decidingPreference } : {}),
170
- ...(r.selection ? { coverageGaps: r.selection.coverageGaps } : {}),
171
- };
166
+ try {
167
+ const r = skillModels(doc, name, sk, opts.catalog!, minWin);
168
+ return {
169
+ skill: sk, from: r.from, ids: r.ids.slice(0, 3), resolve: r.target.kind, pickPinned: r.pickPinned,
170
+ ...(r.selection?.ranked[0] ? { decidingPreference: r.selection.ranked[0].why.decidingPreference } : {}),
171
+ ...(r.selection ? { coverageGaps: r.selection.coverageGaps } : {}),
172
+ };
173
+ } catch (e) { return { skill: sk, error: e instanceof Error ? e.message : String(e) }; } // governed + router ⇒ fail-loud, surfaced
172
174
  }),
173
175
  } : {}),
174
176
  };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Conformance gates (C045) — the UNIFIED contract audit, the cockpit's "are you ready to ship?" checklist grown to fold
3
+ * the readiness DIMENSIONS into the same `Gate[]` model as `contractGates`. It COMPOSES shipped Suluk primitives — no
4
+ * new audit logic lives here:
5
+ * • input hardening + schema readiness ← @suluk/harden (auditDocument · auditReadiness, C043)
6
+ * • cost completeness ← @suluk/cost (costAudit)
7
+ * • settlement "names a lever" ← @suluk/cost (settlementAudit, C044)
8
+ * • implied error responses declared ← @suluk/cost (impliedErrorStatuses vs the op's declared statuses, C044)
9
+ *
10
+ * This is the generic form of toolfactory's `conformance-gate` (which hand-folds contract+errors+governance+stores+
11
+ * hardening): a consumer's CI collapses to `shipSummary([...contractGates(doc, baseline), ...conformanceGates(doc)])`
12
+ * (or `assertConformance(doc)`). Pure (no host) → unit-tested. cockpit never deps journeys (BDD coverage folds in via
13
+ * harden's combineGrades upstream, C043).
14
+ */
15
+ import { type OpenAPIv4Document, type Request } from "@suluk/core";
16
+ import { auditDocument, auditReadiness, type Grade } from "@suluk/harden";
17
+ import { costAudit, settlementAudit, impliedErrorStatuses, eachOperation } from "@suluk/cost";
18
+ import type { Gate, GateStatus } from "./lifecycle";
19
+
20
+ const GRADE_STATUS: Record<Grade, GateStatus> = { A: "ok", B: "ok", C: "todo", D: "todo", F: "error" };
21
+
22
+ /** The numeric statuses a request DECLARES (responses may be a map keyed by status or an array of {status}). */
23
+ function declaredStatuses(req: Request): Set<number> {
24
+ const r = (req as { responses?: unknown }).responses;
25
+ const out = new Set<number>();
26
+ if (Array.isArray(r)) {
27
+ for (const x of r) {
28
+ const s = Number((x as { status?: unknown }).status);
29
+ if (Number.isFinite(s)) out.add(s);
30
+ }
31
+ } else if (r && typeof r === "object") {
32
+ for (const k of Object.keys(r)) {
33
+ const s = Number(k);
34
+ if (Number.isFinite(s)) out.add(s);
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ const findingStatus = (findings: { severity: string }[]): GateStatus =>
41
+ findings.length === 0 ? "ok" : findings.some((f) => f.severity === "high" || f.severity === "error") ? "error" : "todo";
42
+
43
+ /** The CONFORMANCE gates — the readiness dimensions, each composed from a shipped Suluk audit. No host needed. */
44
+ export function conformanceGates(doc: OpenAPIv4Document): Gate[] {
45
+ const gates: Gate[] = [];
46
+
47
+ // input hardening (security)
48
+ const sec = auditDocument(doc);
49
+ gates.push({
50
+ id: "hardened", title: "Input hardening",
51
+ status: GRADE_STATUS[sec.grade],
52
+ detail: `grade ${sec.grade} (${sec.score}/100) — ${sec.bySeverity.high} high · ${sec.bySeverity.medium} medium`,
53
+ action: GRADE_STATUS[sec.grade] === "ok" ? undefined : "harden the input schemas (maxLength/pattern/maximum/maxItems; close objects)",
54
+ });
55
+
56
+ // schema readiness (computed-required / missing-example)
57
+ const rd = auditReadiness(doc);
58
+ gates.push({
59
+ id: "readiness", title: "Schema readiness",
60
+ status: GRADE_STATUS[rd.grade],
61
+ detail: `grade ${rd.grade} (${rd.score}/100) — ${rd.findings.length} finding${rd.findings.length === 1 ? "" : "s"}`,
62
+ action: rd.findings.length ? "fix computed-required fields + add request examples" : undefined,
63
+ });
64
+
65
+ // cost completeness
66
+ const cf = costAudit(doc);
67
+ gates.push({
68
+ id: "costed", title: "Cost declared",
69
+ status: findingStatus(cf),
70
+ detail: cf.length ? `${cf.length} cost finding${cf.length === 1 ? "" : "s"}` : "every priced op declares its cost",
71
+ action: cf.length ? "complete x-suluk-cost" : undefined,
72
+ });
73
+
74
+ // settlement — every priced op names a lever (C044)
75
+ const sf = settlementAudit(doc);
76
+ gates.push({
77
+ id: "settled", title: "Cost settlement (the lever)",
78
+ status: findingStatus(sf),
79
+ detail: sf.length ? `${sf.length} settlement finding${sf.length === 1 ? "" : "s"} (${[...new Set(sf.map((f) => f.rule))].join(", ")})` : "every priced op names a lever (credit | rate-limited | free)",
80
+ action: sf.length ? "add x-suluk-cost.settlement (or the missing x-suluk-ratelimit cap)" : undefined,
81
+ });
82
+
83
+ // implied error responses declared (C044)
84
+ let missing = 0;
85
+ for (const { req } of eachOperation(doc)) {
86
+ const declared = declaredStatuses(req);
87
+ for (const s of impliedErrorStatuses(req)) if (!declared.has(s)) missing++;
88
+ }
89
+ gates.push({
90
+ id: "errors", title: "Implied error responses declared",
91
+ status: missing ? "todo" : "ok",
92
+ detail: missing ? `${missing} facet-implied error status${missing === 1 ? "" : "es"} not declared (credit→402, auth→401, scope→403, ratelimit→429, upstream→502)` : "every facet-implied error response is declared",
93
+ action: missing ? "declare the implied error responses on each operation" : undefined,
94
+ });
95
+
96
+ return gates;
97
+ }
98
+
99
+ /** CI gate (the hard incentive): throw if any conformance gate is an `error` (a blocker). Returns the gates otherwise. */
100
+ export function assertConformance(doc: OpenAPIv4Document): Gate[] {
101
+ const gates = conformanceGates(doc);
102
+ const blockers = gates.filter((g) => g.status === "error");
103
+ if (blockers.length) {
104
+ throw new Error(`@suluk/cockpit: contract is not conformant — ${blockers.map((g) => `${g.title}: ${g.detail}`).join(" · ")}`);
105
+ }
106
+ return gates;
107
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,10 @@ export { componentReport, approveComponents, type ComponentReport } from "./visu
23
23
  export { type Baseline, primitiveCss } from "@suluk/visual";
24
24
  // lifecycle / ship-readiness (L3): the round-trip loop as one checklist — authored → coherent → confident → generated → deployed.
25
25
  export { contractGates, shipSummary, type Gate, type GateStatus } from "./lifecycle";
26
+ // conformance (C045): the UNIFIED contract audit — the readiness DIMENSIONS (harden security + readiness, cost,
27
+ // settlement/lever, implied-errors) folded into the same Gate[] model. A consumer's CI collapses to
28
+ // shipSummary([...contractGates, ...conformanceGates]) or assertConformance(doc).
29
+ export { conformanceGates, assertConformance } from "./conformance";
26
30
  // agents (C027, OBSERVE): the x-suluk-agents tier tree, effective scope, gate findings, reachable surface + a
27
31
  // projection preview — read-only; agent execution + secrets live OUTSIDE the cockpit (C020 no-credentials seam).
28
32
  export {
@@ -108,7 +108,9 @@ describe("C027 cockpit agents view (OBSERVE)", () => {
108
108
  d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "cheap-fast" };
109
109
  const sel = agentsView(d, { catalog: SEED_CATALOG }).agents.find((a) => a.name === "conin")!.modelSelection!.find((m) => m.skill === "operate")!;
110
110
  expect(sel.from).toBe("selected");
111
- expect(sel.ids.length).toBeGreaterThan(0);
111
+ expect(sel.ids!.length).toBeGreaterThan(0);
112
+ expect(sel.resolve).toBe("pinned"); // C030 default, ungoverned
113
+ expect(sel.pickPinned).toBe(true);
112
114
  expect(sel.decidingPreference).toBeTruthy();
113
115
  });
114
116
 
@@ -0,0 +1,73 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { conformanceGates, assertConformance, shipSummary } from "../src/index";
3
+ import type { OpenAPIv4Document } from "@suluk/core";
4
+
5
+ /**
6
+ * C045 — the unified contract audit: conformanceGates composes harden(security+readiness) + cost + settlement(lever) +
7
+ * implied-errors into the cockpit's Gate[] model. The generic form of toolfactory's conformance/governance/cost/errors
8
+ * gates. Composition only — no new audit logic here.
9
+ */
10
+ const docOf = (requests: Record<string, unknown>): OpenAPIv4Document => ({ openapi: "4.0.0-candidate", info: { title: "T" }, paths: { "/x": { requests } } }) as unknown as OpenAPIv4Document;
11
+
12
+ describe("conformanceGates — the five composed dimensions", () => {
13
+ const dirty = docOf({
14
+ charge: {
15
+ method: "post",
16
+ contentSchema: { type: "object", additionalProperties: false, required: ["amountCents", "balance"], properties: { amountCents: { type: "integer" }, balance: { type: "integer", "x-suluk-origin": "computed" } } },
17
+ "x-suluk-cost": { estimateMicroUsd: 1000, components: [] }, // priced, no settlement
18
+ "x-suluk-access": { requires: "authenticated" }, // implies 401
19
+ responses: { "200": { status: 200 } }, // 401 not declared
20
+ },
21
+ });
22
+ const gates = Object.fromEntries(conformanceGates(dirty).map((g) => [g.id, g]));
23
+
24
+ test("emits the five dimensions", () => {
25
+ expect(Object.keys(gates).sort()).toEqual(["costed", "errors", "hardened", "readiness", "settled"]);
26
+ });
27
+ test("hardening + readiness carry a grade", () => {
28
+ expect(gates.hardened.detail).toMatch(/grade [A-F]/);
29
+ expect(gates.readiness.detail).toMatch(/grade [A-F]/);
30
+ });
31
+ test("a priced op with no settlement is flagged on the settlement gate", () => {
32
+ expect(gates.settled.status).not.toBe("ok");
33
+ expect(gates.settled.detail).toContain("cost-without-settlement");
34
+ });
35
+ test("an undeclared facet-implied error is flagged on the errors gate", () => {
36
+ expect(gates.errors.status).toBe("todo"); // 401 implied by auth, not declared
37
+ });
38
+ });
39
+
40
+ describe("a clean contract passes every conformance gate", () => {
41
+ const clean = docOf({
42
+ clean: {
43
+ method: "post",
44
+ contentSchema: { type: "object", additionalProperties: false, required: ["name"], properties: { name: { type: "string", maxLength: 64, pattern: "^[a-z]+$" } }, examples: [{ name: "abc" }] },
45
+ "x-suluk-cost": { estimateMicroUsd: 100, components: [{ source: "compute", basis: "per-call", microUsd: 100 }], settlement: { method: "credit", credits: 1 } },
46
+ "x-suluk-access": { requires: "authenticated" },
47
+ responses: { "200": { status: 200 }, "401": { status: 401 }, "402": { status: 402 } },
48
+ },
49
+ });
50
+
51
+ test("all gates ok; assertConformance returns without throwing", () => {
52
+ const gates = conformanceGates(clean);
53
+ expect(gates.every((g) => g.status === "ok")).toBe(true);
54
+ expect(() => assertConformance(clean)).not.toThrow();
55
+ expect(shipSummary(gates).ready).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe("assertConformance gates CI on error-status dimensions", () => {
60
+ const broken = docOf({
61
+ free: {
62
+ method: "post",
63
+ contentSchema: { type: "object", additionalProperties: false, required: ["n"], properties: { n: { type: "string", maxLength: 8, pattern: "^[a-z]+$" } }, examples: [{ n: "a" }] },
64
+ // settled by rate-limiting but NO x-suluk-ratelimit → a HIGH settlement finding → the settled gate is `error`
65
+ "x-suluk-cost": { estimateMicroUsd: 500, components: [], settlement: { method: "rate-limited" } },
66
+ responses: { "200": { status: 200 } },
67
+ },
68
+ });
69
+
70
+ test("throws when a conformance gate is an error blocker", () => {
71
+ expect(() => assertConformance(broken)).toThrow(/not conformant/i);
72
+ });
73
+ });