922a57fe34
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
/**
|
||
* workflow.yaml 解析與三元組驗證
|
||
*/
|
||
import yaml from 'js-yaml';
|
||
import { readFileSync } from 'node:fs';
|
||
|
||
export interface WorkflowYaml {
|
||
name: string;
|
||
description?: string;
|
||
flow: string[];
|
||
config?: Record<string, Record<string, unknown>>;
|
||
}
|
||
|
||
export interface ParsedTriplet {
|
||
subject: string;
|
||
relation: string;
|
||
object: string;
|
||
}
|
||
|
||
/** 合法關係詞(拒絕 PIPE)*/
|
||
const VALID_RELATIONS = new Set([
|
||
'完成後', '失敗時', '對每個', '條件滿足時',
|
||
'ON_SUCCESS', 'ON_FAIL', 'FOREACH', 'IF', 'ON_CLICK', 'CALLS_SUBFLOW',
|
||
]);
|
||
|
||
const BANNED_RELATIONS = new Set(['PIPE']);
|
||
|
||
export function loadWorkflowYaml(filePath: string): WorkflowYaml {
|
||
const raw = readFileSync(filePath, 'utf8');
|
||
const doc = yaml.load(raw) as WorkflowYaml;
|
||
|
||
if (!doc.name) throw new Error('workflow.yaml 缺少 name 欄位');
|
||
if (!Array.isArray(doc.flow) || doc.flow.length === 0) {
|
||
throw new Error('workflow.yaml 的 flow 欄位必須為非空陣列');
|
||
}
|
||
|
||
return doc;
|
||
}
|
||
|
||
export function parseTriplets(flow: string[]): ParsedTriplet[] {
|
||
const triplets: ParsedTriplet[] = [];
|
||
|
||
for (const line of flow) {
|
||
const parts = line.split('>>').map(s => s.trim());
|
||
if (parts.length !== 3) {
|
||
throw new Error(
|
||
`三元組格式錯誤:「${line}」\n` +
|
||
`正確格式:「A >> 關係詞 >> B」`
|
||
);
|
||
}
|
||
const [subject, relation, object] = parts;
|
||
triplets.push({ subject, relation, object });
|
||
}
|
||
|
||
return triplets;
|
||
}
|
||
|
||
export function validateRelations(triplets: ParsedTriplet[]): void {
|
||
for (const t of triplets) {
|
||
if (BANNED_RELATIONS.has(t.relation)) {
|
||
throw new Error(
|
||
`不允許使用關係詞「${t.relation}」。\n` +
|
||
`「PIPE」已棄用,請改用「完成後」或「ON_SUCCESS」。`
|
||
);
|
||
}
|
||
// 容許 FOREACH iterator 命名變體:「對每個 paragraph」/「FOREACH item」
|
||
// graph-builder.ts 已支援這個 regex(commit e8fca33 2026-05-07)
|
||
const foreachMatch = t.relation.match(/^(?:對每個|FOREACH)\s+\w+$/i);
|
||
if (foreachMatch) continue;
|
||
|
||
if (!VALID_RELATIONS.has(t.relation)) {
|
||
throw new Error(
|
||
`未知關係詞「${t.relation}」。\n` +
|
||
`合法關係詞:${[...VALID_RELATIONS].join('、')}\n` +
|
||
`(FOREACH 支援 iterator 命名:「對每個 X」/「FOREACH X」)`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
export function getNodeNames(triplets: ParsedTriplet[]): string[] {
|
||
const nodes = new Set<string>();
|
||
for (const t of triplets) {
|
||
nodes.add(t.subject);
|
||
nodes.add(t.object);
|
||
}
|
||
return [...nodes];
|
||
}
|