mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 12:20:48 +02:00
feat(gstack2): add unified execution result contract
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { errorWithCode } from "./errors.js";
|
||||
|
||||
export const EXECUTION_RESULT_SCHEMA_VERSION = 1;
|
||||
export const EXECUTION_RESULT_STATUSES = Object.freeze([
|
||||
"success",
|
||||
"degraded",
|
||||
"unsupported",
|
||||
"failed",
|
||||
]);
|
||||
|
||||
export const EXECUTION_RESULT_ERROR_CODES = Object.freeze({
|
||||
EMPTY: "EXECUTION_EMPTY",
|
||||
MALFORMED: "EXECUTION_MALFORMED",
|
||||
DEGRADED: "EXECUTION_DEGRADED",
|
||||
UNSUPPORTED: "EXECUTION_UNSUPPORTED",
|
||||
FAILED: "EXECUTION_FAILED",
|
||||
});
|
||||
|
||||
export const EXECUTION_RESULT_SCHEMA = Object.freeze({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
$id: "https://gstack.dev/schemas/execution-result-v1.json",
|
||||
title: "GStack execution result",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["schemaVersion", "status", "code", "summary", "evidence", "data"],
|
||||
properties: {
|
||||
schemaVersion: { const: EXECUTION_RESULT_SCHEMA_VERSION },
|
||||
status: { enum: EXECUTION_RESULT_STATUSES },
|
||||
code: { type: ["string", "null"], pattern: "^[A-Z][A-Z0-9_]{2,63}$" },
|
||||
summary: { type: "string", minLength: 1 },
|
||||
evidence: { type: "array", items: { type: "string", minLength: 1 } },
|
||||
data: {},
|
||||
},
|
||||
allOf: [
|
||||
{ if: { properties: { status: { const: "success" } } }, then: { properties: { code: { type: "null" }, evidence: { minItems: 1 } } } },
|
||||
{ if: { properties: { status: { enum: ["degraded", "unsupported", "failed"] } } }, then: { properties: { code: { type: "string" } } } },
|
||||
],
|
||||
});
|
||||
|
||||
/** Validate the host-neutral result before any caller can render it as success. */
|
||||
export function validateExecutionResult(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid("Execution result must be an object");
|
||||
const keys = Object.keys(value).sort();
|
||||
const expected = ["code", "data", "evidence", "schemaVersion", "status", "summary"];
|
||||
if (JSON.stringify(keys) !== JSON.stringify(expected)) return invalid("Execution result contains missing or unknown fields");
|
||||
if (value.schemaVersion !== EXECUTION_RESULT_SCHEMA_VERSION) return invalid("Execution result schema version is unsupported");
|
||||
if (!EXECUTION_RESULT_STATUSES.includes(value.status)) return invalid("Execution result status is unsupported");
|
||||
if (typeof value.summary !== "string" || value.summary.trim().length === 0) return invalid("Execution result summary is empty");
|
||||
if (!Array.isArray(value.evidence) || value.evidence.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
||||
return invalid("Execution result evidence is malformed");
|
||||
}
|
||||
if (value.status === "success") {
|
||||
if (value.code !== null) return invalid("Successful execution must not carry an error code");
|
||||
if (value.evidence.length === 0) return invalid("Successful execution requires evidence", EXECUTION_RESULT_ERROR_CODES.EMPTY);
|
||||
} else if (typeof value.code !== "string" || !/^[A-Z][A-Z0-9_]{2,63}$/.test(value.code)) {
|
||||
return invalid("Non-success execution requires a stable error code");
|
||||
}
|
||||
return Object.freeze({ ...value, evidence: Object.freeze([...value.evidence]) });
|
||||
}
|
||||
|
||||
export function executionResult(input) {
|
||||
return validateExecutionResult({
|
||||
schemaVersion: EXECUTION_RESULT_SCHEMA_VERSION,
|
||||
status: input.status,
|
||||
code: input.code ?? null,
|
||||
summary: input.summary,
|
||||
evidence: input.evidence ?? [],
|
||||
data: input.data ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function renderExecutionResult(result, options = {}) {
|
||||
const valid = validateExecutionResult(result);
|
||||
if (options.json) return `${JSON.stringify(valid, null, 2)}\n`;
|
||||
const label = valid.status.toUpperCase();
|
||||
const code = valid.code ? ` [${valid.code}]` : "";
|
||||
const evidence = valid.evidence.map((item) => `- ${item}`).join("\n");
|
||||
return `${label}${code}: ${valid.summary}${evidence ? `\nEvidence:\n${evidence}` : ""}\n`;
|
||||
}
|
||||
|
||||
export function assertSuccessfulExecution(result) {
|
||||
const valid = validateExecutionResult(result);
|
||||
if (valid.status === "success") return valid;
|
||||
throw errorWithCode(valid.summary, valid.code);
|
||||
}
|
||||
|
||||
function invalid(message, code = EXECUTION_RESULT_ERROR_CODES.MALFORMED) {
|
||||
throw errorWithCode(message, code);
|
||||
}
|
||||
Reference in New Issue
Block a user