@suluk/cockpit 0.1.12 → 0.1.13
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 +35 -32
- package/src/index.ts +2 -0
- package/src/lifecycle.ts +82 -0
- package/test/lifecycle.test.ts +67 -0
package/package.json
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/cockpit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/MahmoodKhalil57/suluk.git",
|
|
12
|
+
"directory": "tooling/ts/packages/cockpit"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/MahmoodKhalil57/suluk#readme",
|
|
15
|
+
"bugs": "https://github.com/MahmoodKhalil57/suluk/issues",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "src/index.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@suluk/core": "0.1.6",
|
|
23
|
+
"@suluk/hono": "0.1.1",
|
|
24
|
+
"@suluk/scalar": "0.1.1",
|
|
25
|
+
"@suluk/swagger": "0.1.1",
|
|
26
|
+
"@suluk/shadcn": "0.1.1",
|
|
27
|
+
"@suluk/builder": "0.1.9",
|
|
28
|
+
"@suluk/deploy": "0.1.1",
|
|
29
|
+
"@suluk/cost": "0.1.1",
|
|
30
|
+
"@suluk/visual": "0.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "bun test",
|
|
37
|
+
"typecheck": "tsc --noEmit -p ."
|
|
38
|
+
}
|
|
36
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export { contractToD2, diagramViews, type DiagramView } from "./diagram";
|
|
|
21
21
|
// component preview + pixel-confidence (surfaces @suluk/visual): decompose generated UI into primitives, check vs a baseline.
|
|
22
22
|
export { componentReport, approveComponents, type ComponentReport } from "./visual";
|
|
23
23
|
export { type Baseline, primitiveCss } from "@suluk/visual";
|
|
24
|
+
// lifecycle / ship-readiness (L3): the round-trip loop as one checklist — authored → coherent → confident → generated → deployed.
|
|
25
|
+
export { contractGates, shipSummary, type Gate, type GateStatus } from "./lifecycle";
|
|
24
26
|
// cost formatting, re-exported so the extension shell can render a live /cost ledger without a direct @suluk/cost dep.
|
|
25
27
|
export { formatMicroUsd, summarize, type CostSummary } from "@suluk/cost";
|
|
26
28
|
// modules (C021): install a contract fragment into the hub doc — the cockpit then re-projects it for free.
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle / ship-readiness (L3) — the round-trip loop made legible. One contract walks: authored → coherent →
|
|
3
|
+
* pixel-confident → generated → deployed. contractGates computes the CONTRACT-level gates here (pure: valid /
|
|
4
|
+
* coherent / confident), each with a status + the cheapest next action; the extension adds the host gates
|
|
5
|
+
* (generated-in-sync, deployed-in-sync, health) it can only see with fs + network. The whole thing is one
|
|
6
|
+
* "are you ready to ship?" checklist that aggregates the cockpit's own audits. Pure (no host) → unit-tested.
|
|
7
|
+
*/
|
|
8
|
+
import { validateDocument, buildAda, type OpenAPIv4Document } from "@suluk/core";
|
|
9
|
+
import { convergeContract } from "./converge";
|
|
10
|
+
import { componentReport } from "./visual";
|
|
11
|
+
import type { Baseline } from "@suluk/visual";
|
|
12
|
+
|
|
13
|
+
// "info" is the non-blocking status: a gate that is honestly n/a (no env configured, no workspace open) — it
|
|
14
|
+
// is shown for transparency but NEVER counts against readiness. Distinct from "warn"/"todo", which DO block.
|
|
15
|
+
export type GateStatus = "ok" | "warn" | "error" | "todo" | "info";
|
|
16
|
+
export interface Gate {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
status: GateStatus;
|
|
20
|
+
detail: string;
|
|
21
|
+
/** the command to run to advance this gate (undefined ⇒ nothing to do) */
|
|
22
|
+
action?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The CONTRACT-level ship gates — everything decidable from the document itself (no host needed). */
|
|
26
|
+
export function contractGates(doc: OpenAPIv4Document, baseline: Baseline): Gate[] {
|
|
27
|
+
const gates: Gate[] = [];
|
|
28
|
+
|
|
29
|
+
// 0. non-empty — a contract with zero operations has nothing to ship, however valid/coherent it is in isolation.
|
|
30
|
+
const ops = buildAda(doc).operations.length;
|
|
31
|
+
gates.push({
|
|
32
|
+
id: "operations", title: "Has operations",
|
|
33
|
+
status: ops ? "ok" : "todo",
|
|
34
|
+
detail: ops ? `${ops} operation${ops === 1 ? "" : "s"}` : "no operations — nothing to ship",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 1. valid v4
|
|
38
|
+
const v = validateDocument(doc);
|
|
39
|
+
gates.push({
|
|
40
|
+
id: "valid", title: "Valid v4 contract",
|
|
41
|
+
status: v.valid ? "ok" : "error",
|
|
42
|
+
detail: v.valid ? "passes the meta-schema" : `${v.errors.length} schema error${v.errors.length === 1 ? "" : "s"}`,
|
|
43
|
+
action: v.valid ? undefined : "suluk.validate",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 2. coherent (converge)
|
|
47
|
+
const conv = convergeContract(doc);
|
|
48
|
+
const errs = conv.findings.filter((f) => f.severity === "error").length;
|
|
49
|
+
gates.push({
|
|
50
|
+
id: "coherent", title: "Coherent (no contradictions)",
|
|
51
|
+
status: errs ? "error" : "ok",
|
|
52
|
+
detail: errs ? `${errs} contradiction${errs === 1 ? "" : "s"} (dangling ref / orphan scope / …)` : "converges clean",
|
|
53
|
+
action: errs ? "suluk.convergeContract" : undefined,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 3. pixel-confident components
|
|
57
|
+
const cr = componentReport(doc, baseline);
|
|
58
|
+
const pending = cr.confidence.missing.length + cr.confidence.drifted.length;
|
|
59
|
+
gates.push({
|
|
60
|
+
id: "confident", title: "Components pixel-confident",
|
|
61
|
+
status: cr.used.length === 0 ? "ok" : pending ? "todo" : "ok",
|
|
62
|
+
detail: cr.used.length === 0 ? "no generated components" : pending ? `${pending} primitive${pending === 1 ? "" : "s"} to verify once` : "every primitive approved + unchanged",
|
|
63
|
+
action: pending ? "suluk.previewComponents" : undefined,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return gates;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A one-line readiness summary over a set of gates (contract + host). "info" gates never count against ready. */
|
|
70
|
+
export function shipSummary(gates: Gate[]): { ready: boolean; line: string } {
|
|
71
|
+
const errors = gates.filter((g) => g.status === "error").length;
|
|
72
|
+
const todos = gates.filter((g) => g.status === "todo" || g.status === "warn").length;
|
|
73
|
+
const ok = gates.filter((g) => g.status === "ok").length;
|
|
74
|
+
const info = gates.filter((g) => g.status === "info").length;
|
|
75
|
+
const ready = errors === 0 && todos === 0;
|
|
76
|
+
return {
|
|
77
|
+
ready,
|
|
78
|
+
line: ready
|
|
79
|
+
? `ready to ship — ${ok} gate${ok === 1 ? "" : "s"} pass${info ? ` · ${info} n/a` : ""}`
|
|
80
|
+
: `${errors} blocker${errors === 1 ? "" : "s"} · ${todos} to do · ${ok}/${gates.length} pass`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import { parseDocument } from "@suluk/core";
|
|
3
|
+
import type { Baseline } from "@suluk/visual";
|
|
4
|
+
import { componentReport, approveComponents } from "../src/visual";
|
|
5
|
+
import { contractGates, shipSummary } from "../src/lifecycle";
|
|
6
|
+
|
|
7
|
+
const good = () => parseDocument(`openapi: 4.0.0-candidate
|
|
8
|
+
info: { title: Shop, version: 1.0.0 }
|
|
9
|
+
paths:
|
|
10
|
+
"pet":
|
|
11
|
+
requests:
|
|
12
|
+
listPets: { method: get, responses: { ok: { status: 200, contentSchema: { $ref: "#/components/schemas/Pet" } } } }
|
|
13
|
+
components: { schemas: { Pet: { type: object, properties: { name: { type: string } } } } }`);
|
|
14
|
+
|
|
15
|
+
const gate = (gates: ReturnType<typeof contractGates>, id: string) => gates.find((g) => g.id === id)!;
|
|
16
|
+
|
|
17
|
+
describe("contractGates — the contract-level ship gates", () => {
|
|
18
|
+
test("a valid, coherent contract passes valid + coherent; confidence is 'todo' against an empty baseline", () => {
|
|
19
|
+
const gates = contractGates(good(), {});
|
|
20
|
+
expect(gate(gates, "valid").status).toBe("ok");
|
|
21
|
+
expect(gate(gates, "coherent").status).toBe("ok");
|
|
22
|
+
expect(gate(gates, "confident").status).toBe("todo"); // never verified
|
|
23
|
+
expect(gate(gates, "confident").action).toBe("suluk.previewComponents");
|
|
24
|
+
});
|
|
25
|
+
test("a contradiction (a dangling $ref) makes the coherent gate an error with a fix action", () => {
|
|
26
|
+
const broken = parseDocument(`openapi: 4.0.0-candidate
|
|
27
|
+
info: { title: T, version: 1.0.0 }
|
|
28
|
+
paths: { "p": { requests: { g: { method: get, responses: { ok: { status: 200, contentSchema: { $ref: "#/components/schemas/Ghost" } } } } } } }
|
|
29
|
+
components: { schemas: {} }`);
|
|
30
|
+
const g = gate(contractGates(broken, {}), "coherent");
|
|
31
|
+
expect(g.status).toBe("error");
|
|
32
|
+
expect(g.action).toBe("suluk.convergeContract");
|
|
33
|
+
});
|
|
34
|
+
test("after approving the components, the confidence gate passes", () => {
|
|
35
|
+
const doc = good();
|
|
36
|
+
const baseline: Baseline = approveComponents(componentReport(doc, {}), {}, 1);
|
|
37
|
+
expect(gate(contractGates(doc, baseline), "confident").status).toBe("ok");
|
|
38
|
+
});
|
|
39
|
+
test("an empty contract (zero operations) is NOT ship-ready — the operations gate is 'todo'", () => {
|
|
40
|
+
const empty = parseDocument(`openapi: 4.0.0-candidate
|
|
41
|
+
info: { title: Empty, version: 1.0.0 }
|
|
42
|
+
paths: {}`);
|
|
43
|
+
const gates = contractGates(empty, {});
|
|
44
|
+
expect(gate(gates, "operations").status).toBe("todo"); // nothing to ship
|
|
45
|
+
expect(shipSummary(gates).ready).toBe(false); // even though valid + coherent are clean
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("shipSummary", () => {
|
|
50
|
+
test("all-ok gates ⇒ ready", () => {
|
|
51
|
+
const r = shipSummary([{ id: "a", title: "A", status: "ok", detail: "" }, { id: "b", title: "B", status: "ok", detail: "" }]);
|
|
52
|
+
expect(r.ready).toBe(true);
|
|
53
|
+
expect(r.line).toContain("ready to ship");
|
|
54
|
+
});
|
|
55
|
+
test("an error gate ⇒ NOT ready, counted as a blocker", () => {
|
|
56
|
+
const r = shipSummary([{ id: "a", title: "A", status: "ok", detail: "" }, { id: "b", title: "B", status: "error", detail: "" }, { id: "c", title: "C", status: "todo", detail: "" }]);
|
|
57
|
+
expect(r.ready).toBe(false);
|
|
58
|
+
expect(r.line).toContain("1 blocker");
|
|
59
|
+
expect(r.line).toContain("1 to do");
|
|
60
|
+
});
|
|
61
|
+
test("an 'info' gate is non-blocking — it never makes a clean contract read as not-ready", () => {
|
|
62
|
+
const r = shipSummary([{ id: "a", title: "A", status: "ok", detail: "" }, { id: "b", title: "B", status: "info", detail: "n/a — no environment configured" }]);
|
|
63
|
+
expect(r.ready).toBe(true);
|
|
64
|
+
expect(r.line).toContain("ready to ship");
|
|
65
|
+
expect(r.line).toContain("1 n/a"); // surfaced for transparency, but not counted against ready
|
|
66
|
+
});
|
|
67
|
+
});
|