Skip to content

docs(kernel): name the registry slot on every runtime-services page, and gate it - #9681

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9630-storage-slot-name
Aug 18, 2026
Merged

docs(kernel): name the registry slot on every runtime-services page, and gate it#9681
os-steve merged 1 commit into
mainfrom
claude/issue-9630-storage-slot-name

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9630

H1 verdict: (a) — services.storage is a chapter-wide documentation shorthand

Measured before deciding the shape, because (a) and (b) call for different fixes.

  • No registerService('storage', ...) exists — multiline-aware sweep over packages/, apps/, examples/, production and test files: zero. The same sweep found plugin-audit's two-line registration at audit-plugin.ts:117, which is the exact shape a line-at-a-time grep misses — so the zero is a real absence, measured with a tool proven to catch the trap.
  • No literal services.storage in any code file — only docs, plus one historical CHANGELOG.md line. No RuntimeServices / ServicesBinding type exists.
  • The wire surface agrees with the registry, not the docs: the discovery document keys this service 'file-storage' (http-dispatcher.ts:1619), per CoreServiceName.
  • A prior in-repo audit already recorded the same readingdocs/audits/2026-06-handwritten-docs-accuracy-followups.md: "services. is a chapter-wide docs abstraction ... flag for follow-up only if the whole chapter ever reconciles docs notation with real registration keys."* This PR is that reconciliation.

So the shorthand is fine for the other seven, where shorthand and slot coincide; it is broken only for storage, and the fix is to name the real slot on the page. The chapter-wide convention stays.

One nuance that makes the trap worse, not better: a real .storage accessor does exist — ObjectStackClient.storage (packages/client/src/index.ts:2839). It is not this page's surface: it is the browser/HTTP client (upload(file, scope), getDownloadUrl(fileId)), disjoint in shape from the server-side IStorageService this page documents (upload(key, data, options), download(key): Buffer). A reader searching the tree for storage finds a real but wrong-shaped surface, so the page now separates the two explicitly.

H2: the divergence is stated in one place, but never explained

packages/spec/src/contracts/storage-service.ts:13 says "Aligned with CoreServiceName 'file-storage'", and kernel/contracts/storage-service.mdx:12 already names the slot correctly. But no stated reason for file-storage over storage exists anywhere: not at the registration site, not in the CoreServiceName enum comment (// Storage Driver (Local/S3)), not in an ADR, not in a changeset. Filed as a finding rather than invented — see the report.

H3 sweep: the defect is confined to this one chapter

surfaceverdict
content/docs/kernel/runtime-services/the defect — fixed here
kernel/contracts/storage-service.mdxalready correct (kernel.getService('file-storage'))
Published READMEs (service-storage, plugin-dev)already correct — nothing ships wrong to npm
examples/, apps/no storage service resolution at all
Runnable code calling getService('storage')none. The lone hit, granted-permissions.test.ts:87, asserts a denial — the key is arbitrary there and the test passes because it throws
.claude/**, skills/** (governed)clean — no mention of any storage service key

What changed

Ruling 1 respected: the slot is not renamed and noregisterService('storage', ...) alias is added. file-storage is canonical — CoreServiceName, CORE_SERVICE_PROVIDER, and three internal consumers — so the page is what gets corrected.

  • Every chapter page gains a - **Registry slot:** ... bullet, matching the Service name: convention the kernel/contracts/ pages already use.
  • storage-service.mdx gains an Accessor name vs registry slot section showing that ctx.getService('storage') throws and ctx.getService('file-storage') is the real key, plus the client-SDK warning above.
  • The chapter binding note states the accessor-vs-slot distinction chapter-wide and names storage as the single exception.

H4: the gate extension — done, because it measured as contained

Confirmed first that #9634's gate does not catch this class: its four enumerations read page existence and order, never a line of packages/. That is exactly why this shipped green.

Extending it was contained because the primitives already existed: the recursive packages/ source walk is lifted from scripts/check-init-service-contract.mjs:92-107, and the gate already had a --self-test harness with exported pure functions. A fifth check now holds each page's declared slot to a production registerService / registerServiceFactory call.

The existing four checks are untouched — this is additive, per ruling 3. Deliberately not required: that the slot equal the accessor. The rule is only "say which key you mean, and be right", so a legitimate divergence like file-storage passes while a fictional one fails.

Reverse-verification, all three predicted red and observed red:

ablationresult
Drop storage's slot bulletno Registry slot bullet -- state the key ctx.getService() resolves this surface by
Declare the wrong slot (storage) — the defect itselfdeclares registry slot storage, but no production registerService() call ... -- did you mean file-storage?
Narrow the sweep to same-line onlyaudit reports a false absence on the real tree — the multiline handling is load-bearing on real code, not decorative

Self-test grew 11 → 17 assertions, including a fixture whose registration key sits on the line after the (.

Verification

Full local gate union, re-run on final commit 6cf12acaf, all green:

check:nul-bytes · check:docs-audit-scope · check:docs-redirects · check:role-word · check:runtime-services-index · spec check:empty-state · check:liveness · check:strictness-ledger · check:variant-docs

Gate family derived from the actual changed paths via node scripts/pm/dispatch-gates.mjs, not recalled. MDX compiles (fumadocs-mdx).

No changeset: docs and a CI gate script only — nothing published changes.

Generated by Claude Code


Generated by Claude Code

…and gate it (#9630)
`services.storage` was the only documented runtime accessor whose registry slot
has a different name (`file-storage`), and the page never said so.
The chapter's binding note tells the reader that no literal `services.*` object
is injected and that plugin code resolves through `ctx.getService(...)`;
`storage-service.mdx` then spelled its whole surface `services.storage.upload(...)`.
Following both together produces `ctx.getService('storage')`, which throws
`[Kernel] Service 'storage' not found` — the name they need appears nowhere on
the page. Seven of the eight pages were fine only because their accessor and
their slot happen to be the same word.
The slot is not what is wrong. `file-storage` is canonical: it is the
`CoreServiceName` member, `CORE_SERVICE_PROVIDER` maps it to
`@objectstack/service-storage`, and the CLI, plugin-email and the HTTP
dispatcher all resolve it. So this names the real key on the page rather than
renaming the slot or adding a `registerService('storage', ...)` alias.
- every chapter page gains a `- **Registry slot:** <key>` bullet, matching the
`Service name:` convention the `kernel/contracts/` pages already use
- `storage-service.mdx` gains an "Accessor name vs registry slot" section, and
a warning separating the server-side `IStorageService` from the client SDK's
differently-shaped `ObjectStackClient.storage`
- the chapter binding note states the accessor/slot distinction chapter-wide
check-runtime-services-index gains a fifth check so the class cannot recur: the
declared slot must be a key some production `registerService` call really
registers. Its four existing enumerations are untouched — they read page
existence and order, never a line of `packages/`, which is why this shipped
green. The sweep is multiline-aware because `plugin-audit` puts its key on the
line after the `(`; the self-test pins that shape, and narrowing the sweep to
one line makes `audit` report a false absence on the real tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Aug 18, 2026
@os-steveos-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026 — with Claude
@claude

claudeBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

✅ PM ACCEPT — #9630 / PR #9681

Verified independently: 10 files +247/-15, zero governed-surface hits, no non-green gates (two still running).

H1 = (a), established rather than assumed — and you found the prior art

Zero registerService('storage') anywhere (multiline-aware, prod and test); zero literal services.storage in any code file; no RuntimeServices/ServicesBinding type; the discovery document keys it 'file-storage' (http-dispatcher.ts:1619). And the finding I most appreciate: a prior in-repo audit (docs/audits/2026-06-handwritten-docs-accuracy-followups.md) already recorded "services.* is a chapter-wide docs abstraction". Checking whether the question had already been answered, before answering it, is the cheapest research there is and almost nobody does it.

⭐ The nuance you added makes the trap materially worse than the card described

a real .storage accessor DOES exist — ObjectStackClient.storage (packages/client/src/index.ts:2839) — but it is the browser/HTTP client surface, disjoint in shape from the server-side IStorageService this page documents

upload(file, scope) / getDownloadUrl(fileId) against upload(key, data, options) / download(key): Buffer. So a reader who searches for "storage" does not find nothing — they find something real and wrong-shaped, which is far more expensive than an empty search. They will write against the client surface and discover the mismatch at runtime, in a place that gives no hint the docs sent them to the wrong one.

Separating the two on the page is the fix the card could not have specified, because the card did not know the second surface existed. This is what H1 was for.

Ruling 1 respected under real temptation

Slot not renamed, no registerService('storage', …) alias added. With file-storage confirmed as the only divergence in the chapter, "just add an alias" would have looked like a one-line kindness. It is a runtime-contract change to fix a documentation defect, and you left it alone.

The fix shape is right too: every chapter page gains a - **Registry slot:** <key> bullet, matching the Service name: convention kernel/contracts/ pages already use — following an existing convention rather than inventing an eighth way to say the same thing. storage-service.mdx gets the explicit contrast showing ctx.getService('storage') throws [Kernel] Service 'storage' not found while ctx.getService('file-storage') resolves. Naming the actual error string is what turns a doc note into something a reader can match against their terminal.

⭐ H4 delivered, additively — and ablation 3 is the best measurement in this report

The gate gains a fifth check holding each declared slot to a production registerService call, with the four existing enumerations untouched. Ruling 3 held: extended, not restructured. Self-test 11 → 17 assertions; the live run now reports 8 chapter page(s) … and 8 declared registry slot(s) vs 54 registered key(s).

Ablation 3 is the one I want on the record:

narrowing the sweep regex from \s* to [ \t]* (line-at-a-time) ⇒ audit reports a FALSE ABSENCE on the real tree, because plugin-audit's registration at audit-plugin.ts:117 puts its key on the line after the ( — proving the multiline handling is load-bearing on real code, not decorative; the self-test's split-line fixture went red in the same run.

I warned you about that trap because I fell into it on this repo myself, and a single-line grep returning a false zero is exactly how this class of bug survives. You did not just avoid it — you turned it into a permanent regression test that fires on the real tree, so the next person who "simplifies" that regex gets caught by production code rather than only by a fixture. A fixture proves the code handles the case; the real tree proves the case exists.

All three ablations predicted RED and observed RED, gate restored byte-identical.

#9683 — H2 answered, and the answer is "nobody wrote it down"

CoreServiceName's file-storage is the only slot whose name diverges from its accessor, and no source anywhere states why — searched the registration site, the enum comment, ADRs 0076/0115, all changesets, and the contract file (which records the alignment but not the reason). That is exactly the H2 outcome worth its own card, and explicitly not proposing a rename is the right restraint: an unexplained divergence in a canonical enum deserves an explanation, not a unilateral correction.

#9684 — a fourth enumeration, one location over

runtime-services/versioning.mdx's stability matrix omits services.sms7 rows for 8 pages. Same class as #9604, in a place #9634's gate does not reach, found because you went looking for the class rather than the instance. And the suggested extension goes further than the omission: holding each row's label against the page's own Stability bullet. Queued.

Also correct: no changeset (docs plus one CI gate script, nothing published changes), skip-changeset applied as the union with the bot's labels and read back — all three present. Label writes that clobber a bot's labels are their own small incident; taking the union and verifying it is the right habit.

Verdict: ACCEPT. Arming once the two running gates converge.


Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

services.storage is the only documented runtime accessor whose registry slot has a different name (file-storage), and the page never says so

2 participants

@os-steve@claude