d00f9df2dd
- Delete ConditionDefinition, Transition types from workflow-protocol - Add Target type, change graph to Record<string, Record<string, Target>> - Remove conditions from WorkflowPayload and WORKFLOW_SCHEMA - Replace jsonata with mustache in workflow-moderator - Rewrite evaluate() to simple map lookup + mustache render - Update cli-workflow to use new 3-arg evaluate(graph, role, output) - 296 tests pass, 0 fail Phase 1 of #490 (closes #491)
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
import { basename } from "node:path";
|
|
import type { CasRef, WorkflowPayload } from "@uncaged/workflow-protocol";
|
|
|
|
const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
|
|
|
|
export function isCasRef(value: string): value is CasRef {
|
|
return CAS_REF_PATTERN.test(value);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function isRoleDefinition(value: unknown): boolean {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
const frontmatter = value.frontmatter;
|
|
const frontmatterOk = isRecord(frontmatter) && typeof frontmatter.type === "string";
|
|
const capabilities = value.capabilities;
|
|
const capabilitiesOk =
|
|
Array.isArray(capabilities) && capabilities.every((c) => typeof c === "string");
|
|
return (
|
|
typeof value.description === "string" &&
|
|
typeof value.goal === "string" &&
|
|
capabilitiesOk &&
|
|
typeof value.procedure === "string" &&
|
|
typeof value.output === "string" &&
|
|
frontmatterOk
|
|
);
|
|
}
|
|
|
|
function isTarget(value: unknown): boolean {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
return (
|
|
typeof value.role === "string" && typeof value.prompt === "string" && value.prompt.trim() !== ""
|
|
);
|
|
}
|
|
|
|
function isStringRecord(value: unknown, itemCheck: (item: unknown) => boolean): boolean {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
return Object.values(value).every(itemCheck);
|
|
}
|
|
|
|
function isGraph(value: unknown): boolean {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
return Object.values(value).every(
|
|
(statusMap) => isRecord(statusMap) && Object.values(statusMap).every((t) => isTarget(t)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Derive the expected workflow name from a file path (stem without extension).
|
|
* Returns the stem for `.yaml` / `.yml` files.
|
|
*/
|
|
export function workflowNameFromPath(filePath: string): string {
|
|
const base = basename(filePath);
|
|
if (base.endsWith(".yaml")) return base.slice(0, -5);
|
|
if (base.endsWith(".yml")) return base.slice(0, -4);
|
|
return base;
|
|
}
|
|
|
|
/**
|
|
* Check that the `name` field in a parsed payload matches the expected name
|
|
* derived from the file path. Returns an error message string on mismatch,
|
|
* or null when the names are consistent.
|
|
*/
|
|
export function checkWorkflowFilenameConsistency(
|
|
filePath: string,
|
|
payload: WorkflowPayload,
|
|
): string | null {
|
|
const expected = workflowNameFromPath(filePath);
|
|
if (payload.name !== expected) {
|
|
return `workflow name mismatch: file "${basename(filePath)}" implies name "${expected}" but YAML declares name "${payload.name}"`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Validate YAML-parsed workflow document shape (outputSchema may be inline JSON Schema). */
|
|
export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
|
|
if (!isRecord(raw)) {
|
|
return null;
|
|
}
|
|
if (typeof raw.name !== "string" || typeof raw.description !== "string") {
|
|
return null;
|
|
}
|
|
if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
|
|
return null;
|
|
}
|
|
return raw as WorkflowPayload;
|
|
}
|