Skip to content

register() claims the announcement follows the write into every writable loader, but no test fixture can observe that half #6548

Closed

Description

@baozhoutao

Found while doing #6043 (PR #6547). Filed separately per PD #10. Observation-class: no user-visible defect today — the gap is in coverage, not behavior.

Fact

register() in packages/metadata/src/metadata-manager.ts documents its announcement ordering as covering two writes:

// Announce last, once the write has landed in the registry and every
// writable loader — a subscriber that re-reads on the event must not
// race ahead of the data it is meant to observe.

register-notifies-watchers.test.ts is the file that pins this contract. After PR #6547 it asserts the registry half properly (synchronously, inside the watcher callback, at broadcast time). The writable-loader half is not asserted anywhere, and cannot be from that file as it stands:

  • the fixture's only loader is MemoryLoader, whose contract declares protocol: 'memory:';
  • register()'s persistence loop is gated on loader.contract.protocol !== 'datasource:' || !loader.contract.capabilities.write and continues otherwise;
  • so register() never saves to any loader in that file, and moving the announcement across the save loop changes nothing observable there.

Measured

Hoisting the notifyWatchers(...) block above the realtime publish and the writable-loader save loop — leaving the registry write in place — keeps the whole file green, both before and after PR #6547:

 Test Files  1 passed (1)
      Tests  15 passed (15)

That hoist violates the ordering the comment declares. Nothing reports it.

Why this is observation-class rather than a defect

A subscriber that re-reads via get() resolves against the in-memory registry, which outranks every loader, so it never observes the loader write — the half that IS pinned is the half consumers actually depend on. The unpinned half would only matter to a consumer reading through a loader directly, bypassing the registry, which nothing does today.

Shape of the fix (not chosen)

Needs a writable datasource: loader fixture whose save() records the store's state, then a synchronous peek at that store inside the watcher callback — the same shape #6043 used for the registry. metadata-manager-unregister-invalidate-order.test.ts already builds a writable datasource: loader backed by a real store and could be the donor.

Alternatively, if the loader half is deliberately NOT part of the guarantee, the comment on register() should be narrowed to say so — declared = enforced, in either direction.

Related

#6043 (where this surfaced), #3112 (origin of the test file), PR #6547.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 8, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 8, 2026
  3. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Findings triage: promoted — finding removed, pm:queue applied (labels changed together with this comment).

    • Premise re-verified on origin/main @ f549a0d: the "Announce last, once the write has landed in the registry and every writable loader" comment sits at packages/metadata/src/metadata-manager.ts:741; the pin file register-notifies-watchers.test.ts and the named donor fixture metadata-manager-unregister-invalidate-order.test.ts both exist in packages/metadata/src/.
    • Grading rationale: this is a test-only pin restoring a declared invariant — the method documents an ordering across two writes and only one is observable; the hoist experiment in the body shows the whole file stays green when the declared ordering is violated. Scoped, mechanical, donor fixture already named ⇒ auto-queue class rather than held observation.
    • Direction note for the dev: default to pinning the loader half with the donor-fixture shape. Narrowing the comment instead (the body's alternative) is acceptable only with an explicit statement that the loader half is deliberately outside the guarantee — declared = enforced in either direction, as the body itself puts it.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. self-assigned this
    on Aug 8, 2026
  5. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    Claim: PM loop round 3 (domain:metadata seat, sticker #6367)
    Session: session_01KDU3qAuJyajAQm3GkUXdfA
    Branch: claude/issue-6548-register-loader-half-pin
    Worktree: objectstack-issue-6548
    Domain: domain:metadata
    File surface: packages/metadata/src/register-notifies-watchers.test.ts (writable datasource: fixture + synchronous store peek, donor shape from metadata-manager-unregister-invalidate-order.test.ts — donor read-only) + the register() ordering comment in packages/metadata/src/metadata-manager.ts ONLY if the narrowing route is taken per the triage direction note. Stop on breach; explain in the report.
    Serial constraints cleared: #6547 (#6043, same test file) and #6576 (#5309, same package) both MERGED; nothing in flight touches packages/metadata (#6190 is in metadata-protocol, #3682 in rest/runtime/plugin-security). Sequencing note recorded on the seat board: #6563 (same file as in-flight #6190's declared surface — protocol.ts) is deliberately held for the next slot instead of riding with this card.
    Container class: S — mode:subagent, shared container.


    Generated by Claude Code

  6. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    ACCEPT (PM review, domain:metadata seat, session session_01KDU3qAuJyajAQm3GkUXdfA) — PR #6607, now ready + queued.

    Shipped: the writable-loader half of register()'s announcement ordering is now pinned — a real datasource: StoreLoader fixture (donor shape, donor untouched) peeked synchronously inside the watcher callback, five cases including "every loader, not merely the first" and "an in-flight save holds the announcement back" (gate-controlled, no microtask counting). The triage direction's default route was taken: nothing narrowed, the comment on register() is now enforced as written.

    Review verified against GitHub: exactly one file (+267/−6), no source change; reverse verification predicted-then-measured 4-red/18-green under the precise hoist the issue proved invisible; type-check debt measured and paid to zero (the .js import makes implements MetadataLoader a real check); ESLint and TypeScript Type Check both completed: success, 27 checks zero failures; skip-changeset applied and read back (tests-only).

    The issue closes on merge via the Fixes line.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions