---
number: 12196
title: >-
  feat(github-workflow): one-time non-destructive archive re-bucket migration
  (#12194)
author: neo-opus-ada
state: MERGED
createdAt: '2026-05-30T07:01:38Z'
updatedAt: '2026-05-30T13:06:42Z'
closedAt: '2026-05-30T13:06:38Z'
mergedAt: '2026-05-30T13:06:38Z'
head: feat/rebucket-archive-migration-12194
base: dev
url: 'https://github.com/neomjs/neo/pull/12196'
---
Authored by Opus 4.8 (Claude Code). Session de8830c3-8d1b-47f8-90db-bd236c8b9bd2.

Related: #12194
Related: #12186

The merged bucketing fixes (#12188/#12184) are **forward-only**: sealed-chunk + `oldVersion`-precedence pin the existing ~4,133 v8.1.0 issues, so a normal re-sync won't clear the tickets-view pile-up. This PR delivers the **tool** to clear it — a non-destructive, one-time re-bucket migration:

- `IssueSyncer.migrateArchiveBuckets(metadata, {dryRun})` — recomputes every issue's version from the full release history with `oldVersion` ignored (`#planBuckets(..., {ignoreOldVersion:true})`), relocates mis-bucketed `.md` files via a **two-phase staged move** (no collisions, no delete, no GitHub re-fetch), rewrites the issues `_index.json`, and prunes emptied dirs. Fails loud if no releases load.
- `SyncService.migrateArchiveBuckets({dryRun})` — facade (load → migrate → save), mirroring `refetchIssuesByNumber`.
- `ai/scripts/migrations/rebucketArchive.mjs` — operator-run entry with a `--dry-run` preview.

**Execution is operator-gated + deferred** (large content diff; pair with the concurrency-capped regen + deploy). This PR is the reviewable, tested tool; #12194 closes after the operator runs it and verifies the result ACs.

FAIR-band: over-target — sole active implementer (@neo-gpt rate-limited, @neo-gemini benched) + operator-directed lane (epic #12186; operator chose approach B).

Evidence: L1 (unit: re-bucket moves a sealed-pinned issue to its true release with content preserved + source dir pruned + index updated; dryRun previews without moving; no-releases aborts — 3 new tests, 212 green) → L2 required for the AC that the LIVE archive clears (operator execution). Residual: live run + portal verification [#12194].

## Deltas from ticket (if any)
- **Approach B** (non-destructive) per your call. The recompute hinges on a new `ignoreOldVersion` option on `#planBuckets` — without it, `oldVersion`-precedence re-confirms v8.1.0 (the cached path is valid semver) and the corrected `closedAt→release` is never reached. Default-false keeps all existing call sites unchanged.
- **Scope: issues only** (the dominant pile-up = the tickets-view). Pulls/Discussions, if similarly mis-bucketed, are a sibling follow-up on their own syncers (same pattern).
- **Two-phase staged move** (stage → final) so files swapping chunks during the re-chunk cannot collide mid-move.

## Test Evidence
- `npm run test-unit -- test/playwright/unit/ai/services/github-workflow/` → **212 passed** (3 new, 0 regressions).
- New (`IssueSyncer.spec`): core re-bucket (v8.1.0 → true release; content preserved; source dir pruned; `_index.json` version+chunk updated); dryRun (plan only — file + metadata untouched); no-releases guard (throws).
- `node --check` on all touched files; pre-commit whitespace/shorthand hooks green.

## Post-Merge Validation
- [ ] `node ai/scripts/migrations/rebucketArchive.mjs --dry-run` against the real metadata — review the distribution before executing.
- [ ] Run it for real; confirm `resources/content/archive/issues/` has no v8.1.0 catch-all and no data loss.
- [ ] Regenerate the portal index + run the concurrency-capped SSR regen + deploy; confirm a clean tickets-view.
- [ ] Then close #12194.
