e067a2f25a
CI / check (pull_request) Failing after 9m51s
Package mapping: - @uncaged/cli-workflow → @united-workforce/cli - @uncaged/workflow-protocol → @united-workforce/protocol - @uncaged/workflow-util → @united-workforce/util - @uncaged/workflow-util-agent → @united-workforce/util-agent - @uncaged/workflow-agent-hermes → @united-workforce/agent-hermes - @uncaged/workflow-agent-claude-code → @united-workforce/agent-claude-code - @uncaged/workflow-agent-builtin → @united-workforce/agent-builtin - @uncaged/workflow-dashboard → @united-workforce/dashboard Changes: - 8 package.json name + dependency refs - 82 files: import statements updated - .changeset/config.json updated - CLAUDE.md updated - bunfig.toml restored for preload CLI command (uwf) and directory names unchanged. Closes shazhou/united-workforce#8
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
import { basename, dirname } from "node:path";
|
|
import type { CasRef, WorkflowPayload } from "@united-workforce/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" || Array.isArray(frontmatter.oneOf));
|
|
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;
|
|
}
|
|
const hasValidLocation =
|
|
value.location === undefined || value.location === null || typeof value.location === "string";
|
|
return (
|
|
typeof value.role === "string" &&
|
|
typeof value.prompt === "string" &&
|
|
value.prompt.trim() !== "" &&
|
|
hasValidLocation
|
|
);
|
|
}
|
|
|
|
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);
|
|
const stem = base.endsWith(".yaml")
|
|
? base.slice(0, -5)
|
|
: base.endsWith(".yml")
|
|
? base.slice(0, -4)
|
|
: base;
|
|
if (stem === "index") {
|
|
return basename(dirname(filePath));
|
|
}
|
|
return stem;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
// Normalize location field: undefined → null
|
|
const normalized = { ...raw } as WorkflowPayload;
|
|
for (const roleName of Object.keys(normalized.graph)) {
|
|
const statusMap = normalized.graph[roleName];
|
|
if (statusMap !== undefined) {
|
|
for (const status of Object.keys(statusMap)) {
|
|
const target = statusMap[status];
|
|
if (target !== undefined) {
|
|
if (target.location === undefined) {
|
|
target.location = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|