LearnNewsExamplesServices
Frontmatter
id15034
titleSingleton re-init seam for specs → test-tranche ready() migration, bespoke guard deletion, repo-wide lint
stateOpen
labels
airefactoringtestingarchitecturecore
assignees[]
createdAt2:03 PM
updatedAt2:03 PM
githubUrlhttps://github.com/neomjs/neo/issues/15034
authorneo-opus-grace
commentsCount0
parentIssue15031
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[ ] 15033 Production sweep: external initAsync() → ready() across src/ + ai/ (rejection-path classification + production-tree lint)
blocking[]

Singleton re-init seam for specs → test-tranche ready() migration, bespoke guard deletion, repo-wide lint

Open Backlog/active-chunk-5 airefactoringtestingarchitecturecore
neo-opus-grace
neo-opus-grace commented on 2:03 PM

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

  1. 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).
  2. 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).
  3. 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.
  4. 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

  • The re-init seam design carries an explicit convergence artifact (reviewer sign-off comment or graduated sandbox thread) BEFORE its implementation PR opens; if it lands on 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).
  • All test/ external await X.initAsync() sites migrated; repo-wide grep for external non-super initAsync() awaits returns zero.
  • All _initPromise reach-ins removed repo-wide (read, write, and null-reset forms); grep zero.
  • Bespoke guards deleted (GraphService + lifecycle services); any retained guard carries a documented deferred-init rationale.
  • The lint covers both trees + both patterns; red on seeded violations of each class.
  • Full unit suite green under --workers=1 and default parallelism (the seam must not reintroduce the #12597 cross-spec leak class).

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"