@opengsd/gsd-core 1.3.0 → 1.3.1

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.
@@ -175,16 +175,46 @@ function migrationChecksum(migration) {
175
175
  };
176
176
  return `sha256:${sha256Text(JSON.stringify(serializable))}`;
177
177
  }
178
- function assertAppliedMigrationChecksums(applied, migrations) {
178
+ // Rewrite the stored checksum of any already-applied entry whose id drifted, so the
179
+ // drift is reconciled durably and not re-detected on every subsequent run (issue #670).
180
+ // Returns the number of entries actually changed (so callers know whether a write is needed).
181
+ function reconcileDriftedChecksums(appliedEntries, checksumDrift) {
182
+ if (!Array.isArray(checksumDrift) || checksumDrift.length === 0)
183
+ return 0;
184
+ const reconcile = new Map(checksumDrift.map((d) => [d.id, d.currentChecksum]));
185
+ let changed = 0;
186
+ for (let i = 0; i < appliedEntries.length; i++) {
187
+ const existing = appliedEntries[i];
188
+ if (existing && typeof existing.id === 'string' && reconcile.has(existing.id)) {
189
+ const next = reconcile.get(existing.id);
190
+ if (existing.checksum !== next) {
191
+ appliedEntries[i] = { ...existing, checksum: next };
192
+ changed += 1;
193
+ }
194
+ }
195
+ }
196
+ return changed;
197
+ }
198
+ function collectAppliedChecksumDrift(applied, migrations) {
199
+ const drift = [];
179
200
  for (const migration of migrations) {
180
201
  const entry = applied.get(migration.id);
181
202
  if (!entry || !entry.checksum)
182
203
  continue;
183
- const checksum = migrationChecksum(migration);
184
- if (entry.checksum !== checksum) {
185
- throw new Error(`applied migration checksum changed for ${migration.id}; create a new fix-forward migration id`);
204
+ const currentChecksum = migrationChecksum(migration);
205
+ if (entry.checksum !== currentChecksum) {
206
+ // An already-applied migration is never re-run (it is filtered out of `pending`),
207
+ // so a checksum drift here is functionally inert. A prior release may have edited a
208
+ // shipped migration body (see issue #670). Surface it for reconciliation instead of
209
+ // hard-aborting the user's upgrade.
210
+ drift.push({
211
+ id: migration.id,
212
+ storedChecksum: entry.checksum,
213
+ currentChecksum,
214
+ });
186
215
  }
187
216
  }
217
+ return drift;
188
218
  }
189
219
  function migrationMatchesContext(migration, { runtime, scope }) {
190
220
  if (Array.isArray(migration.runtimes) && migration.runtimes.length > 0) {
@@ -387,7 +417,7 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
387
417
  const validatedMigrations = migrations.map((migration) => (0, installer_migration_authoring_cjs_1.validateInstallerMigrationRecord)(migration));
388
418
  const scopedMigrations = validatedMigrations.filter((migration) => migrationMatchesContext(migration, { runtime, scope }));
389
419
  const applied = appliedMigrationEntries(state);
390
- assertAppliedMigrationChecksums(applied, scopedMigrations);
420
+ const checksumDrift = collectAppliedChecksumDrift(applied, scopedMigrations);
391
421
  const pending = scopedMigrations.filter((migration) => !applied.has(migration.id));
392
422
  const actions = [];
393
423
  const blocked = [];
@@ -474,6 +504,7 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
474
504
  pendingMigrations: pending,
475
505
  actions,
476
506
  blocked,
507
+ checksumDrift,
477
508
  };
478
509
  }
479
510
  function uniqueActionMigrationIds(actions) {
@@ -630,6 +661,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
630
661
  const state = readInstallState(configDir);
631
662
  const applied = appliedMigrationIds(state);
632
663
  const nextApplied = [...state.appliedMigrations];
664
+ reconcileDriftedChecksums(nextApplied, plan.checksumDrift);
633
665
  const actionsByMigrationId = new Map();
634
666
  for (const action of plan.actions) {
635
667
  if (action.migrationId && !actionsByMigrationId.has(action.migrationId)) {
@@ -684,30 +716,36 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
684
716
  }
685
717
  }
686
718
  function markPendingMigrationsApplied({ configDir, plan, now = () => new Date().toISOString(), }) {
687
- if (!plan || !Array.isArray(plan.pendingMigrationIds) || plan.pendingMigrationIds.length === 0) {
719
+ if (!plan)
720
+ return [];
721
+ const hasPending = Array.isArray(plan.pendingMigrationIds) && plan.pendingMigrationIds.length > 0;
722
+ const hasDrift = Array.isArray(plan.checksumDrift) && plan.checksumDrift.length > 0;
723
+ if (!hasPending && !hasDrift)
688
724
  return [];
689
- }
690
725
  const appliedAt = now();
691
726
  const state = readInstallState(configDir);
692
727
  const applied = appliedMigrationIds(state);
693
- const checksumsByMigrationId = new Map();
694
- for (const migration of plan.pendingMigrations || []) {
695
- checksumsByMigrationId.set(migration.id, migrationChecksum(migration));
696
- }
697
728
  const nextApplied = [...state.appliedMigrations];
729
+ const reconciledCount = reconcileDriftedChecksums(nextApplied, plan.checksumDrift);
698
730
  const newlyApplied = [];
699
- for (const id of plan.pendingMigrationIds) {
700
- if (applied.has(id))
701
- continue;
702
- nextApplied.push({
703
- id,
704
- appliedAt,
705
- journal: null,
706
- checksum: checksumsByMigrationId.get(id) || null,
707
- });
708
- newlyApplied.push(id);
731
+ if (hasPending) {
732
+ const checksumsByMigrationId = new Map();
733
+ for (const migration of plan.pendingMigrations || []) {
734
+ checksumsByMigrationId.set(migration.id, migrationChecksum(migration));
735
+ }
736
+ for (const id of plan.pendingMigrationIds) {
737
+ if (applied.has(id))
738
+ continue;
739
+ nextApplied.push({
740
+ id,
741
+ appliedAt,
742
+ journal: null,
743
+ checksum: checksumsByMigrationId.get(id) || null,
744
+ });
745
+ newlyApplied.push(id);
746
+ }
709
747
  }
710
- if (newlyApplied.length > 0) {
748
+ if (newlyApplied.length > 0 || reconciledCount > 0) {
711
749
  writeInstallState(configDir, {
712
750
  schemaVersion: 1,
713
751
  appliedMigrations: nextApplied,
@@ -776,6 +814,7 @@ module.exports = {
776
814
  applyInstallerMigrationPlan,
777
815
  classifyArtifact,
778
816
  discoverInstallerMigrations,
817
+ migrationChecksum,
779
818
  planInstallerMigrations,
780
819
  readInstallManifest,
781
820
  readInstallState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "bin": {
6
6
  "gsd-core": "bin/install.js",