@crewhaus/migration-engine 0.5.8 → 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/dist/index.d.ts +113 -8
- package/dist/index.js +208 -26
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -4,53 +4,158 @@
|
|
|
4
4
|
* walks the registered chain to bridge any pair of source/target versions
|
|
5
5
|
* (forward when `from < to`, reverse when `from > to`).
|
|
6
6
|
*
|
|
7
|
-
* Spec versions are integer ints
|
|
8
|
-
*
|
|
7
|
+
* Spec versions are integer ints. The first migration registered is the
|
|
8
|
+
* no-op skeleton 0 → 1; 0.6.0 adds the first REAL migration, 1 → 2
|
|
9
|
+
* (`MIGRATION_1_TO_2`, design §9.2).
|
|
9
10
|
*
|
|
10
11
|
* Specs are passed in as parsed YAML (raw object); migration steps are
|
|
11
12
|
* pure transforms `(spec) → spec` returning the next-version shape. The
|
|
12
13
|
* engine is unaware of YAML serialisation — callers use yaml-js or the
|
|
13
14
|
* spec parser to round-trip.
|
|
15
|
+
*
|
|
16
|
+
* 0.6.0 (§9.2) — two additive seams on `Migration`:
|
|
17
|
+
*
|
|
18
|
+
* - `edits?(spec)` declares the SAME change `up()` performs as a list of
|
|
19
|
+
* `SpecEdit`-shaped path/value edits. A caller that owns the YAML TEXT
|
|
20
|
+
* (`crewhaus upgrade`, `migrate-all`) applies those through the CST
|
|
21
|
+
* writer (`applySpecEdits` in `@crewhaus/spec-patch`) so comments and
|
|
22
|
+
* key order survive; `up()` stays the object-level truth for callers
|
|
23
|
+
* that only hold a parsed object. {@link MigrationEngine.planUp} walks
|
|
24
|
+
* the chain and hands both back per step.
|
|
25
|
+
* - `irreversible?: true` marks a lossy step. The DOWN-walk throws
|
|
26
|
+
* {@link MigrationIrreversibleError} across such a step instead of
|
|
27
|
+
* calling its `down()` — the marker alone would be decoration.
|
|
14
28
|
*/
|
|
15
29
|
import { CrewhausError } from "@crewhaus/errors";
|
|
16
30
|
export type SpecObject = Record<string, unknown> & {
|
|
17
31
|
readonly version?: number;
|
|
18
32
|
};
|
|
33
|
+
export type MigrationEditPathSegment = string | number;
|
|
34
|
+
/**
|
|
35
|
+
* One comment-preserving edit a migration declares: "make `path` carry
|
|
36
|
+
* `value`" (upsert) or, when `value` is `undefined`, "make `path` absent".
|
|
37
|
+
* Structurally identical to `SpecEdit` in `@crewhaus/spec-patch` — declared
|
|
38
|
+
* here so the engine keeps its one-dependency footprint and stays
|
|
39
|
+
* serialisation-free; callers pass these straight to `applySpecEdits`.
|
|
40
|
+
*/
|
|
41
|
+
export type MigrationEdit = {
|
|
42
|
+
readonly path: ReadonlyArray<MigrationEditPathSegment>;
|
|
43
|
+
readonly value?: unknown;
|
|
44
|
+
readonly rationale?: string;
|
|
45
|
+
};
|
|
19
46
|
export type Migration = {
|
|
20
47
|
readonly from: number;
|
|
21
48
|
readonly to: number;
|
|
22
49
|
up(spec: SpecObject): SpecObject;
|
|
23
50
|
down(spec: SpecObject): SpecObject;
|
|
51
|
+
/**
|
|
52
|
+
* 0.6.0 §9.2 — the edit list equivalent to `up(spec)`, for callers that
|
|
53
|
+
* hold the YAML text and want comments + key order preserved. Optional: a
|
|
54
|
+
* step without it is applied object-level and re-serialised (the pre-0.6.0
|
|
55
|
+
* comment-flattening path), which `planUp` reports per step.
|
|
56
|
+
*/
|
|
57
|
+
edits?(spec: SpecObject): ReadonlyArray<MigrationEdit>;
|
|
58
|
+
/**
|
|
59
|
+
* 0.6.0 §9.2 — declares the step lossy. The engine refuses to walk DOWN
|
|
60
|
+
* across it (`MigrationIrreversibleError`); `down()` is never called.
|
|
61
|
+
* Reserved for a genuinely lossy future step — `MIGRATION_1_TO_2` is a
|
|
62
|
+
* version stamp plus one explicit default and is NOT marked.
|
|
63
|
+
*/
|
|
64
|
+
readonly irreversible?: true;
|
|
24
65
|
};
|
|
25
66
|
export declare class MigrationError extends CrewhausError {
|
|
26
|
-
readonly name
|
|
67
|
+
readonly name: string;
|
|
27
68
|
constructor(message: string, cause?: unknown);
|
|
28
69
|
}
|
|
70
|
+
/** Thrown by the down-walk when it would have to cross an `irreversible` step. */
|
|
71
|
+
export declare class MigrationIrreversibleError extends MigrationError {
|
|
72
|
+
readonly name = "MigrationIrreversibleError";
|
|
73
|
+
readonly from: number;
|
|
74
|
+
readonly to: number;
|
|
75
|
+
constructor(from: number, to: number);
|
|
76
|
+
}
|
|
77
|
+
/** One step of an up-walk as {@link MigrationEngine.planUp} reports it. */
|
|
78
|
+
export type MigrationStepPlan = {
|
|
79
|
+
readonly from: number;
|
|
80
|
+
readonly to: number;
|
|
81
|
+
/** The step's declared edits — `undefined` when the migration supplies none. */
|
|
82
|
+
readonly edits?: ReadonlyArray<MigrationEdit>;
|
|
83
|
+
/** The spec object AFTER this step's `up()`. */
|
|
84
|
+
readonly spec: SpecObject;
|
|
85
|
+
};
|
|
86
|
+
export type MigrationUpPlan = {
|
|
87
|
+
/** The fully migrated spec object (equal to `steps.at(-1).spec`, or the input when no step ran). */
|
|
88
|
+
readonly spec: SpecObject;
|
|
89
|
+
readonly steps: ReadonlyArray<MigrationStepPlan>;
|
|
90
|
+
/** True when EVERY step supplied `edits` — the whole walk can be applied comment-preserving. */
|
|
91
|
+
readonly editsComplete: boolean;
|
|
92
|
+
};
|
|
29
93
|
export declare class MigrationEngine {
|
|
30
94
|
private readonly migrations;
|
|
31
95
|
constructor();
|
|
32
96
|
register(m: Migration): void;
|
|
33
97
|
/**
|
|
34
98
|
* Walk the registered chain to migrate `spec` from its current version
|
|
35
|
-
* to `toVersion`. Throws MigrationError when a step is missing
|
|
99
|
+
* to `toVersion`. Throws MigrationError when a step is missing, and
|
|
100
|
+
* MigrationIrreversibleError when a DOWN-walk would cross an
|
|
101
|
+
* `irreversible` step (checked before any `down()` runs, so a refused
|
|
102
|
+
* downgrade leaves nothing half-applied).
|
|
36
103
|
*/
|
|
37
104
|
migrate(spec: SpecObject, toVersion: number): SpecObject;
|
|
105
|
+
/**
|
|
106
|
+
* 0.6.0 §9.2 — the up-walk with the edit seam exposed: for each step from
|
|
107
|
+
* the spec's version to `toVersion`, the step's `edits(specBefore)` (when
|
|
108
|
+
* declared) and the object after `up()`. Callers holding YAML text apply
|
|
109
|
+
* `edits` through the CST writer and fall back to re-serialising `spec`
|
|
110
|
+
* only for steps that supply none. Throws MigrationError when a step is
|
|
111
|
+
* missing or `toVersion` is below the spec's version (use `migrate`).
|
|
112
|
+
*/
|
|
113
|
+
planUp(spec: SpecObject, toVersion: number): MigrationUpPlan;
|
|
38
114
|
/** Diagnostic: list registered migration keys. */
|
|
39
115
|
list(): ReadonlyArray<string>;
|
|
40
116
|
/**
|
|
41
117
|
* #43 — the highest version this engine can migrate TO, i.e. the current
|
|
42
118
|
* spec-schema version. It is the max `to` across every registered migration
|
|
43
119
|
* (`0` when none are registered — nothing to upgrade to). `crewhaus upgrade`
|
|
44
|
-
* uses this as the drift target so the CLI never hardcodes
|
|
120
|
+
* uses this as the drift target so the CLI never hardcodes a number.
|
|
45
121
|
*/
|
|
46
122
|
latestVersion(): number;
|
|
47
123
|
/** Reset the registry (tests). */
|
|
48
124
|
clear(): void;
|
|
49
125
|
}
|
|
50
126
|
/**
|
|
51
|
-
* Skeleton migration 0 → 1
|
|
52
|
-
*
|
|
127
|
+
* Skeleton migration 0 → 1 (a version stamp only). Declares its edit list
|
|
128
|
+
* too, so a 0 → 2 walk is comment-preserving end to end.
|
|
53
129
|
*/
|
|
54
130
|
export declare const NOOP_0_TO_1: Migration;
|
|
55
|
-
/**
|
|
131
|
+
/** One `model_pool` block found in a spec object, with the path to it. */
|
|
132
|
+
export type ModelPoolSite = {
|
|
133
|
+
/** Path from the spec root to the `model_pool` object (string keys / array indices). */
|
|
134
|
+
readonly path: ReadonlyArray<MigrationEditPathSegment>;
|
|
135
|
+
readonly pool: Record<string, unknown>;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Every place the spec schema hangs a `model_pool` block: the agent (cli /
|
|
139
|
+
* channel / managed / pipeline / research / batch / browser / voice-less
|
|
140
|
+
* pooled shapes), `agent.sub_agents.<name>`, workflow `steps[i]`, graph
|
|
141
|
+
* `nodes.<name>`, crew `roles.<name>`. Pure structural walk over the parsed
|
|
142
|
+
* object — no schema knowledge beyond the key names, so a spec of any
|
|
143
|
+
* target (or an unparseable-by-zod one) still enumerates.
|
|
144
|
+
*/
|
|
145
|
+
export declare function findModelPools(spec: SpecObject): ReadonlyArray<ModelPoolSite>;
|
|
146
|
+
/**
|
|
147
|
+
* 0.6.0 §9.2 — the first REAL schema migration. Stamps `version: 2`, and
|
|
148
|
+
* adds `reward: { quality_source: none }` explicitly — but ONLY on a
|
|
149
|
+
* `model_pool` with `policy: learned` that does not already say (so the
|
|
150
|
+
* default that now steers a learned reward is visible in the file). Nothing
|
|
151
|
+
* else changes: every other 0.6.0 key is optional and absent-is-byte-identical.
|
|
152
|
+
*
|
|
153
|
+
* `edits()` and `up()` describe the same change; `planUp` consumers apply
|
|
154
|
+
* `edits` through the CST writer so the file's comments and key order
|
|
155
|
+
* survive. `down()` un-stamps the version and leaves the explicit default in
|
|
156
|
+
* place (it is valid at v1 and harmless) — the step is trivially reversible,
|
|
157
|
+
* so it is NOT marked `irreversible`.
|
|
158
|
+
*/
|
|
159
|
+
export declare const MIGRATION_1_TO_2: Migration;
|
|
160
|
+
/** A pre-built engine with the full chain registered (0 → 1 → 2). */
|
|
56
161
|
export declare function createDefaultEngine(): MigrationEngine;
|
package/dist/index.js
CHANGED
|
@@ -4,21 +4,47 @@
|
|
|
4
4
|
* walks the registered chain to bridge any pair of source/target versions
|
|
5
5
|
* (forward when `from < to`, reverse when `from > to`).
|
|
6
6
|
*
|
|
7
|
-
* Spec versions are integer ints
|
|
8
|
-
*
|
|
7
|
+
* Spec versions are integer ints. The first migration registered is the
|
|
8
|
+
* no-op skeleton 0 → 1; 0.6.0 adds the first REAL migration, 1 → 2
|
|
9
|
+
* (`MIGRATION_1_TO_2`, design §9.2).
|
|
9
10
|
*
|
|
10
11
|
* Specs are passed in as parsed YAML (raw object); migration steps are
|
|
11
12
|
* pure transforms `(spec) → spec` returning the next-version shape. The
|
|
12
13
|
* engine is unaware of YAML serialisation — callers use yaml-js or the
|
|
13
14
|
* spec parser to round-trip.
|
|
15
|
+
*
|
|
16
|
+
* 0.6.0 (§9.2) — two additive seams on `Migration`:
|
|
17
|
+
*
|
|
18
|
+
* - `edits?(spec)` declares the SAME change `up()` performs as a list of
|
|
19
|
+
* `SpecEdit`-shaped path/value edits. A caller that owns the YAML TEXT
|
|
20
|
+
* (`crewhaus upgrade`, `migrate-all`) applies those through the CST
|
|
21
|
+
* writer (`applySpecEdits` in `@crewhaus/spec-patch`) so comments and
|
|
22
|
+
* key order survive; `up()` stays the object-level truth for callers
|
|
23
|
+
* that only hold a parsed object. {@link MigrationEngine.planUp} walks
|
|
24
|
+
* the chain and hands both back per step.
|
|
25
|
+
* - `irreversible?: true` marks a lossy step. The DOWN-walk throws
|
|
26
|
+
* {@link MigrationIrreversibleError} across such a step instead of
|
|
27
|
+
* calling its `down()` — the marker alone would be decoration.
|
|
14
28
|
*/
|
|
15
29
|
import { CrewhausError } from "@crewhaus/errors";
|
|
16
30
|
export class MigrationError extends CrewhausError {
|
|
31
|
+
// Widened to `string` so the irreversible subclass can carry its own name.
|
|
17
32
|
name = "MigrationError";
|
|
18
33
|
constructor(message, cause) {
|
|
19
34
|
super("config", message, cause);
|
|
20
35
|
}
|
|
21
36
|
}
|
|
37
|
+
/** Thrown by the down-walk when it would have to cross an `irreversible` step. */
|
|
38
|
+
export class MigrationIrreversibleError extends MigrationError {
|
|
39
|
+
name = "MigrationIrreversibleError";
|
|
40
|
+
from;
|
|
41
|
+
to;
|
|
42
|
+
constructor(from, to) {
|
|
43
|
+
super(`migration ${from} → ${to} is irreversible: a spec at v${to} cannot be walked back to v${from} (the step is lossy — restore the pre-migration file from version control or the registry's previous version instead)`);
|
|
44
|
+
this.from = from;
|
|
45
|
+
this.to = to;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
22
48
|
export class MigrationEngine {
|
|
23
49
|
migrations;
|
|
24
50
|
constructor() {
|
|
@@ -39,37 +65,72 @@ export class MigrationEngine {
|
|
|
39
65
|
}
|
|
40
66
|
/**
|
|
41
67
|
* Walk the registered chain to migrate `spec` from its current version
|
|
42
|
-
* to `toVersion`. Throws MigrationError when a step is missing
|
|
68
|
+
* to `toVersion`. Throws MigrationError when a step is missing, and
|
|
69
|
+
* MigrationIrreversibleError when a DOWN-walk would cross an
|
|
70
|
+
* `irreversible` step (checked before any `down()` runs, so a refused
|
|
71
|
+
* downgrade leaves nothing half-applied).
|
|
43
72
|
*/
|
|
44
73
|
migrate(spec, toVersion) {
|
|
45
74
|
const fromVersion = (spec.version ?? 0) | 0;
|
|
46
75
|
if (fromVersion === toVersion)
|
|
47
76
|
return spec;
|
|
48
|
-
let current = spec;
|
|
49
77
|
if (fromVersion < toVersion) {
|
|
50
|
-
|
|
51
|
-
for (let v = fromVersion; v < toVersion; v++) {
|
|
52
|
-
const key = `${v}→${v + 1}`;
|
|
53
|
-
const step = this.migrations.get(key);
|
|
54
|
-
if (!step) {
|
|
55
|
-
throw new MigrationError(`no migration registered for ${key}`);
|
|
56
|
-
}
|
|
57
|
-
current = step.up(current);
|
|
58
|
-
}
|
|
78
|
+
return this.planUp(spec, toVersion).spec;
|
|
59
79
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
current = step.down(current);
|
|
80
|
+
// Walk down. Resolve every step first so an irreversible or missing step
|
|
81
|
+
// is reported before any down() mutates anything.
|
|
82
|
+
const steps = [];
|
|
83
|
+
for (let v = fromVersion; v > toVersion; v--) {
|
|
84
|
+
const key = `${v - 1}→${v}`;
|
|
85
|
+
const step = this.migrations.get(key);
|
|
86
|
+
if (!step) {
|
|
87
|
+
throw new MigrationError(`no migration registered for ${key} (needed for downgrade)`);
|
|
69
88
|
}
|
|
89
|
+
if (step.irreversible === true) {
|
|
90
|
+
throw new MigrationIrreversibleError(step.from, step.to);
|
|
91
|
+
}
|
|
92
|
+
steps.push(step);
|
|
70
93
|
}
|
|
94
|
+
let current = spec;
|
|
95
|
+
for (const step of steps)
|
|
96
|
+
current = step.down(current);
|
|
71
97
|
return current;
|
|
72
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* 0.6.0 §9.2 — the up-walk with the edit seam exposed: for each step from
|
|
101
|
+
* the spec's version to `toVersion`, the step's `edits(specBefore)` (when
|
|
102
|
+
* declared) and the object after `up()`. Callers holding YAML text apply
|
|
103
|
+
* `edits` through the CST writer and fall back to re-serialising `spec`
|
|
104
|
+
* only for steps that supply none. Throws MigrationError when a step is
|
|
105
|
+
* missing or `toVersion` is below the spec's version (use `migrate`).
|
|
106
|
+
*/
|
|
107
|
+
planUp(spec, toVersion) {
|
|
108
|
+
const fromVersion = (spec.version ?? 0) | 0;
|
|
109
|
+
if (toVersion < fromVersion) {
|
|
110
|
+
throw new MigrationError(`planUp: target v${toVersion} is below the spec's v${fromVersion} — downgrades go through migrate()`);
|
|
111
|
+
}
|
|
112
|
+
const steps = [];
|
|
113
|
+
let current = spec;
|
|
114
|
+
let editsComplete = true;
|
|
115
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
116
|
+
const key = `${v}→${v + 1}`;
|
|
117
|
+
const step = this.migrations.get(key);
|
|
118
|
+
if (!step) {
|
|
119
|
+
throw new MigrationError(`no migration registered for ${key}`);
|
|
120
|
+
}
|
|
121
|
+
const edits = step.edits !== undefined ? step.edits(current) : undefined;
|
|
122
|
+
if (edits === undefined)
|
|
123
|
+
editsComplete = false;
|
|
124
|
+
current = step.up(current);
|
|
125
|
+
steps.push({
|
|
126
|
+
from: step.from,
|
|
127
|
+
to: step.to,
|
|
128
|
+
...(edits !== undefined ? { edits } : {}),
|
|
129
|
+
spec: current,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { spec: current, steps, editsComplete };
|
|
133
|
+
}
|
|
73
134
|
/** Diagnostic: list registered migration keys. */
|
|
74
135
|
list() {
|
|
75
136
|
return [...this.migrations.keys()].sort();
|
|
@@ -78,7 +139,7 @@ export class MigrationEngine {
|
|
|
78
139
|
* #43 — the highest version this engine can migrate TO, i.e. the current
|
|
79
140
|
* spec-schema version. It is the max `to` across every registered migration
|
|
80
141
|
* (`0` when none are registered — nothing to upgrade to). `crewhaus upgrade`
|
|
81
|
-
* uses this as the drift target so the CLI never hardcodes
|
|
142
|
+
* uses this as the drift target so the CLI never hardcodes a number.
|
|
82
143
|
*/
|
|
83
144
|
latestVersion() {
|
|
84
145
|
let latest = 0;
|
|
@@ -94,8 +155,8 @@ export class MigrationEngine {
|
|
|
94
155
|
}
|
|
95
156
|
}
|
|
96
157
|
/**
|
|
97
|
-
* Skeleton migration 0 → 1
|
|
98
|
-
*
|
|
158
|
+
* Skeleton migration 0 → 1 (a version stamp only). Declares its edit list
|
|
159
|
+
* too, so a 0 → 2 walk is comment-preserving end to end.
|
|
99
160
|
*/
|
|
100
161
|
export const NOOP_0_TO_1 = Object.freeze({
|
|
101
162
|
from: 0,
|
|
@@ -106,10 +167,131 @@ export const NOOP_0_TO_1 = Object.freeze({
|
|
|
106
167
|
down(spec) {
|
|
107
168
|
return { ...spec, version: 0 };
|
|
108
169
|
},
|
|
170
|
+
edits() {
|
|
171
|
+
return [{ path: ["version"], value: 1, rationale: "schema version stamp (0 → 1)" }];
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
function isRecord(v) {
|
|
175
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Every place the spec schema hangs a `model_pool` block: the agent (cli /
|
|
179
|
+
* channel / managed / pipeline / research / batch / browser / voice-less
|
|
180
|
+
* pooled shapes), `agent.sub_agents.<name>`, workflow `steps[i]`, graph
|
|
181
|
+
* `nodes.<name>`, crew `roles.<name>`. Pure structural walk over the parsed
|
|
182
|
+
* object — no schema knowledge beyond the key names, so a spec of any
|
|
183
|
+
* target (or an unparseable-by-zod one) still enumerates.
|
|
184
|
+
*/
|
|
185
|
+
export function findModelPools(spec) {
|
|
186
|
+
const out = [];
|
|
187
|
+
const consider = (path, block) => {
|
|
188
|
+
if (!isRecord(block))
|
|
189
|
+
return;
|
|
190
|
+
const pool = block["model_pool"];
|
|
191
|
+
if (isRecord(pool))
|
|
192
|
+
out.push({ path: [...path, "model_pool"], pool });
|
|
193
|
+
};
|
|
194
|
+
const agent = spec["agent"];
|
|
195
|
+
consider(["agent"], agent);
|
|
196
|
+
if (isRecord(agent) && isRecord(agent["sub_agents"])) {
|
|
197
|
+
for (const [name, def] of Object.entries(agent["sub_agents"])) {
|
|
198
|
+
consider(["agent", "sub_agents", name], def);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(spec["steps"])) {
|
|
202
|
+
spec["steps"].forEach((step, i) => consider(["steps", i], step));
|
|
203
|
+
}
|
|
204
|
+
for (const mapKey of ["nodes", "roles"]) {
|
|
205
|
+
const map = spec[mapKey];
|
|
206
|
+
if (!isRecord(map))
|
|
207
|
+
continue;
|
|
208
|
+
for (const [name, block] of Object.entries(map))
|
|
209
|
+
consider([mapKey, name], block);
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
/** A learned pool whose `reward.quality_source` is not yet explicit. */
|
|
214
|
+
function poolNeedsExplicitQualitySource(pool) {
|
|
215
|
+
if (pool["policy"] !== "learned")
|
|
216
|
+
return false;
|
|
217
|
+
const reward = pool["reward"];
|
|
218
|
+
if (reward === undefined || reward === null)
|
|
219
|
+
return true;
|
|
220
|
+
if (!isRecord(reward))
|
|
221
|
+
return false; // malformed — leave it for the validator to reject
|
|
222
|
+
return reward["quality_source"] === undefined;
|
|
223
|
+
}
|
|
224
|
+
/** Immutable deep-set along a string/index path (maps only — pool paths never cross arrays past `steps[i]`). */
|
|
225
|
+
function setAtPath(root, path, value) {
|
|
226
|
+
const step = (node, i) => {
|
|
227
|
+
const seg = path[i];
|
|
228
|
+
if (i === path.length - 1) {
|
|
229
|
+
if (Array.isArray(node)) {
|
|
230
|
+
const copy = [...node];
|
|
231
|
+
copy[Number(seg)] = value;
|
|
232
|
+
return copy;
|
|
233
|
+
}
|
|
234
|
+
return { ...(isRecord(node) ? node : {}), [seg]: value };
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(node)) {
|
|
237
|
+
const copy = [...node];
|
|
238
|
+
copy[Number(seg)] = step(node[Number(seg)], i + 1);
|
|
239
|
+
return copy;
|
|
240
|
+
}
|
|
241
|
+
const rec = isRecord(node) ? node : {};
|
|
242
|
+
return { ...rec, [seg]: step(rec[seg], i + 1) };
|
|
243
|
+
};
|
|
244
|
+
return step(root, 0);
|
|
245
|
+
}
|
|
246
|
+
const QUALITY_SOURCE_RATIONALE = "0.6.0: a learned pool's reward quality source is stated explicitly (runtime default `none`)";
|
|
247
|
+
/**
|
|
248
|
+
* 0.6.0 §9.2 — the first REAL schema migration. Stamps `version: 2`, and
|
|
249
|
+
* adds `reward: { quality_source: none }` explicitly — but ONLY on a
|
|
250
|
+
* `model_pool` with `policy: learned` that does not already say (so the
|
|
251
|
+
* default that now steers a learned reward is visible in the file). Nothing
|
|
252
|
+
* else changes: every other 0.6.0 key is optional and absent-is-byte-identical.
|
|
253
|
+
*
|
|
254
|
+
* `edits()` and `up()` describe the same change; `planUp` consumers apply
|
|
255
|
+
* `edits` through the CST writer so the file's comments and key order
|
|
256
|
+
* survive. `down()` un-stamps the version and leaves the explicit default in
|
|
257
|
+
* place (it is valid at v1 and harmless) — the step is trivially reversible,
|
|
258
|
+
* so it is NOT marked `irreversible`.
|
|
259
|
+
*/
|
|
260
|
+
export const MIGRATION_1_TO_2 = Object.freeze({
|
|
261
|
+
from: 1,
|
|
262
|
+
to: 2,
|
|
263
|
+
up(spec) {
|
|
264
|
+
let out = { ...spec, version: 2 };
|
|
265
|
+
for (const site of findModelPools(spec)) {
|
|
266
|
+
if (!poolNeedsExplicitQualitySource(site.pool))
|
|
267
|
+
continue;
|
|
268
|
+
out = setAtPath(out, [...site.path, "reward", "quality_source"], "none");
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
},
|
|
272
|
+
down(spec) {
|
|
273
|
+
return { ...spec, version: 1 };
|
|
274
|
+
},
|
|
275
|
+
edits(spec) {
|
|
276
|
+
const edits = [
|
|
277
|
+
{ path: ["version"], value: 2, rationale: "schema version stamp (1 → 2)" },
|
|
278
|
+
];
|
|
279
|
+
for (const site of findModelPools(spec)) {
|
|
280
|
+
if (!poolNeedsExplicitQualitySource(site.pool))
|
|
281
|
+
continue;
|
|
282
|
+
edits.push({
|
|
283
|
+
path: [...site.path, "reward", "quality_source"],
|
|
284
|
+
value: "none",
|
|
285
|
+
rationale: QUALITY_SOURCE_RATIONALE,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return edits;
|
|
289
|
+
},
|
|
109
290
|
});
|
|
110
|
-
/** A pre-built engine with the
|
|
291
|
+
/** A pre-built engine with the full chain registered (0 → 1 → 2). */
|
|
111
292
|
export function createDefaultEngine() {
|
|
112
293
|
const e = new MigrationEngine();
|
|
113
294
|
e.register(NOOP_0_TO_1);
|
|
295
|
+
e.register(MIGRATION_1_TO_2);
|
|
114
296
|
return e;
|
|
115
297
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/migration-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "IR version migrations: register up/down chains; migrate(spec, fromVersion, toVersion) walks the chain",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
18
|
+
"@crewhaus/errors": "0.6.0"
|
|
19
19
|
},
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"author": {
|