@agent-pattern-labs/iso-preflight
Advanced tools
+49
-7
| import { isJsonObject, isJsonValue } from "./json.js"; | ||
| const DEFAULT_SKIP_STATUSES = ["skip", "skipped"]; | ||
| const DEFAULT_BLOCK_STATUSES = ["block", "blocked", "fail", "failed"]; | ||
| const DEFAULT_PASS_STATUSES = ["pass", "passed", "ok", "allow", "allowed", "ready"]; | ||
| export function loadPreflightConfig(input) { | ||
@@ -11,5 +12,7 @@ if (!isJsonObject(input)) | ||
| throw new Error("preflight config workflows must be an array"); | ||
| const workflows = input.workflows.map((workflow, index) => normalizeWorkflow(workflow, `workflows[${index}]`)); | ||
| assertUnique(workflows.map((workflow) => workflow.name), "workflow name"); | ||
| return { | ||
| version: 1, | ||
| workflows: input.workflows.map((workflow, index) => normalizeWorkflow(workflow, `workflows[${index}]`)), | ||
| workflows, | ||
| }; | ||
@@ -34,2 +37,3 @@ } | ||
| const planned = candidateSet.candidates.map((candidate, index) => planCandidate(workflow, candidate, index)); | ||
| assertUnique(planned.map((candidate) => candidate.id), "candidate id"); | ||
| const blocked = planned.filter((candidate) => candidate.state === "blocked"); | ||
@@ -105,3 +109,5 @@ const skipped = planned.filter((candidate) => candidate.state === "skipped"); | ||
| throw new Error(`${label} must be an array`); | ||
| return input.map((gate, index) => normalizeGate(gate, `${label}[${index}]`)); | ||
| const gates = input.map((gate, index) => normalizeGate(gate, `${label}[${index}]`)); | ||
| assertUnique(gates.map((gate) => gate.id), "gate id"); | ||
| return gates; | ||
| } | ||
@@ -124,6 +130,23 @@ function normalizeGate(input, label) { | ||
| throw new Error(`${label} must be an object`); | ||
| return { | ||
| const policy = { | ||
| passStatuses: optionalStringArray(input.passStatuses, `${label}.passStatuses`), | ||
| skipStatuses: optionalStringArray(input.skipStatuses, `${label}.skipStatuses`), | ||
| blockStatuses: optionalStringArray(input.blockStatuses, `${label}.blockStatuses`), | ||
| }; | ||
| const groups = [ | ||
| ["pass", policy.passStatuses ?? DEFAULT_PASS_STATUSES], | ||
| ["skip", policy.skipStatuses ?? DEFAULT_SKIP_STATUSES], | ||
| ["block", policy.blockStatuses ?? DEFAULT_BLOCK_STATUSES], | ||
| ]; | ||
| const owners = new Map(); | ||
| for (const [group, statuses] of groups) { | ||
| for (const status of statuses) { | ||
| const normalized = normalizeStatus(status); | ||
| const owner = owners.get(normalized); | ||
| if (owner) | ||
| throw new Error(`${label} status "${status}" appears in both ${owner} and ${group} statuses`); | ||
| owners.set(normalized, group); | ||
| } | ||
| } | ||
| return policy; | ||
| } | ||
@@ -135,3 +158,5 @@ function optionalSteps(input, label) { | ||
| throw new Error(`${label} must be an array`); | ||
| return input.map((step, index) => normalizeStep(step, `${label}[${index}]`)); | ||
| const steps = input.map((step, index) => normalizeStep(step, `${label}[${index}]`)); | ||
| assertUnique(steps.map((step) => step.id), "step id"); | ||
| return steps; | ||
| } | ||
@@ -212,2 +237,3 @@ function normalizeStep(input, label) { | ||
| const blockStatuses = statusSet(workflow.gatePolicy?.blockStatuses ?? DEFAULT_BLOCK_STATUSES); | ||
| const passStatuses = statusSet(workflow.gatePolicy?.passStatuses ?? DEFAULT_PASS_STATUSES); | ||
| for (const gate of gates) { | ||
@@ -231,4 +257,12 @@ const status = normalizeStatus(gate.status); | ||
| } | ||
| else if (!passStatuses.has(status)) { | ||
| issues.push({ | ||
| kind: "unknown-gate-status", | ||
| gate: gate.id, | ||
| source: gate.source, | ||
| message: `gate "${gate.id}" has unknown status "${gate.status}"`, | ||
| }); | ||
| } | ||
| } | ||
| const state = issues.some((issue) => issue.kind === "missing-fact" || issue.kind === "missing-source" || issue.kind === "gate-block") | ||
| const state = issues.some((issue) => issue.kind === "missing-fact" || issue.kind === "missing-source" || issue.kind === "gate-block" || issue.kind === "unknown-gate-status") | ||
| ? "blocked" | ||
@@ -331,7 +365,15 @@ : issues.some((issue) => issue.kind === "gate-skip") | ||
| return undefined; | ||
| if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { | ||
| throw new Error(`${label} must be an array of strings`); | ||
| if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) { | ||
| throw new Error(`${label} must be an array of non-empty strings`); | ||
| } | ||
| return value; | ||
| } | ||
| function assertUnique(values, label) { | ||
| const seen = new Set(); | ||
| for (const value of values) { | ||
| if (seen.has(value)) | ||
| throw new Error(`duplicate ${label} "${value}"`); | ||
| seen.add(value); | ||
| } | ||
| } | ||
| //# sourceMappingURL=preflight.js.map |
+2
-1
@@ -25,2 +25,3 @@ export type JsonPrimitive = string | number | boolean | null; | ||
| export interface PreflightGatePolicy { | ||
| passStatuses?: string[]; | ||
| skipStatuses?: string[]; | ||
@@ -86,3 +87,3 @@ blockStatuses?: string[]; | ||
| export interface PreflightIssue { | ||
| kind: "missing-fact" | "missing-source" | "gate-skip" | "gate-block"; | ||
| kind: "missing-fact" | "missing-source" | "gate-skip" | "gate-block" | "unknown-gate-status"; | ||
| message: string; | ||
@@ -89,0 +90,0 @@ fact?: string; |
+1
-1
| { | ||
| "name": "@agent-pattern-labs/iso-preflight", | ||
| "version": "0.1.1", | ||
| "version": "0.1.2", | ||
| "description": "Deterministic preflight planning for AI-agent workflows: validate file-backed facts, apply gates, and produce bounded dispatch rounds without model calls.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+3
-0
@@ -68,2 +68,3 @@ # @agent-pattern-labs/iso-preflight | ||
| "gatePolicy": { | ||
| "passStatuses": ["pass"], | ||
| "skipStatuses": ["skip"], | ||
@@ -84,2 +85,4 @@ "blockStatuses": ["block", "fail"] | ||
| `sourceRequiredFacts` must use the object form with a source. | ||
| Gate statuses are fail-closed: every status must appear in the workflow's pass, | ||
| skip, or block allowlist. Duplicate candidate IDs are rejected. | ||
@@ -86,0 +89,0 @@ ```json |
38773
5.71%820
5.53%129
2.38%