@sweny-ai/core 0.1.160 → 0.1.161
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/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.js +30 -1
- package/dist/cli/workflow-upgrade.d.ts +35 -0
- package/dist/cli/workflow-upgrade.js +139 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +8 -1
- package/dist/loader.js +12 -2
- package/dist/migrations.d.ts +63 -0
- package/dist/migrations.js +93 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +9 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/cli/main.d.ts
CHANGED
|
@@ -17,3 +17,6 @@ export declare function workflowExportAction(name: string): void;
|
|
|
17
17
|
export declare function workflowValidateAction(file: string, options: {
|
|
18
18
|
json?: boolean;
|
|
19
19
|
}): void;
|
|
20
|
+
export declare function workflowUpgradeAction(file: string, options: {
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
}): void;
|
package/dist/cli/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { configuredSkills, configuredSkillsWithDiagnostics } from "../skills/cus
|
|
|
19
19
|
import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
|
|
20
20
|
import { loadAdditionalContext } from "../templates.js";
|
|
21
21
|
import { loadAndValidateWorkflow } from "../loader.js";
|
|
22
|
+
import { upgradeWorkflowFile } from "./workflow-upgrade.js";
|
|
22
23
|
import { validateRuntimeInput } from "../inputs.js";
|
|
23
24
|
import { mergeDryRunIntoInput, parseInputFlag, parseRunBudgetFlags, parseSpendFlags } from "./workflow-input.js";
|
|
24
25
|
import { stringify as stringifyYaml } from "yaml";
|
|
@@ -725,6 +726,8 @@ export function loadWorkflowFile(filePath, knownSkills) {
|
|
|
725
726
|
if (!result.ok) {
|
|
726
727
|
throw new Error(`Invalid workflow file:\n${result.errors.map((e) => ` ${e.message}`).join("\n")}`);
|
|
727
728
|
}
|
|
729
|
+
for (const w of result.warnings ?? [])
|
|
730
|
+
console.error(chalk.yellow(` \u26A0 ${w}`));
|
|
728
731
|
return result.workflow;
|
|
729
732
|
}
|
|
730
733
|
export async function workflowRunAction(file, options) {
|
|
@@ -1122,7 +1125,10 @@ export function workflowValidateAction(file, options) {
|
|
|
1122
1125
|
const result = loadAndValidateWorkflow(file);
|
|
1123
1126
|
// Missing skill env is a warning here, not a failure: `run` is where it blocks.
|
|
1124
1127
|
const warnings = result.ok
|
|
1125
|
-
?
|
|
1128
|
+
? [
|
|
1129
|
+
...(result.warnings ?? []),
|
|
1130
|
+
...skillEnvWarnings(result.workflow, process.env, builtinSkills.concat(configuredSkills(process.env, process.cwd()))),
|
|
1131
|
+
]
|
|
1126
1132
|
: [];
|
|
1127
1133
|
if (options.json) {
|
|
1128
1134
|
const errs = result.ok ? [] : result.errors;
|
|
@@ -1149,6 +1155,29 @@ workflowCmd
|
|
|
1149
1155
|
.description("Validate a workflow YAML or JSON file")
|
|
1150
1156
|
.option("--json", "Output result as JSON")
|
|
1151
1157
|
.action(workflowValidateAction);
|
|
1158
|
+
export function workflowUpgradeAction(file, options) {
|
|
1159
|
+
const result = upgradeWorkflowFile(file, { dryRun: options.dryRun });
|
|
1160
|
+
if (result.status === "error") {
|
|
1161
|
+
console.error(chalk.red(` \u2717 ${result.message}`));
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (result.status === "current") {
|
|
1166
|
+
console.log(chalk.green(` \u2713 ${file} is already at spec_version "${result.version}"`));
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const verb = result.status === "dry-run" ? "Would upgrade" : "Upgraded";
|
|
1170
|
+
console.log(chalk.green(` \u2713 ${verb} ${file}: spec_version "${result.from}" -> "${result.to}"`));
|
|
1171
|
+
for (const step of result.steps)
|
|
1172
|
+
console.log(chalk.dim(` - ${step}`));
|
|
1173
|
+
if (result.status === "dry-run")
|
|
1174
|
+
console.log(chalk.dim(" (dry run, file not changed)"));
|
|
1175
|
+
}
|
|
1176
|
+
workflowCmd
|
|
1177
|
+
.command("upgrade <file>")
|
|
1178
|
+
.description("Rewrite a workflow file at the current spec version (comments are kept where possible)")
|
|
1179
|
+
.option("--dry-run", "Show what would change without writing the file")
|
|
1180
|
+
.action(workflowUpgradeAction);
|
|
1152
1181
|
const workflowRunCmd = workflowCmd
|
|
1153
1182
|
.command("run [file]")
|
|
1154
1183
|
.description(WORKFLOW_RUN_DESCRIPTION)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny workflow upgrade <file> [--dry-run]`
|
|
3
|
+
*
|
|
4
|
+
* Rewrites a workflow file at the current spec version by running the same
|
|
5
|
+
* migration chain the loader applies in memory.
|
|
6
|
+
*
|
|
7
|
+
* Comment preservation (YAML): the file is edited as a YAML document, not
|
|
8
|
+
* re-serialized from scratch. Comments, blank lines, key order and quoting on
|
|
9
|
+
* everything a migration did not touch are kept. NOT preserved: a comment
|
|
10
|
+
* attached to a value a migration replaced or removed, the original position of
|
|
11
|
+
* keys a migration renamed (they are added at the end of their map), and
|
|
12
|
+
* formatting of arrays whose length a migration changed (rewritten whole).
|
|
13
|
+
* JSON files carry no comments and are rewritten with 2-space indentation.
|
|
14
|
+
*/
|
|
15
|
+
import { type MigrationConfig } from "../migrations.js";
|
|
16
|
+
export type UpgradeResult = {
|
|
17
|
+
status: "error";
|
|
18
|
+
message: string;
|
|
19
|
+
} | {
|
|
20
|
+
status: "current";
|
|
21
|
+
version: number;
|
|
22
|
+
} | {
|
|
23
|
+
status: "upgraded" | "dry-run";
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
/** Migration descriptions applied, in order. Empty when only `spec_version` was stamped. */
|
|
27
|
+
steps: string[];
|
|
28
|
+
/** The full text that was (or, for dry-run, would be) written. */
|
|
29
|
+
content: string;
|
|
30
|
+
};
|
|
31
|
+
export interface UpgradeOptions {
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
migration?: MigrationConfig;
|
|
34
|
+
}
|
|
35
|
+
export declare function upgradeWorkflowFile(filePath: string, options?: UpgradeOptions): UpgradeResult;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny workflow upgrade <file> [--dry-run]`
|
|
3
|
+
*
|
|
4
|
+
* Rewrites a workflow file at the current spec version by running the same
|
|
5
|
+
* migration chain the loader applies in memory.
|
|
6
|
+
*
|
|
7
|
+
* Comment preservation (YAML): the file is edited as a YAML document, not
|
|
8
|
+
* re-serialized from scratch. Comments, blank lines, key order and quoting on
|
|
9
|
+
* everything a migration did not touch are kept. NOT preserved: a comment
|
|
10
|
+
* attached to a value a migration replaced or removed, the original position of
|
|
11
|
+
* keys a migration renamed (they are added at the end of their map), and
|
|
12
|
+
* formatting of arrays whose length a migration changed (rewritten whole).
|
|
13
|
+
* JSON files carry no comments and are rewritten with 2-space indentation.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { isMap, parseDocument } from "yaml";
|
|
18
|
+
import { DEFAULT_MIGRATION_CONFIG, migrateWorkflow } from "../migrations.js";
|
|
19
|
+
import { validateParsed } from "../loader.js";
|
|
20
|
+
function isPlainObject(v) {
|
|
21
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
function withoutSpecVersion(obj) {
|
|
24
|
+
const copy = { ...obj };
|
|
25
|
+
delete copy.spec_version;
|
|
26
|
+
return copy;
|
|
27
|
+
}
|
|
28
|
+
function sameJson(a, b) {
|
|
29
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
30
|
+
}
|
|
31
|
+
/** Apply the difference old -> next onto the YAML document, touching only what changed. */
|
|
32
|
+
function applyDiff(doc, at, old, next) {
|
|
33
|
+
if (sameJson(old, next))
|
|
34
|
+
return;
|
|
35
|
+
if (isPlainObject(old) && isPlainObject(next)) {
|
|
36
|
+
for (const key of Object.keys(old)) {
|
|
37
|
+
if (!(key in next))
|
|
38
|
+
doc.deleteIn([...at, key]);
|
|
39
|
+
}
|
|
40
|
+
for (const key of Object.keys(next)) {
|
|
41
|
+
if (key in old)
|
|
42
|
+
applyDiff(doc, [...at, key], old[key], next[key]);
|
|
43
|
+
else
|
|
44
|
+
doc.setIn([...at, key], next[key]);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(old) && Array.isArray(next) && old.length === next.length) {
|
|
49
|
+
next.forEach((item, i) => applyDiff(doc, [...at, i], old[i], item));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (at.length === 0)
|
|
53
|
+
doc.contents = doc.createNode(next);
|
|
54
|
+
else
|
|
55
|
+
doc.setIn(at, next);
|
|
56
|
+
}
|
|
57
|
+
function setSpecVersion(doc, version) {
|
|
58
|
+
const root = doc.contents;
|
|
59
|
+
if (!isMap(root))
|
|
60
|
+
return;
|
|
61
|
+
if (root.has("spec_version")) {
|
|
62
|
+
root.set("spec_version", version);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Insert as the first key. A comment sitting directly above the old first key
|
|
66
|
+
// is the file's header comment; carry it onto the new pair so it stays on top.
|
|
67
|
+
const first = root.items[0];
|
|
68
|
+
const pair = doc.createPair("spec_version", version);
|
|
69
|
+
const firstKey = first?.key;
|
|
70
|
+
if (firstKey && firstKey.commentBefore) {
|
|
71
|
+
pair.key.commentBefore = firstKey.commentBefore;
|
|
72
|
+
firstKey.commentBefore = undefined;
|
|
73
|
+
}
|
|
74
|
+
root.items.unshift(pair);
|
|
75
|
+
}
|
|
76
|
+
export function upgradeWorkflowFile(filePath, options = {}) {
|
|
77
|
+
const config = options.migration ?? DEFAULT_MIGRATION_CONFIG;
|
|
78
|
+
let content;
|
|
79
|
+
try {
|
|
80
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return { status: "error", message: `Could not read "${filePath}": ${err instanceof Error ? err.message : err}` };
|
|
84
|
+
}
|
|
85
|
+
const isYaml = [".yaml", ".yml"].includes(path.extname(filePath).toLowerCase());
|
|
86
|
+
let doc;
|
|
87
|
+
let raw;
|
|
88
|
+
try {
|
|
89
|
+
if (isYaml) {
|
|
90
|
+
doc = parseDocument(content);
|
|
91
|
+
if (doc.errors.length > 0)
|
|
92
|
+
throw doc.errors[0];
|
|
93
|
+
raw = doc.toJS();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
raw = JSON.parse(content);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { status: "error", message: `Could not parse "${filePath}": ${err instanceof Error ? err.message : err}` };
|
|
101
|
+
}
|
|
102
|
+
if (!isPlainObject(raw))
|
|
103
|
+
return { status: "error", message: `Expected a workflow object in "${filePath}"` };
|
|
104
|
+
const migrated = migrateWorkflow(raw, config);
|
|
105
|
+
if (!migrated.ok)
|
|
106
|
+
return { status: "error", message: migrated.message };
|
|
107
|
+
const versionText = String(migrated.to);
|
|
108
|
+
if (migrated.applied.length === 0 && raw.spec_version === versionText) {
|
|
109
|
+
return { status: "current", version: migrated.to };
|
|
110
|
+
}
|
|
111
|
+
// Never write a file that would not load.
|
|
112
|
+
const check = validateParsed(migrated.workflow, { migration: config });
|
|
113
|
+
if (!check.ok) {
|
|
114
|
+
return {
|
|
115
|
+
status: "error",
|
|
116
|
+
message: `The upgraded workflow is not valid, so "${filePath}" was left unchanged:\n${check.errors
|
|
117
|
+
.map((e) => ` ${e.message}`)
|
|
118
|
+
.join("\n")}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
let out;
|
|
122
|
+
if (doc) {
|
|
123
|
+
applyDiff(doc, [], withoutSpecVersion(raw), withoutSpecVersion(migrated.workflow));
|
|
124
|
+
setSpecVersion(doc, versionText);
|
|
125
|
+
out = doc.toString();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
out = JSON.stringify({ spec_version: versionText, ...withoutSpecVersion(migrated.workflow) }, null, 2) + "\n";
|
|
129
|
+
}
|
|
130
|
+
if (!options.dryRun)
|
|
131
|
+
fs.writeFileSync(filePath, out, "utf-8");
|
|
132
|
+
return {
|
|
133
|
+
status: options.dryRun ? "dry-run" : "upgraded",
|
|
134
|
+
from: migrated.from,
|
|
135
|
+
to: migrated.to,
|
|
136
|
+
steps: migrated.applied.map((m) => m.description),
|
|
137
|
+
content: out,
|
|
138
|
+
};
|
|
139
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, so
|
|
|
45
45
|
export type { WorkflowError } from "./schema.js";
|
|
46
46
|
export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
|
|
47
47
|
export type { LoaderResult, LoaderError, LoaderOptions } from "./loader.js";
|
|
48
|
+
export { CURRENT_SPEC_VERSION, MIGRATIONS, migrateWorkflow } from "./migrations.js";
|
|
49
|
+
export type { Migration, MigrationConfig, MigrateResult } from "./migrations.js";
|
|
48
50
|
export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
|
|
49
51
|
export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
|
|
50
52
|
export type { ProviderContextOptions, SkillMcpOptions } from "./mcp.js";
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, so
|
|
|
47
47
|
// the CLI uses for `workflow run`, `workflow validate`, and `publish`).
|
|
48
48
|
// Node-only: loader.ts imports `node:fs`, so this is NOT mirrored in browser.ts.
|
|
49
49
|
export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
|
|
50
|
+
export { CURRENT_SPEC_VERSION, MIGRATIONS, migrateWorkflow } from "./migrations.js";
|
|
50
51
|
// Workflow input validation
|
|
51
52
|
export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
|
|
52
53
|
// MCP auto-injection
|
package/dist/loader.d.ts
CHANGED
|
@@ -11,15 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { Workflow } from "./types.js";
|
|
13
13
|
import { type WorkflowError } from "./schema.js";
|
|
14
|
+
import { type MigrationConfig } from "./migrations.js";
|
|
14
15
|
/** Structural/schema error. `code` is present only for structural errors. */
|
|
15
16
|
export interface LoaderError {
|
|
16
17
|
message: string;
|
|
17
|
-
code?: WorkflowError["code"] | "IO" | "PARSE" | "SCHEMA";
|
|
18
|
+
code?: WorkflowError["code"] | "IO" | "PARSE" | "SCHEMA" | "SPEC_VERSION";
|
|
18
19
|
nodeId?: string;
|
|
19
20
|
}
|
|
20
21
|
export type LoaderResult = {
|
|
21
22
|
ok: true;
|
|
22
23
|
workflow: Workflow;
|
|
24
|
+
/** Non-fatal notes, e.g. the one-line "migrated in memory" warning. */
|
|
25
|
+
warnings?: string[];
|
|
23
26
|
} | {
|
|
24
27
|
ok: false;
|
|
25
28
|
errors: LoaderError[];
|
|
@@ -27,6 +30,10 @@ export type LoaderResult = {
|
|
|
27
30
|
export interface LoaderOptions {
|
|
28
31
|
/** Optional set of known skill IDs for UNKNOWN_SKILL validation. */
|
|
29
32
|
knownSkills?: Set<string>;
|
|
33
|
+
/** Spec version + migration registry. Defaults to the built-in one; tests inject their own. */
|
|
34
|
+
migration?: MigrationConfig;
|
|
35
|
+
/** Path shown in the migration warning. Set by loadAndValidateWorkflow. */
|
|
36
|
+
filePath?: string;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* Load a workflow YAML or JSON file, parse with the canonical Zod schema,
|
package/dist/loader.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
15
|
import { ZodError } from "zod";
|
|
16
16
|
import { workflowZ, validateWorkflow, preflightLegacyVerify } from "./schema.js";
|
|
17
|
+
import { DEFAULT_MIGRATION_CONFIG, migrateWorkflow, migrationWarning, } from "./migrations.js";
|
|
17
18
|
/**
|
|
18
19
|
* Load a workflow YAML or JSON file, parse with the canonical Zod schema,
|
|
19
20
|
* and run structural validation. All three phases contribute to `errors`.
|
|
@@ -36,7 +37,7 @@ export function loadAndValidateWorkflow(filePath, options = {}) {
|
|
|
36
37
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37
38
|
return { ok: false, errors: [{ message: `Could not parse "${filePath}": ${msg}`, code: "PARSE" }] };
|
|
38
39
|
}
|
|
39
|
-
return validateParsed(raw, options);
|
|
40
|
+
return validateParsed(raw, { ...options, filePath: options.filePath ?? filePath });
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* Validate an already-parsed workflow object (e.g. from a marketplace fetch
|
|
@@ -47,6 +48,15 @@ export function validateParsed(raw, options = {}) {
|
|
|
47
48
|
const kind = raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
|
|
48
49
|
return { ok: false, errors: [{ message: `Expected a workflow object, got ${kind}`, code: "SCHEMA" }] };
|
|
49
50
|
}
|
|
51
|
+
// Apply spec_version migrations in memory (older versions), or refuse a
|
|
52
|
+
// version newer than this build supports. Runs first so a migration can
|
|
53
|
+
// reshape a file before the checks below see it.
|
|
54
|
+
const migrated = migrateWorkflow(raw, options.migration ?? DEFAULT_MIGRATION_CONFIG);
|
|
55
|
+
if (!migrated.ok) {
|
|
56
|
+
return { ok: false, errors: [{ message: migrated.message, code: "SPEC_VERSION" }] };
|
|
57
|
+
}
|
|
58
|
+
raw = migrated.workflow;
|
|
59
|
+
const warnings = migrated.applied.length > 0 ? [migrationWarning(migrated.from, migrated.to, options.filePath)] : [];
|
|
50
60
|
// Detect legacy `verify:` blocks (renamed to `eval:` in v0.2.0) before the
|
|
51
61
|
// Zod parse. workflowZ is intentionally non-strict at the top level, so a
|
|
52
62
|
// top-level `verify:` would be silently STRIPPED instead of surfacing the
|
|
@@ -83,5 +93,5 @@ export function validateParsed(raw, options = {}) {
|
|
|
83
93
|
errors: structural.map((e) => ({ message: e.message, code: e.code, nodeId: e.nodeId })),
|
|
84
94
|
};
|
|
85
95
|
}
|
|
86
|
-
return { ok: true, workflow: parsed };
|
|
96
|
+
return warnings.length > 0 ? { ok: true, workflow: parsed, warnings } : { ok: true, workflow: parsed };
|
|
87
97
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow spec versioning and migrations.
|
|
3
|
+
*
|
|
4
|
+
* Every workflow file carries an optional top-level `spec_version` (a string
|
|
5
|
+
* holding a positive integer, e.g. "1"). A file without the field is version 1:
|
|
6
|
+
* that is what every file written before the field existed means.
|
|
7
|
+
*
|
|
8
|
+
* Breaking spec changes bump CURRENT_SPEC_VERSION and add one entry to
|
|
9
|
+
* MIGRATIONS: a pure function from the raw (unvalidated) vN object to the raw
|
|
10
|
+
* vN+1 object. The loader applies the chain in memory so old files keep
|
|
11
|
+
* running; `sweny workflow upgrade <file>` writes the result back to disk.
|
|
12
|
+
*
|
|
13
|
+
* Adding a migration: see `__tests__/migrations.test.ts` for the harness
|
|
14
|
+
* (registry validity check plus a chain test to copy).
|
|
15
|
+
*/
|
|
16
|
+
/** A raw, not-yet-validated workflow object as parsed from YAML or JSON. */
|
|
17
|
+
export type RawWorkflow = Record<string, unknown>;
|
|
18
|
+
export interface Migration {
|
|
19
|
+
/** Version this migration reads. */
|
|
20
|
+
from: number;
|
|
21
|
+
/** Version it produces. Always `from + 1`. */
|
|
22
|
+
to: number;
|
|
23
|
+
/** One line shown by `sweny workflow upgrade`. */
|
|
24
|
+
description: string;
|
|
25
|
+
/** Pure: must not mutate its input. Returns the next-version object. */
|
|
26
|
+
migrate: (raw: RawWorkflow) => RawWorkflow;
|
|
27
|
+
}
|
|
28
|
+
/** The spec version this build of sweny reads natively. */
|
|
29
|
+
export declare const CURRENT_SPEC_VERSION = 1;
|
|
30
|
+
/**
|
|
31
|
+
* Ordered registry, one entry per version step. Empty while v1 is current.
|
|
32
|
+
* Append only; never edit a shipped migration.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MIGRATIONS: readonly Migration[];
|
|
35
|
+
export interface MigrationConfig {
|
|
36
|
+
current: number;
|
|
37
|
+
migrations: readonly Migration[];
|
|
38
|
+
}
|
|
39
|
+
export declare const DEFAULT_MIGRATION_CONFIG: MigrationConfig;
|
|
40
|
+
export type MigrateError = {
|
|
41
|
+
code: "INVALID_VERSION" | "TOO_NEW" | "MIGRATION_FAILED";
|
|
42
|
+
message: string;
|
|
43
|
+
};
|
|
44
|
+
export type MigrateResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
/** The (possibly migrated) object. `spec_version` is a string when it was declared or migrated. */
|
|
47
|
+
workflow: RawWorkflow;
|
|
48
|
+
/** Version the file declared (1 when absent). */
|
|
49
|
+
from: number;
|
|
50
|
+
/** Version of `workflow` after migration. */
|
|
51
|
+
to: number;
|
|
52
|
+
/** Whether the input declared `spec_version`. */
|
|
53
|
+
declared: boolean;
|
|
54
|
+
/** Migrations applied, in order. Empty when the file is already current. */
|
|
55
|
+
applied: Migration[];
|
|
56
|
+
} | ({
|
|
57
|
+
ok: false;
|
|
58
|
+
} & MigrateError);
|
|
59
|
+
/** Throws if a registry is not an unbroken chain of single steps ending at `current`. */
|
|
60
|
+
export declare function assertRegistryValid(config: MigrationConfig): void;
|
|
61
|
+
export declare function migrateWorkflow(raw: RawWorkflow, config?: MigrationConfig): MigrateResult;
|
|
62
|
+
/** The single warning the loader emits when it migrated in memory. */
|
|
63
|
+
export declare function migrationWarning(from: number, to: number, filePath?: string): string;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow spec versioning and migrations.
|
|
3
|
+
*
|
|
4
|
+
* Every workflow file carries an optional top-level `spec_version` (a string
|
|
5
|
+
* holding a positive integer, e.g. "1"). A file without the field is version 1:
|
|
6
|
+
* that is what every file written before the field existed means.
|
|
7
|
+
*
|
|
8
|
+
* Breaking spec changes bump CURRENT_SPEC_VERSION and add one entry to
|
|
9
|
+
* MIGRATIONS: a pure function from the raw (unvalidated) vN object to the raw
|
|
10
|
+
* vN+1 object. The loader applies the chain in memory so old files keep
|
|
11
|
+
* running; `sweny workflow upgrade <file>` writes the result back to disk.
|
|
12
|
+
*
|
|
13
|
+
* Adding a migration: see `__tests__/migrations.test.ts` for the harness
|
|
14
|
+
* (registry validity check plus a chain test to copy).
|
|
15
|
+
*/
|
|
16
|
+
/** The spec version this build of sweny reads natively. */
|
|
17
|
+
export const CURRENT_SPEC_VERSION = 1;
|
|
18
|
+
/**
|
|
19
|
+
* Ordered registry, one entry per version step. Empty while v1 is current.
|
|
20
|
+
* Append only; never edit a shipped migration.
|
|
21
|
+
*/
|
|
22
|
+
export const MIGRATIONS = [];
|
|
23
|
+
export const DEFAULT_MIGRATION_CONFIG = {
|
|
24
|
+
current: CURRENT_SPEC_VERSION,
|
|
25
|
+
migrations: MIGRATIONS,
|
|
26
|
+
};
|
|
27
|
+
/** Throws if a registry is not an unbroken chain of single steps ending at `current`. */
|
|
28
|
+
export function assertRegistryValid(config) {
|
|
29
|
+
const { current, migrations } = config;
|
|
30
|
+
if (!Number.isInteger(current) || current < 1)
|
|
31
|
+
throw new Error("current spec version must be a positive integer");
|
|
32
|
+
if (migrations.length !== current - 1) {
|
|
33
|
+
throw new Error(`expected ${current - 1} migration(s) to reach v${current}, found ${migrations.length}`);
|
|
34
|
+
}
|
|
35
|
+
migrations.forEach((m, i) => {
|
|
36
|
+
if (m.from !== i + 1 || m.to !== i + 2) {
|
|
37
|
+
throw new Error(`migration #${i} must be v${i + 1} -> v${i + 2}, got v${m.from} -> v${m.to}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function parseVersion(value) {
|
|
42
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1)
|
|
43
|
+
return value;
|
|
44
|
+
if (typeof value === "string" && /^[1-9]\d*$/.test(value))
|
|
45
|
+
return Number(value);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
export function migrateWorkflow(raw, config = DEFAULT_MIGRATION_CONFIG) {
|
|
49
|
+
const declared = "spec_version" in raw && raw.spec_version !== undefined;
|
|
50
|
+
const from = declared ? parseVersion(raw.spec_version) : 1;
|
|
51
|
+
if (from === null) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
code: "INVALID_VERSION",
|
|
55
|
+
message: `spec_version must be a positive integer string like "1", got ${JSON.stringify(raw.spec_version)}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (from > config.current) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
code: "TOO_NEW",
|
|
62
|
+
message: `This workflow declares spec_version "${from}", but this version of sweny supports up to "${config.current}". ` +
|
|
63
|
+
`Upgrade sweny (run "sweny upgrade", or npm install -g @sweny-ai/core@latest) and try again.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
let current = declared ? { ...raw, spec_version: String(from) } : raw;
|
|
67
|
+
const applied = [];
|
|
68
|
+
for (let v = from; v < config.current; v++) {
|
|
69
|
+
const step = config.migrations.find((m) => m.from === v);
|
|
70
|
+
if (!step) {
|
|
71
|
+
return { ok: false, code: "MIGRATION_FAILED", message: `No migration registered from spec_version "${v}".` };
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
current = { ...step.migrate(current), spec_version: String(step.to) };
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
code: "MIGRATION_FAILED",
|
|
81
|
+
message: `Migration from spec_version "${step.from}" to "${step.to}" failed: ${msg}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
applied.push(step);
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, workflow: current, from, to: config.current, declared, applied };
|
|
87
|
+
}
|
|
88
|
+
/** The single warning the loader emits when it migrated in memory. */
|
|
89
|
+
export function migrationWarning(from, to, filePath) {
|
|
90
|
+
const target = filePath ? `"${filePath}"` : "<file>";
|
|
91
|
+
return (`Workflow uses spec_version "${from}" (current is "${to}"); migrated in memory for this run. ` +
|
|
92
|
+
`Run \`sweny workflow upgrade ${target}\` to update the file.`);
|
|
93
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1714,6 +1714,7 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
1714
1714
|
id: z.ZodString;
|
|
1715
1715
|
name: z.ZodString;
|
|
1716
1716
|
description: z.ZodDefault<z.ZodString>;
|
|
1717
|
+
spec_version: z.ZodOptional<z.ZodString>;
|
|
1717
1718
|
workflow_type: z.ZodOptional<z.ZodEnum<["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"]>>;
|
|
1718
1719
|
nodes: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
1719
1720
|
name: z.ZodString;
|
|
@@ -2876,6 +2877,7 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2876
2877
|
type?: string | undefined;
|
|
2877
2878
|
})[] | undefined;
|
|
2878
2879
|
judge_model?: string | undefined;
|
|
2880
|
+
spec_version?: string | undefined;
|
|
2879
2881
|
workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
|
|
2880
2882
|
judge_budget?: number | undefined;
|
|
2881
2883
|
context_mode?: "full" | "bounded" | undefined;
|
|
@@ -3064,6 +3066,7 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
3064
3066
|
type?: string | undefined;
|
|
3065
3067
|
})[] | undefined;
|
|
3066
3068
|
judge_model?: string | undefined;
|
|
3069
|
+
spec_version?: string | undefined;
|
|
3067
3070
|
workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
|
|
3068
3071
|
judge_budget?: number | undefined;
|
|
3069
3072
|
context_mode?: "full" | "bounded" | undefined;
|
|
@@ -3266,6 +3269,7 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
3266
3269
|
type?: string | undefined;
|
|
3267
3270
|
})[] | undefined;
|
|
3268
3271
|
judge_model?: string | undefined;
|
|
3272
|
+
spec_version?: string | undefined;
|
|
3269
3273
|
workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
|
|
3270
3274
|
judge_budget?: number | undefined;
|
|
3271
3275
|
context_mode?: "full" | "bounded" | undefined;
|
|
@@ -3649,6 +3653,11 @@ export declare const workflowJsonSchema: {
|
|
|
3649
3653
|
readonly description: {
|
|
3650
3654
|
readonly type: "string";
|
|
3651
3655
|
};
|
|
3656
|
+
readonly spec_version: {
|
|
3657
|
+
readonly type: "string";
|
|
3658
|
+
readonly pattern: "^[1-9][0-9]*$";
|
|
3659
|
+
readonly description: "Workflow spec version as a positive integer string. Absent means 1. Older versions are migrated in memory on load; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.";
|
|
3660
|
+
};
|
|
3652
3661
|
readonly entry: {
|
|
3653
3662
|
readonly type: "string";
|
|
3654
3663
|
readonly minLength: 1;
|
package/dist/schema.js
CHANGED
|
@@ -357,6 +357,10 @@ export const workflowZ = z.object({
|
|
|
357
357
|
id: z.string().min(1),
|
|
358
358
|
name: z.string().min(1),
|
|
359
359
|
description: z.string().default(""),
|
|
360
|
+
spec_version: z
|
|
361
|
+
.string()
|
|
362
|
+
.regex(/^[1-9]\d*$/, "spec_version must be a positive integer string")
|
|
363
|
+
.optional(),
|
|
360
364
|
workflow_type: workflowTypeZ.optional(),
|
|
361
365
|
nodes: z.record(nodeZ),
|
|
362
366
|
edges: z.array(edgeZ),
|
|
@@ -954,6 +958,11 @@ export const workflowJsonSchema = {
|
|
|
954
958
|
id: { type: "string", minLength: 1 },
|
|
955
959
|
name: { type: "string", minLength: 1 },
|
|
956
960
|
description: { type: "string" },
|
|
961
|
+
spec_version: {
|
|
962
|
+
type: "string",
|
|
963
|
+
pattern: "^[1-9][0-9]*$",
|
|
964
|
+
description: "Workflow spec version as a positive integer string. Absent means 1. Older versions are migrated in memory on load; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.",
|
|
965
|
+
},
|
|
957
966
|
entry: { type: "string", minLength: 1, description: "ID of the entry node" },
|
|
958
967
|
rules: {
|
|
959
968
|
type: "array",
|
package/dist/types.d.ts
CHANGED
|
@@ -514,6 +514,8 @@ export interface Workflow {
|
|
|
514
514
|
id: string;
|
|
515
515
|
name: string;
|
|
516
516
|
description: string;
|
|
517
|
+
/** Spec version, a positive integer string (e.g. "1"). Absent means "1". See `migrations.ts`. */
|
|
518
|
+
spec_version?: string;
|
|
517
519
|
/** Optional. Defaults to "generic" when absent. Required on marketplace templates. */
|
|
518
520
|
workflow_type?: WorkflowType;
|
|
519
521
|
nodes: Record<string, Node>;
|