@suluk/cockpit 0.1.11 → 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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ <p align="center">
2
+ <a href="https://github.com/MahmoodKhalil57/suluk">
3
+ <img src="https://raw.githubusercontent.com/MahmoodKhalil57/suluk/main/branding/export/wordmark.png" alt="Suluk" width="360" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">@suluk/cockpit</h1>
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>
10
+
11
+ <p align="center">
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>
13
+ </p>
14
+
15
+ ---
16
+
17
+ > **CANDIDATE tooling — not official OpenAPI.** Suluk is a single-contributor candidate for
18
+ > OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable
19
+ > to ratify anything on the SIG's behalf.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ bun add @suluk/cockpit
25
+ ```
26
+
27
+ ## The Suluk cycle
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).
32
+
33
+ ## License
34
+
35
+ Apache-2.0
package/package.json CHANGED
@@ -1,27 +1,39 @@
1
1
  {
2
2
  "name": "@suluk/cockpit",
3
- "version": "0.1.11",
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
- "type": "module",
6
- "main": "src/index.ts",
7
- "exports": {
8
- ".": "./src/index.ts"
9
- },
10
- "dependencies": {
11
- "@suluk/core": "0.1.0",
12
- "@suluk/hono": "0.1.0",
13
- "@suluk/scalar": "0.1.0",
14
- "@suluk/swagger": "0.1.0",
15
- "@suluk/shadcn": "0.1.0",
16
- "@suluk/builder": "0.1.0",
17
- "@suluk/deploy": "0.1.0",
18
- "@suluk/cost": "0.1.0"
19
- },
20
- "devDependencies": {
21
- "@types/bun": "latest"
22
- },
23
- "scripts": {
24
- "test": "bun test",
25
- "typecheck": "tsc --noEmit -p ."
26
- }
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
+ }
27
39
  }
package/src/index.ts CHANGED
@@ -18,6 +18,11 @@ export { crossCut, documentScopes, defaultViewers, type Viewer, type ViewerView,
18
18
  export { convergeContract, type ConvergeReport, type ConvergeFinding, type ConvergeCode } from "./converge";
19
19
  // diagrams: D2 source for views of the contract (ERD / the declarative cycle / the operation surface) — another projection.
20
20
  export { contractToD2, diagramViews, type DiagramView } from "./diagram";
21
+ // component preview + pixel-confidence (surfaces @suluk/visual): decompose generated UI into primitives, check vs a baseline.
22
+ export { componentReport, approveComponents, type ComponentReport } from "./visual";
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";
21
26
  // cost formatting, re-exported so the extension shell can render a live /cost ledger without a direct @suluk/cost dep.
22
27
  export { formatMicroUsd, summarize, type CostSummary } from "@suluk/cost";
23
28
  // modules (C021): install a contract fragment into the hub doc — the cockpit then re-projects it for free.
@@ -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
+ }
package/src/visual.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Component preview + pixel-confidence (surfaces @suluk/visual in the cockpit). A generated form/table is a
3
+ * COMPOSITION of widget primitives; its pixel-confidence reduces to "are all those primitives approved +
4
+ * unchanged?" — decided by content-hash, WITHOUT re-rendering. componentReport decomposes every entity's
5
+ * form/table into primitives, checks them against a baseline (confident / drifted / pending), and carries an
6
+ * inline control preview for each. approveComponents records the "verify once" — after that, the components are
7
+ * pixel-confident at that content hash forever, until a primitive's source drifts. Pure (no host) → unit-tested.
8
+ */
9
+ import type { OpenAPIv4Document, SchemaOrRef } from "@suluk/core";
10
+ import { formSpec, tableSpec, renderFormTsx, renderTableTsx, type FieldSpec, type FieldWidget } from "@suluk/shadcn";
11
+ import {
12
+ formPrimitives, tablePrimitives, knownWidgets, primitiveControl,
13
+ checkConfidence, approve, confidenceCoverage, hash,
14
+ type Baseline, type UsedPrimitive, type ConfidenceReport, type Capture, type PrimitiveSources,
15
+ } from "@suluk/visual";
16
+
17
+ /** A one-field form for a widget — its REAL generated TSX is what we content-hash (so a renderer edit drifts it). */
18
+ function canonicalField(widget: FieldWidget): FieldSpec {
19
+ return { name: "field", label: "Field", widget, required: true, options: widget === "select" ? ["a", "b", "c"] : undefined };
20
+ }
21
+
22
+ /**
23
+ * Primitive sources for the content-hash — the ACTUAL @suluk/shadcn generator output (the bytes that ship),
24
+ * NOT the isolated preview mock. A widget's source is renderFormTsx of a one-field form (control + layout, so a
25
+ * layout edit drifts every widget too); the table source is renderTableTsx. Editing render-form.ts /
26
+ * render-table.ts therefore drifts the affected primitives — the confidence is honest. (renderPrimitiveHtml /
27
+ * primitiveControl remain ONLY the human-viewable inline preview.)
28
+ */
29
+ function sources(): PrimitiveSources {
30
+ const widgets: Record<string, string> = {};
31
+ for (const w of knownWidgets() as FieldWidget[]) widgets[w] = renderFormTsx({ fields: [canonicalField(w)], warnings: [] });
32
+ const tableLayout = renderTableTsx({ columns: [{ key: "id", header: "Id", type: "integer" }, { key: "name", header: "Name", type: "string" }], warnings: [] });
33
+ return { widgets, tableLayout }; // no separate formLayout marker — each widget render already carries the form layout
34
+ }
35
+
36
+ export interface ComponentReport {
37
+ /** the distinct primitives every generated form/table is composed of (deduped across entities) */
38
+ used: UsedPrimitive[];
39
+ confidence: ConfidenceReport;
40
+ /** 0..1 — fraction of used primitives that are approved + unchanged */
41
+ coverage: number;
42
+ /** primitive key → inline control HTML (widget primitives only — for the preview) */
43
+ preview: Record<string, string>;
44
+ /** which primitives each entity's form/table is built from */
45
+ entities: { name: string; form: string[]; table: string[] }[];
46
+ }
47
+
48
+ /** Decompose a contract's generated components into primitives and check their pixel-confidence vs a baseline. */
49
+ export function componentReport(doc: OpenAPIv4Document, baseline: Baseline): ComponentReport {
50
+ const schemas = (doc.components?.schemas ?? {}) as Record<string, SchemaOrRef>;
51
+ const defs = schemas;
52
+ const src = sources();
53
+ const usedMap = new Map<string, UsedPrimitive>();
54
+ const entities: ComponentReport["entities"] = [];
55
+ for (const [name, schema] of Object.entries(schemas)) {
56
+ let fp: UsedPrimitive[] = [];
57
+ let tp: UsedPrimitive[] = [];
58
+ try { fp = formPrimitives(formSpec(schema, { defs }), src); } catch { /* schema not form-able */ }
59
+ try { tp = tablePrimitives(tableSpec(schema, { defs }), src); } catch { /* not table-able */ }
60
+ for (const p of [...fp, ...tp]) usedMap.set(p.key, p);
61
+ entities.push({ name, form: fp.map((p) => p.key), table: tp.map((p) => p.key) });
62
+ }
63
+ const used = [...usedMap.values()];
64
+ const preview: Record<string, string> = {};
65
+ for (const p of used) {
66
+ const m = p.key.match(/^widget:(.+)$/);
67
+ if (m) preview[p.key] = primitiveControl(m[1]);
68
+ }
69
+ return { used, confidence: checkConfidence(used, baseline), coverage: confidenceCoverage(used, baseline), preview, entities };
70
+ }
71
+
72
+ /** The "verify once": approve every used primitive at its current content hash, returning the new baseline. */
73
+ export function approveComponents(report: ComponentReport, baseline: Baseline, at: number): Baseline {
74
+ const captures: Capture[] = report.used.map((p) => ({
75
+ key: p.key,
76
+ contentHash: p.contentHash,
77
+ // the snapshot proxy: the hash of what the operator just looked at (the control HTML, else the source hash)
78
+ snapshotHash: hash(report.preview[p.key] ?? p.contentHash),
79
+ label: p.label,
80
+ }));
81
+ return approve(captures, baseline, at);
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
+ });
@@ -0,0 +1,69 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { parseDocument } from "@suluk/core";
3
+ import { contentHash, renderPrimitiveHtml, type Baseline } from "@suluk/visual";
4
+ import { renderFormTsx } from "@suluk/shadcn";
5
+ import { componentReport, approveComponents } from "../src/visual";
6
+
7
+ const doc = parseDocument(`openapi: 4.0.0-candidate
8
+ info: { title: Shop, version: 1.0.0 }
9
+ paths: {}
10
+ components:
11
+ schemas:
12
+ Product: { type: object, required: [ name ], properties: { id: { type: integer }, name: { type: string }, sku: { type: string } } }
13
+ Order: { type: object, properties: { id: { type: integer }, status: { type: string, enum: [ open, closed ] } } }`);
14
+
15
+ describe("componentReport — decompose generated UI into primitives", () => {
16
+ test("with an EMPTY baseline, every used primitive is pending (nothing confident yet)", () => {
17
+ const r = componentReport(doc, {});
18
+ expect(r.used.length).toBeGreaterThan(0);
19
+ expect(r.confidence.confident).toBe(false);
20
+ expect(r.confidence.missing.length).toBe(r.used.length);
21
+ expect(r.confidence.approved).toHaveLength(0);
22
+ expect(r.coverage).toBe(0);
23
+ });
24
+ test("decomposes each entity's form + table into shared primitives", () => {
25
+ const r = componentReport(doc, {});
26
+ const product = r.entities.find((e) => e.name === "Product")!;
27
+ expect(product.form.length).toBeGreaterThan(0); // form widgets
28
+ expect(product.table.length).toBeGreaterThan(0); // table layout/cell
29
+ // a text widget primitive is SHARED across entities — deduped in `used`
30
+ const widgetKeys = r.used.map((p) => p.key).filter((k) => k.startsWith("widget:"));
31
+ expect(new Set(widgetKeys).size).toBe(widgetKeys.length);
32
+ });
33
+ test("carries an inline control preview for each widget primitive", () => {
34
+ const r = componentReport(doc, {});
35
+ const widget = r.used.find((p) => p.key.startsWith("widget:"))!;
36
+ expect(r.preview[widget.key]).toBeTruthy();
37
+ expect(r.preview[widget.key]).toMatch(/<(input|select|textarea|label|div)/); // a real control fragment
38
+ });
39
+ test("a widget's content-hash tracks the REAL @suluk/shadcn renderer, not the isolated preview mock", () => {
40
+ const sel = componentReport(doc, {}).used.find((p) => p.key === "widget:select")!;
41
+ // it equals the hash of the ACTUAL generated control (renderFormTsx) — so editing render-form.ts drifts it
42
+ expect(sel.contentHash).toBe(contentHash(renderFormTsx({ fields: [{ name: "field", label: "Field", widget: "select", required: true, options: ["a", "b", "c"] }], warnings: [] })));
43
+ // and is NOT the @suluk/visual preview mock (the bug that made a real control edit never drift)
44
+ expect(sel.contentHash).not.toBe(contentHash(renderPrimitiveHtml({ widget: "select" })));
45
+ });
46
+ });
47
+
48
+ describe("approveComponents — verify once, confident forever (until drift)", () => {
49
+ test("after approval the SAME contract is fully confident — no re-verification", () => {
50
+ const before = componentReport(doc, {});
51
+ const baseline: Baseline = approveComponents(before, {}, 1_000);
52
+ const after = componentReport(doc, baseline);
53
+ expect(after.confidence.confident).toBe(true);
54
+ expect(after.confidence.missing).toHaveLength(0);
55
+ expect(after.confidence.drifted).toHaveLength(0);
56
+ expect(after.coverage).toBe(1);
57
+ });
58
+ test("adding a NEW widget (a new enum/select-backed field) leaves only the new primitive pending", () => {
59
+ const baseline = approveComponents(componentReport(doc, {}), {}, 1_000);
60
+ // a doc that introduces a date widget the baseline never approved
61
+ const withDate = parseDocument(`openapi: 4.0.0-candidate
62
+ info: { title: Shop, version: 1.0.0 }
63
+ paths: {}
64
+ components: { schemas: { Event: { type: object, properties: { when: { type: string, format: date } } } } }`);
65
+ const r = componentReport(withDate, baseline);
66
+ // the approved widgets stay confident; only genuinely-new primitives are pending
67
+ expect(r.confidence.missing.every((p) => !baseline[p.key])).toBe(true);
68
+ });
69
+ });