@aiwg/cli 2026.10.5 → 2026.10.7
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/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +7 -3
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +57 -6
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +143 -14
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +114 -55
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +22 -3
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +1 -0
- package/dist/src/cli/handlers/setup.js +432 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +335 -107
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +81 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +482 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +23 -18
- package/dist/src/decision/qualification/load-staged.js +576 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +12 -5
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +238 -0
- package/dist/src/discovery/plan-apply.js +567 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +22 -96
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +8 -1
- package/dist/src/models/worker-installation.mjs +231 -38
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +124 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +10 -9
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +63 -53
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/workspace-discovery.js +3 -4
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +24 -14
- package/tools/agents/providers/claude.mjs +6 -3
- package/tools/agents/providers/codex.mjs +54 -57
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +13 -4
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +12 -15
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +31 -5
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +289 -80
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +13 -3
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +49 -8
- package/tools/ralph-external/session-launcher.mjs +405 -35
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import Ajv from 'ajv';
|
|
5
|
+
|
|
6
|
+
const addonRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const schema = JSON.parse(fs.readFileSync(path.join(addonRoot, 'schemas', 'flow-graph.schema.json'), 'utf8'));
|
|
8
|
+
const ajv = new Ajv({ allErrors: true, strict: false, validateFormats: false });
|
|
9
|
+
const validateSchema = ajv.compile(schema);
|
|
10
|
+
|
|
11
|
+
const SIDE_EFFECT_RANK = {
|
|
12
|
+
none: 0,
|
|
13
|
+
idempotent: 1,
|
|
14
|
+
'exactly-once': 2,
|
|
15
|
+
'approval-required': 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function diagnostic(code, pathName, message, hint, details) {
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
severity: 'error',
|
|
22
|
+
path: pathName || '/',
|
|
23
|
+
message,
|
|
24
|
+
...(hint ? { hint } : {}),
|
|
25
|
+
...(details ? { details } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pointer(error) {
|
|
30
|
+
const at = error.instancePath || '/';
|
|
31
|
+
if (error.keyword === 'required') return `${at === '/' ? '' : at}/${error.params.missingProperty}`;
|
|
32
|
+
if (error.keyword === 'additionalProperties') return `${at === '/' ? '' : at}/${error.params.additionalProperty}`;
|
|
33
|
+
return at;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function schemaErrorMessage(error) {
|
|
37
|
+
if (error.keyword === 'const') {
|
|
38
|
+
return `must be equal to constant ${JSON.stringify(error.params.allowedValue)}.`;
|
|
39
|
+
}
|
|
40
|
+
return error.message ?? 'Schema validation failed.';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function duplicates(items, key, pathName, diagnostics) {
|
|
44
|
+
const seen = new Map();
|
|
45
|
+
for (const [index, item] of items.entries()) {
|
|
46
|
+
const value = item?.[key];
|
|
47
|
+
if (seen.has(value)) {
|
|
48
|
+
diagnostics.push(diagnostic(
|
|
49
|
+
'DUPLICATE_IDENTIFIER',
|
|
50
|
+
`${pathName}/${index}/${key}`,
|
|
51
|
+
`Duplicate ${key} '${value}'.`,
|
|
52
|
+
`Rename this entry; the first declaration is at ${pathName}/${seen.get(value)}.`,
|
|
53
|
+
));
|
|
54
|
+
} else {
|
|
55
|
+
seen.set(value, index);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function schemaTypes(value) {
|
|
61
|
+
if (!value || typeof value !== 'object' || !value.type) return null;
|
|
62
|
+
return new Set(Array.isArray(value.type) ? value.type : [value.type]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function schemasCompatible(source, target) {
|
|
66
|
+
const sourceTypes = schemaTypes(source);
|
|
67
|
+
const targetTypes = schemaTypes(target);
|
|
68
|
+
if (!sourceTypes || !targetTypes) return true;
|
|
69
|
+
for (const type of sourceTypes) {
|
|
70
|
+
if (targetTypes.has(type)) return true;
|
|
71
|
+
if (type === 'integer' && targetTypes.has('number')) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function gcd(left, right) {
|
|
77
|
+
let a = left;
|
|
78
|
+
let b = right;
|
|
79
|
+
while (b) [a, b] = [b, a % b];
|
|
80
|
+
return a;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function lcm(values) {
|
|
84
|
+
return values.reduce((value, next) => (value * next) / gcd(value, next), 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tarjan(nodeIds, edges) {
|
|
88
|
+
const adjacency = new Map(nodeIds.map((id) => [id, []]));
|
|
89
|
+
for (const edge of edges) adjacency.get(edge.from)?.push(edge.to);
|
|
90
|
+
let nextIndex = 0;
|
|
91
|
+
const stack = [];
|
|
92
|
+
const onStack = new Set();
|
|
93
|
+
const indexes = new Map();
|
|
94
|
+
const lows = new Map();
|
|
95
|
+
const components = [];
|
|
96
|
+
|
|
97
|
+
function visit(id) {
|
|
98
|
+
indexes.set(id, nextIndex);
|
|
99
|
+
lows.set(id, nextIndex);
|
|
100
|
+
nextIndex += 1;
|
|
101
|
+
stack.push(id);
|
|
102
|
+
onStack.add(id);
|
|
103
|
+
for (const target of adjacency.get(id) ?? []) {
|
|
104
|
+
if (!indexes.has(target)) {
|
|
105
|
+
visit(target);
|
|
106
|
+
lows.set(id, Math.min(lows.get(id), lows.get(target)));
|
|
107
|
+
} else if (onStack.has(target)) {
|
|
108
|
+
lows.set(id, Math.min(lows.get(id), indexes.get(target)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (lows.get(id) === indexes.get(id)) {
|
|
112
|
+
const component = [];
|
|
113
|
+
let current;
|
|
114
|
+
do {
|
|
115
|
+
current = stack.pop();
|
|
116
|
+
onStack.delete(current);
|
|
117
|
+
component.push(current);
|
|
118
|
+
} while (current !== id);
|
|
119
|
+
components.push(component);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const id of nodeIds) if (!indexes.has(id)) visit(id);
|
|
124
|
+
return components;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readBinding(reference, nodeById) {
|
|
128
|
+
const [nodeId, outputName] = String(reference).split('.');
|
|
129
|
+
const node = nodeById.get(nodeId);
|
|
130
|
+
const output = node?.outputs?.find((candidate) => candidate.name === outputName);
|
|
131
|
+
return { nodeId, outputName, node, output };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateModeRequirements(owner, pathName, nodeById, diagnostics) {
|
|
135
|
+
if (owner.sideEffectMode === 'exactly-once' && !owner.idempotencyKey) {
|
|
136
|
+
diagnostics.push(diagnostic(
|
|
137
|
+
'UNSAFE_SIDE_EFFECT_MODE',
|
|
138
|
+
pathName,
|
|
139
|
+
'exactly-once side effects require an idempotencyKey.',
|
|
140
|
+
'Declare a stable idempotency key or use the idempotent mode.',
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
if (owner.sideEffectMode === 'approval-required') {
|
|
144
|
+
const gate = owner.approvalGate && nodeById.get(owner.approvalGate);
|
|
145
|
+
if (!gate || gate.kind !== 'gate') {
|
|
146
|
+
diagnostics.push(diagnostic(
|
|
147
|
+
'UNRESOLVED_APPROVAL_GATE',
|
|
148
|
+
`${pathName}/approvalGate`,
|
|
149
|
+
`Approval gate '${owner.approvalGate ?? ''}' does not resolve to a gate node.`,
|
|
150
|
+
'Point approvalGate at a declared node whose kind is gate.',
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateReducer(field, index, diagnostics) {
|
|
157
|
+
const type = field.schema?.type;
|
|
158
|
+
if (['sum', 'min', 'max'].includes(field.reducer) && !['integer', 'number'].includes(type)) {
|
|
159
|
+
diagnostics.push(diagnostic(
|
|
160
|
+
'INCOMPATIBLE_REDUCER',
|
|
161
|
+
`/spec/state/fields/${index}/reducer`,
|
|
162
|
+
`Reducer '${field.reducer}' requires an integer or number schema.`,
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
if (['append', 'set-union'].includes(field.reducer) && type !== 'array') {
|
|
166
|
+
diagnostics.push(diagnostic(
|
|
167
|
+
'INCOMPATIBLE_REDUCER',
|
|
168
|
+
`/spec/state/fields/${index}/reducer`,
|
|
169
|
+
`Reducer '${field.reducer}' requires an array schema.`,
|
|
170
|
+
));
|
|
171
|
+
}
|
|
172
|
+
if (field.reducer === 'merge' && type !== 'object') {
|
|
173
|
+
diagnostics.push(diagnostic(
|
|
174
|
+
'INCOMPATIBLE_REDUCER',
|
|
175
|
+
`/spec/state/fields/${index}/reducer`,
|
|
176
|
+
"Reducer 'merge' requires an object schema.",
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function semanticDiagnostics(manifest, options) {
|
|
182
|
+
const diagnostics = [];
|
|
183
|
+
const spec = manifest.spec;
|
|
184
|
+
const nodes = spec.nodes;
|
|
185
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
186
|
+
const candidateById = new Map(spec.candidates.map((candidate) => [candidate.id, candidate]));
|
|
187
|
+
const capabilityById = new Map(spec.capabilities.map((capability) => [capability.id, capability]));
|
|
188
|
+
const stateByName = new Map(spec.state.fields.map((field) => [field.name, field]));
|
|
189
|
+
const allowedPermissions = new Set(spec.permissions);
|
|
190
|
+
|
|
191
|
+
duplicates(nodes, 'id', '/spec/nodes', diagnostics);
|
|
192
|
+
duplicates(spec.candidates, 'id', '/spec/candidates', diagnostics);
|
|
193
|
+
duplicates(spec.capabilities, 'id', '/spec/capabilities', diagnostics);
|
|
194
|
+
duplicates(spec.state.fields, 'name', '/spec/state/fields', diagnostics);
|
|
195
|
+
duplicates(spec.joins, 'id', '/spec/joins', diagnostics);
|
|
196
|
+
const routesWithIds = spec.routes.filter((route) => route.id);
|
|
197
|
+
duplicates(routesWithIds, 'id', '/spec/routes', diagnostics);
|
|
198
|
+
|
|
199
|
+
for (const [index, field] of spec.state.fields.entries()) validateReducer(field, index, diagnostics);
|
|
200
|
+
|
|
201
|
+
for (const [index, entry] of spec.entry.entries()) {
|
|
202
|
+
if (!nodeById.has(entry)) {
|
|
203
|
+
diagnostics.push(diagnostic(
|
|
204
|
+
'UNRESOLVED_REFERENCE',
|
|
205
|
+
`/spec/entry/${index}`,
|
|
206
|
+
`Entry node '${entry}' is not declared.`,
|
|
207
|
+
'Declare the node or remove the entry reference.',
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const [index, candidate] of spec.candidates.entries()) {
|
|
213
|
+
if (options.catalogIds && !options.catalogIds.has(candidate.id)) {
|
|
214
|
+
diagnostics.push(diagnostic(
|
|
215
|
+
'UNRESOLVED_INDEX_REFERENCE',
|
|
216
|
+
`/spec/candidates/${index}/id`,
|
|
217
|
+
`Stable AIWG index reference '${candidate.id}' was not found in the supplied catalog.`,
|
|
218
|
+
'Run aiwg discover/show to obtain the current stable ID, then update the authorized candidate set.',
|
|
219
|
+
));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const [index, capability] of spec.capabilities.entries()) {
|
|
224
|
+
for (const permission of capability.permissions) {
|
|
225
|
+
if (!allowedPermissions.has(permission)) {
|
|
226
|
+
diagnostics.push(diagnostic(
|
|
227
|
+
'PERMISSION_WIDENING',
|
|
228
|
+
`/spec/capabilities/${index}/permissions`,
|
|
229
|
+
`Capability '${capability.id}' widens graph permission '${permission}'.`,
|
|
230
|
+
'Add the permission to spec.permissions only if the graph is authorized for it.',
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
validateModeRequirements(capability, `/spec/capabilities/${index}`, nodeById, diagnostics);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const [index, node] of nodes.entries()) {
|
|
238
|
+
const nodePath = `/spec/nodes/${index}`;
|
|
239
|
+
duplicates(node.inputs, 'name', `${nodePath}/inputs`, diagnostics);
|
|
240
|
+
duplicates(node.outputs, 'name', `${nodePath}/outputs`, diagnostics);
|
|
241
|
+
if (node.kind === 'gate') {
|
|
242
|
+
if (node.ref) diagnostics.push(diagnostic('ILLEGAL_GATE_REFERENCE', `${nodePath}/ref`, 'Gate nodes cannot resolve an AIWG artifact reference.'));
|
|
243
|
+
} else if (!node.ref) {
|
|
244
|
+
diagnostics.push(diagnostic(
|
|
245
|
+
'UNRESOLVED_INDEX_REFERENCE',
|
|
246
|
+
`${nodePath}/ref`,
|
|
247
|
+
`Node '${node.id}' requires a stable AIWG index reference.`,
|
|
248
|
+
'Use aiwg discover followed by aiwg show metadata and authorize that stable ID in spec.candidates.',
|
|
249
|
+
));
|
|
250
|
+
} else {
|
|
251
|
+
const candidate = candidateById.get(node.ref);
|
|
252
|
+
if (!candidate) {
|
|
253
|
+
diagnostics.push(diagnostic(
|
|
254
|
+
'UNRESOLVED_INDEX_REFERENCE',
|
|
255
|
+
`${nodePath}/ref`,
|
|
256
|
+
`Node '${node.id}' references '${node.ref}', which is absent from spec.candidates.`,
|
|
257
|
+
'Add the stable ID to the authorized candidate set or correct the node reference.',
|
|
258
|
+
));
|
|
259
|
+
} else if (candidate.kind !== node.kind) {
|
|
260
|
+
diagnostics.push(diagnostic(
|
|
261
|
+
'INCOMPATIBLE_REFERENCE_KIND',
|
|
262
|
+
`${nodePath}/ref`,
|
|
263
|
+
`Node kind '${node.kind}' is incompatible with authorized candidate kind '${candidate.kind}'.`,
|
|
264
|
+
));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (node.kind === 'function' && node.deterministic !== true) {
|
|
268
|
+
diagnostics.push(diagnostic(
|
|
269
|
+
'NONDETERMINISTIC_FUNCTION',
|
|
270
|
+
`${nodePath}/deterministic`,
|
|
271
|
+
`Function node '${node.id}' must declare deterministic: true.`,
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const dependency of node.dependsOn ?? []) {
|
|
276
|
+
if (!nodeById.has(dependency)) diagnostics.push(diagnostic('UNRESOLVED_REFERENCE', `${nodePath}/dependsOn`, `Dependency '${dependency}' is not declared.`));
|
|
277
|
+
}
|
|
278
|
+
if (node.fallback && !nodeById.has(node.fallback.node)) {
|
|
279
|
+
diagnostics.push(diagnostic('UNRESOLVED_REFERENCE', `${nodePath}/fallback/node`, `Fallback node '${node.fallback.node}' is not declared.`));
|
|
280
|
+
}
|
|
281
|
+
for (const capabilityId of node.capabilities) {
|
|
282
|
+
if (!capabilityById.has(capabilityId)) {
|
|
283
|
+
diagnostics.push(diagnostic(
|
|
284
|
+
'UNDECLARED_CAPABILITY',
|
|
285
|
+
`${nodePath}/capabilities`,
|
|
286
|
+
`Node '${node.id}' requests undeclared capability '${capabilityId}'.`,
|
|
287
|
+
));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const permission of node.permissions) {
|
|
291
|
+
if (!allowedPermissions.has(permission)) {
|
|
292
|
+
diagnostics.push(diagnostic('PERMISSION_WIDENING', `${nodePath}/permissions`, `Node '${node.id}' widens graph permission '${permission}'.`));
|
|
293
|
+
}
|
|
294
|
+
const permittedByCapability = node.capabilities.some((id) => capabilityById.get(id)?.permissions.includes(permission));
|
|
295
|
+
if (!permittedByCapability) {
|
|
296
|
+
diagnostics.push(diagnostic(
|
|
297
|
+
'PERMISSION_WIDENING',
|
|
298
|
+
`${nodePath}/permissions`,
|
|
299
|
+
`Node '${node.id}' requests '${permission}' without a declared capability granting it.`,
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const minimumMode = Math.max(0, ...node.capabilities.map((id) => SIDE_EFFECT_RANK[capabilityById.get(id)?.sideEffectMode] ?? 0));
|
|
304
|
+
if (SIDE_EFFECT_RANK[node.sideEffectMode] < minimumMode) {
|
|
305
|
+
diagnostics.push(diagnostic('UNSAFE_SIDE_EFFECT_MODE', `${nodePath}/sideEffectMode`, `Node '${node.id}' weakens a capability side-effect guarantee.`));
|
|
306
|
+
}
|
|
307
|
+
validateModeRequirements(node, nodePath, nodeById, diagnostics);
|
|
308
|
+
if (node.retry?.limit > 0 && node.sideEffectMode === 'approval-required') {
|
|
309
|
+
diagnostics.push(diagnostic(
|
|
310
|
+
'UNSAFE_RETRY_MODE',
|
|
311
|
+
`${nodePath}/retry`,
|
|
312
|
+
`Node '${node.id}' cannot automatically retry approval-required side effects.`,
|
|
313
|
+
'Set retry.limit to 0 or move approval into a separate gate before an idempotent operation.',
|
|
314
|
+
));
|
|
315
|
+
}
|
|
316
|
+
if (node.retry?.limit > 0 && node.sideEffectMode === 'exactly-once' && !node.idempotencyKey) {
|
|
317
|
+
diagnostics.push(diagnostic('UNSAFE_RETRY_MODE', `${nodePath}/retry`, `Node '${node.id}' retries exactly-once work without an idempotencyKey.`));
|
|
318
|
+
}
|
|
319
|
+
for (const [inputIndex, input] of node.inputs.entries()) {
|
|
320
|
+
if (input.from) {
|
|
321
|
+
const source = readBinding(input.from, nodeById);
|
|
322
|
+
if (!source.node || !source.output) {
|
|
323
|
+
diagnostics.push(diagnostic(
|
|
324
|
+
'UNRESOLVED_BINDING',
|
|
325
|
+
`${nodePath}/inputs/${inputIndex}/from`,
|
|
326
|
+
`Input binding '${input.from}' does not resolve to a declared node output.`,
|
|
327
|
+
));
|
|
328
|
+
} else if (!schemasCompatible(source.output.schema, input.schema)) {
|
|
329
|
+
diagnostics.push(diagnostic(
|
|
330
|
+
'INCOMPATIBLE_SCHEMA',
|
|
331
|
+
`${nodePath}/inputs/${inputIndex}/schema`,
|
|
332
|
+
`Input '${node.id}.${input.name}' is incompatible with output '${input.from}'.`,
|
|
333
|
+
'Align the source output and target input JSON Schema types.',
|
|
334
|
+
));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (input.state) {
|
|
338
|
+
const field = stateByName.get(input.state);
|
|
339
|
+
if (!field) diagnostics.push(diagnostic('UNRESOLVED_STATE', `${nodePath}/inputs/${inputIndex}/state`, `State field '${input.state}' is not declared.`));
|
|
340
|
+
else if (!schemasCompatible(field.schema, input.schema)) diagnostics.push(diagnostic('INCOMPATIBLE_SCHEMA', `${nodePath}/inputs/${inputIndex}/schema`, `Input '${node.id}.${input.name}' is incompatible with state '${input.state}'.`));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const [outputIndex, output] of node.outputs.entries()) {
|
|
344
|
+
if (!output.state) continue;
|
|
345
|
+
const field = stateByName.get(output.state);
|
|
346
|
+
if (!field) diagnostics.push(diagnostic('UNRESOLVED_STATE', `${nodePath}/outputs/${outputIndex}/state`, `State field '${output.state}' is not declared.`));
|
|
347
|
+
else if (!schemasCompatible(output.schema, field.schema)) diagnostics.push(diagnostic('INCOMPATIBLE_SCHEMA', `${nodePath}/outputs/${outputIndex}/schema`, `Output '${node.id}.${output.name}' is incompatible with state '${output.state}'.`));
|
|
348
|
+
}
|
|
349
|
+
if (node.ceilings) {
|
|
350
|
+
for (const key of Object.keys(spec.ceilings)) {
|
|
351
|
+
if (node.ceilings[key] > spec.ceilings[key]) diagnostics.push(diagnostic('CEILING_WIDENING', `${nodePath}/ceilings/${key}`, `Node ceiling '${key}' exceeds the graph ceiling.`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const [index, route] of spec.routes.entries()) {
|
|
357
|
+
if (!route.progress) continue;
|
|
358
|
+
const field = stateByName.get(route.progress.state);
|
|
359
|
+
if (!field) {
|
|
360
|
+
diagnostics.push(diagnostic(
|
|
361
|
+
'UNRESOLVED_PROGRESS_STATE',
|
|
362
|
+
`/spec/routes/${index}/progress/state`,
|
|
363
|
+
`Progress state '${route.progress.state}' is not declared.`,
|
|
364
|
+
));
|
|
365
|
+
} else if (field.schema?.type !== 'integer' || field.reducer !== 'replace' || !Number.isInteger(field.initial)) {
|
|
366
|
+
diagnostics.push(diagnostic(
|
|
367
|
+
'INVALID_PROGRESS_MEASURE',
|
|
368
|
+
`/spec/routes/${index}/progress`,
|
|
369
|
+
`Progress state '${route.progress.state}' must have an integer initial value and use the replace reducer.`,
|
|
370
|
+
'Use an integer state snapshot so strict decrease can be checked between cycle activations.',
|
|
371
|
+
));
|
|
372
|
+
}
|
|
373
|
+
if (!route.guard || !route.maxIterations) {
|
|
374
|
+
diagnostics.push(diagnostic(
|
|
375
|
+
'INVALID_PROGRESS_MEASURE',
|
|
376
|
+
`/spec/routes/${index}/progress`,
|
|
377
|
+
'A progress-guarded route also requires guard and maxIterations.',
|
|
378
|
+
'Keep maxIterations as the overall safety ceiling.',
|
|
379
|
+
));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const edges = [];
|
|
384
|
+
for (const [index, route] of spec.routes.entries()) {
|
|
385
|
+
if (!nodeById.has(route.from)) diagnostics.push(diagnostic('UNRESOLVED_REFERENCE', `/spec/routes/${index}/from`, `Route source '${route.from}' is not declared.`));
|
|
386
|
+
if (!nodeById.has(route.to)) diagnostics.push(diagnostic('UNRESOLVED_REFERENCE', `/spec/routes/${index}/to`, `Route target '${route.to}' is not declared.`));
|
|
387
|
+
edges.push({ from: route.from, to: route.to, route, path: `/spec/routes/${index}` });
|
|
388
|
+
}
|
|
389
|
+
for (const node of nodes) for (const dependency of node.dependsOn ?? []) edges.push({ from: dependency, to: node.id, route: null, path: '/spec/nodes' });
|
|
390
|
+
|
|
391
|
+
for (const [index, join] of spec.joins.entries()) {
|
|
392
|
+
const joinPath = `/spec/joins/${index}`;
|
|
393
|
+
if (!nodeById.has(join.target)) diagnostics.push(diagnostic('UNRESOLVED_REFERENCE', `${joinPath}/target`, `Join target '${join.target}' is not declared.`));
|
|
394
|
+
for (const source of join.sources) {
|
|
395
|
+
if (!nodeById.has(source)) diagnostics.push(diagnostic('UNRESOLVED_REFERENCE', `${joinPath}/sources`, `Join source '${source}' is not declared.`));
|
|
396
|
+
edges.push({ from: source, to: join.target, route: null, path: joinPath });
|
|
397
|
+
}
|
|
398
|
+
const policy = join.policy;
|
|
399
|
+
const allowed = {
|
|
400
|
+
all: [],
|
|
401
|
+
quorum: ['quorum'],
|
|
402
|
+
fixed: ['count'],
|
|
403
|
+
lcm: ['periods'],
|
|
404
|
+
converged: ['predicate', 'maxIterations'],
|
|
405
|
+
budget: ['maxCostUsd'],
|
|
406
|
+
}[policy.mode];
|
|
407
|
+
const supplied = Object.keys(policy).filter((key) => key !== 'mode');
|
|
408
|
+
if (supplied.some((key) => !allowed.includes(key)) || allowed.some((key) => policy[key] === undefined)) {
|
|
409
|
+
diagnostics.push(diagnostic('IMPOSSIBLE_JOIN', `${joinPath}/policy`, `Join mode '${policy.mode}' has invalid or missing policy parameters.`, `Allowed parameters: ${allowed.join(', ') || 'none'}.`));
|
|
410
|
+
}
|
|
411
|
+
if (policy.mode === 'quorum' && policy.quorum > join.sources.length) diagnostics.push(diagnostic('IMPOSSIBLE_JOIN', `${joinPath}/policy/quorum`, 'Quorum exceeds the number of join sources.'));
|
|
412
|
+
if (policy.mode === 'fixed' && policy.count > join.sources.length) diagnostics.push(diagnostic('IMPOSSIBLE_JOIN', `${joinPath}/policy/count`, 'Fixed count exceeds the number of join sources.'));
|
|
413
|
+
if (policy.mode === 'lcm' && policy.periods) {
|
|
414
|
+
if (policy.periods.length !== join.sources.length) diagnostics.push(diagnostic('IMPOSSIBLE_JOIN', `${joinPath}/policy/periods`, 'LCM joins require one period per source.'));
|
|
415
|
+
else {
|
|
416
|
+
const activation = lcm(policy.periods);
|
|
417
|
+
if (activation > spec.ceilings.activations) diagnostics.push(diagnostic('IMPOSSIBLE_JOIN', `${joinPath}/policy/periods`, `LCM activation ${activation} exceeds the graph activation ceiling ${spec.ceilings.activations}.`));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (policy.mode === 'budget' && policy.maxCostUsd > spec.ceilings.costUsd) diagnostics.push(diagnostic('IMPOSSIBLE_JOIN', `${joinPath}/policy/maxCostUsd`, 'Budget join exceeds the graph cost ceiling.'));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const reachable = new Set(spec.entry.filter((id) => nodeById.has(id)));
|
|
424
|
+
let changed = true;
|
|
425
|
+
while (changed) {
|
|
426
|
+
changed = false;
|
|
427
|
+
for (const edge of edges) {
|
|
428
|
+
if (reachable.has(edge.from) && !reachable.has(edge.to)) {
|
|
429
|
+
reachable.add(edge.to);
|
|
430
|
+
changed = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (const [index, node] of nodes.entries()) {
|
|
435
|
+
if (!reachable.has(node.id)) diagnostics.push(diagnostic('UNREACHABLE_NODE', `/spec/nodes/${index}`, `Node '${node.id}' is unreachable from spec.entry.`));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const component of tarjan(nodes.map((node) => node.id), edges)) {
|
|
439
|
+
const members = new Set(component);
|
|
440
|
+
const internal = edges.filter((edge) => members.has(edge.from) && members.has(edge.to));
|
|
441
|
+
const cyclic = component.length > 1 || internal.some((edge) => edge.from === edge.to);
|
|
442
|
+
if (!cyclic) continue;
|
|
443
|
+
const boundedEdge = internal.find((edge) => edge.route?.guard && edge.route?.maxIterations);
|
|
444
|
+
if (!boundedEdge) {
|
|
445
|
+
diagnostics.push(diagnostic(
|
|
446
|
+
'UNBOUNDED_CYCLE',
|
|
447
|
+
internal[0]?.path ?? '/spec/routes',
|
|
448
|
+
`Cycle among [${component.sort().join(', ')}] has no guarded, finite route.`,
|
|
449
|
+
'Add a CEL guard and finite maxIterations to the feedback route.',
|
|
450
|
+
));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const terminal = readBinding(spec.output.from, nodeById);
|
|
455
|
+
if (!terminal.node || !terminal.output) diagnostics.push(diagnostic('UNRESOLVED_BINDING', '/spec/output/from', `Terminal output '${spec.output.from}' does not resolve.`));
|
|
456
|
+
else if (!schemasCompatible(terminal.output.schema, spec.output.schema)) diagnostics.push(diagnostic('INCOMPATIBLE_SCHEMA', '/spec/output/schema', 'Terminal output schema is incompatible with its source binding.'));
|
|
457
|
+
if (spec.output.mode === 'typed-terminal-failure' && !spec.output.failureSchema) diagnostics.push(diagnostic('MISSING_FAILURE_SCHEMA', '/spec/output/failureSchema', 'typed-terminal-failure output requires failureSchema.'));
|
|
458
|
+
if (spec.output.mode !== 'typed-terminal-failure' && spec.output.failureSchema) diagnostics.push(diagnostic('ILLEGAL_OUTPUT_FIELD', '/spec/output/failureSchema', 'failureSchema is only valid with typed-terminal-failure output.'));
|
|
459
|
+
if (spec.failure.onNodeFailure === 'fallback' && !nodes.some((node) => node.fallback)) diagnostics.push(diagnostic('MISSING_FALLBACK', '/spec/failure/onNodeFailure', 'Global fallback policy requires at least one node fallback.'));
|
|
460
|
+
|
|
461
|
+
return diagnostics;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function normalizeFlowGraph(manifest) {
|
|
465
|
+
const graphId = manifest.metadata.namespace
|
|
466
|
+
? `${manifest.metadata.namespace}/${manifest.metadata.name}`
|
|
467
|
+
: manifest.metadata.name;
|
|
468
|
+
const routeEdges = manifest.spec.routes.map((route, index) => (
|
|
469
|
+
route.id ?? `${graphId}:route:${route.from}->${route.to}:${index}`
|
|
470
|
+
));
|
|
471
|
+
const joinEdges = manifest.spec.joins.flatMap((join) => (
|
|
472
|
+
join.sources.map((source) => `${graphId}:join:${join.id}:${source}->${join.target}`)
|
|
473
|
+
));
|
|
474
|
+
return {
|
|
475
|
+
contractVersion: 'flow.aiwg.io/v1alpha1',
|
|
476
|
+
kind: 'FlowGraphNormalized',
|
|
477
|
+
source: {
|
|
478
|
+
apiVersion: manifest.apiVersion,
|
|
479
|
+
kind: manifest.kind,
|
|
480
|
+
graphId,
|
|
481
|
+
},
|
|
482
|
+
identity: {
|
|
483
|
+
graphId,
|
|
484
|
+
runId: null,
|
|
485
|
+
nodeIds: manifest.spec.nodes.map((node) => `${graphId}:${node.id}`),
|
|
486
|
+
edgeIds: [...routeEdges, ...joinEdges],
|
|
487
|
+
},
|
|
488
|
+
graph: structuredClone(manifest),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function validateFlowGraph(manifest, options = {}) {
|
|
493
|
+
const schemaValid = validateSchema(manifest);
|
|
494
|
+
const diagnostics = schemaValid
|
|
495
|
+
? semanticDiagnostics(manifest, options)
|
|
496
|
+
: (validateSchema.errors ?? []).map((error) => diagnostic(
|
|
497
|
+
'SCHEMA_INVALID',
|
|
498
|
+
pointer(error),
|
|
499
|
+
schemaErrorMessage(error),
|
|
500
|
+
error.keyword === 'additionalProperties' ? 'Remove unknown fields; v1alpha1 is closed by default.' : undefined,
|
|
501
|
+
{ keyword: error.keyword, params: error.params },
|
|
502
|
+
));
|
|
503
|
+
const valid = diagnostics.length === 0;
|
|
504
|
+
return {
|
|
505
|
+
schemaVersion: 'flow.aiwg.io/v1alpha1',
|
|
506
|
+
kind: 'FlowGraphValidationReport',
|
|
507
|
+
valid,
|
|
508
|
+
diagnostics,
|
|
509
|
+
...(valid ? { normalized: normalizeFlowGraph(manifest) } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export { schema as flowGraphSchema };
|