Context
Sub 2 of 2 under #15031, blocked by #15033 (guard deletion is only safe once every external production initAsync() call is gone — the guards are what make those calls harmless today).
This sub owns the coupled pair the parent's V-B-A identified as inseparable: the ~86 test/ call sites cannot migrate to ready() mechanically, because specs re-initialize singletons via the private reach-in (X._initPromise = null; await X.initAsync() — e.g. AdrIngestor.spec.mjs:68, semi-centralized in test/playwright/unit/ai/services/memory-core/util.mjs:25-190). ready() cannot express "run init again" — its promise is already resolved. So the test tranche NEEDS a re-init story first, and the spec migration IS that story's validation: ~86 consumers exercising the seam on day one.
This is the high-blast half. The parent's own gate applies to its own AC: destroy-and-re-create is not reachable for singletons today (destroy() unregisters from the Instance manager, Base.mjs:526, but the class-namespace slot never frees; setupClass returns the existing namespace on re-entry, Neo.mjs:820-830; singleton modules export the INSTANCE). Whatever shape the seam takes — a core.Base/Neo.mjs surface, or a test-util-level seam composed from existing primitives — the design converges via explicit reviewer sign-off (or /ideation-sandbox if the shapes contest) BEFORE the code lands.
Proposed Solution
- Design-converge the re-init seam (the gate above). Candidate shapes to weigh at convergence, not pre-decided here: a documented Base-level test-reset API; a real destroy → re-create path for singletons (requires freeing the namespace slot); or a test-util seam that owns re-init without touching Base. The seam's contract must state what re-runs (full
initAsync), what resets (isReady, the ready promise), and what it refuses (production callers).
- Migrate the ~86
test/ call sites onto ready() + the seam; delete every _initPromise = null reach-in (the memory-core util.mjs centralization is the staging point — migrate the util once, most specs follow).
- Delete the bespoke
_initPromise guards (GraphService, the lifecycle services): with #15033 landed (no external production calls) and the seam landed (specs re-init cleanly), construct fires initAsync exactly once and the guards are dead weight. ready()/isReady become the single source of truth.
- Extend the lint repo-wide: the #15033 production-tree lint grows to cover
test/ and the ._initPromise reach-in pattern everywhere — the debt cannot regrow in either tree.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| re-init seam |
design convergence (this sub's gate) |
specs re-init singletons without private reach-ins |
current _initPromise = null (until this lands) |
Base/util JSDoc + parent ADR-impact update if a Base surface lands |
seam spec + 86 migrated consumers |
| ~86 test call sites |
seam contract |
ready() + seam; zero _initPromise reach-ins |
— |
— |
repo grep zero |
bespoke _initPromise guards |
Base.mjs #readyPromise lifecycle |
deleted |
a retained guard carries a documented deferred-init rationale |
service JSDoc |
grep zero |
| repo-wide lint |
#15033 lint |
extended to test/ + reach-in pattern |
— |
lint header |
guard red-test |
Acceptance Criteria
Out of Scope
- The production tranche (landed by #15033).
- Rewriting
Neo.create() init sequencing.
Related
Parent #15031 · blocked by #15033 · origin PR #15016 · #12597 (CLOSED — the cross-spec singleton-leak history this seam must not reopen) · test/playwright/unit/ai/services/memory-core/util.mjs (the reset-pattern staging point)
Origin Session ID: ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2
Retrieval Hint: "singleton re-init seam test reset ready migration guard deletion _initPromise lint repo-wide"
Context
Sub 2 of 2 under #15031, blocked by #15033 (guard deletion is only safe once every external production
initAsync()call is gone — the guards are what make those calls harmless today).This sub owns the coupled pair the parent's V-B-A identified as inseparable: the ~86
test/call sites cannot migrate toready()mechanically, because specs re-initialize singletons via the private reach-in (X._initPromise = null; await X.initAsync()— e.g.AdrIngestor.spec.mjs:68, semi-centralized intest/playwright/unit/ai/services/memory-core/util.mjs:25-190).ready()cannot express "run init again" — its promise is already resolved. So the test tranche NEEDS a re-init story first, and the spec migration IS that story's validation: ~86 consumers exercising the seam on day one.This is the high-blast half. The parent's own gate applies to its own AC: destroy-and-re-create is not reachable for singletons today (
destroy()unregisters from the Instance manager,Base.mjs:526, but the class-namespace slot never frees;setupClassreturns the existing namespace on re-entry,Neo.mjs:820-830; singleton modules export the INSTANCE). Whatever shape the seam takes — acore.Base/Neo.mjssurface, or a test-util-level seam composed from existing primitives — the design converges via explicit reviewer sign-off (or/ideation-sandboxif the shapes contest) BEFORE the code lands.Proposed Solution
initAsync), what resets (isReady, the ready promise), and what it refuses (production callers).test/call sites ontoready()+ the seam; delete every_initPromise = nullreach-in (the memory-coreutil.mjscentralization is the staging point — migrate the util once, most specs follow)._initPromiseguards (GraphService, the lifecycle services): with #15033 landed (no external production calls) and the seam landed (specs re-init cleanly),constructfiresinitAsyncexactly once and the guards are dead weight.ready()/isReadybecome the single source of truth.test/and the._initPromisereach-in pattern everywhere — the debt cannot regrow in either tree.Contract Ledger Matrix
_initPromise = null(until this lands)ready()+ seam; zero_initPromisereach-ins_initPromiseguardsBase.mjs#readyPromiselifecycletest/+ reach-in patternAcceptance Criteria
core.Base/Neo.mjs, the parent's Decision-Record-impact line is updated accordingly (a new ADR on the singleton init/ready contract is in scope if warranted).test/externalawait X.initAsync()sites migrated; repo-wide grep for external non-superinitAsync()awaits returns zero._initPromisereach-ins removed repo-wide (read, write, and null-reset forms); grep zero.--workers=1and default parallelism (the seam must not reintroduce the #12597 cross-spec leak class).Out of Scope
Neo.create()init sequencing.Related
Parent #15031 · blocked by #15033 · origin PR #15016 · #12597 (CLOSED — the cross-spec singleton-leak history this seam must not reopen) ·
test/playwright/unit/ai/services/memory-core/util.mjs(the reset-pattern staging point)Origin Session ID: ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2
Retrieval Hint: "singleton re-init seam test reset ready migration guard deletion _initPromise lint repo-wide"