feat: add factory capability registry
This commit is contained in:
parent
32c99f0584
commit
0c98e82b72
|
|
@ -154,6 +154,8 @@ fable-agent plinius godmode "improve explanation quality"
|
||||||
### `factory`
|
### `factory`
|
||||||
|
|
||||||
- `factory check`
|
- `factory check`
|
||||||
|
- `factory capabilities`
|
||||||
|
- `--file <path>`
|
||||||
- `factory gate <task>`
|
- `factory gate <task>`
|
||||||
- `--repo <path>`
|
- `--repo <path>`
|
||||||
- `--diagnostic-only`
|
- `--diagnostic-only`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
deploy-webhook:
|
||||||
|
url: http://77.42.112.29:8098/health
|
||||||
|
provides: [deploy]
|
||||||
|
required: true
|
||||||
|
git-proxy:
|
||||||
|
url: http://77.42.112.29:8099/factory-status
|
||||||
|
provides: [factory-status, discovery]
|
||||||
|
required: true
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatCapabilityReport, parseFactoryCapabilities, probeFactoryCapabilities } from "./factory-capabilities.js";
|
||||||
|
|
||||||
|
const yaml = `services:
|
||||||
|
deploy-webhook:
|
||||||
|
url: http://77.42.112.29:8098/health
|
||||||
|
provides: [deploy]
|
||||||
|
required: true
|
||||||
|
git-proxy:
|
||||||
|
url: http://77.42.112.29:8099/factory-status
|
||||||
|
provides: [factory-status, discovery]
|
||||||
|
required: true
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("factory capabilities", () => {
|
||||||
|
it("parses the capability registry", () => {
|
||||||
|
const services = parseFactoryCapabilities(yaml);
|
||||||
|
expect(services).toHaveLength(2);
|
||||||
|
expect(services[0]).toMatchObject({ name: "deploy-webhook", required: true, provides: ["deploy"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails required capabilities that return 404", async () => {
|
||||||
|
const services = parseFactoryCapabilities(yaml);
|
||||||
|
const report = await probeFactoryCapabilities(services, async (url) => ({ ok: !url.includes("8099"), status: url.includes("8099") ? 404 : 200 }));
|
||||||
|
|
||||||
|
expect(report.requiredOk).toBe(false);
|
||||||
|
expect(report.available).toContain("deploy");
|
||||||
|
expect(report.missing).toContain("factory-status");
|
||||||
|
expect(formatCapabilityReport(report)).toContain("FAIL git-proxy provides factory-status, discovery: HTTP 404");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
|
export interface FactoryCapabilityService {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
provides: string[];
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapabilityProbeResult extends FactoryCapabilityService {
|
||||||
|
ok: boolean;
|
||||||
|
status?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapabilityReport {
|
||||||
|
results: CapabilityProbeResult[];
|
||||||
|
available: string[];
|
||||||
|
missing: string[];
|
||||||
|
requiredOk: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchLike = (url: string) => Promise<{ ok: boolean; status: number }>;
|
||||||
|
|
||||||
|
export function parseFactoryCapabilities(text: string): FactoryCapabilityService[] {
|
||||||
|
const services: FactoryCapabilityService[] = [];
|
||||||
|
let current: Partial<FactoryCapabilityService> | null = null;
|
||||||
|
|
||||||
|
for (const raw of text.split(/\r?\n/)) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line || line === "services:" || line.startsWith("#")) continue;
|
||||||
|
const service = raw.match(/^ ([A-Za-z0-9_-]+):\s*$/);
|
||||||
|
if (service) {
|
||||||
|
if (current?.name) services.push(normalizeService(current));
|
||||||
|
current = { name: service[1] };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!current) continue;
|
||||||
|
const prop = line.match(/^(url|provides|required):\s*(.*)$/);
|
||||||
|
if (!prop) continue;
|
||||||
|
const [, key, value] = prop;
|
||||||
|
if (key === "url") current.url = value;
|
||||||
|
if (key === "required") current.required = value === "true";
|
||||||
|
if (key === "provides") current.provides = value.replace(/^\[/, "").replace(/\]$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (current?.name) services.push(normalizeService(current));
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadFactoryCapabilities(file: string): FactoryCapabilityService[] {
|
||||||
|
return parseFactoryCapabilities(fs.readFileSync(file, "utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function probeFactoryCapabilities(services: FactoryCapabilityService[], fetcher: FetchLike = fetch): Promise<CapabilityReport> {
|
||||||
|
const results = await Promise.all(services.map(async (svc): Promise<CapabilityProbeResult> => {
|
||||||
|
try {
|
||||||
|
const res = await fetcher(svc.url);
|
||||||
|
return { ...svc, ok: res.ok, status: res.status, error: res.ok ? undefined : `HTTP ${res.status}` };
|
||||||
|
} catch (error) {
|
||||||
|
return { ...svc, ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const available = [...new Set(results.filter((r) => r.ok).flatMap((r) => r.provides))];
|
||||||
|
const missing = [...new Set(results.filter((r) => !r.ok).flatMap((r) => r.provides))];
|
||||||
|
return { results, available, missing, requiredOk: results.every((r) => !r.required || r.ok) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCapabilityReport(report: CapabilityReport): string {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
" Factory Capabilities",
|
||||||
|
" ─────────────────────────────",
|
||||||
|
...report.results.map((r) => ` ${r.ok ? "OK " : "FAIL"} ${r.name} provides ${r.provides.join(", ")}${r.ok ? "" : `: ${r.error ?? `HTTP ${r.status ?? "unknown"}`}`}`),
|
||||||
|
"",
|
||||||
|
` available: ${report.available.join(", ") || "none"}`,
|
||||||
|
` missing: ${report.missing.join(", ") || "none"}`,
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeService(svc: Partial<FactoryCapabilityService>): FactoryCapabilityService {
|
||||||
|
return {
|
||||||
|
name: svc.name ?? "unknown",
|
||||||
|
url: svc.url ?? "",
|
||||||
|
provides: svc.provides ?? [],
|
||||||
|
required: svc.required ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -43,3 +43,6 @@ export type { ToolPolicy, EffectiveToolPolicy } from "./orchestrator-policy.js";
|
||||||
|
|
||||||
export { appendZteReceipt, createZteReceipt, createZteSpec } from "./zte-protocol.js";
|
export { appendZteReceipt, createZteReceipt, createZteSpec } from "./zte-protocol.js";
|
||||||
export type { ZteReceipt, ZteSpec, ZteSpecOptions } from "./zte-protocol.js";
|
export type { ZteReceipt, ZteSpec, ZteSpecOptions } from "./zte-protocol.js";
|
||||||
|
|
||||||
|
export { formatCapabilityReport, loadFactoryCapabilities, parseFactoryCapabilities, probeFactoryCapabilities } from "./factory-capabilities.js";
|
||||||
|
export type { CapabilityProbeResult, CapabilityReport, FactoryCapabilityService } from "./factory-capabilities.js";
|
||||||
|
|
|
||||||
12
src/index.ts
12
src/index.ts
|
|
@ -1440,6 +1440,18 @@ factory
|
||||||
await printFactoryCheck();
|
await printFactoryCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory
|
||||||
|
.command("capabilities")
|
||||||
|
.description("Probe the factory capability registry")
|
||||||
|
.option("--file <path>", "Capability registry path", "factory-capabilities.yaml")
|
||||||
|
.action(async (opts: { file?: string }) => {
|
||||||
|
const { formatCapabilityReport, loadFactoryCapabilities, probeFactoryCapabilities } = await import("./fable5/factory-capabilities.js");
|
||||||
|
const services = loadFactoryCapabilities(path.resolve(opts.file ?? "factory-capabilities.yaml"));
|
||||||
|
const report = await probeFactoryCapabilities(services);
|
||||||
|
console.log(formatCapabilityReport(report));
|
||||||
|
if (!report.requiredOk) process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
factory
|
factory
|
||||||
.command("gate <task>")
|
.command("gate <task>")
|
||||||
.description("Run factory check plus local ZTE/repo verification gate")
|
.description("Run factory check plus local ZTE/repo verification gate")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue