158 lines
12 KiB
Markdown
158 lines
12 KiB
Markdown
# 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
|
|
|
|
- [x] 1.1 Read `openspec/AGENTS.md` §0, §0a, §0b, §0c, §13, §14
|
|
- [x] 1.2 Read `openspec/specs/storage-uri/spec.md` end-to-end (UriPlugin contract + conformance)
|
|
- [x] 1.3 Read `packages/storage/storage-uri/src/` (parseUri, error-codes, conformance harness)
|
|
- [x] 1.4 Read `packages/drive/drive-uri/src/plugins/{drive,drive-mesh,drive-template}/` as the 3-plugin reference
|
|
- [x] 1.5 Read `packages/storage/storage-http/src/types/fetch.types.ts` for the `FetchLike` re-export pattern
|
|
- [x] 1.6 Confirm the package is not in `STATUS.md` (genesis-original, not pending)
|
|
- [x] 1.7 Draft `proposal.md` including the ≥2-consumer justification
|
|
- [x] 1.8 Draft `design.md` covering URI grammar (ABNF), resolution matrix, traversal algorithm, range matrix, tier-purity argument, error-code matrix, file layout
|
|
- [x] 1.9 Draft `specs/storage-uri-plugin-bundle/spec.md` from the OpenSpec delta skeleton
|
|
- [x] 1.10 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete planned `it(...)` titles
|
|
- [x] 1.11 Build the numbered Requirement/Scenario implementation checklist below (§9)
|
|
|
|
## 2. Scaffolding
|
|
|
|
- [x] 2.1 Create `packages/storage/storage-uri-plugin-bundle/` mirroring `storage/storage-uri/` conventions
|
|
- [x] 2.2 Write `package.json` v0.0.0, `"sideEffects": false`, nine subpath exports per `design.md §9`
|
|
- [x] 2.3 Wire runtime deps: `@suaveplan/error`, `@suaveplan/storage-uri`, `@suaveplan/types`, `@opentelemetry/api`, `zod`, `tslib` (all `workspace:^` / `catalog:`)
|
|
- [x] 2.4 Wire devDeps: `@suaveplan/testing`, `@vitest/coverage-istanbul`, `fast-check`, `mitata`, `typescript`, `vite`, `vite-plugin-dts`, `vitest`
|
|
- [x] 2.5 Write `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts` (via `createTestConfig`)
|
|
- [x] 2.6 Write `tests/setup/polyfills.ts` and `README.md` skeleton
|
|
- [x] 2.7 Confirm tier purity: zero `node:*`, `bun:*`, `window.*` imports anywhere under `src/`
|
|
|
|
## 3. Implementation
|
|
|
|
- [x] 3.1 `src/error-codes/error-codes.ts` — `defineCodes("BUNDLE_URI", [...])` with 9 codes; `BundleUriError extends SuaveplanError` carrying `uri`/`scheme`/`packageSlug`/`relPath`/`rootKind` discriminants
|
|
- [x] 3.2 `src/types/plugin-options.types.ts` — `BundleUriPluginOptions` interface (no inline shapes per §11)
|
|
- [x] 3.3 `src/types/plugin-options.schemas.ts` — `BundleUriPluginOptionsSchema` (Zod): slug regex, rootUrl normalisation, optional `cache`/`fetch`/`logger`/`clock`
|
|
- [x] 3.4 `src/parse/parse.ts` — `parse(uri, options)`, `build(key, meta?)`, traversal-prevention (raw + post-decode + URL-resolve check)
|
|
- [x] 3.5 `src/abort/abort.ts` — pre-aborted short-circuit helper raising `BUNDLE_URI.ABORTED`; AbortError → BundleUriError conversion preserving `cause`
|
|
- [x] 3.6 `src/range/range.ts` — `Range` header builder + 206/200/416 disambiguation + offset/length validation
|
|
- [x] 3.7 `src/cache/cache.ts` — in-memory LRU keyed by URI, byte-bounded, default-off
|
|
- [x] 3.8 `src/streaming/streaming.ts` — `createReadStream` forwarding `Response.body` with one-chunk fallback when body is null
|
|
- [x] 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`
|
|
- [x] 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
|
|
- [x] 3.11 `src/factory/factory.ts` — `bundleUriPlugin(options)` returning the `UriPlugin` (scheme `"bundle"`, parse, build, adapter = StorageShim)
|
|
- [x] 3.12 `src/index.ts` — explicit named re-exports per §6a (no `export *`)
|
|
|
|
## 4. Tests
|
|
|
|
- [x] 4.1 `src/error-codes/error-codes.test.ts` — every `BUNDLE_URI.*` code reachable; registry exhaustion
|
|
- [x] 4.2 `src/factory/factory.test.ts` — option validation, slug regex, rootUrl trailing-slash enforcement, default `fetch`
|
|
- [x] 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
|
|
- [x] 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`
|
|
- [x] 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`
|
|
- [x] 4.6 `src/streaming/streaming.test.ts` — `createReadStream` forwards `Response.body`; one-chunk fallback when body is null
|
|
- [x] 4.7 `src/abort/abort.test.ts` — pre-aborted signal → `BUNDLE_URI.ABORTED` (no fetch); mid-flight abort propagates `AbortError` on `cause`
|
|
- [x] 4.8 `src/cache/cache.test.ts` — off by default; on, evicts oldest at byte ceiling; hit returns same bytes without re-fetch
|
|
- [x] 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`
|
|
- [x] 4.10 `src/conformance/conformance.test.ts` — `runUriPluginConformance` passes against `file://` temp-dir root AND `http://localhost:<port>/` real fixture
|
|
- [x] 4.11 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async)
|
|
- [x] 4.12 No imports from `vitest` or `bun:test` in test files — all via `@suaveplan/testing/runner` (rule 11)
|
|
- [x] 4.13 No `vi.mock` / `jest.mock` — real `node:http` localhost fixture + real temp-dir `file://` resolution only
|
|
|
|
## 5. Documentation
|
|
|
|
- [x] 5.1 Sibling `.md` for every implementation `.ts` (≥200 words, Purpose / Features / Basic + Advanced Examples / API Reference / Implementation Notes)
|
|
- [x] 5.2 `README.md` at package root: description, install, quick-start, `## Modules` linking every co-located `.md`
|
|
- [x] 5.3 README documents the per-package registration recipe (`new URL("./", import.meta.url)`)
|
|
- [x] 5.4 README documents the bundler-flatten composition pattern (Vite / esbuild / webpack `?url` import)
|
|
|
|
## 6. Gates
|
|
|
|
- [x] 6.1 `bun run check` (biome write+unsafe)
|
|
- [x] 6.2 `bunx turbo lint typecheck test --filter=@suaveplan/storage-uri-plugin-bundle`
|
|
- [x] 6.3 `bunx turbo test:coverage --filter=@suaveplan/storage-uri-plugin-bundle` — 100/100/100/100 istanbul
|
|
- [x] 6.4 `bun scripts/lint/check-test-runner-imports.ts` (rule 11)
|
|
- [x] 6.5 Tier-purity grep — `grep -rnE "from ['\"](node:|bun:|process)['\"]" packages/storage/storage-uri-plugin-bundle/src/` returns empty
|
|
- [x] 6.6 Banned-pattern scan returns empty (`@deprecated`, `TODO`, `FIXME`, `ts-ignore`, `ts-expect-error`, `.skip`, `.only`, `any`, `console.log`)
|
|
- [x] 6.7 Barrel-shape scan (§6a) — no `export *` in any feature `index.ts`
|
|
- [x] 6.8 Missing-`.md` scan returns empty
|
|
- [x] 6.9 Bare-version scan on `package.json` returns empty
|
|
- [x] 6.10 Subpath-export count = feature module count (9)
|
|
- [x] 6.11 `bun scripts/openspec-conformance.ts @suaveplan/storage-uri-plugin-bundle`
|
|
- [x] 6.12 Mitata benchmarks present at `src/__benchmarks__/bundle-uri.bench.ts` with baseline in README
|
|
|
|
## 7. Manifest & root docs
|
|
|
|
- [x] 7.1 `bun scripts/folder-reorg/generate-manifest.ts` — refresh `MANIFEST.md`
|
|
- [x] 7.2 `bun scripts/generate-missing.ts` — refresh `MISSING.yml`
|
|
- [x] 7.3 Update `STATUS.md` (genesis-original — append to "Completed" section with the package-summary line)
|
|
- [x] 7.4 Update `SUMMARY.md` with the per-package paragraph reflecting the shipped surface
|
|
- [x] 7.5 `bunx markdownlint-cli2 --fix` on touched files
|
|
- [x] 7.6 `bunx markdownlint-cli2` (no --fix) exits 0
|
|
|
|
## 8. Archival
|
|
|
|
- [x] 8.1 Fold `specs/storage-uri-plugin-bundle/spec.md` into `openspec/specs/storage-uri-plugin-bundle/spec.md`
|
|
- [x] 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`.
|
|
|
|
- [x] R1 Plugin factory shape and registration
|
|
- [x] R1.S1 factory returns a `UriPlugin` with `scheme: "bundle"`
|
|
- [x] R1.S2 factory validates options via Zod; invalid slug → throws
|
|
- [x] R1.S3 rootUrl without trailing slash is normalised (or rejected) deterministically
|
|
- [x] R2 `bundle://` URI grammar enforced by `parse`
|
|
- [x] R2.S1 well-formed URI parses to `{ key, meta: { packageSlug } }`
|
|
- [x] R2.S2 scheme mismatch → `INVALID_URI`
|
|
- [x] R2.S3 empty rel-path → `INVALID_URI`
|
|
- [x] R2.S4 missing authority → `INVALID_URI`
|
|
- [x] R3 Per-package slug enforcement
|
|
- [x] R3.S1 URI slug ≠ options.packageSlug → `PACKAGE_MISMATCH` carrying `actual`
|
|
- [x] R4 Traversal prevention (raw + decoded + URL-resolve)
|
|
- [x] R4.S1 raw `..` segment → `PATH_TRAVERSAL`
|
|
- [x] R4.S2 pct-encoded `%2E%2E` after decode → `PATH_TRAVERSAL`
|
|
- [x] R4.S3 URL-resolve outside rootUrl.pathname → `PATH_TRAVERSAL`
|
|
- [x] R5 Read-only enforcement on every write surface
|
|
- [x] R5.S1 `set` throws `READ_ONLY` with `op:"set"`
|
|
- [x] R5.S2 `setPartial` throws `READ_ONLY`
|
|
- [x] R5.S3 `delete` throws `READ_ONLY`
|
|
- [x] R5.S4 `clear` throws `READ_ONLY`
|
|
- [x] R5.S5 `list` throws `READ_ONLY`
|
|
- [x] R6 GET resolves through injected `FetchLike`
|
|
- [x] R6.S1 HTTP root: bytes returned verbatim
|
|
- [x] R6.S2 `file://` root: bytes returned verbatim
|
|
- [x] R6.S3 `404` → `NOT_FOUND`
|
|
- [x] R6.S4 non-2xx non-404 → `FETCH_FAILED` with `httpStatus`
|
|
- [x] R7 HEAD-backed `exists` and `getMetadata`
|
|
- [x] R7.S1 `exists` returns `true` on `2xx`, `false` on `404`/`410`
|
|
- [x] R7.S2 `getMetadata` parses `content-length`, `content-type`, `etag`, `last-modified`
|
|
- [x] R8 Range reads
|
|
- [x] R8.S1 HTTP root 206 path returns slice
|
|
- [x] R8.S2 HTTP root 200 path slices client-side AND increments `range_fallback_total`
|
|
- [x] R8.S3 416 → `RANGE_INVALID`
|
|
- [x] R8.S4 invalid offset/length → `RANGE_INVALID` (no fetch)
|
|
- [x] R8.S5 `data:`/`blob:` always slow-pathed, counter incremented
|
|
- [x] R9 Streaming reads
|
|
- [x] R9.S1 `createReadStream` forwards `Response.body` when present
|
|
- [x] R9.S2 one-chunk fallback when `Response.body` is null
|
|
- [x] R10 AbortSignal propagation
|
|
- [x] R10.S1 pre-aborted signal → `ABORTED` without invoking fetch
|
|
- [x] R10.S2 mid-flight abort propagates underlying `AbortError` on `cause`
|
|
- [x] R11 Optional in-memory LRU cache
|
|
- [x] R11.S1 cache off by default; counters stay at 0
|
|
- [x] R11.S2 cache on; eviction respects `maxBytes`
|
|
- [x] R11.S3 hit returns same bytes without invoking fetch
|
|
- [x] R12 OTel instrumentation
|
|
- [x] R12.S1 `op_duration_ms` recorded on success
|
|
- [x] R12.S2 `errors_total{code}` recorded on failure
|
|
- [x] R12.S3 query string stripped from `bundle.rel_path` attribute
|
|
- [x] R12.S4 `bundle.root_kind` classified correctly per resolution-matrix §2
|
|
- [x] R13 Conformance harness pass
|
|
- [x] R13.S1 `runUriPluginConformance` passes against `file://` root
|
|
- [x] R13.S2 `runUriPluginConformance` passes against `http://localhost:<port>/` root
|
|
- [x] R14 Error-code registry exhaustion
|
|
- [x] R14.S1 every `BUNDLE_URI.*` code is reachable from a real call site
|