Skip to content

service-storage: restore prefix enumeration cursor-shaped — list(prefix, { cursor, limit }) + adapter-conformance cases (cloud is the first-party caller the retirement could not see, twice) #6781

Description

@os-zhuang

Part of objectstack-ai/cloud#1203 — maintainer ruling 2026-08-08: option B. Filed by the repo:cloud seat per the cross-seat transfer protocol; pm:queue only, routing label left to the triage seat (note the two-surface shape below — this card may want the contract-first split).

Why

#5540 removed list?(prefix) from IStorageService and #5541 removed the adapter implementations, both on the measurement "Nothing in the repo called either". True for this repo; false one repo over: cloud has two production callers the measurement could not see —

  1. packages/service-cloud/src/environment-storage-cleanup.ts:92 — tenant attachment reclamation on environment delete (TS2339 compile-red at any post-retirement pin; cloud#935 is the incident where this sweep silently did nothing, orphaning deleted tenants' uploads forever);
  2. packages/service-cloud/src/storage-service-blob-store.ts:78 — marketplace snapshot GC (structural type, so it breaks at runtime instead: TypeError swallowed into a warning; delisted package detail blobs stay fetchable at the Worker edge).

This blocks cloud#1197 (the .objectstack-sha bump that lands the #5852 cross-org escalation producer fix in deployment): the retirement and the #5852 fix landed the same morning, so no commit on main has one without the other. Full option analysis (A: cloud hand-rolls S3 pagination — recreates the retired defect one repo over; C: tracked-keys sweeps — right end-state for the env sweep but its backfill itself needs enumeration once) is on cloud#1203; the ruling chose B: restore it upstream, correctly shaped.

The shape — already prescribed, not invented here

Both the retirement note and #5266's option 2 reserved this route word for word: "if a first-party caller ever needs real bucket enumeration it returns cursor-shaped — list(prefix, { cursor, limit }) — with adapter-conformance cases proving both backends agree before it ships." Cloud is that first-party caller, twice.

  1. Contract (packages/spec/src/contracts/storage-service.ts — ⚠️ spec surface, one-owner rule applies): list(prefix: string, opts?: { cursor?: string; limit?: number }) returning { items: StorageFileInfo[]; nextCursor?: string }. Cursor-shaped so callers must handle pagination explicitly — the silent-1000-cap and the one-level-vs-recursive dialect split (service-storage: IStorageService.list(prefix) means two different things on the two shipped adapters (local: one level, directories as files; S3: recursive, silently capped at 1000) #5266's two measured defects in the OLD signature) become unrepresentable.
  2. Both adapters, consistent semantics: recursive prefix match, paginated. Local: readdir recursive, directory entries skipped (never stat'd into results); S3: ListObjectsV2 + ContinuationToken loop honoring limit.
  3. Adapter-conformance cases proving Local and S3 agree — at minimum the three service-storage: IStorageService.list(prefix) means two different things on the two shipped adapters (local: one level, directories as files; S3: recursive, silently capped at 1000) #5266 named: nested keys (a/b/c visible under list('a')), directory entries excluded, >1000 objects fully enumerated via cursor.
  4. SwappableStorageService pass-through restored.
  5. storage-adapter-list-retirement.test.ts flips to pin the new contract (both adapters implement it, signatures match the spec) — flipped pins keep bearing load; do not just delete the retirement pins.
  6. Changeset minor (new public API surface).

Consumer side (not this card)

Cloud picks the capability up by moving the pin — cloud#1197 / PR #1205 (parked draft) re-targets to a SHA covering this, and cloud-side behaviour-level tests for both callers are tracked under cloud#1203 (measured there: cloud's deploy pipeline cannot see this class of break — OS_SKIP_DTS=1 skips the only typechecking pass — so type-level red alone is not a regression guard). A follow-on cloud card (option C: tracked-keys reclamation for the environment sweep, using this list once for backfill) is filed after cloud#1197 lands.

Refs: cloud#1203 (ruling + analysis), cloud#935, #5266 (the dialect measurement; tracking), #5540, #5541, cloud#1197 / cloud PR #1205.

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Triage (routing + designation — pm:queue was set by the filing seat per the transfer protocol): + domain:spec, designated to the spec seat as a single-claimant cross-domain card (rule 4 exception path, precedent #6735 / #6428).

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


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    认领 / Claim — dev seat

    • session: session_01F8q5J1MQyocgtNspb15fSn
    • branch: claude/issue-6781-storage-cursor-enumeration
    • base: origin/main

    按分诊评论的单一认领人指派(spec seat,跨域整卡)。声明的文件面(cross-domain,含 domain:cli 的 adapters):

    • packages/spec/src/contracts/storage-service.ts(契约,one-owner)
    • 两个 storage adapter 实现(local / S3)
    • SwappableStorageService 透传
    • storage-adapter-list-retirement.test.ts(翻转为新契约 pin)
    • adapter-conformance 用例 + changeset(minor)

    在途冲突检查:本卡与 cloud#1197 / cloud PR #1205(consumer 侧,明确不在本卡范围)无文件重叠。若有其他会话已在这些文件上在途,请回复本评论。


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions