@zigrivers/scaffold 3.18.0 → 3.19.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/cli/commands/adopt.d.ts.map +1 -1
- package/dist/cli/commands/adopt.js +6 -2
- package/dist/cli/commands/adopt.js.map +1 -1
- package/dist/cli/commands/complete.d.ts.map +1 -1
- package/dist/cli/commands/complete.js +1 -1
- package/dist/cli/commands/complete.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +6 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +2 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +10 -3
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/next.test.js +141 -10
- package/dist/cli/commands/next.test.js.map +1 -1
- package/dist/cli/commands/reset.d.ts.map +1 -1
- package/dist/cli/commands/reset.js +1 -1
- package/dist/cli/commands/reset.js.map +1 -1
- package/dist/cli/commands/rework.d.ts.map +1 -1
- package/dist/cli/commands/rework.js +6 -1
- package/dist/cli/commands/rework.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +6 -3
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +21 -10
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.d.ts.map +1 -1
- package/dist/cli/commands/skip.js +1 -1
- package/dist/cli/commands/skip.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +17 -6
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/status.test.js +76 -7
- package/dist/cli/commands/status.test.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.js +5 -1
- package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
- package/dist/core/pipeline/graph-hash.d.ts +18 -0
- package/dist/core/pipeline/graph-hash.d.ts.map +1 -0
- package/dist/core/pipeline/graph-hash.js +35 -0
- package/dist/core/pipeline/graph-hash.js.map +1 -0
- package/dist/core/pipeline/graph-hash.test.d.ts +2 -0
- package/dist/core/pipeline/graph-hash.test.d.ts.map +1 -0
- package/dist/core/pipeline/graph-hash.test.js +107 -0
- package/dist/core/pipeline/graph-hash.test.js.map +1 -0
- package/dist/core/pipeline/read-eligible.d.ts +20 -0
- package/dist/core/pipeline/read-eligible.d.ts.map +1 -0
- package/dist/core/pipeline/read-eligible.js +28 -0
- package/dist/core/pipeline/read-eligible.js.map +1 -0
- package/dist/core/pipeline/read-eligible.test.d.ts +2 -0
- package/dist/core/pipeline/read-eligible.test.d.ts.map +1 -0
- package/dist/core/pipeline/read-eligible.test.js +99 -0
- package/dist/core/pipeline/read-eligible.test.js.map +1 -0
- package/dist/core/pipeline/resolver.d.ts.map +1 -1
- package/dist/core/pipeline/resolver.js +24 -1
- package/dist/core/pipeline/resolver.js.map +1 -1
- package/dist/core/pipeline/resolver.test.js +86 -0
- package/dist/core/pipeline/resolver.test.js.map +1 -1
- package/dist/core/pipeline/types.d.ts +5 -0
- package/dist/core/pipeline/types.d.ts.map +1 -1
- package/dist/e2e/eligible-cache.test.d.ts +2 -0
- package/dist/e2e/eligible-cache.test.d.ts.map +1 -0
- package/dist/e2e/eligible-cache.test.js +104 -0
- package/dist/e2e/eligible-cache.test.js.map +1 -0
- package/dist/state/root-counter-reader.d.ts +11 -0
- package/dist/state/root-counter-reader.d.ts.map +1 -0
- package/dist/state/root-counter-reader.js +25 -0
- package/dist/state/root-counter-reader.js.map +1 -0
- package/dist/state/root-counter-reader.test.d.ts +2 -0
- package/dist/state/root-counter-reader.test.d.ts.map +1 -0
- package/dist/state/root-counter-reader.test.js +43 -0
- package/dist/state/root-counter-reader.test.js.map +1 -0
- package/dist/state/state-manager.d.ts +23 -2
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +57 -5
- package/dist/state/state-manager.js.map +1 -1
- package/dist/state/state-manager.test.js +246 -1
- package/dist/state/state-manager.test.js.map +1 -1
- package/dist/types/state.d.ts +19 -0
- package/dist/types/state.d.ts.map +1 -1
- package/dist/types/state.test.d.ts +2 -0
- package/dist/types/state.test.d.ts.map +1 -0
- package/dist/types/state.test.js +40 -0
- package/dist/types/state.test.js.map +1 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +3 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eligible-cache.test.js","sourceRoot":"","sources":["../../src/e2e/eligible-cache.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads `.scaffold/state.json` and returns the `save_counter` field.
|
|
3
|
+
* Returns null on any failure (missing file, invalid JSON, missing field).
|
|
4
|
+
*
|
|
5
|
+
* Used by service-scope cache readers (readEligible) to verify that the
|
|
6
|
+
* service's cached next_eligible was written against the current root state
|
|
7
|
+
* (spec §6). Not used at cache WRITE time — StateManager captures the counter
|
|
8
|
+
* internally during loadState to avoid TOCTOU.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readRootSaveCounter(projectRoot: string): number | null;
|
|
11
|
+
//# sourceMappingURL=root-counter-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-counter-reader.d.ts","sourceRoot":"","sources":["../../src/state/root-counter-reader.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUtE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Reads `.scaffold/state.json` and returns the `save_counter` field.
|
|
5
|
+
* Returns null on any failure (missing file, invalid JSON, missing field).
|
|
6
|
+
*
|
|
7
|
+
* Used by service-scope cache readers (readEligible) to verify that the
|
|
8
|
+
* service's cached next_eligible was written against the current root state
|
|
9
|
+
* (spec §6). Not used at cache WRITE time — StateManager captures the counter
|
|
10
|
+
* internally during loadState to avoid TOCTOU.
|
|
11
|
+
*/
|
|
12
|
+
export function readRootSaveCounter(projectRoot) {
|
|
13
|
+
const rootStatePath = path.join(projectRoot, '.scaffold', 'state.json');
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(rootStatePath))
|
|
16
|
+
return null;
|
|
17
|
+
const raw = JSON.parse(fs.readFileSync(rootStatePath, 'utf8'));
|
|
18
|
+
const counter = raw['save_counter'];
|
|
19
|
+
return typeof counter === 'number' ? counter : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=root-counter-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-counter-reader.js","sourceRoot":"","sources":["../../src/state/root-counter-reader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,YAAY,CAAC,CAAA;IACvE,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;YAAE,OAAO,IAAI,CAAA;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAA4B,CAAA;QACzF,MAAM,OAAO,GAAG,GAAG,CAAC,cAAc,CAAC,CAAA;QACnC,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-counter-reader.test.d.ts","sourceRoot":"","sources":["../../src/state/root-counter-reader.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { readRootSaveCounter } from './root-counter-reader.js';
|
|
6
|
+
describe('readRootSaveCounter', () => {
|
|
7
|
+
let tmpRoot;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rcr-'));
|
|
10
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold'), { recursive: true });
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
it('returns null when root state file is missing', () => {
|
|
16
|
+
expect(readRootSaveCounter(tmpRoot)).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
it('returns the counter when state file has a valid save_counter', () => {
|
|
19
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({ save_counter: 42, 'schema-version': 3 }));
|
|
20
|
+
expect(readRootSaveCounter(tmpRoot)).toBe(42);
|
|
21
|
+
});
|
|
22
|
+
it('returns null when state file has invalid JSON', () => {
|
|
23
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), '{ not valid json');
|
|
24
|
+
expect(readRootSaveCounter(tmpRoot)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
it('returns null when state file lacks save_counter (legacy file)', () => {
|
|
27
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({ 'schema-version': 3 }));
|
|
28
|
+
expect(readRootSaveCounter(tmpRoot)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
it('returns null when save_counter is non-number (Codex + Gemini MMR coverage lock)', () => {
|
|
31
|
+
// Coverage lock: regression guard against a future change that forgets
|
|
32
|
+
// the typeof check and coerces with Number() or String().
|
|
33
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({ save_counter: '42', 'schema-version': 3 }));
|
|
34
|
+
expect(readRootSaveCounter(tmpRoot)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it('returns 0 (not null) when save_counter is the number zero', () => {
|
|
37
|
+
// Coverage lock: 0 is a valid counter value (truthy-confusing). A future
|
|
38
|
+
// refactor from `typeof === "number"` to `if (counter)` would regress.
|
|
39
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({ save_counter: 0, 'schema-version': 3 }));
|
|
40
|
+
expect(readRootSaveCounter(tmpRoot)).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=root-counter-reader.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-counter-reader.test.js","sourceRoot":"","sources":["../../src/state/root-counter-reader.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,13 +6,34 @@ export declare class StateManager {
|
|
|
6
6
|
private computeEligible;
|
|
7
7
|
private configProvider?;
|
|
8
8
|
private globalSteps?;
|
|
9
|
+
/**
|
|
10
|
+
* Pipeline-graph hash for the manager's scope. If omitted, saveState writes
|
|
11
|
+
* `next_eligible_hash: undefined`, which consumers treat as invalid cache
|
|
12
|
+
* (live recompute). Legacy-safe default.
|
|
13
|
+
*/
|
|
14
|
+
private pipelineHash?;
|
|
9
15
|
private statePath;
|
|
10
16
|
private pathResolver;
|
|
11
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Captured during service-mode loadState. Used by saveState to stamp
|
|
19
|
+
* next_eligible_root_counter TOCTOU-safely (spec §3). `undefined` = never
|
|
20
|
+
* loaded; `null` = loaded but root file had no save_counter (legacy).
|
|
21
|
+
*/
|
|
22
|
+
private loadedRootCounter;
|
|
23
|
+
constructor(projectRoot: string, computeEligible: (steps: Record<string, StepStateEntry>, options?: {
|
|
24
|
+
scope?: 'global' | 'service';
|
|
25
|
+
globalSteps?: Set<string>;
|
|
26
|
+
}) => string[], configProvider?: (() => {
|
|
12
27
|
project?: {
|
|
13
28
|
services?: unknown[];
|
|
14
29
|
};
|
|
15
|
-
} | undefined) | undefined, pathResolver?: StatePathResolver, globalSteps?: Set<string> | undefined
|
|
30
|
+
} | undefined) | undefined, pathResolver?: StatePathResolver, globalSteps?: Set<string> | undefined,
|
|
31
|
+
/**
|
|
32
|
+
* Pipeline-graph hash for the manager's scope. If omitted, saveState writes
|
|
33
|
+
* `next_eligible_hash: undefined`, which consumers treat as invalid cache
|
|
34
|
+
* (live recompute). Legacy-safe default.
|
|
35
|
+
*/
|
|
36
|
+
pipelineHash?: string | undefined);
|
|
16
37
|
/** Load and validate state.json from disk. Throws ScaffoldError on schema mismatch. */
|
|
17
38
|
loadState(): PipelineState;
|
|
18
39
|
/** Atomically persist state to disk (write tmp + rename). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,gBAAgB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAChH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAKtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAK5D,qBAAa,YAAY;
|
|
1
|
+
{"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,13 +10,27 @@ export class StateManager {
|
|
|
10
10
|
computeEligible;
|
|
11
11
|
configProvider;
|
|
12
12
|
globalSteps;
|
|
13
|
+
pipelineHash;
|
|
13
14
|
statePath;
|
|
14
15
|
pathResolver;
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Captured during service-mode loadState. Used by saveState to stamp
|
|
18
|
+
* next_eligible_root_counter TOCTOU-safely (spec §3). `undefined` = never
|
|
19
|
+
* loaded; `null` = loaded but root file had no save_counter (legacy).
|
|
20
|
+
*/
|
|
21
|
+
loadedRootCounter = undefined;
|
|
22
|
+
constructor(projectRoot, computeEligible, configProvider, pathResolver, globalSteps,
|
|
23
|
+
/**
|
|
24
|
+
* Pipeline-graph hash for the manager's scope. If omitted, saveState writes
|
|
25
|
+
* `next_eligible_hash: undefined`, which consumers treat as invalid cache
|
|
26
|
+
* (live recompute). Legacy-safe default.
|
|
27
|
+
*/
|
|
28
|
+
pipelineHash) {
|
|
16
29
|
this.projectRoot = projectRoot;
|
|
17
30
|
this.computeEligible = computeEligible;
|
|
18
31
|
this.configProvider = configProvider;
|
|
19
32
|
this.globalSteps = globalSteps;
|
|
33
|
+
this.pipelineHash = pipelineHash;
|
|
20
34
|
this.pathResolver = pathResolver ?? new StatePathResolver(projectRoot);
|
|
21
35
|
this.statePath = this.pathResolver.statePath;
|
|
22
36
|
}
|
|
@@ -62,20 +76,58 @@ export class StateManager {
|
|
|
62
76
|
const globalState = globalParsed;
|
|
63
77
|
// Merge: global steps as base, service steps override
|
|
64
78
|
state.steps = { ...globalState.steps, ...state.steps };
|
|
79
|
+
// Capture root's save_counter at the SAME read moment (TOCTOU-safe, spec §3)
|
|
80
|
+
this.loadedRootCounter =
|
|
81
|
+
typeof globalParsed['save_counter'] === 'number'
|
|
82
|
+
? globalParsed['save_counter']
|
|
83
|
+
: null;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Root state file missing — treat as legacy (null counter)
|
|
87
|
+
this.loadedRootCounter = null;
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
return state;
|
|
68
91
|
}
|
|
69
92
|
/** Atomically persist state to disk (write tmp + rename). */
|
|
70
93
|
saveState(state) {
|
|
71
|
-
|
|
94
|
+
const isService = this.pathResolver.isServiceScoped && this.globalSteps;
|
|
95
|
+
const scopeOptions = isService
|
|
96
|
+
? { scope: 'service', globalSteps: this.globalSteps }
|
|
97
|
+
: undefined;
|
|
98
|
+
// FIXES ORIGINAL P0: compute with proper scope so service state only caches
|
|
99
|
+
// service-eligible steps.
|
|
100
|
+
state.next_eligible = this.computeEligible(state.steps, scopeOptions);
|
|
101
|
+
state.next_eligible_hash = this.pipelineHash;
|
|
102
|
+
if (isService) {
|
|
103
|
+
// Cross-file invalidation stamp: capture root counter at load time,
|
|
104
|
+
// reuse at save time (TOCTOU-safe per spec §3). Only stamp when a concrete
|
|
105
|
+
// counter value was captured; under strict `exactOptionalPropertyTypes`,
|
|
106
|
+
// omitting the key (via delete) is safer than assigning `undefined`.
|
|
107
|
+
if (typeof this.loadedRootCounter === 'number') {
|
|
108
|
+
state.next_eligible_root_counter = this.loadedRootCounter;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
delete state.next_eligible_root_counter;
|
|
112
|
+
}
|
|
113
|
+
// Spec §1 exclusivity: service saves must NEVER write save_counter on
|
|
114
|
+
// their own state. Strip any leftover (e.g. from a previously-polluted
|
|
115
|
+
// file) before persisting.
|
|
116
|
+
delete state.save_counter;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Root state: bump the monotonic counter.
|
|
120
|
+
state.save_counter = (state.save_counter ?? 0) + 1;
|
|
121
|
+
// Spec §1 exclusivity: root state doesn't carry next_eligible_root_counter.
|
|
122
|
+
delete state.next_eligible_root_counter;
|
|
123
|
+
}
|
|
124
|
+
// Existing global-step stripping for service-mode persisted steps
|
|
72
125
|
let stateToWrite = state;
|
|
73
|
-
if (
|
|
126
|
+
if (isService) {
|
|
74
127
|
const filteredSteps = {};
|
|
75
128
|
for (const [name, entry] of Object.entries(state.steps)) {
|
|
76
|
-
if (!this.globalSteps.has(name))
|
|
129
|
+
if (!this.globalSteps.has(name))
|
|
77
130
|
filteredSteps[name] = entry;
|
|
78
|
-
}
|
|
79
131
|
}
|
|
80
132
|
stateToWrite = { ...state, steps: filteredSteps };
|
|
81
133
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,MAAM,SAAS,CAAA;AAGxB,MAAM,OAAO,YAAY;
|
|
1
|
+
{"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
5
|
+
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
|
|
6
6
|
import { StateManager } from './state-manager.js';
|
|
7
7
|
import { StatePathResolver } from './state-path-resolver.js';
|
|
8
8
|
const tmpDirs = [];
|
|
@@ -594,5 +594,250 @@ describe('StateManager', () => {
|
|
|
594
594
|
expect(state.steps['testing-strategy']).toBeUndefined();
|
|
595
595
|
});
|
|
596
596
|
});
|
|
597
|
+
describe('StateManager — pipelineHash + loadedRootCounter (Eligible-Cache v2)', () => {
|
|
598
|
+
it('constructor accepts optional pipelineHash parameter', () => {
|
|
599
|
+
const sm = new StateManager('/fake/project', (_steps, _opts) => [], () => undefined, new StatePathResolver('/fake/project'), undefined, 'test-hash-abc');
|
|
600
|
+
expect(sm).toBeDefined();
|
|
601
|
+
});
|
|
602
|
+
it('service-mode loadState captures root save_counter into loadedRootCounter', () => {
|
|
603
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-capture-'));
|
|
604
|
+
try {
|
|
605
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold', 'services', 'api'), { recursive: true });
|
|
606
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({
|
|
607
|
+
'schema-version': 3,
|
|
608
|
+
'scaffold-version': '1.0.0',
|
|
609
|
+
init_methodology: 'deep',
|
|
610
|
+
config_methodology: 'deep',
|
|
611
|
+
'init-mode': 'greenfield',
|
|
612
|
+
created: '2026-04-20T00:00:00.000Z',
|
|
613
|
+
in_progress: null,
|
|
614
|
+
steps: {},
|
|
615
|
+
next_eligible: [],
|
|
616
|
+
'extra-steps': [],
|
|
617
|
+
save_counter: 7,
|
|
618
|
+
}));
|
|
619
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), JSON.stringify({
|
|
620
|
+
'schema-version': 3,
|
|
621
|
+
'scaffold-version': '1.0.0',
|
|
622
|
+
init_methodology: 'deep',
|
|
623
|
+
config_methodology: 'deep',
|
|
624
|
+
'init-mode': 'greenfield',
|
|
625
|
+
created: '2026-04-20T00:00:00.000Z',
|
|
626
|
+
in_progress: null,
|
|
627
|
+
steps: {},
|
|
628
|
+
next_eligible: [],
|
|
629
|
+
'extra-steps': [],
|
|
630
|
+
}));
|
|
631
|
+
const sm = new StateManager(tmpRoot, (_s, _o) => [], () => undefined, new StatePathResolver(tmpRoot, 'api'), new Set(), 'test-hash');
|
|
632
|
+
sm.loadState();
|
|
633
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
634
|
+
expect(sm.loadedRootCounter).toBe(7);
|
|
635
|
+
}
|
|
636
|
+
finally {
|
|
637
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
it('service-mode loadState sets loadedRootCounter to null when root is missing save_counter', () => {
|
|
641
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-capture-null-'));
|
|
642
|
+
try {
|
|
643
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold', 'services', 'api'), { recursive: true });
|
|
644
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({
|
|
645
|
+
'schema-version': 3,
|
|
646
|
+
'scaffold-version': '1.0.0',
|
|
647
|
+
init_methodology: 'deep',
|
|
648
|
+
config_methodology: 'deep',
|
|
649
|
+
'init-mode': 'greenfield',
|
|
650
|
+
created: '2026-04-20T00:00:00.000Z',
|
|
651
|
+
in_progress: null,
|
|
652
|
+
steps: {},
|
|
653
|
+
next_eligible: [],
|
|
654
|
+
'extra-steps': [],
|
|
655
|
+
}));
|
|
656
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), JSON.stringify({
|
|
657
|
+
'schema-version': 3,
|
|
658
|
+
'scaffold-version': '1.0.0',
|
|
659
|
+
init_methodology: 'deep',
|
|
660
|
+
config_methodology: 'deep',
|
|
661
|
+
'init-mode': 'greenfield',
|
|
662
|
+
created: '2026-04-20T00:00:00.000Z',
|
|
663
|
+
in_progress: null,
|
|
664
|
+
steps: {},
|
|
665
|
+
next_eligible: [],
|
|
666
|
+
'extra-steps': [],
|
|
667
|
+
}));
|
|
668
|
+
const sm = new StateManager(tmpRoot, (_s, _o) => [], () => undefined, new StatePathResolver(tmpRoot, 'api'), new Set(), 'test-hash');
|
|
669
|
+
sm.loadState();
|
|
670
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
671
|
+
expect(sm.loadedRootCounter).toBeNull();
|
|
672
|
+
}
|
|
673
|
+
finally {
|
|
674
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
describe('StateManager.saveState — scope-correct eligibility + cache counters', () => {
|
|
680
|
+
let tmpRoot;
|
|
681
|
+
beforeEach(() => {
|
|
682
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-save-'));
|
|
683
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold'), { recursive: true });
|
|
684
|
+
});
|
|
685
|
+
afterEach(() => {
|
|
686
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
687
|
+
});
|
|
688
|
+
it('root-mode saveState bumps save_counter + stamps next_eligible_hash', () => {
|
|
689
|
+
const computeEligible = vi.fn(() => ['a', 'b']);
|
|
690
|
+
const sm = new StateManager(tmpRoot, computeEligible, () => undefined, new StatePathResolver(tmpRoot), undefined, 'pipeline-hash-v1');
|
|
691
|
+
const state = {
|
|
692
|
+
'schema-version': 3,
|
|
693
|
+
'scaffold-version': '1.0.0',
|
|
694
|
+
init_methodology: 'deep',
|
|
695
|
+
config_methodology: 'deep',
|
|
696
|
+
'init-mode': 'greenfield',
|
|
697
|
+
created: '2026-04-20T00:00:00.000Z',
|
|
698
|
+
in_progress: null,
|
|
699
|
+
steps: {},
|
|
700
|
+
next_eligible: [],
|
|
701
|
+
'extra-steps': [],
|
|
702
|
+
};
|
|
703
|
+
sm.saveState(state);
|
|
704
|
+
const written = JSON.parse(fs.readFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), 'utf8'));
|
|
705
|
+
expect(written.save_counter).toBe(1);
|
|
706
|
+
expect(written.next_eligible_hash).toBe('pipeline-hash-v1');
|
|
707
|
+
expect(written.next_eligible).toEqual(['a', 'b']);
|
|
708
|
+
expect(written.next_eligible_root_counter).toBeUndefined(); // root state — no root-counter stamp
|
|
709
|
+
});
|
|
710
|
+
it('root-mode saveState increments save_counter on each save (monotonic)', () => {
|
|
711
|
+
const sm = new StateManager(tmpRoot, () => [], () => undefined, new StatePathResolver(tmpRoot), undefined, 'hash');
|
|
712
|
+
const state = {
|
|
713
|
+
'schema-version': 3,
|
|
714
|
+
'scaffold-version': '1.0.0',
|
|
715
|
+
init_methodology: 'deep',
|
|
716
|
+
config_methodology: 'deep',
|
|
717
|
+
'init-mode': 'greenfield',
|
|
718
|
+
created: '2026-04-20T00:00:00.000Z',
|
|
719
|
+
in_progress: null,
|
|
720
|
+
steps: {},
|
|
721
|
+
next_eligible: [],
|
|
722
|
+
'extra-steps': [],
|
|
723
|
+
};
|
|
724
|
+
sm.saveState(state);
|
|
725
|
+
sm.saveState(state);
|
|
726
|
+
sm.saveState(state);
|
|
727
|
+
const written = JSON.parse(fs.readFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), 'utf8'));
|
|
728
|
+
expect(written.save_counter).toBe(3);
|
|
729
|
+
});
|
|
730
|
+
it('service-mode saveState passes {scope: "service", globalSteps} to computeEligible', () => {
|
|
731
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold', 'services', 'api'), { recursive: true });
|
|
732
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({
|
|
733
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
734
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
735
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
736
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
737
|
+
save_counter: 10,
|
|
738
|
+
}));
|
|
739
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), JSON.stringify({
|
|
740
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
741
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
742
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
743
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
744
|
+
}));
|
|
745
|
+
const globalSteps = new Set(['global-a']);
|
|
746
|
+
const computeEligible = vi.fn((_steps, _opts) => ['svc-b']);
|
|
747
|
+
const sm = new StateManager(tmpRoot, computeEligible, () => undefined, new StatePathResolver(tmpRoot, 'api'), globalSteps, 'svc-hash');
|
|
748
|
+
sm.loadState(); // populate loadedRootCounter
|
|
749
|
+
sm.saveState({
|
|
750
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
751
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
752
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
753
|
+
in_progress: null, steps: { 'svc-a': { status: 'pending', source: 'pipeline', produces: [] } },
|
|
754
|
+
next_eligible: [], 'extra-steps': [],
|
|
755
|
+
});
|
|
756
|
+
expect(computeEligible).toHaveBeenCalled();
|
|
757
|
+
const call = computeEligible.mock.calls[0];
|
|
758
|
+
expect(call[1]).toEqual({ scope: 'service', globalSteps });
|
|
759
|
+
});
|
|
760
|
+
it('service-mode saveState stamps next_eligible_root_counter from loadedRootCounter', () => {
|
|
761
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold', 'services', 'api'), { recursive: true });
|
|
762
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({
|
|
763
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
764
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
765
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
766
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
767
|
+
save_counter: 42,
|
|
768
|
+
}));
|
|
769
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), JSON.stringify({
|
|
770
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
771
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
772
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
773
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
774
|
+
}));
|
|
775
|
+
const sm = new StateManager(tmpRoot, () => [], () => undefined, new StatePathResolver(tmpRoot, 'api'), new Set(), 'svc-hash');
|
|
776
|
+
sm.loadState(); // captures root counter = 42
|
|
777
|
+
sm.saveState({
|
|
778
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
779
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
780
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
781
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
782
|
+
});
|
|
783
|
+
const written = JSON.parse(fs.readFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), 'utf8'));
|
|
784
|
+
expect(written.next_eligible_root_counter).toBe(42);
|
|
785
|
+
expect(written.next_eligible_hash).toBe('svc-hash');
|
|
786
|
+
expect(written.save_counter).toBeUndefined(); // service state — no own counter
|
|
787
|
+
});
|
|
788
|
+
it('omitted pipelineHash produces next_eligible_hash: undefined (legacy-safe)', () => {
|
|
789
|
+
const sm = new StateManager(tmpRoot, () => [], () => undefined, new StatePathResolver(tmpRoot), undefined);
|
|
790
|
+
sm.saveState({
|
|
791
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
792
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
793
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
794
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
795
|
+
});
|
|
796
|
+
const written = JSON.parse(fs.readFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), 'utf8'));
|
|
797
|
+
expect(written.next_eligible_hash).toBeUndefined();
|
|
798
|
+
expect(written.save_counter).toBe(1); // counter still bumps
|
|
799
|
+
});
|
|
800
|
+
it('service-mode saveState deletes save_counter if present on state (spec §1 exclusivity — Codex MMR)', () => {
|
|
801
|
+
fs.mkdirSync(path.join(tmpRoot, '.scaffold', 'services', 'api'), { recursive: true });
|
|
802
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), JSON.stringify({
|
|
803
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
804
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
805
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
806
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
807
|
+
save_counter: 5,
|
|
808
|
+
}));
|
|
809
|
+
fs.writeFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), JSON.stringify({
|
|
810
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
811
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
812
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
813
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
814
|
+
}));
|
|
815
|
+
const sm = new StateManager(tmpRoot, () => [], () => undefined, new StatePathResolver(tmpRoot, 'api'), new Set(), 'svc-hash');
|
|
816
|
+
sm.loadState();
|
|
817
|
+
sm.saveState({
|
|
818
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
819
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
820
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
821
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
822
|
+
// Polluted input — simulates a file written by a bug elsewhere.
|
|
823
|
+
save_counter: 999,
|
|
824
|
+
});
|
|
825
|
+
const written = JSON.parse(fs.readFileSync(path.join(tmpRoot, '.scaffold', 'services', 'api', 'state.json'), 'utf8'));
|
|
826
|
+
expect(written.save_counter).toBeUndefined(); // service must never write this
|
|
827
|
+
});
|
|
828
|
+
// eslint-disable-next-line max-len
|
|
829
|
+
it('root-mode saveState deletes next_eligible_root_counter if present on state (spec §1 exclusivity — Codex MMR)', () => {
|
|
830
|
+
const sm = new StateManager(tmpRoot, () => [], () => undefined, new StatePathResolver(tmpRoot), undefined, 'root-hash');
|
|
831
|
+
sm.saveState({
|
|
832
|
+
'schema-version': 3, 'scaffold-version': '1.0.0',
|
|
833
|
+
init_methodology: 'deep', config_methodology: 'deep',
|
|
834
|
+
'init-mode': 'greenfield', created: '2026-04-20T00:00:00.000Z',
|
|
835
|
+
in_progress: null, steps: {}, next_eligible: [], 'extra-steps': [],
|
|
836
|
+
// Polluted input — simulates a previously-buggy write.
|
|
837
|
+
next_eligible_root_counter: 77,
|
|
838
|
+
});
|
|
839
|
+
const written = JSON.parse(fs.readFileSync(path.join(tmpRoot, '.scaffold', 'state.json'), 'utf8'));
|
|
840
|
+
expect(written.next_eligible_root_counter).toBeUndefined(); // root must never write this
|
|
841
|
+
});
|
|
597
842
|
});
|
|
598
843
|
//# sourceMappingURL=state-manager.test.js.map
|