Files
2026-05-27 23:29:53 +01:00

12 KiB

Tasks — storage-uri-plugin-bundle

Every checkbox MUST reflect work actually done in this change. Numbered Requirement / Scenario checklist (per AGENTS §0c step 6) is the gating list — implementation is not done until every requirement in specs/storage-uri-plugin-bundle/spec.md is ticked off here.

1. Spec authoring

  • 1.1 Read openspec/AGENTS.md §0, §0a, §0b, §0c, §13, §14
  • 1.2 Read openspec/specs/storage-uri/spec.md end-to-end (UriPlugin contract + conformance)
  • 1.3 Read packages/storage/storage-uri/src/ (parseUri, error-codes, conformance harness)
  • 1.4 Read packages/drive/drive-uri/src/plugins/{drive,drive-mesh,drive-template}/ as the 3-plugin reference
  • 1.5 Read packages/storage/storage-http/src/types/fetch.types.ts for the FetchLike re-export pattern
  • 1.6 Confirm the package is not in STATUS.md (genesis-original, not pending)
  • 1.7 Draft proposal.md including the ≥2-consumer justification
  • 1.8 Draft design.md covering URI grammar (ABNF), resolution matrix, traversal algorithm, range matrix, tier-purity argument, error-code matrix, file layout
  • 1.9 Draft specs/storage-uri-plugin-bundle/spec.md from the OpenSpec delta skeleton
  • 1.10 Every requirement in the spec delta carries a **Verified by:** line naming concrete planned it(...) titles
  • 1.11 Build the numbered Requirement/Scenario implementation checklist below (§9)

2. Scaffolding

  • 2.1 Create packages/storage/storage-uri-plugin-bundle/ mirroring storage/storage-uri/ conventions
  • 2.2 Write package.json v0.0.0, "sideEffects": false, nine subpath exports per design.md §9
  • 2.3 Wire runtime deps: @suaveplan/error, @suaveplan/storage-uri, @suaveplan/types, @opentelemetry/api, zod, tslib (all workspace:^ / catalog:)
  • 2.4 Wire devDeps: @suaveplan/testing, @vitest/coverage-istanbul, fast-check, mitata, typescript, vite, vite-plugin-dts, vitest
  • 2.5 Write tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts (via createTestConfig)
  • 2.6 Write tests/setup/polyfills.ts and README.md skeleton
  • 2.7 Confirm tier purity: zero node:*, bun:*, window.* imports anywhere under src/

3. Implementation

  • 3.1 src/error-codes/error-codes.ts — defineCodes("BUNDLE_URI", [...]) with 9 codes; BundleUriError extends SuaveplanError carrying uri/scheme/packageSlug/relPath/rootKind discriminants
  • 3.2 src/types/plugin-options.types.ts — BundleUriPluginOptions interface (no inline shapes per §11)
  • 3.3 src/types/plugin-options.schemas.ts — BundleUriPluginOptionsSchema (Zod): slug regex, rootUrl normalisation, optional cache/fetch/logger/clock
  • 3.4 src/parse/parse.ts — parse(uri, options), build(key, meta?), traversal-prevention (raw + post-decode + URL-resolve check)
  • 3.5 src/abort/abort.ts — pre-aborted short-circuit helper raising BUNDLE_URI.ABORTED; AbortError → BundleUriError conversion preserving cause
  • 3.6 src/range/range.ts — Range header builder + 206/200/416 disambiguation + offset/length validation
  • 3.7 src/cache/cache.ts — in-memory LRU keyed by URI, byte-bounded, default-off
  • 3.8 src/streaming/streaming.ts — createReadStream forwarding Response.body with one-chunk fallback when body is null
  • 3.9 src/storage-shim/storage-shim.ts — StorageLike adapter wrapping FetchLike: get, exists (HEAD), getMetadata (HEAD), getPartial, createReadStream; every write/list/delete/clear throws READ_ONLY
  • 3.10 src/otel/otel.ts — tracer + meter wiring; recordOp(op, rootKind, outcome, ms, bytes?); query-string stripping on bundle.rel_path attribute; 256-char truncation
  • 3.11 src/factory/factory.ts — bundleUriPlugin(options) returning the UriPlugin (scheme "bundle", parse, build, adapter = StorageShim)
  • 3.12 src/index.ts — explicit named re-exports per §6a (no export *)

4. Tests

  • 4.1 src/error-codes/error-codes.test.ts — every BUNDLE_URI.* code reachable; registry exhaustion
  • 4.2 src/factory/factory.test.ts — option validation, slug regex, rootUrl trailing-slash enforcement, default fetch
  • 4.3 src/parse/parse.test.ts — accept well-formed URIs; reject scheme mismatch (INVALID_URI), empty rel-path (INVALID_URI), slug mismatch (PACKAGE_MISMATCH), raw .. (PATH_TRAVERSAL), %2E%2E decoded (PATH_TRAVERSAL), URL-resolve escape (PATH_TRAVERSAL); build round-trips
  • 4.4 src/storage-shim/storage-shim.test.ts — real file:// temp-dir round-trip via fetch; real node:http localhost fixture round-trip; HEAD exists/metadata; every write/list/delete/clear throws READ_ONLY
  • 4.5 src/range/range.test.ts — 206 verbatim against HTTP fixture; 200 fallback increments range_fallback_total; 416 → RANGE_INVALID; offset/length validation → RANGE_INVALID
  • 4.6 src/streaming/streaming.test.ts — createReadStream forwards Response.body; one-chunk fallback when body is null
  • 4.7 src/abort/abort.test.ts — pre-aborted signal → BUNDLE_URI.ABORTED (no fetch); mid-flight abort propagates AbortError on cause
  • 4.8 src/cache/cache.test.ts — off by default; on, evicts oldest at byte ceiling; hit returns same bytes without re-fetch
  • 4.9 src/otel/otel.test.ts — duration histogram + counters via in-memory MeterProvider; span attributes via in-memory SpanExporter; query string stripped from bundle.rel_path
  • 4.10 src/conformance/conformance.test.ts — runUriPluginConformance passes against file:// temp-dir root AND http://localhost:<port>/ real fixture
  • 4.11 Every it(...) carries { timeout: 5000 } (or 30000 for genuinely async)
  • 4.12 No imports from vitest or bun:test in test files — all via @suaveplan/testing/runner (rule 11)
  • 4.13 No vi.mock / jest.mock — real node:http localhost fixture + real temp-dir file:// resolution only

5. Documentation

  • 5.1 Sibling .md for every implementation .ts (≥200 words, Purpose / Features / Basic + Advanced Examples / API Reference / Implementation Notes)
  • 5.2 README.md at package root: description, install, quick-start, ## Modules linking every co-located .md
  • 5.3 README documents the per-package registration recipe (new URL("./", import.meta.url))
  • 5.4 README documents the bundler-flatten composition pattern (Vite / esbuild / webpack ?url import)

6. Gates

  • 6.1 bun run check (biome write+unsafe)
  • 6.2 bunx turbo lint typecheck test --filter=@suaveplan/storage-uri-plugin-bundle
  • 6.3 bunx turbo test:coverage --filter=@suaveplan/storage-uri-plugin-bundle — 100/100/100/100 istanbul
  • 6.4 bun scripts/lint/check-test-runner-imports.ts (rule 11)
  • 6.5 Tier-purity grep — grep -rnE "from ['\"](node:|bun:|process)['\"]" packages/storage/storage-uri-plugin-bundle/src/ returns empty
  • 6.6 Banned-pattern scan returns empty (@deprecated, TODO, FIXME, ts-ignore, ts-expect-error, .skip, .only, any, console.log)
  • 6.7 Barrel-shape scan (§6a) — no export * in any feature index.ts
  • 6.8 Missing-.md scan returns empty
  • 6.9 Bare-version scan on package.json returns empty
  • 6.10 Subpath-export count = feature module count (9)
  • 6.11 bun scripts/openspec-conformance.ts @suaveplan/storage-uri-plugin-bundle
  • 6.12 Mitata benchmarks present at src/__benchmarks__/bundle-uri.bench.ts with baseline in README

7. Manifest & root docs

  • 7.1 bun scripts/folder-reorg/generate-manifest.ts — refresh MANIFEST.md
  • 7.2 bun scripts/generate-missing.ts — refresh MISSING.yml
  • 7.3 Update STATUS.md (genesis-original — append to "Completed" section with the package-summary line)
  • 7.4 Update SUMMARY.md with the per-package paragraph reflecting the shipped surface
  • 7.5 bunx markdownlint-cli2 --fix on touched files
  • 7.6 bunx markdownlint-cli2 (no --fix) exits 0

8. Archival

  • 8.1 Fold specs/storage-uri-plugin-bundle/spec.md into openspec/specs/storage-uri-plugin-bundle/spec.md
  • 8.2 Move openspec/changes/add-storage-uri-plugin-bundle/ → openspec/archive/<yyyy-mm-dd>-add-storage-uri-plugin-bundle/

9. Numbered Requirement / Scenario checklist (§0c step 6 gate)

Implementation is NOT complete until every box below is checked. Each item maps 1:1 to a ### Requirement / #### Scenario in specs/storage-uri-plugin-bundle/spec.md.

  • R1 Plugin factory shape and registration
    • R1.S1 factory returns a UriPlugin with scheme: "bundle"
    • R1.S2 factory validates options via Zod; invalid slug → throws
    • R1.S3 rootUrl without trailing slash is normalised (or rejected) deterministically
  • R2 bundle:// URI grammar enforced by parse
    • R2.S1 well-formed URI parses to { key, meta: { packageSlug } }
    • R2.S2 scheme mismatch → INVALID_URI
    • R2.S3 empty rel-path → INVALID_URI
    • R2.S4 missing authority → INVALID_URI
  • R3 Per-package slug enforcement
    • R3.S1 URI slug ≠ options.packageSlug → PACKAGE_MISMATCH carrying actual
  • R4 Traversal prevention (raw + decoded + URL-resolve)
    • R4.S1 raw .. segment → PATH_TRAVERSAL
    • R4.S2 pct-encoded %2E%2E after decode → PATH_TRAVERSAL
    • R4.S3 URL-resolve outside rootUrl.pathname → PATH_TRAVERSAL
  • R5 Read-only enforcement on every write surface
    • R5.S1 set throws READ_ONLY with op:"set"
    • R5.S2 setPartial throws READ_ONLY
    • R5.S3 delete throws READ_ONLY
    • R5.S4 clear throws READ_ONLY
    • R5.S5 list throws READ_ONLY
  • R6 GET resolves through injected FetchLike
    • R6.S1 HTTP root: bytes returned verbatim
    • R6.S2 file:// root: bytes returned verbatim
    • R6.S3 404 → NOT_FOUND
    • R6.S4 non-2xx non-404 → FETCH_FAILED with httpStatus
  • R7 HEAD-backed exists and getMetadata
    • R7.S1 exists returns true on 2xx, false on 404/410
    • R7.S2 getMetadata parses content-length, content-type, etag, last-modified
  • R8 Range reads
    • R8.S1 HTTP root 206 path returns slice
    • R8.S2 HTTP root 200 path slices client-side AND increments range_fallback_total
    • R8.S3 416 → RANGE_INVALID
    • R8.S4 invalid offset/length → RANGE_INVALID (no fetch)
    • R8.S5 data:/blob: always slow-pathed, counter incremented
  • R9 Streaming reads
    • R9.S1 createReadStream forwards Response.body when present
    • R9.S2 one-chunk fallback when Response.body is null
  • R10 AbortSignal propagation
    • R10.S1 pre-aborted signal → ABORTED without invoking fetch
    • R10.S2 mid-flight abort propagates underlying AbortError on cause
  • R11 Optional in-memory LRU cache
    • R11.S1 cache off by default; counters stay at 0
    • R11.S2 cache on; eviction respects maxBytes
    • R11.S3 hit returns same bytes without invoking fetch
  • R12 OTel instrumentation
    • R12.S1 op_duration_ms recorded on success
    • R12.S2 errors_total{code} recorded on failure
    • R12.S3 query string stripped from bundle.rel_path attribute
    • R12.S4 bundle.root_kind classified correctly per resolution-matrix §2
  • R13 Conformance harness pass
    • R13.S1 runUriPluginConformance passes against file:// root
    • R13.S2 runUriPluginConformance passes against http://localhost:<port>/ root
  • R14 Error-code registry exhaustion
    • R14.S1 every BUNDLE_URI.* code is reachable from a real call site