---
id: 11291
title: Migrate PR archives to versioned archive/pulls paths
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
  - build
assignees:
  - neo-gpt
createdAt: '2026-05-13T07:44:35Z'
updatedAt: '2026-05-13T13:14:20Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11291'
author: neo-gpt
commentsCount: 0
parentIssue: 11187
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-13T13:14:20Z'
---
# Migrate PR archives to versioned archive/pulls paths

## Context

This is the missing AC8 companion ticket for Epic #11187: reshape the existing PR archive corpus from legacy `resources/content/pr-archive/` into the new single-root archive substrate at `resources/content/archive/pulls/vN.M.K/{flat|chunk-N}/`.

The gap surfaced after Gemini created the #11187 fanout (#11284-#11288). The fanout covered active discussions, read-path fallback, CI/build scripts, consumer cleanup, and validation/docs, but did not create a dedicated ticket for Epic #11187 AC8:

> Reshape `pr-archive/XXxx/` to `archive/pulls/v*/{flat|chunk-N}/` via release-mapping.

PR #11282 already shipped the MetadataManager prerequisite that preserves pull-request `mergedAt`, `milestone`, and `archiveVersion`, which are required inputs for deterministic version inference.

## The Problem

The issue archive has version folders, but the PR archive is currently ID-range oriented and lacks release-version grouping. Moving to the single-root archive shape requires mapping existing archived PR files to release versions before moving them under `archive/pulls/`.

Without a dedicated AC8 ticket, the migration can be accidentally folded into broad consumer cleanup or release-script work. That would blur the hardest part of the PR archive migration: deterministic version inference and dry-run evidence for existing historical files.

## The Architectural Reality

Current and target surfaces:

- Current legacy corpus: `resources/content/pr-archive/` with PR ID-range layout.
- Target corpus: `resources/content/archive/pulls/vN.M.K/{flat|chunk-N}/pr-NNNN.md`.
- Existing active PR corpus: `resources/content/pulls/pr-XXxx/pr-NNNN.md` remains active-tier ID-range per Epic #11187.
- Metadata prerequisite: `ai/services/github-workflow/sync/MetadataManager.mjs` now preserves `mergedAt`, `milestone`, and `archiveVersion` for pull metadata via PR #11282.
- Archive path primitive: `ai/services/github-workflow/shared/archivePath.mjs` owns lazy 100-item archive chunking semantics.
- Release/archive consumers include `buildScripts/release/publish.mjs`, `buildScripts/release/analyzeClosedSinceRelease.mjs`, `.github/workflows/data-sync-pipeline.yml`, and downstream docs/indexing scripts handled by sibling tickets.

Duplicate sweep notes:

- #11117 is closed and represents the pre-#11187 PR archive/chunking shape; it targeted `pr-archive/` and is superseded by the single-root `archive/pulls/` substrate.
- #11286 covers CI pipelines and release scripts, not the historical PR archive corpus migration and version inference itself.
- #11287 covers consumer cleanup, not the data migration plan.
- No open ticket found for AC8's PR archive version-mapping migration.

## The Fix

Implement a deterministic PR archive migration plan for existing `pr-archive` files.

The implementation should:

- Infer the target release version for each archived PR using the best available metadata in deterministic order.
- Prefer explicit `archiveVersion` when present.
- Fall back to `milestone`/release metadata when it is structurally reliable.
- Fall back to `mergedAt` against release-cut dates when needed.
- Emit an anomaly report for PRs that cannot be mapped without ambiguity.
- Support a dry-run/report mode before moving files.
- Move files only after the report is reviewable and deterministic.
- Use the Epic #11187 lazy archive chunking contract: flat when a version folder has ≤100 PRs, `chunk-N` when it exceeds 100.

## Contract Ledger Matrix

| Target Surface | Source of Authority | Proposed Behavior | Fallback | Docs | Evidence |
|---|---|---|---|---|---|
| `resources/content/archive/pulls/vN.M.K/` | Epic #11187 AC8 | Existing archived PR files grouped by release version under single archive root | Ambiguous PRs stay reported, not silently guessed | Migration report plus docs reference in #11288 | Dry-run output + fixture tests |
| PR version inference order | This ticket + PR #11282 metadata retention | `archiveVersion` first, then reliable milestone/release metadata, then `mergedAt` release-cut mapping | Emit anomaly for unmappable or conflicting data | Inline JSDoc / migration README if script added | Fixture matrix covering all branches |
| Legacy `pr-archive/` | #11117 superseded by #11187 | Drained or explicitly left as compatibility-only after migration | Read-path fallback from #11285 during transition | Consumer cleanup in #11287 | Post-migration count diff |

## Acceptance Criteria

- [ ] Migration logic maps legacy `resources/content/pr-archive/**/pr-NNNN.md` files into `resources/content/archive/pulls/vN.M.K/{flat|chunk-N}/`.
- [ ] Version inference is deterministic and documented in code or migration output: `archiveVersion` → reliable milestone/release metadata → `mergedAt` against release-cut dates → anomaly report.
- [ ] Dry-run/report mode lists planned moves, inferred version, inference source, and anomalies before writing files.
- [ ] Fixture or targeted test coverage exercises explicit `archiveVersion`, milestone-based inference, `mergedAt` fallback, and unmappable PR anomaly cases.
- [ ] Migration preserves active-tier PR layout under `resources/content/pulls/pr-XXxx/`.
- [ ] Migration uses the Epic #11187 archive chunking contract: flat ≤100 files per version, `chunk-N` for >100.
- [ ] Post-run validation reports legacy count, moved count, anomaly count, and target count.
- [ ] PR body cites the dry-run output and explains any anomalies left for human review.

## Out of Scope

- Active `pulls/` layout changes.
- Issue archive migration; that is Epic #11187 AC7 / sibling lane.
- Discussion archive creation; that is Epic #11187 AC9 / sibling lane.
- Broad consumer cleanup beyond what is needed to execute and verify the PR archive migration; consumer cleanup belongs to #11287.
- Release pipeline rewrites beyond migration support; release scripts belong to #11286.

## Avoided Traps

- **Folding AC8 into consumer cleanup.** Rejected because version inference and data movement require their own evidence trail.
- **Guessing release versions silently.** Rejected; ambiguous PRs must surface as anomalies.
- **Using raw ID range as the target archive shape.** Rejected because Epic #11187 selected versioned archive folders with lazy chunking for archive-tier content.
- **Moving files without dry-run evidence.** Rejected because archive corpus migration is high-blast and generated-sync-noise prone.

## Related

- Epic #11187 AC8
- #11117 — superseded pre-#11187 PR archive/chunking shape
- #11282 / #11281 — MetadataManager preserved PR fields required for inference
- #11285 — read-path fallback during transition
- #11286 — CI and release-script archive substrate updates
- #11287 — consumer cleanup
- #11288 — validation/docs/anomaly hooks
- `resources/content/pr-archive/`
- `resources/content/archive/pulls/`
- `ai/services/github-workflow/shared/archivePath.mjs`

Origin Session ID: d6d89930-f408-42a0-b60e-ec4487a8cc46

Handoff Retrieval Hints:

- `query_raw_memories(query="AC8 pr-archive version mapping archive pulls migration archiveVersion mergedAt milestone")`
- `ask_knowledge_base(query="PR archive migration archive/pulls release mapping archiveVersion mergedAt")`
- GitHub evidence: Epic #11187 AC8 and PR #11282 metadata-retention merge

## Timeline

- 2026-05-13T07:44:36Z @neo-gpt added the `enhancement` label
- 2026-05-13T07:44:36Z @neo-gpt added the `ai` label
- 2026-05-13T07:44:36Z @neo-gpt added the `architecture` label
- 2026-05-13T07:44:37Z @neo-gpt added the `build` label
- 2026-05-13T07:45:04Z @neo-gpt added parent issue #11187
- 2026-05-13T08:19:21Z @neo-gpt cross-referenced by PR #11295
- 2026-05-13T08:26:36Z @neo-opus-ada cross-referenced by PR #11296
- 2026-05-13T10:26:39Z @neo-gpt assigned to @neo-gpt
- 2026-05-13T10:28:29Z @neo-opus-ada cross-referenced by PR #11297
- 2026-05-13T10:36:31Z @neo-opus-ada cross-referenced by PR #11298
- 2026-05-13T11:15:23Z @neo-gpt cross-referenced by PR #11301
- 2026-05-13T13:14:20Z @tobiu referenced in commit `ca422cc` - "feat(github-workflow): migrate PR archives to archive root (#11291) (#11301)

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-13T13:14:20Z @tobiu closed this issue
- 2026-05-14T11:36:30Z @neo-opus-ada cross-referenced by #11187
- 2026-05-14T12:48:00Z @neo-opus-ada cross-referenced by #11360
- 2026-05-14T14:49:14Z @neo-opus-ada cross-referenced by PR #11362
- 2026-05-14T14:58:31Z @neo-opus-ada referenced in commit `4128b6c` - "chore(test): remove orphaned migratePrArchiveAc8.spec — script deleted in prior commit (#11360)

The migrate-pr-archive-ac8.mjs script was a one-shot migration tool whose job
completed when PR #11291 ran. Its --fallback-version v13.0.0 default became
the bug-enabler that #11360 fixes. The script was deleted in the prior commit;
this commit removes the corresponding orphaned unit test that imports the
deleted module.

Mechanical proof of the new migration (migrate-substrate-11360.mjs) is via
--dry-run manifest output (#11360 AC6), reproducible by reviewers without a
spec file. No replacement spec is filed because the new script is also
one-shot; once #11362 merges and the substrate is clean, the new script can
be deleted in a follow-up cleanup.

Refs #11360"
- 2026-05-14T15:29:25Z @neo-gpt cross-referenced by #11364
- 2026-05-14T15:40:53Z @tobiu referenced in commit `559c73d` - "fix(github-workflow): substrate cleanup — legacy archives + 195-PR collision + syncer fallback + pr- prefix (#11360) (#11362)

* fix(github-workflow): remove pre-stage-to-archive fallback in 3 syncers + pr- prefix on active PR paths (#11360)

Per Epic #11187 Phase 6 mental model (Discussion #11359 rev4): active
resources/content/{issues,pulls,discussions}/ holds backlog AND closed-for-next-release;
archive/{type}/vN.M.K/ holds past-release items only, created at release-cut by publish.mjs.

Three syncers (Issue, PR, Discussion) pre-staged closed-post-latest-release items into archive
via `defaultArchiveVersion || 'unversioned'` / `'legacy'` fallbacks. Per @neo-gpt V-B-A this was
the architectural bug behind the substrate corruption — closed-post-v12.1 items were being
bucketed into a not-yet-existing version archive (or the migration-script-hardcoded v13.0.0).

Changes:
- IssueSyncer/PullRequestSyncer/DiscussionSyncer #planArchiveBuckets: skip bucketing when no
  release-version applies (instead of fallback to 'unversioned'/'legacy').
- IssueSyncer/PullRequestSyncer #get*Path closed branch: when no plan, return active path
  (instead of re-deriving the wrong version + archive).
- DiscussionSyncer #getDiscussionPath: when no plan, return active flat path (was: null).
- PullRequestSyncer #getPullRequestPath: concat `pr-` prefix to chunkPath() for active PR chunk
  dirs (`pulls/pr-<NNN>xx/`). Previously produced wrong dir `pulls/<NNN>xx/`. This bug had 6
  empirical instances on dev (pr-11137/11143/11146/11149/11151/11153).

Adds ai/scripts/migrate-substrate-11360.mjs — one-shot manifest generator + applier for the
data-corpus cleanup. Sibling pattern of migrate-pr-archive-ac8.mjs (which this PR deletes in the
subsequent commit). Mechanical proof per AC4-AC6: --dry-run emits JSON manifest with full
collision breakdown (187/4/4 + 25 + 5 + 3,171 ops); --apply executes atomically.

Code-fix commit. Data-corpus cleanup follows in the next commit (#11360 AC7 atomicity).

Refs #11187 #11360 #11359

* chore(content): substrate cleanup — legacy archives + 195-PR collision + active-tier residue + metadata reset (#11360)

Atomic per AC7: file cleanup + .sync-metadata.json reset in same commit so startup sync
cannot preserve bad placement knowledge between commits.

Generated via `node ai/scripts/migrate-substrate-11360.mjs --apply` (committed in prior commit).
Mechanical manifest summary (per #11360 AC4-AC6 — see ticket for breakdown):

- 3,153 legacy issue-archive/ deletions
- 18 legacy pr-archive/ deletions (17 PR markdown + README)
- 187 v13 duplicate deletions (byte-identical to active)
- 4 stale-active replaces (#11123, #11125, #11127, #11137 — v13 newer-merged content wins)
- 4 missing-active moves (#11129, #11130, #11139, #11142 — v13 → active)
- archive/pulls/v13.0.0/ subtree deletion
- 25 flat issues moved to chunked: issues/issue-N.md → issues/<NNN>xx/issue-N.md
- 5 wrong-dir pulls moved: pulls/111xx/pr-N.md → pulls/pr-111xx/pr-N.md (excluded #11137 which
  is also a stale-active replace; the replace op handles its redirect to correct target path)
- .sync-metadata.json deleted (force regen on next sync from GitHub authoritative state)
- ai/scripts/migrate-pr-archive-ac8.mjs deleted (one-shot, harmful --fallback-version v13.0.0
  default was the bug enabler per Discussion #11359)

Cross-family signals on the graduating Discussion #11359 rev4:
- @neo-opus-ada (author): [GRADUATION_PROPOSED]
- @neo-gemini-pro: [GRADUATION_APPROVED] (rev1 + §6.3 pragmatic-extension to rev4)
- @neo-gpt: [GRADUATION_APPROVED] rev4 (after DEFERRED at rev3; V-B-A surfaced this exact
  collision-aware 187/4/4 breakdown which is now mechanically proven by the manifest)

Refs #11187 #11360 #11359

* chore(test): remove orphaned migratePrArchiveAc8.spec — script deleted in prior commit (#11360)

The migrate-pr-archive-ac8.mjs script was a one-shot migration tool whose job
completed when PR #11291 ran. Its --fallback-version v13.0.0 default became
the bug-enabler that #11360 fixes. The script was deleted in the prior commit;
this commit removes the corresponding orphaned unit test that imports the
deleted module.

Mechanical proof of the new migration (migrate-substrate-11360.mjs) is via
--dry-run manifest output (#11360 AC6), reproducible by reviewers without a
spec file. No replacement spec is filed because the new script is also
one-shot; once #11362 merges and the substrate is clean, the new script can
be deleted in a follow-up cleanup.

Refs #11360

* test(github-workflow): supersede #11288 unversioned-target scenario in IssueSyncer spec (#11360)

#11360's syncer fix removes the `'unversioned'` archive fallback (architectural bug
fix per Epic #11187 Phase 6 mental model: closed-post-latest-release items stay in
active, not pre-staged into not-yet-existing version archives).

The pre-existing IssueSyncer.spec.mjs #11288 anomaly-hook test asserted that:
  1. A bucket-shift to `'unversioned'` fires an `[ARCHIVE ANOMALY]` warning
  2. The final path contains `'unversioned'`

Both assertions are now invalid by design — `'unversioned'` is no longer a valid
bucket. The #11288 anomaly-hook contract (warn on closedAt-shift across buckets)
is preserved for shifts between real release buckets (v11 → v12); only the
"shift to 'unversioned'" variant is removed.

Updated test asserts the new correct behavior: closed-post-latest-release items
land in active (`/issues/`, not `/archive/`, not `'unversioned'`).

The architectural-anomaly variant "previously-archived item now has no plan"
(i.e., archive → active shift suggesting metadata-vs-architecture drift) is a
worth-warning new failure mode but deliberately out-of-scope for #11360 — file
follow-up sub-ticket post-merge if substrate-rigor reasoning supports it.

Refs #11288 #11360 #11187

* fix(github-workflow): address @neo-gpt PR #11362 Cycle 1 executable-surface blockers (#11360)

Four narrow fixes for blockers @neo-gpt surfaced in
https://github.com/neomjs/neo/pull/11362#issuecomment-4451929148 (correction at
https://github.com/neomjs/neo/pull/11362#pullrequestreview-4291032804):

1. IssueSyncer.spec.mjs:189 — assert active-tier placement via configured
   `issuesDir` rather than the literal substring `/issues/`. The earlier
   assertion silently broke because the spec rewires `issuesDir` to a tmp dir
   (e.g., `tmp/issue-syncer-test-XXX-YYY/`), which contains no `/issues/`
   substring even when behavior is correct.

2. package.json — drop the stale `ai:migrate-pr-archive-ac8` script entry that
   pointed at the one-shot migration script deleted in commit `7163ea128`.

3. resources/content/pulls/pr-111xx/pr-11127.md — strip trailing whitespace on
   3 blockquote-separator lines (markdown blockquote-paragraph format that
   `git diff --check` flags) and the trailing blank line at EOF.

4. ai/scripts/migrate-substrate-11360.mjs — pipe-safe stdout emit so the
   ~737 KiB dry-run manifest reaches downstream parsers reliably. Replaces
   `console.log(...); process.exit(0)` with
   `process.stdout.write(..., () => process.exit(0))` so the kernel buffer
   drains before exit (default 64 KiB pipe buffer truncated trailing ops
   under `... | head` / `... | jq`).

Locally: `IssueSyncer.spec.mjs` 4/4 pass; `PullRequestSyncer.spec.mjs` 1/1 pass;
`node ai/scripts/migrate-substrate-11360.mjs --dry-run | wc -c` = 737347;
`node ... --dry-run | python3 -c 'import json,sys; json.load(sys.stdin)'`
parses cleanly with `operations.length === 3399` matching the PR body.

Broader Epic #11187 substrate rot (config `defaultArchiveVersion: 'unversioned'`,
`archiveDir: 'resources/content/issue-archive'` post-migration dead ref,
MetadataManager `archiveVersion` propagation, PullRequestSyncer pre-stage
branch) is delegated outside this PR per operator directive — see A2A
`MESSAGE:220c166d-cc96-408d-916f-c26e28d49b09`.

Co-Authored-By: @neo-gpt

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-16T13:35:09Z @neo-opus-ada cross-referenced by PR #11458

