clinkx 0.1.10 → 0.2.1
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/clinkx-workflows/dist/artifacts.d.ts +65 -0
- package/clinkx-workflows/dist/artifacts.js +268 -0
- package/clinkx-workflows/dist/artifacts.js.map +1 -0
- package/clinkx-workflows/dist/backend.d.ts +33 -0
- package/clinkx-workflows/dist/backend.js +9 -0
- package/clinkx-workflows/dist/backend.js.map +1 -0
- package/clinkx-workflows/dist/child-env.d.ts +23 -0
- package/clinkx-workflows/dist/child-env.js +53 -0
- package/clinkx-workflows/dist/child-env.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +51 -0
- package/clinkx-workflows/dist/clink-client.js +216 -0
- package/clinkx-workflows/dist/clink-client.js.map +1 -0
- package/clinkx-workflows/dist/config.d.ts +126 -0
- package/clinkx-workflows/dist/config.js +226 -0
- package/clinkx-workflows/dist/config.js.map +1 -0
- package/clinkx-workflows/dist/definition-normalizer.d.ts +59 -0
- package/clinkx-workflows/dist/definition-normalizer.js +75 -0
- package/clinkx-workflows/dist/definition-normalizer.js.map +1 -0
- package/clinkx-workflows/dist/engine.d.ts +235 -0
- package/clinkx-workflows/dist/engine.js +1044 -0
- package/clinkx-workflows/dist/engine.js.map +1 -0
- package/clinkx-workflows/dist/errors.d.ts +74 -0
- package/clinkx-workflows/dist/errors.js +84 -0
- package/clinkx-workflows/dist/errors.js.map +1 -0
- package/clinkx-workflows/dist/fidelity.d.ts +112 -0
- package/clinkx-workflows/dist/fidelity.js +140 -0
- package/clinkx-workflows/dist/fidelity.js.map +1 -0
- package/clinkx-workflows/dist/fingerprint.d.ts +69 -0
- package/clinkx-workflows/dist/fingerprint.js +143 -0
- package/clinkx-workflows/dist/fingerprint.js.map +1 -0
- package/clinkx-workflows/dist/index.d.ts +16 -0
- package/clinkx-workflows/dist/index.js +42 -0
- package/clinkx-workflows/dist/index.js.map +1 -0
- package/clinkx-workflows/dist/loader.d.ts +64 -0
- package/clinkx-workflows/dist/loader.js +371 -0
- package/clinkx-workflows/dist/loader.js.map +1 -0
- package/clinkx-workflows/dist/logger.d.ts +16 -0
- package/clinkx-workflows/dist/logger.js +31 -0
- package/clinkx-workflows/dist/logger.js.map +1 -0
- package/clinkx-workflows/dist/path-validation.d.ts +23 -0
- package/clinkx-workflows/dist/path-validation.js +73 -0
- package/clinkx-workflows/dist/path-validation.js.map +1 -0
- package/clinkx-workflows/dist/prompt-budget.d.ts +31 -0
- package/clinkx-workflows/dist/prompt-budget.js +78 -0
- package/clinkx-workflows/dist/prompt-budget.js.map +1 -0
- package/clinkx-workflows/dist/queue.d.ts +16 -0
- package/clinkx-workflows/dist/queue.js +46 -0
- package/clinkx-workflows/dist/queue.js.map +1 -0
- package/clinkx-workflows/dist/ranking-reducer.d.ts +11 -0
- package/clinkx-workflows/dist/ranking-reducer.js +245 -0
- package/clinkx-workflows/dist/ranking-reducer.js.map +1 -0
- package/clinkx-workflows/dist/reducers/index.d.ts +8 -0
- package/clinkx-workflows/dist/reducers/index.js +12 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -0
- package/clinkx-workflows/dist/run-id.d.ts +17 -0
- package/clinkx-workflows/dist/run-id.js +26 -0
- package/clinkx-workflows/dist/run-id.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-answer.d.ts +8 -0
- package/clinkx-workflows/dist/run-summary/cards/council-answer.js +75 -0
- package/clinkx-workflows/dist/run-summary/cards/council-answer.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-code-review.d.ts +13 -0
- package/clinkx-workflows/dist/run-summary/cards/council-code-review.js +90 -0
- package/clinkx-workflows/dist/run-summary/cards/council-code-review.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-debug.d.ts +9 -0
- package/clinkx-workflows/dist/run-summary/cards/council-debug.js +79 -0
- package/clinkx-workflows/dist/run-summary/cards/council-debug.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-default.d.ts +11 -0
- package/clinkx-workflows/dist/run-summary/cards/council-default.js +57 -0
- package/clinkx-workflows/dist/run-summary/cards/council-default.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-discover.d.ts +10 -0
- package/clinkx-workflows/dist/run-summary/cards/council-discover.js +79 -0
- package/clinkx-workflows/dist/run-summary/cards/council-discover.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/generic.d.ts +2 -0
- package/clinkx-workflows/dist/run-summary/cards/generic.js +4 -0
- package/clinkx-workflows/dist/run-summary/cards/generic.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/index.d.ts +6 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js +17 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/utils.d.ts +6 -0
- package/clinkx-workflows/dist/run-summary/utils.js +30 -0
- package/clinkx-workflows/dist/run-summary/utils.js.map +1 -0
- package/clinkx-workflows/dist/run-summary-derived.d.ts +19 -0
- package/clinkx-workflows/dist/run-summary-derived.js +100 -0
- package/clinkx-workflows/dist/run-summary-derived.js.map +1 -0
- package/clinkx-workflows/dist/run-summary.d.ts +70 -0
- package/clinkx-workflows/dist/run-summary.js +125 -0
- package/clinkx-workflows/dist/run-summary.js.map +1 -0
- package/clinkx-workflows/dist/schema.d.ts +609 -0
- package/clinkx-workflows/dist/schema.js +123 -0
- package/clinkx-workflows/dist/schema.js.map +1 -0
- package/clinkx-workflows/dist/server.d.ts +16 -0
- package/clinkx-workflows/dist/server.js +33 -0
- package/clinkx-workflows/dist/server.js.map +1 -0
- package/clinkx-workflows/dist/shutdown.d.ts +54 -0
- package/clinkx-workflows/dist/shutdown.js +120 -0
- package/clinkx-workflows/dist/shutdown.js.map +1 -0
- package/clinkx-workflows/dist/state-schema.d.ts +141 -0
- package/clinkx-workflows/dist/state-schema.js +21 -0
- package/clinkx-workflows/dist/state-schema.js.map +1 -0
- package/clinkx-workflows/dist/state.d.ts +37 -0
- package/clinkx-workflows/dist/state.js +838 -0
- package/clinkx-workflows/dist/state.js.map +1 -0
- package/clinkx-workflows/dist/template-loader.d.ts +30 -0
- package/clinkx-workflows/dist/template-loader.js +77 -0
- package/clinkx-workflows/dist/template-loader.js.map +1 -0
- package/clinkx-workflows/dist/template.d.ts +54 -0
- package/clinkx-workflows/dist/template.js +128 -0
- package/clinkx-workflows/dist/template.js.map +1 -0
- package/clinkx-workflows/dist/transport.d.ts +91 -0
- package/clinkx-workflows/dist/transport.js +249 -0
- package/clinkx-workflows/dist/transport.js.map +1 -0
- package/clinkx-workflows/dist/types.d.ts +137 -0
- package/clinkx-workflows/dist/types.js +11 -0
- package/clinkx-workflows/dist/types.js.map +1 -0
- package/clinkx-workflows/dist/validators/council.d.ts +1488 -0
- package/clinkx-workflows/dist/validators/council.js +509 -0
- package/clinkx-workflows/dist/validators/council.js.map +1 -0
- package/clinkx-workflows/dist/validators/index.d.ts +40 -0
- package/clinkx-workflows/dist/validators/index.js +43 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -0
- package/clinkx-workflows/dist/workflow-receipt.d.ts +4 -0
- package/clinkx-workflows/dist/workflow-receipt.js +177 -0
- package/clinkx-workflows/dist/workflow-receipt.js.map +1 -0
- package/clinkx-workflows/dist/workflow-tools.d.ts +77 -0
- package/clinkx-workflows/dist/workflow-tools.js +1131 -0
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -0
- package/clinkx-workflows/dist/workflows/council-default.d.ts +123 -0
- package/clinkx-workflows/dist/workflows/council-default.js +141 -0
- package/clinkx-workflows/dist/workflows/council-default.js.map +1 -0
- package/clinkx-workflows/dist/workflows/index.d.ts +12 -0
- package/clinkx-workflows/dist/workflows/index.js +15 -0
- package/clinkx-workflows/dist/workflows/index.js.map +1 -0
- package/conf/adapters/claude.json +13 -1
- package/conf/adapters/codex.json +11 -2
- package/conf/adapters/gemini.json +9 -0
- package/conf/adapters/glm.json +10 -0
- package/conf/adapters/hapi/claude.json +12 -2
- package/conf/adapters/hapi/codex.json +11 -2
- package/conf/adapters/hapi/gemini.json +9 -0
- package/conf/adapters/hapi/glm.json +10 -0
- package/conf/prompts/json-codereviewer.txt +6 -0
- package/conf/prompts/json-debug.txt +5 -0
- package/conf/prompts/json-default.txt +5 -0
- package/conf/prompts/json.txt +4 -1
- package/dist/config.d.ts +29 -4
- package/dist/config.js +23 -3
- package/dist/config.js.map +1 -1
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +2 -1
- package/dist/handler.js.map +1 -1
- package/dist/local-clink-backend.d.ts +30 -0
- package/dist/local-clink-backend.js +106 -0
- package/dist/local-clink-backend.js.map +1 -0
- package/dist/parsers/claude-stream-json.d.ts +1 -1
- package/dist/parsers/claude-stream-json.js +26 -8
- package/dist/parsers/claude-stream-json.js.map +1 -1
- package/dist/parsers/extract.d.ts +2 -0
- package/dist/parsers/extract.js +46 -20
- package/dist/parsers/extract.js.map +1 -1
- package/dist/pipeline.d.ts +2 -4
- package/dist/pipeline.js +246 -31
- package/dist/pipeline.js.map +1 -1
- package/dist/prompt.js +8 -1
- package/dist/prompt.js.map +1 -1
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +14 -0
- package/dist/registry.js.map +1 -1
- package/dist/result-contract.d.ts +6 -1
- package/dist/result-contract.js +10 -22
- package/dist/result-contract.js.map +1 -1
- package/dist/runner.js +59 -12
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +29 -2
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +119 -45
- package/dist/server.js.map +1 -1
- package/package.json +12 -5
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprint generation for referenced adapter configs (1-3).
|
|
3
|
+
*
|
|
4
|
+
* Hashes the complete post-load normalized config of every adapter/role
|
|
5
|
+
* actually referenced by a workflow definition. The fingerprint is used
|
|
6
|
+
* for fast mismatch rejection on resume — if it doesn't match, the run
|
|
7
|
+
* is rejected without needing to diff the full snapshot.
|
|
8
|
+
*
|
|
9
|
+
* What is included:
|
|
10
|
+
* - All CliAdapterConfigSchema fields for referenced adapters
|
|
11
|
+
* - Resolved role prompt contents (not just prompt_file paths)
|
|
12
|
+
* - Explicit child env controls (CLINKX_MAX_CONCURRENT, etc.)
|
|
13
|
+
*
|
|
14
|
+
* What is NOT included:
|
|
15
|
+
* - Unreferenced adapters
|
|
16
|
+
* - Full tools/list enum
|
|
17
|
+
* - serverInfo.version (unreliable — hardcoded vs package.json divergence)
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { logger } from "./logger.js";
|
|
21
|
+
/**
|
|
22
|
+
* Collect the set of (cli_name, role) pairs actually referenced by a workflow definition.
|
|
23
|
+
* Only these adapters are included in the fingerprint.
|
|
24
|
+
*/
|
|
25
|
+
export function collectReferencedAdapters(definition) {
|
|
26
|
+
const refs = new Set();
|
|
27
|
+
for (const stage of definition.stages) {
|
|
28
|
+
for (const call of stage.calls) {
|
|
29
|
+
refs.add(call.cli_name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return refs;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Collect the set of (cli_name, role) pairs for role prompt resolution.
|
|
36
|
+
* Returns keys in "cli_name.role" format.
|
|
37
|
+
*/
|
|
38
|
+
export function collectReferencedRoles(definition) {
|
|
39
|
+
const refs = new Set();
|
|
40
|
+
for (const stage of definition.stages) {
|
|
41
|
+
for (const call of stage.calls) {
|
|
42
|
+
refs.add(`${call.cli_name}.${call.role}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return refs;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build a ChildConfigSnapshot from the workflow definition and runtime context.
|
|
49
|
+
*
|
|
50
|
+
* @param definition - Normalized workflow definition (determines which adapters are referenced)
|
|
51
|
+
* @param adapterConfigs - Complete post-load adapter configs keyed by adapter name
|
|
52
|
+
* @param resolvedPrompts - Resolved role prompt contents keyed by "adapter.role"
|
|
53
|
+
* @param childEnv - Explicit child env controls applied at spawn time
|
|
54
|
+
* @returns Snapshot containing only referenced adapters
|
|
55
|
+
*/
|
|
56
|
+
export function buildConfigSnapshot(definition, adapterConfigs, resolvedPrompts, childEnv) {
|
|
57
|
+
const referencedAdapters = collectReferencedAdapters(definition);
|
|
58
|
+
const referencedRoles = collectReferencedRoles(definition);
|
|
59
|
+
// Filter to only referenced adapters
|
|
60
|
+
const filteredAdapters = {};
|
|
61
|
+
for (const name of referencedAdapters) {
|
|
62
|
+
const config = adapterConfigs[name];
|
|
63
|
+
if (config !== undefined) {
|
|
64
|
+
filteredAdapters[name] = config;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Filter to only referenced role prompts
|
|
68
|
+
const filteredPrompts = {};
|
|
69
|
+
for (const key of referencedRoles) {
|
|
70
|
+
const prompt = resolvedPrompts[key];
|
|
71
|
+
if (prompt !== undefined) {
|
|
72
|
+
filteredPrompts[key] = prompt;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
adapters: filteredAdapters,
|
|
77
|
+
resolvedPrompts: filteredPrompts,
|
|
78
|
+
childEnv,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compute a deterministic SHA-256 hash of a ChildConfigSnapshot.
|
|
83
|
+
*
|
|
84
|
+
* Uses JSON.stringify with sorted keys to ensure deterministic serialization
|
|
85
|
+
* regardless of property insertion order.
|
|
86
|
+
*/
|
|
87
|
+
export function hashSnapshot(snapshot) {
|
|
88
|
+
const serialized = JSON.stringify(snapshot, sortedReplacer);
|
|
89
|
+
return createHash("sha256").update(serialized, "utf-8").digest("hex");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generate a complete Fingerprint from a ChildConfigSnapshot.
|
|
93
|
+
*
|
|
94
|
+
* This is the primary API for Phase 4 to persist and compare.
|
|
95
|
+
*/
|
|
96
|
+
export function generateFingerprint(snapshot) {
|
|
97
|
+
const hash = hashSnapshot(snapshot);
|
|
98
|
+
logger.debug({
|
|
99
|
+
adapter_count: Object.keys(snapshot.adapters).length,
|
|
100
|
+
prompt_count: Object.keys(snapshot.resolvedPrompts).length,
|
|
101
|
+
env_count: Object.keys(snapshot.childEnv).length,
|
|
102
|
+
hash,
|
|
103
|
+
}, "generated config fingerprint");
|
|
104
|
+
return {
|
|
105
|
+
algorithm: "sha256",
|
|
106
|
+
hash,
|
|
107
|
+
computedAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Compare two fingerprints for equality.
|
|
112
|
+
* Only compares algorithm and hash — computedAt is informational.
|
|
113
|
+
*/
|
|
114
|
+
export function fingerprintsMatch(a, b) {
|
|
115
|
+
return a.algorithm === b.algorithm && a.hash === b.hash;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* End-to-end fingerprint generation from workflow definition and runtime context.
|
|
119
|
+
*
|
|
120
|
+
* Convenience function that builds the snapshot and generates the fingerprint
|
|
121
|
+
* in one call. Returns both the snapshot (for persistence) and the fingerprint
|
|
122
|
+
* (for fast comparison).
|
|
123
|
+
*/
|
|
124
|
+
export function generateWorkflowFingerprint(definition, adapterConfigs, resolvedPrompts, childEnv) {
|
|
125
|
+
const snapshot = buildConfigSnapshot(definition, adapterConfigs, resolvedPrompts, childEnv);
|
|
126
|
+
const fingerprint = generateFingerprint(snapshot);
|
|
127
|
+
return { snapshot, fingerprint };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* JSON.stringify replacer that sorts object keys for deterministic serialization.
|
|
131
|
+
* Arrays maintain their original order (order-significant).
|
|
132
|
+
*/
|
|
133
|
+
function sortedReplacer(_key, value) {
|
|
134
|
+
if (value != null && typeof value === "object" && !Array.isArray(value)) {
|
|
135
|
+
const sorted = {};
|
|
136
|
+
for (const k of Object.keys(value).sort()) {
|
|
137
|
+
sorted[k] = value[k];
|
|
138
|
+
}
|
|
139
|
+
return sorted;
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* clinkx-workflows MCP server entry point (6-9).
|
|
4
|
+
*
|
|
5
|
+
* 1. Verify logger writes to stderr (STDIO correctness invariant).
|
|
6
|
+
* 2. Create the MCP server with workflow tool handlers.
|
|
7
|
+
* 3. Connect STDIO transport (stdin/stdout for JSON-RPC, stderr for logs).
|
|
8
|
+
* 4. Wire graceful shutdown via the shutdown coordinator.
|
|
9
|
+
*
|
|
10
|
+
* Also serves as the library entry point — re-exports the public API
|
|
11
|
+
* for in-process integration (Option C).
|
|
12
|
+
*/
|
|
13
|
+
export type { ClinkBackend, ToolCallResult } from "./backend.js";
|
|
14
|
+
export { registerWorkflowTools, getWorkflowToolDefinitions, handleWorkflowToolCall, WORKFLOW_TOOL_NAMES, } from "./workflow-tools.js";
|
|
15
|
+
export type { WorkflowToolsOptions, WorkflowToolDefinition, } from "./workflow-tools.js";
|
|
16
|
+
export { createWorkflowServer } from "./server.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* clinkx-workflows MCP server entry point (6-9).
|
|
4
|
+
*
|
|
5
|
+
* 1. Verify logger writes to stderr (STDIO correctness invariant).
|
|
6
|
+
* 2. Create the MCP server with workflow tool handlers.
|
|
7
|
+
* 3. Connect STDIO transport (stdin/stdout for JSON-RPC, stderr for logs).
|
|
8
|
+
* 4. Wire graceful shutdown via the shutdown coordinator.
|
|
9
|
+
*
|
|
10
|
+
* Also serves as the library entry point — re-exports the public API
|
|
11
|
+
* for in-process integration (Option C).
|
|
12
|
+
*/
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import { createWorkflowServer } from "./server.js";
|
|
15
|
+
import { logger, assertStderrLogger } from "./logger.js";
|
|
16
|
+
export { registerWorkflowTools, getWorkflowToolDefinitions, handleWorkflowToolCall, WORKFLOW_TOOL_NAMES, } from "./workflow-tools.js";
|
|
17
|
+
export { createWorkflowServer } from "./server.js";
|
|
18
|
+
async function main() {
|
|
19
|
+
assertStderrLogger();
|
|
20
|
+
const server = createWorkflowServer();
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
logger.info("clinkx-workflows MCP server starting (STDIO transport)");
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
// Graceful shutdown: coordinate with any in-flight workflow,
|
|
25
|
+
// then close the server and exit.
|
|
26
|
+
const shutdown = async (signal) => {
|
|
27
|
+
logger.info({ signal }, "shutdown signal received");
|
|
28
|
+
// ShutdownCoordinator handles in-flight workflow cancellation,
|
|
29
|
+
// state persistence, and child termination via its own signal handlers.
|
|
30
|
+
// We just need to close the MCP server transport cleanly.
|
|
31
|
+
await server.close();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
};
|
|
34
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM").catch(() => process.exit(1)));
|
|
35
|
+
process.once("SIGINT", () => void shutdown("SIGINT").catch(() => process.exit(1)));
|
|
36
|
+
logger.info("clinkx-workflows MCP server connected and ready");
|
|
37
|
+
}
|
|
38
|
+
main().catch((err) => {
|
|
39
|
+
logger.fatal({ err }, "clinkx-workflows failed to start");
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKzD,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,KAAK,UAAU,IAAI;IACjB,kBAAkB,EAAE,CAAC;IAErB,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC;IACtC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,MAAM,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IAEtE,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,6DAA6D;IAC7D,kCAAkC;IAClC,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACxC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,0BAA0B,CAAC,CAAC;QACpD,+DAA+D;QAC/D,wEAAwE;QACxE,0DAA0D;QAC1D,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnF,MAAM,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;AACjE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,kCAAkC,CAAC,CAAC;IAC1D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader for clinkx-workflows (2-6a, 2-6b, 2-8a, 2-9a, 7-3).
|
|
3
|
+
*
|
|
4
|
+
* v1b: loads built-in TypeScript definitions AND external YAML files.
|
|
5
|
+
* Built-in definitions take precedence over same-named YAML files.
|
|
6
|
+
* Static validation only — no live child required.
|
|
7
|
+
*
|
|
8
|
+
* ## Deferred validation (2-9a)
|
|
9
|
+
* cli_name and role validation are deliberately kept OUT of the loader.
|
|
10
|
+
* These are runtime transport concerns:
|
|
11
|
+
* - cli_name existence depends on child's adapter registry (not available at load time)
|
|
12
|
+
* - role acceptance depends on per-adapter role configuration
|
|
13
|
+
* - Current ClinkX exposes only a global role union via tools/list, not per-CLI role maps
|
|
14
|
+
* This design allows list_workflows to work without a live child.
|
|
15
|
+
*/
|
|
16
|
+
import type { WorkflowDefinition } from "./schema.js";
|
|
17
|
+
import type { ValidatorRegistry } from "./validators/index.js";
|
|
18
|
+
import { type NormalizedWorkflowDefinition } from "./definition-normalizer.js";
|
|
19
|
+
export interface LoaderOptions {
|
|
20
|
+
/** Absolute path to the templates root directory. */
|
|
21
|
+
readonly templatesRoot: string;
|
|
22
|
+
/** Validator registry for schema ID validation. */
|
|
23
|
+
readonly validatorRegistry: ValidatorRegistry;
|
|
24
|
+
/**
|
|
25
|
+
* Absolute path to the workflows directory for YAML discovery.
|
|
26
|
+
* If not provided, uses CLINKX_WORKFLOWS_PATH env or package-relative default.
|
|
27
|
+
*/
|
|
28
|
+
readonly workflowsPath?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface LoadedWorkflow {
|
|
31
|
+
readonly definition: WorkflowDefinition;
|
|
32
|
+
readonly normalized: NormalizedWorkflowDefinition;
|
|
33
|
+
readonly source: "builtin" | "yaml";
|
|
34
|
+
readonly sourcePath?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
export interface WorkflowCatalogEntry {
|
|
37
|
+
readonly definition: WorkflowDefinition;
|
|
38
|
+
readonly source: "builtin" | "yaml";
|
|
39
|
+
readonly path?: string | undefined;
|
|
40
|
+
readonly shadowedYaml: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load and validate a workflow by name.
|
|
44
|
+
*
|
|
45
|
+
* Resolution order:
|
|
46
|
+
* 1. Built-in TypeScript definitions (highest precedence)
|
|
47
|
+
* 2. External YAML files in the workflows directory
|
|
48
|
+
*
|
|
49
|
+
* Returns the validated definition and its normalized (snapshot-ready) form.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadWorkflow(name: string, options: LoaderOptions): LoadedWorkflow;
|
|
52
|
+
/**
|
|
53
|
+
* List all available workflows (built-in + YAML).
|
|
54
|
+
* Built-in definitions take precedence over same-named YAML files.
|
|
55
|
+
* No live child required.
|
|
56
|
+
*
|
|
57
|
+
* @param workflowsPath - Optional absolute path to workflows directory for YAML discovery
|
|
58
|
+
*/
|
|
59
|
+
export declare function listWorkflows(workflowsPath?: string): Map<string, WorkflowDefinition>;
|
|
60
|
+
/**
|
|
61
|
+
* List workflow definitions with source metadata.
|
|
62
|
+
* Built-in definitions take precedence over same-named YAML files.
|
|
63
|
+
*/
|
|
64
|
+
export declare function listWorkflowCatalog(workflowsPath?: string): Map<string, WorkflowCatalogEntry>;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader for clinkx-workflows (2-6a, 2-6b, 2-8a, 2-9a, 7-3).
|
|
3
|
+
*
|
|
4
|
+
* v1b: loads built-in TypeScript definitions AND external YAML files.
|
|
5
|
+
* Built-in definitions take precedence over same-named YAML files.
|
|
6
|
+
* Static validation only — no live child required.
|
|
7
|
+
*
|
|
8
|
+
* ## Deferred validation (2-9a)
|
|
9
|
+
* cli_name and role validation are deliberately kept OUT of the loader.
|
|
10
|
+
* These are runtime transport concerns:
|
|
11
|
+
* - cli_name existence depends on child's adapter registry (not available at load time)
|
|
12
|
+
* - role acceptance depends on per-adapter role configuration
|
|
13
|
+
* - Current ClinkX exposes only a global role union via tools/list, not per-CLI role maps
|
|
14
|
+
* This design allows list_workflows to work without a live child.
|
|
15
|
+
*/
|
|
16
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
17
|
+
import { resolve, extname } from "node:path";
|
|
18
|
+
import { parse as parseYaml } from "yaml";
|
|
19
|
+
import { WorkflowDefinitionSchema } from "./schema.js";
|
|
20
|
+
import { WorkflowValidationError } from "./errors.js";
|
|
21
|
+
import { resolveAllTemplates } from "./template-loader.js";
|
|
22
|
+
import { normalizeForSnapshot } from "./definition-normalizer.js";
|
|
23
|
+
import { getBuiltinWorkflows } from "./workflows/index.js";
|
|
24
|
+
import { getWorkflowsPath } from "./config.js";
|
|
25
|
+
import { logger } from "./logger.js";
|
|
26
|
+
/**
|
|
27
|
+
* Load and validate a workflow by name.
|
|
28
|
+
*
|
|
29
|
+
* Resolution order:
|
|
30
|
+
* 1. Built-in TypeScript definitions (highest precedence)
|
|
31
|
+
* 2. External YAML files in the workflows directory
|
|
32
|
+
*
|
|
33
|
+
* Returns the validated definition and its normalized (snapshot-ready) form.
|
|
34
|
+
*/
|
|
35
|
+
export function loadWorkflow(name, options) {
|
|
36
|
+
const resolved = resolveWorkflowRaw(name, options);
|
|
37
|
+
// Parse through zod schema (applies defaults, validates structure)
|
|
38
|
+
const parseResult = WorkflowDefinitionSchema.safeParse(resolved.raw);
|
|
39
|
+
if (!parseResult.success) {
|
|
40
|
+
throw new WorkflowValidationError(`Schema validation failed for workflow "${name}": ` +
|
|
41
|
+
parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "));
|
|
42
|
+
}
|
|
43
|
+
const definition = parseResult.data;
|
|
44
|
+
// Static validation pipeline (2-6b)
|
|
45
|
+
validateWorkflow(definition, options.validatorRegistry);
|
|
46
|
+
// Resolve templates (2-7a)
|
|
47
|
+
const templates = resolveAllTemplates(definition.stages, options.templatesRoot);
|
|
48
|
+
// Normalize (2-10a)
|
|
49
|
+
const normalized = normalizeForSnapshot(definition, templates);
|
|
50
|
+
return {
|
|
51
|
+
definition,
|
|
52
|
+
normalized,
|
|
53
|
+
source: resolved.source,
|
|
54
|
+
sourcePath: resolved.sourcePath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a raw workflow definition object by name.
|
|
59
|
+
* Built-in TypeScript definitions take precedence over YAML files.
|
|
60
|
+
*/
|
|
61
|
+
function resolveWorkflowRaw(name, options) {
|
|
62
|
+
// 1. Check built-in definitions first
|
|
63
|
+
const builtins = getBuiltinWorkflows();
|
|
64
|
+
const builtin = builtins.get(name);
|
|
65
|
+
// 2. Check YAML files in the workflows directory
|
|
66
|
+
const yamlWorkflows = discoverYamlWorkflows(options.workflowsPath);
|
|
67
|
+
const yamlRaw = yamlWorkflows.get(name);
|
|
68
|
+
if (builtin != null) {
|
|
69
|
+
// Warn if a builtin shadows a YAML file with the same name
|
|
70
|
+
if (yamlRaw != null) {
|
|
71
|
+
logger.warn({ name, yaml_path: yamlRaw.filePath }, "built-in workflow shadows YAML file with same name");
|
|
72
|
+
}
|
|
73
|
+
return { raw: builtin, source: "builtin" };
|
|
74
|
+
}
|
|
75
|
+
if (yamlRaw != null) {
|
|
76
|
+
return { raw: yamlRaw.raw, source: "yaml", sourcePath: yamlRaw.filePath };
|
|
77
|
+
}
|
|
78
|
+
// Collect all available names for the error message
|
|
79
|
+
const allNames = new Set([...builtins.keys(), ...yamlWorkflows.keys()]);
|
|
80
|
+
throw new WorkflowValidationError(`Unknown workflow: "${name}". Available: ${[...allNames].sort().join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* List all available workflows (built-in + YAML).
|
|
84
|
+
* Built-in definitions take precedence over same-named YAML files.
|
|
85
|
+
* No live child required.
|
|
86
|
+
*
|
|
87
|
+
* @param workflowsPath - Optional absolute path to workflows directory for YAML discovery
|
|
88
|
+
*/
|
|
89
|
+
export function listWorkflows(workflowsPath) {
|
|
90
|
+
const catalog = listWorkflowCatalog(workflowsPath);
|
|
91
|
+
return new Map([...catalog.entries()].map(([name, entry]) => [name, entry.definition]));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* List workflow definitions with source metadata.
|
|
95
|
+
* Built-in definitions take precedence over same-named YAML files.
|
|
96
|
+
*/
|
|
97
|
+
export function listWorkflowCatalog(workflowsPath) {
|
|
98
|
+
const catalog = new Map();
|
|
99
|
+
// 1. Load YAML workflows first (lower precedence)
|
|
100
|
+
const yamlWorkflows = discoverYamlWorkflows(workflowsPath);
|
|
101
|
+
for (const [name, entry] of yamlWorkflows) {
|
|
102
|
+
const raw = entry.raw;
|
|
103
|
+
const parseResult = WorkflowDefinitionSchema.safeParse(raw);
|
|
104
|
+
if (parseResult.success) {
|
|
105
|
+
catalog.set(name, {
|
|
106
|
+
definition: parseResult.data,
|
|
107
|
+
source: "yaml",
|
|
108
|
+
path: entry.filePath,
|
|
109
|
+
shadowedYaml: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
logger.warn({ name, errors: parseResult.error.issues }, "Skipping invalid YAML workflow");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 2. Overlay built-in definitions (higher precedence — overwrites same-named YAML)
|
|
117
|
+
const builtins = getBuiltinWorkflows();
|
|
118
|
+
for (const [name, raw] of builtins) {
|
|
119
|
+
const parseResult = WorkflowDefinitionSchema.safeParse(raw);
|
|
120
|
+
if (parseResult.success) {
|
|
121
|
+
catalog.set(name, {
|
|
122
|
+
definition: parseResult.data,
|
|
123
|
+
source: "builtin",
|
|
124
|
+
shadowedYaml: yamlWorkflows.has(name),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return catalog;
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// 7-3: YAML Workflow Discovery
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Resolve the workflows directory path.
|
|
135
|
+
* Uses explicit path if provided, otherwise falls back to config/env.
|
|
136
|
+
* Resolves relative paths against the package root (dirname of this file → src/ → package root).
|
|
137
|
+
*/
|
|
138
|
+
function resolveWorkflowsDir(explicitPath) {
|
|
139
|
+
const raw = explicitPath ?? getWorkflowsPath();
|
|
140
|
+
if (raw.startsWith("/")) {
|
|
141
|
+
return raw;
|
|
142
|
+
}
|
|
143
|
+
// Resolve relative to the package root (two levels up from dist/src/)
|
|
144
|
+
const packageRoot = resolve(import.meta.dirname, "..");
|
|
145
|
+
return resolve(packageRoot, raw);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Discover workflow definitions from YAML files in the workflows directory.
|
|
149
|
+
* Returns a Map from workflow name (derived from YAML `name` field) to raw parsed object.
|
|
150
|
+
* Invalid YAML files are logged and skipped.
|
|
151
|
+
*/
|
|
152
|
+
function discoverYamlWorkflows(explicitPath) {
|
|
153
|
+
const result = new Map();
|
|
154
|
+
const dir = resolveWorkflowsDir(explicitPath);
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = readdirSync(dir);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Directory doesn't exist or isn't readable — no YAML workflows
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const ext = extname(entry);
|
|
165
|
+
if (ext !== ".yaml" && ext !== ".yml")
|
|
166
|
+
continue;
|
|
167
|
+
const filePath = resolve(dir, entry);
|
|
168
|
+
try {
|
|
169
|
+
const content = readFileSync(filePath, "utf-8");
|
|
170
|
+
const parsed = parseYaml(content);
|
|
171
|
+
if (parsed == null || typeof parsed !== "object") {
|
|
172
|
+
logger.warn({ file: filePath }, "YAML workflow file did not parse to an object, skipping");
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const obj = parsed;
|
|
176
|
+
const name = obj["name"];
|
|
177
|
+
if (typeof name !== "string" || name === "") {
|
|
178
|
+
logger.warn({ file: filePath }, "YAML workflow file missing 'name' field, skipping");
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (result.has(name)) {
|
|
182
|
+
logger.warn({ file: filePath, name }, "Duplicate YAML workflow name, keeping first occurrence");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
result.set(name, { raw: parsed, filePath });
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
logger.warn({ file: filePath, error: String(err) }, "Failed to parse YAML workflow file");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// 2-6b: Static Validation Pipeline
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/**
|
|
197
|
+
* Validate a parsed workflow definition.
|
|
198
|
+
* All checks are static — no live child required.
|
|
199
|
+
*/
|
|
200
|
+
function validateWorkflow(def, validatorRegistry) {
|
|
201
|
+
validateUniqueStageIds(def);
|
|
202
|
+
validateUniqueCallIdsPerStage(def);
|
|
203
|
+
validateNonEmptyStages(def);
|
|
204
|
+
validateFinalStage(def);
|
|
205
|
+
validateContextFromReferences(def);
|
|
206
|
+
validateNoDuplicateContextFromRefs(def);
|
|
207
|
+
validateContextProducerContract(def, validatorRegistry);
|
|
208
|
+
validateNoSideEffects(def);
|
|
209
|
+
}
|
|
210
|
+
/** Unique stage IDs across the workflow. */
|
|
211
|
+
function validateUniqueStageIds(def) {
|
|
212
|
+
const seen = new Set();
|
|
213
|
+
for (const stage of def.stages) {
|
|
214
|
+
if (seen.has(stage.id)) {
|
|
215
|
+
throw new WorkflowValidationError(`Duplicate stage ID: "${stage.id}"`);
|
|
216
|
+
}
|
|
217
|
+
seen.add(stage.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Unique call IDs within each stage. */
|
|
221
|
+
function validateUniqueCallIdsPerStage(def) {
|
|
222
|
+
for (const stage of def.stages) {
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
for (const call of stage.calls) {
|
|
225
|
+
if (seen.has(call.id)) {
|
|
226
|
+
throw new WorkflowValidationError(`Duplicate call ID "${call.id}" in stage "${stage.id}"`);
|
|
227
|
+
}
|
|
228
|
+
seen.add(call.id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Non-empty stages. */
|
|
233
|
+
function validateNonEmptyStages(def) {
|
|
234
|
+
for (const stage of def.stages) {
|
|
235
|
+
if (stage.calls.length === 0) {
|
|
236
|
+
throw new WorkflowValidationError(`Stage "${stage.id}" has no calls`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Final stage must have exactly one call. */
|
|
241
|
+
function validateFinalStage(def) {
|
|
242
|
+
const lastStage = def.stages[def.stages.length - 1];
|
|
243
|
+
if (lastStage == null)
|
|
244
|
+
return; // Already caught by min(1) in schema
|
|
245
|
+
if (lastStage.calls.length !== 1) {
|
|
246
|
+
throw new WorkflowValidationError(`Final stage "${lastStage.id}" must have exactly one call, ` +
|
|
247
|
+
`got ${String(lastStage.calls.length)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* context_from references only EARLIER stages (forward-reference rejection).
|
|
252
|
+
* Accepts "stage_id" (whole stage) or "stage_id.call_id" (specific call).
|
|
253
|
+
*/
|
|
254
|
+
function validateContextFromReferences(def) {
|
|
255
|
+
const stageOrder = new Map();
|
|
256
|
+
const callsByStage = new Map();
|
|
257
|
+
for (let i = 0; i < def.stages.length; i++) {
|
|
258
|
+
const stage = def.stages[i];
|
|
259
|
+
stageOrder.set(stage.id, i);
|
|
260
|
+
const callIds = new Set();
|
|
261
|
+
for (const call of stage.calls) {
|
|
262
|
+
callIds.add(call.id);
|
|
263
|
+
}
|
|
264
|
+
callsByStage.set(stage.id, callIds);
|
|
265
|
+
}
|
|
266
|
+
for (let i = 0; i < def.stages.length; i++) {
|
|
267
|
+
const stage = def.stages[i];
|
|
268
|
+
for (const call of stage.calls) {
|
|
269
|
+
if (call.context_from == null)
|
|
270
|
+
continue;
|
|
271
|
+
for (const ref of call.context_from) {
|
|
272
|
+
const dotIndex = ref.indexOf(".");
|
|
273
|
+
const refStageId = dotIndex === -1 ? ref : ref.substring(0, dotIndex);
|
|
274
|
+
const refCallId = dotIndex === -1 ? undefined : ref.substring(dotIndex + 1);
|
|
275
|
+
const refStageIdx = stageOrder.get(refStageId);
|
|
276
|
+
if (refStageIdx == null) {
|
|
277
|
+
throw new WorkflowValidationError(`context_from reference "${ref}" in call "${call.id}" (stage "${stage.id}") ` +
|
|
278
|
+
`references unknown stage "${refStageId}"`);
|
|
279
|
+
}
|
|
280
|
+
if (refStageIdx >= i) {
|
|
281
|
+
throw new WorkflowValidationError(`Forward reference in context_from: call "${call.id}" (stage "${stage.id}") ` +
|
|
282
|
+
`references stage "${refStageId}" which is not earlier`);
|
|
283
|
+
}
|
|
284
|
+
if (refCallId != null) {
|
|
285
|
+
const refCalls = callsByStage.get(refStageId);
|
|
286
|
+
if (refCalls == null || !refCalls.has(refCallId)) {
|
|
287
|
+
throw new WorkflowValidationError(`context_from reference "${ref}" in call "${call.id}" (stage "${stage.id}") ` +
|
|
288
|
+
`references unknown call "${refCallId}" in stage "${refStageId}"`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** No duplicate context_from refs within a single call. */
|
|
296
|
+
function validateNoDuplicateContextFromRefs(def) {
|
|
297
|
+
for (const stage of def.stages) {
|
|
298
|
+
for (const call of stage.calls) {
|
|
299
|
+
if (call.context_from == null)
|
|
300
|
+
continue;
|
|
301
|
+
const seen = new Set();
|
|
302
|
+
for (const ref of call.context_from) {
|
|
303
|
+
if (seen.has(ref)) {
|
|
304
|
+
throw new WorkflowValidationError(`Duplicate context_from reference "${ref}" in call "${call.id}" (stage "${stage.id}")`);
|
|
305
|
+
}
|
|
306
|
+
seen.add(ref);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 2-8a: Reverse-reference validator-contract enforcement.
|
|
313
|
+
* Any call referenced by context_from must declare validator_schema_id.
|
|
314
|
+
*/
|
|
315
|
+
function validateContextProducerContract(def, validatorRegistry) {
|
|
316
|
+
// Collect all referenced call identifiers
|
|
317
|
+
const referencedCalls = new Set();
|
|
318
|
+
for (const stage of def.stages) {
|
|
319
|
+
for (const call of stage.calls) {
|
|
320
|
+
if (call.context_from == null)
|
|
321
|
+
continue;
|
|
322
|
+
for (const ref of call.context_from) {
|
|
323
|
+
const dotIndex = ref.indexOf(".");
|
|
324
|
+
if (dotIndex === -1) {
|
|
325
|
+
// Whole-stage reference — all calls in that stage are referenced
|
|
326
|
+
const refStage = def.stages.find((s) => s.id === ref);
|
|
327
|
+
if (refStage != null) {
|
|
328
|
+
for (const refCall of refStage.calls) {
|
|
329
|
+
referencedCalls.add(`${ref}.${refCall.id}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
referencedCalls.add(ref);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Validate each referenced call
|
|
340
|
+
for (const ref of referencedCalls) {
|
|
341
|
+
const dotIndex = ref.indexOf(".");
|
|
342
|
+
const stageId = ref.substring(0, dotIndex);
|
|
343
|
+
const callId = ref.substring(dotIndex + 1);
|
|
344
|
+
const stage = def.stages.find((s) => s.id === stageId);
|
|
345
|
+
if (stage == null)
|
|
346
|
+
continue; // Already caught by context_from validation
|
|
347
|
+
const call = stage.calls.find((c) => c.id === callId);
|
|
348
|
+
if (call == null)
|
|
349
|
+
continue;
|
|
350
|
+
if (call.validator_schema_id == null) {
|
|
351
|
+
throw new WorkflowValidationError(`Call "${callId}" in stage "${stageId}" is referenced by context_from ` +
|
|
352
|
+
`but does not declare validator_schema_id — required for context-producing calls`);
|
|
353
|
+
}
|
|
354
|
+
if (!validatorRegistry.has(call.validator_schema_id)) {
|
|
355
|
+
throw new WorkflowValidationError(`Call "${callId}" in stage "${stageId}" declares validator_schema_id ` +
|
|
356
|
+
`"${call.validator_schema_id}" which is not registered. ` +
|
|
357
|
+
`Available: ${validatorRegistry.ids().join(", ")}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 2-6b: Side-effecting call rejection.
|
|
363
|
+
* v1 is read-only — any call intended for side effects is a load-time validation error.
|
|
364
|
+
* Currently this checks for known side-effect indicators; extend as needed.
|
|
365
|
+
*/
|
|
366
|
+
function validateNoSideEffects(_def) {
|
|
367
|
+
// v1: no side-effect indicators are defined in the schema yet.
|
|
368
|
+
// This is a placeholder for future extension (e.g., a `side_effects: true` flag).
|
|
369
|
+
// All v1 workflows are treated as read-only.
|
|
370
|
+
}
|
|
371
|
+
//# sourceMappingURL=loader.js.map
|