Skip to content

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

Description

@os-steve

Found during the #9604 sweep (H3: check every ctx.registerService name against the runtime-services chapter). Reported rather than ridden along — #9604's dispatch scoped the diff to the services.sms omission, and this one carries a judgment call.

What was measured

Sweeping every production registerService call on main at e8dba8a8a (test files, fixtures and mocks excluded) and matching it against the eight *-service.mdx pages in content/docs/kernel/runtime-services/:

documented accessor registered slot site
services.data data packages/objectql/src/plugin.ts:312
services.sharing sharing packages/plugins/plugin-sharing/src/sharing-plugin.ts:494
services.audit audit packages/plugins/plugin-audit/src/audit-plugin.ts:117
services.queue queue packages/services/service-queue/src/queue-service-plugin.ts:82
services.email email packages/plugins/plugin-email/src/email-plugin.ts:370
services.sms sms packages/services/service-sms/src/sms-plugin.ts:181
services.settings settings packages/services/service-settings/src/settings-service-plugin.ts:130
services.storage file-storage packages/services/service-storage/src/storage-service-plugin.ts:237

Seven of eight map one-to-one. storage is the exception: there is no registerService('storage', ...) anywhere in the tree (checked multiline-aware). The slot is file-storage, and that spelling is canonical rather than incidental — packages/spec/src/system/core-services.zod.ts:27 lists 'file-storage' in the CoreServiceName enum, and CORE_SERVICE_PROVIDER maps it to @objectstack/service-storage. Every internal consumer uses it: packages/cli/src/commands/migrate/files-to-references.ts:197, packages/plugins/plugin-email/src/email-plugin.ts:223, packages/runtime/src/http-dispatcher.ts:1135.

Why it may be a defect rather than a legitimate difference

The chapter's own binding note (runtime-services/index.mdx:8-15) says a literal services.* object is not injected by the open framework, and that plugin code resolves services through ctx.getService(...). It then sends the reader to storage-service.mdx, whose entire surface is spelled services.storage.upload(...), services.storage.download(...), and so on.

A reader who follows both instructions together writes ctx.getService('storage') and gets nothing back. The name they need — file-storage — appears nowhere on the page. That trap exists only for this one accessor, which is what makes it worth a card: it is not a general property of the services.* naming, it is a single unannounced exception.

The judgment (why this is not in #9604's PR)

Two readings, and they call for different fixes:

  • Legitimate. The managed runtime's services.* binding is its own namespace and need not equal the registry slot. Then the docs are correct and the fix is a one-line note on storage-service.mdx (and possibly the chapter's binding note) naming file-storage as the slot for ctx.getService.
  • Drift. The accessor was meant to match the slot. Then either the page should document services.fileStorage / the slot name, or the slot is misnamed — the second being a breaking change to a published CoreServiceName member, so almost certainly not the route.

The first reading looks much more likely, which makes this a small docs fix, but "which namespace is authoritative for services.*" is a spec question rather than a copy-editing one, so it is filed rather than guessed.

Suggested resolution

Add the slot name to storage-service.mdx next to the canonical source, and consider extending the chapter's binding note to say that the services.* accessor name and the registry slot name are separate namespaces which coincide for every accessor except this one. Cheap, and it removes the only unannounced exception in the table above.

Refs: #9604

Activity

  1. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor

    Claiming this card.

    • session: session_01XqDQYVU5smx29ts9pAErja
    • branch: claude/issue-9630-storage-slot-name (pushed, empty)
    • worktree: objectstack-9630

    Starting with the H1 measurement (what services.storage actually denotes) before any edit, since it decides the shape of the fix.


    Generated by Claude Code

  2. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 9630,
      "status": "done",
      "branch": "claude/issue-9630-storage-slot-name",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9681",
      "premise_still_valid": true,
      "summary": "H1 VERDICT: (a) — services.storage is a chapter-wide documentation shorthand, not a real accessor. Evidence: zero registerService('storage') anywhere (multiline-aware, prod AND test); zero literal `services.storage` in any code file (docs + one historical CHANGELOG line only); no RuntimeServices/ServicesBinding type; the discovery document keys the service 'file-storage' (http-dispatcher.ts:1619); and a prior in-repo audit (docs/audits/2026-06-handwritten-docs-accuracy-followups.md) already recorded 'services.* is a chapter-wide docs abstraction'. IMPORTANT NUANCE that makes the trap worse: a real .storage accessor DOES exist — ObjectStackClient.storage (packages/client/src/index.ts:2839) — but it is the browser/HTTP client surface (upload(file,scope), getDownloadUrl(fileId)), disjoint in shape from the server-side IStorageService this page documents (upload(key,data,options), download(key):Buffer). So a reader searching for 'storage' finds a real but wrong-shaped surface; the page now separates the two. Ruling 1 respected: slot NOT renamed, NO alias added. Fix = every chapter page gains a '- **Registry slot:** <key>' bullet (matching the Service name: convention kernel/contracts/ pages already use), storage-service.mdx gains an 'Accessor name vs registry slot' section showing ctx.getService('storage') throws [Kernel] Service 'storage' not found while ctx.getService('file-storage') resolves, and the chapter binding note states the distinction chapter-wide. H4 DELIVERED: check-runtime-services-index.mjs gains a FIFTH check holding each declared slot to a production registerService call — additive, the four existing enumerations untouched.",
      "tests": "All at final commit 6cf12acaf (union re-run on that exact tree; local HEAD == remote HEAD == 6cf12acaf). Gate family derived from actual changed paths via `node scripts/pm/dispatch-gates.mjs`, not recalled; every run echoed its script name (no zero-match silent pass). GREEN: check:nul-bytes (75 self-test assertions; 6185 files scanned) · check:docs-audit-scope (242+22 self-test cases; 179 hand-written docs in sync) · check:docs-redirects (48 assertions) · check:role-word · check:runtime-services-index · spec check:empty-state · check:liveness · check:strictness-ledger · check:variant-docs. Extended gate: self-test 11 -> 17 assertions; real run now reports '8 chapter page(s) ... and 8 declared registry slot(s) vs 54 registered key(s)'. REVERSE VERIFICATION — 3 ablations, direction predicted RED and observed RED each time, run from the committed state: (1) drop storage's slot bullet => 'no `- **Registry slot:** <key>` bullet -- state the key ctx.getService() resolves this surface by'; (2) declare the wrong slot 'storage' (the defect itself) => 'declares registry slot `storage`, but no production registerService() call under packages/ registers that key -- did you mean `file-storage`?'; (3) narrow 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. Gate restored byte-identical (git status clean, regex line matches original). MDX compiles (fumadocs-mdx). No changeset: docs + one CI gate script, nothing published changes — `skip-changeset` label applied as the UNION with the bot's `documentation`/`size/m` and READ BACK (all three present).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9683: CoreServiceName's `file-storage` member is the only slot whose name diverges from its accessor, and no source anywhere states why (H2 result — searched the registration site, the enum comment, ADRs 0076/0115, all changesets, and the contract file; the contract file records the alignment but not the reason). Explicitly does NOT propose renaming the slot.",
        "filed as #9684: runtime-services/versioning.mdx's stability matrix omits `services.sms` — 7 rows for 8 pages. A FOURTH enumeration of the chapter that #9634's gate does not hold; same class as #9604, one location over. Suggests extending the gate to that matrix and to each row's label vs the page's own Stability bullet."
      ]
    }

    H3 sweep table (the defect is confined to one chapter — nothing ships wrong to npm):

    surface verdict
    content/docs/kernel/runtime-services/ the defect — fixed in #9681
    kernel/contracts/storage-service.mdx:12 already correct (kernel.getService('file-storage'))
    Published READMEs (service-storage:39,150, plugin-dev:52,70) already correct — no npm-shipped README carries the wrong key
    examples/, apps/ no storage service resolution at all
    Runnable code calling getService('storage') none. Lone hit granted-permissions.test.ts:87 asserts a denial — key is arbitrary there, test passes because it throws
    .claude/**, skills/** (governed) clean — zero mentions of any storage service key, nothing for a maintainer to review

    Card's 8-row table re-verified on current main (c07d6e8b9, all line numbers still exact): data objectql/src/plugin.ts:312 · sharing :494 · audit :117 · queue :82 · email :370 · sms :181 · settings :130 · storage → file-storage :237. Seven map 1:1; storage is the lone exception, as filed.

    Generated by Claude Code


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions