@izkac/forgekit 0.3.2 → 0.3.3
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/package.json +1 -1
- package/src/e2e.mjs +3 -1
- package/src/integrity.mjs +11 -2
- package/src/integrity.test.mjs +24 -0
- package/src/spine.mjs +3 -1
package/package.json
CHANGED
package/src/e2e.mjs
CHANGED
|
@@ -60,7 +60,9 @@ if (!sessionId) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const { dir, session } = loadSession(sessionId);
|
|
63
|
-
|
|
63
|
+
// init writes: target the live change dir only (never fall back into the
|
|
64
|
+
// archive). run/check/status read: allow the archive fallback.
|
|
65
|
+
const file = e2ePath({ session, sessionDir: dir, forWrite: sub === 'init' });
|
|
64
66
|
|
|
65
67
|
if (sub === 'init') {
|
|
66
68
|
try {
|
package/src/integrity.mjs
CHANGED
|
@@ -98,12 +98,18 @@ function liveOrArchived(changesDir, change) {
|
|
|
98
98
|
export function resolveChangeDir(opts = {}) {
|
|
99
99
|
const cwd = opts.cwd ?? process.cwd();
|
|
100
100
|
const session = opts.session ?? null;
|
|
101
|
+
// Write callers (spine/e2e init) must always target the LIVE change dir —
|
|
102
|
+
// scaffolding into an archived record would corrupt frozen history. Only
|
|
103
|
+
// read callers (checks, gates) fall back to the archive.
|
|
104
|
+
const forWrite = opts.forWrite === true;
|
|
101
105
|
const change = session && isNonEmptyString(session.openspecChange) ? session.openspecChange : null;
|
|
102
106
|
if (!change) return null;
|
|
103
107
|
|
|
104
108
|
const openspecChanges = path.join(cwd, 'openspec', 'changes');
|
|
105
109
|
const openspecDir = path.join(openspecChanges, change);
|
|
106
|
-
if (session.planType === 'openspec')
|
|
110
|
+
if (session.planType === 'openspec') {
|
|
111
|
+
return forWrite ? openspecDir : liveOrArchived(openspecChanges, change);
|
|
112
|
+
}
|
|
107
113
|
|
|
108
114
|
let specsRoot = DEFAULT_SPECS_DIR;
|
|
109
115
|
try {
|
|
@@ -113,11 +119,14 @@ export function resolveChangeDir(opts = {}) {
|
|
|
113
119
|
}
|
|
114
120
|
const specsChanges = path.join(cwd, specsRoot, 'changes');
|
|
115
121
|
const specsDir = path.join(specsChanges, change);
|
|
116
|
-
if (session.planType === 'specs')
|
|
122
|
+
if (session.planType === 'specs') {
|
|
123
|
+
return forWrite ? specsDir : liveOrArchived(specsChanges, change);
|
|
124
|
+
}
|
|
117
125
|
|
|
118
126
|
// planType unknown — prefer whichever exists (live first, then archived)
|
|
119
127
|
if (fs.existsSync(openspecDir)) return openspecDir;
|
|
120
128
|
if (fs.existsSync(specsDir)) return specsDir;
|
|
129
|
+
if (forWrite) return openspecDir;
|
|
121
130
|
return (
|
|
122
131
|
findArchivedChangeDir(openspecChanges, change) ??
|
|
123
132
|
findArchivedChangeDir(specsChanges, change) ??
|
package/src/integrity.test.mjs
CHANGED
|
@@ -136,12 +136,36 @@ test('resolveChangeDir: falls back to the archived copy after archive', () => {
|
|
|
136
136
|
recursive: true,
|
|
137
137
|
});
|
|
138
138
|
assert.equal(resolveChangeDir({ cwd, session }), archived);
|
|
139
|
+
|
|
140
|
+
// forWrite (spine/e2e init) never follows into the archive — writes must
|
|
141
|
+
// target the live change dir so scaffolding can't corrupt frozen history.
|
|
142
|
+
assert.equal(resolveChangeDir({ cwd, session, forWrite: true }), liveDir);
|
|
139
143
|
} finally {
|
|
140
144
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
});
|
|
144
148
|
|
|
149
|
+
test('spinePath/e2ePath forWrite target the live dir even after archive', () => {
|
|
150
|
+
const cwd = tmp('forge-write-guard-');
|
|
151
|
+
try {
|
|
152
|
+
const session = { planType: 'openspec', openspecChange: 'add-customer-registry' };
|
|
153
|
+
const changesDir = path.join(cwd, 'openspec', 'changes');
|
|
154
|
+
// Only the archived copy exists — mimics running init after archive.
|
|
155
|
+
const archived = path.join(changesDir, 'archive', '2026-07-20-add-customer-registry');
|
|
156
|
+
fs.mkdirSync(archived, { recursive: true });
|
|
157
|
+
const liveDir = path.join(changesDir, 'add-customer-registry');
|
|
158
|
+
|
|
159
|
+
// Read path resolves the archive; write path stays on the (absent) live dir.
|
|
160
|
+
assert.equal(spinePath({ cwd, session }), path.join(archived, 'spine.json'));
|
|
161
|
+
assert.equal(spinePath({ cwd, session, forWrite: true }), path.join(liveDir, 'spine.json'));
|
|
162
|
+
assert.equal(e2ePath({ cwd, session }), path.join(archived, 'e2e.json'));
|
|
163
|
+
assert.equal(e2ePath({ cwd, session, forWrite: true }), path.join(liveDir, 'e2e.json'));
|
|
164
|
+
} finally {
|
|
165
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
145
169
|
test('runIntegrityChecks: passes after archive (spine resolves in archive dir)', () => {
|
|
146
170
|
const cwd = tmp('forge-int-archived-');
|
|
147
171
|
try {
|
package/src/spine.mjs
CHANGED
|
@@ -48,7 +48,9 @@ if (!sessionId) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const { dir, session } = loadSession(sessionId);
|
|
51
|
-
|
|
51
|
+
// init writes: target the live change dir only (never fall back into the
|
|
52
|
+
// archive). check/status read: allow the archive fallback.
|
|
53
|
+
const file = spinePath({ session, sessionDir: dir, forWrite: sub === 'init' });
|
|
52
54
|
|
|
53
55
|
if (sub === 'init') {
|
|
54
56
|
try {
|