@united-workforce/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- package/dist/__tests__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +20 -23
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- package/dist/__tests__/pid-recycling.test.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +271 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +321 -0
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +4 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +24 -27
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +499 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +9 -9
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +6 -6
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-list-filters.test.js +344 -9
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +412 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.js +10 -14
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +8 -14
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +4 -4
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +49 -21
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +283 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +36 -21
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +210 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +687 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- package/dist/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +66 -31
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +7 -33
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +15 -2
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +7 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +27 -302
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +379 -140
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +130 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.js.map +1 -1
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +83 -66
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -1
- package/package.json +8 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +20 -23
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/pid-recycling.test.ts +328 -0
- package/src/__tests__/prompt.test.ts +397 -0
- package/src/__tests__/resolve-head-hash.test.ts +4 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +24 -30
- package/src/__tests__/step-ask.test.ts +670 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +9 -9
- package/src/__tests__/thread-cancel-status.test.ts +6 -6
- package/src/__tests__/thread-list-filters.test.ts +434 -8
- package/src/__tests__/thread-poke.test.ts +545 -0
- package/src/__tests__/thread-resume.test.ts +10 -14
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-suspend-step.test.ts +8 -14
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread.test.ts +4 -4
- package/src/__tests__/validate-semantic.test.ts +59 -31
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +39 -21
- package/src/__tests__/workflow-show-resolution.test.ts +285 -0
- package/src/__tests__/workflow-validate.test.ts +806 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +97 -47
- package/src/commands/config.ts +7 -35
- package/src/commands/prompt.ts +15 -2
- package/src/commands/setup.ts +29 -357
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +463 -169
- package/src/commands/workflow.ts +159 -4
- package/src/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/schemas.ts +13 -3
- package/src/store.ts +86 -20
- package/src/validate-semantic.ts +109 -78
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
package/src/validate-semantic.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import type { WorkflowPayload } from "@united-workforce/protocol";
|
|
2
|
+
import { Liquid } from "liquidjs";
|
|
2
3
|
|
|
3
4
|
type SchemaObj = Record<string, unknown>;
|
|
4
5
|
|
|
5
6
|
const RESERVED_NAMES = new Set(["$START", "$END", "$SUSPEND"]);
|
|
6
|
-
const PSEUDO_TARGETS = new Set(["$END"
|
|
7
|
-
|
|
8
|
-
/** Extract mustache variable names from a prompt string. */
|
|
9
|
-
function extractMustacheVars(prompt: string): string[] {
|
|
10
|
-
const vars: string[] = [];
|
|
11
|
-
const re = /\{\{\{?([^}]+)\}\}\}?/g;
|
|
12
|
-
let m: RegExpExecArray | null = re.exec(prompt);
|
|
13
|
-
while (m !== null) {
|
|
14
|
-
vars.push(m[1]);
|
|
15
|
-
m = re.exec(prompt);
|
|
16
|
-
}
|
|
17
|
-
return vars;
|
|
18
|
-
}
|
|
7
|
+
const PSEUDO_TARGETS = new Set(["$END"]);
|
|
8
|
+
const SUSPEND_TARGET = "$SUSPEND";
|
|
19
9
|
|
|
20
10
|
/** Check if a frontmatter schema is a oneOf (multi-exit) type. */
|
|
21
11
|
function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } {
|
|
@@ -42,13 +32,6 @@ function getConstStatuses(fm: SchemaObj): string[] {
|
|
|
42
32
|
return [];
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
/** Get property names from a schema object. */
|
|
46
|
-
function getPropertyNames(schema: SchemaObj): Set<string> {
|
|
47
|
-
const props = schema.properties;
|
|
48
|
-
if (typeof props !== "object" || props === null) return new Set();
|
|
49
|
-
return new Set(Object.keys(props as Record<string, unknown>));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
35
|
/** Extract $status const values from oneOf variants. */
|
|
53
36
|
function getOneOfStatuses(variants: SchemaObj[]): string[] {
|
|
54
37
|
const statuses: string[] = [];
|
|
@@ -64,6 +47,83 @@ function getOneOfStatuses(variants: SchemaObj[]): string[] {
|
|
|
64
47
|
return statuses;
|
|
65
48
|
}
|
|
66
49
|
|
|
50
|
+
/** Generate mock data from schema property names for template rendering. */
|
|
51
|
+
function generateMockData(schema: SchemaObj): Record<string, string> {
|
|
52
|
+
const mock: Record<string, string> = {};
|
|
53
|
+
const props = schema.properties as Record<string, SchemaObj> | undefined;
|
|
54
|
+
if (!props) return mock;
|
|
55
|
+
for (const key of Object.keys(props)) {
|
|
56
|
+
mock[key] = `mock_${key}`;
|
|
57
|
+
}
|
|
58
|
+
return mock;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pre-process a template to replace `$`-prefixed variables (like `$status`)
|
|
63
|
+
* which are invalid in LiquidJS syntax but always valid at runtime.
|
|
64
|
+
* Replaces `{{ $varName }}` with a literal placeholder so the strict render
|
|
65
|
+
* does not reject them.
|
|
66
|
+
*/
|
|
67
|
+
function sanitizeReservedVars(template: string): string {
|
|
68
|
+
return template.replace(/\{\{\s*\$\w+\s*\}\}/g, "RESERVED");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Extract variable name from a LiquidJS UndefinedVariableError message. */
|
|
72
|
+
function extractVarName(err: unknown): string {
|
|
73
|
+
const msg = String(err);
|
|
74
|
+
const match = msg.match(/undefined variable: ([^,\s]+)/);
|
|
75
|
+
return match ? match[1] : "unknown";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Validate edge templates using LiquidJS strict-render for a multi-exit role. */
|
|
79
|
+
function validateMultiExitTemplates(
|
|
80
|
+
roleName: string,
|
|
81
|
+
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
82
|
+
variants: SchemaObj[],
|
|
83
|
+
errors: string[],
|
|
84
|
+
): void {
|
|
85
|
+
const strictEngine = new Liquid({ strictVariables: true });
|
|
86
|
+
|
|
87
|
+
for (const [status, target] of Object.entries(graphEntry)) {
|
|
88
|
+
const variant = variants.find((v) => {
|
|
89
|
+
const props = v.properties as Record<string, SchemaObj> | undefined;
|
|
90
|
+
return props?.$status?.const === status;
|
|
91
|
+
});
|
|
92
|
+
if (!variant) continue;
|
|
93
|
+
const mockData = generateMockData(variant);
|
|
94
|
+
try {
|
|
95
|
+
strictEngine.parseAndRenderSync(sanitizeReservedVars(target.prompt), mockData);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const varName = extractVarName(err);
|
|
98
|
+
errors.push(
|
|
99
|
+
`template variable "${varName}" not found in role "${roleName}" variant "${status}"`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Validate edge templates using LiquidJS strict-render for a flat schema. */
|
|
106
|
+
function validateFlatTemplates(
|
|
107
|
+
roleName: string,
|
|
108
|
+
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
109
|
+
fm: SchemaObj,
|
|
110
|
+
errors: string[],
|
|
111
|
+
): void {
|
|
112
|
+
const strictEngine = new Liquid({ strictVariables: true });
|
|
113
|
+
const mockData = generateMockData(fm);
|
|
114
|
+
|
|
115
|
+
for (const [status, target] of Object.entries(graphEntry)) {
|
|
116
|
+
try {
|
|
117
|
+
strictEngine.parseAndRenderSync(sanitizeReservedVars(target.prompt), mockData);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
const varName = extractVarName(err);
|
|
120
|
+
errors.push(
|
|
121
|
+
`template variable "${varName}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
67
127
|
/** Check reserved names and role/graph reference integrity. */
|
|
68
128
|
function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
|
|
69
129
|
const roleNames = new Set(Object.keys(payload.roles));
|
|
@@ -89,6 +149,27 @@ function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
|
|
|
89
149
|
}
|
|
90
150
|
}
|
|
91
151
|
|
|
152
|
+
/** Validate each graph edge's target role, including the removed $SUSPEND target. */
|
|
153
|
+
function checkEdgeTargets(
|
|
154
|
+
payload: WorkflowPayload,
|
|
155
|
+
roleNames: Set<string>,
|
|
156
|
+
errors: string[],
|
|
157
|
+
): void {
|
|
158
|
+
for (const [node, statusMap] of Object.entries(payload.graph)) {
|
|
159
|
+
for (const [status, target] of Object.entries(statusMap)) {
|
|
160
|
+
if (target.role === SUSPEND_TARGET) {
|
|
161
|
+
errors.push(
|
|
162
|
+
`edge ${node}→${status}: "${SUSPEND_TARGET}" is no longer a valid graph target. Emit $status: "${SUSPEND_TARGET}" from the "${node}" role output instead.`,
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!PSEUDO_TARGETS.has(target.role) && !roleNames.has(target.role)) {
|
|
167
|
+
errors.push(`edge ${node}→${status}: unknown target role "${target.role}"`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
92
173
|
/** Check $START/$END constraints, edge targets, and reachability. */
|
|
93
174
|
function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
|
|
94
175
|
const roleNames = new Set(Object.keys(payload.roles));
|
|
@@ -107,17 +188,13 @@ function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
|
|
|
107
188
|
errors.push("$END must not have outgoing edges");
|
|
108
189
|
}
|
|
109
190
|
|
|
110
|
-
if (graphNodes.has(
|
|
111
|
-
errors.push(
|
|
191
|
+
if (graphNodes.has(SUSPEND_TARGET)) {
|
|
192
|
+
errors.push(
|
|
193
|
+
`"${SUSPEND_TARGET}" is no longer a valid graph node — it is now an engine-level reserved $status. Emit $status: "${SUSPEND_TARGET}" from a role output instead.`,
|
|
194
|
+
);
|
|
112
195
|
}
|
|
113
196
|
|
|
114
|
-
|
|
115
|
-
for (const [status, target] of Object.entries(statusMap)) {
|
|
116
|
-
if (!PSEUDO_TARGETS.has(target.role) && !roleNames.has(target.role)) {
|
|
117
|
-
errors.push(`edge ${node}→${status}: unknown target role "${target.role}"`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
197
|
+
checkEdgeTargets(payload, roleNames, errors);
|
|
121
198
|
|
|
122
199
|
checkReachability(roleNames, collectReachableRoles(payload.graph), errors);
|
|
123
200
|
}
|
|
@@ -207,31 +284,7 @@ function checkStatusEdges(
|
|
|
207
284
|
}
|
|
208
285
|
}
|
|
209
286
|
|
|
210
|
-
/** Check
|
|
211
|
-
function checkMultiExitMustache(
|
|
212
|
-
roleName: string,
|
|
213
|
-
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
214
|
-
variants: SchemaObj[],
|
|
215
|
-
errors: string[],
|
|
216
|
-
): void {
|
|
217
|
-
for (const [status, target] of Object.entries(graphEntry)) {
|
|
218
|
-
const vars = extractMustacheVars(target.prompt);
|
|
219
|
-
const variant = variants.find((v) => {
|
|
220
|
-
const props = v.properties as Record<string, SchemaObj> | undefined;
|
|
221
|
-
return props?.$status?.const === status;
|
|
222
|
-
});
|
|
223
|
-
if (!variant) continue;
|
|
224
|
-
const propNames = getPropertyNames(variant);
|
|
225
|
-
for (const v of vars) {
|
|
226
|
-
if (v === "$status") continue;
|
|
227
|
-
if (!propNames.has(v)) {
|
|
228
|
-
errors.push(`prompt variable "${v}" not found in role "${roleName}" variant "${status}"`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Check status-edge consistency and mustache for each role. */
|
|
287
|
+
/** Check status-edge consistency and template vars for each role. */
|
|
235
288
|
function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void {
|
|
236
289
|
for (const [roleName, role] of Object.entries(payload.roles)) {
|
|
237
290
|
if (RESERVED_NAMES.has(roleName)) continue;
|
|
@@ -247,12 +300,11 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
|
|
|
247
300
|
|
|
248
301
|
checkOneOfDiscriminant(roleName, variants, statuses, errors);
|
|
249
302
|
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
|
250
|
-
|
|
303
|
+
validateMultiExitTemplates(roleName, graphEntry, variants, errors);
|
|
251
304
|
} else if (hasStatusConst(fm)) {
|
|
252
305
|
const statuses = getConstStatuses(fm as SchemaObj);
|
|
253
306
|
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
|
254
|
-
|
|
255
|
-
checkFlatMustache(roleName, graphEntry, fm as SchemaObj, errors);
|
|
307
|
+
validateFlatTemplates(roleName, graphEntry, fm as SchemaObj, errors);
|
|
256
308
|
} else {
|
|
257
309
|
errors.push(
|
|
258
310
|
`role "${roleName}" must define "$status" as const (or oneOf with const) in frontmatter`,
|
|
@@ -261,27 +313,6 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
|
|
|
261
313
|
}
|
|
262
314
|
}
|
|
263
315
|
|
|
264
|
-
/** Check mustache vars in all edge prompts against flat schema properties. */
|
|
265
|
-
function checkFlatMustache(
|
|
266
|
-
roleName: string,
|
|
267
|
-
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
268
|
-
fm: SchemaObj,
|
|
269
|
-
errors: string[],
|
|
270
|
-
): void {
|
|
271
|
-
const propNames = getPropertyNames(fm);
|
|
272
|
-
for (const [status, target] of Object.entries(graphEntry)) {
|
|
273
|
-
const vars = extractMustacheVars(target.prompt);
|
|
274
|
-
for (const v of vars) {
|
|
275
|
-
if (v === "$status") continue;
|
|
276
|
-
if (!propNames.has(v)) {
|
|
277
|
-
errors.push(
|
|
278
|
-
`prompt variable "${v}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
316
|
/**
|
|
286
317
|
* Validate a parsed WorkflowPayload for semantic correctness.
|
|
287
318
|
* Returns an array of error messages. Empty array = valid.
|
package/src/validate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename, dirname } from "node:path";
|
|
2
2
|
import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
|
|
3
|
+
import { CURRENT_WORKFLOW_VERSION } from "@united-workforce/protocol";
|
|
3
4
|
|
|
4
5
|
const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
|
|
5
6
|
|
|
@@ -113,12 +114,26 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
|
|
|
113
114
|
if (typeof raw.name !== "string" || typeof raw.description !== "string") {
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
117
|
+
// version is optional in legacy YAML — falls back to CURRENT_WORKFLOW_VERSION.
|
|
118
|
+
// When present, it MUST be an integer (booleans, strings, floats are rejected).
|
|
119
|
+
if (raw.version !== undefined) {
|
|
120
|
+
if (
|
|
121
|
+
typeof raw.version !== "number" ||
|
|
122
|
+
!Number.isInteger(raw.version) ||
|
|
123
|
+
typeof raw.version === "boolean"
|
|
124
|
+
) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
116
128
|
if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
|
|
117
129
|
return null;
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
// Normalize location field: undefined → null
|
|
121
133
|
const normalized = { ...raw } as WorkflowPayload;
|
|
134
|
+
if (normalized.version === undefined || normalized.version === null) {
|
|
135
|
+
normalized.version = CURRENT_WORKFLOW_VERSION;
|
|
136
|
+
}
|
|
122
137
|
for (const roleName of Object.keys(normalized.graph)) {
|
|
123
138
|
const statusMap = normalized.graph[roleName];
|
|
124
139
|
if (statusMap !== undefined) {
|
|
@@ -135,3 +150,15 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
|
|
|
135
150
|
|
|
136
151
|
return normalized;
|
|
137
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns true when the parsed YAML document had no top-level `version` field.
|
|
156
|
+
* Used by `uwf workflow add` to emit a deprecation warning while still
|
|
157
|
+
* accepting legacy workflow YAML.
|
|
158
|
+
*/
|
|
159
|
+
export function isMissingVersion(raw: unknown): boolean {
|
|
160
|
+
if (!isRecord(raw)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return raw.version === undefined;
|
|
164
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup-validate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup-validate.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
5
|
-
import { cmdSetup, validateModel } from "../commands/setup.js";
|
|
6
|
-
describe("validateModel", () => {
|
|
7
|
-
const BASE_URL = "https://api.example.com/v1";
|
|
8
|
-
const API_KEY = "sk-test-key";
|
|
9
|
-
const MODEL = "test-model";
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
vi.restoreAllMocks();
|
|
12
|
-
});
|
|
13
|
-
test("success path — returns ok on 200", async () => {
|
|
14
|
-
const mockFetch = vi
|
|
15
|
-
.spyOn(globalThis, "fetch")
|
|
16
|
-
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
17
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
18
|
-
expect(result).toEqual({ ok: true, value: undefined });
|
|
19
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
20
|
-
const [url, opts] = mockFetch.mock.calls[0];
|
|
21
|
-
expect(url).toBe(`${BASE_URL}/chat/completions`);
|
|
22
|
-
expect(opts.headers).toEqual(expect.objectContaining({ Authorization: `Bearer ${API_KEY}` }));
|
|
23
|
-
const body = JSON.parse(opts.body);
|
|
24
|
-
expect(body).toEqual({
|
|
25
|
-
model: MODEL,
|
|
26
|
-
messages: [{ role: "user", content: "hi" }],
|
|
27
|
-
max_tokens: 1,
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
test("HTTP 401 — returns error containing 401", async () => {
|
|
31
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }));
|
|
32
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
33
|
-
expect(result.ok).toBe(false);
|
|
34
|
-
if (!result.ok) {
|
|
35
|
-
expect(result.error).toContain("401");
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
test("HTTP 404 — returns error containing 404", async () => {
|
|
39
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Not Found", { status: 404, statusText: "Not Found" }));
|
|
40
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
41
|
-
expect(result.ok).toBe(false);
|
|
42
|
-
if (!result.ok) {
|
|
43
|
-
expect(result.error).toContain("404");
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
test("network timeout — returns error mentioning timeout", async () => {
|
|
47
|
-
const err = new DOMException("signal timed out", "AbortError");
|
|
48
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(err);
|
|
49
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
50
|
-
expect(result.ok).toBe(false);
|
|
51
|
-
if (!result.ok) {
|
|
52
|
-
expect(result.error.toLowerCase()).toMatch(/timeout|timed out/);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
test("network error (DNS/connection) — returns error mentioning connectivity", async () => {
|
|
56
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed"));
|
|
57
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
58
|
-
expect(result.ok).toBe(false);
|
|
59
|
-
if (!result.ok) {
|
|
60
|
-
expect(result.error.toLowerCase()).toMatch(/connect|reach|network/);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
test("request body correctness", async () => {
|
|
64
|
-
const mockFetch = vi
|
|
65
|
-
.spyOn(globalThis, "fetch")
|
|
66
|
-
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
67
|
-
await validateModel(BASE_URL, API_KEY, "my-special-model");
|
|
68
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
69
|
-
expect(body).toEqual({
|
|
70
|
-
model: "my-special-model",
|
|
71
|
-
messages: [{ role: "user", content: "hi" }],
|
|
72
|
-
max_tokens: 1,
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
describe("cmdSetup with validation", () => {
|
|
77
|
-
let storageRoot;
|
|
78
|
-
beforeEach(async () => {
|
|
79
|
-
storageRoot = await mkdtemp(join(tmpdir(), "uwf-setup-validate-"));
|
|
80
|
-
});
|
|
81
|
-
afterEach(async () => {
|
|
82
|
-
vi.restoreAllMocks();
|
|
83
|
-
await rm(storageRoot, { recursive: true, force: true });
|
|
84
|
-
});
|
|
85
|
-
const setupArgs = () => ({
|
|
86
|
-
provider: "testprovider",
|
|
87
|
-
baseUrl: "https://api.test.com/v1",
|
|
88
|
-
apiKey: "sk-test",
|
|
89
|
-
model: "test-model",
|
|
90
|
-
storageRoot,
|
|
91
|
-
});
|
|
92
|
-
test("includes validation result on success", async () => {
|
|
93
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
94
|
-
const result = await cmdSetup(setupArgs());
|
|
95
|
-
expect(result.validation).toEqual({ ok: true, value: undefined });
|
|
96
|
-
// Config file should still be written
|
|
97
|
-
expect(result.configPath).toBeTruthy();
|
|
98
|
-
});
|
|
99
|
-
test("includes validation failure — config still saved", async () => {
|
|
100
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }));
|
|
101
|
-
const result = await cmdSetup(setupArgs());
|
|
102
|
-
expect(result.validation).toBeDefined();
|
|
103
|
-
expect(result.validation.ok).toBe(false);
|
|
104
|
-
// Config file should still be written despite validation failure
|
|
105
|
-
expect(result.configPath).toBeTruthy();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
//# sourceMappingURL=setup-validate.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup-validate.test.js","sourceRoot":"","sources":["../../src/__tests__/setup-validate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
5
|
-
import { cmdSetup, validateModel } from "../commands/setup.js";
|
|
6
|
-
|
|
7
|
-
describe("validateModel", () => {
|
|
8
|
-
const BASE_URL = "https://api.example.com/v1";
|
|
9
|
-
const API_KEY = "sk-test-key";
|
|
10
|
-
const MODEL = "test-model";
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
vi.restoreAllMocks();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("success path — returns ok on 200", async () => {
|
|
17
|
-
const mockFetch = vi
|
|
18
|
-
.spyOn(globalThis, "fetch")
|
|
19
|
-
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
20
|
-
|
|
21
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
22
|
-
|
|
23
|
-
expect(result).toEqual({ ok: true, value: undefined });
|
|
24
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
25
|
-
|
|
26
|
-
const [url, opts] = mockFetch.mock.calls[0]!;
|
|
27
|
-
expect(url).toBe(`${BASE_URL}/chat/completions`);
|
|
28
|
-
expect((opts as RequestInit).headers).toEqual(
|
|
29
|
-
expect.objectContaining({ Authorization: `Bearer ${API_KEY}` }),
|
|
30
|
-
);
|
|
31
|
-
const body = JSON.parse((opts as RequestInit).body as string);
|
|
32
|
-
expect(body).toEqual({
|
|
33
|
-
model: MODEL,
|
|
34
|
-
messages: [{ role: "user", content: "hi" }],
|
|
35
|
-
max_tokens: 1,
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("HTTP 401 — returns error containing 401", async () => {
|
|
40
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
41
|
-
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
45
|
-
|
|
46
|
-
expect(result.ok).toBe(false);
|
|
47
|
-
if (!result.ok) {
|
|
48
|
-
expect(result.error).toContain("401");
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("HTTP 404 — returns error containing 404", async () => {
|
|
53
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
54
|
-
new Response("Not Found", { status: 404, statusText: "Not Found" }),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
58
|
-
|
|
59
|
-
expect(result.ok).toBe(false);
|
|
60
|
-
if (!result.ok) {
|
|
61
|
-
expect(result.error).toContain("404");
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("network timeout — returns error mentioning timeout", async () => {
|
|
66
|
-
const err = new DOMException("signal timed out", "AbortError");
|
|
67
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(err);
|
|
68
|
-
|
|
69
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
70
|
-
|
|
71
|
-
expect(result.ok).toBe(false);
|
|
72
|
-
if (!result.ok) {
|
|
73
|
-
expect(result.error.toLowerCase()).toMatch(/timeout|timed out/);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("network error (DNS/connection) — returns error mentioning connectivity", async () => {
|
|
78
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed"));
|
|
79
|
-
|
|
80
|
-
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
81
|
-
|
|
82
|
-
expect(result.ok).toBe(false);
|
|
83
|
-
if (!result.ok) {
|
|
84
|
-
expect(result.error.toLowerCase()).toMatch(/connect|reach|network/);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("request body correctness", async () => {
|
|
89
|
-
const mockFetch = vi
|
|
90
|
-
.spyOn(globalThis, "fetch")
|
|
91
|
-
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
92
|
-
|
|
93
|
-
await validateModel(BASE_URL, API_KEY, "my-special-model");
|
|
94
|
-
|
|
95
|
-
const body = JSON.parse((mockFetch.mock.calls[0]![1] as RequestInit).body as string);
|
|
96
|
-
expect(body).toEqual({
|
|
97
|
-
model: "my-special-model",
|
|
98
|
-
messages: [{ role: "user", content: "hi" }],
|
|
99
|
-
max_tokens: 1,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("cmdSetup with validation", () => {
|
|
105
|
-
let storageRoot: string;
|
|
106
|
-
|
|
107
|
-
beforeEach(async () => {
|
|
108
|
-
storageRoot = await mkdtemp(join(tmpdir(), "uwf-setup-validate-"));
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
afterEach(async () => {
|
|
112
|
-
vi.restoreAllMocks();
|
|
113
|
-
await rm(storageRoot, { recursive: true, force: true });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const setupArgs = () => ({
|
|
117
|
-
provider: "testprovider",
|
|
118
|
-
baseUrl: "https://api.test.com/v1",
|
|
119
|
-
apiKey: "sk-test",
|
|
120
|
-
model: "test-model",
|
|
121
|
-
storageRoot,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("includes validation result on success", async () => {
|
|
125
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
126
|
-
new Response(JSON.stringify({}), { status: 200 }),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const result = await cmdSetup(setupArgs());
|
|
130
|
-
|
|
131
|
-
expect(result.validation).toEqual({ ok: true, value: undefined });
|
|
132
|
-
// Config file should still be written
|
|
133
|
-
expect(result.configPath).toBeTruthy();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("includes validation failure — config still saved", async () => {
|
|
137
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
138
|
-
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const result = await cmdSetup(setupArgs());
|
|
142
|
-
|
|
143
|
-
expect(result.validation).toBeDefined();
|
|
144
|
-
expect((result.validation as { ok: boolean }).ok).toBe(false);
|
|
145
|
-
// Config file should still be written despite validation failure
|
|
146
|
-
expect(result.configPath).toBeTruthy();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
File without changes
|