Epic #11372 tracks ADR 0004 Phase 1 for the Universal Ordinal-100 Content Architecture. PR #11381 has now been approved as Lane A: it adds contentPath.mjs, contentBucketDir(), chunkNumberFor(), and the validator primitives that downstream syncer and lookup code should consume.
This ticket is the narrow Lane B follow-up for ADR 0004 Phase 1 items 2, 3, and 5:
Index map: _index.json schema and maintenance in syncers.
LocalFileService rewrite: index-based lookup.
Syncer updates: IssueSyncer, PullRequestSyncer, and DiscussionSyncer consume contentPath.mjs and maintain _index.json.
Merge dependency: implementation should not start until PR #11381 is merged into dev, because this work consumes the helper introduced there.
The Problem
ADR 0004 retires ID-range folder derivation. Under the target ordinal-100 architecture, an item's chunk is based on its zero-based ordinal position inside a collection, not on the GitHub issue, PR, or discussion number. That means ID-keyed lookup cannot infer the path from the ID anymore.
The current implementation still relies on the old derivation model:
LocalFileService.mjs:3 imports chunkPath.
LocalFileService.mjs:78-89 derives active issue paths with chunkPath(normalizedId) and then recursively scans archive paths.
IssueSyncer.mjs:12-13 imports chunkPath and archivePath; IssueSyncer.mjs:370-410 uses chunkPath(issue.number) for active issues and archivePath() for archived issues.
PullRequestSyncer.mjs:11-12 imports chunkPath and archivePath; PullRequestSyncer.mjs:168-199 uses pr-${chunkPath(pr.number)} for active PRs and archivePath() for archived PRs.
DiscussionSyncer.mjs:149-178 still uses flat active discussion paths and archivePath() for archived discussions.
This is expected residual state after Lane A. It becomes wrong-shape as soon as Phase 1 moves past the additive helper and starts emitting the new resources/content/ structure.
The Architectural Reality
ADR 0004 §2.1 defines one target shape for active and archive tiers: every type lives under chunk-N/, including issues, pulls, discussions, release notes, and archive buckets.
ADR 0004 §3.2 defines the replacement for ID-derived lookup: syncers maintain resources/content/_index.json with entries containing at least {type, id, version, chunkNumber, path}. LocalFileService#getIssueById and sibling ID lookups use the index instead of deriving a folder from the numeric ID or recursively scanning the archive tree.
ADR 0004 §3.3 requires the syncers to compute itemCount and zero-based itemIndex for active and archive buckets, call contentPath(), and update the index alongside file writes.
This is a write-path plus lookup-path contract. It is not a clean-slate migration ticket; migration remains ADR 0004 Phase 1 item 10 after the emitters and consumers are ready.
The Fix
Implement the Lane B path/index substrate after PR #11381 lands:
Add or extend a shared index-map primitive for resources/content/_index.json.
Define a stable index schema for active and archived entries across issues, pulls, and discussions.
Update IssueSyncer, PullRequestSyncer, and DiscussionSyncer so they:
compute active and archive ordinal positions,
call contentPath() for active and archive paths,
write files to the ADR 0004 chunk-N/ shape,
update/remove _index.json entries in the same sync pass that writes/removes markdown files.
Update LocalFileService issue and discussion ID lookups to use _index.json as the primary lookup surface.
Remove or quarantine old chunkPath() assumptions from the touched write/read paths.
Add focused unit coverage for index maintenance and lookup behavior.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
resources/content/_index.json
ADR 0004 §3.2
Canonical ID-to-path map for content emitted by GH workflow syncers; entries include type, id, optional version/bucket, chunkNumber, and path
Missing/malformed index should fail visibly for ID lookup; sync should regenerate during full sync
JSDoc on the index helper plus ticket/PR body schema summary
Unit tests for write/update/remove and malformed-index behavior
Syncer write paths
ADR 0004 §3.1 and §3.3
IssueSyncer, PullRequestSyncer, and DiscussionSyncer compute itemIndex/itemCount and call contentPath() for active and archive tiers
No legacy chunkPath() fallback for newly emitted content
Updated method JSDoc where path behavior changes
Unit tests proving active and archive output paths use chunk-N/
LocalFileService ID lookup
ADR 0004 §3.2 and §3.5
Lookup reads _index.json and then opens the indexed file path
If index entry is missing, return structured NOT_FOUND or a clearly documented regeneration hint; do not silently derive legacy paths as the primary path
Unit tests for active issue, archived issue, discussion, missing index entry, and stale path
Acceptance Criteria
resources/content/_index.json schema is implemented or generated by a dedicated helper with Anchor & Echo JSDoc.
IssueSyncer active and archive write paths consume contentPath() and maintain _index.json entries for created, moved, updated, dropped, and archived issues.
PullRequestSyncer active and archive write paths consume contentPath() and maintain _index.json entries for created, moved, updated, and archived PRs.
DiscussionSyncer active and archive write paths consume contentPath() and maintain _index.json entries for created, moved, updated, and archived discussions.
LocalFileService#getIssueById uses _index.json as the primary lookup mechanism and no longer derives active issue paths via chunkPath(issueNumber).
LocalFileService#getDiscussionById uses _index.json as the primary lookup mechanism and no longer relies on flat-path/recursive legacy active lookup as the primary path.
Old chunkPath() assumptions are removed or explicitly quarantined from the touched paths; any remaining references are listed in the PR body with a follow-up rationale.
Tests cover index write/update/remove behavior, active and archive lookup behavior, missing-entry behavior, and stale indexed-path behavior.
The PR body includes a dependency note that PR #11381 was merged before implementation began.
Out of Scope
Release-notes chunking and ReleaseNotesSyncer creation. That is ADR 0004 Phase 1 item 6.
TicketSource, PullRequestSource, DiscussionSource, and IssueIngestor consumer rewires. Those remain Lane C / #11361 territory.
publish.mjs release-cut review. That is ADR 0004 Phase 1 item 7.
Clean-slate migration or deletion of existing resources/content/ data. That is ADR 0004 Phase 1 item 10 and must wait until emitters and consumers are ready.
Phase 2 portal, SEO, and middleware work.
Avoided Traps
Do not reintroduce ID-range math as an optimization. ADR 0004 explicitly retires folder-name ID encoding.
Do not hide missing index data behind recursive scans as the normal path. Recursive traversal is a migration/debug fallback at most; the contract is index-backed lookup.
Do not begin implementation before PR #11381 lands on dev; this ticket consumes the helper shipped by that PR.
Do not bundle clean-slate migration into this PR. The value here is making the new syncer logic correct before any deletion/resync step.
Handoff Retrieval Hint: ADR 0004 Lane B LocalFileService index lookup syncers contentPath _index.json #11372 #11381
tobiu referenced in commit 0e4c016 - "feat(github-workflow/shared): consolidate path primitives into universal contentPath.mjs (#11379) (#11381) on May 15, 2026, 10:19 AM
tobiu referenced in commit bed6713 - "feat(github-workflow): adopt content index map (#11390) (#11403) on May 15, 2026, 11:49 AM
tobiu referenced in commit ecadd27 - "feat(github-workflow): clean-slate purge of resources/content/ to enable ordinal-100 re-sync (#11451) (#11461) on May 16, 2026, 4:51 PM
Context
Epic #11372 tracks ADR 0004 Phase 1 for the Universal Ordinal-100 Content Architecture. PR #11381 has now been approved as Lane A: it adds
contentPath.mjs,contentBucketDir(),chunkNumberFor(), and the validator primitives that downstream syncer and lookup code should consume.This ticket is the narrow Lane B follow-up for ADR 0004 Phase 1 items 2, 3, and 5:
_index.jsonschema and maintenance in syncers.LocalFileServicerewrite: index-based lookup.IssueSyncer,PullRequestSyncer, andDiscussionSyncerconsumecontentPath.mjsand maintain_index.json.Merge dependency: implementation should not start until PR #11381 is merged into
dev, because this work consumes the helper introduced there.The Problem
ADR 0004 retires ID-range folder derivation. Under the target ordinal-100 architecture, an item's chunk is based on its zero-based ordinal position inside a collection, not on the GitHub issue, PR, or discussion number. That means ID-keyed lookup cannot infer the path from the ID anymore.
The current implementation still relies on the old derivation model:
LocalFileService.mjs:3importschunkPath.LocalFileService.mjs:78-89derives active issue paths withchunkPath(normalizedId)and then recursively scans archive paths.IssueSyncer.mjs:12-13importschunkPathandarchivePath;IssueSyncer.mjs:370-410useschunkPath(issue.number)for active issues andarchivePath()for archived issues.PullRequestSyncer.mjs:11-12importschunkPathandarchivePath;PullRequestSyncer.mjs:168-199usespr-${chunkPath(pr.number)}for active PRs andarchivePath()for archived PRs.DiscussionSyncer.mjs:149-178still uses flat active discussion paths andarchivePath()for archived discussions.This is expected residual state after Lane A. It becomes wrong-shape as soon as Phase 1 moves past the additive helper and starts emitting the new
resources/content/structure.The Architectural Reality
ADR 0004 §2.1 defines one target shape for active and archive tiers: every type lives under
chunk-N/, including issues, pulls, discussions, release notes, and archive buckets.ADR 0004 §3.2 defines the replacement for ID-derived lookup: syncers maintain
resources/content/_index.jsonwith entries containing at least{type, id, version, chunkNumber, path}.LocalFileService#getIssueByIdand sibling ID lookups use the index instead of deriving a folder from the numeric ID or recursively scanning the archive tree.ADR 0004 §3.3 requires the syncers to compute
itemCountand zero-baseditemIndexfor active and archive buckets, callcontentPath(), and update the index alongside file writes.This is a write-path plus lookup-path contract. It is not a clean-slate migration ticket; migration remains ADR 0004 Phase 1 item 10 after the emitters and consumers are ready.
The Fix
Implement the Lane B path/index substrate after PR #11381 lands:
resources/content/_index.json.issues,pulls, anddiscussions.IssueSyncer,PullRequestSyncer, andDiscussionSyncerso they:contentPath()for active and archive paths,chunk-N/shape,_index.jsonentries in the same sync pass that writes/removes markdown files.LocalFileServiceissue and discussion ID lookups to use_index.jsonas the primary lookup surface.chunkPath()assumptions from the touched write/read paths.Contract Ledger Matrix
resources/content/_index.jsonIssueSyncer,PullRequestSyncer, andDiscussionSyncercompute itemIndex/itemCount and callcontentPath()for active and archive tierschunkPath()fallback for newly emitted contentchunk-N/LocalFileServiceID lookup_index.jsonand then opens the indexed file pathNOT_FOUNDor a clearly documented regeneration hint; do not silently derive legacy paths as the primary pathAcceptance Criteria
resources/content/_index.jsonschema is implemented or generated by a dedicated helper with Anchor & Echo JSDoc.IssueSynceractive and archive write paths consumecontentPath()and maintain_index.jsonentries for created, moved, updated, dropped, and archived issues.PullRequestSynceractive and archive write paths consumecontentPath()and maintain_index.jsonentries for created, moved, updated, and archived PRs.DiscussionSynceractive and archive write paths consumecontentPath()and maintain_index.jsonentries for created, moved, updated, and archived discussions.LocalFileService#getIssueByIduses_index.jsonas the primary lookup mechanism and no longer derives active issue paths viachunkPath(issueNumber).LocalFileService#getDiscussionByIduses_index.jsonas the primary lookup mechanism and no longer relies on flat-path/recursive legacy active lookup as the primary path.chunkPath()assumptions are removed or explicitly quarantined from the touched paths; any remaining references are listed in the PR body with a follow-up rationale.Out of Scope
ReleaseNotesSyncercreation. That is ADR 0004 Phase 1 item 6.TicketSource,PullRequestSource,DiscussionSource, andIssueIngestorconsumer rewires. Those remain Lane C / #11361 territory.publish.mjsrelease-cut review. That is ADR 0004 Phase 1 item 7.resources/content/data. That is ADR 0004 Phase 1 item 10 and must wait until emitters and consumers are ready.Avoided Traps
dev; this ticket consumes the helper shipped by that PR.Related
learn/agentos/decisions/0004-github-content-architecture.mdHandoff Retrieval Hint:
ADR 0004 Lane B LocalFileService index lookup syncers contentPath _index.json #11372 #11381