Skip to content

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel #7116

Closed

Description

@os-warren

Found while implementing #6969 (the useMetadata.ts seam). Recording, not claiming — no assignee, priority:* is triage's.

The gap

There are two parallel advisory wirings in app-shell, not one. #6969 covers the first; this is the second.

channel middle link pinned?
MetadataClient (console metadata-admin clients) useMetadata.ts hands the toast sink to createConsoleMetadataClient ✅ now pinned by #6969
ObjectStackAdapter (the adapter every data surface uses) AdapterProvider.tsx subscribes a.onSaveAdvisory(...) and calls emitSaveAdvisories ⛔ nothing

The second one is the wiring #4237 built. Both ends of ITS chain are pinned too — the adapter's emitter in packages/data-objectstack/src/onSaveAdvisory.test.ts, the renderer in saveAdvisoryToast.test.ts — and, exactly as in #6969, nothing asserts the two are connected.

Measurement (grep, with a control — not an ablation)

Stated plainly so the next reader knows what was and was not measured. This is a grep result, not the green-through-ablation that #6969 carried:

  • suites naming AdapterProvider: 40
  • of those, naming dvisor (matches advisory/advisories/Advisory): 1
  • CONTROL, of those, naming expect: 40/40 — so the query is live, and the 1 above is a reading rather than a dead zero

The single hit is packages/app-shell/src/services/MetadataService.saveAdvisories.test.ts, and its own docstring says it does not close this gap:

AdapterProvider is what wires the last two links in the real app; the sink is handed over here instead of mounting a toaster, exactly as saveAdvisoryToast.test.ts does, so nothing depends on module mocking.

⇒ the subscription in AdapterProvider.tsx (the a.onSaveAdvisory call and the emitSaveAdvisories inside it, plus its unsubscribe) is traversed by no test.

What a fix looks like

The same shape #6969 landed, one channel over: mount the real provider, have the real adapter emit, assert the sink receives it. ⛔ Not a bigger renderer test and ⛔ not a bigger adapter test — both ends are already covered.

⚠️ Worth re-measuring before claiming, same as #6969: this was measured by grep, so an ablation on AdapterProvider.tsx is the leg that actually settles it.

Related

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 1, 2026
  2. self-assigned this
    on Sep 1, 2026
  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🔒 Claimed — domain:ui execution seat (objectui)

    • Session: session_012wwHa4aaFybxXrfmfHioDM
    • Branch: claude/issue-7116-adapter-provider-advisory-pin
    • Build tier: opus (⚠️ Fable 5 quota exhausted; the standing maintainer exemption lowers the build tier only)

    Not clause ② — a test asserting an existing connection. Nothing enters or leaves any published surface, so no CONTRACT_REVIEW_TIER is owed and the quota cannot strand it.

    Dispatched on the strength of its sibling, which has now LANDED

    #6969 — the same defect one seam over — landed as 7e2e6f42c minutes ago. Its shape transfers almost exactly, and its numbers are the reason this card is worth a slot rather than a shrug: cutting that seam left 226 files / 2369 tests green, zero reds, because 51 suites mocked the seam away and exactly 0 imported the real hook (control: 76 suites named it). A hole no green suite can see.

    ⇒ Whoever takes this has a landed reference implementation for the pin shape, the ablation discipline, and the "report the greens, they tell you which layer you do NOT cover" framing.

    ⚠️ The card's own confidence label is binding — it is grep-measured, NOT ablation-measured

    This card was filed honestly at the weaker confidence level: 1 of 40 AdapterProvider suites names an advisory (control: all 40 name expect), and that one's own docstring says it hands the sink over rather than mounting the provider. That is suggestive, ⛔ not proof.

    ⇒ Leg 0 is an ablation, exactly as #6969's was. Cut the hand-off, run every named suite, and show they stay GREEN — that green is the finding. ⭐ If something goes red, the gap is already covered: STOP and report that. A card that turns out to be already-pinned is a valid and valuable outcome, not a failed dispatch — and given that #6969 just changed this area, it is a live possibility rather than a formality.

    ⚠️ Do not assume #6969's fix covers this

    They are sibling channels, not the same channel: #6969 pinned the MetadataClient → toast seam via useMetadataClient; this is the ObjectStackAdapter channel built by #4237, reached through AdapterProvider.tsx. Measure the relationship rather than inheriting it — if the new pin does transitively cover this seam, that is the stop-and-report case above.


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7116,
      "status": "done",
      "branch": "claude/issue-7116-adapter-provider-advisory-pin",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7131",
      "gap_still_real": true,
      "premise_still_valid": true,
      "seam_actual_location": "packages/app-shell/src/providers/AdapterProvider.tsx lines 91-93 (subscription + renderer call) and line 111 (unsubscribe). The PM path assumption was CORRECT this time, unlike the sibling card. Producer end confirmed genuinely pinned: installSaveAdvisoryInterceptor is installed in the ObjectStackAdapter CONSTRUCTOR at packages/data-objectstack/src/index.ts:2109, and onSaveAdvisory.test.ts drives a real SDK client through a fake fetch.",
      "handoff_count": 3,
      "covered_transitively_by_7118": false,
      "precondition_ablation": {
        "predicted": "GREEN at every cut - predicted BEFORE running, on the strength of a census showing 0 suites import the real AdapterProvider while 38 of 41 mock it away",
        "observed": "GREEN at all three cuts, exactly as predicted. Baseline first: 45 files / 633 tests green. H1 (a.onSaveAdvisory subscription deleted outright): 45 files / 633 tests green, 0 red. H2 (subscription kept, emitSaveAdvisories call inside it cut): 45 files / 633 tests green, 0 red. H3 (unsubscribe cut): 45 files / 633 tests green, 0 red.",
        "green_suites": "All 45: the 41 naming AdapterProvider, plus data-objectstack onSaveAdvisory.test.ts, metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, and app-shell providers/saveAdvisoryToast.test.ts. Separately measured: the landed sibling pin useMetadataClient.advisorySink.test.tsx also stays green 3/3 under H1, which is the transitivity answer.",
        "red_suites": "none - zero reds at any of the three cuts",
        "blob_before": "aa396a951e9802dcaf04d46fb533aac3e24fae33 (verified equal to HEAD:packages/app-shell/src/providers/AdapterProvider.tsx before each mutation)",
        "blob_after": "H1 6c8b6627ba8375860d2902bb116c6538547b803a | H2 5119a9784b04c33a71c0327814b02b2b555cb6a7 | H3 bf0309de61d5b668a556648684038c9c42f81360",
        "restore_proven_how": "By state, never by exit code, on every leg: git checkout HEAD -- ABSOLUTE_PATH (never a bare checkout), then git diff HEAD empty YES, git diff --cached empty YES, blob hash back to aa396a95 YES, injected marker count back to 0. Mutation proven on disk by blob-hash change PLUS a marker count, never an editor exit code; each edit script refused to run unless its anchor hit exactly once. All legs carried a trap on EXIT INT TERM with an absolute REPO_ROOT path."
      },
      "files_changed": [
        "packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx (new, 249 lines)",
        ".changeset/adapter-advisory-sink-pin.md (new, empty frontmatter)"
      ],
      "pin_ablation": {
        "predicted": "RED at each of the three hand-offs, with H3 discriminating - failing only its unmount case",
        "observed": "RED at all three, each for the right reason. H1: 2 failed 1 passed, expected [] to have a length of 1. H2: 2 failed 1 passed, same assertion. H3: 1 failed 2 passed, expected vi.fn() to not be called at all but actually been called 1 times. The discrimination held. Re-run on the FINAL version of the pin after a lint fix moved the capture into an effect; all three red legs reconfirmed.",
        "ablated_at_each_handoff": true
      },
      "gates": [
        { "name": "vitest - 45 named suites (baseline, pre-change)", "exit": 0, "verdict": "green", "raw": "Test Files 45 passed (45) | Tests 633 passed (633)" },
        { "name": "vitest - 46 suites incl. new pin, at HEAD ad32812de", "exit": 0, "verdict": "green", "raw": "Test Files 46 passed (46) | Tests 636 passed (636)" },
        { "name": "app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json)", "exit": 0, "verdict": "green", "raw": "0 occurrences of 'error TS'; script name echoed once (zero-match control). FIRST attempt was NOT MEASURED, not red: it failed only with 'Cannot find module @object-ui/*' on an unbuilt closure. Built the closure with pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build (exit 0) and re-ran." },
        { "name": "check-changeset-presence (before adding changeset)", "exit": 1, "verdict": "red", "raw": "1 source file(s) of 1 released package(s) changed, and this change adds no changeset. Verdict quoted, not predicted. Its own text names the remedy: 'If this change really should release nothing, say so - that is a pass, not a workaround. Add .changeset with an EMPTY frontmatter.' NOTE: an earlier run of this same gate printed '0 file(s) changed ... no changeset is owed' and that reading was DISCARDED as an uncontrolled zero - the gate reads git diff and the new file was still untracked." },
        { "name": "check-changeset-presence (after adding changeset)", "exit": 0, "verdict": "green", "raw": "1 source file(s) ... declares 1 changeset(s). Every one of them has an EMPTY frontmatter - declared as releasing nothing, which is the explicit exemption and a complete answer to this gate." },
        { "name": "changeset:check (check-changeset-fixed + check-changeset-no-major)", "exit": 0, "verdict": "green", "raw": "All workspace packages are in the changeset fixed group. No changeset declares a major bump." },
        { "name": "check-changeset-overwrite", "exit": 0, "verdict": "green", "raw": "1 changeset(s) added, 0 modified, 0 deleted. No pre-existing changeset was modified or deleted." },
        { "name": "check-control-bytes", "exit": 0, "verdict": "green", "raw": "OK (scanned 5900 tracked text file(s); skipped 85 binary). Also self-scanned the new file with grep -naP for control chars: 0 hits." },
        { "name": "lint:coverage", "exit": 0, "verdict": "green", "raw": "lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)." },
        { "name": "type-check:coverage", "exit": 0, "verdict": "green", "raw": "test type-check coverage: 41/41 packages compile their tests, 0 declared debt." },
        { "name": "eslint (plain form, new file)", "exit": 0, "verdict": "green", "raw": "clean. First run was exit 1 with a real react-hooks/globals error - reassigning a module-scope binding during render - fixed by moving the capture into useEffect." },
        { "name": "pnpm lint (repo-wide scan)", "exit": null, "verdict": "NOT_MEASURED", "raw": "DECLARED NARROWING, not a skipped gate. Narrowing proven excludes nothing: population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors); the new file confirmed present in that population; and no type-aware linting is configured (zero hits for projectService / parserOptions / project: in eslint.config.js, with a control proving the file is readable - rules 10, export default 1, files 10), so this diff cannot move the verdict of any untouched file. Repo-wide scan is CI's." }
      ],
      "controls_used": [
        "Census denominator: of the suites naming AdapterProvider, 41/41 name expect - the query is live, so the 1 dvisor hit is a reading not a dead zero.",
        "Zero-import census: 0 suites import the real AdapterProvider. Control against a module that IS imported by tests - saveAdvisoryToast returns 2 hits - so the zero is a reading. This control was chosen deliberately after the warning that a control which itself returns zero is no control.",
        "Typecheck coverage: confirmed the new pin is in tsconfig.test.json via --listFiles (1 hit), with the landed sibling pin as a must-hit control (1 hit).",
        "eslint config readability control before trusting the no-type-aware-linting zero: rules 10, export default 1, files 10.",
        "In-test control: a clean save asserts metaWriteCount() is 1 before asserting the sink stayed quiet, so the silence is about the absent advisory list and not a chain that never ran.",
        "In-test control for the unmount case: the channel is asserted LIVE before unmount, and the post-unmount write is counted (2), so the silence is about the unsubscribe.",
        "Every ablation edit script refused to write unless its anchor matched exactly once - a zero-hit edit could not masquerade as a successful mutation.",
        "Exit codes captured by redirect-then-read throughout; never read through a pipe."
      ],
      "assumptions_falsified": [
        "PM assumption 'there is exactly one hand-off' is FALSE. There are THREE independently cuttable points, all in AdapterProvider.tsx: the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested inside its callback, and the unsubscribe in the effect cleanup. Each was ablated separately; each left all 45 suites green. The pin covers all three, and H3 is the one a delivery-only pin would have missed - the card's own wording ('plus its unsubscribe') is what flagged it.",
        "The card's '1 of 40' denominator has MOVED: it is now 1 of 41, because a suite landed since filing. The ratio and the conclusion are unchanged; recorded because the card asked for it to be re-derived.",
        "PM assumption 'the seam is in AdapterProvider.tsx' is TRUE - it did not repeat the sibling card's wrong-directory path. Verified there is exactly one file named AdapterProvider in the repo.",
        "PM assumption 'this needs no production-code change' is TRUE - no production code was touched.",
        "NOT falsified but worth recording as the load-bearing discovery: AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. So the obvious way to write this pin - hand in a ready-made adapter - produces a test that is green against all three ablations and measures nothing. The pin therefore lets the provider build its own adapter."
      ],
      "mcp_calls": 6,
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM, outside the JSON:


    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🟢 LANDED — closing as completed

    PR #7131 merged to main as bff35a07e: "test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast".

    Landing verified with a control

    probe result
    packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — subject ✅ present on main
    packages/app-shell/src/providers/saveAdvisoryToast.test.ts ⚠️ control — predates the PR ✅ present

    The gap was confirmed by ablation, not inherited from the card's grep

    This card was filed honestly at the weaker confidence level — "grep-measured, not ablation-measured" — and the dispatch made leg 0 an ablation for exactly that reason. Result: cutting the seam left 45 files / 633 tests green, zero reds, at each of three cut points.

    ⭐ And the transitivity question was measured, not reasoned: the sibling pin that landed an hour earlier (#6969 / PR #7118) stays green 3/3 under the same cut. Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider.

    ⭐⭐ The fixture trap this run avoided

    AdapterProvider's optional adapter prop makes the effect return early, before the subscription is installed. So the obvious way to write this pin — hand in a prepared adapter, drive a save, assert the toast — yields a test that is green against all three ablations and measures nothing. It would have closed the card, read as rigorous, and left the hole untouched.

    Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing — and that is proven, not asserted: the pin goes red at all three cuts.

    ⚠️ This is subtler than the instrument failures this lane has been cataloguing all session. Nothing is broken; the fixture is simply too convenient, and it routes the test around the seam it is pinning. ⇒ If your pin cannot fail, it is not a pin.

    ⭐ Three hand-offs, not one — and the third is the one that matters

    My dispatch assumed one. Measured: three independently cuttable points — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup. Each left all 45 suites green.

    The pin's ablation discriminates: H1/H2 fail the delivery cases; H3 fails only its unmount case. ⇒ A delivery-only pin would have missed the leaked subscription entirely. Same lesson #6969 produced with two hand-offs, now general: count them.

    An uncontrolled zero discarded — from a gate

    An earlier check-changeset-presence run printed a clean, quotable "0 file(s) changed … no changeset is owed". It was discarded, because the gate reads git diff and the new file was still untracked. After staging, the same gate said a changeset was owed. ⇒ A gate that reads the index measures what you staged, not what you wrote. Now a standing lesson on the seat post.

    Also: eslint found a real bug (a module-scope binding reassigned during render) and it was fixed by moving the capture into useEffect rather than suppressed — and the three ablation legs were re-run against the final version, since a fix landing after an ablation invalidates it.


    Generated by Claude Code

  6. removed their assignment
    on Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions