@kontextmind/kxm 0.6.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/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue } from "./vnext-config.ts";
|
|
2
|
+
|
|
3
|
+
export const VNEXT_COMPILED_WORKFLOW_SCHEMA = "kxm.compiled-workflow.v1";
|
|
4
|
+
export const VNEXT_RESERVED_STEP_KINDS = ["workflow"] as const;
|
|
5
|
+
|
|
6
|
+
const WORKFLOW_SCHEMA = "kxm.workflow.v1";
|
|
7
|
+
const STEP_ID_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
8
|
+
const SUPPORTED_STEP_KINDS = new Set(["agent", "moa", "gate", "approval", "wait"]);
|
|
9
|
+
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
10
|
+
const JOIN_STRATEGIES = new Set(["all", "all-settled", "quorum", "first-success"]);
|
|
11
|
+
const EVIDENCE_KINDS = new Set(["assignment-result", "gate", "receipt", "approval", "artifact"]);
|
|
12
|
+
const REPOSITORY_ACCESS = new Set(["none", "read", "write"]);
|
|
13
|
+
const DISTINCT_BY = new Set(["provider", "model", "profile"]);
|
|
14
|
+
const BANNED_RUNTIME_HINT = "not yet supported";
|
|
15
|
+
|
|
16
|
+
export type ReadonlyJsonValue =
|
|
17
|
+
| string
|
|
18
|
+
| number
|
|
19
|
+
| boolean
|
|
20
|
+
| null
|
|
21
|
+
| readonly ReadonlyJsonValue[]
|
|
22
|
+
| { readonly [key: string]: ReadonlyJsonValue };
|
|
23
|
+
export type ReadonlyJsonObject = { readonly [key: string]: ReadonlyJsonValue };
|
|
24
|
+
|
|
25
|
+
export type VnextTerminalStatus = "completed" | "failed" | "cancelled";
|
|
26
|
+
export type VnextCompiledStepKind = "agent" | "moa" | "gate" | "approval" | "wait";
|
|
27
|
+
export type VnextRepositoryAccess = "none" | "read" | "write";
|
|
28
|
+
|
|
29
|
+
export type VnextCompiledTransition =
|
|
30
|
+
| { readonly to: "step"; readonly target: string; readonly edge: "forward" | "back"; readonly maxTransitions?: number }
|
|
31
|
+
| { readonly to: "terminal"; readonly terminalStatus: VnextTerminalStatus; readonly maxTransitions?: number };
|
|
32
|
+
|
|
33
|
+
export interface VnextCompiledProducerPolicy {
|
|
34
|
+
readonly minimumProducers: number;
|
|
35
|
+
readonly eligibleAgents: readonly string[];
|
|
36
|
+
readonly acceptedStatuses: readonly ["passed"];
|
|
37
|
+
readonly degradation?: { readonly minimumProducers: number };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VnextCompiledEvidence {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly kind: "assignment-result" | "gate" | "receipt" | "approval" | "artifact";
|
|
43
|
+
readonly minimum: number;
|
|
44
|
+
readonly reusableAcrossAttempts: boolean;
|
|
45
|
+
readonly producerPolicy?: VnextCompiledProducerPolicy;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface VnextCompiledAssignments {
|
|
49
|
+
readonly allowedAgents: readonly string[]; // declared, else [step.agent] when present, else []
|
|
50
|
+
readonly minimum: number;
|
|
51
|
+
readonly target: number;
|
|
52
|
+
readonly maximum: number;
|
|
53
|
+
readonly maxParallel: number;
|
|
54
|
+
readonly maxAttemptsPerAssignment: number;
|
|
55
|
+
readonly maxWriteRepositories?: number;
|
|
56
|
+
readonly distinctBy: readonly ("provider" | "model" | "profile")[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VnextCompiledJoin {
|
|
60
|
+
readonly strategy: "all" | "all-settled" | "quorum" | "first-success";
|
|
61
|
+
readonly minimumPassed?: number;
|
|
62
|
+
readonly cancelRemaining?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface VnextCompiledStepBase {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly index: number;
|
|
68
|
+
readonly description?: string;
|
|
69
|
+
readonly instructions?: string;
|
|
70
|
+
readonly maxAttempts: number;
|
|
71
|
+
readonly timeoutMs?: number;
|
|
72
|
+
readonly safeSpeculation: boolean;
|
|
73
|
+
readonly repositories: Readonly<Record<string, VnextRepositoryAccess>>;
|
|
74
|
+
readonly tools?: ReadonlyJsonObject;
|
|
75
|
+
readonly secrets: readonly ReadonlyJsonObject[];
|
|
76
|
+
readonly model?: ReadonlyJsonValue;
|
|
77
|
+
readonly requiredEvidence: readonly VnextCompiledEvidence[];
|
|
78
|
+
readonly outcomes: readonly string[];
|
|
79
|
+
readonly transitions: Readonly<Record<string, VnextCompiledTransition>>;
|
|
80
|
+
readonly requiresPlanHash: boolean;
|
|
81
|
+
readonly assignments: VnextCompiledAssignments;
|
|
82
|
+
readonly join: VnextCompiledJoin;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type VnextCompiledStep =
|
|
86
|
+
| (VnextCompiledStepBase & { readonly kind: "agent" | "moa"; readonly agent: string })
|
|
87
|
+
| (VnextCompiledStepBase & { readonly kind: "gate"; readonly gate: string; readonly expect: "pass" | "fail" })
|
|
88
|
+
| (VnextCompiledStepBase & { readonly kind: "approval" })
|
|
89
|
+
| (VnextCompiledStepBase & { readonly kind: "wait"; readonly signal: string });
|
|
90
|
+
|
|
91
|
+
export interface VnextCompiledPlan {
|
|
92
|
+
readonly schema: typeof VNEXT_COMPILED_WORKFLOW_SCHEMA;
|
|
93
|
+
readonly workflowId: string;
|
|
94
|
+
readonly sourcePath?: string;
|
|
95
|
+
readonly coordinator: string;
|
|
96
|
+
readonly limits: {
|
|
97
|
+
readonly maxTransitions?: number;
|
|
98
|
+
readonly maxRunDurationMs?: number;
|
|
99
|
+
readonly maxAgentTimeMs?: number;
|
|
100
|
+
readonly maxModelCost?: number;
|
|
101
|
+
readonly currency?: string;
|
|
102
|
+
};
|
|
103
|
+
readonly transitionBudget: number;
|
|
104
|
+
readonly hasBackEdges: boolean;
|
|
105
|
+
readonly entryStepId: string;
|
|
106
|
+
readonly order: readonly string[];
|
|
107
|
+
readonly steps: Readonly<Record<string, VnextCompiledStep>>;
|
|
108
|
+
readonly reproOracle?: { readonly stageId: string; readonly evidenceKey: string };
|
|
109
|
+
readonly planHash?: { readonly stageId: string; readonly evidenceKey: string };
|
|
110
|
+
readonly requirePlanHash: readonly string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface VnextEngineCompileIssue {
|
|
114
|
+
readonly code: string;
|
|
115
|
+
readonly workflowId: string;
|
|
116
|
+
readonly stepId?: string;
|
|
117
|
+
readonly outcome?: string;
|
|
118
|
+
readonly message: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class VnextEngineCompileError extends Error {
|
|
122
|
+
readonly issues: readonly VnextEngineCompileIssue[];
|
|
123
|
+
|
|
124
|
+
constructor(issues: readonly VnextEngineCompileIssue[]) {
|
|
125
|
+
const sorted = sortIssues(issues);
|
|
126
|
+
super(sorted.map((issue) => `${issue.workflowId}: ${issue.code}: ${issue.message}`).join("\n"));
|
|
127
|
+
this.name = "VnextEngineCompileError";
|
|
128
|
+
this.issues = sorted;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface VnextWorkflowCompileInput {
|
|
133
|
+
readonly id: string;
|
|
134
|
+
readonly value: JsonObject;
|
|
135
|
+
readonly logicalPath?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface IssueSink {
|
|
139
|
+
workflowId: string;
|
|
140
|
+
issues: VnextEngineCompileIssue[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function compileVnextWorkflow(input: VnextWorkflowCompileInput): VnextCompiledPlan {
|
|
144
|
+
const sink: IssueSink = { workflowId: input.id, issues: [] };
|
|
145
|
+
const value = input.value;
|
|
146
|
+
if (value.schema !== WORKFLOW_SCHEMA) {
|
|
147
|
+
pushIssue(sink, { code: "workflow_schema_unsupported", message: "schema is not kxm.workflow.v1" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const rawSteps = valuesOf(value, "steps").map(objectValue).filter((step): step is JsonObject => Boolean(step));
|
|
151
|
+
if (rawSteps.length === 0) {
|
|
152
|
+
pushIssue(sink, { code: "steps_empty", message: "workflow has no steps" });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stepIndex = new Map<string, number>();
|
|
156
|
+
for (const [index, step] of rawSteps.entries()) {
|
|
157
|
+
const id = stringValue(step.id);
|
|
158
|
+
if (!id || !isStepId(id)) {
|
|
159
|
+
pushIssue(sink, { code: "step_id_invalid", message: "step id is invalid", ...(id ? { stepId: id } : {}) });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (stepIndex.has(id)) {
|
|
163
|
+
pushIssue(sink, { code: "step_id_duplicate", stepId: id, message: `step ${id} is duplicated` });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
stepIndex.set(id, index);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const limitsObject = objectValue(value.limits);
|
|
170
|
+
const limits = compileLimits(limitsObject, sink);
|
|
171
|
+
const requirePlanHash = names(value.requirePlanHash);
|
|
172
|
+
const compiledSteps: VnextCompiledStep[] = [];
|
|
173
|
+
let hasBackEdges = false;
|
|
174
|
+
|
|
175
|
+
for (const [index, step] of rawSteps.entries()) {
|
|
176
|
+
const compiled = compileStep(step, index, stepIndex, requirePlanHash, sink);
|
|
177
|
+
if (!compiled) continue;
|
|
178
|
+
compiledSteps.push(compiled);
|
|
179
|
+
if (Object.values(compiled.transitions).some((transition) => transition.to === "step" && transition.edge === "back")) {
|
|
180
|
+
hasBackEdges = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (hasBackEdges && typeof limitsObject?.maxTransitions !== "number") {
|
|
185
|
+
pushIssue(sink, { code: "workflow_cycle_unbounded", message: "workflow with back-edges requires limits.maxTransitions" });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const reproOracle = compileOracle("reproOracle", objectValue(value.reproOracle), compiledSteps, sink);
|
|
189
|
+
const planHash = compileOracle("planHash", objectValue(value.planHash), compiledSteps, sink);
|
|
190
|
+
for (const stageId of requirePlanHash) {
|
|
191
|
+
if (!stepIndex.has(stageId)) {
|
|
192
|
+
pushIssue(sink, { code: "plan_hash_stage_unknown", message: `requirePlanHash references unknown stage ${stageId}` });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sink.issues.length > 0) throw new VnextEngineCompileError(sink.issues);
|
|
197
|
+
|
|
198
|
+
const order = compiledSteps.map((step) => step.id);
|
|
199
|
+
const steps = Object.create(null) as Record<string, VnextCompiledStep>;
|
|
200
|
+
for (const step of compiledSteps) steps[step.id] = step;
|
|
201
|
+
const entryStepId = order[0];
|
|
202
|
+
if (!entryStepId) throw new VnextEngineCompileError(sink.issues);
|
|
203
|
+
const coordinator = stringValue(value.coordinator) ?? "coordinator";
|
|
204
|
+
const transitionBudget = limits.maxTransitions ?? order.length;
|
|
205
|
+
const plan: VnextCompiledPlan = {
|
|
206
|
+
schema: VNEXT_COMPILED_WORKFLOW_SCHEMA,
|
|
207
|
+
workflowId: input.id,
|
|
208
|
+
...(input.logicalPath !== undefined ? { sourcePath: input.logicalPath } : {}),
|
|
209
|
+
coordinator,
|
|
210
|
+
limits,
|
|
211
|
+
transitionBudget,
|
|
212
|
+
hasBackEdges,
|
|
213
|
+
entryStepId,
|
|
214
|
+
order,
|
|
215
|
+
steps,
|
|
216
|
+
...(reproOracle ? { reproOracle } : {}),
|
|
217
|
+
...(planHash ? { planHash } : {}),
|
|
218
|
+
requirePlanHash: [...requirePlanHash],
|
|
219
|
+
};
|
|
220
|
+
return deepFreeze(plan);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function compileLimits(
|
|
224
|
+
limits: JsonObject | undefined,
|
|
225
|
+
sink: IssueSink,
|
|
226
|
+
): VnextCompiledPlan["limits"] {
|
|
227
|
+
if (!limits) return {};
|
|
228
|
+
const compiled: {
|
|
229
|
+
maxTransitions?: number;
|
|
230
|
+
maxRunDurationMs?: number;
|
|
231
|
+
maxAgentTimeMs?: number;
|
|
232
|
+
maxModelCost?: number;
|
|
233
|
+
currency?: string;
|
|
234
|
+
} = {};
|
|
235
|
+
if (limits.maxTransitions !== undefined) {
|
|
236
|
+
if (isCount(limits.maxTransitions)) compiled.maxTransitions = limits.maxTransitions;
|
|
237
|
+
else pushIssue(sink, { code: "count_invalid", message: "limits.maxTransitions must be an integer >= 1" });
|
|
238
|
+
}
|
|
239
|
+
if (limits.maxRunDurationMs !== undefined) {
|
|
240
|
+
if (isDuration(limits.maxRunDurationMs)) compiled.maxRunDurationMs = limits.maxRunDurationMs;
|
|
241
|
+
else pushIssue(sink, { code: "duration_invalid", message: "limits.maxRunDurationMs must be an integer >= 0" });
|
|
242
|
+
}
|
|
243
|
+
if (limits.maxAgentTimeMs !== undefined) {
|
|
244
|
+
if (isDuration(limits.maxAgentTimeMs)) compiled.maxAgentTimeMs = limits.maxAgentTimeMs;
|
|
245
|
+
else pushIssue(sink, { code: "duration_invalid", message: "limits.maxAgentTimeMs must be an integer >= 0" });
|
|
246
|
+
}
|
|
247
|
+
if (limits.maxModelCost !== undefined) {
|
|
248
|
+
if (isCost(limits.maxModelCost)) compiled.maxModelCost = limits.maxModelCost;
|
|
249
|
+
else pushIssue(sink, { code: "cost_invalid", message: "limits.maxModelCost must be a finite number > 0" });
|
|
250
|
+
}
|
|
251
|
+
if (limits.currency !== undefined) {
|
|
252
|
+
if (typeof limits.currency === "string") compiled.currency = limits.currency;
|
|
253
|
+
}
|
|
254
|
+
return compiled;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function compileStep(
|
|
258
|
+
step: JsonObject,
|
|
259
|
+
index: number,
|
|
260
|
+
stepIndex: ReadonlyMap<string, number>,
|
|
261
|
+
requirePlanHash: readonly string[],
|
|
262
|
+
sink: IssueSink,
|
|
263
|
+
): VnextCompiledStep | undefined {
|
|
264
|
+
const id = stringValue(step.id);
|
|
265
|
+
if (!id || !isStepId(id) || stepIndex.get(id) !== index) return undefined;
|
|
266
|
+
const kind = stringValue(step.kind);
|
|
267
|
+
if (kind === "workflow") {
|
|
268
|
+
pushIssue(sink, { code: "step_kind_reserved", stepId: id, message: `kind workflow is ${BANNED_RUNTIME_HINT}` });
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
if (!kind || !SUPPORTED_STEP_KINDS.has(kind)) {
|
|
272
|
+
pushIssue(sink, { code: "step_kind_unknown", stepId: id, message: `kind ${String(kind)} is unknown` });
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const agent = stringValue(step.agent);
|
|
277
|
+
const gate = stringValue(step.gate);
|
|
278
|
+
const signal = stringValue(step.signal);
|
|
279
|
+
if ((kind === "agent" || kind === "moa") && !agent) {
|
|
280
|
+
pushIssue(sink, { code: "step_agent_missing", stepId: id, message: `${id} is missing agent` });
|
|
281
|
+
}
|
|
282
|
+
if (kind === "gate" && !gate) {
|
|
283
|
+
pushIssue(sink, { code: "step_gate_missing", stepId: id, message: `${id} is missing gate` });
|
|
284
|
+
}
|
|
285
|
+
if (kind === "wait" && !signal) {
|
|
286
|
+
pushIssue(sink, { code: "step_signal_missing", stepId: id, message: `${id} is missing signal` });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (step.expect !== undefined && (kind !== "gate" || (step.expect !== "pass" && step.expect !== "fail"))) {
|
|
290
|
+
pushIssue(sink, { code: "gate_expect_invalid", stepId: id, message: "expect is gate-only pass or fail" });
|
|
291
|
+
}
|
|
292
|
+
if (kind === "gate" && Object.keys(objectValue(step.on) ?? {}).some((outcome) => outcome === "implementation_failure" || outcome === "repro_missing")) {
|
|
293
|
+
pushIssue(sink, { code: "gate_outcome_renamed", stepId: id, message: "use implementation-failure and repro-missing" });
|
|
294
|
+
}
|
|
295
|
+
const maxAttempts = compileCountField(step.maxAttempts, 1, `${id}.maxAttempts`, id, sink);
|
|
296
|
+
const timeoutMs = compileOptionalDuration(step.timeoutMs, `${id}.timeoutMs`, id, sink);
|
|
297
|
+
const assignments = compileAssignments(step, id, agent, sink);
|
|
298
|
+
const join = compileJoin(step, id, sink);
|
|
299
|
+
const requiredEvidence = compileEvidence(step, id, sink);
|
|
300
|
+
const transitions = compileTransitions(step, id, index, stepIndex, sink);
|
|
301
|
+
const outcomes = Object.keys(transitions).sort(compareCodeUnits);
|
|
302
|
+
const orderedTransitions = Object.create(null) as Record<string, VnextCompiledTransition>;
|
|
303
|
+
for (const outcome of outcomes) {
|
|
304
|
+
const transition = transitions[outcome];
|
|
305
|
+
if (transition) orderedTransitions[outcome] = transition;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const description = stringValue(step.description);
|
|
309
|
+
const instructions = stringValue(step.instructions);
|
|
310
|
+
const base: VnextCompiledStepBase = {
|
|
311
|
+
id,
|
|
312
|
+
index,
|
|
313
|
+
...(description !== undefined ? { description } : {}),
|
|
314
|
+
...(instructions !== undefined ? { instructions } : {}),
|
|
315
|
+
maxAttempts,
|
|
316
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
317
|
+
safeSpeculation: step.safeSpeculation === true,
|
|
318
|
+
repositories: compileRepositories(step.repositories),
|
|
319
|
+
...(isJsonObject(step.tools) ? { tools: cloneJsonObject(step.tools) } : {}),
|
|
320
|
+
secrets: compileSecrets(step.secrets),
|
|
321
|
+
...(step.model !== undefined ? { model: cloneJsonValue(step.model) } : {}),
|
|
322
|
+
requiredEvidence,
|
|
323
|
+
outcomes,
|
|
324
|
+
transitions: orderedTransitions,
|
|
325
|
+
requiresPlanHash: requirePlanHash.includes(id),
|
|
326
|
+
assignments,
|
|
327
|
+
join,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (kind === "agent" || kind === "moa") {
|
|
331
|
+
if (!agent) return undefined;
|
|
332
|
+
return { ...base, kind, agent };
|
|
333
|
+
}
|
|
334
|
+
if (kind === "gate") {
|
|
335
|
+
if (!gate) return undefined;
|
|
336
|
+
return { ...base, kind, gate, expect: step.expect === "fail" ? "fail" : "pass" };
|
|
337
|
+
}
|
|
338
|
+
if (kind === "wait") {
|
|
339
|
+
if (!signal) return undefined;
|
|
340
|
+
return { ...base, kind, signal };
|
|
341
|
+
}
|
|
342
|
+
return { ...base, kind: "approval" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function compileAssignments(
|
|
346
|
+
step: JsonObject,
|
|
347
|
+
stepId: string,
|
|
348
|
+
primaryAgentId: string | undefined,
|
|
349
|
+
sink: IssueSink,
|
|
350
|
+
): VnextCompiledAssignments {
|
|
351
|
+
const assignment = objectValue(step.assignments);
|
|
352
|
+
const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
|
|
353
|
+
const allowedAgents = declaredAgents.length > 0 ? declaredAgents : primaryAgentId ? [primaryAgentId] : [];
|
|
354
|
+
// Mirrors validateWorkflow in vnext-config.ts (minimum 1, target = minimum, maximum = target, maxParallel = maximum) for every kind. Do not resolve kind-conditional defaults here; the loader is the ceiling authority. Phase 7 changes both together.
|
|
355
|
+
const minimum = compileCountField(assignment?.minimum, 1, `${stepId}.assignments.minimum`, stepId, sink);
|
|
356
|
+
const target = compileCountField(assignment?.target, minimum, `${stepId}.assignments.target`, stepId, sink);
|
|
357
|
+
const maximum = compileCountField(assignment?.maximum, target, `${stepId}.assignments.maximum`, stepId, sink);
|
|
358
|
+
const maxParallel = compileCountField(assignment?.maxParallel, maximum, `${stepId}.assignments.maxParallel`, stepId, sink);
|
|
359
|
+
const maxAttemptsPerAssignment = compileCountField(
|
|
360
|
+
assignment?.maxAttemptsPerAssignment,
|
|
361
|
+
1,
|
|
362
|
+
`${stepId}.assignments.maxAttemptsPerAssignment`,
|
|
363
|
+
stepId,
|
|
364
|
+
sink,
|
|
365
|
+
);
|
|
366
|
+
const maxWriteRepositories = compileOptionalCount(
|
|
367
|
+
assignment?.maxWriteRepositories,
|
|
368
|
+
`${stepId}.assignments.maxWriteRepositories`,
|
|
369
|
+
stepId,
|
|
370
|
+
sink,
|
|
371
|
+
);
|
|
372
|
+
const distinctBy = names(assignment?.distinctBy).filter((value): value is "provider" | "model" | "profile" => DISTINCT_BY.has(value));
|
|
373
|
+
return {
|
|
374
|
+
allowedAgents: [...allowedAgents],
|
|
375
|
+
minimum,
|
|
376
|
+
target,
|
|
377
|
+
maximum,
|
|
378
|
+
maxParallel,
|
|
379
|
+
maxAttemptsPerAssignment,
|
|
380
|
+
...(maxWriteRepositories !== undefined ? { maxWriteRepositories } : {}),
|
|
381
|
+
distinctBy,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function compileJoin(step: JsonObject, stepId: string, sink: IssueSink): VnextCompiledJoin {
|
|
386
|
+
const join = objectValue(step.join);
|
|
387
|
+
if (!join) return { strategy: "all" };
|
|
388
|
+
const declared = stringValue(join.strategy);
|
|
389
|
+
const strategy = declared && JOIN_STRATEGIES.has(declared)
|
|
390
|
+
? declared as VnextCompiledJoin["strategy"]
|
|
391
|
+
: "all";
|
|
392
|
+
const minimumPassed = compileOptionalCount(join.minimumPassed, `${stepId}.join.minimumPassed`, stepId, sink);
|
|
393
|
+
const compiled: VnextCompiledJoin = {
|
|
394
|
+
strategy,
|
|
395
|
+
...(minimumPassed !== undefined ? { minimumPassed } : {}),
|
|
396
|
+
...(typeof join.cancelRemaining === "boolean" ? { cancelRemaining: join.cancelRemaining } : {}),
|
|
397
|
+
};
|
|
398
|
+
return compiled;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function compileEvidence(step: JsonObject, stepId: string, sink: IssueSink): VnextCompiledEvidence[] {
|
|
402
|
+
const evidence: VnextCompiledEvidence[] = [];
|
|
403
|
+
const seen = new Set<string>();
|
|
404
|
+
for (const candidate of valuesOf(step, "requiredEvidence")) {
|
|
405
|
+
const requirement = objectValue(candidate);
|
|
406
|
+
if (!requirement) continue;
|
|
407
|
+
const key = stringValue(requirement.key);
|
|
408
|
+
if (!key) continue;
|
|
409
|
+
if (seen.has(key)) {
|
|
410
|
+
pushIssue(sink, { code: "evidence_key_duplicate", stepId, message: `${stepId} evidence key ${key} is duplicated` });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
seen.add(key);
|
|
414
|
+
const kind = stringValue(requirement.kind);
|
|
415
|
+
if (!kind || !EVIDENCE_KINDS.has(kind)) continue;
|
|
416
|
+
const minimum = compileCountField(requirement.minimum, 1, `${stepId}.${key}.minimum`, stepId, sink);
|
|
417
|
+
const producerPolicy = objectValue(requirement.producerPolicy);
|
|
418
|
+
const compiled: VnextCompiledEvidence = {
|
|
419
|
+
key,
|
|
420
|
+
kind: kind as VnextCompiledEvidence["kind"],
|
|
421
|
+
minimum,
|
|
422
|
+
reusableAcrossAttempts: requirement.reusableAcrossAttempts === true,
|
|
423
|
+
...(producerPolicy ? { producerPolicy: compileProducerPolicy(producerPolicy, stepId, key, sink) } : {}),
|
|
424
|
+
};
|
|
425
|
+
evidence.push(compiled);
|
|
426
|
+
}
|
|
427
|
+
return evidence;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function compileProducerPolicy(
|
|
431
|
+
policy: JsonObject,
|
|
432
|
+
stepId: string,
|
|
433
|
+
key: string,
|
|
434
|
+
sink: IssueSink,
|
|
435
|
+
): VnextCompiledProducerPolicy {
|
|
436
|
+
const minimumProducers = compileCountField(
|
|
437
|
+
policy.minimumProducers,
|
|
438
|
+
1,
|
|
439
|
+
`${stepId}.${key}.producerPolicy.minimumProducers`,
|
|
440
|
+
stepId,
|
|
441
|
+
sink,
|
|
442
|
+
);
|
|
443
|
+
const degradation = objectValue(policy.degradation);
|
|
444
|
+
const degradationMinimum = compileOptionalCount(
|
|
445
|
+
degradation?.minimumProducers,
|
|
446
|
+
`${stepId}.${key}.producerPolicy.degradation.minimumProducers`,
|
|
447
|
+
stepId,
|
|
448
|
+
sink,
|
|
449
|
+
);
|
|
450
|
+
return {
|
|
451
|
+
minimumProducers,
|
|
452
|
+
eligibleAgents: names(policy.eligibleAgents),
|
|
453
|
+
acceptedStatuses: ["passed"],
|
|
454
|
+
...(degradationMinimum !== undefined ? { degradation: { minimumProducers: degradationMinimum } } : {}),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function compileTransitions(
|
|
459
|
+
step: JsonObject,
|
|
460
|
+
stepId: string,
|
|
461
|
+
index: number,
|
|
462
|
+
stepIndex: ReadonlyMap<string, number>,
|
|
463
|
+
sink: IssueSink,
|
|
464
|
+
): Record<string, VnextCompiledTransition> {
|
|
465
|
+
const outcomes = objectValue(step.on);
|
|
466
|
+
const compiled = Object.create(null) as Record<string, VnextCompiledTransition>;
|
|
467
|
+
if (!outcomes || Object.keys(outcomes).length === 0) {
|
|
468
|
+
pushIssue(sink, { code: "transitions_missing", stepId, message: `${stepId} has no declared transitions` });
|
|
469
|
+
return compiled;
|
|
470
|
+
}
|
|
471
|
+
for (const [outcome, raw] of Object.entries(outcomes)) {
|
|
472
|
+
const parsed = parseTransition(raw);
|
|
473
|
+
const maxTransitions = compileOptionalCount(
|
|
474
|
+
parsed.maxTransitions,
|
|
475
|
+
`${stepId}.${outcome}.maxTransitions`,
|
|
476
|
+
stepId,
|
|
477
|
+
sink,
|
|
478
|
+
outcome,
|
|
479
|
+
);
|
|
480
|
+
if (parsed.target === "$terminal") {
|
|
481
|
+
const status = parsed.terminalStatus;
|
|
482
|
+
if (status === undefined) {
|
|
483
|
+
pushIssue(sink, {
|
|
484
|
+
code: "transition_terminal_status_missing",
|
|
485
|
+
stepId,
|
|
486
|
+
outcome,
|
|
487
|
+
message: `${stepId}.${outcome} terminal transition requires terminalStatus`,
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (!TERMINAL_STATUSES.has(status)) {
|
|
492
|
+
pushIssue(sink, {
|
|
493
|
+
code: "transition_terminal_status_invalid",
|
|
494
|
+
stepId,
|
|
495
|
+
outcome,
|
|
496
|
+
message: `${stepId}.${outcome} terminalStatus is invalid`,
|
|
497
|
+
});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
compiled[outcome] = {
|
|
501
|
+
to: "terminal",
|
|
502
|
+
terminalStatus: status as VnextTerminalStatus,
|
|
503
|
+
...(maxTransitions !== undefined ? { maxTransitions } : {}),
|
|
504
|
+
};
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (parsed.terminalStatus !== undefined) {
|
|
508
|
+
pushIssue(sink, {
|
|
509
|
+
code: "transition_terminal_status_unexpected",
|
|
510
|
+
stepId,
|
|
511
|
+
outcome,
|
|
512
|
+
message: `${stepId}.${outcome} step target must not declare terminalStatus`,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (!parsed.target || !stepIndex.has(parsed.target)) {
|
|
516
|
+
pushIssue(sink, {
|
|
517
|
+
code: "transition_target_unknown",
|
|
518
|
+
stepId,
|
|
519
|
+
outcome,
|
|
520
|
+
message: `${stepId}.${outcome} references unknown target ${String(parsed.target)}`,
|
|
521
|
+
});
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const targetIndex = stepIndex.get(parsed.target)!;
|
|
525
|
+
const back = targetIndex <= index;
|
|
526
|
+
if (back && maxTransitions === undefined && parsed.maxTransitions === undefined) {
|
|
527
|
+
pushIssue(sink, {
|
|
528
|
+
code: "back_edge_unbounded",
|
|
529
|
+
stepId,
|
|
530
|
+
outcome,
|
|
531
|
+
message: `${stepId}.${outcome} back-edge requires maxTransitions`,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
compiled[outcome] = {
|
|
535
|
+
to: "step",
|
|
536
|
+
target: parsed.target,
|
|
537
|
+
edge: back ? "back" : "forward",
|
|
538
|
+
...(maxTransitions !== undefined ? { maxTransitions } : {}),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return compiled;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function parseTransition(value: JsonValue): { target?: string; maxTransitions?: JsonValue; terminalStatus?: string } {
|
|
545
|
+
if (typeof value === "string") return { target: value };
|
|
546
|
+
const object = objectValue(value);
|
|
547
|
+
if (!object) return {};
|
|
548
|
+
const target = stringValue(object.target);
|
|
549
|
+
const terminalStatus = stringValue(object.terminalStatus);
|
|
550
|
+
return {
|
|
551
|
+
...(target ? { target } : {}),
|
|
552
|
+
...(object.maxTransitions !== undefined ? { maxTransitions: object.maxTransitions } : {}),
|
|
553
|
+
...(terminalStatus ? { terminalStatus } : {}),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function compileOracle(
|
|
558
|
+
field: "reproOracle" | "planHash",
|
|
559
|
+
oracle: JsonObject | undefined,
|
|
560
|
+
steps: readonly VnextCompiledStep[],
|
|
561
|
+
sink: IssueSink,
|
|
562
|
+
): { stageId: string; evidenceKey: string } | undefined {
|
|
563
|
+
if (!oracle) return undefined;
|
|
564
|
+
const stageId = stringValue(oracle.stageId);
|
|
565
|
+
const evidenceKey = stringValue(oracle.evidenceKey);
|
|
566
|
+
const step = stageId === undefined ? undefined : steps.find((candidate) => candidate.id === stageId);
|
|
567
|
+
if (!stageId || !step) {
|
|
568
|
+
pushIssue(sink, { code: "oracle_stage_unknown", message: `${field} references unknown stage ${String(stageId)}` });
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
if (!evidenceKey || !step.requiredEvidence.some((item) => item.key === evidenceKey)) {
|
|
572
|
+
pushIssue(sink, {
|
|
573
|
+
code: "oracle_evidence_unknown",
|
|
574
|
+
message: `${field} references undeclared evidence ${String(evidenceKey)} in ${stageId}`,
|
|
575
|
+
});
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
return { stageId, evidenceKey };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function compileRepositories(value: JsonValue | undefined): Readonly<Record<string, VnextRepositoryAccess>> {
|
|
582
|
+
const source = objectValue(value) ?? {};
|
|
583
|
+
const repositories: Record<string, VnextRepositoryAccess> = {};
|
|
584
|
+
for (const [id, access] of Object.entries(source)) {
|
|
585
|
+
if (typeof access === "string" && REPOSITORY_ACCESS.has(access)) {
|
|
586
|
+
repositories[id] = access as VnextRepositoryAccess;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return repositories;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function compileSecrets(value: JsonValue | undefined): ReadonlyJsonObject[] {
|
|
593
|
+
if (!Array.isArray(value)) return [];
|
|
594
|
+
return value.map(objectValue).filter((entry): entry is JsonObject => Boolean(entry)).map(cloneJsonObject);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function compileCountField(
|
|
598
|
+
value: JsonValue | undefined,
|
|
599
|
+
fallback: number,
|
|
600
|
+
path: string,
|
|
601
|
+
stepId: string,
|
|
602
|
+
sink: IssueSink,
|
|
603
|
+
): number {
|
|
604
|
+
if (value === undefined) return fallback;
|
|
605
|
+
if (isCount(value)) return value;
|
|
606
|
+
pushIssue(sink, { code: "count_invalid", stepId, message: `${path} must be an integer >= 1` });
|
|
607
|
+
return fallback;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function compileOptionalCount(
|
|
611
|
+
value: JsonValue | undefined,
|
|
612
|
+
path: string,
|
|
613
|
+
stepId: string,
|
|
614
|
+
sink: IssueSink,
|
|
615
|
+
outcome?: string,
|
|
616
|
+
): number | undefined {
|
|
617
|
+
if (value === undefined) return undefined;
|
|
618
|
+
if (isCount(value)) return value;
|
|
619
|
+
pushIssue(sink, {
|
|
620
|
+
code: "count_invalid",
|
|
621
|
+
stepId,
|
|
622
|
+
...(outcome ? { outcome } : {}),
|
|
623
|
+
message: `${path} must be an integer >= 1`,
|
|
624
|
+
});
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function compileOptionalDuration(
|
|
629
|
+
value: JsonValue | undefined,
|
|
630
|
+
path: string,
|
|
631
|
+
stepId: string,
|
|
632
|
+
sink: IssueSink,
|
|
633
|
+
): number | undefined {
|
|
634
|
+
if (value === undefined) return undefined;
|
|
635
|
+
if (isDuration(value)) return value;
|
|
636
|
+
pushIssue(sink, { code: "duration_invalid", stepId, message: `${path} must be an integer >= 0` });
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isCount(value: JsonValue): value is number {
|
|
641
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function isDuration(value: JsonValue): value is number {
|
|
645
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function isCost(value: JsonValue): value is number {
|
|
649
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function isStepId(value: string): boolean {
|
|
653
|
+
return value.length >= 1 && value.length <= 64 && STEP_ID_PATTERN.test(value);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
|
657
|
+
return value !== undefined && value !== null && typeof value === "object" && !Array.isArray(value);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function objectValue(value: JsonValue | undefined): JsonObject | undefined {
|
|
661
|
+
return isJsonObject(value) ? value : undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function stringValue(value: JsonValue | undefined): string | undefined {
|
|
665
|
+
return typeof value === "string" ? value : undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function valuesOf(object: JsonObject, field: string): JsonValue[] {
|
|
669
|
+
const value = object[field];
|
|
670
|
+
return Array.isArray(value) ? value : [];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function names(value: JsonValue | undefined): string[] {
|
|
674
|
+
return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : [];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function cloneJsonValue(value: JsonValue): ReadonlyJsonValue {
|
|
678
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
679
|
+
if (Array.isArray(value)) return value.map((entry) => cloneJsonValue(entry));
|
|
680
|
+
return cloneJsonObject(isJsonObject(value) ? value : {});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function cloneJsonObject(value: JsonObject): ReadonlyJsonObject {
|
|
684
|
+
const copy: { [key: string]: ReadonlyJsonValue } = {};
|
|
685
|
+
for (const [key, child] of Object.entries(value)) copy[key] = cloneJsonValue(child);
|
|
686
|
+
return copy;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function deepFreeze<T>(value: T): T {
|
|
690
|
+
if (value === null || typeof value !== "object") return value;
|
|
691
|
+
if (Array.isArray(value)) {
|
|
692
|
+
for (const entry of value) deepFreeze(entry);
|
|
693
|
+
} else {
|
|
694
|
+
for (const entry of Object.values(value as Record<string, unknown>)) deepFreeze(entry);
|
|
695
|
+
}
|
|
696
|
+
return Object.freeze(value);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function compareCodeUnits(left: string, right: string): number {
|
|
700
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function compareOptional(left: string | undefined, right: string | undefined): number {
|
|
704
|
+
if (left === right) return 0;
|
|
705
|
+
if (left === undefined) return -1;
|
|
706
|
+
if (right === undefined) return 1;
|
|
707
|
+
return compareCodeUnits(left, right);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function sortIssues(issues: readonly VnextEngineCompileIssue[]): VnextEngineCompileIssue[] {
|
|
711
|
+
return [...issues].sort((left, right) => {
|
|
712
|
+
const byStep = compareOptional(left.stepId, right.stepId);
|
|
713
|
+
if (byStep !== 0) return byStep;
|
|
714
|
+
const byOutcome = compareOptional(left.outcome, right.outcome);
|
|
715
|
+
if (byOutcome !== 0) return byOutcome;
|
|
716
|
+
return compareCodeUnits(left.code, right.code);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function pushIssue(sink: IssueSink, issue: Omit<VnextEngineCompileIssue, "workflowId">): void {
|
|
721
|
+
sink.issues.push({ workflowId: sink.workflowId, ...issue });
|
|
722
|
+
}
|