@suluk/cockpit 0.1.4 → 0.1.6

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": "@suluk/cockpit",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/cycle.ts CHANGED
@@ -14,6 +14,7 @@ import type { OpenAPIv4Document, Request, SchemaOrRef, SecurityRequirement } fro
14
14
  import { audit, coverage } from "@suluk/hono";
15
15
  import { formSpec, tableSpec } from "@suluk/shadcn";
16
16
  import { costAudit, costTable, formatMicroUsd } from "@suluk/cost";
17
+ import { readProviders } from "@suluk/builder";
17
18
 
18
19
  export type LayerStatus = "ok" | "warn" | "error" | "info";
19
20
 
@@ -26,7 +27,7 @@ export interface CycleItem {
26
27
  }
27
28
 
28
29
  export interface CycleLayer {
29
- id: "data" | "contract" | "auth" | "document" | "cost" | "docs" | "state" | "ui" | "tests";
30
+ id: "data" | "contract" | "auth" | "document" | "cost" | "docs" | "state" | "ui" | "providers" | "tests";
30
31
  title: string;
31
32
  status: LayerStatus;
32
33
  summary: string;
@@ -140,6 +141,14 @@ export function buildCycle(doc: OpenAPIv4Document, opts: { principal?: Principal
140
141
  const undeclared = costFindings.filter((f) => f.code === "no-cost-model").length;
141
142
  const costItems: CycleItem[] = declaredCosts.map((d) => ({ label: d.operation, ref: d.operation, detail: `${formatMicroUsd(d.estimateMicroUsd)} · ${d.sources.join(", ")}` }));
142
143
 
144
+ // ── providers: the swappable facet bindings recorded on the document (M3)
145
+ const providerBindings = readProviders(doc);
146
+ const providerItems: CycleItem[] = providerBindings.map((b) => ({
147
+ label: b.facet, ref: b.facet,
148
+ detail: `${b.title}${b.alternatives.length ? ` · ${b.alternatives.length} alternatives` : ""}`,
149
+ status: b.known ? undefined : "warn",
150
+ }));
151
+
143
152
  // ── tests: doc-level contract checks
144
153
  const checks = docChecks(doc);
145
154
  const testsItems: CycleItem[] = checks.map((c) => ({ label: c.name, status: c.pass ? "ok" : "error", detail: c.pass ? "pass" : c.message }));
@@ -168,6 +177,12 @@ export function buildCycle(doc: OpenAPIv4Document, opts: { principal?: Principal
168
177
  { id: "docs", title: "Docs", status: "ok", summary: "Scalar · Swagger", items: docsItems },
169
178
  { id: "state", title: "State (Nano Stores)", status: "ok", summary: `${stateItems.length} stores`, items: stateItems },
170
179
  { id: "ui", title: "UI (shadcn)", status: uiItems.length ? "ok" : "info", summary: `${uiItems.length} forms/tables`, items: uiItems },
180
+ {
181
+ id: "providers", title: "Providers",
182
+ status: providerItems.length ? "ok" : "info",
183
+ summary: providerItems.length ? providerBindings.map((b) => `${b.facet}→${b.impl}`).join(" · ") : "none",
184
+ items: providerItems,
185
+ },
171
186
  {
172
187
  id: "tests", title: "Tests (contract checks)",
173
188
  status: checks.every((c) => c.pass) ? "ok" : "error",
package/src/drift.ts CHANGED
Binary file
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export { entityNames, generateForm, generateTable, generateStoresModule, exportV
11
11
  export { deployPlan, deployMarkdown } from "./deploy";
12
12
  export type { DeployPlan, DeployStep, DeployProvider } from "@suluk/deploy";
13
13
  // drift (OBSERVE): compare a LOCAL contract against a DEPLOYED one — the "what's drifted in prod" view (C020).
14
- export { diffContracts, canonical, type ContractDiff, type ChangedOp, type OpRef } from "./drift";
14
+ export { diffContracts, canonical, type ContractDiff, type ChangedOp, type OpRef, type ProviderDelta, type ProviderChange } from "./drift";
15
15
  // cross-cut (M1): one contract refracted through every viewer — the scope-gated surface, the moat.
16
16
  export { crossCut, documentScopes, defaultViewers, type Viewer, type ViewerView, type GatedOp, type CrossCut } from "./crosscut";
17
17
  // cost formatting, re-exported so the extension shell can render a live /cost ledger without a direct @suluk/cost dep.
@@ -20,5 +20,8 @@ export { formatMicroUsd, summarize, type CostSummary } from "@suluk/cost";
20
20
  export {
21
21
  installModule, namespaceModule, previewInstall, gradeModule,
22
22
  ECOMMERCE, CRM, BILLING, FIRST_PARTY_REGISTRY,
23
+ PROVIDER_CATALOG, providerFacets, readProviders, swapProvider,
24
+ parseRegistry, validateModule,
23
25
  type SulukModule, type InstallResult, type ModuleEntry, type ModuleRegistry, type ModuleGrade, type InstallPreview,
26
+ type ProviderImpl, type ProviderBinding, type RegistrySource, type ParsedRegistry,
24
27
  } from "@suluk/builder";
@@ -14,8 +14,8 @@ const petstore = parseDocument(petstoreSrc);
14
14
  describe("buildCycle — the cockpit spine (every layer from one hub)", () => {
15
15
  const model = buildCycle(petstore);
16
16
 
17
- test("models all nine layers (incl. cost)", () => {
18
- expect(model.layers.map((l) => l.id)).toEqual(["data", "contract", "auth", "document", "cost", "docs", "state", "ui", "tests"]);
17
+ test("models all ten layers (incl. cost + providers)", () => {
18
+ expect(model.layers.map((l) => l.id)).toEqual(["data", "contract", "auth", "document", "cost", "docs", "state", "ui", "providers", "tests"]);
19
19
  });
20
20
  test("the cost layer reflects declared costs (x-suluk-cost) — none on the bare petstore", () => {
21
21
  expect(model.layers.find((l) => l.id === "cost")!.summary).toBe("no costs declared");
@@ -110,6 +110,6 @@ describe("codegen actions land real artifacts", () => {
110
110
 
111
111
  describe("cycleSummary — flat status line", () => {
112
112
  test("returns one entry per layer", () => {
113
- expect(cycleSummary(buildCycle(petstore)).length).toBe(9);
113
+ expect(cycleSummary(buildCycle(petstore)).length).toBe(10);
114
114
  });
115
115
  });
@@ -139,6 +139,15 @@ paths: { "p": { requests: { g: { method: get, responses: { ok: { status: 200 } }
139
139
  expect(c.changes.some((x) => x.includes("shape"))).toBe(true); // the residual is not suppressed
140
140
  });
141
141
 
142
+ test("provider-slot drift (x-suluk-providers) is reported", () => {
143
+ const local = { openapi: "4.0.0-candidate", info: { title: "T", version: "1.0.0" }, paths: {}, "x-suluk-providers": { payments: "paddle", email: "resend" } } as unknown as Parameters<typeof diffContracts>[0];
144
+ const deployed = { openapi: "4.0.0-candidate", info: { title: "T", version: "1.0.0" }, paths: {}, "x-suluk-providers": { payments: "stripe" } } as unknown as Parameters<typeof diffContracts>[0];
145
+ const d = diffContracts(local, deployed);
146
+ expect(d.identical).toBe(false);
147
+ expect(d.providers.changed).toEqual([{ facet: "payments", from: "stripe", to: "paddle" }]);
148
+ expect(d.providers.added).toEqual([{ facet: "email", impl: "resend" }]);
149
+ expect(d.summary).toContain("providers");
150
+ });
142
151
  test("canonical() is cycle-safe (does not overflow the stack)", () => {
143
152
  const a: Record<string, unknown> = { x: 1 };
144
153
  a.self = a;
@@ -43,4 +43,10 @@ describe("installModule → the cockpit cycle lights up", () => {
43
43
  test("the document still validates against the v4 meta-schema after the merge", () => {
44
44
  expect(after.valid).toBe(true);
45
45
  });
46
+ test("the Providers layer lights up with the module's provider slot (payments→stripe)", () => {
47
+ const providers = layer(after, "providers");
48
+ expect(providers.summary).toContain("payments→stripe");
49
+ expect(providers.items.map((i) => i.label)).toContain("payments");
50
+ expect(layer(before, "providers").summary).toBe("none"); // the host had no provider slots
51
+ });
46
52
  });