@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izkac/forgekit",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/e2e.mjs CHANGED
@@ -60,7 +60,9 @@ if (!sessionId) {
60
60
  }
61
61
 
62
62
  const { dir, session } = loadSession(sessionId);
63
- const file = e2ePath({ session, sessionDir: dir });
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') return liveOrArchived(openspecChanges, change);
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') return liveOrArchived(specsChanges, change);
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) ??
@@ -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
- const file = spinePath({ session, sessionDir: dir });
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 {