From dd460a559adb836920f3efe46da06e390ed20a09 Mon Sep 17 00:00:00 2001 From: Edward Twumasi <45181928+teckedd-code2save@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:42:24 +0000 Subject: [PATCH 1/2] feat: add repo-to-runtime intent contract --- src/lib/intent-contract.ts | 204 +++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/lib/intent-contract.ts diff --git a/src/lib/intent-contract.ts b/src/lib/intent-contract.ts new file mode 100644 index 0000000..c3cd7f0 --- /dev/null +++ b/src/lib/intent-contract.ts @@ -0,0 +1,204 @@ +import { parse } from "yaml"; +import type { ProjectTopology } from "./topology"; + +export type IntentRisk = "read" | "mutate" | "high"; + +export interface GroundControlIntentService { + name: string; + image?: string; + port?: number; + health?: { path: string }; + dependsOn?: string[]; +} + +export interface GroundControlIntent { + version: "v1"; + project: string; + domains?: string[]; + services: GroundControlIntentService[]; + policy?: { + restart?: "allow" | "approve" | "deny"; + redeploy?: "allow" | "approve" | "deny"; + dns?: "allow" | "approve" | "deny"; + destructive?: "allow" | "approve" | "deny"; + }; +} + +export interface DriftEvidence { + source: "manifest" | "topology" | "proxy"; + detail: string; +} + +export interface DriftFinding { + code: "project_missing" | "service_missing" | "port_mismatch" | "domain_missing"; + service?: string; + expected: string; + observed: string; + risk: IntentRisk; + evidence: DriftEvidence[]; + proposedAction: string; + reversible: boolean; +} + +function ensureString(value: unknown, field: string): string { + if (typeof value !== "string" || !value.trim()) { + throw new Error(`groundcontrol.yaml: ${field} must be a non-empty string`); + } + return value.trim(); +} + +export function parseGroundControlIntent(input: string): GroundControlIntent { + const raw = parse(input) as Record | null; + if (!raw || typeof raw !== "object") { + throw new Error("groundcontrol.yaml: expected an object"); + } + if (raw.version !== "v1") { + throw new Error("groundcontrol.yaml: version must be v1"); + } + const project = ensureString(raw.project, "project"); + if (!Array.isArray(raw.services) || raw.services.length === 0) { + throw new Error("groundcontrol.yaml: services must contain at least one service"); + } + + const names = new Set(); + const services = raw.services.map((value, index) => { + if (!value || typeof value !== "object") { + throw new Error(`groundcontrol.yaml: services[${index}] must be an object`); + } + const item = value as Record; + const name = ensureString(item.name, `services[${index}].name`); + if (names.has(name)) throw new Error(`groundcontrol.yaml: duplicate service ${name}`); + names.add(name); + + const service: GroundControlIntentService = { name }; + if (item.image !== undefined) service.image = ensureString(item.image, `services[${index}].image`); + if (item.port !== undefined) { + if (!Number.isInteger(item.port) || Number(item.port) <= 0 || Number(item.port) > 65535) { + throw new Error(`groundcontrol.yaml: services[${index}].port must be a valid TCP port`); + } + service.port = Number(item.port); + } + if (item.health !== undefined) { + if (!item.health || typeof item.health !== "object") { + throw new Error(`groundcontrol.yaml: services[${index}].health must be an object`); + } + service.health = { + path: ensureString((item.health as Record).path, `services[${index}].health.path`), + }; + } + if (item.dependsOn !== undefined) { + if (!Array.isArray(item.dependsOn) || item.dependsOn.some(v => typeof v !== "string")) { + throw new Error(`groundcontrol.yaml: services[${index}].dependsOn must be a string array`); + } + service.dependsOn = item.dependsOn as string[]; + } + return service; + }); + + const domains = raw.domains === undefined + ? undefined + : Array.isArray(raw.domains) && raw.domains.every(v => typeof v === "string") + ? raw.domains as string[] + : (() => { throw new Error("groundcontrol.yaml: domains must be a string array"); })(); + + return { + version: "v1", + project, + services, + ...(domains ? { domains } : {}), + ...(raw.policy && typeof raw.policy === "object" + ? { policy: raw.policy as GroundControlIntent["policy"] } + : {}), + }; +} + +function declaredPorts(ports: string[]): number[] { + return ports + .flatMap(value => value.split(":")) + .map(value => Number(value.replace(/\/tcp$|\/udp$/i, ""))) + .filter(value => Number.isInteger(value) && value > 0 && value <= 65535); +} + +export function detectIntentDrift( + intent: GroundControlIntent, + topology: ProjectTopology, +): DriftFinding[] { + const findings: DriftFinding[] = []; + const project = topology.projects.find( + p => p.slug === intent.project || p.name === intent.project || p.slug.endsWith(`/${intent.project}`), + ); + + if (!project) { + return [{ + code: "project_missing", + expected: intent.project, + observed: "not present in live topology", + risk: "mutate", + evidence: [ + { source: "manifest", detail: `project=${intent.project}` }, + { source: "topology", detail: "no matching project node" }, + ], + proposedAction: "inspect repository/deployment target before creating any runtime resources", + reversible: true, + }]; + } + + for (const expected of intent.services) { + const live = project.services.find(service => service.service === expected.name); + if (!live) { + findings.push({ + code: "service_missing", + service: expected.name, + expected: "service present", + observed: "service absent", + risk: "mutate", + evidence: [ + { source: "manifest", detail: `service=${expected.name}` }, + { source: "topology", detail: `project=${project.slug} has no matching service` }, + ], + proposedAction: `prepare a reversible deploy/restart plan for service ${expected.name}`, + reversible: true, + }); + continue; + } + + if (expected.port !== undefined) { + const livePorts = declaredPorts(live.ports); + if (!livePorts.includes(expected.port)) { + findings.push({ + code: "port_mismatch", + service: expected.name, + expected: String(expected.port), + observed: live.ports.length ? live.ports.join(", ") : "no declared port", + risk: "high", + evidence: [ + { source: "manifest", detail: `port=${expected.port}` }, + { source: "topology", detail: `ports=${live.ports.join(",") || "none"}` }, + ], + proposedAction: "reconcile service port and reverse-proxy target, then verify the customer-facing journey", + reversible: true, + }); + } + } + } + + for (const domain of intent.domains ?? []) { + const exists = project.sites.some(site => site.domain.toLowerCase() === domain.toLowerCase()); + if (!exists) { + findings.push({ + code: "domain_missing", + expected: domain, + observed: "no matching proxy site", + risk: "high", + evidence: [ + { source: "manifest", detail: `domain=${domain}` }, + { source: "proxy", detail: `project=${project.slug} has no matching site` }, + ], + proposedAction: "prepare DNS/proxy mutation for approval, then verify HTTPS and the declared journey", + reversible: true, + }); + } + } + + return findings; +} From ab233817cfb21f5df3c776a5e1310d593af6270d Mon Sep 17 00:00:00 2001 From: Edward Twumasi <45181928+teckedd-code2save@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:42:27 +0000 Subject: [PATCH 2/2] test: cover intent parsing and drift --- src/lib/intent-contract.test.ts | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/lib/intent-contract.test.ts diff --git a/src/lib/intent-contract.test.ts b/src/lib/intent-contract.test.ts new file mode 100644 index 0000000..b3a0ff9 --- /dev/null +++ b/src/lib/intent-contract.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { detectIntentDrift, parseGroundControlIntent } from "./intent-contract"; + +describe("groundcontrol intent contract", () => { + it("parses a minimal v1 manifest", () => { + const intent = parseGroundControlIntent(` +version: v1 +project: api +domains: + - api.example.com +services: + - name: web + image: ghcr.io/acme/api + port: 4000 + health: + path: /health +`); + expect(intent.project).toBe("api"); + expect(intent.services[0]?.port).toBe(4000); + }); + + it("rejects duplicate services", () => { + expect(() => parseGroundControlIntent(` +version: v1 +project: api +services: + - name: web + - name: web +`)).toThrow(/duplicate service web/); + }); + + it("returns deterministic service, port and domain drift with evidence", () => { + const intent = parseGroundControlIntent(` +version: v1 +project: api +domains: [api.example.com] +services: + - name: web + port: 4000 + - name: worker +`); + + const findings = detectIntentDrift(intent, { + projects: [{ + slug: "api", + name: "api", + path: "/opt/api", + parent: null, + hasGit: true, + services: [{ + service: "web", + image: "ghcr.io/acme/api", + build: false, + ports: ["3000:3000"], + }], + extraContainers: [], + sites: [], + }], + unclaimedContainers: [], + }); + + expect(findings.map(f => f.code)).toEqual([ + "port_mismatch", + "service_missing", + "domain_missing", + ]); + expect(findings.every(f => f.evidence.length > 0)).toBe(true); + expect(findings.every(f => f.reversible)).toBe(true); + }); +});