From 9bf9369dc21de0108f0b056c410a88c3ad7de7ac Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:08:49 -0700 Subject: [PATCH 01/13] docs(spec): approve documentation pass positioning Lead with boundary-friendly serializable error data while keeping Result ergonomics as the normal TypeScript handling model. Record the approved audience, Epicenter attribution policy, Hono scope, and verify-before-delete allowlist. --- ...10T012026-greenfield-documentation-pass.md | 722 ++++++++++++++++++ 1 file changed, 722 insertions(+) create mode 100644 specs/20260710T012026-greenfield-documentation-pass.md diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md new file mode 100644 index 0000000..d85b571 --- /dev/null +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -0,0 +1,722 @@ +# Greenfield Documentation Pass + +**Date**: 2026-07-10 +**Status**: In Progress +**Author**: Maintainer-approved specification +**Branch**: `codex/docs-greenfield-pass` + +## Overview + +Rebuild wellcrafted's documentation around one accurate product story, one runnable learning path, and one authoritative reference page for every published subpath. This pass changes documentation, examples, contributor guidance, validation, and distributable agent skills. It does not change public APIs, publish packages, deploy the site, push the branch, or open a pull request. + +Wave 0 is approved and recorded in [Wave 0 Decisions](#wave-0-decisions). Deferred API and compatibility questions do not block the first content wave. Any change that would enforce JSON-safe error fields remains a separate API proposal and is not part of this documentation pass. + +## Uncompromised Product Direction + +### Product sentence + +Approved product sentence: + +> wellcrafted defines expected errors as plain, boundary-friendly data that can move through JSON, HTTP, workers, IPC, logs, and UI when every field is JSON-compatible. Its familiar `{ data, error }` Result shape makes that data ergonomic to return and handle in ordinary TypeScript. + +The optional shorter front-door form remains: + +> Typed failures without a new programming model. + +The mechanism should follow immediately: named plain-object error variants, the `{ data, error }` Result shape, `async/await`, early returns, exact guards, and `switch`. + +The serialization limitation belongs beside the lead, not in a footnote. `defineErrors` does not type-enforce JSON-compatible fields, and arbitrary fields do not serialize perfectly. The promise applies when the complete error value contains JSON-compatible data. Runtime validation and end-to-end static typing are separate boundary concerns. + +### Product category + +The approved category is an error-handling library with a small supporting toolkit. It is not a general TypeScript utility collection whose modules happen to include errors. + +The evidence is asymmetric. At Epicenter commit `4d438c0`, 244 tracked TypeScript, JavaScript, and Svelte files import wellcrafted. Of those, 176 import `wellcrafted/result`, `wellcrafted/error`, or both. The other entry points reinforce the same application contract through logging, tests, framework adapters, JSON parsing, branded domain values, and lifecycle helpers. These counts are internal research evidence only; they must not appear in public documentation. + +Approved narrative roles are below. These describe documentation prominence, not compatibility stability: + +| Narrative role | Subpaths | Documentation treatment | +| --- | --- | --- | +| Product-defining | `result`, `error` | README, start path, guides, and complete reference | +| Established extensions of the contract | `logger`, `query`, `testing`, `json` | Integration or guide coverage plus complete reference | +| Supporting utilities | `brand`, `function` | Concise guide where justified plus complete reference | +| Published integration, not product-defining | `standard-schema` | Complete reference and a focused integration recipe | + +Every published subpath gets a reference page even when it is not product-defining. + +### Audience and jobs to be done + +Primary audience: TypeScript application authors. The start path should assume TypeScript familiarity while teaching wellcrafted's Result and error vocabulary from first principles. + +Secondary audiences are library authors exposing typed service contracts and framework users adapting those contracts to TanStack Query, Hono, validators, tests, or UI reporting. + +The documentation must help a reader: + +1. Name the expected ways an operation can fail. +2. Convert a throwing I/O boundary into a typed value without wrapping unrelated code. +3. Propagate, recover from, or branch on a failure with ordinary control flow. +4. Carry the same error vocabulary into HTTP responses, logs, tests, and UI. +5. Adapt a Result where a framework requires a thrown error. +6. Represent legitimate absence as `Ok(null)` and understand that the shape is unambiguous only under a non-null-error convention. +7. Decide when manual Result propagation is too small a tool and an effect system is warranted. + +Effect is an honest boundary and escape hatch, not the lead competitor. Boundary-friendly error data is the lead promise, qualified immediately: serialization remains a convention and depends on every field being JSON-compatible. + +## Motivation + +### Current state + +PR #133 was valuable corrective subtraction. It merged 22 commits, added 515 lines, deleted 4,541 lines, removed two redundant root guides and eight site pages, rewrote the README around real usage, and corrected several API and exhaustiveness claims. + +It did not establish a durable ownership model. Current guidance is still spread across: + +| Surface | Current scale | Current role | +| --- | ---: | --- | +| `README.md` | 282 lines | Best narrative, install, examples, tradeoffs, comparison, partial API glance, skills | +| `docs/` | 8,985 lines, 24 pages | Tutorials, reference fragments, recipes, decisions, marketing, and history | +| `src/*README.md` | 1,285 lines | Duplicate public tutorials and one current query guide | +| `skills/` | 1,259 lines | Agent guidance with duplicated claims and incomplete newer APIs | +| `specs/` | 5,438 lines | Active-looking historical plans, launch copy, and superseded designs | +| `CHANGELOG.md` | 1,222 lines | Release history, its correct role | +| `CONTRIBUTING.md` | absent | No contributor front door | +| `examples/` | absent | No runnable learning path | + +The repository contains roughly 402 TypeScript or JavaScript-like documentation fences. None are compiled as documentation examples. + +### Problems + +1. **A newcomer cannot run the first example.** The README and quick start depend on undefined values or application helpers. There is no canonical executable example. +2. **Public reference coverage is incomplete.** Six subpaths lack dedicated reference pages, and the pages that resemble reference also mix tutorial and rationale. +3. **Individual corrections drift because facts have multiple owners.** The serializability convention was fixed in one philosophy page while source READMEs, core docs, integrations, and skills kept absolute claims. +4. **Installation and contributor guidance are false.** The site describes an unsupported root import, conflicts on TypeScript versions, recommends legacy module resolution, uses npm for a Bun-only repository, and calls a nonexistent `dev` script. +5. **Current APIs are missing from public guidance.** The v0.44 query adapters and `defineKeys` are absent from the site. Official test helpers are absent from the testing guide. +6. **Claims outrun evidence.** Bundle size, competitor size, production reliability, performance, and serialization claims have no committed method that can reproduce them. +7. **Documentation checks do not protect the repository.** Existing Mintlify scripts run from the wrong directory, docs checks are absent from CI, examples are not compiled, and export coverage is not enforced. + +### Desired state + +A first-time reader can answer all of these without consulting source, changelog, or historical specs: + +- What is wellcrafted? +- Why would I use it instead of `try/catch` alone? +- What does it cost in control flow and capability? +- How do I install it and run one working example? +- How do `defineErrors`, `Result`, `trySync`, and `tryAsync` compose? +- What happens at serialization and framework boundaries? +- Where is the complete current API for each published subpath? + +## Evidence Read + +### Release and public surface + +- `HEAD`, `origin/main`, npm `latest`, and tag `v0.44.0` resolve to the 0.44.0 release. The branch starts from commit `ea58b21`. +- `package.json`, `tsdown.config.ts`, source entry points, emitted declarations, tests, changelog, npm metadata, and the published tarball agree on nine ESM subpaths. +- The npm package has no root `"."` export. Root imports are unsupported. Node ESM reports `ERR_PACKAGE_PATH_NOT_EXPORTED`; Bun 1.3.1 reports `ERR_MODULE_NOT_FOUND`. +- The published surface has 33 runtime export slots and 27 type export slots. Coverage is keyed by `(subpath, export kind, symbol)`: collapsing type/value pairs within each subpath yields 57 subpath-scoped identifiers, while collapsing repeated spellings across all subpaths yields 53 global names. +- The published tarball has 51 files, 67.3 kB packed, and 245.7 kB unpacked. These numbers describe the package archive, not application bundle cost. +- The package declares no runtime dependencies, peer dependencies, engines, or compatibility matrix. + +Authoritative entry points: + +- `src/result/index.ts` +- `src/error/index.ts` +- `src/logger/index.ts` +- `src/json.ts` +- `src/brand.ts` +- `src/function.ts` +- `src/query/index.ts` +- `src/standard-schema/index.ts` +- `src/testing.ts` + +### Real consumers + +Epicenter usage was inspected at commit `4d438c0`, including these representative files: + +- `packages/client/src/transcribe.ts`: narrow throwing boundaries, named service errors, manual propagation. +- `packages/workspace/src/shared/actions.ts`: Results as a boundary protocol and the bare-tagged-error footgun. +- `packages/workspace/src/document/table.ts`: `Ok(null)`, classified domain errors, partial success, and exhaustive branching. +- `packages/workspace/src/document/sqlite-writer.ts`: named recoverable failures sent to the logger instead of propagated. +- `packages/server/src/routes/blob-errors.ts`: Result envelopes and typed error fields at an HTTP boundary. +- `packages/ui/src/sonner/toast-on-error.ts`: tagged errors flowing into UI copy without losing the original value. +- `apps/whispering/src/lib/report/index.ts`: one error vocabulary feeding logs, toast, OS notification, and details UI. +- `packages/app-shell/src/account-popover/account-popover.svelte`: current `resultQueryOptions` and `resultMutationOptions` usage. +- `docs/articles/ok-null-is-fine-err-null-is-a-lie.md`: the Result shape's central invariant and limit. +- `docs/articles/wrap-only-what-throws.md`: the clearest explanation of surgical failure boundaries. + +Import evidence: + +| Subpath | Epicenter files | +| --- | ---: | +| `result` | 135 | +| `error` | 109 | +| `logger` | 33 | +| `brand` | 24 | +| `testing` | 17 | +| `query` | 16 | +| `json` | 13 | +| `function` | 6 | +| `standard-schema` | 0 | + +The root Epicenter catalog and lock resolve wellcrafted 0.44.0. Its physical `node_modules` was still cached at 0.43.0 during research, so Epicenter verification must run `bun install` before its typecheck can be treated as evidence for the renamed query adapters. + +### Documentation history and current site + +- PR #133's strongest retained decisions are the README's `try/catch` opening, real service shape, manual propagation cost, explicit `never` guard, and Effect escape hatch. +- PR #133 correctly deleted large duplicate guides and several orphaned pages. This pass must preserve that subtraction instead of recreating a second encyclopedia. +- Two current site pages are not in navigation: `for-the-pragmatic-fp-developer` and `from-effect-to-pragmatic-errors`. They overlap each other and the README tradeoff. +- The site has no missing configured navigation targets, but valid links do not imply correct examples or complete API coverage. + +### Baseline verification + +Observed on 2026-07-10 at the 0.44.0 baseline: + +- `bun test`: 158 tests passed, 0 failed. +- `bun run typecheck`: passed. +- `bun run build`: passed. +- Unpinned `bunx mint` resolved Mint 4.2.684. `bun run docs:validate` under Node 26.3.1 failed because Mint rejects Node 25 and newer. +- The same unpinned root script under bundled Node 24.14.0 reported that it must run where `docs.json` exists. The package script runs from the wrong directory. +- `mint validate` from `docs/` under Node 24.14.0 reached the site and failed on the unsupported `@mintlify/components` import in `docs/index.mdx`. +- `mint broken-links` from `docs/` under Node 24.14.0 passed. +- `https://bundlephobia.com/api/size?package=wellcrafted@0.44.0` returned an HTTP 500 build error. The current badge and comparison table do not provide a working proof path. + +These Mint and Bundlephobia results are dated observations from mutable external tools, not permanent repository proofs. The implementation must pin local tooling before treating them as a baseline. + +## Public Surface Map + +The reference must cover these current exports without inventing a stability promise. + +| Subpath | Runtime exports | Type exports | +| --- | --- | --- | +| `result` | `Ok`, `Err`, `isResult`, `isOk`, `isErr`, `trySync`, `tryAsync`, `unwrap`, `resolve`, `tapErr`, `partitionResults` | `Ok`, `Err`, `Result`, `UnwrapOk`, `UnwrapErr` | +| `error` | `defineErrors`, `extractErrorMessage` | `AnyTaggedError`, `ErrorBody`, `ErrorsConfig`, `ValidatedConfig`, `DefineErrorsReturn`, `InferError`, `InferErrors` | +| `logger` | `consoleSink`, `createLogger`, `memorySink`, `composeSinks`, `tapErr` | `LogEvent`, `LogLevel`, `LogSink`, `Logger`, `LoggableError` | +| `json` | `JsonParseError`, `parseJson` | `JsonValue`, `JsonObject`, `JsonParseError` | +| `brand` | none | `Brand` | +| `function` | `once` | none | +| `query` | `createQueryFactories`, `defineKeys`, `resultMutationOptions`, `resultQueryOptions` | no named aliases | +| `standard-schema` | `ErrSchema`, `FAILURES`, `OkSchema`, `ResultSchema`, `hasJsonSchema`, `hasValidate` | `Err`, `Ok`, `Result`, `StandardJSONSchemaV1`, `StandardSchemaV1`, `StandardTypedV1` | +| `testing` | `expectErr`, `expectOk` | none | + +`defineQuery` and `defineMutation` are returned by `createQueryFactories`; they are not importable top-level exports. Query definitions have these exact shapes: + +- Query: `{ options, fetch(), ensure() }`; it is not callable. +- Mutation: callable with variables and has `.options`; there is no `.execute()`. +- `resultQueryOptions` and `resultMutationOptions`: client-agnostic adapters for hook-local or reactive options. +- `defineKeys`: static entries preserve literal readonly tuples. Factory entries without `as const` preserve tuple shape but widen literal positions; add `as const` when the literal positions matter. + +Some published types look like implementation machinery, notably `ErrorBody`, `ErrorsConfig`, `ValidatedConfig`, `DefineErrorsReturn`, and `FAILURES`. This documentation pass must document what is exported today and separately report whether those names should be removed in a future API change. It must not silently change exports. + +## Documentation Ownership + +### Current concept ownership inventory + +| Concept | Current competing homes | Drift already observed | +| --- | --- | --- | +| Product promise and audience | `README.md`, `docs/index.mdx`, `design-principles`, two Effect essays, package description | Error library versus general toolkit; lowercase versus title-case brand; unsupported claims | +| Installation and compatibility | README install, `getting-started/installation`, package metadata | Unsupported root import, TypeScript 5.0 versus 4.5, legacy module resolution, no tested runtime matrix | +| First working Result | README examples, quick start, Result core page, source README, result skill | No complete runnable example; generic truthiness checks hide falsy errors | +| Error definition | README, error core page, optional-keys, source error README, define-errors skill, evolution essays | Tagged body confused with `Err` wrapper; variant rules repeated | +| Serialization boundary | README, site index, error core, Hono, three philosophy pages, source error README, patterns skill | Convention described elsewhere as type-enforced or perfect | +| Service composition | README, quick start, real-world, service-layer, migration, patterns skill | Same manual propagation pattern copied with different error shapes | +| Query integration | README bullet, public integration, source query README, query skill, changelog | Public site and skill omit v0.44 adapters and `defineKeys` | +| Testing | README bullet, testing integration, source JSDoc/tests | Official helpers omitted; custom matcher rejects `Ok(null)` | +| Brand model | brand core, validation integration, brand decision, source JSDoc, brand skill | Tutorial, reference, validator recipe, and representation rationale mixed | +| Public export inventory | `package.json`, source barrels, emitted declarations, README glance, changelog | No complete reader-facing map and no automated coverage | +| Contributor workflow | installation page, `AGENTS.md`, `CHANGESET_GUIDE.md` | npm and nonexistent commands conflict with Bun policy | + +### Source of fact versus reader-facing projection + +Repository artifacts and public documentation have different roles; they are not two independent owners: + +| Fact or content | Factual source | Reader-facing projection and drift rule | +| --- | --- | --- | +| Importable subpaths | `package.json#exports` and packed-package smoke tests | Reference index is mechanically checked against the manifest | +| Exact symbols and signatures | Source entry points, JSDoc, emitted declarations, and focused tests | Each reference page is checked by `(subpath, export kind, symbol)` coverage | +| Runtime edge cases | Implementation and focused tests | Reference explains them; a claim without a test is labeled convention or removed | +| Product sentence and audience | Maintainer decision recorded in this spec | README owns the full front-door wording; site index reuses only the sentence and links | +| First example code | `examples/quick-start.ts` | README and quick start include, extract, or mechanically compare with that file | +| Tradeoff | `docs/decisions/pragmatic-tradeoffs.mdx` | README carries a short summary and link, not a second rationale | +| Runnable code | `examples/` | Guides consume checked code and do not keep independent long copies | +| Task-oriented workflow | One page in `docs/guides/` | Reference and decisions link to it without retelling the workflow | +| Complete current API | One page per subpath in `docs/reference/` | Other pages link to the reference instead of copying full signatures | +| Third-party adaptation | One page in `docs/integrations/` | It states external prerequisites and boundary conversions only | +| Contributor workflow | `CONTRIBUTING.md` | Consumer installation links to it and contains no source setup | +| Agent instructions | `skills/` | Checked against the same exports and canonical examples | +| Release history | `CHANGELOG.md` | Current docs use only current names; historical names remain in changelog | +| In-flight implementation plan | `specs/` with `Draft` or `In Progress` | A separate approved cleanup handles spent historical specs | + +### Target information architecture + +```text +README.md +CONTRIBUTING.md +examples/ +|-- quick-start.ts +|-- service-boundary.ts +|-- serialization-boundary.ts +`-- tanstack-query.ts + +docs/ +|-- index.mdx +|-- start/ +| |-- installation.mdx +| |-- quick-start.mdx +| `-- migrating-from-try-catch.mdx +|-- guides/ +| |-- defining-error-vocabularies.mdx +| |-- composing-results.mdx +| |-- service-boundaries.mdx +| `-- serialization-boundaries.mdx +|-- reference/ +| |-- result.mdx +| |-- error.mdx +| |-- logger.mdx +| |-- json.mdx +| |-- brand.mdx +| |-- function.mdx +| |-- query.mdx +| |-- standard-schema.mdx +| `-- testing.mdx +|-- integrations/ +| |-- tanstack-query.mdx +| |-- validation-libraries.mdx +| `-- hono.mdx +`-- decisions/ + |-- result-shape.mdx + |-- error-contract.mdx + |-- brand-representation.mdx + `-- pragmatic-tradeoffs.mdx +``` + +The final page count is not a target. Concept ownership is the target. A page that cannot name a distinct reader job should be merged or deleted. + +### Page contracts + +| Target page or artifact | Reader question it alone answers | Required unique content | Content forbidden here | +| --- | --- | --- | --- | +| `README.md` | What is this, can I run it, and what does it cost? | Approved sentence, install command, included quick start, short tradeoff, map to docs | Full API inventory, competitor size table, long design rationale | +| `CONTRIBUTING.md` | How do I work on this repository? | Bun setup, non-mutating checks, docs workflow, changesets, PR expectations | Consumer framework setup or current API tutorial | +| `examples/quick-start.ts` | Does the first example compile and run? | One offline success and failure | Undefined helpers, network, framework code | +| Other `examples/*` | Does this complete pattern work against the package? | One complete service, serialization, or query scenario per file | Marketing prose or duplicate API reference | +| `docs/index.mdx` | Where should I go next? | One-sentence orientation and navigation by reader job | Second product essay, size claims, deep examples | +| `docs/start/installation.mdx` | What must a consumer install and configure? | Supported package managers, subpaths, tested compiler/runtime matrix | Contributor setup, broad framework recipes, untested compatibility | +| `docs/start/quick-start.mdx` | Can I understand and run the core loop? | Walkthrough of the canonical quick-start file | Brand, query, or service architecture survey | +| `docs/start/migrating-from-try-catch.mdx` | How do I adopt this incrementally? | Narrow-boundary migration sequence and rollback advice | Complete Result or error reference | +| `docs/guides/defining-error-vocabularies.mdx` | How do I choose and define variants? | Variant naming, fields, optional keys, cause ownership | Exhaustive export list or error API history | +| `docs/guides/composing-results.mdx` | How do I propagate, recover, and discriminate safely? | Non-null and truthy error conventions, falsy errors, manual propagation, `Ok(null)` | Service folder architecture or framework adapters | +| `docs/guides/service-boundaries.mdx` | How do Results move through application layers? | One approved service-to-caller flow and error transformation ownership | Unproved production metrics or full integration APIs | +| `docs/guides/serialization-boundaries.mdx` | What survives JSON and what does not? | Recursive JSON-data model, exact versus semantic preservation, positive and negative fixtures, validation boundary | “Perfect,” “intact,” or type-enforced claims | +| Nine `docs/reference/*.mdx` pages | What is importable from this one subpath now? | Every current value/type export, signature, behavior, edge case, links outward | Product positioning, multi-page tutorials, historical names | +| `docs/integrations/tanstack-query.mdx` | How do Results adapt to TanStack's throwing contract? | Two-family choice, reactive snapshot rule, prerequisites, cache boundary | Full query symbol reference or general service architecture | +| `docs/integrations/validation-libraries.mdx` | How does `Brand` work with runtime validators? | ArkType, Zod, and Valibot boundary recipe | Brand representation internals or Standard Schema API reference | +| `docs/integrations/hono.mdx` | How do I preserve and validate a Result-shaped HTTP payload? | A careful HTTP boundary guide that distinguishes JSON shape preservation, runtime validation, and end-to-end static typing, with an explicit client contract | Claim that preserving a JSON shape validates it or gives `response.json()` exact types by itself | +| `docs/decisions/result-shape.mdx` | Why `{ data, error }`, and what is its limit? | `Ok(null)` collision, non-null and truthy error conventions, rejected alternatives | General Result tutorial | +| `docs/decisions/error-contract.mdx` | Why named object variants and `defineErrors`? | `name`/`message`, Rust influence, builder deletion, serializability convention | Factory how-to or complete signatures | +| `docs/decisions/brand-representation.mdx` | Why nested marker brands? | Representation tradeoff and assignability rationale | Validator tutorial | +| `docs/decisions/pragmatic-tradeoffs.mdx` | When is manual Result flow too small? | No `?`, DI, concurrency, resources; Effect escape hatch | Competitor size or unsupported superiority claims | + +## Keep, Rewrite, Move, and Delete Map + +This is the approved file-level disposition. A “Delete” row becomes executable only after its replacement has passed pre-deletion verification. + +| Current path | Proposed action | Replacement owner or unique content to migrate | Approval status | +| --- | --- | --- | --- | +| `README.md` | Rewrite in place | Lead with boundary-friendly error data, then the familiar Result shape; keep the manual propagation cost and Effect escape hatch | Approved | +| `docs/index.mdx` | Rewrite in place | Target site map | Included | +| `docs/getting-started/installation.mdx` | Build replacement, then retire old path | `docs/start/installation.mdx` | Approved after proof | +| `docs/getting-started/quick-start.mdx` | Build replacement, then retire old path | `docs/start/quick-start.mdx` from canonical example | Approved after proof | +| `docs/migration/from-try-catch.mdx` | Build replacement, then retire old path | `docs/start/migrating-from-try-catch.mdx`; keep surgical adoption flow | Approved after proof | +| `docs/core/result-pattern.mdx` | Split, then delete | Result guide, `reference/result`, `decisions/result-shape` | Approved after proof | +| `docs/core/error-system.mdx` | Split, then delete | Error guide, `reference/error`, `decisions/error-contract` | Approved after proof | +| `docs/core/brand-types.mdx` | Split, then delete | `reference/brand`, validation integration, brand decision | Approved after proof | +| `docs/patterns/optional-keys.mdx` | Merge, then delete | `guides/defining-error-vocabularies` | Approved after proof | +| `docs/patterns/real-world.mdx` | Merge, then delete | Approved examples in `guides/service-boundaries` | Approved after proof | +| `docs/patterns/service-layer.mdx` | Merge, then delete | `guides/service-boundaries` | Approved after proof | +| `docs/integrations/tanstack-query.mdx` | Rewrite in place | Current TanStack page contract | Included | +| `docs/integrations/testing.mdx` | Replace, then delete | `reference/testing`; no second testing integration page | Approved after proof | +| `docs/integrations/validation-libraries.mdx` | Rewrite in place | Brand validator recipe only | Included | +| `docs/integrations/hono-serialization.mdx` | Rewrite and rename | `docs/integrations/hono.mdx` with the approved shape-versus-validation-versus-static-typing contract | Approved | +| `docs/philosophy/err-null-is-ok-null.md` | Build replacement, then retire old path | `decisions/result-shape` | Approved after proof | +| `docs/philosophy/error-api-evolution.mdx` | Merge, then delete | Unique builder-history evidence into `decisions/error-contract` | Approved after proof | +| `docs/philosophy/rust-inspiration.mdx` | Merge, then delete | Unique Rust mapping into `decisions/error-contract` | Approved after proof | +| `docs/philosophy/why-name-and-message.mdx` | Merge, then delete | `name`/`message` rationale into `decisions/error-contract` | Approved after proof | +| `docs/philosophy/brand-implementation.mdx` | Build replacement, then retire old path | `decisions/brand-representation` | Approved after proof | +| `docs/philosophy/for-the-pragmatic-fp-developer.mdx` | Merge, then delete | Distinct tradeoffs into `decisions/pragmatic-tradeoffs` | Approved after proof | +| `docs/philosophy/from-effect-to-pragmatic-errors.mdx` | Merge, then delete | Distinct TypeScript constraints into `decisions/pragmatic-tradeoffs` | Approved after proof | +| `docs/philosophy/production-reliability.mdx` | Delete | No unsupported metric migrates; grounded boundary examples move to guides | Approved after proof | +| `docs/philosophy/developer-experience.mdx` | Delete after unique audit | Any grounded editor/testing behavior moves to guide or reference | Approved after proof | +| `docs/philosophy/design-principles.mdx` | Delete after unique audit | Approved concise principles move to README or tradeoff decision | Approved after proof | +| `src/README.md` | Delete after migration | No public tutorial remains in source root | Approved after proof | +| `src/error/README.md` | Delete after migration | Unique rules move to error guide/reference | Approved after proof | +| `src/query/README.md` | Delete after migration | Current two-family guidance moves to query integration/reference | Approved after proof | +| `skills/*/SKILL.md` | Rewrite in place | Agent-specific condensation of current examples and reference facts | Included | +| Historical `specs/*` and `specs/launch/*` | No deletion in this pass yet | Produce a separate path-by-path cleanup proposal after docs cutover | Deferred | + +Deletion happens only from the maintainer-approved allowlist, after the replacement path is linked, compiled, and verified while the old files still exist on disk. + +## Factual Corrections That Land Regardless of Messaging + +1. State that root imports are unsupported, not merely bad for tree shaking. +2. Document exactly nine current subpaths and current 0.44.0 names. +3. Replace old `queryOptions` and `mutationOptions` guidance with `resultQueryOptions` and `resultMutationOptions`, except in explicitly historical changelog text. +4. Document `defineKeys`, including its factory-return literal-widening caveat, and the difference between client-agnostic options adapters and `QueryClient`-bound factories. +5. State that a `defineErrors` variant returns an `Err` wrapper; the tagged body is under `.error`. +6. State that JSON serializability is a convention. Define exact preservation recursively over JSON data: `null`, booleans, strings, finite numbers other than negative zero when numeric identity matters, dense array elements with no extra properties, and own enumerable string-keyed fields on plain objects with the standard Object prototype. `NaN`, infinities, negative zero, sparse holes, extra array properties, symbol or non-enumerable fields, `undefined`, functions, `Date`, native `Error`, `bigint`, class instances, null-prototype objects, and cyclic graphs do not satisfy an exact round-trip promise. The exported `JsonValue` type is broader because TypeScript cannot exclude values such as `NaN`. JSON semantic normalization can be described separately from exact JavaScript preservation. +7. Remove claims that `Date`, functions, native `Error`, or other fields are rejected by `defineErrors` types. Only `message: string` and the reserved `name` behavior are enforced. +8. State that error objects are shallow-frozen, not deeply immutable. +9. No generic discriminator can distinguish `Ok(null)` from permitted `Err(null)` because they are structurally identical. Under the documented non-null-error convention, use `error !== null` or `isErr(result)`. `if (error)` requires the stronger truthy-error convention and fails for `null`, `undefined`, `false`, `0`, `""`, and `NaN`, all permitted by the current `Err` types. Preserve `Ok(null)` as valid and reject the null-unsafe custom matcher. +10. Teach `expectOk` and `expectErr` as the official test helpers. +11. Stop saying the whole package or Result surface never throws. `unwrap`, `resolve`, test assertions, and query adapters intentionally cross into throwing contracts. +12. Replace the unsound generic `parseJson` tutorial with `wellcrafted/json` returning `JsonValue`, followed by validation or narrowing. +13. Separate JSON shape preservation from static end-to-end HTTP typing and runtime validation. +14. Use Bun for contributor commands and remove the nonexistent `dev` command. +15. Establish one supported TypeScript, module-resolution, standard-library, and runtime matrix from consumer tests. Do not preserve the current 5.0 versus 4.5 conflict. +16. Document any TanStack type prerequisite for `wellcrafted/query`; do not hide the current undeclared type dependency. +17. Fix Mintlify scripts to run in `docs/`, use a supported Node runtime, and remove the invalid component import. +18. `package.json#files` includes `LICENSE`, but the repository file is missing and the published tarball omits it. Adding the file or changing package metadata is a separate packaging decision. Documentation must not claim the tarball contains a license file today. + +Items 16 and 18 may reveal package metadata work. They must be reported and approved separately before changing non-documentation behavior. + +## Claims to Prove or Remove + +| Current claim | Decision | Proof required to retain it | +| --- | --- | --- | +| Full library is under 2 kB | Remove | Pinned consumer bundle scenarios, package version, bundler, minifier, target, compression command, and CI budget | +| Competitor size table | Remove | Same reproducible method for pinned versions and equivalent entry points, plus maintenance owner | +| Zero dependencies | Qualify as zero declared runtime dependencies | npm metadata check; query type prerequisite explained separately | +| Tree-shakeable, pay only for what you use | Remove or state only as subpath architecture | Reproducible consumer builds for representative imports | +| Zero unhandled exceptions, thousands of hours, production tested | Remove unless explicitly approved | Named source, pinned revision or measurement window, collection method, and permission to publish | +| Debugging time reduced from hours to minutes | Remove | Approved case study with evidence | +| 22,824 production lines or 244 importer files | Do not publish | Internal research may retain the pinned methodology, but public documentation carries no usage or vanity metric | +| All errors or Results serialize perfectly or intact | Remove | Impossible under the current `unknown` payload contract; replace with conditional wording | +| Works with any framework or runtime | Remove | Declared runtime matrix and automated consumer tests | +| Direct query execution is much faster | Remove | A benchmark that defines workload and measures the claimed difference | + +Preferred rule: if a claim needs a paragraph of methodology, link to a committed benchmark or do not put the number in the front door. + +## Allowed Change Boundary + +This is a documentation and documentation-infrastructure branch. + +Allowed paths and change types: + +- `README.md`, `CONTRIBUTING.md`, `docs/**`, `examples/**`, `skills/**`, and this spec. +- JSDoc or comments in `src/**` when a public statement is wrong. Runtime logic and signatures remain unchanged. +- Documentation validation scripts, non-mutating check scripts, and focused verification fixtures under `scripts/**`. +- `package.json` scripts, a pinned documentation-tool dev dependency, and resulting `bun.lock` changes. +- `.github/workflows/**` changes needed to run approved checks under pinned runtimes. + +Forbidden without a new approval: + +- Runtime behavior, public exports, types, signatures, or package entry points. +- Peer or runtime dependency metadata, `engines`, package file inclusion, versions, or release configuration. +- Publishing, deployment, pushing, pull-request creation, or edits in Epicenter. +- Historical spec deletion outside an explicit path-by-path cleanup approval. + +## Executable Example Strategy + +Examples are consumer-shaped code with package subpath imports. They must compile against the built package, not internal source paths. + +1. `examples/quick-start.ts` runs without network access and demonstrates both success and failure. +2. `examples/service-boundary.ts` defines a closed error vocabulary, wraps only a throwing operation, and manually propagates one error. +3. `examples/serialization-boundary.ts` demonstrates the valid JSON-compatible case and the `cause` caveat without claiming type enforcement. +4. `examples/tanstack-query.ts` typechecks both direct adapters and factory-returned handles against the current API. + +The root package self-reference may power the learning files after `bun run build`, but it is not package-consumer proof: root dev dependencies and `skipLibCheck` can hide published-package defects. A dedicated examples tsconfig uses `skipLibCheck: false`. At least the quick start and service example run under Bun. + +Package proof uses a separate isolated temporary consumer: + +1. Pack the built package with `bun pm pack`. +2. Create a temporary project outside the repository package boundary. +3. Install the tarball plus only the explicit dependency required by that fixture. +4. Typecheck with `skipLibCheck: false` under each approved TypeScript and module-resolution configuration. +5. Import all nine subpaths, assert that the root is unsupported, and invoke runtime-sensitive APIs such as `partitionResults` and `composeSinks`. +6. Test `wellcrafted/query` once without `@tanstack/query-core` to document the current failure and once with the pinned compatible version to prove the supported path. + +Runtime smoke jobs execute affected APIs under each runtime version the installation page promises. A typecheck alone does not establish runtime compatibility. + +Documentation code has two classes: + +- **Canonical checked examples**: included from, extracted from, or mechanically compared with `examples/` files. These compile and, where practical, run in CI. +- **Illustrative snippets**: explicitly marked as partial. They remain short and contain no fake imports or API names. They do not become a CI promise until a prototype proves a reliable extraction mechanism. + +The implementation may choose checked inclusion or extraction after testing Mintlify's supported syntax. It must not keep manually duplicated long examples with no drift check. + +## Verification and CI Strategy + +### Repository scripts + +First add or repair baseline-green scripts: + +- `lint:check`: run Biome lint without writes. +- `format:check`: run Biome formatting checks without writes. +- `docs:dev`: run the pinned local Mint binary from `docs/` with a documented Node 24 requirement. +- `docs:validate`: run the pinned local Mint validator from `docs/`. +- `docs:links`: run the pinned local Mint link checker from `docs/`. +- `docs:examples`: build the package, typecheck canonical examples, and run executable examples. +- `package:smoke`: verify the packed tarball and all nine subpaths from an isolated consumer. +- `compat:types`: run the approved TypeScript, module-resolution, standard-library, and TanStack fixtures with `skipLibCheck: false`. +- `compat:runtime`: invoke runtime-sensitive APIs under each runtime version promised in installation. + +Pin the Mintlify CLI version rather than letting `bunx` silently change validation behavior. + +Strict content gates are designed and enabled only after the canonical content exists: + +- `docs:exports`: derive `(subpath, export kind, symbol)` tuples from the manifest and emitted declarations, then compare them with machine-readable markers on exactly one reference page. Searching prose does not count as coverage. +- `docs:claims`: scan `README.md`, `CONTRIBUTING.md`, `docs/`, `examples/`, and `skills/`; exclude `CHANGELOG.md`, historical specs, and explicitly marked historical quotations. Reject a documented list of retired names, unsupported root imports, conflicting requirements, npm contributor commands, serialization absolutes, unsupported metrics, and stale links. +- `docs:snippets`: enable only after an include, extraction, or comparison prototype passes against the intended Markdown and MDX corpus. Canonical example drift is the first required case. + +Retained legacy files may have explicit temporary exclusions through the pre-deletion proof. The deletion wave removes each exclusion atomically with its approved legacy file, then reruns the strict gates. + +### CI gates + +The main workflow should run: + +```text +bun install --frozen-lockfile +bun run lint:check +bun run format:check +bun run typecheck +bun run build +bun test +bun run docs:examples +bun run package:smoke +bun run compat:types +bun run compat:runtime +bun run docs:exports after its canonical contract is green +bun run docs:claims after its canonical contract is green +bun run docs:snippets after its extraction prototype is green +bun run docs:validate in a Node 24 job with the pinned local Mint binary +bun run docs:links in a Node 24 job with the pinned local Mint binary +``` + +The workflow must explicitly install Node 24 before invoking Mint. Runtime smoke jobs install the runtime they claim to cover and invoke the relevant APIs, not just import them. + +### Manual acceptance + +1. Render the Mintlify site under a supported local runtime. +2. Inspect desktop and narrow-width navigation, code blocks, tables, cards, and overflow. +3. Follow the README to installation, runnable quick start, tradeoff, guides, and all nine reference pages. +4. Run a first-reader review with no prior conversation context. +5. Run a skeptical API review against exports, emitted declarations, tests, and the npm tarball. +6. Run a claim review that tries to disprove every numeric, compatibility, production, serialization, and competitor statement. +7. Finish with `git diff --check`, status review, link validation, and post-implementation review. + +No validation is reported as passed unless the command actually ran. Environment blockers name the exact runtime and error. + +## Incremental Documentation Waves + +Commits are created only after explicit approval. If approved, each wave is one atomic conventional commit and leaves the branch reviewable. + +### Wave 0: Resolve positioning and exact scope + +- [x] Resolve the Wave 0 decisions below. +- [x] Approve allowed file paths, optional source-comment changes, tooling changes, and the exact documentation deletion allowlist. +- [x] Record decisions in this spec. + +### Wave 1: Make baseline verification green + +- [ ] Repair and pin Mintlify validation. +- [ ] Run Mint commands from `docs/` under an explicit Node 24 job. +- [ ] Add non-mutating lint and format checks. +- [ ] Fix the current site validation warning. +- [ ] Commit only when the existing content passes these baseline checks. + +### Wave 2: Add examples and isolated package proof + +- [ ] Add the four canonical learning examples. +- [ ] Add strict example typechecking and runnable offline examples. +- [ ] Add isolated packed-consumer, compatibility-type, and runtime fixtures. +- [ ] Wire only checks that already pass into CI. + +### Wave 3: Replace the front door and start path + +- [ ] Rewrite README around the approved product sentence and runnable example. +- [ ] Rewrite site index as navigation, not duplicate positioning. +- [ ] Create new installation, quick start, and migration paths while leaving their old files on disk and out of the new navigation until cutover. +- [ ] Add `CONTRIBUTING.md` with Bun, checks, docs workflow, changesets, and PR expectations. + +### Wave 4: Build the guide path + +- [ ] Add error-vocabulary, Result-composition, service-boundary, and serialization-boundary guides. +- [ ] Ground examples in approved Epicenter patterns. +- [ ] Keep exact signatures out of guides unless needed for the task. + +### Wave 5: Establish complete reference ownership + +- [ ] Add exactly one reference page for each of nine subpaths. +- [ ] Cover every current runtime and type export. +- [ ] Align JSDoc edge cases and examples with the same facts. +- [ ] Report public-looking implementation exports as deferred API questions. + +### Wave 6: Rebuild integrations and agent skills + +- [ ] Rewrite TanStack Query around the current two-family model and `defineKeys`. +- [ ] Put official testing usage in `reference/testing`; do not create a duplicate testing integration page. +- [ ] Rewrite Hono as the approved HTTP boundary guide with distinct shape, runtime-validation, and static-typing contracts; keep validation focused on brand validators. +- [ ] Reconcile all five distributable skills against current exports and canonical examples. + +### Wave 7: Consolidate decisions + +- [ ] Preserve only durable rationale and honest tradeoffs. +- [ ] Create consolidated Result, error-contract, tradeoff, and brand decisions while leaving their old source pages on disk until deletion. +- [ ] Remove unsupported production or marketing claims. + +### Wave 8: Prove and enable strict content gates + +- [ ] Prototype machine-readable export markers, exact claims roots/exclusions, and canonical snippet comparison. +- [ ] Make each strict gate pass against canonical content. +- [ ] Add explicit temporary exclusions only for retained legacy files. +- [ ] Enable the passing gates in CI. + +### Wave 9: Cut over while the old path remains + +- [ ] Rewire navigation and every inbound link to the new owners. +- [ ] Confirm old pages and source READMEs have no remaining unique content. +- [ ] Remove current-path dependence on old files while leaving them on disk. +- [ ] Commit the cutover separately. + +### Wave 10: Record the pre-deletion proof checkpoint + +- [ ] Run full type, test, build, packed-package, compatibility, runtime, docs, examples, exports, claims, snippets, and visual checks while old files remain. +- [ ] Record exact commands, versions, and results in this spec. +- [ ] Commit the proof checkpoint before deleting anything. + +### Wave 11: Delete only approved obsolete owners + +- [ ] Delete only paths in the Wave 0 allowlist. +- [ ] Remove every temporary legacy exclusion. +- [ ] Sweep for stale names, imports, claims, and links. +- [ ] Rerun the full suite before committing the deletion, so the deletion commit is independently green. + +### Wave 12: Independent final review + +- [ ] Run fresh-context first-reader review. +- [ ] Run skeptical API and claims review. +- [ ] Incorporate grounded findings or record why they were rejected. +- [ ] Add the review summary and final proof results to this spec. +- [ ] Stop on a local PR-ready branch. Do not push, publish, deploy, or open a PR without permission. + +## Edge Cases and Deferred API Questions + +### Public exports that look internal + +The documentation must not erase names that are currently importable. It should describe them honestly and raise a separate API proposal if the maintainer wants to remove them before 1.0. + +### Query type dependency + +Published query declarations import `@tanstack/query-core`, but package metadata lists it only as a dev dependency. Documentation can state the current prerequisite. Adding a peer dependency is package behavior and requires separate approval. + +### Runtime compatibility + +`Object.groupBy`, `AsyncDisposable`, and `Symbol.asyncDispose` make broad runtime and TypeScript promises unsafe. The pass must establish a tested matrix or narrow the docs. Polyfills and public implementation changes are out of scope. + +### Serialization boundaries + +The container shape can be plain while a payload is not JSON-compatible. Guides must distinguish container, tagged body, payload fields, raw cause, wire validation, and static typing. + +### Historical changelog names + +Old query and error API names remain valid in release history. Claims sweeps must ignore explicitly historical changelog sections while rejecting those names in current guidance. + +### Documentation-only changesets + +Whether documentation-only waves need changesets is a contributor policy decision. `CONTRIBUTING.md` must state one rule rather than copying stale guidance. + +## Wave 0 Decisions + +Wave 0 was approved on 2026-07-10 with one change to the proposed positioning. These decisions now govern implementation. + +1. **Positioning, reader, and brand voice** + - Lead with errors as plain, boundary-friendly data that can move through JSON, HTTP, workers, IPC, logs, and UI when every field is JSON-compatible. + - Present `{ data, error }` as the familiar, ergonomic Result shape for returning and handling that data with ordinary TypeScript. + - Write for TypeScript application authors. Use lowercase `wellcrafted` everywhere except historical quotations or case-sensitive identifiers. + - Keep “Typed failures without a new programming model” available as secondary copy. Effect remains a closing tradeoff, not the lead comparison. + - Keep the limitation explicit: serializability is a convention, not a type constraint, and arbitrary fields do not serialize perfectly. Do not imply that `defineErrors` enforces JSON-safe fields. + - If JSON-safe error fields would require a public API or type change, stop and raise a separate proposal. Do not expand this documentation pass. + +2. **Product prominence and public production evidence** + - Use the approved narrative roles: `result` and `error` define the product story; the other seven subpaths support or integrate with that contract as specified above. + - Use adapted, attributed Epicenter examples in public documentation. The approved source patterns include `transcribe` for service flow, `table` for `Ok(null)` and domain variants, `blob-errors` for Hono, and the account popover for current query adapters. + - Do not publish importer counts, production-line counts, reliability claims, or other vanity metrics. Internal research numbers may remain in this spec as evidence, but they are not public copy. + - Keep agent skills as a secondary convenience, not a headline differentiator. + +3. **Information architecture and exact deletion allowlist** + - The target information architecture and page contracts are approved. + - Keep `docs/integrations/hono.mdx` as a careful HTTP boundary guide. It must distinguish preserving a JSON shape from runtime validation and end-to-end static typing; none implies either of the others. + - All rows marked “Approved after proof” in the disposition table are the exact deletion allowlist. Delete them only after replacement, cutover, and the recorded pre-deletion verification pass. + - Keep historical spec deletion deferred to a separate path-by-path cleanup. + +### Implementation note: 2026-07-10 + +Wave 0 is complete and the spec is now in progress. Implementation begins with Wave 1's baseline verification work. This approval does not authorize runtime or public API changes, including JSON-safe field enforcement, and it does not bypass the verify-before-delete sequence. + +## Deferred Decisions + +These decisions do not block the first documentation waves. + +1. **Compatibility language before 1.0** + - First run the isolated consumer and runtime matrix. Then choose the exact tested promise. + - Recommendation after evidence: document the tested current matrix and say pre-1.0 minor releases may contain breaking changes described by changesets and changelog. + +2. **Low-level exported types** + - Document every current export in this pass. + - Consider a separate API cleanup for `ErrorBody`, `ErrorsConfig`, `ValidatedConfig`, `DefineErrorsReturn`, and `FAILURES`. + +3. **Package metadata issues** + - Keep `@tanstack/query-core` peer metadata, `engines`, and the missing repository `LICENSE` file out of this branch unless scope is explicitly expanded. + +4. **Documentation-only changesets** + - Resolve before finalizing `CONTRIBUTING.md`; do not let it block positioning or examples. + +5. **Historical spec cleanup** + - Produce a separate exact allowlist after the public documentation cutover. Do not delete broad categories from this branch. + +## Success Criteria + +- [ ] A reader in the approved primary audience can install wellcrafted and run a complete example. +- [ ] The README states what wellcrafted is, who it is for, what it costs, and when to use something larger. +- [ ] Every published subpath has exactly one reference page. +- [ ] Every current `(subpath, export kind, symbol)` tuple appears in exactly one reference page's machine-readable coverage. +- [ ] Guides, references, integrations, decisions, JSDoc, examples, and skills each have one distinct ownership role. +- [ ] Serialization language matches the actual convention, defined preservation model, and positive/negative fixtures. +- [ ] Query and testing docs match v0.44.0 behavior and names. +- [ ] Consumer setup and contributor setup are separate and correct. +- [ ] Importer counts, reliability claims, and other vanity metrics are absent from public docs; any remaining numeric, competitor, production, performance, or compatibility claim is reproducible or removed. +- [ ] Canonical examples compile, isolated packed consumers pass strict typechecks, and runtime-sensitive fixtures execute under every promised runtime. +- [ ] Docs validation, links, examples, package smoke, compatibility, export coverage, claims, and proven snippet checks run in CI. +- [ ] Desktop and narrow-width site review has no blocking navigation, layout, or code issues. +- [ ] First-reader, API, and claims findings are resolved or explicitly recorded with evidence for rejection. +- [ ] `bun test`, `bun run typecheck`, `bun run build`, non-mutating lint and format checks, docs checks, and `git diff --check` pass. +- [ ] The final local branch is PR-ready with no unrelated or unapproved changes. + +## References + +- `AGENTS.md`: repository policy and Bun commands. +- `package.json`: published exports, scripts, package metadata, and version. +- `tsdown.config.ts`: build entry points and target. +- `README.md`: strongest current product narrative. +- `docs/docs.json`: current site navigation. +- `src/error/types.ts`: serializability is a convention, not a type constraint. +- `src/result/result.ts`: Result shape, discriminants, `Ok(null)`, throwing adapters. +- `src/query/utils.ts`: current two-family query behavior. +- `src/testing.ts`: official test helpers. +- `CHANGELOG.md`: release and rename history. +- `skills/`: distributable agent guidance. +- PR #133: `https://github.com/wellcrafted-dev/wellcrafted/pull/133`. +- Epicenter representative files listed under [Real consumers](#real-consumers). + +## Review + +Three independent draft reviews challenged the first version from first-reader, API/claims, and execution-order perspectives. Material findings were incorporated: + +- Replaced overlapping ownership with factual-source versus reader-facing-projection rules and machine checks. +- Added concept-level current ownership, exact page contracts, and a path-by-path deletion allowlist. +- Recast the product sentence, audience, and narrative roles as explicit maintainer choices rather than conclusions proved by import counts. +- Split true Wave 0 gates from compatibility, low-level export, metadata, changeset, and historical-spec decisions that can wait for evidence. +- Made baseline verification green before strict content gates and separated cutover, pre-deletion proof, deletion, and post-deletion proof. +- Replaced root self-reference as package proof with an isolated packed consumer, strict type fixtures, and runtime-sensitive smoke jobs. +- Defined non-mutating lint and format gates, explicit Node 24 Mint jobs, and machine-readable export/claims/snippet contracts that must be prototyped before CI enforcement. +- Corrected Result discrimination for `Err(null)` and all falsy errors, recursive JSON preservation limits, `defineKeys` factory inference, runtime compatibility, root-import error wording, export coverage keys, dated external-tool observations, and the missing repository license file. + +The final draft spot checks returned “clear for maintainer decision.” Wave 0 is now recorded; content implementation has not started. No files are staged or committed. From b115fda01e8adf39d912b60346f77aaa3d62c1ea Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:13:12 -0700 Subject: [PATCH 02/13] chore(docs): make baseline verification reproducible Pin the Mint CLI, run documentation checks from the site directory under Node 24, and add non-mutating lint and format gates. Apply the minimal existing formatting fixes required for the repository-wide format check. --- .github/workflows/main.yml | 21 +- bun.lock | 2036 ++++++++++++++++- docs/docs.json | 4 +- docs/index.mdx | 2 - package.json | 9 +- skills-lock.json | 206 +- ...10T012026-greenfield-documentation-pass.md | 19 +- src/result/result.test.ts | 4 +- 8 files changed, 2071 insertions(+), 230 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ce7829..207e05b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,26 @@ jobs: echo "No pre-1.0 major changesets found." - run: bun install --frozen-lockfile - - run: bun run lint + env: + PUPPETEER_SKIP_DOWNLOAD: "true" + - run: bun run lint:check + - run: bun run format:check - run: bun run typecheck - run: bun run build - run: bun test + + docs: + name: Docs (Node 24) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v6 + with: + node-version: 24 + package-manager-cache: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + env: + PUPPETEER_SKIP_DOWNLOAD: "true" + - run: bun run docs:validate + - run: bun run docs:links diff --git a/bun.lock b/bun.lock index 53d91e6..5d268e5 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "wellcrafted", @@ -10,6 +9,7 @@ "@tanstack/query-core": "^5.82.0", "@types/bun": "^1.3.5", "arktype": "^2.1.29", + "mint": "4.2.684", "tsdown": "^0.12.5", "typescript": "^5.8.3", "valibot": "^1.2.0", @@ -18,10 +18,20 @@ }, }, "packages": { + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="], "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], + "@asyncapi/parser": ["@asyncapi/parser@3.4.0", "", { "dependencies": { "@asyncapi/specs": "^6.8.0", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.0.0", "node-fetch": "2.6.7" } }, "sha512-Sxn74oHiZSU6+cVeZy62iPZMFMvKp4jupMFHelSICCMw1qELmUHPvuZSr+ZHDmNGgHcEpzJM5HN02kR7T4g+PQ=="], + + "@asyncapi/specs": ["@asyncapi/specs@6.8.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "7.28.0", "@babel/types": "7.28.0", "@jridgewell/gen-mapping": "0.3.12", "@jridgewell/trace-mapping": "0.3.29", "jsesc": "3.1.0" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -52,6 +62,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-klJKPPQvUk9Rlp0Dd56gQw/+Wt6uUprHdHWtbDC93f3Iv+knA2tLWpcYoOZJgPV+9s+RBmYv0DGy4mUlr20esg=="], + "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.12", "", { "dependencies": { "@changesets/config": "3.1.1", "@changesets/get-version-range-type": "0.4.0", "@changesets/git": "3.0.4", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "detect-indent": "6.1.0", "fs-extra": "7.0.1", "lodash.startcase": "4.4.0", "outdent": "0.5.0", "prettier": "2.8.8", "resolve-from": "5.0.0", "semver": "7.7.2" } }, "sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.3", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "semver": "7.7.2" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], @@ -92,6 +104,86 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], + + "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], + + "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], + + "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], + + "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.4", "@jridgewell/trace-mapping": "0.3.29" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], @@ -100,10 +192,42 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.4" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], + "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="], + + "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="], + + "@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="], + + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "7.27.6", "@types/node": "12.20.55", "find-up": "4.1.0", "fs-extra": "8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "7.27.6", "@changesets/types": "4.1.0", "@manypkg/find-root": "1.1.0", "fs-extra": "8.1.0", "globby": "11.1.0", "read-yaml-file": "1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + + "@mintlify/cli": ["@mintlify/cli@4.0.1287", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.1002", "@mintlify/link-rot": "3.0.1190", "@mintlify/models": "0.0.335", "@mintlify/prebuild": "1.0.1149", "@mintlify/previewing": "4.0.1215", "@mintlify/validation": "0.1.778", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.2.0", "open": "8.4.2", "openid-client": "6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "4.3.6" }, "optionalDependencies": { "keytar": "7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-IYHBC9AUaYFpRFGAfkpiieiwrp/AboC6lyaxWKhrs0iehWkuVnvzO0zhKtDBkt2afYoVW2LjDwpn1cXci5Z09w=="], + + "@mintlify/common": ["@mintlify/common@1.0.1002", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.335", "@mintlify/openapi-parser": "0.0.8", "@mintlify/validation": "0.1.778", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.1", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.14", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "3.34.0", "tailwindcss-v3": "npm:tailwindcss@3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-qocemWvcolNTtudriyO/V22QVF8kN5zjYB7Zhuwx28PTzTEccq3ksp/Glut5/4+fmz8gkoBAjNieGAXFBQEu/Q=="], + + "@mintlify/link-rot": ["@mintlify/link-rot@3.0.1190", "", { "dependencies": { "@mintlify/common": "1.0.1002", "@mintlify/models": "0.0.335", "@mintlify/prebuild": "1.0.1149", "@mintlify/previewing": "4.0.1215", "@mintlify/scraping": "4.0.867", "@mintlify/validation": "0.1.778", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-3Q3w0NahIMy/T7OwJOOekeIMVEJ/R0BrYQhd4lqjgB7sAINv2PVOaRz5IEBT+6XnJydUblgoddDfotpg4kDNrA=="], + + "@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="], + + "@mintlify/models": ["@mintlify/models@0.0.335", "", { "dependencies": { "axios": "1.16.1", "openapi-types": "12.1.3" } }, "sha512-LYGD1y8y8wVurrKLHg6p1tgHta4dcmzeb/ZoRI9U+rBEIGvQaHoAMmV29EvuVZpJ7IKFnLoRKeCU1CkXpEuIVQ=="], + + "@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="], + + "@mintlify/prebuild": ["@mintlify/prebuild@1.0.1149", "", { "dependencies": { "@mintlify/common": "1.0.1002", "@mintlify/openapi-parser": "0.0.8", "@mintlify/scraping": "4.0.867", "@mintlify/validation": "0.1.778", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.1" } }, "sha512-eXKMUT+icscJW3H+xDxclACuknxRlf7Adgu60GckhoScT2s/eIHEUlvk7q33b1P1PtgSux67b6NekcU6ctl+aA=="], + + "@mintlify/previewing": ["@mintlify/previewing@4.0.1215", "", { "dependencies": { "@mintlify/common": "1.0.1002", "@mintlify/prebuild": "1.0.1149", "@mintlify/validation": "0.1.778", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.22.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.8.0", "tar": "7.5.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-FKJZFih+nBRyhPmCQvbam3iNiWYeQxrGiqYoT297CNdhEhH+EVcKKj1VyO2AQWHWwpdCxUVh/lwJAocNONNkFg=="], + + "@mintlify/scraping": ["@mintlify/scraping@4.0.867", "", { "dependencies": { "@mintlify/common": "1.0.1002", "@mintlify/openapi-parser": "0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "24.3.1", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-SraLj5iaaHYFMga3g+RFT2w31E5+fnIFzWvhrRnYfQJ6m33ke0QqqDWTzszdNRP8pXJnVfCT5eXDKrIZhxsq/g=="], + + "@mintlify/validation": ["@mintlify/validation@0.1.778", "", { "dependencies": { "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.335", "arktype": "2.1.27", "fractional-indexing": "3.2.0", "js-yaml": "4.1.1", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.1", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-019mUJ2nesAZlHKrm6tUPmEprG4wjhaWRN+VgQmq8a/tb0sO7HwkFGtpMncNwVd8dmfbBPGDoU7KdoS7uYLixw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "1.4.4", "@emnapi/runtime": "1.4.4", "@tybys/wasm-util": "0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -112,12 +236,60 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "1.19.1" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@openapi-contrib/openapi-schema-to-json-schema": ["@openapi-contrib/openapi-schema-to-json-schema@3.2.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" } }, "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.75.1", "", {}, "sha512-UH07DRi7xXqAsJ/sFbJJg0liIXnapB6P5uADXIiF1s6WQjZzcTIkKHca0s522QVxmijPxVX5ijCYxSr7eSq5CQ=="], "@oxc-project/types": ["@oxc-project/types@0.75.1", "", {}, "sha512-7ZJy+51qWpZRvynaQUezeYfjCtaSdiXIWFUZIlOuTSfDXpXqnSl/m1IUPLx6XrOy6s0SFv3CLE14vcZy63bz7g=="], + "@posthog/core": ["@posthog/core@1.7.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA=="], + + "@puppeteer/browsers": ["@puppeteer/browsers@2.7.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.0", "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ=="], + "@quansync/fs": ["@quansync/fs@0.1.3", "", { "dependencies": { "quansync": "0.2.10" } }, "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.3", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gE4HGjIioZaMGZupq2zQQdqhlRV2b2qnjFHHkJEW50zVDmiVNWwdHjwvZDPx9JfW5y4GuHgp/zKDLZZbJlQ1/Q=="], "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-h2HfOtqmjIHIz9WdpKAJ8sBfLNGkrMlwrCfNV2MDDGu0x3YdYBYPE+ozS5PvE53Tp8y6EYn2/thNWJTGWy/N3Q=="], @@ -144,234 +316,1874 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.24", "", {}, "sha512-NMiim/enJlffMP16IanVj1ajFNEg8SaMEYyxyYfJoEyt5EiFT3HUH/T2GRdeStNWp+/kg5U8DiJqnQBgLQ8uCw=="], + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + + "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + + "@shikijs/twoslash": ["@shikijs/twoslash@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0", "twoslash": "^0.3.6" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-pNaLJWMA3LU7PhT8tm9OQBZ1epy0jmdgeJzntBtr1EVXLbHxGzTj3mnf9vOdcl84l96qnlJXkJ/NGXZYBpXl5g=="], + + "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + + "@sindresorhus/slugify": ["@sindresorhus/slugify@2.2.0", "", { "dependencies": { "@sindresorhus/transliterate": "^1.0.0", "escape-string-regexp": "^5.0.0" } }, "sha512-9Vybc/qX8Kj6pxJaapjkFbiUJPk7MAkCh/GFCxIBnnsuYCFPIXKvnLidG8xlepht3i24L5XemUmGtrJ3UWrl6w=="], + + "@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + + "@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="], + + "@stoplight/json": ["@stoplight/json@3.21.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "jsonc-parser": "~2.2.1", "lodash": "^4.17.21", "safe-stable-stringify": "^1.1" } }, "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g=="], + + "@stoplight/json-ref-readers": ["@stoplight/json-ref-readers@1.2.2", "", { "dependencies": { "node-fetch": "^2.6.0", "tslib": "^1.14.1" } }, "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ=="], + + "@stoplight/json-ref-resolver": ["@stoplight/json-ref-resolver@3.1.6", "", { "dependencies": { "@stoplight/json": "^3.21.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0 || ^13.0.0", "@types/urijs": "^1.19.19", "dependency-graph": "~0.11.0", "fast-memoize": "^2.5.2", "immer": "^9.0.6", "lodash": "^4.17.21", "tslib": "^2.6.0", "urijs": "^1.19.11" } }, "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A=="], + + "@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="], + + "@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="], + + "@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.1", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ=="], + + "@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.23.0", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA=="], + + "@stoplight/spectral-functions": ["@stoplight/spectral-functions@1.10.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "^3.17.1", "@stoplight/spectral-core": "^1.23.0", "@stoplight/spectral-formats": "^1.8.1", "@stoplight/spectral-runtime": "^1.1.2", "ajv": "^8.18.0", "ajv-draft-04": "~1.0.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "lodash": "^4.18.1", "tslib": "^2.8.1" } }, "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA=="], + + "@stoplight/spectral-parsers": ["@stoplight/spectral-parsers@1.0.5", "", { "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/types": "^14.1.1", "@stoplight/yaml": "~4.3.0", "tslib": "^2.8.1" } }, "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ=="], + + "@stoplight/spectral-ref-resolver": ["@stoplight/spectral-ref-resolver@1.0.5", "", { "dependencies": { "@stoplight/json-ref-readers": "1.2.2", "@stoplight/json-ref-resolver": "~3.1.6", "@stoplight/spectral-runtime": "^1.1.2", "dependency-graph": "0.11.0", "tslib": "^2.8.1" } }, "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA=="], + + "@stoplight/spectral-runtime": ["@stoplight/spectral-runtime@1.1.6", "", { "dependencies": { "@stoplight/json": "^3.20.1", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "lodash": "^4.18.1", "node-fetch": "^2.7.0", "tslib": "^2.8.1" } }, "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg=="], + + "@stoplight/types": ["@stoplight/types@13.20.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA=="], + + "@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="], + + "@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="], + + "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.82.0", "", {}, "sha512-JrjoVuaajBQtnoWSg8iaPHaT4mW73lK2t+exxHNOSMqy0+13eKLqJgTKXKImLejQIfdAHQ6Un0njEhOvUtOd5w=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + "@types/acorn": ["@types/acorn@4.0.6", "", { "dependencies": { "@types/estree": "*" } }, "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/es-aggregate-error": ["@types/es-aggregate-error@1.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], + "@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="], + "@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], + "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], - "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], - "ast-kit": ["ast-kit@2.1.1", "", { "dependencies": { "@babel/parser": "7.28.0", "pathe": "2.0.3" } }, "sha512-mfh6a7gKXE8pDlxTvqIc/syH/P3RkzbOF6LeHdcKztLEzYe6IMsRCL7N8vI7hqTGWNxpkCuuRTpT21xNWqhRtQ=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "1.0.2" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "acorn": ["acorn@8.11.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w=="], - "birpc": ["birpc@2.4.0", "", {}, "sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "4.1.2" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "3.1.1", "shebang-command": "2.0.0", "which": "2.0.2" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "ajv-errors": ["ajv-errors@3.0.0", "", { "peerDependencies": { "ajv": "^8.0.1" } }, "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ=="], - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "dts-resolver": ["dts-resolver@2.1.1", "", {}, "sha512-3BiGFhB6mj5Kv+W2vdJseQUYW+SKVzAFJL6YNP6ursbrwy1fXHRotfHi3xLNxe4wZl/K8qbAFeCDjZLjzqxxRw=="], + "ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "4.1.3", "strip-ansi": "6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "0.7.0", "iconv-lite": "0.4.24", "tmp": "0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "@nodelib/fs.walk": "1.2.8", "glob-parent": "5.1.2", "merge2": "1.4.1", "micromatch": "4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "1.1.0" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], - "fdir": ["fdir@6.4.6", "", { "optionalDependencies": { "picomatch": "4.0.2" } }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], - "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], - "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "ast-kit": ["ast-kit@2.1.1", "", { "dependencies": { "@babel/parser": "7.28.0", "pathe": "2.0.3" } }, "sha512-mfh6a7gKXE8pDlxTvqIc/syH/P3RkzbOF6LeHdcKztLEzYe6IMsRCL7N8vI7hqTGWNxpkCuuRTpT21xNWqhRtQ=="], - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "2.1.0", "dir-glob": "3.0.1", "fast-glob": "3.3.3", "ignore": "5.3.2", "merge2": "1.4.1", "slash": "3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - "human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], - "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "bare-events": ["bare-events@2.9.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg=="], - "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + "bare-fs": ["bare-fs@4.7.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ=="], - "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + "bare-path": ["bare-path@3.1.1", "", {}, "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "bare-stream": ["bare-stream@2.13.3", "", { "dependencies": { "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ=="], - "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "bare-url": ["bare-url@2.4.5", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "3.0.3", "picomatch": "2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "1.0.2" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + "birpc": ["birpc@2.4.0", "", {}, "sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg=="], - "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "2.1.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], - "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "0.2.10" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "js-yaml": "3.14.1", "pify": "4.0.1", "strip-bom": "3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + "chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "rolldown": ["rolldown@1.0.0-beta.24", "", { "dependencies": { "@oxc-project/runtime": "0.75.1", "@oxc-project/types": "0.75.1", "@rolldown/pluginutils": "1.0.0-beta.24", "ansis": "4.1.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.24", "@rolldown/binding-darwin-x64": "1.0.0-beta.24", "@rolldown/binding-freebsd-x64": "1.0.0-beta.24", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.24", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.24", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.24", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.24", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.24", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.24", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.24", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.24", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.24" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-eDyipoOnoHQ5p6INkJ8g31eKGlqPSCAN9PapyOTw5HET4FYIWALZnSgpMZ67mdn+xT3jAsqGidNnBcIM6EAUhA=="], + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], - "rolldown-plugin-dts": ["rolldown-plugin-dts@0.13.13", "", { "dependencies": { "@babel/generator": "7.28.0", "@babel/parser": "7.28.0", "@babel/types": "7.28.0", "ast-kit": "2.1.1", "birpc": "2.4.0", "debug": "4.4.1", "dts-resolver": "2.1.1", "get-tsconfig": "4.10.1" }, "optionalDependencies": { "typescript": "5.8.3" }, "peerDependencies": { "rolldown": "1.0.0-beta.24" } }, "sha512-Nchx9nQoa4IpfQ/BJzodKMvtJ3H3dT322siAJSp3uvQJ+Pi1qgEjOp7hSQwGSQRhaC5gC+9hparbWEH5oiAL9Q=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "4.1.2" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "1.2.3" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "chromium-bidi": ["chromium-bidi@2.1.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-vtRWBK2uImo5/W2oG6/cDkkHSm+2t6VHgnj+Rcwhb0pP74OoUb4GipyRX/T/y39gYQPhioP0DPShn+A7P6CHNw=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "7.0.6", "signal-exit": "4.1.0" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], - "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], - "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "6.4.6", "picomatch": "4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "color-blend": ["color-blend@4.0.0", "", {}, "sha512-fYODTHhI/NG+B5GnzvuL3kiFrK/UnkUezWFTgEPBTY5V+kpyfAn95Vn9sJeeCX6omrCOdxnqCL3CvH+6sXtIbw=="], - "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "tsdown": ["tsdown@0.12.9", "", { "dependencies": { "ansis": "4.1.0", "cac": "6.7.14", "chokidar": "4.0.3", "debug": "4.4.1", "diff": "8.0.2", "empathic": "2.0.0", "hookable": "5.5.3", "rolldown": "1.0.0-beta.24", "rolldown-plugin-dts": "0.13.13", "semver": "7.7.2", "tinyexec": "1.0.1", "tinyglobby": "0.2.14", "unconfig": "7.3.2" }, "optionalDependencies": { "typescript": "5.8.3" }, "bin": { "tsdown": "dist/run.mjs" } }, "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "unconfig": ["unconfig@7.3.2", "", { "dependencies": { "@quansync/fs": "0.1.3", "defu": "6.1.4", "jiti": "2.4.2", "quansync": "0.2.10" } }, "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "zod": ["zod@4.3.3", "", {}, "sha512-bQ7Rxwfn04DCrTjjRfD9SavY2vWdmf3REjs/mkc1LdwI1KkcHClBRJmnvmA/6epGeqlHePtIRF1J4SrMMlW7IA=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "3.1.1", "shebang-command": "2.0.0", "which": "2.0.2" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="], + + "decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "detect-port": ["detect-port@1.5.1", "", { "dependencies": { "address": "^1.0.1", "debug": "4" }, "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" } }, "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "devtools-protocol": ["devtools-protocol@0.0.1402036", "", {}, "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + + "dns-socket": ["dns-socket@4.2.2", "", { "dependencies": { "dns-packet": "^5.2.4" } }, "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg=="], + + "dts-resolver": ["dts-resolver@2.1.1", "", {}, "sha512-3BiGFhB6mj5Kv+W2vdJseQUYW+SKVzAFJL6YNP6ursbrwy1fXHRotfHi3xLNxe4wZl/K8qbAFeCDjZLjzqxxRw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "engine.io": ["engine.io@6.6.9", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0" } }, "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "4.1.3", "strip-ansi": "6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + + "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="], + + "es-aggregate-error": ["es-aggregate-error@1.0.14", "", { "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" } }, "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="], + + "es-toolkit": ["es-toolkit@1.49.0", "", {}, "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="], + + "express": ["express@4.22.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "0.7.0", "iconv-lite": "0.4.24", "tmp": "0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "@nodelib/fs.walk": "1.2.8", "glob-parent": "5.1.2", "merge2": "1.4.1", "micromatch": "4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="], + + "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "1.1.0" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + + "favicons": ["favicons@7.2.0", "", { "dependencies": { "escape-html": "^1.0.3", "sharp": "^0.33.1", "xml2js": "^0.6.1" } }, "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.4.6", "", { "optionalDependencies": { "picomatch": "4.0.2" } }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + + "form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "gcd": ["gcd@0.0.1", "", {}, "sha512-VNx3UEGr+ILJTiMs1+xc5SX1cMgJCrXezKPa003APUWNqQqaF6n25W8VcR7nHN6yRWbvvUTwCpZCFJeWC2kXlw=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "glob": ["glob@7.1.6", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "2.1.0", "dir-glob": "3.0.1", "fast-glob": "3.3.3", "ignore": "5.3.2", "merge2": "1.4.1", "slash": "3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "got": ["got@13.0.0", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], + + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], + + "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-mdast": ["hast-util-to-mdast@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-to-html": "^9.0.0", "hast-util-to-text": "^4.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "mdast-util-to-string": "^4.0.0", "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-DsL/SvCK9V7+vfc6SLQ+vKIyBDXTk2KLSbfBYkH4zeF/uR1yBajHRhkzuaUSGOB1WJSTieJBdHwxlC+HLKvZZw=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "hex-rgb": ["hex-rgb@5.0.0", "", {}, "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="], + + "ico-endec": ["ico-endec@0.1.6", "", {}, "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "ink": ["ink@6.3.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-2CbJAa7XeziZYe6pDS5RVLirRY28iSGMQuEV8jRU5NQsONQNfcR/BZHHc9vkMg2lGYTHTM2pskxC1YmY28p6bQ=="], + + "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "inquirer": ["inquirer@12.3.0", "", { "dependencies": { "@inquirer/core": "^10.1.2", "@inquirer/prompts": "^7.2.1", "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.1" }, "peerDependencies": { "@types/node": ">=18" } }, "sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ip-regex": ["ip-regex@4.3.0", "", {}, "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-document.all": ["is-document.all@1.0.0", "", { "dependencies": { "call-bound": "^1.0.4" } }, "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + + "is-ip": ["is-ip@3.1.0", "", { "dependencies": { "ip-regex": "^4.0.0" } }, "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-online": ["is-online@10.0.0", "", { "dependencies": { "got": "^12.1.0", "p-any": "^4.0.0", "p-timeout": "^5.1.0", "public-ip": "^5.0.0" } }, "sha512-WCPdKwNDjXJJmUubf2VHLMDBkUZEtuOvpXUfUnUFbEnM6In9ByiScL4f4jKACz/fsb2qDkesFerW3snf/AYz3A=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], + + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + + "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], + + "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "lcm": ["lcm@0.0.3", "", { "dependencies": { "gcd": "^0.0.1" } }, "sha512-TB+ZjoillV6B26Vspf9l2L/vKaRY/4ep3hahcyVkCGFgsTNRUQdc24bQeNFiZeoxH0vr5+7SfNRMQuPHv/1IrQ=="], + + "leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lodash.topath": ["lodash.topath@4.5.2", "", {}, "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + + "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.1", "", { "dependencies": { "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "3.0.3", "picomatch": "2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mint": ["mint@4.2.684", "", { "dependencies": { "@mintlify/cli": "4.0.1287" }, "bin": { "mint": "index.js" } }, "sha512-pas9UC0J+ux6LKwoMzSzNzxsurhA57XrqSCTnAue/0kSP4+9Q0QC4KXbjXMPbz69vlclHRpsrMDsl/5ujtHYYg=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + + "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], + + "next-mdx-remote-client": ["next-mdx-remote-client@1.1.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@types/mdx": "^2.0.13", "remark-mdx-remove-esm": "^1.3.2", "serialize-error": "^13.0.1", "vfile": "^6.0.3", "vfile-matter": "^5.0.1" }, "peerDependencies": { "react": ">= 18.3.0 < 19.0.0", "react-dom": ">= 18.3.0 < 19.0.0" } }, "sha512-IElOrn02JjGQZxx+re7wMx/1AUG+Arte9aDImAtxjAfMw6xuSCaH5mTCunKelkWzFyFdRb565jO8jRICvvh96g=="], + + "nimma": ["nimma@0.2.3", "", { "dependencies": { "@jsep-plugin/regex": "^1.0.1", "@jsep-plugin/ternary": "^1.0.2", "astring": "^1.8.1", "jsep": "^1.2.0" }, "optionalDependencies": { "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA=="], + + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + + "node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], + + "node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], + + "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + + "non-error": ["non-error@0.1.0", "", {}, "sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], + + "oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "openid-client": ["openid-client@6.8.2", "", { "dependencies": { "jose": "^6.1.3", "oauth4webapi": "^3.8.4" } }, "sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-any": ["p-any@4.0.0", "", { "dependencies": { "p-cancelable": "^3.0.0", "p-some": "^6.0.0" } }, "sha512-S/B50s+pAVe0wmEZHmBs/9yJXeZ5KhHzOsgKzt0hRdgkoR3DxW9ts46fcsWi/r3VnzsnkKS7q4uimze+zjdryw=="], + + "p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "2.1.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-some": ["p-some@6.0.0", "", { "dependencies": { "aggregate-error": "^4.0.0", "p-cancelable": "^3.0.0" } }, "sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg=="], + + "p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "0.2.10" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "posthog-node": ["posthog-node@5.17.2", "", { "dependencies": { "@posthog/core": "1.7.1" } }, "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + + "public-ip": ["public-ip@5.0.0", "", { "dependencies": { "dns-socket": "^4.2.2", "got": "^12.0.0", "is-ip": "^3.1.0" } }, "sha512-xaH3pZMni/R2BG7ZXXaWS9Wc9wFlhyDVJF47IJ+3ali0TGv+2PsckKxbmo+rnx3ZxiV2wblVhtdS3bohAP6GGw=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "puppeteer": ["puppeteer@24.3.1", "", { "dependencies": { "@puppeteer/browsers": "2.7.1", "chromium-bidi": "2.1.2", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1402036", "puppeteer-core": "24.3.1", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-k0OJ7itRwkr06owp0CP3f/PsRD7Pdw4DjoCUZvjGr+aNgS1z6n/61VajIp0uBjl+V5XAQO1v/3k9bzeZLWs9OQ=="], + + "puppeteer-core": ["puppeteer-core@24.3.1", "", { "dependencies": { "@puppeteer/browsers": "2.7.1", "chromium-bidi": "2.1.2", "debug": "^4.4.0", "devtools-protocol": "0.0.1402036", "typed-query-selector": "^2.12.0", "ws": "^8.18.1" } }, "sha512-585ccfcTav4KmlSmYbwwOSeC8VdutQHn2Fuk0id/y/9OoeO7Gg5PK1aUGdZjEmos0TAq+pCpChqFurFbpNd3wA=="], + + "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "js-yaml": "3.14.1", "pify": "4.0.1", "strip-bom": "3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], + + "rehype-minify-whitespace": ["rehype-minify-whitespace@6.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0" } }, "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw=="], + + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], + + "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="], + + "remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], + + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + + "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], + + "remark-mdx-remove-esm": ["remark-mdx-remove-esm@1.3.2", "", { "dependencies": { "@types/mdast": "^4.0.4", "unist-util-remove": "^4.0.0" }, "peerDependencies": { "unified": "^11" } }, "sha512-BvL8VSdVXy9S7NlHP56nUJAHFc45h5E9HnHiLUGHe5tw3Yvm/3cVZvAzlkEEh2i+fkq2uKrf2xn5VmItBhMypA=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], + + "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], + + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], + + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], + + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.0.0-beta.24", "", { "dependencies": { "@oxc-project/runtime": "0.75.1", "@oxc-project/types": "0.75.1", "@rolldown/pluginutils": "1.0.0-beta.24", "ansis": "4.1.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.24", "@rolldown/binding-darwin-x64": "1.0.0-beta.24", "@rolldown/binding-freebsd-x64": "1.0.0-beta.24", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.24", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.24", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.24", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.24", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.24", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.24", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.24", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.24", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.24" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-eDyipoOnoHQ5p6INkJ8g31eKGlqPSCAN9PapyOTw5HET4FYIWALZnSgpMZ67mdn+xT3jAsqGidNnBcIM6EAUhA=="], + + "rolldown-plugin-dts": ["rolldown-plugin-dts@0.13.13", "", { "dependencies": { "@babel/generator": "7.28.0", "@babel/parser": "7.28.0", "@babel/types": "7.28.0", "ast-kit": "2.1.1", "birpc": "2.4.0", "debug": "4.4.1", "dts-resolver": "2.1.1", "get-tsconfig": "4.10.1" }, "optionalDependencies": { "typescript": "5.8.3" }, "peerDependencies": { "rolldown": "1.0.0-beta.24" } }, "sha512-Nchx9nQoa4IpfQ/BJzodKMvtJ3H3dT322siAJSp3uvQJ+Pi1qgEjOp7hSQwGSQRhaC5gC+9hparbWEH5oiAL9Q=="], + + "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "1.2.3" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "serialize-error": ["serialize-error@13.0.1", "", { "dependencies": { "non-error": "^0.1.0", "type-fest": "^5.4.1" } }, "sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA=="], + + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "sharp-ico": ["sharp-ico@0.1.5", "", { "dependencies": { "decode-ico": "*", "ico-endec": "*", "sharp": "*" } }, "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socket.io": ["socket.io@4.8.0", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA=="], + + "socket.io-adapter": ["socket.io-adapter@2.5.8", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.21.0" } }, "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw=="], + + "socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], + + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "7.0.6", "signal-exit": "4.1.0" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "streamx": ["streamx@2.28.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.11", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "has-property-descriptors": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.10", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.2" } }, "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "sucrase": ["sucrase@3.34.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "7.1.6", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tailwindcss-v3": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], + + "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], + + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "6.4.6", "picomatch": "4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trim-trailing-lines": ["trim-trailing-lines@2.1.0", "", {}, "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tsdown": ["tsdown@0.12.9", "", { "dependencies": { "ansis": "4.1.0", "cac": "6.7.14", "chokidar": "4.0.3", "debug": "4.4.1", "diff": "8.0.2", "empathic": "2.0.0", "hookable": "5.5.3", "rolldown": "1.0.0-beta.24", "rolldown-plugin-dts": "0.13.13", "semver": "7.7.2", "tinyexec": "1.0.1", "tinyglobby": "0.2.14", "unconfig": "7.3.2" }, "optionalDependencies": { "typescript": "5.8.3" }, "bin": { "tsdown": "dist/run.mjs" } }, "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "twoslash": ["twoslash@0.3.9", "", { "dependencies": { "@typescript/vfs": "^1.6.4", "twoslash-protocol": "0.3.9" }, "peerDependencies": { "typescript": "^5.5.0 || ^6.0.0" } }, "sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q=="], + + "twoslash-protocol": ["twoslash-protocol@0.3.9", "", {}, "sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="], + + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "unconfig": ["unconfig@7.3.2", "", { "dependencies": { "@quansync/fs": "0.1.3", "defu": "6.1.4", "jiti": "2.4.2", "quansync": "0.2.10" } }, "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-builder": ["unist-builder@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-map": ["unist-util-map@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA=="], + + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], + + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zod": ["zod@4.3.3", "", {}, "sha512-bQ7Rxwfn04DCrTjjRfD9SavY2vWdmf3REjs/mkc1LdwI1KkcHClBRJmnvmA/6epGeqlHePtIRF1J4SrMMlW7IA=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.20.4", "", { "peerDependencies": { "zod": "^3.20.0" } }, "sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@inquirer/external-editor/chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + + "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@mintlify/cli/fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="], + + "@mintlify/cli/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@mintlify/common/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@mintlify/common/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], + + "@mintlify/link-rot/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], + + "@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "@mintlify/mdx/mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "@mintlify/openapi-parser/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], + + "@mintlify/prebuild/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], + + "@mintlify/prebuild/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "@mintlify/previewing/chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], + + "@mintlify/previewing/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], + + "@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "@mintlify/scraping/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="], + + "@mintlify/scraping/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], + + "@mintlify/scraping/remark-mdx": ["remark-mdx@3.0.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA=="], + + "@mintlify/scraping/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], + + "@mintlify/validation/arktype": ["arktype@2.1.27", "", { "dependencies": { "@ark/schema": "0.55.0", "@ark/util": "0.55.0", "arkregex": "0.0.3" } }, "sha512-enctOHxI4SULBv/TDtCVi5M8oLd4J5SVlPUblXDzSsOYQNMzmVbUosGBnJuZDKmFlN5Ie0/QVEuTE+Z5X1UhsQ=="], + + "@mintlify/validation/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], + + "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.3", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ=="], + + "@puppeteer/browsers/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="], + + "@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], + + "@stoplight/spectral-runtime/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], + + "@typescript/vfs/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "body-parser/qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "extract-zip/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "front-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "get-uri/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "hast-util-from-parse5/property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "hast-util-to-estree/property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "hast-util-to-jsx-runtime/property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "hastscript/property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "http-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pac-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "pac-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "pac-proxy-agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "proxy-agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + + "react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "read-cache/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "serialize-error/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], + + "socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "socks-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "socks-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "tailwindcss-v3/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "tailwindcss-v3/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "tailwindcss-v3/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "tailwindcss-v3/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@changesets/parse/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@mintlify/cli/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/cli/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@mintlify/link-rot/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/link-rot/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@mintlify/link-rot/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "@mintlify/prebuild/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/prebuild/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@mintlify/prebuild/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@mintlify/prebuild/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "@mintlify/previewing/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "@mintlify/previewing/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/previewing/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@mintlify/previewing/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "@mintlify/previewing/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "@mintlify/scraping/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/scraping/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@mintlify/validation/arktype/@ark/schema": ["@ark/schema@0.55.0", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-IlSIc0FmLKTDGr4I/FzNHauMn0MADA6bCjT1wauu4k6MyxhC1R9gz0olNpIRvK7lGGDwtc/VO0RUDNvVQW5WFg=="], + + "@mintlify/validation/arktype/@ark/util": ["@ark/util@0.55.0", "", {}, "sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA=="], + + "@mintlify/validation/arktype/arkregex": ["arkregex@0.0.3", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-bU21QJOJEFJK+BPNgv+5bVXkvRxyAvgnon75D92newgHxkBJTgiFwQxusyViYyJkETsddPlHyspshDQcCzmkNg=="], + + "@puppeteer/browsers/tar-fs/tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], + + "@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@shikijs/core/hast-util-to-html/property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "front-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "tailwindcss-v3/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "tailwindcss-v3/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@mintlify/previewing/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "@puppeteer/browsers/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@puppeteer/browsers/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "tailwindcss-v3/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], } } diff --git a/docs/docs.json b/docs/docs.json index d4a940c..32278a9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -69,9 +69,7 @@ }, { "group": "\ud83d\udd04 Migration", - "pages": [ - "migration/from-try-catch" - ] + "pages": ["migration/from-try-catch"] } ] } diff --git a/docs/index.mdx b/docs/index.mdx index 50a0ab8..a7a715d 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -3,8 +3,6 @@ title: wellcrafted description: 'Tagged errors and Result types as plain objects. Under 2KB, zero dependencies.' --- -import { Card, CardGroup } from '@mintlify/components' - # wellcrafted Tagged errors and Result types as plain objects. Under 2KB, zero dependencies. diff --git a/package.json b/package.json index a3b26b1..b29b3a7 100644 --- a/package.json +++ b/package.json @@ -49,14 +49,16 @@ "scripts": { "build": "tsdown", "format": "biome format --write .", + "format:check": "biome format .", "lint": "biome lint --write .", + "lint:check": "biome lint .", "test": "bun test", "test:watch": "bun test --watch", "typecheck": "tsc --noEmit", "release": "bun run build && changeset version && changeset publish", - "docs:dev": "bunx mint dev", - "docs:validate": "bunx mint validate", - "docs:links": "bunx mint broken-links" + "docs:dev": "cd docs && mint dev", + "docs:validate": "cd docs && mint validate", + "docs:links": "cd docs && mint broken-links" }, "keywords": [ "typescript", @@ -82,6 +84,7 @@ "@tanstack/query-core": "^5.82.0", "@types/bun": "^1.3.5", "arktype": "^2.1.29", + "mint": "4.2.684", "tsdown": "^0.12.5", "typescript": "^5.8.3", "valibot": "^1.2.0", diff --git a/skills-lock.json b/skills-lock.json index a2bead1..4c830fc 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,105 +1,105 @@ { - "version": 1, - "skills": { - "control-flow": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "84eef70ffe87b484f05aff50fbcf6dcc2b57bf139a52c1d9dbc28d3cbec396d8" - }, - "define-errors": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "a1fbe2a13b3d984dc4d1ace7dbde7e6bd1cf1853f050e6d50218c47c6d34f261" - }, - "documentation": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "ef13dc06e07373b649f523ac6f101a882410d95078439ecc1ffba8df6b7e23f7" - }, - "error-handling": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "9573172994a1877b5d599a93256559dd6e6f742596c02cefd1b6410af06ea193" - }, - "factory-function-composition": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "c688003c35ced55eed158aa28ddb897e138084b9d2f05ef0a18874bc5f310810" - }, - "git": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "6de2120d1a50c401bd7adcf265dc91abf4780ca8efb485d3ef397b15ff2339d6" - }, - "honesty": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "4df5c59eb8a48525275a4fccadd2b65ba7eea414f35660ac5b5025f64a2d271b" - }, - "incremental-commits": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "317ed5aa14b2ef0ee70078434ec8ad2e7457eeab7d4ec8496180902fdc2637e6" - }, - "method-shorthand-jsdoc": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "3d5e57247ba5a8d884b253f8b754609b6c65de18dc1f5846ade3ccc8089ebcc0" - }, - "progress-summary": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "fedbc60aaf10f9e1a255b9e62c99fcf19dc8186215acd261b0dc504faf8b0c45" - }, - "query-layer": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "5bf873615791f2b6fbacf1033ad19b83348100befe296d9c3ac433b882f0072d" - }, - "services-layer": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "f92460c2634c8c1781d63f58c25baa2d7aaf5424f09ccfdf0933a11eb91df530" - }, - "single-or-array-pattern": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "727ef422a45fb941aba24cf1ce1595fde9d2917659001c0d6151df7b49aa4b37" - }, - "spec-execution": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "b970f8b3055afdeacf1307a63020f08b1736b605e816694b3a1eeadfd3379837" - }, - "specification-writing": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "bb77523e654fa285a1a9b6c0c16bea3fbedebec7cf9d7688b07886c6964c3ddf" - }, - "technical-articles": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "9da35308b3dbaa6eef32283ba5812e837e0f900a0b72519f3341cfdeedd061c2" - }, - "testing": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "1a07baa86c5fac4f946232ad34313eb43576d463913047747fc7914cbdc0230b" - }, - "typescript": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "33c2ba5b14d29b198f5c8bc0759ba4c16fe125b042fb76ec98d7cb4f0c5ccf71" - }, - "workflow": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "d224576c520c4f7c14a7d59689baba292c6fe9ed2e70e1a18be2a08ef8cc4b91" - }, - "writing-voice": { - "source": "EpicenterHQ/epicenter", - "sourceType": "github", - "computedHash": "55d11a2595d6bd025221b4e86ed3acdef25ed2279717c6e0f3cd3b6cf4b8d502" - } - } + "version": 1, + "skills": { + "control-flow": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "84eef70ffe87b484f05aff50fbcf6dcc2b57bf139a52c1d9dbc28d3cbec396d8" + }, + "define-errors": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "a1fbe2a13b3d984dc4d1ace7dbde7e6bd1cf1853f050e6d50218c47c6d34f261" + }, + "documentation": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "ef13dc06e07373b649f523ac6f101a882410d95078439ecc1ffba8df6b7e23f7" + }, + "error-handling": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "9573172994a1877b5d599a93256559dd6e6f742596c02cefd1b6410af06ea193" + }, + "factory-function-composition": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "c688003c35ced55eed158aa28ddb897e138084b9d2f05ef0a18874bc5f310810" + }, + "git": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "6de2120d1a50c401bd7adcf265dc91abf4780ca8efb485d3ef397b15ff2339d6" + }, + "honesty": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "4df5c59eb8a48525275a4fccadd2b65ba7eea414f35660ac5b5025f64a2d271b" + }, + "incremental-commits": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "317ed5aa14b2ef0ee70078434ec8ad2e7457eeab7d4ec8496180902fdc2637e6" + }, + "method-shorthand-jsdoc": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "3d5e57247ba5a8d884b253f8b754609b6c65de18dc1f5846ade3ccc8089ebcc0" + }, + "progress-summary": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "fedbc60aaf10f9e1a255b9e62c99fcf19dc8186215acd261b0dc504faf8b0c45" + }, + "query-layer": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "5bf873615791f2b6fbacf1033ad19b83348100befe296d9c3ac433b882f0072d" + }, + "services-layer": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "f92460c2634c8c1781d63f58c25baa2d7aaf5424f09ccfdf0933a11eb91df530" + }, + "single-or-array-pattern": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "727ef422a45fb941aba24cf1ce1595fde9d2917659001c0d6151df7b49aa4b37" + }, + "spec-execution": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "b970f8b3055afdeacf1307a63020f08b1736b605e816694b3a1eeadfd3379837" + }, + "specification-writing": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "bb77523e654fa285a1a9b6c0c16bea3fbedebec7cf9d7688b07886c6964c3ddf" + }, + "technical-articles": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "9da35308b3dbaa6eef32283ba5812e837e0f900a0b72519f3341cfdeedd061c2" + }, + "testing": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "1a07baa86c5fac4f946232ad34313eb43576d463913047747fc7914cbdc0230b" + }, + "typescript": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "33c2ba5b14d29b198f5c8bc0759ba4c16fe125b042fb76ec98d7cb4f0c5ccf71" + }, + "workflow": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "d224576c520c4f7c14a7d59689baba292c6fe9ed2e70e1a18be2a08ef8cc4b91" + }, + "writing-voice": { + "source": "EpicenterHQ/epicenter", + "sourceType": "github", + "computedHash": "55d11a2595d6bd025221b4e86ed3acdef25ed2279717c6e0f3cd3b6cf4b8d502" + } + } } diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index d85b571..2878fb0 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -516,11 +516,20 @@ Commits are created only after explicit approval. If approved, each wave is one ### Wave 1: Make baseline verification green -- [ ] Repair and pin Mintlify validation. -- [ ] Run Mint commands from `docs/` under an explicit Node 24 job. -- [ ] Add non-mutating lint and format checks. -- [ ] Fix the current site validation warning. -- [ ] Commit only when the existing content passes these baseline checks. +- [x] Repair and pin Mintlify validation. +- [x] Run Mint commands from `docs/` under an explicit Node 24 job. +- [x] Add non-mutating lint and format checks. +- [x] Fix the current site validation warning. +- [x] Commit only when the existing content passes these baseline checks. + +Verification on 2026-07-10: + +- `PUPPETEER_SKIP_DOWNLOAD=true bun install --frozen-lockfile` passed with the exact `mint@4.2.684` dependency. +- `bun run docs:validate` and `bun run docs:links` passed from `docs/` with Node 24.17.0. The CI workflow now runs both commands in an explicit Node 24 job. +- `bun run lint:check` exited successfully without writes. It still reports 12 pre-existing warnings: one unused suppression and 11 non-null assertions in tests. +- `bun run format:check`, `bun run typecheck`, `bun run build`, and `bun test` passed. The test run completed 158 tests with no failures. +- The unsupported `@mintlify/components` import was removed. Mintlify provides `Card` and `CardGroup` as built-in components. +- The primary orchestrator independently reran the baseline checks before committing this wave. ### Wave 2: Add examples and isolated package proof diff --git a/src/result/result.test.ts b/src/result/result.test.ts index 25bc2bb..6db7893 100644 --- a/src/result/result.test.ts +++ b/src/result/result.test.ts @@ -34,7 +34,9 @@ describe("Ok / Err structural invariants", () => { test("Err with meaningful values works as expected", () => { expect(Err("string error").error).toBe("string error"); expect(Err(new Error("native")).error).toBeInstanceOf(Error); - expect(Err({ name: "Tagged", message: "failed" }).error.name).toBe("Tagged"); + expect(Err({ name: "Tagged", message: "failed" }).error.name).toBe( + "Tagged", + ); expect(Err(0).error).toBe(0); expect(Err(false).error).toBe(false); }); From f7a148f351380888126a623ea15e8c6aed449923 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:32:37 -0700 Subject: [PATCH 03/13] test(docs): add executable package examples Add canonical learning examples plus strict type, packed-consumer, and runtime fixtures. Exercise all nine published subpaths under Bun and supported Node jobs while keeping root imports and the TanStack prerequisite explicit. --- .github/workflows/main.yml | 27 +++ examples/assert.ts | 13 ++ examples/quick-start.ts | 30 +++ examples/serialization-boundary.ts | 40 ++++ examples/service-boundary.ts | 63 ++++++ examples/tanstack-query.ts | 53 +++++ examples/tsconfig.json | 14 ++ package.json | 6 +- .../fixtures/compatibility/all-subpaths.ts | 79 +++++++ .../fixtures/compatibility/tsconfig.base.json | 14 ++ .../compatibility/tsconfig.bundler.json | 7 + .../compatibility/tsconfig.nodenext.json | 7 + .../package-consumer/non-query-subpaths.ts | 27 +++ .../package-consumer/query-prerequisite.ts | 4 + scripts/fixtures/package-consumer/query.ts | 9 + scripts/fixtures/runtime/all-subpaths.mjs | 79 +++++++ scripts/package-smoke.ts | 199 ++++++++++++++++++ ...10T012026-greenfield-documentation-pass.md | 17 +- 18 files changed, 683 insertions(+), 5 deletions(-) create mode 100644 examples/assert.ts create mode 100644 examples/quick-start.ts create mode 100644 examples/serialization-boundary.ts create mode 100644 examples/service-boundary.ts create mode 100644 examples/tanstack-query.ts create mode 100644 examples/tsconfig.json create mode 100644 scripts/fixtures/compatibility/all-subpaths.ts create mode 100644 scripts/fixtures/compatibility/tsconfig.base.json create mode 100644 scripts/fixtures/compatibility/tsconfig.bundler.json create mode 100644 scripts/fixtures/compatibility/tsconfig.nodenext.json create mode 100644 scripts/fixtures/package-consumer/non-query-subpaths.ts create mode 100644 scripts/fixtures/package-consumer/query-prerequisite.ts create mode 100644 scripts/fixtures/package-consumer/query.ts create mode 100644 scripts/fixtures/runtime/all-subpaths.mjs create mode 100644 scripts/package-smoke.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 207e05b..cdfda91 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 # Guard against `"wellcrafted": major` changesets while still in 0.x. # wellcrafted@1.0.0 was published and unpublished on 2025-07-17; npm @@ -45,6 +47,31 @@ jobs: - run: bun run typecheck - run: bun run build - run: bun test + - run: bun run docs:examples + - run: bun run package:smoke + - run: bun run compat:types + - run: bun run compat:runtime + + runtime: + name: Runtime (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22, 24] + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + package-manager-cache: false + - run: bun install --frozen-lockfile + env: + PUPPETEER_SKIP_DOWNLOAD: "true" + - run: bun run build + - run: node scripts/fixtures/runtime/all-subpaths.mjs docs: name: Docs (Node 24) diff --git a/examples/assert.ts b/examples/assert.ts new file mode 100644 index 0000000..faeda9f --- /dev/null +++ b/examples/assert.ts @@ -0,0 +1,13 @@ +export function assert(condition: unknown, message: string): asserts condition { + if (condition) return; + throw new Error(message); +} + +export function assertEqual(actual: unknown, expected: unknown): void { + const actualJson = JSON.stringify(actual); + const expectedJson = JSON.stringify(expected); + assert( + actualJson === expectedJson, + `Expected ${expectedJson}, received ${actualJson}`, + ); +} diff --git a/examples/quick-start.ts b/examples/quick-start.ts new file mode 100644 index 0000000..371ae32 --- /dev/null +++ b/examples/quick-start.ts @@ -0,0 +1,30 @@ +import { defineErrors, type InferErrors } from "wellcrafted/error"; +import { Ok, type Result } from "wellcrafted/result"; +import { assertEqual } from "./assert.js"; + +const PortError = defineErrors({ + Invalid: ({ input }: { input: string }) => ({ + message: `Expected a port from 1 to 65535, received "${input}".`, + input, + }), +}); + +type PortError = InferErrors; + +function parsePort(input: string): Result { + const port = Number(input); + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + return PortError.Invalid({ input }); + } + + return Ok(port); +} + +const success = parsePort("3000"); +assertEqual(success, { data: 3000, error: null }); + +const failure = parsePort("not-a-port"); +assertEqual(failure.data, null); +assertEqual(failure.error?.name, "Invalid"); + +console.log("quick-start: success and failure paths passed"); diff --git a/examples/serialization-boundary.ts b/examples/serialization-boundary.ts new file mode 100644 index 0000000..3a5b4a1 --- /dev/null +++ b/examples/serialization-boundary.ts @@ -0,0 +1,40 @@ +import { defineErrors } from "wellcrafted/error"; +import { assertEqual } from "./assert.js"; + +const UploadError = defineErrors({ + Rejected: ({ + fileName, + reasons, + }: { + fileName: string; + reasons: string[]; + }) => ({ + message: `Upload rejected for "${fileName}".`, + fileName, + reasons, + }), + Unexpected: ({ cause }: { cause: unknown }) => ({ + message: "Upload failed unexpectedly.", + cause, + }), +}); + +const boundaryFriendly = UploadError.Rejected({ + fileName: "report.csv", + reasons: ["too large", "unsupported encoding"], +}); +assertEqual(JSON.parse(JSON.stringify(boundaryFriendly)), boundaryFriendly); + +// defineErrors does not enforce JSON-safe fields. A native Error is accepted, +// but its non-enumerable details do not survive JSON serialization. +const withNativeCause = UploadError.Unexpected({ + cause: new Error("disk full"), +}); +const roundTripped = JSON.parse(JSON.stringify(withNativeCause)) as { + error: { cause: unknown }; +}; +assertEqual(roundTripped.error.cause, {}); + +console.log( + "serialization-boundary: JSON-compatible data and cause caveat passed", +); diff --git a/examples/service-boundary.ts b/examples/service-boundary.ts new file mode 100644 index 0000000..0205b3d --- /dev/null +++ b/examples/service-boundary.ts @@ -0,0 +1,63 @@ +/** + * Adapted from Epicenter's service-boundary pattern at commit 4d438c0: + * https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/client/src/transcribe.ts + */ +import { + defineErrors, + extractErrorMessage, + type InferErrors, +} from "wellcrafted/error"; +import { Ok, type Result, trySync } from "wellcrafted/result"; +import { assertEqual } from "./assert.js"; + +const UserError = defineErrors({ + ReadFailed: ({ cause }: { cause: string }) => ({ + message: `Could not read the user record: ${cause}`, + cause, + }), + NotFound: ({ userId }: { userId: string }) => ({ + message: `No user exists with id "${userId}".`, + userId, + }), +}); + +type UserError = InferErrors; +type User = { id: string; displayName: string }; + +function createUserService({ records }: { records: Map }) { + function findUser(userId: string): Result { + return trySync({ + try: () => { + if (userId === "storage-offline") { + throw new Error("storage is offline"); + } + return records.get(userId) ?? null; + }, + catch: (cause) => + UserError.ReadFailed({ cause: extractErrorMessage(cause) }), + }); + } + + return { + getDisplayName(userId: string): Result { + const userResult = findUser(userId); + if (userResult.error !== null) return userResult; + if (userResult.data === null) return UserError.NotFound({ userId }); + + return Ok(userResult.data.displayName); + }, + }; +} + +const users = createUserService({ + records: new Map([["user-1", { id: "user-1", displayName: "Ada" }]]), +}); + +assertEqual(users.getDisplayName("user-1"), { + data: "Ada", + error: null, +}); +assertEqual(users.getDisplayName("missing").error?.name, "NotFound"); +assertEqual(users.getDisplayName("storage-offline").error?.name, "ReadFailed"); + +console.log("service-boundary: success, domain, and I/O failures passed"); diff --git a/examples/tanstack-query.ts b/examples/tanstack-query.ts new file mode 100644 index 0000000..3cb5c6e --- /dev/null +++ b/examples/tanstack-query.ts @@ -0,0 +1,53 @@ +import { QueryClient } from "@tanstack/query-core"; +import { defineErrors } from "wellcrafted/error"; +import { + createQueryFactories, + defineKeys, + resultMutationOptions, + resultQueryOptions, +} from "wellcrafted/query"; +import { Ok } from "wellcrafted/result"; + +const TodoError = defineErrors({ + NotFound: ({ todoId }: { todoId: string }) => ({ + message: `No todo exists with id "${todoId}".`, + todoId, + }), +}); + +const todoKeys = defineKeys({ + all: ["todos"], + detail: (todoId: string) => ["todos", todoId] as const, +}); + +const directQuery = resultQueryOptions({ + queryKey: todoKeys.detail("todo-1"), + queryFn: () => Ok({ id: "todo-1", title: "Write docs" }), +}); + +const directMutation = resultMutationOptions({ + mutationKey: ["todos", "rename"], + mutationFn: ({ todoId, title }: { todoId: string; title: string }) => + Ok({ id: todoId, title }), +}); + +const { defineMutation, defineQuery } = createQueryFactories(new QueryClient()); +const todoQuery = defineQuery({ + queryKey: todoKeys.detail("todo-1"), + queryFn: () => Ok({ id: "todo-1", title: "Write docs" }), +}); +const renameTodo = defineMutation({ + mutationKey: ["todos", "rename"], + mutationFn: ({ todoId, title }: { todoId: string; title: string }) => { + if (todoId === "missing") return TodoError.NotFound({ todoId }); + return Ok({ id: todoId, title }); + }, +}); + +directQuery.queryKey; +directMutation.mutationKey; +todoQuery.options; +todoQuery.fetch; +todoQuery.ensure; +renameTodo.options; +renameTodo({ todoId: "todo-1", title: "Ship docs" }); diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..c9cc864 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "declaration": false, + "declarationMap": false, + "lib": ["ESNext", "DOM"], + "noEmit": true, + "outDir": "../dist-examples", + "rootDir": "..", + "skipLibCheck": false, + "types": [] + }, + "include": ["./*.ts"] +} diff --git a/package.json b/package.json index b29b3a7..b577753 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,11 @@ "release": "bun run build && changeset version && changeset publish", "docs:dev": "cd docs && mint dev", "docs:validate": "cd docs && mint validate", - "docs:links": "cd docs && mint broken-links" + "docs:links": "cd docs && mint broken-links", + "docs:examples": "bun run build && bun node_modules/typescript/bin/tsc --project examples/tsconfig.json && bun examples/quick-start.ts && bun examples/service-boundary.ts && bun examples/serialization-boundary.ts", + "package:smoke": "bun run build && bun scripts/package-smoke.ts", + "compat:types": "bun run build && bun node_modules/typescript/bin/tsc --project scripts/fixtures/compatibility/tsconfig.bundler.json && bun node_modules/typescript/bin/tsc --project scripts/fixtures/compatibility/tsconfig.nodenext.json", + "compat:runtime": "bun run build && bun scripts/fixtures/runtime/all-subpaths.mjs" }, "keywords": [ "typescript", diff --git a/scripts/fixtures/compatibility/all-subpaths.ts b/scripts/fixtures/compatibility/all-subpaths.ts new file mode 100644 index 0000000..3f7288d --- /dev/null +++ b/scripts/fixtures/compatibility/all-subpaths.ts @@ -0,0 +1,79 @@ +import type { Brand } from "wellcrafted/brand"; +import { defineErrors, type InferErrors } from "wellcrafted/error"; +import { once } from "wellcrafted/function"; +import { parseJson, type JsonValue } from "wellcrafted/json"; +import { + composeSinks, + createLogger, + memorySink, + type LogSink, +} from "wellcrafted/logger"; +import { + createQueryFactories, + defineKeys, + resultMutationOptions, + resultQueryOptions, +} from "wellcrafted/query"; +import { Err, Ok, partitionResults, type Result } from "wellcrafted/result"; +import { OkSchema, type StandardSchemaV1 } from "wellcrafted/standard-schema"; +import { expectErr, expectOk } from "wellcrafted/testing"; + +// @ts-expect-error — the package intentionally has no root export +import type {} from "wellcrafted"; + +type UserId = string & Brand<"UserId">; +const userId = "user-1" as UserId; + +const FixtureError = defineErrors({ + Missing: ({ id }: { id: UserId }) => ({ + message: `Missing ${id}`, + id, + }), +}); +type FixtureError = InferErrors; + +const result: Result = Ok(userId); +const json: JsonValue = { id: expectOk(result) }; +parseJson(JSON.stringify(json)); +expectErr(Err("expected fixture failure")); +partitionResults([Ok(1), Err("failure")]); + +const runOnce = once(() => userId); +runOnce(); + +const { sink } = memorySink(); +const composed: LogSink = composeSinks(sink); +createLogger("compatibility/fixture", composed).info("loaded"); + +const stringSchema = { + "~standard": { + version: 1, + vendor: "fixture", + validate: (value: unknown) => + typeof value === "string" + ? { value } + : { issues: [{ message: "Expected a string" }] }, + }, +} satisfies StandardSchemaV1; +OkSchema(stringSchema); + +const keys = defineKeys({ + all: ["fixtures"], + detail: (id: UserId) => ["fixtures", id] as const, +}); +resultQueryOptions({ + queryKey: keys.detail(userId), + queryFn: () => result, +}); +resultMutationOptions({ + mutationKey: ["fixtures", "save"], + mutationFn: (id: UserId) => Ok(id), +}); + +declare const queryClient: Parameters[0]; +const factories = createQueryFactories(queryClient); +factories.defineQuery({ queryKey: keys.all, queryFn: () => result }); +factories.defineMutation({ + mutationKey: ["fixtures", "save"], + mutationFn: (id: UserId) => Ok(id), +}); diff --git a/scripts/fixtures/compatibility/tsconfig.base.json b/scripts/fixtures/compatibility/tsconfig.base.json new file mode 100644 index 0000000..8809a05 --- /dev/null +++ b/scripts/fixtures/compatibility/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": false, + "lib": ["ESNext", "DOM"], + "noEmit": true, + "rootDir": "../../..", + "skipLibCheck": false, + "strict": true, + "target": "ES2024", + "types": [], + "verbatimModuleSyntax": true + }, + "files": ["./all-subpaths.ts"] +} diff --git a/scripts/fixtures/compatibility/tsconfig.bundler.json b/scripts/fixtures/compatibility/tsconfig.bundler.json new file mode 100644 index 0000000..c45ab3a --- /dev/null +++ b/scripts/fixtures/compatibility/tsconfig.bundler.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler" + } +} diff --git a/scripts/fixtures/compatibility/tsconfig.nodenext.json b/scripts/fixtures/compatibility/tsconfig.nodenext.json new file mode 100644 index 0000000..e5275ec --- /dev/null +++ b/scripts/fixtures/compatibility/tsconfig.nodenext.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/scripts/fixtures/package-consumer/non-query-subpaths.ts b/scripts/fixtures/package-consumer/non-query-subpaths.ts new file mode 100644 index 0000000..d5a4204 --- /dev/null +++ b/scripts/fixtures/package-consumer/non-query-subpaths.ts @@ -0,0 +1,27 @@ +import type { Brand } from "wellcrafted/brand"; +import { defineErrors } from "wellcrafted/error"; +import { once } from "wellcrafted/function"; +import { parseJson } from "wellcrafted/json"; +import { composeSinks, memorySink } from "wellcrafted/logger"; +import { Ok, partitionResults } from "wellcrafted/result"; +import { OkSchema } from "wellcrafted/standard-schema"; +import { expectOk } from "wellcrafted/testing"; + +type FixtureId = string & Brand<"FixtureId">; +const id = "fixture-1" as FixtureId; +const FixtureError = defineErrors({ + Missing: () => ({ message: "Fixture is missing." }), +}); + +expectOk(Ok(id)); +parseJson('{"ready":true}'); +partitionResults([Ok(1), FixtureError.Missing()]); +once(() => id)(); +composeSinks(memorySink().sink); +OkSchema({ + "~standard": { + version: 1, + vendor: "fixture", + types: undefined as unknown as { input: string; output: string }, + }, +}); diff --git a/scripts/fixtures/package-consumer/query-prerequisite.ts b/scripts/fixtures/package-consumer/query-prerequisite.ts new file mode 100644 index 0000000..13276fa --- /dev/null +++ b/scripts/fixtures/package-consumer/query-prerequisite.ts @@ -0,0 +1,4 @@ +import { resultQueryOptions } from "wellcrafted/query"; +import { Ok } from "wellcrafted/result"; + +resultQueryOptions({ queryKey: ["fixture"], queryFn: () => Ok("ready") }); diff --git a/scripts/fixtures/package-consumer/query.ts b/scripts/fixtures/package-consumer/query.ts new file mode 100644 index 0000000..4d8d098 --- /dev/null +++ b/scripts/fixtures/package-consumer/query.ts @@ -0,0 +1,9 @@ +import { QueryClient } from "@tanstack/query-core"; +import { createQueryFactories, resultQueryOptions } from "wellcrafted/query"; +import { Ok } from "wellcrafted/result"; + +resultQueryOptions({ queryKey: ["fixture"], queryFn: () => Ok("ready") }); +createQueryFactories(new QueryClient()).defineQuery({ + queryKey: ["fixture"], + queryFn: () => Ok("ready"), +}); diff --git a/scripts/fixtures/runtime/all-subpaths.mjs b/scripts/fixtures/runtime/all-subpaths.mjs new file mode 100644 index 0000000..1633f5e --- /dev/null +++ b/scripts/fixtures/runtime/all-subpaths.mjs @@ -0,0 +1,79 @@ +import { strict as assert } from "node:assert"; +import * as brand from "wellcrafted/brand"; +import { defineErrors } from "wellcrafted/error"; +import { once } from "wellcrafted/function"; +import { parseJson } from "wellcrafted/json"; +import { composeSinks, createLogger, memorySink } from "wellcrafted/logger"; +import { resultQueryOptions } from "wellcrafted/query"; +import { Err, Ok, partitionResults } from "wellcrafted/result"; +import { OkSchema } from "wellcrafted/standard-schema"; +import { expectErr, expectOk } from "wellcrafted/testing"; + +assert.deepEqual(Object.keys(brand), []); + +const FixtureError = defineErrors({ + Missing: () => ({ message: "Fixture is missing." }), +}); +assert.equal(FixtureError.Missing().error.name, "Missing"); + +const incrementOnce = once(() => 1); +assert.equal(incrementOnce(), 1); +assert.equal(incrementOnce(), 1); + +assert.deepEqual(parseJson('{"ready":true}').data, { ready: true }); + +let disposed = false; +const disposableSink = Object.assign(() => {}, { + [Symbol.asyncDispose]: async () => { + disposed = true; + }, +}); +const { sink, events } = memorySink(); +const composed = composeSinks(sink, disposableSink); +createLogger("runtime/fixture", composed).info("loaded"); +assert.equal(events.length, 1); +await composed[Symbol.asyncDispose](); +assert.equal(disposed, true); + +const options = resultQueryOptions({ + queryKey: ["runtime", "fixture"], + queryFn: () => Ok("ready"), +}); +assert.equal(await options.queryFn({}), "ready"); + +const { oks, errs } = partitionResults([Ok(1), Err("failure"), Ok(2)]); +assert.deepEqual( + oks.map((result) => result.data), + [1, 2], +); +assert.deepEqual( + errs.map((result) => result.error), + ["failure"], +); + +const stringSchema = { + "~standard": { + version: 1, + vendor: "fixture", + validate: (value) => + typeof value === "string" + ? { value } + : { issues: [{ message: "Expected a string" }] }, + }, +}; +const okSchema = OkSchema(stringSchema); +assert.deepEqual(okSchema["~standard"].validate(Ok("ready")), { + value: Ok("ready"), +}); + +assert.equal(expectOk(Ok("ready")), "ready"); +assert.equal(expectErr(Err("failure")), "failure"); + +await assert.rejects(import("wellcrafted"), (error) => { + return ( + error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || + error?.code === "ERR_MODULE_NOT_FOUND" + ); +}); + +console.log("runtime fixture: all nine subpaths passed; root import rejected"); diff --git a/scripts/package-smoke.ts b/scripts/package-smoke.ts new file mode 100644 index 0000000..1cabef1 --- /dev/null +++ b/scripts/package-smoke.ts @@ -0,0 +1,199 @@ +import { cp, mkdtemp, mkdir, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const REPOSITORY_ROOT = resolve(import.meta.dir, ".."); +const FIXTURE_ROOT = join(import.meta.dir, "fixtures", "package-consumer"); +const RUNTIME_FIXTURE = join( + import.meta.dir, + "fixtures", + "runtime", + "all-subpaths.mjs", +); +const TSC = join(REPOSITORY_ROOT, "node_modules", "typescript", "bin", "tsc"); +const TANSTACK_QUERY_VERSION = "5.82.0"; + +type CommandResult = { + exitCode: number; + stderr: string; + stdout: string; +}; + +async function run( + command: string[], + { cwd = REPOSITORY_ROOT }: { cwd?: string } = {}, +): Promise { + const process = Bun.spawn(command, { cwd, stderr: "pipe", stdout: "pipe" }); + const [exitCode, stderr, stdout] = await Promise.all([ + process.exited, + new Response(process.stderr).text(), + new Response(process.stdout).text(), + ]); + return { exitCode, stderr, stdout }; +} + +function assertPassed(result: CommandResult, label: string): void { + if (result.exitCode === 0) return; + throw new Error( + `${label} failed with exit code ${result.exitCode}.\n${result.stdout}${result.stderr}`, + ); +} + +async function writeTsconfig({ + consumerRoot, + file, + moduleResolution, + name, +}: { + consumerRoot: string; + file: string; + moduleResolution: "Bundler" | "NodeNext"; + name: string; +}): Promise { + const path = join(consumerRoot, `tsconfig.${name}.json`); + await writeFile( + path, + `${JSON.stringify( + { + compilerOptions: { + lib: ["ESNext", "DOM"], + module: moduleResolution === "Bundler" ? "ESNext" : "NodeNext", + moduleResolution, + noEmit: true, + skipLibCheck: false, + strict: true, + target: "ES2024", + types: [], + verbatimModuleSyntax: true, + }, + files: [file], + }, + null, + 2, + )}\n`, + ); + return path; +} + +async function typecheck( + consumerRoot: string, + configPath: string, +): Promise { + return run(["bun", TSC, "--project", configPath], { cwd: consumerRoot }); +} + +async function main(): Promise { + const temporaryRoot = await mkdtemp( + join(tmpdir(), "wellcrafted-package-smoke-"), + ); + const consumerRoot = join(temporaryRoot, "consumer"); + + try { + await mkdir(consumerRoot); + assertPassed( + await run([ + "bun", + "pm", + "pack", + "--destination", + temporaryRoot, + "--ignore-scripts", + ]), + "bun pm pack", + ); + const packedFiles = await readdir(temporaryRoot); + const tarballName = packedFiles.find((file) => file.endsWith(".tgz")); + if (tarballName === undefined) { + throw new Error("bun pm pack completed without creating a tarball"); + } + const tarball = join(temporaryRoot, tarballName); + + await writeFile( + join(consumerRoot, "package.json"), + '{"name":"wellcrafted-package-smoke","private":true,"type":"module"}\n', + ); + await cp(FIXTURE_ROOT, consumerRoot, { recursive: true }); + await cp(RUNTIME_FIXTURE, join(consumerRoot, "runtime.mjs")); + + assertPassed( + await run(["bun", "add", "--exact", tarball], { cwd: consumerRoot }), + "installing the packed package", + ); + + for (const moduleResolution of ["Bundler", "NodeNext"] as const) { + const config = await writeTsconfig({ + consumerRoot, + file: "non-query-subpaths.ts", + moduleResolution, + name: `non-query-${moduleResolution.toLowerCase()}`, + }); + assertPassed( + await typecheck(consumerRoot, config), + `${moduleResolution} non-query consumer typecheck`, + ); + } + + const missingDependencyConfig = await writeTsconfig({ + consumerRoot, + file: "query-prerequisite.ts", + moduleResolution: "NodeNext", + name: "query-without-tanstack", + }); + const missingDependency = await typecheck( + consumerRoot, + missingDependencyConfig, + ); + const missingDependencyOutput = + missingDependency.stdout + missingDependency.stderr; + if ( + missingDependency.exitCode === 0 || + !missingDependencyOutput.includes( + "Cannot find module '@tanstack/query-core'", + ) + ) { + throw new Error( + `wellcrafted/query unexpectedly typechecked without @tanstack/query-core.\n${missingDependencyOutput}`, + ); + } + + assertPassed( + await run( + [ + "bun", + "add", + "--dev", + "--exact", + `@tanstack/query-core@${TANSTACK_QUERY_VERSION}`, + ], + { cwd: consumerRoot }, + ), + "installing the explicit TanStack Query prerequisite", + ); + + for (const moduleResolution of ["Bundler", "NodeNext"] as const) { + const config = await writeTsconfig({ + consumerRoot, + file: "query.ts", + moduleResolution, + name: `query-${moduleResolution.toLowerCase()}`, + }); + assertPassed( + await typecheck(consumerRoot, config), + `${moduleResolution} query consumer typecheck`, + ); + } + + assertPassed( + await run(["bun", "runtime.mjs"], { cwd: consumerRoot }), + "packed-package runtime smoke", + ); + + console.log( + "package smoke: tarball, nine subpaths, unsupported root, strict typechecks, and query prerequisite passed", + ); + } finally { + await rm(temporaryRoot, { force: true, recursive: true }); + } +} + +await main(); diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 2878fb0..464cd3d 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -533,10 +533,19 @@ Verification on 2026-07-10: ### Wave 2: Add examples and isolated package proof -- [ ] Add the four canonical learning examples. -- [ ] Add strict example typechecking and runnable offline examples. -- [ ] Add isolated packed-consumer, compatibility-type, and runtime fixtures. -- [ ] Wire only checks that already pass into CI. +- [x] Add the four canonical learning examples. +- [x] Add strict example typechecking and runnable offline examples. +- [x] Add isolated packed-consumer, compatibility-type, and runtime fixtures. +- [x] Wire only checks that already pass into CI. + +Verification on 2026-07-10: + +- `bun run docs:examples` built the package, typechecked all four examples with TypeScript 5.8.3, `strict: true`, and `skipLibCheck: false`, then ran the quick-start, service-boundary, and serialization-boundary examples offline. The TanStack Query example is typechecked but intentionally not executed as part of the learning path. +- `bun run compat:types` passed against all nine subpaths under both Bundler and NodeNext resolution. The fixture uses the ES2024 target with the ESNext and DOM libraries and keeps library checking enabled. This is compatibility evidence for the later installation decision, not yet a broader public support promise. +- `bun run package:smoke` packed the built package with `bun pm pack`, installed it in a temporary consumer outside the repository package boundary, and passed strict Bundler and NodeNext consumer typechecks. It confirmed that `wellcrafted/query` fails typechecking without `@tanstack/query-core`, then passed with the explicit pinned `@tanstack/query-core@5.82.0` prerequisite. Its runtime fixture imported all nine subpaths, invoked `partitionResults` and `composeSinks`, and confirmed that the unsupported root import rejects. +- The runtime fixture passed under Bun 1.3.1, Node 22.17.0, and Node 24.4.1. CI now reruns the Bun fixture in the main job and the same fixture in Node 22 and Node 24 matrix jobs; those maintained-major jobs do not expand the package's undeclared metadata by themselves. +- The main CI job now runs `docs:examples`, `package:smoke`, `compat:types`, and `compat:runtime` after the existing build and test gates. The Bun runtime used there is pinned to 1.3.1. +- The final local pass also completed the frozen install, formatting check, typecheck, build, 158-test suite, Mint validation, Mint link check, and `git diff --check`. Lint exited successfully with the same 12 pre-existing warnings recorded in Wave 1 and no new warning from Wave 2 files. ### Wave 3: Replace the front door and start path From ab659bdd11d7caf5a422862d966089279d5d0382 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:47:59 -0700 Subject: [PATCH 04/13] docs: rebuild the front door and start path Lead with boundary-friendly error data and keep the JSON-compatibility limitation explicit. Add checked installation, quick-start, migration, and contributor paths without cutting over or deleting legacy owners. --- CONTRIBUTING.md | 74 +++++ README.md | 293 ++++-------------- docs/docs.json | 2 +- docs/index.mdx | 85 +---- docs/snippets/quick-start.mdx | 34 ++ docs/start/installation.mdx | 105 +++++++ docs/start/migrating-from-try-catch.mdx | 95 ++++++ docs/start/quick-start.mdx | 62 ++++ examples/quick-start.ts | 24 +- package.json | 15 +- scripts/check-quick-start-docs.ts | 70 +++++ ...10T012026-greenfield-documentation-pass.md | 20 +- 12 files changed, 554 insertions(+), 325 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/snippets/quick-start.mdx create mode 100644 docs/start/installation.mdx create mode 100644 docs/start/migrating-from-try-catch.mdx create mode 100644 docs/start/quick-start.mdx create mode 100644 scripts/check-quick-start-docs.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..78752df --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing to wellcrafted + +wellcrafted uses Bun for installation, scripts, tests, and release tooling. Consumer setup belongs in the [installation guide](docs/start/installation.mdx); this page covers work inside the repository. + +## Set up the repository + +CI currently uses Bun 1.3.1. Install dependencies from the lockfile: + +```bash +bun install --frozen-lockfile +``` + +Use `bun install` when intentionally changing dependencies, and commit the resulting `bun.lock` update with the package change. + +Mint requires Node.js 24 for local documentation commands. The library's tested runtime matrix is separate from this documentation-tool requirement. + +## Run checks + +Before opening a pull request, run the checks relevant to your change. The full local pass is: + +```bash +bun run lint:check +bun run format:check +bun run typecheck +bun run build +bun test +bun run docs:examples +bun run package:smoke +bun run compat:types +bun run compat:runtime +``` + +`lint:check` and `format:check` do not write files. Use `bun run lint` and `bun run format` when you want Biome to apply fixes. + +Run Mint under Node 24 when documentation changes: + +```bash +bun run docs:validate +bun run docs:links +``` + +Preview the site with `bun run docs:dev`. The command runs the pinned Mint binary from `docs/`. + +## Work on documentation + +Runnable learning code belongs in `examples/`. Documentation can include or extract that code, but a check must catch drift from the canonical file. Keep partial snippets short and label them when surrounding application code is intentionally omitted. + +Each public page should own one reader question: + +- `docs/start/` gets a reader installed and through the first Result. +- `docs/guides/` owns task-oriented application workflows. +- `docs/reference/` owns exact current exports and signatures. +- `docs/integrations/` owns third-party boundary adaptation. +- `docs/decisions/` owns design rationale and tradeoffs. + +Use lowercase `wellcrafted` in prose. Treat JSON serialization as a convention: `defineErrors` does not enforce JSON-compatible fields, and preserving a JSON shape is separate from runtime validation and end-to-end static typing. + +## Add a changeset when behavior changes + +Documentation-only changes do not need a changeset. Add one when a pull request changes the installed package's public API or runtime behavior: + +```bash +bunx changeset +``` + +Write the summary for a package consumer. While wellcrafted remains on `0.x`, use a minor changeset for a breaking change rather than a major changeset; CI rejects a major bump because npm cannot reuse the unpublished `1.0.0` version. + +Do not run the release script as part of a normal contribution. The release workflow consumes changesets after changes land on `main`. + +## Open a focused pull request + +Keep the pull request to one coherent change. Explain what changed, why it changed, and the tradeoffs a reviewer should check. Add focused tests for behavior changes and update canonical examples or documentation when a public contract changes. + +List the validation commands you actually ran and call out anything you could not run. CI must pass before release automation can publish from `main`. diff --git a/README.md b/README.md index c10be62..af37e45 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,12 @@ # wellcrafted [![npm version](https://badge.fury.io/js/wellcrafted.svg)](https://www.npmjs.com/package/wellcrafted) -[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Bundle Size](https://img.shields.io/bundlephobia/minzip/wellcrafted)](https://bundlephobia.com/package/wellcrafted) -*Define your errors. Type the rest.* +wellcrafted defines expected errors as plain, boundary-friendly data that can move through JSON, HTTP, workers, IPC, logs, and UI when every field is JSON-compatible. Its familiar `{ data, error }` Result shape makes that data ergonomic to return and handle in ordinary TypeScript. -Tagged errors and Result types as plain objects. < 2KB, zero dependencies. +`defineErrors` does not type-enforce JSON-compatible fields, and arbitrary fields do not serialize perfectly. The boundary-friendly promise applies only when the complete error value contains JSON-compatible data. Runtime validation and end-to-end static typing are separate concerns. -`try/catch` throws away your error's type the moment you catch it. You get `catch (error: unknown)` and you're guessing again. And a thrown `Error` travels badly: `JSON.stringify(new Error("boom"))` is `{}`, so the message vanishes the moment it hits a log line, a Web Worker, or an IPC boundary, where `instanceof` stops working too. - -wellcrafted fixes both. Define your errors once as plain data, return them instead of throwing, and check them with the `{ data, error }` shape you already know from Supabase, SvelteKit load functions, and TanStack Query. No `.isOk()` method chains, no `.map().andThen().orElse()` pipelines. Check `error`, use `data`. - -```typescript -import { defineErrors } from "wellcrafted/error"; -import { trySync } from "wellcrafted/result"; - -// The key becomes error.name. The fields you return are typed on the error. -const { ParseError } = defineErrors({ - ParseError: ({ path }: { path: string }) => ({ - message: `Could not parse ${path}`, - path, - }), -}); - -const { data, error } = trySync({ - try: () => JSON.parse(raw), - catch: () => ParseError({ path: "config.json" }), -}); - -if (error) { - // error is { name: "ParseError"; message: string; path: string } - console.error(error.message, error.path); -} else { - // data is the parsed value, error is null - use(data); -} -``` - -That's the whole idea: define an error, wrap the throwing call, destructure the result, check `error`. A *tagged error* is just an object with a `name` field you can `switch` on. Everything below is that pattern at scale. +wellcrafted is an error-handling library for TypeScript application authors. Define named failure variants as objects, return them through Results, and handle them with `async/await`, early returns, exact checks, and `switch`. ## Install @@ -47,236 +14,82 @@ That's the whole idea: define an error, wrap the throwing call, destructure the npm install wellcrafted ``` -## A real service - -Here is the pattern in shipping code, lightly trimmed from [Whispering](https://github.com/EpicenterHQ/epicenter)'s transcription layer. Each service owns a small vocabulary of things that can go wrong, declared up front with `defineErrors`. - -```typescript -import { - defineErrors, - extractErrorMessage, - type InferErrors, -} from "wellcrafted/error"; -import { type Result, tryAsync } from "wellcrafted/result"; - -export const ElevenLabsError = defineErrors({ - MissingApiKey: () => ({ message: "ElevenLabs API key is required" }), - FileTooLarge: ({ sizeMb, maxMb }: { sizeMb: number; maxMb: number }) => ({ - message: `File ${sizeMb.toFixed(1)}MB exceeds ${maxMb}MB limit`, - sizeMb, - maxMb, - }), - Unexpected: ({ cause }: { cause: unknown }) => ({ - message: extractErrorMessage(cause), - cause, - }), -}); -export type ElevenLabsError = InferErrors; - -async function transcribe( - audio: Blob, - apiKey: string, -): Promise> { - if (!apiKey) return ElevenLabsError.MissingApiKey(); // a factory already is an Err - - const sizeMb = audio.size / (1024 * 1024); - if (sizeMb > 1000) return ElevenLabsError.FileTooLarge({ sizeMb, maxMb: 1000 }); - - return tryAsync({ - try: () => callElevenLabs(apiKey, audio), // may throw - catch: (cause) => ElevenLabsError.Unexpected({ cause }), - }); -} -``` - -The caller checks `error`, then `switch`es on `error.name` to handle each case with the right fields in scope: - -```typescript -const { data, error } = await transcribe(audio, apiKey); -if (error) { - switch (error.name) { - case "MissingApiKey": return promptForKey(); - case "FileTooLarge": return warn(`Audio too large: ${error.sizeMb}MB`); - case "Unexpected": return report(error.cause); - } -} -showTranscript(data); // data is string, error is null -``` - -Three things are doing the work here, and the rest of this README is just those three things. - -## Define your errors - -You can put any value in `Ok` and `Err`. So why `defineErrors`? - -Because errors aren't random. A function fails in a handful of known ways, and a namespace is where you enumerate them up front: the closed set of what can go wrong. A user service fails with `AlreadyExists`, `CreateFailed`, or `InvalidEmail`, and nothing else. That set is exactly a Rust error enum: the namespace is the enum, each key is a variant, and `switch (error.name)` is the `match`. `defineErrors` brings the [thiserror](https://docs.rs/thiserror) pattern to TypeScript as plain objects instead of classes. - -Enumerating the set up front is what makes the rest pay off. The union flows into your `Result` signature, so a caller sees every way the call can fail right in the type, and a `switch` with a [`never` guard](#exhaustiveness) turns a forgotten variant into a compile error. - -Each key becomes a variant. Your constructor returns `{ message, ...fields }`; `defineErrors` stamps the key on as `name` and hands back a factory that returns `Err<...>` directly. `InferErrors` extracts the union of every variant for your `Result` signatures. - -```typescript -const UserError = defineErrors({ - AlreadyExists: ({ email }: { email: string }) => ({ - message: `User ${email} already exists`, - email, - }), - CreateFailed: ({ email, cause }: { email: string; cause: unknown }) => ({ - message: `Failed to create user ${email}: ${extractErrorMessage(cause)}`, - email, - cause, - }), -}); -type UserError = InferErrors; -// ^? { name: "AlreadyExists"; message: string; email: string } -// | { name: "CreateFailed"; message: string; email: string; cause: unknown } -``` - -Two properties make this pay off: - -**Errors are data, not classes.** Plain frozen objects, no prototype chain. The fields you put on them are plain own properties, so they survive `JSON.stringify`, a Web Worker, or an IPC hop with no `stack` noise and no `instanceof` that breaks across package boundaries. The error you create is the error that arrives. +wellcrafted has no root export. Import from a published subpath such as `wellcrafted/error` or `wellcrafted/result`. See the [installation guide](docs/start/installation.mdx) for every subpath and the tested compiler and runtime matrix. -**Every factory returns `Err<...>` directly.** No wrapping step. Return it from a `tryAsync` catch handler or as a standalone early return (`if (existing) return UserError.AlreadyExists({ email })`). The `Result` type flows out naturally. +## Quick start -## Wrap throwing code +Define the failures a function can return, then use the Result shape to handle both outcomes. -`trySync` and `tryAsync` turn a throwing operation into a `Result`. The `catch` handler receives the raw error and returns one of your `defineErrors` variants. Anything you don't wrap keeps throwing exactly as before, so you can adopt this one function at a time. + +```typescript quick-start.ts +import { defineErrors, type InferErrors } from "wellcrafted/error"; +import { Ok, type Result } from "wellcrafted/result"; -```typescript -import { trySync, tryAsync, Ok } from "wellcrafted/result"; - -// Synchronous -const { data, error } = trySync({ - try: () => JSON.parse(rawInput), - catch: (cause) => JsonError.ParseFailed({ input: rawInput, cause }), -}); - -// Asynchronous -const { data, error } = await tryAsync({ - try: () => fetch(url).then((r) => r.json()), - catch: (cause) => HttpError.Connection({ url, cause }), -}); -``` - -When `catch` returns `Ok(fallback)` instead of an error, there is no error branch left: the return type narrows to `Ok`, so `error` is always `null` and you can skip the check. - -```typescript -const { data: items } = trySync({ - try: (): string[] => JSON.parse(riskyJson), - catch: () => Ok([] as string[]), // recovered; there is no error to check +const PortError = defineErrors({ + Invalid: ({ input }: { input: string }) => ({ + message: `Expected a port from 1 to 65535, received "${input}".`, + input, + }), }); -``` -## Compose across layers +type PortError = InferErrors; -Each layer defines its own vocabulary and folds the layer below into a `cause` field. `extractErrorMessage` formats that cause inside the factory, so call sites stay clean. You propagate with a plain `if (error) return` (there is no `?` operator; see [what you give up](#what-you-give-up)). +function parsePort(input: string): Result { + const port = Number(input); + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + return PortError.Invalid({ input }); + } -```typescript -async function getUser(userId: string): Promise> { - const { data: response, error } = await tryAsync({ - try: () => fetch(`/api/users/${userId}`), - catch: (cause) => UserServiceError.FetchFailed({ userId, cause }), - // raw fetch error becomes cause ^^^^^ - }); - if (error) return Err(error); // propagate as-is - - if (response.status === 404) return UserServiceError.NotFound({ userId }); - return Ok(await response.json()); + return Ok(port); } -``` - -Your tagged fields are plain data, so the chain logs and crosses the wire cleanly. The raw `cause` is the exception: it serializes only as well as whatever you caught, which is why the factories above fold it through `extractErrorMessage` into the `message` string. - -## The Result type - -The foundation is one discriminated union: - -```typescript -type Ok = { data: T; error: null }; -type Err = { error: E; data: null }; -type Result = Ok | Err; -``` - -Check `error` first and TypeScript narrows `data` for you: -```typescript -const { data, error } = await someOperation(); -if (error) return; // error is E, data is null -// data is T, error is null -``` - -`if (error)` works because errors from `defineErrors` are always objects, and an object is truthy. The exact check is `error !== null` (or the `isErr` guard); reach for it if you ever put a falsy value like `0` or `""` in an `Err`. - -### Exhaustiveness - -`switch (error.name)` narrows each case, and your editor autocompletes every variant. To make a *new* variant a compile error until it is handled, add a `never` check in `default`: +const success = parsePort("3000"); +if (success.error !== null) { + console.error(success.error.message); +} else { + console.log(`Listening on port ${success.data}.`); +} -```typescript -switch (error.name) { - case "NotFound": return notFound(); - case "FetchFailed": return badGateway(); - default: - error satisfies never; // add a variant and this line fails to compile +const failure = parsePort("not-a-port"); +if (failure.error !== null) { + console.error(failure.error.message); } ``` + -Plain TypeScript does not enforce exhaustive `switch` on its own; this one line is how you opt in. - -## What you give up - -wellcrafted is not an effect system, and the honest cost is control flow. There is no `?` operator, so you propagate with an explicit `if (error) return` at each step. There is no dependency injection, no automatic short-circuiting, and no built-in concurrency. If you need those, reach for [Effect](https://effect.website); that is what it is for. +The example is extracted from the checked [`examples/quick-start.ts`](examples/quick-start.ts) source. The documentation checks compare both copies with that file so they cannot drift unnoticed. -In exchange, the whole API is `{ data, error }`, `async/await`, and `switch`: no new runtime, no generators, no pipe operators to learn. +`PortError.Invalid({ input })` returns an `Err` directly. The tagged body is under `.error`, while `InferErrors` derives the union of every variant for the function signature. `Ok(port)` returns the other half of the union. -## Also in the box +The exact `error !== null` check narrows both fields. A shorter `if (error)` is fine for object errors created by `defineErrors`, but it is not a universal Result check because the current `Err` type permits falsy values such as `0` and `""`. -Each lives behind its own subpath import, so you pay for only what you use. +Run the canonical success and failure paths with Bun: -- `wellcrafted/brand`: `Brand` makes distinct types from primitives (`type UserId = string & Brand<"UserId">`) so the compiler catches mix-ups. Zero runtime. -- `wellcrafted/logger`: a small DI-based structured logger keyed on log level, built to take your `defineErrors` types directly. No global singleton. -- `wellcrafted/testing`: `expectOk` / `expectErr` unwrap a `Result` in a test, or throw with a readable message. -- `wellcrafted/json`: `parseJson` is `JSON.parse` that returns a `Result` instead of throwing. -- `wellcrafted/query`: TanStack Query adapters for Result-returning functions. Queries expose `.options`, `.fetch`, and `.ensure`; mutations are callable and expose `.options`. -- `wellcrafted/standard-schema`: wrap a `Result` as a [Standard Schema](https://github.com/standard-schema/standard-schema) for validators that speak the spec. - -## How it compares - -| | wellcrafted | neverthrow | better-result | fp-ts | Effect | -|---|---|---|---|---|---| -| Error definition | `defineErrors` factories | Bring your own | `TaggedError` classes | Bring your own | Class-based with `_tag` | -| Error shape | Plain frozen objects | Any type | Class instances | Any type | Class instances | -| Composition | Manual `if (error)` | `.map().andThen()` | `Result.gen()` generators | Pipe operators | `yield*` generators | -| Bundle size | < 2KB | ~5KB | ~2KB | ~30KB | ~50KB | -| Syntax | async/await | Method chains | Method chains + generators | Pipe operators | Generators | - -Every Result library hands you a container. wellcrafted hands you what goes inside it, then gets out of the way. - -## API at a glance +```bash +bun run docs:examples +``` -From `wellcrafted/result`: +## Errors at application boundaries -- `Ok(data)` / `Err(error)`: construct a success or failure -- `trySync` / `tryAsync`: wrap a throwing operation, sync or async -- `Result`: the `Ok | Err` union +Named object variants let an application use the same error vocabulary in a service return type, a JSON-compatible HTTP payload, a structured log, and UI branching. There is no error-class transformation layer when the complete value is already JSON-compatible. -From `wellcrafted/error`: +That condition matters. A `Date`, native `Error`, `bigint`, function, class instance, `undefined`, or cyclic value does not have an exact JSON round trip. `defineErrors` accepts those fields because serializability is a convention, not a type constraint. Validate untrusted wire data separately; preserving `{ data, error }` does not prove its payload types. -- `defineErrors(config)`: define a namespace of error variant factories -- `extractErrorMessage(value)`: pull a readable string out of any `unknown` -- `InferErrors`: the union of all variants; `InferError`: one variant +The checked [service-boundary example](examples/service-boundary.ts) shows a narrow throwing boundary, a named domain failure, and manual propagation. It is adapted from [Epicenter's transcription service at commit `4d438c0`](https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/client/src/transcribe.ts). -Less common but there when you need them: `isOk` / `isErr` type guards, `unwrap` (extract or throw), `partitionResults` (split an array of Results), and `resolve` (handle values that may or may not be Results). +## The tradeoff -## Teach your AI agent +wellcrafted keeps control flow visible. There is no `?` operator or automatic short-circuiting, so each propagation step is an explicit early return. It also does not provide dependency injection, structured concurrency, or resource management. -If you use an AI coding agent, install the skills that teach it the patterns and anti-patterns directly: +If an application needs those capabilities across many layers, use an effect system such as [Effect](https://effect.website). wellcrafted is for the smaller job: typed expected failures as data, handled with ordinary TypeScript. -```bash -npx skills add wellcrafted-dev/wellcrafted -``` +## Documentation -This installs five skills: `define-errors`, `result-types`, `query-factories`, `branded-types`, and `patterns` (the architectural style guide). They work with any agent that supports [`npx skills`](https://www.npmjs.com/package/skills). Install once, update with `npx skills update`. +- [Install wellcrafted](docs/start/installation.mdx) +- [Run the quick start](docs/start/quick-start.mdx) +- [Migrate one throwing boundary](docs/start/migrating-from-try-catch.mdx) +- [Contribute to wellcrafted](CONTRIBUTING.md) -## License +## Agent skills -MIT +As an optional convenience, the repository includes distributable skills for coding agents that teach wellcrafted's core patterns. Install them with `npx skills add wellcrafted-dev/wellcrafted`; the library and documentation do not depend on them. diff --git a/docs/docs.json b/docs/docs.json index 32278a9..5451197 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,7 +1,7 @@ { "$schema": "https://mintlify.com/docs.json", "theme": "maple", - "name": "WellCrafted", + "name": "wellcrafted", "colors": { "primary": "#3B82F6", "light": "#60A5FA", diff --git a/docs/index.mdx b/docs/index.mdx index a7a715d..f41c147 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1,89 +1,38 @@ --- title: wellcrafted -description: 'Tagged errors and Result types as plain objects. Under 2KB, zero dependencies.' +description: 'Plain error data and ergonomic Result types for TypeScript applications.' --- # wellcrafted -Tagged errors and Result types as plain objects. Under 2KB, zero dependencies. +wellcrafted defines expected errors as plain, boundary-friendly data that can move through JSON, HTTP, workers, IPC, logs, and UI when every field is JSON-compatible. Its familiar `{ data, error }` Result shape makes that data ergonomic to return and handle in ordinary TypeScript. -wellcrafted gives you a Rust-inspired `Result` type and a `defineErrors` helper that puts the errors a function can return into its signature, instead of hiding them behind `throw`. The errors are plain frozen objects, so they survive `JSON.stringify`, a Web Worker, or an IPC boundary intact. +`defineErrors` does not type-enforce JSON-compatible fields, and arbitrary fields do not serialize perfectly. Runtime validation and end-to-end static typing are separate boundary concerns. -If you just want the fast tour, the [README](https://github.com/wellcrafted-dev/wellcrafted) covers what, why, and how in one screen. These pages go deeper. +## Start here - - The essential patterns, in five minutes + + Choose a package subpath and check the tested compiler and runtime matrix. - - A deep dive into the Result type + + Define one expected failure and handle both Result paths. - - Real-world implementations + + Introduce Results one throwing boundary at a time. -## The problem it solves - -Nothing in this signature tells you what can go wrong: - -```typescript -// Which of these throws, and with what? -async function saveUser(user: User): Promise { - await validate(user); - await checkPermissions(); - await database.save(user); -} -``` - -With a `Result` return type, every failure is in the type, and the caller has to deal with it: - -```typescript -async function saveUser( - user: User, -): Promise> { - const validation = await validate(user); - if (validation.error) return validation; - - const auth = await checkPermissions(); - if (auth.error) return auth; - - return database.save(user); -} -``` - -The core is about fifty lines of code you can read in one sitting. There is no runtime, no generators, and no pipe operators: just `{ data, error }`, `async/await`, and `switch`. - -## The primitives - - - - Explicit success and failure states you check with `{ data, error }` - - - `defineErrors` variants: structured, serializable, switchable on `name` - - - Distinct types from primitives, caught at compile time - - - -## Why it works this way - -`defineErrors` is modeled on Rust's [thiserror](https://docs.rs/thiserror): name the handful of things that can go wrong in a domain, up front, as data. The rest of the design follows from picking shapes you already know (`{ data, error }` from Supabase and SvelteKit) over inventing new ones. - -Read more on the [design principles](/philosophy/design-principles), the [Rust inspiration](/philosophy/rust-inspiration) behind `defineErrors`, and why we lean on [developer experience](/philosophy/developer-experience) over machinery. - -## Going further +## Continue by task - - Layered services that each own their error vocabulary + + Adapt Result-returning functions to TanStack's throwing contract. - - Result-returning queries and mutations, reactive or imperative + + Combine compile-time brands with ArkType, Zod, or Valibot validation. - - Adopt it one function at a time + + Inspect the checked quick-start, service, serialization, and query scenarios. diff --git a/docs/snippets/quick-start.mdx b/docs/snippets/quick-start.mdx new file mode 100644 index 0000000..bb41aa0 --- /dev/null +++ b/docs/snippets/quick-start.mdx @@ -0,0 +1,34 @@ +```typescript quick-start.ts +import { defineErrors, type InferErrors } from "wellcrafted/error"; +import { Ok, type Result } from "wellcrafted/result"; + +const PortError = defineErrors({ + Invalid: ({ input }: { input: string }) => ({ + message: `Expected a port from 1 to 65535, received "${input}".`, + input, + }), +}); + +type PortError = InferErrors; + +function parsePort(input: string): Result { + const port = Number(input); + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + return PortError.Invalid({ input }); + } + + return Ok(port); +} + +const success = parsePort("3000"); +if (success.error !== null) { + console.error(success.error.message); +} else { + console.log(`Listening on port ${success.data}.`); +} + +const failure = parsePort("not-a-port"); +if (failure.error !== null) { + console.error(failure.error.message); +} +``` diff --git a/docs/start/installation.mdx b/docs/start/installation.mdx new file mode 100644 index 0000000..c0b3609 --- /dev/null +++ b/docs/start/installation.mdx @@ -0,0 +1,105 @@ +--- +title: 'Install wellcrafted' +description: 'Install wellcrafted and choose the supported subpath for your application.' +icon: 'download' +--- + +# Install wellcrafted + +Install `wellcrafted` with your application's package manager. + + + + ```bash + npm install wellcrafted + ``` + + + ```bash + pnpm add wellcrafted + ``` + + + ```bash + yarn add wellcrafted + ``` + + + ```bash + bun add wellcrafted + ``` + + + +## Import a subpath + +wellcrafted has no root export. Importing from `"wellcrafted"` is unsupported; choose the subpath that owns the API you need. + +```typescript +import { defineErrors } from "wellcrafted/error"; +import { Ok, type Result } from "wellcrafted/result"; +``` + +The package publishes nine subpaths: + +| Subpath | Purpose | +| --- | --- | +| `wellcrafted/result` | Result values, guards, throwing-boundary helpers, and Result utilities | +| `wellcrafted/error` | Named error variants and error-message extraction | +| `wellcrafted/logger` | Composable logging sinks and loggers | +| `wellcrafted/json` | JSON value types and Result-returning JSON parsing | +| `wellcrafted/brand` | Compile-time branded values | +| `wellcrafted/function` | Small function utilities | +| `wellcrafted/query` | TanStack Query adapters and factories | +| `wellcrafted/standard-schema` | Standard Schema adaptation | +| `wellcrafted/testing` | Result assertions for tests | + +## Tested configurations + +The current consumer fixtures prove these configurations. They are tested configurations, not minimum-version guarantees for every TypeScript or JavaScript runtime. + +| Area | Tested configuration | +| --- | --- | +| TypeScript | 5.8.3 with `strict: true` and `skipLibCheck: false` | +| Module resolution | `Bundler` and `NodeNext` | +| Compiler target and libraries | `ES2024` target with `ESNext` and `DOM` libraries | +| Bun | 1.3.1 | +| Node.js | 22.17.0 and 24.4.1 | + +wellcrafted is ESM-only. The compatibility fixtures import all nine subpaths from the packed package and exercise runtime-sensitive APIs; they do not establish a broader promise for untested configurations. + +## TanStack Query prerequisite + +The declarations for `wellcrafted/query` import `@tanstack/query-core`. The current package does not declare that dependency for consumers, so install it explicitly when you use the query subpath. The packed-consumer fixture proves version 5.82.0. + + + + ```bash + npm install wellcrafted @tanstack/query-core@5.82.0 + ``` + + + ```bash + pnpm add wellcrafted @tanstack/query-core@5.82.0 + ``` + + + ```bash + yarn add wellcrafted @tanstack/query-core@5.82.0 + ``` + + + ```bash + bun add wellcrafted @tanstack/query-core@5.82.0 + ``` + + + + + + Define one error vocabulary and handle both Result paths. + + + Set up the repository with Bun and run its checks. + + diff --git a/docs/start/migrating-from-try-catch.mdx b/docs/start/migrating-from-try-catch.mdx new file mode 100644 index 0000000..0861844 --- /dev/null +++ b/docs/start/migrating-from-try-catch.mdx @@ -0,0 +1,95 @@ +--- +title: 'Migrate from try/catch' +description: 'Adopt typed expected failures one throwing boundary at a time.' +icon: 'arrows-rotate' +--- + +# Migrate from try/catch + +Start with one operation whose expected failures matter to its caller. Do not convert every `throw`: programmer errors and invariants can keep using exceptions, while expected I/O and domain failures become Result data. + +## 1. Wrap the narrow throwing operation + +Suppose an existing dependency reads configuration text and may throw: + +```typescript +async function loadConfig(read: () => Promise): Promise { + return read(); +} +``` + +Define the failure the application needs, then wrap only the call that throws: + +```typescript +import { + defineErrors, + extractErrorMessage, + type InferErrors, +} from "wellcrafted/error"; +import { type Result, tryAsync } from "wellcrafted/result"; + +const ConfigError = defineErrors({ + ReadFailed: ({ reason }: { reason: string }) => ({ + message: `Could not read the configuration: ${reason}`, + reason, + }), +}); +type ConfigError = InferErrors; + +async function loadConfig( + read: () => Promise, +): Promise> { + return tryAsync({ + try: read, + catch: (cause) => + ConfigError.ReadFailed({ reason: extractErrorMessage(cause) }), + }); +} +``` + +The raw caught value is `unknown`. This example turns it into a string field that is useful to the caller and compatible with JSON. Keeping a raw `cause: unknown` is also allowed, but `defineErrors` does not make that value serializable. + +## 2. Update the direct caller + +Handle or propagate the Result with ordinary control flow. This partial caller omits the application's storage, reporting, and startup functions: + +```typescript +const result = await loadConfig(readFromDisk); +if (result.error !== null) { + report(result.error.message); + return; +} + +startApplication(result.data); +``` + +Move upward one caller at a time. A layer can return the same error, transform it into its own vocabulary, recover with `Ok(fallback)`, or turn it back into a throw where an external contract requires one. + +## 3. Keep a compatibility adapter + +Callers do not all need to migrate together. Keep the old throwing contract at the edge while new code consumes the Result-returning function: + +```typescript +async function loadConfigOrThrow( + read: () => Promise, +): Promise { + const result = await loadConfig(read); + if (result.error !== null) { + throw new Error(result.error.message, { cause: result.error }); + } + + return result.data; +} +``` + +This adapter makes the boundary explicit. If the migration needs to roll back, unchanged callers can continue using it while the Result-returning function is removed or revised. Delete the adapter only after its callers have moved. + +## 4. Check the cost before continuing + +Stop when explicit propagation becomes more ceremony than clarity. wellcrafted does not provide automatic short-circuiting, dependency injection, structured concurrency, or resource management. If the migration needs those capabilities across many layers, an effect system such as [Effect](https://effect.website) may be the better model. + +The repository's checked [`service-boundary.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/examples/service-boundary.ts) shows the same incremental pattern in a complete service. It is adapted from [Epicenter's transcription service at commit `4d438c0`](https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/client/src/transcribe.ts). + + + A Result-shaped JSON value preserves a container shape only when its fields are JSON-compatible. That does not validate an untrusted response or give `response.json()` an exact static type. Validate at the receiving boundary when the data is not already trusted. + diff --git a/docs/start/quick-start.mdx b/docs/start/quick-start.mdx new file mode 100644 index 0000000..f9fd5bd --- /dev/null +++ b/docs/start/quick-start.mdx @@ -0,0 +1,62 @@ +--- +title: 'Quick start' +description: 'Define a typed error and handle success and failure with ordinary TypeScript.' +icon: 'play' +--- + +import QuickStartExample from "/snippets/quick-start.mdx"; + +# Quick start + +This example defines one expected failure, returns it through the familiar `{ data, error }` shape, and exercises both outcomes. + + + +The checked source lives in [`examples/quick-start.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/examples/quick-start.ts). The documentation check compares the example above with that file so they cannot drift unnoticed. + +## Define the failure vocabulary + +`defineErrors` turns each key into a factory. `PortError.Invalid({ input })` returns an `Err` value directly: + +```typescript +{ + data: null, + error: { + name: "Invalid", + message: 'Expected a port from 1 to 65535, received "not-a-port".', + input: "not-a-port", + }, +} +``` + +The tagged error body is under `.error`. `InferErrors` derives the union of every variant in the namespace for the function signature. + +## Return and handle the Result + +`Ok(port)` creates `{ data: port, error: null }`. An error factory creates `{ data: null, error: taggedError }`. Checking `error !== null` narrows the matching `data` field without a method chain or a new control-flow model. + +The exact null check works for any Result that follows the non-null-error convention. A shorter `if (error)` check also works for object errors from `defineErrors`, but it misclassifies permitted falsy errors such as `0` or an empty string. + +## Run the repository example + +The repository uses Bun: + +```bash +bun install --frozen-lockfile +bun run docs:examples +``` + +The example prints one successful port, one expected validation message, and a final confirmation that both paths passed. + + + `defineErrors` requires `message`, reserves `name`, and shallow-freezes the tagged body. It does not require the remaining fields to be JSON-compatible. If this Result crosses a JSON boundary, every field in the complete value must be JSON-compatible, and the receiver must validate untrusted data separately. + + + + + Introduce Results without rewriting an application at once. + + + Review subpaths and the tested compatibility matrix. + + diff --git a/examples/quick-start.ts b/examples/quick-start.ts index 371ae32..781a4e6 100644 --- a/examples/quick-start.ts +++ b/examples/quick-start.ts @@ -1,6 +1,7 @@ +// docs:quick-start:start + import { defineErrors, type InferErrors } from "wellcrafted/error"; import { Ok, type Result } from "wellcrafted/result"; -import { assertEqual } from "./assert.js"; const PortError = defineErrors({ Invalid: ({ input }: { input: string }) => ({ @@ -21,10 +22,25 @@ function parsePort(input: string): Result { } const success = parsePort("3000"); -assertEqual(success, { data: 3000, error: null }); +if (success.error !== null) { + console.error(success.error.message); +} else { + console.log(`Listening on port ${success.data}.`); +} const failure = parsePort("not-a-port"); -assertEqual(failure.data, null); -assertEqual(failure.error?.name, "Invalid"); +if (failure.error !== null) { + console.error(failure.error.message); +} + +// docs:quick-start:end + +if (success.data !== 3000 || success.error !== null) { + throw new Error("Expected the valid port to succeed."); +} + +if (failure.data !== null || failure.error?.name !== "Invalid") { + throw new Error("Expected the invalid port to fail."); +} console.log("quick-start: success and failure paths passed"); diff --git a/package.json b/package.json index b577753..70c926a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wellcrafted", "version": "0.44.0", - "description": "Delightful TypeScript patterns for elegant, type-safe applications", + "description": "Plain error data and ergonomic Result types for TypeScript applications", "type": "module", "files": [ "dist", @@ -59,21 +59,20 @@ "docs:dev": "cd docs && mint dev", "docs:validate": "cd docs && mint validate", "docs:links": "cd docs && mint broken-links", - "docs:examples": "bun run build && bun node_modules/typescript/bin/tsc --project examples/tsconfig.json && bun examples/quick-start.ts && bun examples/service-boundary.ts && bun examples/serialization-boundary.ts", + "docs:examples": "bun run build && bun node_modules/typescript/bin/tsc --project examples/tsconfig.json && bun scripts/check-quick-start-docs.ts && bun examples/quick-start.ts && bun examples/service-boundary.ts && bun examples/serialization-boundary.ts", "package:smoke": "bun run build && bun scripts/package-smoke.ts", "compat:types": "bun run build && bun node_modules/typescript/bin/tsc --project scripts/fixtures/compatibility/tsconfig.bundler.json && bun node_modules/typescript/bin/tsc --project scripts/fixtures/compatibility/tsconfig.nodenext.json", "compat:runtime": "bun run build && bun scripts/fixtures/runtime/all-subpaths.mjs" }, "keywords": [ "typescript", - "delightful", - "elegant", - "type-safe", - "well-crafted", - "polished", - "utilities", "result", + "result-types", "error-handling", + "typed-errors", + "structured-errors", + "error-data", + "json", "brand-types" ], "author": "", diff --git a/scripts/check-quick-start-docs.ts b/scripts/check-quick-start-docs.ts new file mode 100644 index 0000000..6d2aba4 --- /dev/null +++ b/scripts/check-quick-start-docs.ts @@ -0,0 +1,70 @@ +const SOURCE_START = "// docs:quick-start:start"; +const SOURCE_END = "// docs:quick-start:end"; +const README_START = ""; +const README_END = ""; + +async function read(path: string): Promise { + return Bun.file(path).text(); +} + +function extractBetween({ + content, + start, + end, + path, +}: { + content: string; + start: string; + end: string; + path: string; +}): string { + const startIndex = content.indexOf(start); + const endIndex = content.indexOf(end); + if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) { + throw new Error(`Could not find the quick-start markers in ${path}.`); + } + + return content.slice(startIndex + start.length, endIndex).trim(); +} + +function extractCodeFence(content: string, path: string): string { + const match = content.match(/```typescript(?:[^\n]*)\n([\s\S]*?)\n```/); + if (!match?.[1]) { + throw new Error(`Could not find the TypeScript code fence in ${path}.`); + } + + return match[1].trim(); +} + +const source = extractBetween({ + content: await read("examples/quick-start.ts"), + start: SOURCE_START, + end: SOURCE_END, + path: "examples/quick-start.ts", +}); + +const readme = extractCodeFence( + extractBetween({ + content: await read("README.md"), + start: README_START, + end: README_END, + path: "README.md", + }), + "README.md", +); + +const snippet = extractCodeFence( + await read("docs/snippets/quick-start.mdx"), + "docs/snippets/quick-start.mdx", +); + +for (const [path, content] of [ + ["README.md", readme], + ["docs/snippets/quick-start.mdx", snippet], +] as const) { + if (content !== source) { + throw new Error(`${path} has drifted from examples/quick-start.ts.`); + } +} + +console.log("quick-start docs match the canonical example"); diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 464cd3d..ff61799 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -549,10 +549,22 @@ Verification on 2026-07-10: ### Wave 3: Replace the front door and start path -- [ ] Rewrite README around the approved product sentence and runnable example. -- [ ] Rewrite site index as navigation, not duplicate positioning. -- [ ] Create new installation, quick start, and migration paths while leaving their old files on disk and out of the new navigation until cutover. -- [ ] Add `CONTRIBUTING.md` with Bun, checks, docs workflow, changesets, and PR expectations. +- [x] Rewrite README around the approved product sentence and runnable example. +- [x] Rewrite site index as navigation, not duplicate positioning. +- [x] Create new installation, quick start, and migration paths while leaving their old files on disk and out of the new navigation until cutover. +- [x] Add `CONTRIBUTING.md` with Bun, checks, docs workflow, changesets, and PR expectations. + +Verification on 2026-07-10: + +- The README and site index now use the exact approved serialization-first sentence with the JSON-compatibility limitation immediately beside it. Unsupported size, competitor, reliability, broad runtime, and serialization claims were removed from the front door; agent skills remain a short secondary note. +- `docs/start/installation.mdx` states only the Wave 2-proven TypeScript 5.8.3, Bundler and NodeNext, ES2024 with ESNext and DOM libraries, Bun 1.3.1, and Node 22.17.0 and 24.4.1 configurations. It identifies all nine subpaths, the unsupported root import, ESM-only output, and the explicit tested `@tanstack/query-core@5.82.0` prerequisite without changing package dependency metadata. +- `docs/start/quick-start.mdx` imports the supported Mint reusable snippet at `docs/snippets/quick-start.mdx`. `scripts/check-quick-start-docs.ts`, run by `docs:examples`, proved that both the snippet and README code fence match the documented region of `examples/quick-start.ts`. +- `docs/start/migrating-from-try-catch.mdx` teaches a narrow throwing boundary, staged caller migration, a rollback-compatible throwing adapter, and the Effect escape hatch. It keeps JSON shape preservation separate from runtime validation and static typing and links the attributed Epicenter service pattern. +- The legacy getting-started and migration files remain on disk, and their sidebar routes remain unchanged for the Wave 9 cutover. Only the site name in `docs/docs.json` changed to lowercase `wellcrafted`. +- `CONTRIBUTING.md` records Bun setup, non-mutating checks, Node 24 Mint usage, canonical-example ownership, the documentation-only changeset rule, the pre-1.0 minor-breaking convention, and focused pull-request expectations. `package.json` now uses a factual description and concrete discovery keywords; no changeset was added because this wave does not change installed runtime behavior or public APIs. +- `bun run docs:examples` passed, including strict example typechecking, the focused snippet comparison, and all three offline examples. `bun run package:smoke`, `bun run compat:types`, and `bun run compat:runtime` also passed. +- `bun run lint:check`, `bun run format:check`, `bun run typecheck`, `bun run build`, and `bun test` passed. The test run completed 158 tests with no failures; lint reported only the same 12 pre-existing warnings recorded in Waves 1 and 2. +- Under Node 24.17.0, `bun run docs:validate` and `bun run docs:links` passed. The public-content claim sweep and `git diff --check` also passed. ### Wave 4: Build the guide path From b3dc0385f0bc631202fa8383855259dc7ec22ba4 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:23:59 -0700 Subject: [PATCH 05/13] docs: add task-focused application guides Document error vocabulary design, Result composition, service boundaries, and conditional JSON preservation. Ground public examples in attributed Epicenter patterns while keeping runtime validation and static typing distinct. --- docs/guides/composing-results.mdx | 98 ++++++++++++++++ docs/guides/defining-error-vocabularies.mdx | 93 +++++++++++++++ docs/guides/serialization-boundaries.mdx | 110 ++++++++++++++++++ docs/guides/service-boundaries.mdx | 57 +++++++++ ...10T012026-greenfield-documentation-pass.md | 17 ++- 5 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 docs/guides/composing-results.mdx create mode 100644 docs/guides/defining-error-vocabularies.mdx create mode 100644 docs/guides/serialization-boundaries.mdx create mode 100644 docs/guides/service-boundaries.mdx diff --git a/docs/guides/composing-results.mdx b/docs/guides/composing-results.mdx new file mode 100644 index 0000000..c5fc98c --- /dev/null +++ b/docs/guides/composing-results.mdx @@ -0,0 +1,98 @@ +--- +title: Compose Result-returning functions +description: Keep expected failures in linear TypeScript control flow with exact Result discrimination. +--- + +# Compose Result-returning functions + +Check a `Result`, return its error branch immediately, and keep the success path unindented. This is ordinary TypeScript control flow; the important detail is using the exact discriminator. + +The following lookup pattern is adapted from [Epicenter's table implementation at commit `4d438c0`](https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/workspace/src/document/table.ts). + +```typescript +import { defineErrors, type InferErrors } from "wellcrafted/error"; +import { Ok, type Result } from "wellcrafted/result"; + +const TableError = defineErrors({ + ReadFailed: ({ table }: { table: string }) => ({ + message: `Could not read table "${table}".`, + table, + }), + RowNotFound: ({ table, rowId }: { table: string; rowId: string }) => ({ + message: `Row "${rowId}" was not found in "${table}".`, + table, + rowId, + }), +}); + +type TableError = InferErrors; +type Row = { id: string; title: string }; + +const tables = new Map>([ + [ + "documents", + new Map([["row-1", { id: "row-1", title: "Draft" }]]), + ], +]); + +function findRow(table: string, rowId: string): Result { + const rows = tables.get(table); + if (rows === undefined) return TableError.ReadFailed({ table }); + + return Ok(rows.get(rowId) ?? null); +} + +function requireRow(table: string, rowId: string): Result { + const rowResult = findRow(table, rowId); + if (rowResult.error !== null) return rowResult; + if (rowResult.data === null) { + return TableError.RowNotFound({ table, rowId }); + } + + return Ok(rowResult.data); +} +``` + +`Ok(null)` is useful here: absence is a successful lookup result, while `requireRow` decides that absence is an error for its own contract. The lower-level function does not have to invent a failure that its caller may not want. + +## Discriminate on `error` + +Use `result.error !== null` or `isErr(result)` for the error branch. Both follow the Result shape's actual discriminator. + +```typescript +import { isErr } from "wellcrafted/result"; + +const result = requireRow("documents", "row-2"); + +if (isErr(result)) { + console.error(result.error.message); +} else { + console.log(result.data.title); +} +``` + +Do not use `if (result.error)` as the general rule. The public `Err` type permits falsy error values such as `0`, `false`, `""`, `undefined`, and `NaN`; a truthiness check misses them. + +`Err(null)` is the shape's hard edge. It produces `{ data: null, error: null }`, which is structurally identical to `Ok(null)`, so no generic guard can recover the author's intent. Keep error values non-null and meaningful. `Err(undefined)` remains distinguishable by `error !== null`, but it is still a poor error value and fails truthiness checks. + +## Recover when the caller owns a fallback + +Recovery turns a specific error into `Ok(fallback)` when the caller can honestly satisfy its contract. Handle only the variant you can recover from and propagate the rest. + +```typescript +function findOrCreate(rowId: string): Result { + const result = requireRow("documents", rowId); + if (result.error === null) return result; + if (result.error.name !== "RowNotFound") return result; + + return Ok({ id: rowId, title: "Untitled" }); +} +``` + +Do not recover merely to hide an error. A fallback belongs at the layer that can decide it is a valid success. + +## Propagate without widening the boundary + +Return the narrowed error branch itself when the caller already includes that error type. Wrap an error in a new variant only when crossing into a domain that owns a different vocabulary. This keeps each layer's signature honest and avoids losing structured fields to repeated message-only transformations. + +For throwing dependencies, move the conversion into a narrow boundary rather than wrapping an entire workflow. The [service boundary guide](/guides/service-boundaries) shows that pattern. diff --git a/docs/guides/defining-error-vocabularies.mdx b/docs/guides/defining-error-vocabularies.mdx new file mode 100644 index 0000000..d7f0eec --- /dev/null +++ b/docs/guides/defining-error-vocabularies.mdx @@ -0,0 +1,93 @@ +--- +title: Define an error vocabulary +description: Model expected failures as named plain-object variants with the fields each caller needs. +--- + +# Define an error vocabulary + +Use one `defineErrors` call to name the expected failures in a domain. Each variant produces a plain error body with a stable `name`, a human-readable `message`, and any fields callers need for branching, logging, transport, or UI. + +```typescript +import { + defineErrors, + type InferError, + type InferErrors, +} from "wellcrafted/error"; + +const UploadError = defineErrors({ + Rejected: ({ fileName, reasons }: { + fileName: string; + reasons: string[]; + }) => ({ + message: `Upload rejected for "${fileName}".`, + fileName, + reasons, + }), + StorageUnavailable: ({ region }: { region: string }) => ({ + message: `Upload storage is unavailable in ${region}.`, + region, + }), +}); + +type UploadError = InferErrors; +type RejectedError = InferError; +``` + +The namespace is the domain; each key says what failed. Prefer `UploadError.Rejected` and `UploadError.StorageUnavailable` over a generic `UploadError.Failed` with a second `reason` discriminator. + +## Return a variant + +Calling a variant returns an `Err` wrapper. The tagged error body lives under `.error`. + +```typescript +const result = UploadError.Rejected({ + fileName: "report.csv", + reasons: ["too large", "unsupported encoding"], +}); + +result; +// { +// data: null, +// error: { +// name: "Rejected", +// message: 'Upload rejected for "report.csv".', +// fileName: "report.csv", +// reasons: ["too large", "unsupported encoding"] +// } +// } +``` + +`name` is reserved and stamped from the variant key. `message` must be a string. The remaining fields are inferred from the object returned by that variant, so narrowing on `error.name` also narrows its fields. + +Use `InferErrors` when a function can return any variant in the vocabulary. Use `InferError` when a helper or handler accepts exactly one variant, such as `RejectedError` above. + +```typescript +function describe(error: UploadError) { + switch (error.name) { + case "Rejected": + return `${error.fileName}: ${error.reasons.join(", ")}`; + case "StorageUnavailable": + return `Try another region instead of ${error.region}.`; + } +} +``` + +## Design fields for their boundaries + +Make a field required when every occurrence of that variant needs it. Use an optional field only when its absence is meaningful for the same failure mode; otherwise split the cases into separate variants with honest required fields. If an optional field is absent at a JSON boundary, omit the key deliberately rather than relying on an `undefined` value that JSON will discard. + +The constructor owns message formatting. Callers pass structured inputs such as `fileName`, `reasons`, and `region`; the variant turns them into one consistent message. This keeps wording out of call sites and preserves the original fields for code that should not parse human text. + +Use JSON-compatible fields when an error must cross JSON, HTTP, a worker, IPC, logs, or UI without a transformation layer. Strings, booleans, `null`, finite numbers, arrays, and plain objects composed from those values are the predictable case. + +`defineErrors` does not type-enforce JSON-compatible fields. It accepts arbitrary fields, including native `Error` objects, functions, and class instances, and those values do not round-trip through JSON perfectly. Treat JSON compatibility as a deliberate field-design rule; use runtime validation separately when a boundary receives untrusted data. + +Keep a raw `cause: unknown` when the error stays inside a process and the original value helps debugging. When the vocabulary crosses a serialization boundary, its constructor should normalize that cause into deliberate fields such as `causeMessage`, `operation`, or a stable application code. The constructor owns that choice; callers should pass the raw cause rather than format it differently at every catch site. + +Error bodies are shallow-frozen. Their top-level fields cannot be reassigned, but nested arrays and objects are not deeply immutable. + +## Keep the vocabulary closed + +A useful vocabulary is small enough for a caller to handle exhaustively and specific enough that each variant carries honest fields. Add a variant when callers need to distinguish a new failure mode. Add a field when callers need structured context, not merely to make the error look more detailed. + +Next, [compose Results](/guides/composing-results) to propagate these variants through application code. diff --git a/docs/guides/serialization-boundaries.mdx b/docs/guides/serialization-boundaries.mdx new file mode 100644 index 0000000..d59f80a --- /dev/null +++ b/docs/guides/serialization-boundaries.mdx @@ -0,0 +1,110 @@ +--- +title: Preserve errors across serialization boundaries +description: Move Result-shaped error data through JSON when every field is compatible, without confusing shape preservation with validation or typing. +--- + +# Preserve errors across serialization boundaries + +wellcrafted errors are plain data, so a Result can cross JSON, HTTP, workers, IPC, logs, and UI without an error-class transformation layer when every field in the complete value is JSON-compatible. + +That promise is conditional. `defineErrors` does not type-enforce JSON-compatible fields, and arbitrary fields do not serialize perfectly. You choose the fields, so you also own their boundary behavior. + +## Use boundary-friendly fields + +```typescript +import { defineErrors } from "wellcrafted/error"; + +const UploadError = defineErrors({ + Rejected: ({ + fileName, + reasons, + }: { + fileName: string; + reasons: string[]; + }) => ({ + message: `Upload rejected for "${fileName}".`, + fileName, + reasons, + }), +}); + +const result = UploadError.Rejected({ + fileName: "report.csv", + reasons: ["too large", "unsupported encoding"], +}); + +const json = JSON.stringify(result); +const received: unknown = JSON.parse(json); +``` + +The Result container, tagged error name, message, file name, and reasons survive this round trip because the entire value is composed from plain objects, dense arrays, strings, and `null`. + +For exact JavaScript-value preservation, stay with `null`, booleans, strings, finite numbers where negative-zero identity does not matter, dense arrays without extra properties, and own enumerable string-keyed fields on ordinary objects. Apply the same rule recursively to every nested value. + +JSON can normalize, omit, or reject other values: + +| Field value | JSON behavior | +| --- | --- | +| `undefined`, functions, symbols | Omitted from objects; replaced with `null` in arrays | +| `NaN`, `Infinity`, `-Infinity` | Normalized to `null` | +| `-0` | Normalized to `0` | +| `Date` | Converted to a string | +| Native `Error` | Usually becomes an empty object because its details are non-enumerable | +| `bigint` or a cyclic graph | `JSON.stringify` throws | +| Class instances, null-prototype objects, sparse arrays | Do not satisfy the exact plain-data round-trip model | + +The exported `JsonValue` type is useful for describing JSON-shaped data, but TypeScript cannot express every runtime restriction, such as excluding `NaN` from `number`. A type annotation is not a serialization proof. + +Native causes show why the condition matters: + +```typescript +const UnexpectedError = defineErrors({ + Failed: ({ cause }: { cause: unknown }) => ({ + message: "Upload failed unexpectedly.", + cause, + }), +}); + +const withNativeCause = UnexpectedError.Failed({ + cause: new Error("disk full"), +}); +const roundTripped = JSON.parse(JSON.stringify(withNativeCause)); + +roundTripped.error.cause; +// {} — Error.message and Error.stack are non-enumerable +``` + +## Normalize a caught cause before the wire + +Inside one process, preserving a raw `cause: unknown` can help debugging. Across JSON, normalize it to the fields the receiver needs. + +```typescript +import { extractErrorMessage } from "wellcrafted/error"; + +const BoundaryError = defineErrors({ + StorageUnavailable: ({ cause }: { cause: unknown }) => ({ + message: "Upload storage is unavailable.", + causeMessage: extractErrorMessage(cause), + }), +}); + +const result = BoundaryError.StorageUnavailable({ + cause: new Error("disk full"), +}); +``` + +This variant keeps a string rather than the native `Error`. If callers need more context, add deliberate JSON-compatible fields such as `operation`, `resourceId`, or a stable application code. + +The runnable [`examples/serialization-boundary.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/examples/serialization-boundary.ts) also shows the negative case: `defineErrors` accepts a native `Error`, but its details do not survive the JSON round trip. + +## Separate three boundary guarantees + +Preserving a JSON shape, validating at runtime, and providing end-to-end static types solve different problems. + +1. Shape preservation means compatible values encode and decode with the intended structure. +2. Runtime validation checks that untrusted bytes actually match the expected Result and payload schemas. +3. End-to-end static typing gives the producer and consumer a shared compile-time contract. + +None implies the other two. `JSON.parse` returns data that must still be treated as unknown until it is validated or narrowed. A shared TypeScript type does not inspect incoming bytes. A runtime schema can reject bad input even when the producer was written in another language and shares no TypeScript types. + +At an HTTP boundary, define all three contracts explicitly: the JSON response shape, the schema used to validate received data, and the mechanism—shared types, generated clients, or manual declarations—that gives each side static types. diff --git a/docs/guides/service-boundaries.mdx b/docs/guides/service-boundaries.mdx new file mode 100644 index 0000000..ee5826d --- /dev/null +++ b/docs/guides/service-boundaries.mdx @@ -0,0 +1,57 @@ +--- +title: Put Results at service boundaries +description: Convert narrow throwing operations into domain errors and propagate them through linear service code. +--- + +# Put Results at service boundaries + +A service boundary is where an uncontrolled throwing API becomes a controlled error vocabulary. Wrap only the operation that throws, give that failure a domain name, and let the rest of the service use `Result` returns and early exits. + +This partial task excerpt is adapted from [Epicenter's transcription service flow at commit `4d438c0`](https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/client/src/transcribe.ts). It omits imports, the `UserError` vocabulary, and setup; [`examples/service-boundary.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/examples/service-boundary.ts) is the canonical runnable source. + +```typescript +function findUser( + records: Map, + userId: string, +): Result { + return trySync({ + try: () => { + if (userId === "storage-offline") { + throw new Error("storage is offline"); + } + return records.get(userId) ?? null; + }, + catch: (cause) => + UserError.ReadFailed({ cause: extractErrorMessage(cause) }), + }); +} + +function getDisplayName( + records: Map, + userId: string, +): Result { + const userResult = findUser(records, userId); + if (userResult.error !== null) return userResult; + if (userResult.data === null) return UserError.NotFound({ userId }); + + return Ok(userResult.data.displayName); +} +``` + +The `trySync` block owns one failure unit: reading the record. `getDisplayName` does not catch anything. It manually propagates `ReadFailed`, classifies a successful empty lookup as `NotFound`, and leaves the happy path at the bottom. + +## Keep dependencies explicit + +Pass databases, clients, filesystems, clocks, and configuration into a service factory or method. A service should not reach into UI state or emit notifications. Explicit inputs make the boundary testable and keep its `Result` signature useful outside one screen or framework. + +## Wrap at the narrowest useful point + +Use a separate `trySync` or `tryAsync` boundary when sequential operations have different failure meanings. A broad catch around an entire workflow can only produce a vague error and makes it unclear which work completed before the failure. + +When you destructure a Result, remember that `error` is the raw error body. Return `Err(error)` if you need to rebuild a Result. Returning the already-narrowed Result object, as the example does, avoids that extra wrapper. + +## Choose where errors change vocabulary + +Propagate an error unchanged while it still means the same thing to the caller. Create a new variant when crossing into a domain that needs a different contract—for example, turning a storage failure into a user-facing account-loading failure while preserving the original value as context. + +A raw `cause: unknown` is useful inside one process, but it may not be JSON-compatible. This example normalizes it to a string at the storage boundary to match the canonical file. If an error will cross a wire, worker, IPC, log, or persistence boundary, make that ownership decision deliberately. The [serialization boundary guide](/guides/serialization-boundaries) shows the distinction. diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index ff61799..77bf4df 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -568,9 +568,20 @@ Verification on 2026-07-10: ### Wave 4: Build the guide path -- [ ] Add error-vocabulary, Result-composition, service-boundary, and serialization-boundary guides. -- [ ] Ground examples in approved Epicenter patterns. -- [ ] Keep exact signatures out of guides unless needed for the task. +- [x] Add error-vocabulary, Result-composition, service-boundary, and serialization-boundary guides. +- [x] Ground examples in approved Epicenter patterns. +- [x] Keep exact signatures out of guides unless needed for the task. + +Verification on 2026-07-10: + +- Added exactly four task-focused guides under `docs/guides/`: defining error vocabularies, composing Results, service boundaries, and serialization boundaries. No navigation or legacy documentation changed in this wave. +- The Result guide adapts and attributes Epicenter's table lookup pattern for `Ok(null)`, reachable read and not-found errors, manual propagation, and recovery to an honest `Ok` fallback. It documents `error !== null` and `isErr` as the exact guards, the falsy-error truthiness limitation, and the structural `Err(null)`/`Ok(null)` collision. +- The service guide adapts and attributes Epicenter's transcription flow through a short task excerpt synchronized with the canonical example's cause-as-string behavior. It wraps one reachable throwing operation, propagates the narrowed Result, and keeps domain classification separate from the I/O boundary. +- The error guide distinguishes required and optional fields, keeps formatting and cause normalization owned by constructors, and shows `InferError` for one variant. The error and serialization guides lead with plain boundary-friendly data only when every field is JSON-compatible and state that `defineErrors` does not type-enforce that condition. +- The serialization guide covers positive and negative cases, states that unsupported array entries become `null`, demonstrates a native `Error` cause becoming `{}`, and separates JSON shape preservation, runtime validation, and end-to-end static typing. +- Public guide text contains no importer counts, reliability claims, production metrics, or other vanity metrics. Exact API signatures appear only where the task requires a compilable usage pattern. +- `bun run format` passed without changing existing files. `bun run docs:examples` passed its strict typecheck, canonical snippet comparison, and three offline runtime examples. +- With Node 24.17.0, `bun run docs:validate` and `bun run docs:links` passed. ### Wave 5: Establish complete reference ownership From 03ea0035dcfa81a47200d6b4292ecd1e64d37e52 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:39:29 -0700 Subject: [PATCH 06/13] docs(reference): cover every published subpath Add one authoritative reference owner for each package subpath and correct misleading public comments without changing runtime behavior or signatures. Record implementation-facing exports and observed mismatches as deferred API work. --- docs/reference/brand.mdx | 41 +++++++ docs/reference/error.mdx | 79 ++++++++++++ docs/reference/function.mdx | 54 ++++++++ docs/reference/json.mdx | 70 +++++++++++ docs/reference/logger.mdx | 78 ++++++++++++ docs/reference/query.mdx | 96 +++++++++++++++ docs/reference/result.mdx | 97 +++++++++++++++ docs/reference/standard-schema.mdx | 115 ++++++++++++++++++ docs/reference/testing.mdx | 38 ++++++ ...10T012026-greenfield-documentation-pass.md | 24 +++- src/brand.ts | 12 +- src/error/defineErrors.ts | 5 + src/error/extractErrorMessage.ts | 8 +- src/function.ts | 4 + src/json.ts | 17 +-- src/logger/console-sink.ts | 14 +-- src/logger/index.ts | 5 +- src/logger/types.ts | 6 +- src/query/utils.ts | 4 +- src/result/result.ts | 6 +- src/standard-schema/types.ts | 5 +- src/testing.ts | 7 +- 22 files changed, 744 insertions(+), 41 deletions(-) create mode 100644 docs/reference/brand.mdx create mode 100644 docs/reference/error.mdx create mode 100644 docs/reference/function.mdx create mode 100644 docs/reference/json.mdx create mode 100644 docs/reference/logger.mdx create mode 100644 docs/reference/query.mdx create mode 100644 docs/reference/result.mdx create mode 100644 docs/reference/standard-schema.mdx create mode 100644 docs/reference/testing.mdx diff --git a/docs/reference/brand.mdx b/docs/reference/brand.mdx new file mode 100644 index 0000000..b034699 --- /dev/null +++ b/docs/reference/brand.mdx @@ -0,0 +1,41 @@ +--- +title: Brand reference +description: Current type export from wellcrafted/brand. +--- + +# `wellcrafted/brand` + +This subpath exports one type-only nominal marker. It has no runtime exports. + +```typescript +import type { Brand } from "wellcrafted/brand"; +``` + +## Export + +| Export | Kind | Contract | +| --- | --- | --- | +| `Brand` | Type | `Brand` adds a composable unique-symbol marker keyed by `T`. | + +```typescript +type UserId = string & Brand<"UserId">; +type OrderId = string & Brand<"OrderId">; + +function UserId(value: string): UserId { + return value as UserId; +} +``` + +`Brand` performs no validation and emits no JavaScript. Use a constructor or runtime schema at the boundary where a plain value becomes branded. The assertion belongs in that one boundary, not throughout application code. + +Brands compose through intersection. A child can include its parent marker, and multiple markers can coexist without collapsing to `never`. + +```typescript +type AbsolutePath = string & Brand<"AbsolutePath">; +type ConfigPath = AbsolutePath & Brand<"ConfigPath">; + +declare const configPath: ConfigPath; +const absolutePath: AbsolutePath = configPath; +``` + +The brand marker gives static assignability only. It does not prove that a string is a valid user ID, path, or other domain value at runtime. diff --git a/docs/reference/error.mdx b/docs/reference/error.mdx new file mode 100644 index 0000000..3932a7b --- /dev/null +++ b/docs/reference/error.mdx @@ -0,0 +1,79 @@ +--- +title: Error reference +description: Current runtime and type exports from wellcrafted/error. +--- + +# `wellcrafted/error` + +This subpath defines named error variants and a best-effort message extractor. + +```typescript +import { + defineErrors, + extractErrorMessage, + type InferErrors, +} from "wellcrafted/error"; +``` + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `defineErrors` | Value | Converts a variant configuration into factories that return Err wrappers. | +| `extractErrorMessage` | Value | Produces display text from an unknown value using a fixed precedence. | +| `AnyTaggedError` | Type | Minimum `{ name: string; message: string }` tagged-error shape. | +| `ErrorBody` | Type | Low-level constructor body constraint requiring `message: string`. | +| `ErrorsConfig` | Type | Low-level record of variant constructor functions. | +| `ValidatedConfig` | Type | Low-level mapped type that reserves `name` for each variant. | +| `DefineErrorsReturn` | Type | Low-level mapping from a configuration to its Err-returning factories. | +| `InferError` | Type | Extracts the tagged body from one variant factory. | +| `InferErrors` | Type | Extracts the union of tagged bodies from a vocabulary. | + +The four low-level configuration types are public today and documented here as exported. Their long-term public role is an open API-cleanup question; prefer `defineErrors`, `InferError`, and `InferErrors` in application code. + +## `defineErrors` + +```typescript +function defineErrors( + config: TConfig & ValidatedConfig, +): DefineErrorsReturn; +``` + +Each configuration key becomes a factory. The constructor returns a body with `message`; the factory shallow-copies that body, stamps `name` after the spread, shallow-freezes the result, and returns it inside `Err`. + +```typescript +const AccountError = defineErrors({ + NotFound: ({ accountId }: { accountId: string }) => ({ + message: `Account "${accountId}" was not found.`, + accountId, + }), +}); + +const result = AccountError.NotFound({ accountId: "account-1" }); +// { data: null, error: { name: "NotFound", message: "...", accountId: "..." } } + +type AccountError = InferErrors; +type NotFoundError = InferError; +``` + +The type contract enforces `message: string` and rejects a caller-supplied `name`. Other fields are unconstrained. At runtime, the stamped variant key wins even if an unchecked caller supplies `name` in the body. + +Freezing is shallow: nested objects and arrays remain mutable. JSON compatibility is also a convention, not a type guarantee. Errors cross JSON predictably only when every field in the complete value is JSON-compatible. See [Preserve errors across serialization boundaries](/guides/serialization-boundaries). + +## `extractErrorMessage` + +```typescript +function extractErrorMessage(error: unknown): string; +``` + +The extractor checks values in this order: + +1. Native `Error` instances use `.message`. +2. Strings pass through; numbers, booleans, bigint values, and symbols become strings. +3. `null` and `undefined` become their literal names. +4. Arrays use `JSON.stringify`. +5. Objects use the first string field among `message`, `error`, `description`, `title`, `reason`, and `details`. +6. Other objects try `JSON.stringify`, then fall back to `String` if stringification throws. +7. Remaining values use `String`. + +This helper is not total despite its current return type. A cyclic array or an array containing bigint throws because the array branch does not catch `JSON.stringify`. A custom object `toJSON()` can return `undefined`, producing `undefined` at runtime. Those behaviors are recorded as deferred API issues rather than changed in this documentation pass. diff --git a/docs/reference/function.mdx b/docs/reference/function.mdx new file mode 100644 index 0000000..f96de5f --- /dev/null +++ b/docs/reference/function.mdx @@ -0,0 +1,54 @@ +--- +title: Function reference +description: Current runtime export from wellcrafted/function. +--- + +# `wellcrafted/function` + +This subpath exports one run-at-most-once wrapper and no named types. + +```typescript +import { once } from "wellcrafted/function"; +``` + +## Export + +| Export | Kind | Contract | +| --- | --- | --- | +| `once` | Value | Invokes a function on the first call and returns its cached result on later calls. | + +```typescript +function once( + fn: (...args: TArgs) => TReturn, +): (...args: TArgs) => TReturn; +``` + +The first call's arguments are passed to `fn`. Later arguments are ignored, and later calls return the same cached value, including the same object reference. + +```typescript +const initialize = once((name: string) => ({ name })); + +const first = initialize("first"); +const second = initialize("ignored"); + +first === second; // true +second.name; // "first" +``` + +## First-call failure edge + +The wrapper marks itself called before invoking `fn`. If the first invocation throws, later calls do not retry and return the uninitialized cache—`undefined` at runtime—even though the generic signature still says `TReturn`. + +```typescript +const initialize = once(() => { + throw new Error("failed"); +}); + +try { + initialize(); +} catch {} + +initialize(); // undefined at runtime; no retry +``` + +That mismatch is a deferred API bug, not a behavior changed by this documentation pass. Use `once` only when a first-call throw is impossible, already terminal, or otherwise acceptable. diff --git a/docs/reference/json.mdx b/docs/reference/json.mdx new file mode 100644 index 0000000..8752e6f --- /dev/null +++ b/docs/reference/json.mdx @@ -0,0 +1,70 @@ +--- +title: JSON reference +description: Current runtime and type exports from wellcrafted/json. +--- + +# `wellcrafted/json` + +This subpath parses JSON into a recursive value type without a generic type assertion. + +```typescript +import { parseJson, type JsonValue } from "wellcrafted/json"; +``` + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `JsonParseError` | Value and type | Factory for the tagged parse failure and the inferred error-body type. | +| `parseJson` | Value | Returns `Result` for one JSON string. | +| `JsonValue` | Type | Recursive approximation of JSON values. | +| `JsonObject` | Type | `Record`. | + +`JsonParseError` occupies both TypeScript namespaces: the value is an Err-returning factory, while the type is its tagged error body. + +## Value types + +```typescript +type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +type JsonObject = Record; +``` + +These types are useful recursive approximations, not serialization proofs. TypeScript's `number` includes `NaN`, infinities, and negative zero even though JSON normalizes some of those values. Runtime values can also violate annotations. Exact round-trip behavior still depends on the complete runtime value. + +## Parsing + +```typescript +function parseJson( + text: string, +): Result; +``` + +`parseJson` calls `JSON.parse` without a reviver. A successful parse is typed as `JsonValue`; it is not asserted to an application-specific shape. Validate or narrow the result before treating it as a user, configuration, or API payload. + +```typescript +const result = parseJson('{"count":1}'); + +if (result.error !== null) { + console.error(result.error.message); +} else { + const value: JsonValue = result.data; + // Validate value before using it as a known object shape. +} +``` + +## Parse failures + +```typescript +const JsonParseError: (input: { cause: unknown }) => Err; +``` + +The factory stores a readable message and the raw thrown `cause`. That cause is useful for local diagnostics but is not necessarily JSON-compatible or boundary-friendly. Normalize it before sending the error through JSON, HTTP, workers, IPC, or persistence. + +A valid JSON value with the wrong application shape is still an Ok parse. Runtime schema validation is a separate operation. diff --git a/docs/reference/logger.mdx b/docs/reference/logger.mdx new file mode 100644 index 0000000..5d68322 --- /dev/null +++ b/docs/reference/logger.mdx @@ -0,0 +1,78 @@ +--- +title: Logger reference +description: Current runtime and type exports from wellcrafted/logger. +--- + +# `wellcrafted/logger` + +This subpath provides dependency-injected structured logging around tagged errors. + +```typescript +import { createLogger, memorySink } from "wellcrafted/logger"; +``` + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `consoleSink` | Value | Sends each event to the matching `console[event.level]` method with a source prefix. | +| `createLogger` | Value | Creates a source-bound logger using an injected sink or `consoleSink`. | +| `memorySink` | Value | Returns an isolated `{ sink, events }` pair for tests. | +| `composeSinks` | Value | Fans events and async disposal out to sinks sequentially. | +| `tapErr` | Value | Re-export of the Result error-tap helper. | +| `LogEvent` | Type | Normalized `{ ts, level, source, message, data? }` event. | +| `LogLevel` | Type | `"trace" \| "debug" \| "info" \| "warn" \| "error"`. | +| `LogSink` | Type | Event callback intersected with `Partial`. | +| `Logger` | Type | Five logging methods with typed warn/error inputs. | +| `LoggableError` | Type | A raw tagged error or its Err wrapper. | + +`LogSink` and `composeSinks` use `AsyncDisposable` and `Symbol.asyncDispose`. Consumer type libraries and runtimes must support those names; see the tested matrix in [installation](/start/installation). + +## Events and loggers + +```typescript +type LogEvent = { + ts: number; + level: LogLevel; + source: string; + message: string; + data?: unknown; +}; + +function createLogger(source: string, sink?: LogSink): Logger; +``` + +`createLogger` timestamps events with wall-clock epoch milliseconds from `Date.now()`. The value is JSON-friendly but not monotonic. + +`warn` and `error` accept a raw `{ name, message, ...fields }` error or the Err wrapper returned by `defineErrors`; native `Error` is structurally accepted because it has string `name` and `message` fields. These methods copy the error message into the event and put the tagged error in `data`. `trace`, `debug`, and `info` accept free-form message and data arguments. + +Sink exceptions are not caught. If a sink throws, the logger call throws. + +## Built-in sinks + +```typescript +const consoleSink: (event: LogEvent) => void; + +function memorySink(): { + sink: LogSink; + events: LogEvent[]; +}; +``` + +`consoleSink` delegates to the matching console method. stdout and stderr routing is runtime-defined; use a custom sink when a CLI needs an exact stream contract. + +`memorySink` stores the same event objects it receives in a mutable array. Each call creates independent state. + +## Composition and taps + +```typescript +function composeSinks(...sinks: LogSink[]): LogSink; + +function tapErr( + logFn: (error: E) => void, +): (result: Result) => Result; +``` + +`composeSinks` emits in argument order. If one sink throws, later sinks do not run. Its async-dispose method also awaits members in order and stops if one disposal throws. The runtime return value always has `[Symbol.asyncDispose]`, but the public `LogSink` return type exposes cleanup as optional, so callers may need to narrow or use optional access. + +`tapErr` calls the supplied method only for Err and returns the same Result reference. A throwing logger method escapes. diff --git a/docs/reference/query.mdx b/docs/reference/query.mdx new file mode 100644 index 0000000..a332b29 --- /dev/null +++ b/docs/reference/query.mdx @@ -0,0 +1,96 @@ +--- +title: Query reference +description: Current runtime exports from wellcrafted/query. +--- + +# `wellcrafted/query` + +This subpath adapts Result-returning functions to TanStack Query's throwing data/error channels. + +It has an explicit tested type prerequisite: install `@tanstack/query-core@5.82.0` when importing `wellcrafted/query`. The current package does not declare that prerequisite in dependency metadata, so consumers must install it themselves. + +```bash +bun add @tanstack/query-core@5.82.0 +``` + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `resultQueryOptions` | Value | Converts a Result-returning query function into TanStack query observer options. | +| `resultMutationOptions` | Value | Converts a Result-returning mutation function into TanStack mutation observer options. | +| `createQueryFactories` | Value | Binds reusable query and mutation definitions to one `QueryClient`. | +| `defineKeys` | Value | Identity helper that validates query-key maps while preserving tuple information. | + +There are no named type exports from this subpath. `defineQuery` and `defineMutation` are returned by `createQueryFactories`; they are not top-level imports. + +## Direct adapters + +```typescript +const queryOptions = resultQueryOptions({ + queryKey: ["account", accountId], + queryFn: () => accountService.get(accountId), +}); + +const mutationOptions = resultMutationOptions({ + mutationKey: ["account", "rename"], + mutationFn: (input: { accountId: string; name: string }) => + accountService.rename(input), +}); +``` + +The inputs mirror TanStack's observer options except that the query or mutation function returns `Result` synchronously or asynchronously. The adapters replace that function with an async function: Ok resolves with `data`; Err throws its contained value into TanStack's error channel. + +Use these client-agnostic adapters when a framework hook owns execution and cache access. They do not require a `QueryClient` argument. + +## QueryClient-bound factories + +```typescript +import { QueryClient } from "@tanstack/query-core"; +import { createQueryFactories } from "wellcrafted/query"; + +const queryClient = new QueryClient(); +const { defineQuery, defineMutation } = createQueryFactories(queryClient); +``` + +`defineQuery(input)` returns this shape: + +```typescript +{ + options, + fetch(): Promise>, + ensure(): Promise>, +} +``` + +The query definition is not callable. `fetch()` delegates to `QueryClient.fetchQuery`, which applies TanStack freshness behavior. `ensure()` delegates to `QueryClient.ensureQueryData`, which prefers existing cache data. Both return the cache data type `TQueryData`, which can differ from the observer's selected `TData`. + +`defineMutation(input)` returns a callable function with one property: + +```typescript +const saveAccount = defineMutation({ + mutationKey: ["account", "save"], + mutationFn: accountService.save, +}); + +const result = await saveAccount(input); +saveAccount.options; +``` + +There is no `.execute()` method. Calling the function builds and executes a mutation through the bound client's mutation cache. + +The factory helpers catch values thrown by TanStack and cast them to the configured `TError` before returning Err. They do not validate the thrown value at runtime. + +## Query keys + +```typescript +const accountKeys = defineKeys({ + all: ["accounts"], + detail: (accountId: string) => ["accounts", accountId], + exactDetail: (accountId: string) => ["accounts", accountId] as const, +}); +``` + +Static entries preserve readonly literal tuples without `as const`. Factory entries preserve tuple shape but widen literal positions under contextual typing. Add `as const` inside a factory when those literal positions must remain exact. Empty arrays and values that are not key tuples are rejected. + +For choosing between adapters and bound factories in UI code, see the TanStack Query integration guide. diff --git a/docs/reference/result.mdx b/docs/reference/result.mdx new file mode 100644 index 0000000..3d412a9 --- /dev/null +++ b/docs/reference/result.mdx @@ -0,0 +1,97 @@ +--- +title: Result reference +description: Current runtime and type exports from wellcrafted/result. +--- + +# `wellcrafted/result` + +Import Result constructors, guards, throwing adapters, and small flow helpers from this subpath. + +```typescript +import { Ok, isErr, type Result } from "wellcrafted/result"; +``` + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `Ok` | Value and type | `Ok(data)` returns `{ data, error: null }`; `Ok` describes that shape. | +| `Err` | Value and type | `Err(error)` returns `{ data: null, error }`; `Err` describes that shape. | +| `Result` | Type | `Ok \| Err`. | +| `UnwrapOk` | Type | Extracts the success type from a Result union. | +| `UnwrapErr` | Type | Extracts the error type from a Result union. | +| `isResult` | Value | Shape guard for a non-null object with both `data` and `error` properties. | +| `isOk` | Value | Narrows when `error === null`. | +| `isErr` | Value | Narrows when `error !== null`. | +| `trySync` | Value | Runs a synchronous callback and delegates a caught value to a Result-returning `catch` callback. | +| `tryAsync` | Value | Awaits an asynchronous callback and delegates rejection or a thrown value to a Result-returning `catch` callback. | +| `unwrap` | Value | Returns Ok data or throws the contained Err value. | +| `resolve` | Value | Returns a plain value or Ok data; throws the contained Err value. | +| `tapErr` | Value | Calls a function on Err and returns the original Result reference. | +| `partitionResults` | Value | Groups Result wrappers into `{ oks, errs }` with `Object.groupBy`. | + +`Ok` and `Err` each occupy both TypeScript namespaces: use the same imported name as a constructor in value position and as a generic in type position. + +## Result shape and guards + +```typescript +type Result = + | { data: T; error: null } + | { data: null; error: E }; + +function isResult( + value: unknown, +): value is Result; + +function isOk(result: Result): result is Ok; +function isErr(result: Result): result is Err; +``` + +`isResult` checks only the container shape. It does not validate either payload. At an untrusted boundary, validate the data or error body separately. + +Use `error !== null` or `isErr`. A truthiness check misses permitted falsy error values such as `0`, `false`, `""`, `undefined`, and `NaN`. + +`Ok(null)` is valid. `Err(null)` produces the same `{ data: null, error: null }` structure, so no generic guard can distinguish the author's intent. Keep Err values non-null and meaningful. + +## Catching throwing operations + +```typescript +function trySync | Err>(options: { + try: () => T; + catch: (error: unknown) => R; +}): Ok | R; + +function tryAsync | Err>(options: { + try: () => Promise; + catch: (error: unknown) => R; +}): Promise | R>; +``` + +These helpers catch the `try` callback only. If the `catch` callback itself throws, that thrown value escapes. Keep the wrapped operation narrow so each boundary owns one failure meaning. + +## Throwing adapters + +```typescript +function unwrap(result: Result): T; +function resolve(value: T | Result): T; +``` + +Both functions deliberately cross from Result flow into a throwing contract. They throw the contained error value exactly as stored; it does not have to be an `Error` instance. `resolve` first uses the shallow `isResult` check, so a plain object with both keys is treated as a Result-shaped value. + +## Error taps and partitions + +```typescript +function tapErr( + logFn: (error: E) => void, +): (result: Result) => Result; + +function partitionResults( + results: Result[], +): { oks: Ok[]; errs: Err[] }; +``` + +`tapErr` returns the same object reference. If `logFn` throws, the tap throws. + +`partitionResults` preserves the original Ok and Err wrapper objects and their relative order inside each group. It depends on runtime support for `Object.groupBy`; use the tested runtime matrix from [installation](/start/installation). + +For application flow patterns, see [Compose Result-returning functions](/guides/composing-results). diff --git a/docs/reference/standard-schema.mdx b/docs/reference/standard-schema.mdx new file mode 100644 index 0000000..7766fbd --- /dev/null +++ b/docs/reference/standard-schema.mdx @@ -0,0 +1,115 @@ +--- +title: Standard Schema reference +description: Current runtime, type, and namespace exports from wellcrafted/standard-schema. +--- + +# `wellcrafted/standard-schema` + +This subpath wraps Standard Schema-compatible validators and JSON Schema converters around the `{ data, error }` Result shape. + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `OkSchema` | Value | Wraps one schema as an Ok-shaped schema. | +| `ErrSchema` | Value | Wraps one schema as an Err-shaped schema. | +| `ResultSchema` | Value | Combines data and error schemas into a Result-shaped union schema. | +| `FAILURES` | Value | Shared failure-result constants used by the wrappers' shape checks. | +| `hasValidate` | Value | Guard for a callable `~standard.validate` member. | +| `hasJsonSchema` | Value | Shallow guard for a non-null `~standard.jsonSchema` object. | +| `Ok` | Type | Capability-preserving Ok wrapper type. | +| `Err` | Type | Capability-preserving Err wrapper type. | +| `Result` | Type | Capability-preserving Result wrapper type. | +| `StandardTypedV1` | Type and namespace | Base Standard Typed contract and its helper types. | +| `StandardSchemaV1` | Type and namespace | Runtime-validation contract and its result, issue, and inference types. | +| `StandardJSONSchemaV1` | Type and namespace | JSON Schema conversion contract and its option and inference types. | + +`Ok`, `Err`, and `Result` are type-only exports on this subpath. Their runtime constructors live in `wellcrafted/result` and are not re-exported here. + +`FAILURES` is public today, with `EXPECTED_OBJECT`, `EXPECTED_DATA_ERROR_PROPS`, `EXPECTED_ERROR_NULL`, and `EXPECTED_ERROR_NOT_NULL` constants. Whether this implementation-facing object should remain public is a deferred API-cleanup question. + +## Result schema wrappers + +```typescript +function OkSchema( + innerSchema: TSchema, +): Ok; + +function ErrSchema( + innerSchema: TSchema, +): Err; + +function ResultSchema< + TDataSchema extends StandardTypedV1, + TErrorSchema extends StandardTypedV1, +>( + dataSchema: TDataSchema, + errorSchema: TErrorSchema, +): Result; +``` + +An Ok or Err wrapper exposes runtime validation when its inner schema has `validate`, and JSON Schema conversion when its inner schema has `jsonSchema`. A Result wrapper exposes a capability only when both input schemas have it. + +Runtime validation first requires a non-null object with own or inherited `data` and `error` keys. It branches on `error === null`: Ok validates `data`; Err validates `error`. This is a shallow Result-shape check, not a deep payload check by itself. The inner schemas provide payload validation. + +The wrappers normalize the inactive output slot to `null`. An Err input with non-null `data` still follows the error branch; its returned value has `data: null`. Inner issues are prefixed with `data` or `error` in their paths. + +Generated JSON Schemas require both keys and set `additionalProperties: false`. Result conversion emits one Ok branch and one Err branch. + +## Capability guards + +```typescript +function hasValidate( + schema: T, +): schema is T & StandardSchemaV1; + +function hasJsonSchema( + schema: T, +): schema is T & StandardJSONSchemaV1; +``` + +`hasValidate` checks that the validation member is callable. `hasJsonSchema` checks only that the converter member is a non-null object; it does not verify callable `input` and `output` members. A malformed converter can pass the guard and fail when invoked. + +## `StandardTypedV1` namespace + +The base type is `{ readonly "~standard": StandardTypedV1.Props }`. + +| Member | Purpose | +| --- | --- | +| `StandardTypedV1.Props` | Version, vendor, and optional inferred-type metadata. | +| `StandardTypedV1.Types` | Paired input and output types. | +| `StandardTypedV1.InferInput` | Extracts a schema's input type. | +| `StandardTypedV1.InferOutput` | Extracts a schema's output type. | + +## `StandardSchemaV1` namespace + +The validation type stores a `validate(value, options?)` function under `~standard`. + +| Member | Purpose | +| --- | --- | +| `StandardSchemaV1.Props` | Base typed properties plus the validation function. | +| `StandardSchemaV1.Result` | Success-or-failure validation result. | +| `StandardSchemaV1.SuccessResult` | Typed `value` with no issues. | +| `StandardSchemaV1.FailureResult` | Readonly issue array. | +| `StandardSchemaV1.Options` | Optional vendor `libraryOptions`. | +| `StandardSchemaV1.Issue` | Message and optional path. | +| `StandardSchemaV1.PathSegment` | Object form of a property-key path segment. | +| `StandardSchemaV1.InferInput` | Extracts the input type. | +| `StandardSchemaV1.InferOutput` | Extracts the output type. | + +Validation may return its result synchronously or as a Promise; the wrappers preserve either mode. + +## `StandardJSONSchemaV1` namespace + +The conversion type stores input and output converter functions under `~standard.jsonSchema`. + +| Member | Purpose | +| --- | --- | +| `StandardJSONSchemaV1.Props` | Base typed properties plus the converter. | +| `StandardJSONSchemaV1.Converter` | `input(options)` and `output(options)` JSON Schema producers. | +| `StandardJSONSchemaV1.Target` | Known draft/OpenAPI targets plus future strings. | +| `StandardJSONSchemaV1.Options` | Required target and optional vendor `libraryOptions`. | +| `StandardJSONSchemaV1.InferInput` | Extracts the input type. | +| `StandardJSONSchemaV1.InferOutput` | Extracts the output type. | + +Converter functions may throw when the requested target is unsupported. Capability detection does not establish that every target is implemented. diff --git a/docs/reference/testing.mdx b/docs/reference/testing.mdx new file mode 100644 index 0000000..5466d1a --- /dev/null +++ b/docs/reference/testing.mdx @@ -0,0 +1,38 @@ +--- +title: Testing reference +description: Current runtime exports from wellcrafted/testing. +--- + +# `wellcrafted/testing` + +This test-only subpath provides framework-agnostic Result assertions. It has no named type exports. + +```typescript +import { expectErr, expectOk } from "wellcrafted/testing"; +``` + +## Exports + +| Export | Kind | Contract | +| --- | --- | --- | +| `expectOk` | Value | Returns Ok data and throws a plain `Error` for Err. | +| `expectErr` | Value | Returns Err error data and throws a plain `Error` for Ok. | + +```typescript +function expectOk(result: Result): T; +function expectErr(result: Result): E; +``` + +The returned value is narrowed, so tests do not need casts or optional access. + +```typescript +const user = expectOk(loadUser("user-1")); +expect(user.name).toBe("Ada"); + +const error = expectErr(loadUser("missing")); +expect(error.name).toBe("NotFound"); +``` + +These helpers intentionally throw on the opposite branch. They use plain `Error`, so they work with Bun's test runner and other runners that report thrown exceptions as failures. + +They follow the same `error === null` discriminator as `isOk` and `isErr`. The `Err(null)` structural collision therefore applies: `expectErr(Err(null))` throws as though it received Ok, while `expectOk(Err(null))` returns the `null` data slot. Keep Err values non-null and meaningful. diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 77bf4df..ac3c0a5 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -585,10 +585,20 @@ Verification on 2026-07-10: ### Wave 5: Establish complete reference ownership -- [ ] Add exactly one reference page for each of nine subpaths. -- [ ] Cover every current runtime and type export. -- [ ] Align JSDoc edge cases and examples with the same facts. -- [ ] Report public-looking implementation exports as deferred API questions. +- [x] Add exactly one reference page for each of nine subpaths. +- [x] Cover every current runtime and type export. +- [x] Align JSDoc edge cases and examples with the same facts. +- [x] Report public-looking implementation exports as deferred API questions. + +Verification on 2026-07-10: + +- Added exactly nine reference owners under `docs/reference/`, one for each published subpath: result, error, logger, json, brand, function, query, standard-schema, and testing. Navigation and legacy pages remain unchanged for the later cutover wave. +- Audited each page against the authoritative v0.44.0 inventory. The pages cover every runtime and type export, the dual-space `Ok`, `Err`, and `JsonParseError` names, all three Standard Schema namespace surfaces, and the fact that query definitions are factory returns rather than top-level exports. +- Recorded the required edge contracts: shallow Result and Standard Schema guards, `Ok(null)`/`Err(null)`, contained-value throwing adapters, callback and sink exceptions, wrapper-preserving partitioning, shallow freezing and conditional serialization, wall-clock logger timestamps, JSON numeric limits, `once` first-throw behavior, query cache and casting boundaries, and testing's null-error collision. +- Corrected only factual public JSDoc and comments in the identified source files. No runtime logic, type signature, export, or package metadata changed. +- Kept `ErrorBody`, `ErrorsConfig`, `ValidatedConfig`, `DefineErrorsReturn`, and `FAILURES` documented as current exports while recording their long-term public role as a separate API-cleanup question. +- `bun run format:check`, `bun run typecheck`, `bun run build`, and `bun test` passed; the test run completed 158 tests with no failures. `bun run docs:examples` passed its build, strict example typecheck, canonical snippet comparison, and three offline examples. +- Under Node 24.17.0, `bun run docs:validate` and `bun run docs:links` passed. The focused current-reference claims sweep found no retired APIs, unsupported root imports, serialization absolutes, vanity metrics, reliability claims, or uppercase brand uses. ### Wave 6: Rebuild integrations and agent skills @@ -642,7 +652,11 @@ Verification on 2026-07-10: ### Public exports that look internal -The documentation must not erase names that are currently importable. It should describe them honestly and raise a separate API proposal if the maintainer wants to remove them before 1.0. +The documentation must not erase names that are currently importable. `ErrorBody`, `ErrorsConfig`, `ValidatedConfig`, `DefineErrorsReturn`, and `FAILURES` are documented in their current reference owners. A separate API proposal should decide whether they remain public before 1.0; this pass does not remove or hide them. + +### Documented runtime/type mismatches + +`once` marks the first call as used before invoking the wrapped function. If that invocation throws, later calls return `undefined` without retrying despite the `TReturn` signature. `extractErrorMessage` can throw for cyclic or bigint-containing arrays and can return `undefined` when a custom `toJSON()` does so despite its `string` signature. The reference records current behavior; changing either contract requires separate API work. ### Query type dependency diff --git a/src/brand.ts b/src/brand.ts index ec4b2d5..a684180 100644 --- a/src/brand.ts +++ b/src/brand.ts @@ -33,6 +33,8 @@ declare const brand: unique symbol; * * @example Single brand — preventing ID mix-ups * ```typescript + * import type { Brand } from "wellcrafted/brand"; + * * type UserId = string & Brand<"UserId">; * type OrderId = string & Brand<"OrderId">; * @@ -42,6 +44,8 @@ declare const brand: unique symbol; * * @example Hierarchical brands — child assignable to parent * ```typescript + * import type { Brand } from "wellcrafted/brand"; + * * type AbsolutePath = string & Brand<"AbsolutePath">; * type ConfigPath = AbsolutePath & Brand<"ConfigPath">; * @@ -52,6 +56,8 @@ declare const brand: unique symbol; * * @example Multiple inheritance * ```typescript + * import type { Brand } from "wellcrafted/brand"; + * * type Serializable = unknown & Brand<"Serializable">; * type Validated = unknown & Brand<"Validated">; * type SafeData = Serializable & Validated & Brand<"SafeData">; @@ -65,6 +71,9 @@ declare const brand: unique symbol; * value position = runtime validator. One name, zero ambiguity. * * ```typescript + * import { type } from "arktype"; + * import type { Brand } from "wellcrafted/brand"; + * * // Type-only brand (no runtime validation needed) * type Guid = string & Brand<"Guid">; * @@ -85,6 +94,7 @@ declare const brand: unique symbol; * import { type } from "arktype"; * import { z } from "zod"; * import * as v from "valibot"; + * import type { Brand } from "wellcrafted/brand"; * * // Define the type ONCE — it's just a type, no library dependency * type FileId = string & Brand<"FileId">; @@ -98,8 +108,6 @@ declare const brand: unique symbol; * // Valibot * const FileId = v.pipe(v.string(), v.transform((s): FileId => s as FileId)); * ``` - * - * @see {@link https://wellcrafted.dev/integrations/validation-libraries | Using Brand with Validation Libraries} */ type Brand = { [brand]: { [K in T]: true }; diff --git a/src/error/defineErrors.ts b/src/error/defineErrors.ts index bd390af..4523c6f 100644 --- a/src/error/defineErrors.ts +++ b/src/error/defineErrors.ts @@ -12,6 +12,11 @@ import type { * factory returns `Err<...>` directly — ready for `trySync`/`tryAsync` catch * handlers. The variant name is stamped as `name` on the error object. * + * The type contract requires `message: string` and reserves `name`; other + * fields are unconstrained. JSON compatibility is a caller-owned convention, + * not something `defineErrors` enforces. The stamped error object is frozen + * shallowly, so nested values remain mutable. + * * @example * ```ts * const HttpError = defineErrors({ diff --git a/src/error/extractErrorMessage.ts b/src/error/extractErrorMessage.ts index e5f68ef..b655766 100644 --- a/src/error/extractErrorMessage.ts +++ b/src/error/extractErrorMessage.ts @@ -1,5 +1,11 @@ /** - * Extracts a readable error message from an unknown error value + * Extracts a readable error message from an unknown error value. + * + * This is a best-effort display helper, not a total serializer. Arrays are + * passed directly to `JSON.stringify`, so cyclic arrays or arrays containing + * `bigint` throw. Plain-object stringification errors are caught, but a custom + * `toJSON()` that returns `undefined` can still produce `undefined` at runtime + * despite the current `string` return type. * * @param error - The unknown error to extract a message from * @returns A string representation of the error diff --git a/src/function.ts b/src/function.ts index 6251610..6b7a03c 100644 --- a/src/function.ts +++ b/src/function.ts @@ -3,6 +3,10 @@ * every later call is a no-op that returns that same cached result. Arguments passed after * the first call are ignored. * + * The call is marked as used before `fn` runs. If the first invocation throws, + * later calls do not retry and return the uninitialized cached value + * (`undefined` at runtime). Use `once` only when that behavior is acceptable. + * * Canonical use: an idempotent `[Symbol.dispose]` whose teardown is reachable from more than * one path and must not run twice. `once` makes that guarantee declarative instead of a * hand-rolled `let disposed` flag. diff --git a/src/json.ts b/src/json.ts index 936b9cb..3271a0a 100644 --- a/src/json.ts +++ b/src/json.ts @@ -6,8 +6,11 @@ import { import { type Result, trySync } from "./result/index.js"; /** - * JSON-serializable value types. - * Ensures data can be safely serialized via JSON.stringify. + * Recursive approximation of JSON-shaped values. + * + * This is not a serialization proof. TypeScript's `number` includes `NaN`, + * infinities, and negative zero, which JSON normalizes rather than preserving + * exactly. Runtime values can also violate their annotations. * * @example * ```typescript @@ -49,10 +52,10 @@ export const { JsonParseError } = defineErrors({ /** * The error {@link parseJson} produces when its input is not valid JSON. * - * JSON parsing has exactly one failure mode (the engine throws a `SyntaxError`), - * so this is a single variant. A valid-JSON-but-wrong-shape value is not a - * parse failure: validate the returned {@link JsonValue} against a schema for - * that case. + * The wrapper exposes one parse-failure variant. Its `cause` field preserves + * the raw thrown value and is not necessarily JSON-compatible. A + * valid-JSON-but-wrong-shape value is not a parse failure: validate the + * returned {@link JsonValue} against an application schema for that case. */ export type JsonParseError = InferError; @@ -63,7 +66,7 @@ export type JsonParseError = InferError; * Unlike `JSON.parse`, which returns `any` and throws on malformed input, this: * - types the success value as {@link JsonValue}, forcing you to narrow or * validate before treating it as a known shape - * - reports failure as a tagged {@link JsonParseError} rather than an exception + * - converts a thrown parser failure into a tagged {@link JsonParseError} * * No reviver argument is accepted. A reviver can return arbitrary values, which * would make the {@link JsonValue} success type a lie. diff --git a/src/logger/console-sink.ts b/src/logger/console-sink.ts index 348ea26..b9de12d 100644 --- a/src/logger/console-sink.ts +++ b/src/logger/console-sink.ts @@ -19,17 +19,9 @@ import type { LogSink } from "./types.js"; * * No dispose handler — `console` is not a resource. * - * ### CLI authors: stream routing - * - * `console[level]` routes by level, not uniformly to stdout: - * - `console.log` (not used here) is the only method that writes stdout. - * - `console.info`, `console.debug`, `console.warn`, `console.error`, - * `console.trace` all write **stderr** in Node/Bun. - * - * For a CLI that emits structured program output on stdout and diagnostics - * on stderr, this default is correct — every logger event goes to stderr. - * Authors who expect `log.info` to print to stdout will be surprised; write - * a custom sink that routes to `process.stdout` if that's the requirement. + * Stream routing is runtime-defined. The sink delegates each level to the + * matching `console` method and makes no stdout/stderr guarantee. CLI authors + * who need an exact stream contract should provide a custom sink. */ export const consoleSink = ((event) => { const prefix = `[${event.source}]`; diff --git a/src/logger/index.ts b/src/logger/index.ts index ebf48c2..72f4d51 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -7,8 +7,9 @@ * - Level is a call-site decision, never a property of the error variant. * - DI-only — no global registry, no default logger singleton. * - * Runtime-agnostic. The browser-safe bits live here. Bun/Node-only sinks - * (e.g. a JSONL file appender) ship downstream so this entry stays pure. + * The entry has no Bun/Node-only sink implementation, but its `LogSink` type + * and `composeSinks` cleanup use `AsyncDisposable` and `Symbol.asyncDispose`. + * Consumers need matching type libraries and runtime support for disposal. */ export type { LogEvent, diff --git a/src/logger/types.ts b/src/logger/types.ts index 009ade2..e9cbbd7 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -13,9 +13,9 @@ export type LogLevel = "trace" | "debug" | "info" | "warn" | "error"; /** * The normalized event every sink receives. * - * - `ts` is epoch millis (not a `Date`) — cheap to create, easy to serialize, - * trivially monotonic for ordering. Sinks that want ISO-8601 on the wire - * convert at serialization time. + * - `ts` is wall-clock epoch millis from `Date.now()` (not a `Date`) — cheap + * to create and easy to serialize. It is not a monotonic clock. Sinks that + * want ISO-8601 on the wire convert at serialization time. * - `source` is the logger's namespace, stamped once at `createLogger` and * carried on every event. Analogous to `tracing`'s `target`. * - `message` is human text. For `warn`/`error` it's copied from the diff --git a/src/query/utils.ts b/src/query/utils.ts index 578dad7..37cd5fd 100644 --- a/src/query/utils.ts +++ b/src/query/utils.ts @@ -14,7 +14,7 @@ import { Err, Ok, type Result, resolve } from "../result/index.js"; * Input for `resultQueryOptions` and `defineQuery`. * * Mirrors TanStack Query's `QueryObserverOptions` but expects `queryFn` to - * return a Wellcrafted `Result`. The Result is unwrapped into TanStack's + * return a wellcrafted `Result`. The Result is unwrapped into TanStack's * throwing data/error contract by `resultQueryOptions`. * * @template TQueryFnData - The success type produced by `queryFn` @@ -41,7 +41,7 @@ type QueryOptionsInput< * Input for `resultMutationOptions` and `defineMutation`. * * Mirrors TanStack Query's `MutationObserverOptions` but expects `mutationFn` - * to return a Wellcrafted `Result`. The Result is unwrapped into TanStack's + * to return a wellcrafted `Result`. The Result is unwrapped into TanStack's * throwing data/error contract by `resultMutationOptions`. * * @template TData - The success type produced by `mutationFn` diff --git a/src/result/result.ts b/src/result/result.ts index e3a8fc6..0cf37ff 100644 --- a/src/result/result.ts +++ b/src/result/result.ts @@ -33,8 +33,8 @@ export type Err = { error: E; data: null }; * - `Err`: Represents a failure outcome, containing an `error` field with the error value of type `E`. * In this case, the `data` field is `null`. * - * This type promotes explicit error handling by requiring developers to check - * the variant of the `Result` before accessing its potential value or error. + * This type supports explicit error handling by letting callers narrow the + * variant before using its success value or error. * It helps avoid runtime errors often associated with implicit error handling (e.g., relying on `try-catch` for all errors). * * @template T - The type of the success value if the operation is successful (held in `Ok`). @@ -98,8 +98,6 @@ export const Ok = (data: T): Ok => ({ data, error: null }); * `MyError.Unexpected({ cause: error })` is always non-null by construction, * so the discriminator works regardless of what was thrown. * - * See `docs/philosophy/err-null-is-ok-null.md` for the full rationale. - * * @template E - The type of the error value. * @param error - The error value to wrap. Don't pass `null` or `undefined`. * @returns An `Err` object with the provided error and `data` set to `null`. diff --git a/src/standard-schema/types.ts b/src/standard-schema/types.ts index 63111b7..e4771f0 100644 --- a/src/standard-schema/types.ts +++ b/src/standard-schema/types.ts @@ -205,7 +205,10 @@ export function hasValidate( } /** - * Checks if a schema has JSON Schema generation capability. + * Checks shallowly for a non-null `jsonSchema` object. + * + * This guard does not verify that `input` and `output` converter members are + * callable. Invoking malformed converter objects may still fail at runtime. */ export function hasJsonSchema( schema: T, diff --git a/src/testing.ts b/src/testing.ts index 7e1e6e4..af5d165 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -7,9 +7,10 @@ import { isErr, isOk, type Result } from "./result/index.js"; * These helpers intentionally **throw**. A failed expectation should abort the * test, and every test runner reports a thrown error as a failure. Tests are * the one place throwing is the correct control flow, so these are fenced into - * the `wellcrafted/testing` entry point and kept out of `wellcrafted/result`, - * which stays throw-free. Importing from `wellcrafted/testing` in production - * code is a smell worth linting against. + * the `wellcrafted/testing` entry point. Importing from + * `wellcrafted/testing` in production code is a smell worth linting against. + * Other entry points also contain deliberate throwing adapters such as + * `unwrap`, `resolve`, and the TanStack Query helpers. * * They are framework-agnostic: they throw a plain `Error` rather than calling * into a specific runner, so they work under bun, vitest, jest, or From 92c73e47dd8b1185d0742b39dc13df5cd557d7cf Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:05:06 -0700 Subject: [PATCH 07/13] docs(integrations): rebuild boundary guidance Document current TanStack adapters, runtime brand validation, and Hono's independent shape, validation, and static-typing contracts. Reconcile distributable skills with current APIs and conditional serialization. --- docs/integrations/hono.mdx | 138 ++++ docs/integrations/tanstack-query.mdx | 710 ++---------------- docs/integrations/validation-libraries.mdx | 192 +---- skills/branded-types/SKILL.md | 128 +--- skills/define-errors/SKILL.md | 269 ++----- skills/patterns/SKILL.md | 390 ++-------- skills/query-factories/SKILL.md | 213 ++---- skills/result-types/SKILL.md | 294 ++------ ...10T012026-greenfield-documentation-pass.md | 18 +- 9 files changed, 516 insertions(+), 1836 deletions(-) create mode 100644 docs/integrations/hono.mdx diff --git a/docs/integrations/hono.mdx b/docs/integrations/hono.mdx new file mode 100644 index 0000000..de5a39f --- /dev/null +++ b/docs/integrations/hono.mdx @@ -0,0 +1,138 @@ +--- +title: Hono HTTP boundaries +description: Preserve, validate, and type Result-shaped JSON across a Hono boundary without conflating those guarantees. +icon: hono +--- + +# Hono HTTP boundaries + +A Hono route can send a Result-shaped JSON envelope directly when every field in the complete value is JSON-compatible. That preserves a useful wire shape; it does not by itself validate untrusted data or create end-to-end static types. + +Keep three guarantees separate: + +| Guarantee | What provides it | What it does not provide | +| --- | --- | --- | +| JSON shape preservation | JSON-compatible Result, data, and error fields | Runtime validation or shared TypeScript types | +| Runtime validation | A schema that checks the full untrusted envelope and payload | A producer/consumer compile-time contract | +| End-to-end static typing | Hono's shared route type and `hc` client | Runtime inspection of received bytes | + +None implies either of the others. + +## Return the Result envelope + +This error vocabulary is adapted from [Epicenter's blob errors at commit `4d438c0`](https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/server/src/routes/blob-errors.ts). Its intended wire fields are strings and finite numbers. Callers must actually supply compatible values because `defineErrors` does not enforce that condition. + +```typescript +import { Hono } from "hono"; +import { defineErrors, type InferErrors } from "wellcrafted/error"; +import { Ok, type Result } from "wellcrafted/result"; + +const BlobError = defineErrors({ + InvalidSha256: ({ value }: { value: string }) => ({ + message: `Invalid sha256: "${value}".`, + value, + }), + BlobTooLarge: ({ size, maxBytes }: { size: number; maxBytes: number }) => ({ + message: `Blob exceeds the ${maxBytes} byte limit.`, + size, + maxBytes, + }), + NotFound: ({ sha256 }: { sha256: string }) => ({ + message: "Blob not found.", + sha256, + }), +}); + +type BlobError = InferErrors; +type BlobMetadata = { sha256: string; sizeBytes: number }; + +declare function getBlobMetadata( + sha256: string, +): Promise>; + +function statusForBlobError(error: BlobError): 400 | 404 | 413 { + switch (error.name) { + case "InvalidSha256": return 400; + case "NotFound": return 404; + case "BlobTooLarge": return 413; + } +} + +const app = new Hono(); + +const routes = app.get("/api/blobs/:sha256", async (c) => { + const result = await getBlobMetadata(c.req.param("sha256")); + if (result.error !== null) { + return c.json(result, statusForBlobError(result.error)); + } + + return c.json(result, 200); +}); + +export type AppType = typeof routes; +``` + +The HTTP status and JSON body are separate contracts. Status selects the transport response; the Result envelope carries application data or a tagged error. Do not infer one solely from the other, and do not replace a useful 4xx status with a blanket 500 merely because the body is Err. + +`defineErrors` does not enforce JSON-compatible fields. A raw native `Error`, `bigint`, function, class instance, `undefined`, or cyclic value can be accepted by the factory and fail or change during JSON serialization. Normalize such fields before this boundary. + +## Validate untrusted responses at runtime + +Treat `response.json()` as untrusted. Validate the full envelope, including the active payload, rather than checking only that `data` and `error` keys exist. + +```typescript +import { z } from "zod"; + +const blobMetadataSchema = z.object({ + sha256: z.string().regex(/^[a-f0-9]{64}$/), + sizeBytes: z.number().int().nonnegative(), +}).strict(); + +const blobErrorSchema = z.discriminatedUnion("name", [ + z.object({ + name: z.literal("InvalidSha256"), + message: z.string(), + value: z.string(), + }).strict(), + z.object({ + name: z.literal("BlobTooLarge"), + message: z.string(), + size: z.number(), + maxBytes: z.number(), + }).strict(), + z.object({ + name: z.literal("NotFound"), + message: z.string(), + sha256: z.string(), + }).strict(), +]); + +const blobResultSchema = z.union([ + z.object({ data: blobMetadataSchema, error: z.null() }).strict(), + z.object({ data: z.null(), error: blobErrorSchema }).strict(), +]); + +const unknownBody: unknown = await response.json(); +const body = blobResultSchema.parse(unknownBody); +``` + +This validation proves the received runtime value matches the schema. It does not prove that the server's TypeScript handler was compiled against the same schema. + +## Share static route types with `hc` + +Hono's client can derive request and response types from the exported route type. + +```typescript +import { hc } from "hono/client"; +import type { AppType } from "./server"; + +const client = hc("https://api.example.com"); +const response = await client.api.blobs[":sha256"].$get({ + param: { sha256 }, +}); +const body = await response.json(); +``` + +This gives producer and consumer a shared compile-time contract when they use the same `AppType`. It does not validate the bytes at runtime; deployments can drift, intermediaries can alter responses, and an unchecked server implementation can still violate a type. Use the runtime schema as well when the boundary is untrusted or independently deployed. + +Conversely, a schema-validated `fetch` client can be runtime-safe without importing `AppType`. Choose each guarantee explicitly instead of treating JSON, validation, or `hc` as a substitute for the others. diff --git a/docs/integrations/tanstack-query.mdx b/docs/integrations/tanstack-query.mdx index 4dc1831..1a2e1d9 100644 --- a/docs/integrations/tanstack-query.mdx +++ b/docs/integrations/tanstack-query.mdx @@ -1,692 +1,130 @@ --- -title: 'TanStack Query Integration' -description: 'Complete guide to using wellcrafted with TanStack Query for reactive data management' -icon: 'database' +title: TanStack Query +description: Adapt Result-returning functions to TanStack Query hooks and QueryClient-bound workflows. +icon: database --- -# TanStack Query Integration +# TanStack Query -The `wellcrafted/query` module adapts your Result-returning functions for TanStack Query. Define the data-fetching logic once and use it either way: a query exposes `.options` for reactive components plus `.fetch()`/`.ensure()` for imperative calls, while a mutation is callable directly in an event handler and also exposes `.options`. +`wellcrafted/query` converts a Result-returning query or mutation function into TanStack Query's contract: Ok resolves into the data channel, while Err throws its contained value into the error channel. -## Quick Start +Install the tested TanStack type prerequisite explicitly. The current wellcrafted package does not declare it for consumers. -```typescript -import { createQueryFactories } from 'wellcrafted/query'; -import { QueryClient } from '@tanstack/query-core'; - -// 1. Create your QueryClient -const queryClient = new QueryClient({ - defaultOptions: { - queries: { staleTime: 5 * 60 * 1000 } // 5 minutes - } -}); - -// 2. Create factory functions bound to your client -const { defineQuery, defineMutation } = createQueryFactories(queryClient); - -// 3. Define your operations -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: () => services.getUser(userId), // Returns Result - staleTime: 10 * 60 * 1000, // Override default for this query -}); - -// 4. Use in components or imperatively -const query = useQuery(userQuery.options); // React -const query = createQuery(() => userQuery.options); // Svelte 5 -const { data, error } = await userQuery.fetch(); // Imperative +```bash +bun add @tanstack/query-core@5.82.0 ``` -## The Dual Interface Pattern - -Every query and mutation provides **two ways to use them**: - -### 1. Reactive Interface (`.options`) -**Perfect for components that need automatic state management** +There are two adapter families. Choose by who owns execution. -**React:** +| Need | Use | +| --- | --- | +| A framework hook owns execution | `resultQueryOptions` or `resultMutationOptions` | +| One `QueryClient` owns reusable reactive and imperative handles | `createQueryFactories` | -```tsx -import { useQuery } from '@tanstack/react-query'; +## Direct options adapters -// Reactive usage in React components (pass options directly) -const recordingsQuery = useQuery(rpc.recordings.getAllRecordings.options); +Use direct adapters inside React, Svelte, Vue, or another TanStack framework binding when the hook already owns the observer. -// Component automatically re-renders when data changes -if (recordingsQuery.isPending) return ; -if (recordingsQuery.error) return ; -return ( - <> - {recordingsQuery.data?.map(recording => ( - - ))} - -); -``` - -**Svelte 5:** +This Svelte pattern is adapted from [Epicenter's account popover at commit `4d438c0`](https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/app-shell/src/account-popover/account-popover.svelte). ```svelte - - -{#if recordingsQuery.isPending} - -{:else if recordingsQuery.error} - -{:else if recordingsQuery.data} - {#each recordingsQuery.data as recording} - - {/each} -{/if} -``` - -**Benefits:** -- Automatic state management (`isPending`, `isError`, `isSuccess`) -- Component re-renders on data changes -- Built-in loading and error states -- Cache synchronization across components -- Background refetching and stale-while-revalidate - -### 2. Imperative Interface -**Perfect for event handlers, utilities, and performance-critical code** - -```typescript -// Event handlers -async function handleRefresh() { - const { data, error } = await rpc.recordings.getAllRecordings.fetch(); - if (error) { - showErrorToast(error.message); - return; - } - // Use data... -} - -// Sequential operations -async function processWorkflow() { - const { data: user, error: userError } = await userQuery.ensure(); - if (userError) return handleError(userError); - - const { data: result, error: processError } = await processUserMutation(user); - if (processError) return handleError(processError); - - // Success! -} -``` - -Queries are not directly callable. Use `.fetch()` when you want TanStack's freshness policy, or `.ensure()` when you want cache-first data and only want to fetch if data is missing. - -Mutations are directly callable because there is only one imperative action: - -```typescript -const ensured = await userQuery.ensure(); -const fetched = await userQuery.fetch(); - -const result = await createUser(userData); -``` - -**Benefits:** -- No reactive overhead (no subscriptions or observers) -- Still uses TanStack Query cache (respects `staleTime`, etc.) -- Perfect for one-time operations -- Lightweight and fast -- Works outside of component context - -## Performance Comparison - -### When Creating Reactive Observers is Overkill - -```typescript -// ❌ Unnecessary overhead for simple event handlers (either framework) -// React: const mutation = useMutation(rpc.recordings.deleteRecording.options); -// Svelte: const mutation = createMutation(() => rpc.recordings.deleteRecording.options); -mutation.mutate(recordingId, { - onSuccess: () => showToast('Deleted!'), - onError: (error) => showToast(error.message) -}); - -// ✅ Direct execution - much faster (works in any framework) -const { error } = await rpc.recordings.deleteRecording(recordingId); -if (error) { - showToast(error.message); -} else { - showToast('Deleted!'); -} -``` - -### When Reactive State is Essential - -**React:** - -```tsx -// ✅ Reactive state needed for UI feedback -const deleteRecordingMutation = useMutation(rpc.recordings.deleteRecording.options); - -// Component shows loading state -return deleteRecordingMutation.isPending ? ( - -) : ( - -); -``` - -**Svelte 5:** + import { createMutation, createQuery, QueryClient } from "@tanstack/svelte-query"; + import { resultMutationOptions, resultQueryOptions } from "wellcrafted/query"; + + const queryClient = new QueryClient(); + + const profile = createQuery( + () => + resultQueryOptions({ + queryKey: ["account-profile", accountCacheKey], + queryFn: () => auth.getProfile(), + enabled: auth.state.status !== "signed-out", + }), + () => queryClient, + ); -```svelte - - - -{#if deleteRecordingMutation.isPending} - -{:else} - -{/if} -``` - -## Query Definitions - -### Basic Query - -```typescript -const getAllUsers = defineQuery({ - queryKey: ['users'], - queryFn: () => services.db.getAllUsers(), - staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes -}); ``` -### Parameterized Queries - -```typescript -// For dynamic parameters, use accessor functions -const getUserById = (userId: Accessor) => defineQuery({ - queryKey: ['users', userId()], - queryFn: () => services.db.getUserById(userId()), - enabled: userId() !== '', // Only run when userId is provided -}); - -// Usage -const userId = () => route.params.id; -const userQuery = createQuery(() => getUserById(userId).options); -``` - -### Dependent Queries - -```typescript -const userProfileQuery = (userId: Accessor) => defineQuery({ - queryKey: ['users', userId(), 'profile'], - queryFn: async () => { - // First get basic user data - const { data: user, error: userError } = await services.db.getUserById(userId()); - if (userError) return Err(userError); - - // Then get profile data - const { data: profile, error: profileError } = await services.api.getUserProfile(user.profileId); - if (profileError) return Err(profileError); - - return Ok({ ...user, profile }); - }, - enabled: userId() !== '', -}); -``` +The accessor is important. Options are ordinary object snapshots. If `queryKey`, `enabled`, or another option depends on reactive state, construct the adapter inside the framework's reactive accessor so that state is read again. Constructing it once outside captures only the values from that moment. Follow the equivalent snapshot rules for your TanStack framework binding. -## Mutation Definitions +## QueryClient-bound factories -### Basic Mutation with Cache Updates +Use `createQueryFactories` when one client should own both framework options and imperative execution. ```typescript -const createRecording = defineMutation({ - mutationKey: ['recordings', 'create'], - mutationFn: async (recording: Recording) => { - const { data, error } = await services.db.createRecording(recording); - if (error) return Err(error); - - // Optimistically update cache - queryClient.setQueryData(['recordings'], (old) => { - if (!old) return [recording]; - return [...old, recording]; - }); - - // Invalidate related queries - queryClient.invalidateQueries({ queryKey: ['recordings', 'latest'] }); - - return Ok(data); - }, -}); -``` +import { QueryClient } from "@tanstack/query-core"; +import { createQueryFactories } from "wellcrafted/query"; -### Multi-Step Mutations - -```typescript -const transcribeRecording = defineMutation({ - mutationKey: ['recordings', 'transcribe'], - mutationFn: async (recordingId: string) => { - // Step 1: Update status to 'transcribing' - const { error: updateError } = await recordings.updateRecording({ - id: recordingId, - transcriptionStatus: 'TRANSCRIBING', - }); - if (updateError) return Err(updateError); - - // Step 2: Perform actual transcription - const { data: transcription, error: transcribeError } = await services.ai.transcribe(recordingId); - if (transcribeError) { - // Revert status on failure - await recordings.updateRecording({ - id: recordingId, - transcriptionStatus: 'FAILED', - }); - return Err(transcribeError); - } - - // Step 3: Save transcription results - const { error: saveError } = await recordings.updateRecording({ - id: recordingId, - transcribedText: transcription, - transcriptionStatus: 'COMPLETED', - }); - if (saveError) return Err(saveError); - - return Ok(transcription); - }, -}); -``` - -## Error Transformation Pattern - -A critical responsibility of the query layer is transforming service-specific errors into UI-friendly formats: - -```typescript -// Service layer returns domain-specific errors with typed fields -const RecorderError = defineErrors({ - StartFailed: ({ deviceId }: { deviceId: string }) => ({ - message: `Failed to start recording on device ${deviceId}`, - deviceId, - }), - StopFailed: ({ recordingId }: { recordingId: string }) => ({ - message: `Failed to stop recording ${recordingId}`, - recordingId, - }), - DeviceUnavailable: ({ deviceId, reason }: { deviceId: string; reason: string }) => ({ - message: `Recording device ${deviceId} unavailable: ${reason}`, - deviceId, - reason, - }), -}); - -// Query layer transforms them for UI consumption -const startRecording = defineMutation({ - mutationFn: async () => { - const { error } = await services.recorder.startRecording(); - if (error) { - // Transform service error to UI error - return Err({ - title: "❌ Failed to start recording", - description: error.message, // Keep original message - action: { type: 'more-details', error }, // Preserve original error - }); - } - return Ok(undefined); - }, - onError: (uiError) => { - // uiError is now UI-ready - showToast(uiError.title, { description: uiError.description }); - }, -}); -``` - -## Advanced Patterns - -### Query Key Factories - -Organize your query keys with factories to avoid magic strings: - -```typescript -const recordingKeys = { - all: ['recordings'] as const, - lists: () => [...recordingKeys.all, 'list'] as const, - list: (filters: string) => [...recordingKeys.lists(), filters] as const, - details: () => [...recordingKeys.all, 'detail'] as const, - detail: (id: string) => [...recordingKeys.details(), id] as const, -}; - -const getAllRecordings = defineQuery({ - queryKey: recordingKeys.all, - queryFn: () => services.db.getAllRecordings(), -}); -``` - -### Optimistic Updates with Rollback +const queryClient = new QueryClient(); +const { defineQuery, defineMutation } = createQueryFactories(queryClient); -```typescript -const updateRecording = defineMutation({ - mutationFn: async (recording: Recording) => { - const { data, error } = await services.db.updateRecording(recording); - if (error) return Err(error); - - // Update cache immediately - queryClient.setQueryData(['recordings'], (old: Recording[]) => - old?.map(r => r.id === recording.id ? recording : r) ?? [] - ); - - return Ok(data); - }, - onError: (error, recording, context) => { - // Rollback optimistic update on error - if (context?.previousRecordings) { - queryClient.setQueryData(['recordings'], context.previousRecordings); - } - }, - onMutate: async (recording) => { - // Cancel outgoing refetches - await queryClient.cancelQueries({ queryKey: ['recordings'] }); - - // Snapshot previous value - const previousRecordings = queryClient.getQueryData(['recordings']); - - // Return context for rollback - return { previousRecordings }; - }, +const profile = defineQuery({ + queryKey: ["account-profile", accountId], + queryFn: () => accountService.getProfile(accountId), }); -``` - -### Settings-Dependent Operations - -Perfect for operations that depend on user settings: -```typescript -const transcribeBlob = defineMutation({ - mutationFn: async (blob: Blob) => { - // Access reactive settings at runtime - const selectedService = settings.value['transcription.selectedService']; - const apiKey = settings.value[`apiKeys.${selectedService.toLowerCase()}`]; - - switch (selectedService) { - case 'OpenAI': - return services.transcription.openai.transcribe(blob, { - apiKey, - model: settings.value['transcription.openai.model'] - }); - case 'Groq': - return services.transcription.groq.transcribe(blob, { - apiKey, - model: settings.value['transcription.groq.model'] - }); - default: - return Err({ message: `Unsupported service: ${selectedService}` }); - } - }, +const signOut = defineMutation({ + mutationKey: ["account", "signOut"], + mutationFn: () => accountService.signOut(), }); ``` -## Organizing Your Query Layer - -### File Structure - -``` -src/ -├── lib/ -│ ├── query/ -│ │ ├── _client.ts # QueryClient setup and factory functions -│ │ ├── index.ts # Unified RPC namespace export -│ │ ├── recordings.ts # Recording-related queries/mutations -│ │ ├── transcription.ts # Transcription operations -│ │ ├── auth.ts # Authentication queries/mutations -│ │ └── notifications.ts # Notification operations -│ └── services/ # Pure business logic (no TanStack Query) -``` - -### RPC Namespace Pattern - -Create a unified namespace for all operations: +A query definition has exactly this shape: ```typescript -// query/index.ts -import { recordings } from './recordings'; -import { transcription } from './transcription'; -import { auth } from './auth'; -import { notifications } from './notifications'; - -export const rpc = { - recordings, - transcription, - auth, - notifications, -}; - -// Usage throughout app -import { rpc } from './query'; // or '$lib/query' in SvelteKit - -// Everything is available through one import -// React: -const recordingsQuery = useQuery(rpc.recordings.getAllRecordings.options); -// Svelte 5: -const recordingsQuery = createQuery(() => rpc.recordings.getAllRecordings.options); - -// Imperative (any framework): -await rpc.transcription.transcribeBlob(blob); -``` - -### Component Usage Patterns - -**React:** - -```tsx -// Reactive pattern for data display -const recordingsQuery = useQuery(rpc.recordings.getAllRecordings.options); - -// Imperative pattern for actions -async function handleDelete(recording: Recording) { - const { error } = await rpc.recordings.deleteRecording(recording); - if (error) { - showErrorToast(error.message); - } else { - showSuccessToast('Recording deleted'); - } +{ + options, + fetch(), + ensure(), } ``` -**Svelte 5:** +It is not callable. `fetch()` applies TanStack's freshness policy. `ensure()` prefers cached data and fetches when the cache has no value. -```typescript -// Reactive pattern for data display -const recordingsQuery = createQuery(() => rpc.recordings.getAllRecordings.options); - -// Imperative pattern for actions -async function handleDelete(recording: Recording) { - const { error } = await rpc.recordings.deleteRecording(recording); - if (error) { - showErrorToast(error.message); - } else { - showSuccessToast('Recording deleted'); - } -} -``` - -## Best Practices - -### 1. Choose the Right Interface - -**Use `.options` when:** -- Component needs to display loading states -- Data changes should trigger re-renders -- You want automatic error boundaries -- Background refetching is beneficial - -**Use callable mutations, `.fetch()`, or `.ensure()` when:** -- Event handlers that just need the result -- Sequential operations in workflows -- Performance is critical -- Working outside component context - -Mutations are directly callable. Queries are intentionally explicit because `.fetch()` and `.ensure()` choose different cache policies. - -### 2. Transform Errors at the Right Layer +A mutation definition is callable and has `.options`: ```typescript -// ✅ Good: Transform at query layer -const getUser = defineQuery({ - queryKey: ['users', id], - queryFn: async () => { - const { data, error } = await services.db.getUser(id); - if (error) { - return Err({ - title: "Failed to load user", - description: error.message, - }); - } - return Ok(data); - }, -}); - -// ❌ Avoid: Transforming in components -const userQuery = createQuery({ - queryFn: () => services.db.getUser(id), -}); -// Component has to handle raw service errors +const result = await signOut(undefined); +signOut.options; ``` -### 3. Use Query Key Factories +There is no `.execute()` method. -```typescript -// ✅ Good: Organized and maintainable -const userKeys = { - all: ['users'] as const, - detail: (id: string) => [...userKeys.all, id] as const, -}; - -// ❌ Avoid: Magic strings everywhere -queryKey: ['users', id, 'details', 'profile'] -``` - -### 4. Leverage Direct Client Access +## Keep cache operations on `QueryClient` -Unlike SSR applications, static sites can access the QueryClient directly: +wellcrafted adapts Result-returning functions; it does not wrap TanStack's cache API. Invalidation, optimistic updates, reads, and prefetching stay on the client that owns the cache. ```typescript -// Available because we're client-side only -const cachedUser = queryClient.getQueryData(['users', userId]); -const isUserCached = queryClient.getQueryState(['users', userId])?.status === 'success'; - -// Run mutations directly without hooks -const result = await rpc.users.updateUser(variables); +queryClient.setQueryData(["account-profile", accountId], nextProfile); +await queryClient.invalidateQueries({ queryKey: ["account-profile"] }); ``` -## Migration from Other Patterns - -### From Raw TanStack Query +The bound query helpers and callable mutations use that same client's query or mutation cache. Values thrown by TanStack are cast to the configured error type when returned as Err; the helpers do not validate thrown values at runtime. -```typescript -// Before: Manual Result unwrapping -const userQuery = useQuery({ - queryKey: ['users', id], - queryFn: async () => { - const result = await getUserFromAPI(id); - if (result.error) throw result.error; - return result.data; - }, -}); - -// After: Automatic Result handling -// React: -const userQuery = useQuery(rpc.users.getUserById(id).options); -// Svelte 5: -const userQuery = createQuery(() => rpc.users.getUserById(id).options); -// Result unwrapping handled automatically by wellcrafted -``` +## Define query keys -### From Service Layer Direct Calls +`defineKeys` validates a key map and preserves tuple information. ```typescript -// Before: No caching or reactivity -async function loadUserData() { - const { data, error } = await services.getUser(id); - if (error) setError(error); - else setUser(data); -} - -// After: Cached and reactive -// React: -const userQuery = useQuery(rpc.users.getUserById(id).options); -// Svelte 5: -const userQuery = createQuery(() => rpc.users.getUserById(id).options); -// Automatic state management, caching, background updates -``` - -## Framework-Specific Examples - -### React with wellcrafted +import { defineKeys } from "wellcrafted/query"; -```tsx -// Define once, use everywhere -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: () => services.getUserById(userId), +const accountKeys = defineKeys({ + all: ["accounts"], + detail: (accountId: string) => ["accounts", accountId], + exactDetail: (accountId: string) => ["accounts", accountId] as const, }); - -// React component usage (options passed directly) -function UserProfile({ userId }) { - const query = useQuery(userQuery.options); - - if (query.isPending) return ; - if (query.error) return ; - return ; -} - -// Imperative usage in event handlers -async function refreshUser() { - const { data, error } = await userQuery.fetch(); -} ``` -### Svelte 5 with wellcrafted - -```typescript -// Define once, use everywhere -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: () => services.getUserById(userId), -}); - -// Reactive component usage (Svelte 5 requires accessor function) -const query = createQuery(() => userQuery.options); - -// Imperative usage in actions -async function refreshUser() { - const { data, error } = await userQuery.fetch(); -} -``` +Static entries preserve readonly literal tuples without `as const`. Factory entries preserve tuple shape but widen literal positions. Add `as const` inside a factory when those positions must stay literal. -## See Also - - - - Learn the fundamentals of Result types and error handling - - - Understanding TaggedErrors and structured error handling - - - Building services that work perfectly with queries - - - -This integration provides the perfect balance of type safety, performance, and developer experience for modern reactive applications. +For every family, the Result-to-throw conversion is intentional: components read TanStack's normal `data` and `error` state, while service code can continue returning Results. diff --git a/docs/integrations/validation-libraries.mdx b/docs/integrations/validation-libraries.mdx index 9f69856..8b2bf95 100644 --- a/docs/integrations/validation-libraries.mdx +++ b/docs/integrations/validation-libraries.mdx @@ -1,172 +1,67 @@ --- -title: 'Using Brand with Validation Libraries' -description: 'Framework-agnostic branded types that work with ArkType, Zod, Valibot, and any runtime validator' -icon: 'puzzle-piece' +title: Validate branded values +description: Create runtime boundaries for wellcrafted Brand types with ArkType, Zod, or Valibot. +icon: puzzle-piece --- -# Using Brand with Validation Libraries +# Validate branded values -wellcrafted's `Brand` is a pure type utility. It doesn't care what runtime validator you use: ArkType, Zod, Valibot, or anything else. Define the branded type once, then create a runtime validator with whichever library you prefer. - -## The Lock-in Problem - -Every major validation library ships its own branding mechanism: - - - - ```typescript - const FileId = z.string().brand<"FileId">(); - type FileId = z.infer; - // FileId = string & z.$brand<"FileId"> (Zod-specific type) - ``` - - - ```typescript - const FileId = type("string").brand("FileId"); - type FileId = typeof FileId.infer; - // FileId = Brand (ArkType-specific type) - ``` - - - ```typescript - const FileId = v.pipe(v.string(), v.brand("FileId")); - type FileId = v.InferOutput; - // FileId = string & v.Brand<"FileId"> (Valibot-specific type) - ``` - - - -Each produces a **library-specific** branded type. If you switch validators, or use multiple validators in the same project, your domain types break. Your branded types become coupled to your validation library. - -## The Pattern - -Decouple the type from the validator: +`Brand` is type-only. It distinguishes values at compile time but performs no parsing, runtime validation, or serialization work. Pair the type with a validator at the boundary where an unknown or plain value becomes trusted domain data. ```typescript -import { type Brand } from "wellcrafted/brand"; +import type { Brand } from "wellcrafted/brand"; -// 1. Define the type: framework-agnostic, zero dependencies type FileId = string & Brand<"FileId">; ``` -Then create a runtime validator with your library of choice. All three produce the same `FileId` type: +The validator checks the runtime value first. Its final transform is the one place that asserts the branded output. ```typescript import { type } from "arktype"; + import type { Brand } from "wellcrafted/brand"; type FileId = string & Brand<"FileId">; - const FileId = type("string").pipe((s): FileId => s as FileId); + const FileId = type("string") + .narrow((value, ctx) => + value.length > 0 ? true : ctx.mustBe("a non-empty file id"), + ) + .pipe((value): FileId => value as FileId); ``` ```typescript import { z } from "zod"; + import type { Brand } from "wellcrafted/brand"; type FileId = string & Brand<"FileId">; - const FileId = z.string().transform((s): FileId => s as FileId); + const FileId = z + .string() + .min(1) + .transform((value): FileId => value as FileId); ``` ```typescript import * as v from "valibot"; + import type { Brand } from "wellcrafted/brand"; type FileId = string & Brand<"FileId">; - const FileId = v.pipe(v.string(), v.transform((s): FileId => s as FileId)); + const FileId = v.pipe( + v.string(), + v.minLength(1), + v.transform((value): FileId => value as FileId), + ); ``` -The explicit return type annotation `(s): FileId => ...` is what bridges the runtime validator to the branded type. The validator handles runtime checks; `Brand` handles compile-time safety. +TypeScript lets the type and validator share `FileId` because types and values occupy separate namespaces. In a type annotation it means the brand; at runtime it means the validator. -## Same Name for Type and Value +## Add domain validation before branding -TypeScript has two parallel namespaces: types and values. You can use the same PascalCase name for both the branded type and its runtime validator. TypeScript resolves which one you mean from context. - -```typescript -/** - * Unique file identifier in the storage system. - * Format: UUID v4 string. - */ -type FileId = string & Brand<"FileId">; -const FileId = type("string").pipe((s): FileId => s as FileId); -``` - -This gives you a single hover experience. Hover over `FileId` anywhere in your codebase, whether in a function signature, a schema definition, or an import, and you see the same JSDoc. - -```typescript -// In a function signature: hovering FileId shows the JSDoc above -function deleteFile(id: FileId): Promise { /* ... */ } - -// In a schema definition: same hover, same docs -const FileUpload = type({ id: FileId, name: "string" }); -``` - - -**No naming tax.** Contrast this with Zod's conventional pattern where you need two names: `fileIdSchema` for the validator and `FileId` for the type. With the dual-declaration pattern, one name flows through your entire system: type annotations, runtime validation, schema composition, and IDE hovers. - - -You can also use a type-only brand when no runtime validation is needed: - -```typescript -// Type-only: no runtime validator, just compile-time safety -type Guid = string & Brand<"Guid">; - -// Dual-declaration: type + validator share the name -type FileId = Guid & Brand<"FileId">; -const FileId = type("string").pipe((s): FileId => s as FileId); -``` - -## Composition: Hierarchical Brands - -wellcrafted brands compose through intersection. Child types are assignable to parent types, but not vice versa. Combine this with the dual-declaration pattern to get a full hierarchy of types and runtime validators: - -```typescript -/** Base identifier: any UUID v4 string. */ -type Guid = string & Brand<"Guid">; -const Guid = type("string").pipe((s): Guid => s as Guid); - -/** Unique file identifier in the storage system. */ -type FileId = Guid & Brand<"FileId">; -const FileId = type("string").pipe((s): FileId => s as FileId); - -/** Image file identifier: a FileId that points to an image. */ -type ImageId = FileId & Brand<"ImageId">; -const ImageId = type("string").pipe((s): ImageId => s as ImageId); -``` - -Each level is both a type and a runtime validator. The type hierarchy gives you compile-time subtyping: an `ImageId` is assignable anywhere a `FileId` or `Guid` is expected: - -```typescript -function getFile(id: FileId): Promise { /* ... */ } -function getGuid(id: Guid): string { return id; } - -const imageId = ImageId("img-abc-123"); -getFile(imageId); // ✅ ImageId extends FileId -getGuid(imageId); // ✅ ImageId extends Guid - -const fileId = FileId("file-xyz-789"); -const bad: ImageId = fileId; // ❌ Parent not assignable to child -``` - -This works because of wellcrafted's nested object structure: when brands intersect, their boolean markers merge: - -``` -ImageId's brand = { [brand]: { Guid: true, FileId: true, ImageId: true } } -FileId's brand = { [brand]: { Guid: true, FileId: true } } -Guid's brand = { [brand]: { Guid: true } } -``` - -`ImageId` has all of `FileId`'s markers (plus its own), so it satisfies the `FileId` constraint. - - -Zod's `.brand()`, ArkType's `.brand()`, and Valibot's `v.brand()` don't support hierarchical stacking. Their brand types are flat: you can't express "an ImageId is also a FileId" with built-in brands. - - -## Adding Real Validation - -The `.pipe` / `.transform` pattern isn't limited to passthrough casting. Add real runtime checks before branding: +The cast does not validate anything by itself. Put every runtime condition before the transform. @@ -174,22 +69,22 @@ The `.pipe` / `.transform` pattern isn't limited to passthrough casting. Add rea type Email = string & Brand<"Email">; const Email = type("string") - .narrow((s, ctx) => { - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) { - return ctx.mustBe("a valid email address"); - } - return true; - }) - .pipe((s): Email => s as Email); + .narrow((value, ctx) => + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) + ? true + : ctx.mustBe("a valid email address"), + ) + .pipe((value): Email => value as Email); ``` ```typescript type Email = string & Brand<"Email">; - const Email = z.string() + const Email = z + .string() .email() - .transform((s): Email => s as Email); + .transform((value): Email => value as Email); ``` @@ -199,21 +94,12 @@ The `.pipe` / `.transform` pattern isn't limited to passthrough casting. Add rea const Email = v.pipe( v.string(), v.email(), - v.transform((s): Email => s as Email), + v.transform((value): Email => value as Email), ); ``` -The branded type now guarantees two things: the value is a `string` (structural) **and** it's been validated as an email (nominal). Functions accepting `Email` know the validation already happened. - -## See Also +Only values produced by the successful validator path carry the runtime-validation evidence your application expects. A direct `value as Email` elsewhere can bypass that boundary, so keep assertions inside constructors or schemas. - - - Core guide to branded types, common patterns, and best practices - - - Why wellcrafted uses nested boolean markers for hierarchical brands - - +Branding does not change the serialized value. A branded string still crosses JSON as a string, and the receiver must validate it again before treating it as branded. diff --git a/skills/branded-types/SKILL.md b/skills/branded-types/SKILL.md index 5e3f5bf..5041203 100644 --- a/skills/branded-types/SKILL.md +++ b/skills/branded-types/SKILL.md @@ -1,130 +1,70 @@ --- name: branded-types -description: Type-safe distinct primitives with Brand from wellcrafted. Use when creating nominal types for IDs, tokens, or any primitive that shouldn't be interchangeable. +description: Use wellcrafted Brand for compile-time distinctions and pair it with explicit constructor or validator boundaries. --- -# Branded Types +# Branded types ```typescript -import type { Brand } from 'wellcrafted/brand'; +import type { Brand } from "wellcrafted/brand"; ``` -## The Problem - -TypeScript's structural typing lets you pass any `string` where another `string` is expected. A `UserId` and an `OrderId` are both strings — the compiler won't stop you from mixing them up. +`Brand` is type-only. It adds no runtime validation, parsing, or serialization behavior. ```typescript -function getUser(id: string) { /* ... */ } -function getOrder(id: string) { /* ... */ } - -const userId = '123'; -const orderId = '456'; -getUser(orderId); // No error — but wrong -``` +type UserId = string & Brand<"UserId">; +type OrderId = string & Brand<"OrderId">; -## The Brand Type - -`Brand` creates a phantom brand on a primitive. Two branded types from the same base are incompatible. - -```typescript -type UserId = string & Brand<'UserId'>; -type OrderId = string & Brand<'OrderId'>; +function getUser(userId: UserId) {} -function getUser(id: UserId) { /* ... */ } - -const userId = 'abc' as UserId; -const orderId = 'xyz' as OrderId; -getUser(userId); // compiles -getUser(orderId); // type error +declare const orderId: OrderId; +getUser(orderId); // type error ``` -Zero runtime footprint — `Brand` exists only at the type level. - -## Brand Constructor Pattern +## Create one boundary -Never scatter `as UserId` casts across the codebase. Create a brand constructor — one function, one `as` cast, single source of truth. +For an identity-only distinction, centralize the assertion in a PascalCase constructor: ```typescript -import type { Brand } from 'wellcrafted/brand'; - -// 1. Define the branded type -type UserId = string & Brand<'UserId'>; +type UserId = string & Brand<"UserId">; -// 2. Create the brand constructor — THE ONLY place with `as UserId` -// PascalCase matches the type name (TypeScript allows same-name type + value) -function UserId(id: string): UserId { - return id as UserId; +function UserId(value: string): UserId { + return value as UserId; } -// 3. Use everywhere -const id = UserId('abc-123'); -getUser(UserId(rawString)); +const userId = UserId(rawId); ``` -PascalCase constructors avoid parameter shadowing: +Do not scatter `as UserId` through application code. One constructor keeps the conversion searchable and gives you one place to add validation later. -```typescript -// No shadowing — UserId() is PascalCase, userId is camelCase -function processUser(userId: string) { - getUser(UserId(userId)); -} -``` - -## Adding Runtime Validation - -Brand constructors can validate before casting: +For constrained data, validate before branding. The type and validator can share a name because TypeScript has separate type and value namespaces. ```typescript -function Email(value: string): Email { - if (!value.includes('@')) { - throw new Error(`Invalid email: ${value}`); - } - return value as Email; -} -type Email = string & Brand<'Email'>; +import { z } from "zod"; + +type Email = string & Brand<"Email">; +const Email = z + .string() + .email() + .transform((value): Email => value as Email); ``` -Add validation when the brand represents a constrained value (emails, UUIDs, positive numbers). Skip it when the brand is purely for identity distinction (UserId, OrderId). +The assertion is safe only to the extent that the preceding runtime checks establish the application's rule. A direct assertion elsewhere bypasses that evidence. -## Anti-Patterns +## Assignability -### Scattered as casts +Different markers on the same base are incompatible. Brands can also form hierarchies through intersection: ```typescript -// WRONG — assertions everywhere, no single source of truth -const id = someString as UserId; // file1.ts -doSomething(otherId as UserId); // file2.ts -const parsed = key.split(':')[0] as UserId; // file3.ts - -// CORRECT — one constructor, used everywhere -const id = UserId(someString); -doSomething(UserId(otherId)); -const parsed = UserId(key.split(':')[0]); -``` - -### Missing constructor +type AbsolutePath = string & Brand<"AbsolutePath">; +type ConfigPath = AbsolutePath & Brand<"ConfigPath">; -```typescript -// WRONG — type exists but no constructor -type PostId = string & Brand<'PostId'>; -// Consumers forced to write `as PostId` everywhere - -// CORRECT — always pair the type with a constructor -type PostId = string & Brand<'PostId'>; -function PostId(id: string): PostId { - return id as PostId; -} +declare const configPath: ConfigPath; +const absolutePath: AbsolutePath = configPath; ``` -## Naming Convention - -| Branded Type | Constructor | -| --- | --- | -| `UserId` | `UserId()` | -| `OrderId` | `OrderId()` | -| `Email` | `Email()` | -| `ApiToken` | `ApiToken()` | +The child is assignable to the parent; the parent is not assignable to the child. -The constructor uses PascalCase matching the type name. TypeScript allows a type and value to share the same name since they occupy different namespaces. +Branding does not change the runtime value. A branded string crosses JSON as a string, and the receiving boundary must validate and brand it again. -See also: `patterns` skill for factory function patterns. +Use the validation integration page for focused ArkType, Zod, and Valibot recipes. Use the brand reference for the exact exported type contract. diff --git a/skills/define-errors/SKILL.md b/skills/define-errors/SKILL.md index 6bcf72b..6edabce 100644 --- a/skills/define-errors/SKILL.md +++ b/skills/define-errors/SKILL.md @@ -1,271 +1,92 @@ --- name: define-errors -description: Define typed, serializable error variants with defineErrors from wellcrafted. Use when creating error types, handling domain errors, or reviewing error definitions. +description: Define named wellcrafted error variants with honest fields, constructor-owned formatting, and explicit boundary behavior. --- -# defineErrors +# Define errors ```typescript import { defineErrors, extractErrorMessage, - type InferErrors, type InferError, -} from 'wellcrafted/error'; -``` - -## Core Rules - -1. All variants for a domain live in one `defineErrors` call — never spread across multiple calls -2. The factory returns `{ message, ...fields }` — no `.withMessage()` or `.withContext()` chains -3. `cause: unknown` is a field like any other — accept it in input, forward in return -4. Call `extractErrorMessage(cause)` inside the factory, never at the call site -5. Each call like `MyError.Variant({ ... })` returns `Err(...)` automatically -6. Shadow the const with a same-name type using `InferErrors` -7. Use `InferError` for a single variant's type -8. Variant names describe the specific failure mode — never `Service`, `Error`, or `Failed` -9. Aim for 2–5 variants per domain, each named by failure mode - -See also: `result-types` skill for `trySync`/`tryAsync` wrapping patterns. - -## Patterns - -### Zero-arg variant — static message - -```typescript -const UserError = defineErrors({ - AlreadyExists: () => ({ - message: 'A user with this email already exists', - }), -}); -type UserError = InferErrors; - -// Call site -return UserError.AlreadyExists(); -``` - -### Structured fields — message computed from input - -```typescript -const DbError = defineErrors({ - NotFound: ({ table, id }: { table: string; id: string }) => ({ - message: `${table} '${id}' not found`, - table, - id, - }), -}); -type DbError = InferErrors; - -// Call site -return DbError.NotFound({ table: 'users', id: '123' }); -// error.message → "users '123' not found" -// error.table → "users" -// error.id → "123" + type InferErrors, +} from "wellcrafted/error"; ``` -### Cause wrapping — extractErrorMessage inside the factory +## Define one vocabulary per domain ```typescript -import { extractErrorMessage } from 'wellcrafted/error'; - -const FileError = defineErrors({ - ReadFailed: ({ path, cause }: { path: string; cause: unknown }) => ({ - message: `Failed to read '${path}': ${extractErrorMessage(cause)}`, - path, - cause, +const UploadError = defineErrors({ + Rejected: ({ fileName, reasons }: { + fileName: string; + reasons: string[]; + }) => ({ + message: `Upload rejected for "${fileName}".`, + fileName, + reasons, }), - WriteFailed: ({ path, cause }: { path: string; cause: unknown }) => ({ - message: `Failed to write '${path}': ${extractErrorMessage(cause)}`, - path, - cause, + StorageUnavailable: ({ region }: { region: string }) => ({ + message: `Upload storage is unavailable in ${region}.`, + region, }), }); -type FileError = InferErrors; -// Call site — pass the raw caught error, never call extractErrorMessage here -catch: (error) => FileError.ReadFailed({ path: '/tmp/config.json', cause: error }), +type UploadError = InferErrors; +type RejectedError = InferError; ``` -### Multiple variants — discriminated union built-in +Each variant call returns an Err wrapper directly. The tagged body is under `.error`. ```typescript -const HttpError = defineErrors({ - Connection: ({ url, cause }: { url: string; cause: unknown }) => ({ - message: `Failed to connect to ${url}: ${extractErrorMessage(cause)}`, - url, - cause, - }), - Timeout: ({ url, ms }: { url: string; ms: number }) => ({ - message: `Request to ${url} timed out after ${ms}ms`, - url, - ms, - }), - Response: ({ status, body }: { status: number; body: unknown }) => ({ - message: `HTTP ${status}: ${extractErrorMessage(body)}`, - status, - body, - }), +const result = UploadError.Rejected({ + fileName: "report.csv", + reasons: ["too large"], }); -type HttpError = InferErrors; -// HttpError is automatically the union of all three variants -// Extracting a single variant type -type TimeoutError = InferError; +result.data; // null +result.error.name; // "Rejected" +result.error.fileName; // "report.csv" ``` -### Composing errors across layers +The key supplies `name`; the constructor must return `message: string`. Other fields are inferred but unconstrained. -Each layer defines its own error vocabulary. Inner errors become `cause` fields in higher-level errors. +## Name and shape variants honestly -```typescript -// Low-level: HTTP errors -const HttpError = defineErrors({ - Connection: ({ url, cause }: { url: string; cause: unknown }) => ({ - message: `Failed to connect to ${url}: ${extractErrorMessage(cause)}`, - url, - cause, - }), -}); +Use a distinct variant when callers need to distinguish a failure. Names such as `NotFound`, `InvalidInput`, `ReadFailed`, and `StorageUnavailable` state what happened. A `Failed` suffix is fine when paired with a specific operation; avoid context-free names such as `Error`, `Service`, or bare `Failed`. -// High-level: domain errors wrap HTTP errors via cause -const UserError = defineErrors({ - FetchFailed: ({ userId, cause }: { userId: string; cause: unknown }) => ({ - message: `Failed to fetch user ${userId}: ${extractErrorMessage(cause)}`, - userId, - cause, - }), -}); +Make fields required when every occurrence needs them. Use optional fields only when absence is meaningful for the same failure mode. If cases require different fields or message branches, split them into variants. -// The HTTP error becomes cause in the domain error -const { data, error } = await tryAsync({ - try: () => fetch(`/api/users/${userId}`), - catch: (cause) => UserError.FetchFailed({ userId, cause }), -}); -``` +The constructor owns message formatting. Callers pass structured inputs; they do not assemble messages or parse fields back out of human text. -See also: `patterns` skill for service layer architecture with error composition. +## Own the cause boundary -## Type Extraction +For an in-process diagnostic error, preserve a raw cause and format it inside the constructor: ```typescript -// Full union type for all variants -type HttpError = InferErrors; - -// Single variant type -type ConnectionError = InferError; -``` - -## Anti-Patterns - -### One defineErrors per variant - -```typescript -// WRONG — defeats the namespace grouping -const NotFoundError = defineErrors({ NotFound: () => ({ message: 'Not found' }) }); -const TimeoutError = defineErrors({ Timeout: () => ({ message: 'Timed out' }) }); - -// CORRECT — all variants for a domain in one call -const HttpError = defineErrors({ - NotFound: () => ({ message: 'Not found' }), - Timeout: () => ({ message: 'Timed out' }), -}); -``` - -### extractErrorMessage at the call site - -```typescript -// WRONG — call site does message extraction -catch: (error) => MyError.Failed({ message: extractErrorMessage(error) }); - -// CORRECT — pass raw cause, factory calls extractErrorMessage -catch: (error) => MyError.Failed({ cause: error }); -``` - -### Generic variant names - -```typescript -// WRONG — "Service" says nothing about the failure mode -const UserError = defineErrors({ - Service: ({ message }: { message: string }) => ({ message }), -}); - -// CORRECT — name each variant by what actually went wrong -const UserError = defineErrors({ - AlreadyExists: ({ email }: { email: string }) => ({ - message: `User ${email} already exists`, - email, - }), - CreateFailed: ({ cause }: { cause: unknown }) => ({ - message: `Failed to create user: ${extractErrorMessage(cause)}`, - cause, - }), -}); -``` - -### Monolithic catch-all variant - -```typescript -// WRONG — one variant with an operation string hides failure modes -const DbError = defineErrors({ - Failed: ({ operation, cause }: { operation: string; cause: unknown }) => ({ - message: `Failed to ${operation}: ${extractErrorMessage(cause)}`, - operation, - cause, - }), -}); - -// CORRECT — each operation is its own variant -const DbError = defineErrors({ - QueryFailed: ({ cause }: { cause: unknown }) => ({ - message: `Query failed: ${extractErrorMessage(cause)}`, - cause, - }), - InsertFailed: ({ cause }: { cause: unknown }) => ({ - message: `Insert failed: ${extractErrorMessage(cause)}`, +const FileError = defineErrors({ + ReadFailed: ({ path, cause }: { path: string; cause: unknown }) => ({ + message: `Could not read "${path}": ${extractErrorMessage(cause)}`, + path, cause, }), }); ``` -### Discriminated union inputs (sub-discriminants) +For a JSON boundary, normalize the cause into compatible fields instead: ```typescript -// WRONG — reason field creates a sub-discriminant, forces double narrowing -const FormError = defineErrors({ - Invalid: (input: { - reason: 'bad_email' | 'weak_password' | 'mismatch'; - value?: string; - }) => ({ - message: { bad_email: `Invalid email: '${input.value}'`, /* ... */ }[input.reason], - ...input, - }), -}); - -// CORRECT — each failure is its own variant with honest types -const FormError = defineErrors({ - InvalidEmail: ({ value }: { value: string }) => ({ - message: `Invalid email: '${value}'`, - value, - }), - WeakPassword: () => ({ - message: 'Password must be at least 8 characters', - }), - PasswordMismatch: () => ({ - message: 'Passwords do not match', +const WireError = defineErrors({ + ReadFailed: ({ path, cause }: { path: string; cause: unknown }) => ({ + message: `Could not read "${path}".`, + path, + causeMessage: extractErrorMessage(cause), }), }); ``` -### Conditional logic in factories - -If the factory branches on its inputs to decide the message, each branch should be its own variant. The branching is evidence that multiple errors are hiding in one. +`defineErrors` does not type-enforce JSON compatibility. Native Errors, functions, bigint values, class instances, and cyclic values can be accepted and may change or fail during serialization. The plain-data promise applies only when every field in the complete value is JSON-compatible. -### Using ReturnType instead of InferErrors +Error bodies are shallow-frozen. Nested objects and arrays remain mutable. -```typescript -// WRONG -type MyError = ReturnType; - -// CORRECT -type MyError = InferErrors; -``` +The canonical vocabulary examples are in `examples/quick-start.ts`, `examples/service-boundary.ts`, and `examples/serialization-boundary.ts`. Use the public error reference for low-level exported types and extractor edge cases. diff --git a/skills/patterns/SKILL.md b/skills/patterns/SKILL.md index 9690ca7..ba1e689 100644 --- a/skills/patterns/SKILL.md +++ b/skills/patterns/SKILL.md @@ -1,367 +1,119 @@ --- name: patterns -description: Architectural patterns for code that uses wellcrafted. Covers control flow with trySync/tryAsync, factory function composition, service layers with Result types, error composition across boundaries, and the single-or-array pattern. +description: Compose wellcrafted service, propagation, serialization, validation, and UI boundaries with explicit ownership. --- -# Patterns +# Application patterns -A style guide for code that uses wellcrafted. Not API reference — architectural taste. +Use this skill for architecture decisions. Use the public references for exhaustive API signatures. -## Human-Readable Control Flow +## Service boundary -Mirror natural human reasoning: try the thing, check if it failed, continue on the happy path. - -### Linearizing try-catch into guards - -Before — nested, mixed throw/return: +Create services as factory functions with explicit dependencies. Wrap only the dependency call that throws, then keep Result propagation linear. ```typescript -async function handleRequest(userId: string) { - try { - const user = await fetchUser(userId); - const posts = await fetchPosts(user.id); - return Response.json({ user, posts }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return Response.json({ error: message }, { status: 500 }); +function createUserService({ + readRecord, +}: { + readRecord(userId: string): User | null; +}) { + function findUser(userId: string): Result { + return trySync({ + try: () => readRecord(userId), + catch: (cause) => + UserError.ReadFailed({ cause: extractErrorMessage(cause) }), + }); } -} -``` - -After — linear guards with `tryAsync`: - -```typescript -import { tryAsync, Err } from 'wellcrafted/result'; - -async function handleRequest(userId: string) { - const { data: user, error: userError } = await tryAsync({ - try: () => fetchUser(userId), - catch: (cause) => UserError.FetchFailed({ userId, cause }), - }); - if (userError) return Response.json({ error: userError.message }, { status: 502 }); - - const { data: posts, error: postsError } = await tryAsync({ - try: () => fetchPosts(user.id), - catch: (cause) => PostError.FetchFailed({ userId: user.id, cause }), - }); - if (postsError) return Response.json({ error: postsError.message }, { status: 502 }); - - return Response.json({ user, posts }); -} -``` - -Each guard has the same shape: do the thing → check → return early on failure. The happy path accumulates at the bottom. - -### Natural-language boolean variables - -Name booleans so they read like thoughts: - -```typescript -const isAuthenticated = session && !session.expired; -const needsRefresh = token.expiresAt < Date.now() + BUFFER_MS; -const canSkipValidation = input.source === 'trusted' && input.validated; - -if (!isAuthenticated) return Response.json({ error: 'Unauthorized' }, { status: 401 }); -if (needsRefresh) await refreshToken(token); -``` - -### Early returns as guard clauses - -```typescript -async function createUser(email: string): Promise> { - // Guard: validate input - if (!email.includes('@')) return UserError.InvalidEmail({ email }); - - // Guard: check uniqueness - const existing = await db.findByEmail(email); - if (existing) return UserError.AlreadyExists({ email }); - - // Happy path - return tryAsync({ - try: () => db.users.create({ email }), - catch: (cause) => UserError.CreateFailed({ email, cause }), - }); -} -``` - -## Factory Function Composition - -### The universal signature - -Every factory function follows this shape: - -```typescript -function createSomething(dependencies, options?) { - return { /* methods */ }; -} -``` - -Two arguments max. First is resources, second is config. Dependencies come first because they're what makes the factory reusable — the same factory with different deps produces different behavior. - -```typescript -// Single dependency -function createUserService(db: Database) { - return { - getById(userId: string) { /* uses db */ }, - create(data: CreateUserInput) { /* uses db */ }, - }; -} - -// Multiple dependencies -function createNotificationService({ email, sms }: { email: EmailClient; sms: SmsClient }) { - return { - notify(userId: string, message: string) { /* uses email, sms */ }, - }; -} -``` - -### Separating option layers - -Each layer owns its own configuration. Don't mix them. - -```typescript -// WRONG — mixed options blob -sendEmail({ - timeout: 5000, // client option - retries: 3, // client option - to: 'alice@co.com', // method option - subject: 'Hello', // method option -}); - -// CORRECT — each layer has its own options -const client = createEmailClient({ timeout: 5000, retries: 3 }); -const service = createEmailService(client); -service.send({ to: 'alice@co.com', subject: 'Hello' }); -``` - -### Anti-patterns -```typescript -// WRONG — client creation hidden inside -function sendEmail(clientOptions: ClientOpts, emailOptions: EmailOpts) { - const client = createClient(clientOptions); // Hidden! - return client.send(emailOptions); -} - -// CORRECT — visible dependency chain -const client = createEmailClient(clientOptions); -const service = createEmailService(client); -service.send(emailOptions); -``` - -```typescript -// WRONG — function takes client as first argument everywhere -function getUser(db: Database, userId: string) { ... } -function createUser(db: Database, data: UserInput) { ... } - -// CORRECT — factory captures the dependency -const userService = createUserService(db); -userService.getById(userId); -userService.create(data); -``` - -### Internal zone ordering - -Inside a factory, organize code in four zones: - -```typescript -function createUserService(db: Database, options?: { maxRetries?: number }) { - // Zone 1 — Immutable state (const from deps/options) - const maxRetries = options?.maxRetries ?? 3; - - // Zone 2 — Mutable state (let declarations) - let connectionCount = 0; - - // Zone 3 — Private helpers (not exposed) - function withRetry(fn: () => Promise): Promise { /* ... */ } - - // Zone 4 — Public API (always last) return { - async getById(userId: string): Promise> { /* ... */ }, - async create(data: CreateUserInput): Promise> { /* ... */ }, + getDisplayName(userId: string): Result { + const userResult = findUser(userId); + if (userResult.error !== null) return userResult; + if (userResult.data === null) return UserError.NotFound({ userId }); + return Ok(userResult.data.displayName); + }, }; } ``` -The return object is always last — it's the complete public API. +Services do not reach into UI state. Pass clients, storage, configuration, and clocks through factory or method inputs. -## Service Layer Pattern +## Propagation and recovery -Services are factory functions that return objects with methods returning `Result`. Each service defines its own error vocabulary with `defineErrors`. +Use `error !== null`, `isErr`, or `isOk`; never generic truthiness. Falsy Err values are permitted, and `Ok(null)` is valid. -### Complete example +When you keep the whole Result, return its narrowed Err branch directly. When you destructure, wrap the raw error again: ```typescript -import { defineErrors, extractErrorMessage, type InferErrors } from 'wellcrafted/error'; -import { Ok, Err, tryAsync, type Result } from 'wellcrafted/result'; +const result = await readUser(userId); +if (result.error !== null) return result; -// 1. Define domain errors -const UserError = defineErrors({ - NotFound: ({ userId }: { userId: string }) => ({ - message: `User ${userId} not found`, - userId, - }), - CreateFailed: ({ email, cause }: { email: string; cause: unknown }) => ({ - message: `Failed to create user ${email}: ${extractErrorMessage(cause)}`, - email, - cause, - }), - FetchFailed: ({ cause }: { cause: unknown }) => ({ - message: `Failed to fetch users: ${extractErrorMessage(cause)}`, - cause, - }), -}); -type UserError = InferErrors; - -// 2. Factory function returns service object -function createUserService(db: Database) { - return { - async getById(userId: string): Promise> { - const { data: user, error } = await tryAsync({ - try: () => db.users.findById(userId), - catch: (cause) => UserError.FetchFailed({ cause }), - }); - // error here is the raw tagged error, not an Err: wrap it before returning - if (error) return Err(error); - if (!user) return UserError.NotFound({ userId }); - return Ok(user); - }, - - async create(email: string): Promise> { - return tryAsync({ - try: () => db.users.insert({ email }), - catch: (cause) => UserError.CreateFailed({ email, cause }), - }); - }, - }; -} +const { data, error } = await readPosts(result.data.id); +if (error !== null) return Err(error); -// 3. Export factory + Live instance -type UserService = ReturnType; -const UserServiceLive = createUserService(productionDb); +return Ok({ user: result.data, posts: data }); ``` -The factory is for testing (inject mocks), the Live instance is for production. - -### Namespace re-exports +Recover with `Ok(fallback)` only at a layer that can honestly satisfy its success contract. -Organize services hierarchically: +## Serialization boundary -```typescript -// services/index.ts -import { UserServiceLive } from './user'; -import { PostServiceLive } from './post'; - -export const services = { - users: UserServiceLive, - posts: PostServiceLive, -} as const; -``` +Reuse the domain vocabulary across JSON, HTTP, workers, IPC, logs, or persistence when the complete Result, data, and error values are JSON-compatible. No separate wire type is required in that case. -## Error Composition Across Layers - -Each layer defines its own error vocabulary. Inner errors become `cause` fields in higher-level errors. `extractErrorMessage` formats them inside the factory. +When an existing payload is not boundary-friendly, normalize the incompatible fields or introduce a boundary-specific wire vocabulary. Choose based on ownership: change the domain variant when every caller needs the normalized field; add a wire variant when in-process callers still need richer values such as a raw cause. ```typescript -// Layer 1: HTTP client errors -const HttpError = defineErrors({ - Connection: ({ url, cause }: { url: string; cause: unknown }) => ({ - message: `Failed to connect to ${url}: ${extractErrorMessage(cause)}`, - url, - cause, - }), - Response: ({ url, status }: { url: string; status: number }) => ({ - message: `${url} returned HTTP ${status}`, - url, - status, - }), -}); - -// Layer 2: domain service wraps HTTP errors via cause -const UserError = defineErrors({ - NotFound: ({ userId }: { userId: string }) => ({ - message: `User ${userId} not found`, - userId, - }), - FetchFailed: ({ userId, cause }: { userId: string; cause: unknown }) => ({ - message: `Failed to fetch user ${userId}: ${extractErrorMessage(cause)}`, - userId, - cause, +const WireError = defineErrors({ + ReadFailed: ({ resourceId, cause }: { + resourceId: string; + cause: unknown; + }) => ({ + message: `Could not read "${resourceId}".`, + resourceId, + causeMessage: extractErrorMessage(cause), }), }); -type UserError = InferErrors; - -// The HTTP error becomes cause in the domain error -async function getUser(userId: string): Promise> { - const { data: response, error } = await tryAsync({ - try: () => fetch(`/api/users/${userId}`), - catch: (cause) => UserError.FetchFailed({ userId, cause }), - // raw fetch error ^^^^ becomes cause - }); - // error is the raw tagged error here: wrap it before returning - if (error) return Err(error); - - if (response.status === 404) return UserError.NotFound({ userId }); - - return tryAsync({ - try: () => response.json() as Promise, - catch: (cause) => UserError.FetchFailed({ userId, cause }), - }); -} ``` -The full error chain is JSON-serializable at every level. Log it, send it over the wire, display it in a toast. +`defineErrors` does not enforce JSON-compatible fields. Do not claim a raw native Error, arbitrary cause chain, class instance, bigint value, function, or cycle is wire-safe. The conditional promise applies only when every field in the complete value is JSON-compatible. -See also: `define-errors` skill for error variant definitions. `result-types` skill for trySync/tryAsync patterns. +## Validation boundary -## The Single-or-Array Pattern - -Accept both single items and arrays, normalize at the top, process uniformly. +Treat parsed JSON as unknown. Preserving a Result-shaped envelope does not validate its payload. ```typescript -function deleteUsers(userOrUsers: User | User[]): Promise> { - const users = Array.isArray(userOrUsers) ? userOrUsers : [userOrUsers]; - - // One code path for both cases - const ids = users.map((u) => u.id); - return tryAsync({ - try: () => db.users.bulkDelete(ids), - catch: (cause) => DbError.DeleteFailed({ cause }), - }); +const unknownBody: unknown = await response.json(); +const parsed = resultEnvelopeSchema.safeParse(unknownBody); +if (!parsed.success) { + reportInvalidResponse(parsed.error.issues); + return; } -// Works with one -await deleteUsers(user); - -// Works with many -await deleteUsers([user1, user2, user3]); +const result = parsed.data; ``` -### Naming convention +Do not write `await response.json() as Result`. A static assertion inspects no bytes. Validate the full envelope and active data or error payload. -| Parameter | Normalized Variable | -| --- | --- | -| `userOrUsers` | `users` | -| `itemOrItems` | `items` | -| `postOrPosts` | `posts` | +## UI and query boundary -### Anti-patterns +Transform domain errors when the UI owns a different error contract. Then let the TanStack adapter convert the Result into its data/error channels. ```typescript -// WRONG — separate functions for single vs array -function deleteUser(user: User): Promise<...>; -function deleteUsers(users: User[]): Promise<...>; - -// WRONG — forcing arrays everywhere -deleteUsers([user]); // awkward for single items +const profileOptions = resultQueryOptions({ + queryKey: ["profile", userId], + queryFn: async () => { + const result = await userService.getProfile(userId); + if (result.error !== null) { + return Err({ + title: "Could not load the profile", + description: result.error.message, + }); + } + return Ok(result.data); + }, +}); +``` -// WRONG — duplicated logic in overloads -function deleteUser(user: User) { return db.delete(user.id); } -function deleteUsers(users: User[]) { return db.bulkDelete(users.map(u => u.id)); } +Keep cache reads, writes, and invalidation on `QueryClient`. Use the query-factories skill for the direct-adapter and bound-factory shapes. -// CORRECT — single implementation -function deleteUsers(userOrUsers: User | User[]) { - const users = Array.isArray(userOrUsers) ? userOrUsers : [userOrUsers]; - return db.bulkDelete(users.map((u) => u.id)); -} -``` +Canonical checked examples live in `examples/`. Public guides own the longer explanations; this skill should stay a compact implementation checklist. diff --git a/skills/query-factories/SKILL.md b/skills/query-factories/SKILL.md index 95da4de..141492c 100644 --- a/skills/query-factories/SKILL.md +++ b/skills/query-factories/SKILL.md @@ -1,203 +1,98 @@ --- name: query-factories -description: TanStack Query integration with wellcrafted's createQueryFactories, defineQuery, and defineMutation. Use when setting up queries/mutations that return Result types or using reactive options and imperative helpers. +description: Adapt wellcrafted Results to TanStack Query through direct options adapters or QueryClient-bound factories. --- -# Query Factories +# TanStack Query adapters -```typescript -import { createQueryFactories } from 'wellcrafted/query'; -``` - -## Setup +Install the tested type prerequisite explicitly; wellcrafted does not currently declare it for consumers. -`createQueryFactories` takes a TanStack `QueryClient` and returns `defineQuery` and `defineMutation`: +```bash +bun add @tanstack/query-core@5.82.0 +``` ```typescript -import { QueryClient } from '@tanstack/react-query'; // or @tanstack/svelte-query -import { createQueryFactories } from 'wellcrafted/query'; - -const queryClient = new QueryClient(); -const { defineQuery, defineMutation } = createQueryFactories(queryClient); +import { + createQueryFactories, + defineKeys, + resultMutationOptions, + resultQueryOptions, +} from "wellcrafted/query"; ``` -## defineQuery +## Choose one family -Define a query whose `queryFn` returns a `Result`: +Use `resultQueryOptions` and `resultMutationOptions` when a framework hook owns execution: ```typescript -import { Ok, Err, type Result } from 'wellcrafted/result'; - -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: async (): Promise> => { - const { data, error } = await getUser(userId); - if (error) { - return Err({ - title: 'Failed to load user', - description: error.message, - }); - } - return Ok(data); - }, +const profileOptions = resultQueryOptions({ + queryKey: ["account-profile", accountId], + queryFn: () => accountService.getProfile(accountId), }); -``` - -## defineMutation - -Same pattern for mutations: -```typescript -const createPost = defineMutation({ - mutationKey: ['posts', 'create'], - mutationFn: async (input: { title: string; body: string }) => { - const { data, error } = await postService.create(input); - if (error) { - return Err({ - title: 'Failed to create post', - description: error.message, - }); - } - return Ok(data); - }, +const signOutOptions = resultMutationOptions({ + mutationKey: ["account", "signOut"], + mutationFn: () => accountService.signOut(), }); ``` -`mutationKey` is required on `defineMutation` and `resultMutationOptions`, just as `queryKey` is required on `defineQuery`. - -## Reactive Options and Imperative Helpers - -Every query and mutation provides reactive options plus imperative helpers. +Ok resolves into TanStack's data channel. Err throws its contained value into TanStack's error channel. -### Reactive: `.options` - -Pass `.options` to your framework's query hook. It's a static object — wrap it in an accessor for Svelte, pass directly in React. +Options are snapshots. In reactive frameworks, construct them inside the hook's reactive accessor when keys, `enabled`, or other options depend on reactive state. ```typescript -// React -import { useQuery, useMutation } from '@tanstack/react-query'; - -const query = useQuery(userQuery.options); -const mutation = useMutation(createPost.options); -``` - -```typescript -// Svelte -import { createQuery, createMutation } from '@tanstack/svelte-query'; - -const query = createQuery(() => userQuery.options); -const mutation = createMutation(() => createPost.options); +const profile = createQuery(() => + resultQueryOptions({ + queryKey: ["account-profile", accountId], + queryFn: () => accountService.getProfile(accountId), + enabled: accountId !== null, + }), +); ``` -### Imperative: `.fetch()`, `.ensure()`, and callable mutations - -Use in event handlers and workflows without reactive overhead: +Use `createQueryFactories(queryClient)` when the same client should own options and imperative handles: ```typescript -// Queries use .fetch() for TanStack freshness policy -const fetched = await userQuery.fetch(); - -// Queries use .ensure() for cache-first reads -const ensured = await userQuery.ensure(); +const { defineQuery, defineMutation } = createQueryFactories(queryClient); -// Mutations are callable -const created = await createPost({ - title: 'Hello', - body: 'World', +const profile = defineQuery({ + queryKey: ["account-profile", accountId], + queryFn: () => accountService.getProfile(accountId), }); -``` - -### When to use each -| `.options` (reactive) | `.fetch()`, `.ensure()`, callable mutation (imperative) | -| --- | --- | -| Component data display | Event handlers | -| Loading/error states | Sequential workflows | -| Auto-refetch | One-time operations | -| Cache synchronization | Outside component context | - -## Error Transformation - -Transform service errors into user-facing errors at the query boundary. Service errors describe what went wrong technically; user-facing errors describe what to show the user. - -```typescript -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: async () => { - // Service returns technical error (UserError.NotFound, UserError.FetchFailed) - const { data, error } = await getUser(userId); - - if (error) { - // Transform to user-facing error - return Err({ - title: 'Failed to load user', - description: error.message, - }); - } - - return Ok(data); - }, +const signOut = defineMutation({ + mutationKey: ["account", "signOut"], + mutationFn: () => accountService.signOut(), }); ``` -### Anti-pattern: returning raw service errors +The query shape is `{ options, fetch(), ensure() }` and is not callable. `fetch()` uses TanStack's freshness policy; `ensure()` prefers existing cache data. -```typescript -// WRONG — raw service errors leak into the UI layer -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: () => getUser(userId), // Raw UserError reaches components -}); +The mutation is callable and has `.options`: -// CORRECT — transform at the boundary -const userQuery = defineQuery({ - queryKey: ['users', userId], - queryFn: async () => { - const { data, error } = await getUser(userId); - if (error) return Err({ title: 'Failed to load user', description: error.message }); - return Ok(data); - }, -}); +```typescript +await signOut(undefined); +signOut.options; ``` -## Query Key Organization +There is no `.execute()` method. -Organize keys hierarchically for targeted cache invalidation: +## Keep cache work on QueryClient -```typescript -const userKeys = { - all: ['users'] as const, - lists: ['users', 'list'] as const, - byId: (id: string) => ['users', id] as const, - posts: (id: string) => ['users', id, 'posts'] as const, -}; -``` +Call `invalidateQueries`, `setQueryData`, `getQueryData`, prefetching, and other cache methods directly on the owning `QueryClient`. wellcrafted adapts Result-returning functions; it does not replace TanStack's cache API. -Invalidating `['users']` clears everything; invalidating `['users', id]` clears just that user. +Thrown values caught by bound helpers are cast to the configured error type, not runtime-validated. -## Cache Management - -Optimistic updates for instant UI feedback: +## Define keys ```typescript -const updateUser = defineMutation({ - mutationKey: ['users', 'update'], - mutationFn: async (input: { userId: string; name: string }) => { - const { data, error } = await userService.update(input); - if (error) return Err({ title: 'Failed to update user', description: error.message }); - - // Optimistic cache update - queryClient.setQueryData( - userKeys.byId(input.userId), - (old) => old ? { ...old, name: input.name } : old, - ); - - // Invalidate to refetch fresh data in background - queryClient.invalidateQueries({ queryKey: userKeys.all }); - - return Ok(data); - }, +const accountKeys = defineKeys({ + all: ["accounts"], + detail: (accountId: string) => ["accounts", accountId], + exactDetail: (accountId: string) => ["accounts", accountId] as const, }); ``` -See also: `result-types` skill for the Result type pattern. `define-errors` skill for creating error variants. +Static entries preserve readonly literal tuples. Factory entries preserve tuple shape but widen literal positions unless the return uses `as const`. + +`examples/tanstack-query.ts` is the canonical checked example. Use the public integration guide for the attributed reactive account pattern and the query reference for exact contracts. diff --git a/skills/result-types/SKILL.md b/skills/result-types/SKILL.md index 086c9bb..ec0cc97 100644 --- a/skills/result-types/SKILL.md +++ b/skills/result-types/SKILL.md @@ -1,288 +1,88 @@ --- name: result-types -description: Working with Result types, Ok, Err, trySync, tryAsync, and utility functions from wellcrafted. Use when wrapping unsafe code, handling errors with Results, or destructuring { data, error } responses. +description: Use wellcrafted Result values, exact guards, throwing boundaries, recovery, and small Result utilities. --- -# Result Types +# Result types ```typescript -import { Ok, Err, trySync, tryAsync, type Result } from 'wellcrafted/result'; +import { + Err, + Ok, + isErr, + isOk, + partitionResults, + resolve, + tapErr, + tryAsync, + trySync, + unwrap, + type Result, +} from "wellcrafted/result"; ``` -## The Shape - -Results are plain objects with two properties — `data` and `error`. Successful results carry a `T` in `data` with `error: null`; failed results carry an `E` in `error` with `data: null`. - -```typescript -type Ok = { data: T; error: null }; -type Err = { error: E; data: null }; -type Result = Ok | Err; -``` - -This is the same destructuring shape used by Supabase and SvelteKit load functions. **Always discriminate by the error side — `isErr(result)` or `result.error !== null`:** +## Shape and discrimination ```typescript -const { data, error } = await someOperation(); -if (error !== null) { - // error is E, data is null - return; -} -// data is T here +type Result = + | { data: T; error: null } + | { data: null; error: E }; ``` -### Never discriminate by `data` - -`Ok(null)` is a legitimate value (`T` can be `null` — common for "not found is not an error"), so `data === null` is ambiguous: it could be `Ok` or `Err`. The only reliable discriminator is the error side. +Use `result.error !== null`, `isErr(result)`, or `isOk(result)`. Do not use generic truthiness: the public Err type permits `0`, `false`, `""`, `undefined`, and `NaN`. ```typescript -// Wrong — Ok(null) is legal; this treats success as failure -if (result.data === null) { /* handle "error" */ } +const result = await loadUser(userId); +if (result.error !== null) return result; -// Right — non-null on the error side means Err, by convention -if (result.error !== null) { /* handle error */ } - -// Right — named guard, same check, clearer intent -if (isErr(result)) { /* handle error */ } +return Ok(result.data.name); ``` -**Don't call `Err(null)`.** It produces `{ data: null, error: null }` — structurally identical to `Ok(null)`. Under the shape, `isErr`/`isOk` read it as Ok, so `Err(null)` silently becomes success. `Err(undefined)` is also discouraged — the discriminator technically works (`undefined !== null` is true), but the value is meaningless and trips `if (error)` falsy checks downstream. Either: - -- Use `Ok(null)`/`Ok(undefined)` (if what you meant was success-with-no-payload). -- Define a tagged error via `defineErrors` with a real name. -- Wrap a caught exception in one of your `defineErrors` variants, e.g. `MyError.Unexpected({ cause: error })` (see below). There is no `TaggedError` factory to import; you create the namespace yourself with `defineErrors`. +`Ok(null)` is valid. `Err(null)` creates the same `{ data: null, error: null }` structure, so no guard can recover the intended branch. Keep error values non-null and meaningful. `isResult` checks only for a non-null object with both keys; it does not validate payloads. -At every `catch (error: unknown)` boundary, don't pass the raw `unknown` to `Err`. Wrap it in a tagged error via `defineErrors`. The tagged error is non-null by construction, so the shape's invariant holds regardless of what was thrown (including `throw null`). See `docs/philosophy/err-null-is-ok-null.md` for why this is a documentation rule rather than a type-level constraint. +## Wrap a narrow throwing operation -## Constructors +Use `trySync` or `tryAsync` around the smallest operation with one failure meaning. The catch callback returns an Err-producing variant or an Ok fallback. ```typescript -// Success -const result = Ok({ id: '123', name: 'Alice' }); - -// Failure -const result = Err({ name: 'NotFound', message: 'User not found' }); - -// Void success — use Ok(undefined) -const result = Ok(undefined); -``` - -## trySync and tryAsync - -Wrap throwing operations into Results. The `catch` handler receives the raw error and returns an error variant. - -```typescript -import { defineErrors, extractErrorMessage } from 'wellcrafted/error'; - -const JsonError = defineErrors({ - ParseFailed: ({ input, cause }: { input: string; cause: unknown }) => ({ - message: `Invalid JSON: ${extractErrorMessage(cause)}`, - input: input.slice(0, 100), - cause, - }), +const userResult = trySync({ + try: () => records.get(userId) ?? null, + catch: (cause) => + UserError.ReadFailed({ cause: extractErrorMessage(cause) }), }); -// Synchronous -const { data, error } = trySync({ - try: () => JSON.parse(rawInput), - catch: (cause) => JsonError.ParseFailed({ input: rawInput, cause }), -}); - -// Asynchronous — always await -const { data, error } = await tryAsync({ - try: () => fetch(url).then((r) => r.json()), - catch: (cause) => HttpError.Connection({ url, cause }), -}); +if (userResult.error !== null) return userResult; +if (userResult.data === null) return UserError.NotFound({ userId }); +return Ok(userResult.data); ``` -See also: `define-errors` skill for creating error variants. - -## Key Rules - -1. Use `trySync` for synchronous code, `tryAsync` for async -2. Always `await` tryAsync — it returns a Promise -3. Match return types — if try returns `T`, catch should return `Err` or `Ok` for recovery -4. Use `Ok(undefined)` for void operations -5. Return `Err(error)` to propagate errors up the chain -6. Pass the raw caught error as `cause` — let the error factory call `extractErrorMessage` - -## Recovery Pattern - -When `catch` returns `Ok(fallback)` instead of `Err`, the return type narrows to `Ok` — no error checking needed: - -```typescript -const { data: config } = trySync({ - try: (): unknown => JSON.parse(configJson), - catch: () => Ok({ theme: 'dark', fontSize: 14 }), -}); -// config is always defined — the catch recovered -``` - -```typescript -// File existence check with fallback -const { data: exists } = trySync({ - try: () => fs.existsSync(path), - catch: () => Ok(false), -}); -``` - -## Wrapping Guidelines - -### Minimal wrap — only the risky operation +When destructuring, `error` is the raw error body. Wrap it to return a Result: ```typescript -// CORRECT: Wrap only the call that can throw -const { data: response, error } = await tryAsync({ - try: () => fetch(`/api/users/${userId}`), - catch: (cause) => UserError.FetchFailed({ userId, cause }), -}); +const { data, error } = await operation(); if (error !== null) return Err(error); - -// Continue with non-throwing operations -const user = await response.json(); -return Ok(user); -``` - -```typescript -// WRONG: Wrapping too much -const { data, error } = await tryAsync({ - try: async () => { - const response = await fetch(`/api/users/${userId}`); - const user = await response.json(); - await updateCache(user); - return user; - }, - catch: (error) => Err(error), // Too vague -}); -``` - -### Immediate return pattern - -Return errors immediately after checking. This creates linear control flow. - -```typescript -// CORRECT: Check and return immediately -const { data: user, error: fetchError } = await getUser(userId); -if (fetchError) return Err(fetchError); - -const { data: posts, error: postsError } = await getPosts(user.id); -if (postsError) return Err(postsError); - -return Ok({ user, posts }); +return Ok(data); ``` -```typescript -// WRONG: Nested error handling -const { data: user, error: fetchError } = await getUser(userId); -if (!fetchError) { - const { data: posts, error: postsError } = await getPosts(user.id); - if (!postsError) { - return Ok({ user, posts }); - } else { - return Err(postsError); - } -} else { - return Err(fetchError); -} -``` - -### When to extend the try block - -Include multiple operations in one block when they must succeed or fail together: +Return `Ok(fallback)` only when the current layer can honestly recover. ```typescript -// Atomic operation — all steps are part of "save document" -const { data, error } = await tryAsync({ - try: async () => { - const validated = schema.parse(document); - const saved = await db.documents.insert(validated); - await index.add(saved.id, saved.content); - return saved; - }, - catch: (cause) => DbError.InsertFailed({ cause }), +const config = trySync({ + try: () => JSON.parse(text) as unknown, + catch: () => Ok(defaultConfig), }); ``` -## The Destructured-Error Gotcha +The helpers catch only the `try` callback. A throwing catch callback escapes. -When you destructure `{ data, error }`, the `error` variable is the raw error value — NOT wrapped in `Err`. You must wrap it before returning from a function that returns `Result`: - -```typescript -// WRONG — error is the raw value, not a Result -const { data, error } = await tryAsync({ ... }); -if (error !== null) return error; // Type error: returns raw error, not Result +## Deliberate throwing boundaries -// CORRECT — wrap with Err() to return a proper Result -const { data, error } = await tryAsync({ ... }); -if (error !== null) return Err(error); -``` - -This is different from returning the entire result object: - -```typescript -// Also correct — result is already a Result type -const result = await tryAsync({ ... }); -if (result.error !== null) return result; // Returns the full Result -``` - -## Utility Functions - -### isOk / isErr — type guards - -```typescript -import { isOk, isErr } from 'wellcrafted/result'; - -const result = await getUser(userId); - -if (isOk(result)) { - console.log(result.data.name); -} - -if (isErr(result)) { - console.log(result.error.message); -} -``` - -### unwrap — extract data or throw - -```typescript -import { unwrap } from 'wellcrafted/result'; - -// Returns data if Ok, throws error if Err -const user = unwrap(await getUser(userId)); -``` - -Use sparingly — `unwrap` throws, which defeats the purpose of Result types. Useful in tests and scripts where you know the operation should succeed. - -### resolve — handle values that may or may not be Results - -```typescript -import { resolve } from 'wellcrafted/result'; - -// If value is a Result: returns its data if Ok, throws if Err (like unwrap) -// If value is not a Result: returns the value unchanged -const data = resolve(maybeResult); -``` - -### partitionResults — split an array of Results - -```typescript -import { partitionResults } from 'wellcrafted/result'; - -const results = await Promise.all(userIds.map(getUser)); -const { oks, errs } = partitionResults(results); -// oks: Ok[] the successful Results (access .data on each) -// errs: Err[] the failed Results (access .error on each) -``` +`unwrap(result)` returns Ok data and throws the contained Err value. `resolve(value)` does the same for a value that may already be plain. Use them where a throwing consumer requires that contract, not as routine Result flow. -The arrays hold the Result objects themselves, not the unwrapped values. Read `.data` off each `Ok` and `.error` off each `Err`. +## Small utilities -## Wrapping Summary +`tapErr(logFn)` calls the function for Err and returns the same Result reference; a throwing callback escapes. -| Scenario | Approach | -| --- | --- | -| Single risky operation | Wrap just that operation | -| Sequential operations | Wrap each separately, return immediately on error | -| Atomic operations | Wrap together in one block | -| Different error types | Separate blocks with appropriate error types | +`partitionResults(results)` returns the original wrappers grouped as `{ oks, errs }`. It requires runtime support for `Object.groupBy`. -See also: `define-errors` skill for error variant definitions. `patterns` skill for service architecture. +The canonical runnable flows are `examples/quick-start.ts` and `examples/service-boundary.ts`. Use the public Result reference for the complete current surface. diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index ac3c0a5..6fd4c56 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -602,10 +602,20 @@ Verification on 2026-07-10: ### Wave 6: Rebuild integrations and agent skills -- [ ] Rewrite TanStack Query around the current two-family model and `defineKeys`. -- [ ] Put official testing usage in `reference/testing`; do not create a duplicate testing integration page. -- [ ] Rewrite Hono as the approved HTTP boundary guide with distinct shape, runtime-validation, and static-typing contracts; keep validation focused on brand validators. -- [ ] Reconcile all five distributable skills against current exports and canonical examples. +- [x] Rewrite TanStack Query around the current two-family model and `defineKeys`. +- [x] Put official testing usage in `reference/testing`; do not create a duplicate testing integration page. +- [x] Rewrite Hono as the approved HTTP boundary guide with distinct shape, runtime-validation, and static-typing contracts; keep validation focused on brand validators. +- [x] Reconcile all five distributable skills against current exports and canonical examples. + +Verification on 2026-07-10: + +- Rewrote `docs/integrations/tanstack-query.mdx` around direct options adapters versus `QueryClient`-bound factories, the reactive snapshot rule, current query and mutation handle shapes, `defineKeys`, direct cache ownership, and the explicit tested `@tanstack/query-core@5.82.0` prerequisite. The reactive example is adapted from and attributed to Epicenter's account popover at commit `4d438c0`; no performance or architecture superiority claim remains. +- Rewrote `docs/integrations/validation-libraries.mdx` as focused runtime-boundary recipes for ArkType, Zod, and Valibot. It keeps `Brand` type-only, puts validation before the single branding assertion, and makes clear that branding changes neither runtime validation nor serialization behavior by itself. +- Added `docs/integrations/hono.mdx`, adapted from and attributed to Epicenter's blob errors at commit `4d438c0`. It treats conditional JSON shape preservation, full-envelope runtime validation, and Hono `AppType`/`hc` static typing as three independent guarantees and states that none implies the others. HTTP status mapping remains separate from the Result envelope. +- Audited `docs/reference/testing.mdx` as the sole new testing owner; it already covers both helpers, opposite-branch throws, narrowing, and the `Err(null)` collision, so no duplicate testing integration content was added. The legacy Hono and testing pages remain on disk, and navigation remains unchanged for the cutover and deletion waves. +- Reconciled all five distributable skills. Result guidance uses exact null guards and current propagation/throwing boundaries; error guidance covers Err wrappers, honest fields, constructor ownership, shallow freeze, and conditional serialization; query guidance covers both families and current handle shapes; brand guidance stays type-only and boundary-focused; patterns is a compact service, propagation, serialization, validation, and UI checklist pointing to canonical examples and public owners. It reuses domain vocabularies when their complete values are JSON-compatible and introduces normalized fields or wire variants only when existing payloads are not boundary-friendly. +- `bun run format` passed without changing existing files. `bun run typecheck` and `bun test` passed with 158 tests and no failures. `bun run docs:examples` passed its build, strict example typecheck, canonical snippet comparison, and three offline examples. +- Under Node 24.17.0, `bun run docs:validate` and `bun run docs:links` passed. The focused current Wave 6 claims sweep found no retired APIs, unsupported root imports, unsafe generic error truthiness, serialization absolutes, vanity metrics, reliability claims, performance claims, or uppercase brand uses. `git diff --check` passed. ### Wave 7: Consolidate decisions From 54bdc6066bec63be12255d193d88c7829f06eb90 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:18:08 -0700 Subject: [PATCH 08/13] docs(decisions): consolidate durable design rationale Record the Result shape limit, named plain-object error contract, explicit propagation tradeoffs, and nested brand representation without carrying forward unsupported metrics or superiority claims. --- docs/decisions/brand-representation.mdx | 55 ++++++++++++++++ docs/decisions/error-contract.mdx | 49 ++++++++++++++ docs/decisions/pragmatic-tradeoffs.mdx | 40 +++++++++++ docs/decisions/result-shape.mdx | 66 +++++++++++++++++++ ...10T012026-greenfield-documentation-pass.md | 16 ++++- 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 docs/decisions/brand-representation.mdx create mode 100644 docs/decisions/error-contract.mdx create mode 100644 docs/decisions/pragmatic-tradeoffs.mdx create mode 100644 docs/decisions/result-shape.mdx diff --git a/docs/decisions/brand-representation.mdx b/docs/decisions/brand-representation.mdx new file mode 100644 index 0000000..0d74ada --- /dev/null +++ b/docs/decisions/brand-representation.mdx @@ -0,0 +1,55 @@ +--- +title: Represent brands as nested markers +description: Why wellcrafted uses a private unique symbol with nested marker keys for composable branded types. +--- + +# Represent brands as nested markers + +## Decision + +`Brand` is a type-only marker built from a private unique symbol and a nested key map. + +```typescript +declare const brand: unique symbol; + +type Brand = { + [brand]: { [K in T]: true }; +}; +``` + +No marker exists at runtime. The representation affects TypeScript assignability only. + +## Why the marker is nested + +A flat literal slot does not compose: + +```typescript +type FlatBrand = { [brand]: T }; + +type A = string & FlatBrand<"A">; +type B = A & FlatBrand<"B">; +// The shared slot requires "A" & "B", which collapses to never. +``` + +With nested keys, intersections accumulate properties instead of intersecting incompatible literal values. + +```typescript +type AbsolutePath = string & Brand<"AbsolutePath">; +type ConfigPath = AbsolutePath & Brand<"ConfigPath">; +``` + +`ConfigPath` includes the `AbsolutePath` marker through the explicit intersection, so a child is assignable to its parent while the parent is not assignable to the child. Sibling markers remain distinct. The same structure supports multiple markers, deep hierarchies, and branded string, number, or object bases. + +The hierarchy is not inferred from names. `string & Brand<"ConfigPath">` alone is not a child of `AbsolutePath`; the type must explicitly include `AbsolutePath`. + +## Why `true` + +The nested value only records marker presence. `true` is a modest, readable presence value. It is not uniquely optimal; another compatible nested sentinel could provide the same assignability behavior. + +## Runtime limits + +A type assertion can lie. `value as UserId` performs no validation, and the private marker provides no runtime identity or `instanceof` behavior. Constructors and schemas must establish any runtime invariant before returning a branded value. + +Serialization carries only the base runtime value. A branded string is still a string in JSON, and a receiver must validate and brand it again. + +For constructors and assignability, see the [Brand reference](/reference/brand). For ArkType, Zod, and Valibot boundary recipes, see [Validate branded values](/integrations/validation-libraries). diff --git a/docs/decisions/error-contract.mdx b/docs/decisions/error-contract.mdx new file mode 100644 index 0000000..db63a4e --- /dev/null +++ b/docs/decisions/error-contract.mdx @@ -0,0 +1,49 @@ +--- +title: Use named plain-object error variants +description: Why wellcrafted errors use name, message, structured fields, and plain constructor functions. +--- + +# Use named plain-object error variants + +## Decision + +Expected errors are named plain-object variants. `name` identifies the variant for code; `message` is human-readable text following JavaScript's existing `Error` vocabulary. Additional own enumerable fields carry structured context. + +```typescript +const BlobError = defineErrors({ + NotFound: ({ sha256 }: { sha256: string }) => ({ + message: "Blob not found.", + sha256, + }), +}); +``` + +The variant constructor must return `message: string`. `name` is reserved and stamped from the configuration key after the body is spread. Each generated factory returns an Err wrapper, with the tagged body under `.error`. The body is `Readonly` in its public type and shallow-frozen at runtime; nested values are not deeply immutable. + +Short variant names belong under a descriptive namespace. `BlobError.NotFound` carries both domain and failure meaning without repeating `Error` in every key. Callers branch on `error.name` and structured data, not `instanceof`. + +## Why plain objects + +Native `Error` puts `name` and `message` on a familiar interface, but those properties are normally non-enumerable and class identity depends on a prototype. JSON does not preserve that prototype, and default serialization does not preserve those non-enumerable fields. + +wellcrafted stamps enumerable own fields onto an ordinary object, so compatible data can preserve its shape without reconstructing an error class. This is conditional: `defineErrors` does not enforce JSON-compatible fields. A native cause, function, bigint value, class instance, `undefined`, or cycle can still change or fail across JSON. The qualification belongs beside every serialization claim. + +## Rust influence, adapted to TypeScript + +Rust's enum-based error patterns supplied a useful mental model: a namespace resembles an enum, keys resemble variants, and each constructor carries associated data plus display text. TypeScript then narrows a union with `switch`, with an optional `never` exhaustiveness check. + +The mapping is not one-to-one. TypeScript uses ordinary functions and objects rather than enum layout, macros, ownership, exhaustive `match`, or the `?` operator. The factory returns an Err wrapper because the chosen TypeScript Result container needs an explicit error branch. + +## Why the builder was removed + +Manual literals repeated reserved fields at every call site. Early factories reduced repetition, but overload-heavy APIs made type extraction brittle. Fluent builders then introduced modes and phantom type-only calls that obscured the underlying operation. + +The durable simplification is a plain function: + +```typescript +input => ({ message, ...fields }) +``` + +`defineErrors` groups those functions, stamps the variant key, freezes the top-level body, and wraps it in Err. Required, optional, static, and computed-message variants are ordinary function signatures rather than builder modes. + +For defining a vocabulary, see [Define an error vocabulary](/guides/defining-error-vocabularies). For the exact factory and type exports, see the [Error reference](/reference/error). For boundary limitations, see [Preserve errors across serialization boundaries](/guides/serialization-boundaries). diff --git a/docs/decisions/pragmatic-tradeoffs.mdx b/docs/decisions/pragmatic-tradeoffs.mdx new file mode 100644 index 0000000..78fe681 --- /dev/null +++ b/docs/decisions/pragmatic-tradeoffs.mdx @@ -0,0 +1,40 @@ +--- +title: Keep propagation explicit and know when to stop +description: Where wellcrafted's ordinary TypeScript model fits, and when an effect system or exception is the better contract. +--- + +# Keep propagation explicit and know when to stop + +## Decision + +wellcrafted keeps expected failures as data and propagation as visible TypeScript. Functions accept ordinary parameters, return Results, use `async` and `await`, and translate errors with early returns. + +```typescript +const userResult = await users.get(userId); +if (userResult.error !== null) return userResult; + +const postsResult = await posts.list(userResult.data.id); +if (postsResult.error !== null) return postsResult; + +return Ok({ user: userResult.data, posts: postsResult.data }); +``` + +That visibility is both the benefit and the cost. wellcrafted does not add a `?` operator, automatic Result chains, a dependency environment, fibers, structured concurrency, cancellation, resource scopes, retry scheduling, or automatic error conversion. Applications own propagation and translation at each boundary. + +## Choose the contract by the job + +| Job | Prefer | +| --- | --- | +| Expected failures as plain data, ordinary function parameters, `async` workflows, a few explicit early returns, or adapters at HTTP/query/UI boundaries | wellcrafted | +| Typed dependency graphs, coordinated cancellation or concurrency, scoped resources, scheduled retries, or automatic propagation across many layers | Effect or another effect system | +| Programmer bugs, violated invariants, or integration with an external API that explicitly expects throwing control flow | Raw exceptions or a deliberate throwing adapter | + +Results are not a rule that every thrown value must disappear. `trySync` and `tryAsync` convert narrow external operations into expected domain failures. In the other direction, `unwrap`, `resolve`, testing assertions, and query adapters deliberately throw because their consumers use throwing contracts. + +## Stopping rule + +Stop adding manual Result plumbing when the application repeatedly needs capabilities outside this model. Warning signs include the same error union threaded through many unrelated layers, hand-built dependency environments, coordinated cancellation across concurrent tasks, resource lifetime protocols, or repeated retry and scheduling machinery. + +At that point, adopting Effect is an escape hatch, not a failure and not a claim that either tool is universally superior. Choose the model that owns the complexity already present in the application. + +For the small-model workflow, see [Put Results at service boundaries](/guides/service-boundaries) and [Compose Result-returning functions](/guides/composing-results). For exact throwing edges, see the [Result reference](/reference/result). diff --git a/docs/decisions/result-shape.mdx b/docs/decisions/result-shape.mdx new file mode 100644 index 0000000..2026ec9 --- /dev/null +++ b/docs/decisions/result-shape.mdx @@ -0,0 +1,66 @@ +--- +title: Keep the data and error Result shape +description: Why wellcrafted keeps its familiar Result shape, including its null collision and error conventions. +--- + +# Keep the data and error Result shape + +## Decision + +wellcrafted keeps these two Result shapes: + +```typescript +type Ok = { data: T; error: null }; +type Err = { data: null; error: E }; +``` + +The error field is the discriminator. `error === null` means Ok; `error !== null` means Err. This supports direct property access and familiar destructuring while keeping propagation in ordinary TypeScript control flow. + +```typescript +const result = await loadUser(userId); +if (result.error !== null) return result; + +return Ok(result.data.name); +``` + +Tagged error unions narrow further through `switch`. Use a `never` check when the caller needs exhaustiveness. + +```typescript +function handle(error: UserError) { + switch (error.name) { + case "NotFound": + return showMissing(error.userId); + case "ReadFailed": + return showReadFailure(error.message); + default: + error satisfies never; + } +} +``` + +## The structural limit + +`Ok(null)` is a valid success, commonly representing an absent value that is not itself an error. That choice creates one hard limit: + +```typescript +Ok(null); // { data: null, error: null } +Err(null); // { data: null, error: null } +``` + +The two values are structurally identical. No runtime guard can recover whether the author intended success or failure. `data` cannot become the discriminator without breaking `Ok(null)`. + +The convention is therefore to keep error values non-null and meaningful. Use `error !== null` or `isErr`; those checks still recognize other falsy values permitted by `E`, including `0`, `false`, `""`, `undefined`, and `NaN`. A shorter truthiness convention—`if (error)`—is safe only when an application separately guarantees truthy object errors. The API enforces neither convention. + +`isResult` is intentionally shallow. It checks for a non-null object with `data` and `error` keys, not the payloads. If both fields are populated, the non-null error field governs and the guards classify the value as Err. + +## Rejected alternatives + +Adding an explicit status or variant tag would distinguish `Ok(null)` from `Err(null)`, but it would change the public shape rather than document it. That requires a separate API decision. + +Discriminating on `data` was rejected because successful null data is useful and already supported. + +Constraining `E` with `NonNullable` was tried and reverted. It blocks direct null literals, but it cannot prevent unchecked values or dishonest casts from producing null at runtime. It also makes `catch (error: unknown)` boundaries awkward and encourages casts that silence the constraint without enforcing the invariant. + +This decision does not make a never-throws promise. `unwrap`, `resolve`, testing assertions, and query adapters deliberately enter throwing contracts. + +For usage, see [Compose Result-returning functions](/guides/composing-results). For exact exports and guard behavior, see the [Result reference](/reference/result). diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 6fd4c56..2a98fd6 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -619,9 +619,19 @@ Verification on 2026-07-10: ### Wave 7: Consolidate decisions -- [ ] Preserve only durable rationale and honest tradeoffs. -- [ ] Create consolidated Result, error-contract, tradeoff, and brand decisions while leaving their old source pages on disk until deletion. -- [ ] Remove unsupported production or marketing claims. +- [x] Preserve only durable rationale and honest tradeoffs. +- [x] Create consolidated Result, error-contract, tradeoff, and brand decisions while leaving their old source pages on disk until deletion. +- [x] Remove unsupported production or marketing claims. + +Verification on 2026-07-10: + +- Added exactly four rationale owners under `docs/decisions/`: Result shape, error contract, pragmatic tradeoffs, and brand representation. They link to task guides and exact references instead of duplicating API tutorials. Navigation and all legacy decision sources remain unchanged for the later cutover and deletion waves. +- The Result decision records the exact two shapes, error-side discriminator, direct access and destructuring, `switch` exhaustiveness, `Ok(null)`/`Err(null)` collision, non-null and truthy-error conventions, shallow `isResult`, and error-governed malformed envelopes. It records why an explicit tag, data discrimination, and the reverted `NonNullable` constraint are not current contracts and makes no never-throws claim. +- The error decision preserves `name` as variant identity, `message` as human text using JavaScript's existing vocabulary, enumerable own structured fields, conditional JSON behavior, exact factory/freeze behavior, the limited Rust enum analogy, and the durable simplification from manual literals through brittle overloads and fluent modes to plain constructor functions. It contains no historical counts, dates, or usage metrics. +- The tradeoff decision states the cost of explicit propagation and translation and gives a factual choice table for wellcrafted, Effect, and raw exceptions. Effect is an escape hatch when dependency graphs, cancellation/concurrency, resource scopes, scheduling, retries, or automatic propagation are the actual problem; no persona, anecdote, metric, or superiority claim remains. +- The brand decision records the private unique-symbol nested marker, why flat literal slots collapse under intersection, explicit hierarchy construction, verified assignability across sibling, child, deep, multiple, number, and object cases, the modest role of `true`, and the lack of runtime validation, identity, or serialized marker data. +- `bun run format` passed without changing existing files. `bun run typecheck` and `bun test` passed with 158 tests and no failures. `bun run docs:examples` passed its build, strict example typecheck, canonical snippet comparison, and three offline examples. +- Under Node 24.17.0, `bun run docs:validate` and `bun run docs:links` passed. The focused public decision claim sweep found no importer counts, production or reliability claims, performance claims, unsupported serialization promises, historical call-site counts, or uppercase brand uses. `git diff --check` passed. ### Wave 8: Prove and enable strict content gates From ef883acefac18fa845b3e064381ca3f4200edf17 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:52:04 -0700 Subject: [PATCH 09/13] test(docs): enforce content ownership gates Derive exact export coverage from declarations, reject unsupported public claims with explicit legacy exclusions, and require canonical snippet sources, targets, and owner imports. Run all passing gates in CI. --- .github/workflows/main.yml | 3 + README.md | 4 +- docs/guides/serialization-boundaries.mdx | 8 + docs/guides/service-boundaries.mdx | 8 + docs/integrations/tanstack-query.mdx | 8 + docs/reference/brand.mdx | 2 + docs/reference/error.mdx | 10 + docs/reference/function.mdx | 2 + docs/reference/json.mdx | 6 + docs/reference/logger.mdx | 11 + docs/reference/query.mdx | 5 + docs/reference/result.mdx | 17 + docs/reference/standard-schema.mdx | 32 + docs/reference/testing.mdx | 3 + docs/snippets/quick-start.mdx | 2 + docs/snippets/serialization-boundary.mdx | 37 + docs/snippets/service-boundary.mdx | 49 ++ docs/snippets/tanstack-query.mdx | 57 ++ examples/quick-start.ts | 4 +- examples/serialization-boundary.ts | 8 +- examples/service-boundary.ts | 5 +- examples/tanstack-query.ts | 2 + package.json | 5 +- scripts/check-doc-claims.test.ts | 195 +++++ scripts/check-doc-claims.ts | 738 ++++++++++++++++++ scripts/check-doc-exports.ts | 237 ++++++ scripts/check-doc-snippets.test.ts | 210 +++++ scripts/check-doc-snippets.ts | 348 +++++++++ scripts/check-quick-start-docs.ts | 70 -- ...10T012026-greenfield-documentation-pass.md | 22 +- 30 files changed, 2025 insertions(+), 83 deletions(-) create mode 100644 docs/snippets/serialization-boundary.mdx create mode 100644 docs/snippets/service-boundary.mdx create mode 100644 docs/snippets/tanstack-query.mdx create mode 100644 scripts/check-doc-claims.test.ts create mode 100644 scripts/check-doc-claims.ts create mode 100644 scripts/check-doc-exports.ts create mode 100644 scripts/check-doc-snippets.test.ts create mode 100644 scripts/check-doc-snippets.ts delete mode 100644 scripts/check-quick-start-docs.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cdfda91..e2f2ee7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,6 +48,9 @@ jobs: - run: bun run build - run: bun test - run: bun run docs:examples + - run: bun run docs:exports + - run: bun run docs:claims + - run: bun run docs:snippets - run: bun run package:smoke - run: bun run compat:types - run: bun run compat:runtime diff --git a/README.md b/README.md index af37e45..8a7fb65 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ wellcrafted has no root export. Import from a published subpath such as `wellcra Define the failures a function can return, then use the Result shape to handle both outcomes. - + ```typescript quick-start.ts import { defineErrors, type InferErrors } from "wellcrafted/error"; import { Ok, type Result } from "wellcrafted/result"; @@ -55,7 +55,7 @@ if (failure.error !== null) { console.error(failure.error.message); } ``` - + The example is extracted from the checked [`examples/quick-start.ts`](examples/quick-start.ts) source. The documentation checks compare both copies with that file so they cannot drift unnoticed. diff --git a/docs/guides/serialization-boundaries.mdx b/docs/guides/serialization-boundaries.mdx index d59f80a..a939ecb 100644 --- a/docs/guides/serialization-boundaries.mdx +++ b/docs/guides/serialization-boundaries.mdx @@ -3,12 +3,20 @@ title: Preserve errors across serialization boundaries description: Move Result-shaped error data through JSON when every field is compatible, without confusing shape preservation with validation or typing. --- +import SerializationBoundaryExample from "/snippets/serialization-boundary.mdx"; + # Preserve errors across serialization boundaries wellcrafted errors are plain data, so a Result can cross JSON, HTTP, workers, IPC, logs, and UI without an error-class transformation layer when every field in the complete value is JSON-compatible. That promise is conditional. `defineErrors` does not type-enforce JSON-compatible fields, and arbitrary fields do not serialize perfectly. You choose the fields, so you also own their boundary behavior. +## Checked local example + +This checked example puts a JSON-compatible error beside the native `Error` counterexample. The snippet is kept in sync with `examples/serialization-boundary.ts`. + + + ## Use boundary-friendly fields ```typescript diff --git a/docs/guides/service-boundaries.mdx b/docs/guides/service-boundaries.mdx index ee5826d..fcef9d7 100644 --- a/docs/guides/service-boundaries.mdx +++ b/docs/guides/service-boundaries.mdx @@ -3,6 +3,8 @@ title: Put Results at service boundaries description: Convert narrow throwing operations into domain errors and propagate them through linear service code. --- +import ServiceBoundaryExample from "/snippets/service-boundary.mdx"; + # Put Results at service boundaries A service boundary is where an uncontrolled throwing API becomes a controlled error vocabulary. Wrap only the operation that throws, give that failure a domain name, and let the rest of the service use `Result` returns and early exits. @@ -40,6 +42,12 @@ function getDisplayName( The `trySync` block owns one failure unit: reading the record. `getDisplayName` does not catch anything. It manually propagates `ReadFailed`, classifies a successful empty lookup as `NotFound`, and leaves the happy path at the bottom. +## Checked local example + +The local canonical example includes the imports, error vocabulary, and service factory omitted from the adapted excerpt above. The snippet is checked against `examples/service-boundary.ts`. + + + ## Keep dependencies explicit Pass databases, clients, filesystems, clocks, and configuration into a service factory or method. A service should not reach into UI state or emit notifications. Explicit inputs make the boundary testable and keep its `Result` signature useful outside one screen or framework. diff --git a/docs/integrations/tanstack-query.mdx b/docs/integrations/tanstack-query.mdx index 1a2e1d9..b9ba309 100644 --- a/docs/integrations/tanstack-query.mdx +++ b/docs/integrations/tanstack-query.mdx @@ -4,6 +4,8 @@ description: Adapt Result-returning functions to TanStack Query hooks and QueryC icon: database --- +import TanStackQueryExample from "/snippets/tanstack-query.mdx"; + # TanStack Query `wellcrafted/query` converts a Result-returning query or mutation function into TanStack Query's contract: Ok resolves into the data channel, while Err throws its contained value into the error channel. @@ -21,6 +23,12 @@ There are two adapter families. Choose by who owns execution. | A framework hook owns execution | `resultQueryOptions` or `resultMutationOptions` | | One `QueryClient` owns reusable reactive and imperative handles | `createQueryFactories` | +## Checked local example + +The canonical example exercises both adapter families and `defineKeys` against the current package declarations. It is kept in sync with `examples/tanstack-query.ts`. + + + ## Direct options adapters Use direct adapters inside React, Svelte, Vue, or another TanStack framework binding when the hook already owns the observer. diff --git a/docs/reference/brand.mdx b/docs/reference/brand.mdx index b034699..eb041ca 100644 --- a/docs/reference/brand.mdx +++ b/docs/reference/brand.mdx @@ -5,6 +5,8 @@ description: Current type export from wellcrafted/brand. # `wellcrafted/brand` +{/* docs:export subpath="wellcrafted/brand" kind="type" symbol="Brand" */} + This subpath exports one type-only nominal marker. It has no runtime exports. ```typescript diff --git a/docs/reference/error.mdx b/docs/reference/error.mdx index 3932a7b..f4fd7f7 100644 --- a/docs/reference/error.mdx +++ b/docs/reference/error.mdx @@ -5,6 +5,16 @@ description: Current runtime and type exports from wellcrafted/error. # `wellcrafted/error` +{/* docs:export subpath="wellcrafted/error" kind="value" symbol="defineErrors" */} +{/* docs:export subpath="wellcrafted/error" kind="value" symbol="extractErrorMessage" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="AnyTaggedError" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="ErrorBody" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="ErrorsConfig" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="ValidatedConfig" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="DefineErrorsReturn" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="InferError" */} +{/* docs:export subpath="wellcrafted/error" kind="type" symbol="InferErrors" */} + This subpath defines named error variants and a best-effort message extractor. ```typescript diff --git a/docs/reference/function.mdx b/docs/reference/function.mdx index f96de5f..b60679c 100644 --- a/docs/reference/function.mdx +++ b/docs/reference/function.mdx @@ -5,6 +5,8 @@ description: Current runtime export from wellcrafted/function. # `wellcrafted/function` +{/* docs:export subpath="wellcrafted/function" kind="value" symbol="once" */} + This subpath exports one run-at-most-once wrapper and no named types. ```typescript diff --git a/docs/reference/json.mdx b/docs/reference/json.mdx index 8752e6f..f6a22be 100644 --- a/docs/reference/json.mdx +++ b/docs/reference/json.mdx @@ -5,6 +5,12 @@ description: Current runtime and type exports from wellcrafted/json. # `wellcrafted/json` +{/* docs:export subpath="wellcrafted/json" kind="value" symbol="JsonParseError" */} +{/* docs:export subpath="wellcrafted/json" kind="value" symbol="parseJson" */} +{/* docs:export subpath="wellcrafted/json" kind="type" symbol="JsonValue" */} +{/* docs:export subpath="wellcrafted/json" kind="type" symbol="JsonObject" */} +{/* docs:export subpath="wellcrafted/json" kind="type" symbol="JsonParseError" */} + This subpath parses JSON into a recursive value type without a generic type assertion. ```typescript diff --git a/docs/reference/logger.mdx b/docs/reference/logger.mdx index 5d68322..a50e79a 100644 --- a/docs/reference/logger.mdx +++ b/docs/reference/logger.mdx @@ -5,6 +5,17 @@ description: Current runtime and type exports from wellcrafted/logger. # `wellcrafted/logger` +{/* docs:export subpath="wellcrafted/logger" kind="value" symbol="consoleSink" */} +{/* docs:export subpath="wellcrafted/logger" kind="value" symbol="createLogger" */} +{/* docs:export subpath="wellcrafted/logger" kind="value" symbol="memorySink" */} +{/* docs:export subpath="wellcrafted/logger" kind="value" symbol="composeSinks" */} +{/* docs:export subpath="wellcrafted/logger" kind="value" symbol="tapErr" */} +{/* docs:export subpath="wellcrafted/logger" kind="type" symbol="LogEvent" */} +{/* docs:export subpath="wellcrafted/logger" kind="type" symbol="LogLevel" */} +{/* docs:export subpath="wellcrafted/logger" kind="type" symbol="LogSink" */} +{/* docs:export subpath="wellcrafted/logger" kind="type" symbol="Logger" */} +{/* docs:export subpath="wellcrafted/logger" kind="type" symbol="LoggableError" */} + This subpath provides dependency-injected structured logging around tagged errors. ```typescript diff --git a/docs/reference/query.mdx b/docs/reference/query.mdx index a332b29..fc84743 100644 --- a/docs/reference/query.mdx +++ b/docs/reference/query.mdx @@ -5,6 +5,11 @@ description: Current runtime exports from wellcrafted/query. # `wellcrafted/query` +{/* docs:export subpath="wellcrafted/query" kind="value" symbol="createQueryFactories" */} +{/* docs:export subpath="wellcrafted/query" kind="value" symbol="defineKeys" */} +{/* docs:export subpath="wellcrafted/query" kind="value" symbol="resultMutationOptions" */} +{/* docs:export subpath="wellcrafted/query" kind="value" symbol="resultQueryOptions" */} + This subpath adapts Result-returning functions to TanStack Query's throwing data/error channels. It has an explicit tested type prerequisite: install `@tanstack/query-core@5.82.0` when importing `wellcrafted/query`. The current package does not declare that prerequisite in dependency metadata, so consumers must install it themselves. diff --git a/docs/reference/result.mdx b/docs/reference/result.mdx index 3d412a9..647010e 100644 --- a/docs/reference/result.mdx +++ b/docs/reference/result.mdx @@ -5,6 +5,23 @@ description: Current runtime and type exports from wellcrafted/result. # `wellcrafted/result` +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="Ok" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="Err" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="isResult" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="isOk" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="isErr" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="trySync" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="tryAsync" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="unwrap" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="resolve" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="tapErr" */} +{/* docs:export subpath="wellcrafted/result" kind="value" symbol="partitionResults" */} +{/* docs:export subpath="wellcrafted/result" kind="type" symbol="Ok" */} +{/* docs:export subpath="wellcrafted/result" kind="type" symbol="Err" */} +{/* docs:export subpath="wellcrafted/result" kind="type" symbol="Result" */} +{/* docs:export subpath="wellcrafted/result" kind="type" symbol="UnwrapOk" */} +{/* docs:export subpath="wellcrafted/result" kind="type" symbol="UnwrapErr" */} + Import Result constructors, guards, throwing adapters, and small flow helpers from this subpath. ```typescript diff --git a/docs/reference/standard-schema.mdx b/docs/reference/standard-schema.mdx index 7766fbd..aea0863 100644 --- a/docs/reference/standard-schema.mdx +++ b/docs/reference/standard-schema.mdx @@ -5,6 +5,38 @@ description: Current runtime, type, and namespace exports from wellcrafted/stand # `wellcrafted/standard-schema` +{/* docs:export subpath="wellcrafted/standard-schema" kind="value" symbol="ErrSchema" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="value" symbol="FAILURES" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="value" symbol="OkSchema" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="value" symbol="ResultSchema" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="value" symbol="hasJsonSchema" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="value" symbol="hasValidate" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="type" symbol="Err" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="type" symbol="Ok" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="type" symbol="Result" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="type" symbol="StandardJSONSchemaV1" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="type" symbol="StandardSchemaV1" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="type" symbol="StandardTypedV1" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardTypedV1.Props" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardTypedV1.Types" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardTypedV1.InferInput" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardTypedV1.InferOutput" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.Props" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.Result" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.SuccessResult" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.FailureResult" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.Options" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.Issue" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.PathSegment" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.InferInput" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardSchemaV1.InferOutput" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardJSONSchemaV1.Props" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardJSONSchemaV1.Converter" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardJSONSchemaV1.Target" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardJSONSchemaV1.Options" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardJSONSchemaV1.InferInput" */} +{/* docs:export subpath="wellcrafted/standard-schema" kind="namespace" symbol="StandardJSONSchemaV1.InferOutput" */} + This subpath wraps Standard Schema-compatible validators and JSON Schema converters around the `{ data, error }` Result shape. ## Exports diff --git a/docs/reference/testing.mdx b/docs/reference/testing.mdx index 5466d1a..9b3e481 100644 --- a/docs/reference/testing.mdx +++ b/docs/reference/testing.mdx @@ -5,6 +5,9 @@ description: Current runtime exports from wellcrafted/testing. # `wellcrafted/testing` +{/* docs:export subpath="wellcrafted/testing" kind="value" symbol="expectErr" */} +{/* docs:export subpath="wellcrafted/testing" kind="value" symbol="expectOk" */} + This test-only subpath provides framework-agnostic Result assertions. It has no named type exports. ```typescript diff --git a/docs/snippets/quick-start.mdx b/docs/snippets/quick-start.mdx index bb41aa0..1bfd18b 100644 --- a/docs/snippets/quick-start.mdx +++ b/docs/snippets/quick-start.mdx @@ -1,3 +1,4 @@ +{/* docs:snippet quick-start:start */} ```typescript quick-start.ts import { defineErrors, type InferErrors } from "wellcrafted/error"; import { Ok, type Result } from "wellcrafted/result"; @@ -32,3 +33,4 @@ if (failure.error !== null) { console.error(failure.error.message); } ``` +{/* docs:snippet quick-start:end */} diff --git a/docs/snippets/serialization-boundary.mdx b/docs/snippets/serialization-boundary.mdx new file mode 100644 index 0000000..4e19567 --- /dev/null +++ b/docs/snippets/serialization-boundary.mdx @@ -0,0 +1,37 @@ +{/* docs:snippet serialization-boundary:start */} +```typescript serialization-boundary.ts +import { defineErrors } from "wellcrafted/error"; + +const UploadError = defineErrors({ + Rejected: ({ + fileName, + reasons, + }: { + fileName: string; + reasons: string[]; + }) => ({ + message: `Upload rejected for "${fileName}".`, + fileName, + reasons, + }), + Unexpected: ({ cause }: { cause: unknown }) => ({ + message: "Upload failed unexpectedly.", + cause, + }), +}); + +const boundaryFriendly = UploadError.Rejected({ + fileName: "report.csv", + reasons: ["too large", "unsupported encoding"], +}); + +// defineErrors does not enforce JSON-safe fields. A native Error is accepted, +// but its non-enumerable details do not survive JSON serialization. +const withNativeCause = UploadError.Unexpected({ + cause: new Error("disk full"), +}); +const roundTripped = JSON.parse(JSON.stringify(withNativeCause)) as { + error: { cause: unknown }; +}; +``` +{/* docs:snippet serialization-boundary:end */} diff --git a/docs/snippets/service-boundary.mdx b/docs/snippets/service-boundary.mdx new file mode 100644 index 0000000..8e421ed --- /dev/null +++ b/docs/snippets/service-boundary.mdx @@ -0,0 +1,49 @@ +{/* docs:snippet service-boundary:start */} +```typescript service-boundary.ts +import { + defineErrors, + extractErrorMessage, + type InferErrors, +} from "wellcrafted/error"; +import { Ok, type Result, trySync } from "wellcrafted/result"; + +const UserError = defineErrors({ + ReadFailed: ({ cause }: { cause: string }) => ({ + message: `Could not read the user record: ${cause}`, + cause, + }), + NotFound: ({ userId }: { userId: string }) => ({ + message: `No user exists with id "${userId}".`, + userId, + }), +}); + +type UserError = InferErrors; +type User = { id: string; displayName: string }; + +function createUserService({ records }: { records: Map }) { + function findUser(userId: string): Result { + return trySync({ + try: () => { + if (userId === "storage-offline") { + throw new Error("storage is offline"); + } + return records.get(userId) ?? null; + }, + catch: (cause) => + UserError.ReadFailed({ cause: extractErrorMessage(cause) }), + }); + } + + return { + getDisplayName(userId: string): Result { + const userResult = findUser(userId); + if (userResult.error !== null) return userResult; + if (userResult.data === null) return UserError.NotFound({ userId }); + + return Ok(userResult.data.displayName); + }, + }; +} +``` +{/* docs:snippet service-boundary:end */} diff --git a/docs/snippets/tanstack-query.mdx b/docs/snippets/tanstack-query.mdx new file mode 100644 index 0000000..ed51868 --- /dev/null +++ b/docs/snippets/tanstack-query.mdx @@ -0,0 +1,57 @@ +{/* docs:snippet tanstack-query:start */} +```typescript tanstack-query.ts +import { QueryClient } from "@tanstack/query-core"; +import { defineErrors } from "wellcrafted/error"; +import { + createQueryFactories, + defineKeys, + resultMutationOptions, + resultQueryOptions, +} from "wellcrafted/query"; +import { Ok } from "wellcrafted/result"; + +const TodoError = defineErrors({ + NotFound: ({ todoId }: { todoId: string }) => ({ + message: `No todo exists with id "${todoId}".`, + todoId, + }), +}); + +const todoKeys = defineKeys({ + all: ["todos"], + detail: (todoId: string) => ["todos", todoId] as const, +}); + +const directQuery = resultQueryOptions({ + queryKey: todoKeys.detail("todo-1"), + queryFn: () => Ok({ id: "todo-1", title: "Write docs" }), +}); + +const directMutation = resultMutationOptions({ + mutationKey: ["todos", "rename"], + mutationFn: ({ todoId, title }: { todoId: string; title: string }) => + Ok({ id: todoId, title }), +}); + +const { defineMutation, defineQuery } = createQueryFactories(new QueryClient()); +const todoQuery = defineQuery({ + queryKey: todoKeys.detail("todo-1"), + queryFn: () => Ok({ id: "todo-1", title: "Write docs" }), +}); +const renameTodo = defineMutation({ + mutationKey: ["todos", "rename"], + mutationFn: ({ todoId, title }: { todoId: string; title: string }) => { + if (todoId === "missing") return TodoError.NotFound({ todoId }); + return Ok({ id: todoId, title }); + }, +}); + +directQuery.queryKey; +directMutation.mutationKey; +todoQuery.options; +todoQuery.fetch; +todoQuery.ensure; +renameTodo.options; +renameTodo({ todoId: "todo-1", title: "Ship docs" }); +``` +{/* docs:snippet tanstack-query:end */} diff --git a/examples/quick-start.ts b/examples/quick-start.ts index 781a4e6..c99e879 100644 --- a/examples/quick-start.ts +++ b/examples/quick-start.ts @@ -1,4 +1,4 @@ -// docs:quick-start:start +// docs:snippet quick-start:start import { defineErrors, type InferErrors } from "wellcrafted/error"; import { Ok, type Result } from "wellcrafted/result"; @@ -33,7 +33,7 @@ if (failure.error !== null) { console.error(failure.error.message); } -// docs:quick-start:end +// docs:snippet quick-start:end if (success.data !== 3000 || success.error !== null) { throw new Error("Expected the valid port to succeed."); diff --git a/examples/serialization-boundary.ts b/examples/serialization-boundary.ts index 3a5b4a1..52d180f 100644 --- a/examples/serialization-boundary.ts +++ b/examples/serialization-boundary.ts @@ -1,6 +1,8 @@ -import { defineErrors } from "wellcrafted/error"; import { assertEqual } from "./assert.js"; +// docs:snippet serialization-boundary:start +import { defineErrors } from "wellcrafted/error"; + const UploadError = defineErrors({ Rejected: ({ fileName, @@ -23,7 +25,6 @@ const boundaryFriendly = UploadError.Rejected({ fileName: "report.csv", reasons: ["too large", "unsupported encoding"], }); -assertEqual(JSON.parse(JSON.stringify(boundaryFriendly)), boundaryFriendly); // defineErrors does not enforce JSON-safe fields. A native Error is accepted, // but its non-enumerable details do not survive JSON serialization. @@ -33,6 +34,9 @@ const withNativeCause = UploadError.Unexpected({ const roundTripped = JSON.parse(JSON.stringify(withNativeCause)) as { error: { cause: unknown }; }; +// docs:snippet serialization-boundary:end + +assertEqual(JSON.parse(JSON.stringify(boundaryFriendly)), boundaryFriendly); assertEqual(roundTripped.error.cause, {}); console.log( diff --git a/examples/service-boundary.ts b/examples/service-boundary.ts index 0205b3d..cd3ebd2 100644 --- a/examples/service-boundary.ts +++ b/examples/service-boundary.ts @@ -2,13 +2,15 @@ * Adapted from Epicenter's service-boundary pattern at commit 4d438c0: * https://github.com/EpicenterHQ/epicenter/blob/4d438c0/packages/client/src/transcribe.ts */ +import { assertEqual } from "./assert.js"; + +// docs:snippet service-boundary:start import { defineErrors, extractErrorMessage, type InferErrors, } from "wellcrafted/error"; import { Ok, type Result, trySync } from "wellcrafted/result"; -import { assertEqual } from "./assert.js"; const UserError = defineErrors({ ReadFailed: ({ cause }: { cause: string }) => ({ @@ -48,6 +50,7 @@ function createUserService({ records }: { records: Map }) { }, }; } +// docs:snippet service-boundary:end const users = createUserService({ records: new Map([["user-1", { id: "user-1", displayName: "Ada" }]]), diff --git a/examples/tanstack-query.ts b/examples/tanstack-query.ts index 3cb5c6e..e53416d 100644 --- a/examples/tanstack-query.ts +++ b/examples/tanstack-query.ts @@ -1,3 +1,4 @@ +// docs:snippet tanstack-query:start import { QueryClient } from "@tanstack/query-core"; import { defineErrors } from "wellcrafted/error"; import { @@ -51,3 +52,4 @@ todoQuery.fetch; todoQuery.ensure; renameTodo.options; renameTodo({ todoId: "todo-1", title: "Ship docs" }); +// docs:snippet tanstack-query:end diff --git a/package.json b/package.json index 70c926a..91b631c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,10 @@ "docs:dev": "cd docs && mint dev", "docs:validate": "cd docs && mint validate", "docs:links": "cd docs && mint broken-links", - "docs:examples": "bun run build && bun node_modules/typescript/bin/tsc --project examples/tsconfig.json && bun scripts/check-quick-start-docs.ts && bun examples/quick-start.ts && bun examples/service-boundary.ts && bun examples/serialization-boundary.ts", + "docs:exports": "bun run build && bun scripts/check-doc-exports.ts", + "docs:claims": "bun scripts/check-doc-claims.ts", + "docs:snippets": "bun scripts/check-doc-snippets.ts", + "docs:examples": "bun run build && bun node_modules/typescript/bin/tsc --project examples/tsconfig.json && bun run docs:snippets && bun examples/quick-start.ts && bun examples/service-boundary.ts && bun examples/serialization-boundary.ts", "package:smoke": "bun run build && bun scripts/package-smoke.ts", "compat:types": "bun run build && bun node_modules/typescript/bin/tsc --project scripts/fixtures/compatibility/tsconfig.bundler.json && bun node_modules/typescript/bin/tsc --project scripts/fixtures/compatibility/tsconfig.nodenext.json", "compat:runtime": "bun run build && bun scripts/fixtures/runtime/all-subpaths.mjs" diff --git a/scripts/check-doc-claims.test.ts b/scripts/check-doc-claims.test.ts new file mode 100644 index 0000000..6288c63 --- /dev/null +++ b/scripts/check-doc-claims.test.ts @@ -0,0 +1,195 @@ +/** + * Documentation Claims Gate Tests + * + * Verifies that public-claim scanning catches prohibited API guidance and + * unsupported claims while preserving explicit limitations and valid examples. + * + * Key behaviors: + * - Retired imports and unsafe Result truthiness checks are detected across formatting + * - Unsupported serialization, reliability, and vanity claims are rejected + * - Negated limitations and current API guidance remain valid + */ + +import { describe, expect, test } from "bun:test"; +import { findClaimFindings } from "./check-doc-claims"; + +type ClaimCase = { + content: string; + name: string; + rule: string; +}; + +const REJECTED_CLAIMS: readonly ClaimCase[] = [ + { + name: "multiline aliased queryOptions import and use", + rule: "retired-api", + content: ` +import { + queryOptions as createQuery, +} from "wellcrafted/query"; + +const options = createQuery({ queryKey: ["users"] }); +`, + }, + { + name: "multiline aliased mutationOptions import and use", + rule: "retired-api", + content: ` +import { + mutationOptions as createMutation, +} from "wellcrafted/query"; + +const options = createMutation({ mutationFn: saveUser }); +`, + }, + { + name: "multiline queryOptions call", + rule: "retired-api", + content: ` +const options = queryOptions +({ queryKey: ["users"] }); +`, + }, + { + name: "multiline mutationOptions call", + rule: "retired-api", + content: ` +const options = mutationOptions +({ mutationFn: saveUser }); +`, + }, + { + name: "direct Result error truthiness", + rule: "unsafe-result-check", + content: "if (result.error) return result;", + }, + { + name: "negated direct Result error truthiness", + rule: "unsafe-result-check", + content: "if (!result.error) return result.data;", + }, + { + name: "Boolean Result error coercion", + rule: "unsafe-result-check", + content: "const failed = Boolean(result.error);", + }, + { + name: "double-negated Result error coercion", + rule: "unsafe-result-check", + content: "const failed = !!result.error;", + }, + { + name: "Result error ternary discrimination", + rule: "unsafe-result-check", + content: "return result.error ? result : Ok(result.data);", + }, + { + name: "consumer vanity metric", + rule: "unsupported-metric", + content: "Trusted by 1,234 consumers.", + }, + { + name: "uptime reliability claim", + rule: "reliability-claim", + content: "Delivers 99.99% uptime.", + }, + { + name: "JSON field enforcement claim", + rule: "serialization-type-enforcement", + content: "defineErrors guarantees JSON-compatible fields.", + }, + { + name: "absolute JSON survival claim", + rule: "serialization-absolute", + content: "Errors always survive JSON serialization.", + }, +]; + +const ALLOWED_CLAIMS: readonly Omit[] = [ + { + name: "explicit JSON field limitation", + content: "defineErrors does not guarantee JSON-compatible fields.", + }, + { + name: "explicit serialization limitation", + content: "Errors do not always survive JSON serialization.", + }, + { + name: "qualified unsafe Result example", + content: + "Do not use `Boolean(result.error)` as a general Result check because falsy errors are permitted.", + }, + { + name: "exact Result discrimination", + content: "if (result.error !== null) return result;", + }, + { + name: "Result predicate discrimination", + content: "if (isErr(result)) return result;", + }, + { + name: "optional error access", + content: "return result.error?.message;", + }, + { + name: "current query adapter", + content: ` +import { resultQueryOptions } from "wellcrafted/query"; + +const options = resultQueryOptions({ queryKey: ["users"] }); +`, + }, +]; + +describe("rejected public claims", () => { + for (const claimCase of REJECTED_CLAIMS) { + test(`${claimCase.name} reports ${claimCase.rule}`, () => { + const findings = findClaimFindings("docs/test.mdx", claimCase.content); + + expect(findings.some((finding) => finding.rule === claimCase.rule)).toBe( + true, + ); + }); + } +}); + +describe("allowed public claims", () => { + for (const claimCase of ALLOWED_CLAIMS) { + test(`${claimCase.name} reports no findings`, () => { + expect(findClaimFindings("docs/test.mdx", claimCase.content)).toEqual([]); + }); + } +}); + +describe("claim allowances", () => { + test("decision pages can allow an attributed retired API reference", () => { + const content = ` + +The former API was \`queryOptions\`. + +`; + + expect( + findClaimFindings("docs/decisions/query-history.mdx", content), + ).toEqual([]); + }); + + test("decision pages cannot allow unsafe Result checks", () => { + const content = ` + +if (result.error) return result; + +`; + const findings = findClaimFindings( + "docs/decisions/result-history.mdx", + content, + ); + + expect(findings.some((finding) => finding.rule === "gate-config")).toBe( + true, + ); + expect( + findings.some((finding) => finding.rule === "unsafe-result-check"), + ).toBe(true); + }); +}); diff --git a/scripts/check-doc-claims.ts b/scripts/check-doc-claims.ts new file mode 100644 index 0000000..4c002e2 --- /dev/null +++ b/scripts/check-doc-claims.ts @@ -0,0 +1,738 @@ +import { readdir, stat } from "node:fs/promises"; +import { extname, posix, resolve, sep } from "node:path"; + +type ClaimRuleName = + | "conflicting-setup" + | "npm-contributor-command" + | "reliability-claim" + | "retired-api" + | "serialization-absolute" + | "serialization-type-enforcement" + | "unsafe-result-check" + | "unsupported-metric" + | "unsupported-root-import" + | "uppercase-brand"; + +type ClaimRule = { + message: string; + name: ClaimRuleName; + patterns: readonly RegExp[]; + shouldReport?: (context: { + line: string; + match: RegExpExecArray; + path: string; + }) => boolean; +}; + +type Allowance = { + endLine: number; + reason: string; + rule: ClaimRuleName; + startLine: number; + uses: number; +}; + +export type Finding = { + line: number; + message: string; + path: string; + rule: ClaimRuleName | "gate-config"; +}; + +const REPOSITORY_ROOT = resolve(import.meta.dir, ".."); + +const PUBLIC_CLAIM_ROOTS = [ + "README.md", + "CONTRIBUTING.md", + "docs", + "examples", + "skills", +] as const; + +const CLAIM_ALLOWANCE_PATTERN = + //g; +const DECISION_ALLOWABLE_CLAIM_RULES = new Set(["retired-api"]); + +const APPROVED_LEGACY_CLAIM_EXCLUSIONS = [ + "docs/core/brand-types.mdx", + "docs/core/error-system.mdx", + "docs/core/result-pattern.mdx", + "docs/getting-started/installation.mdx", + "docs/getting-started/quick-start.mdx", + "docs/integrations/testing.mdx", + "docs/migration/from-try-catch.mdx", + "docs/patterns/optional-keys.mdx", + "docs/patterns/real-world.mdx", + "docs/patterns/service-layer.mdx", + "docs/philosophy/brand-implementation.mdx", + "docs/philosophy/design-principles.mdx", + "docs/philosophy/developer-experience.mdx", + "docs/philosophy/err-null-is-ok-null.md", + "docs/philosophy/error-api-evolution.mdx", + "docs/philosophy/for-the-pragmatic-fp-developer.mdx", + "docs/philosophy/from-effect-to-pragmatic-errors.mdx", + "docs/philosophy/production-reliability.mdx", + "docs/philosophy/rust-inspiration.mdx", + "docs/philosophy/why-name-and-message.mdx", +] as const; + +// This retained page is waiting for the already-approved rewrite/rename cutover. +// It is not part of the deletion allowlist approved for Wave 11. +const TEMPORARY_REWRITE_CLAIM_EXCLUSIONS = [ + "docs/integrations/hono-serialization.mdx", +] as const; + +const LEGACY_CLAIM_EXCLUSIONS = [ + ...APPROVED_LEGACY_CLAIM_EXCLUSIONS, + ...TEMPORARY_REWRITE_CLAIM_EXCLUSIONS, +]; + +const DOC_EXTENSIONS = new Set([".json", ".md", ".mdx"]); +const EXAMPLE_EXTENSIONS = new Set([ + ".cjs", + ".js", + ".jsx", + ".md", + ".mdx", + ".mjs", + ".ts", + ".tsx", +]); +const SKILL_EXTENSIONS = new Set([".md", ".mdx"]); + +const SERIALIZATION_NEGATION_PATTERN = + /\b(?:are not|aren't|cannot|can't|do not|does not|doesn't|is not|isn't|must not|never|no|not|should not|without|won't|will not)\b/i; +const UNSAFE_CHECK_QUALIFICATION_PATTERN = + /\b(?:avoid|do not use|don't use|misclassif|not a universal|not the general|safe only|unsafe|works only|works for object)\b/i; + +const RESULT_ERROR_EXPRESSION = String.raw`(?:[A-Za-z_$][\w$]*\.)*error`; +const UNSAFE_RESULT_PATTERNS = [ + new RegExp( + String.raw`\b(?:if|while)\s*\(\s*(?:!\s*)?${RESULT_ERROR_EXPRESSION}\s*\)`, + "g", + ), + new RegExp(String.raw`\bBoolean\s*\(\s*${RESULT_ERROR_EXPRESSION}\s*\)`, "g"), + new RegExp(String.raw`!!\s*${RESULT_ERROR_EXPRESSION}\b`, "g"), + new RegExp( + String.raw`${RESULT_ERROR_EXPRESSION}\s*(?:\?(?!\.)|&&|\|\|)`, + "g", + ), +] as const; + +function hasSerializationNegation( + line: string, + matchIndex: number, + matchLength: number, +): boolean { + const prefix = line.slice(0, matchIndex); + const punctuationIndex = Math.max( + prefix.lastIndexOf("."), + prefix.lastIndexOf(";"), + prefix.lastIndexOf(":"), + prefix.lastIndexOf("!"), + prefix.lastIndexOf("?"), + ); + return SERIALIZATION_NEGATION_PATTERN.test( + line.slice( + Math.max(punctuationIndex + 1, matchIndex - 120), + Math.min(line.length, matchIndex + matchLength), + ), + ); +} + +const CLAIM_RULES: readonly ClaimRule[] = [ + { + name: "retired-api", + message: "Current guidance uses a retired API name.", + patterns: [ + /\b(?:createTaggedError(?:Group|s)?|mutationOptions|queryOptions)\s*\(/, + /import\s*\{[^}]*\b(?:createTaggedError(?:Group|s)?|mutationOptions|queryOptions)\b/, + /`(?:createTaggedError(?:Group|s)?|mutationOptions|queryOptions)`/, + /\.execute\s*\(/, + ], + shouldReport: ({ line, match }) => + !match[0].startsWith(".execute") || + !hasSerializationNegation(line, match.index, match[0].length), + }, + { + name: "unsupported-root-import", + message: 'The package has no supported root import from "wellcrafted".', + patterns: [ + /\bfrom\s+["']wellcrafted["']/, + /\bimport\s*\(\s*["']wellcrafted["']\s*\)/, + /\brequire\s*\(\s*["']wellcrafted["']\s*\)/, + ], + }, + { + name: "conflicting-setup", + message: "Current guidance contains an unsupported or conflicting setup.", + patterns: [ + /\bTypeScript\s+4(?:\.\d+){0,2}\b/i, + /\bbun\s+run\s+dev\b/i, + /\bmoduleResolution\b[^\n]*(?:["']node["']|\bnode\b)/i, + ], + shouldReport: ({ line, match }) => { + if (/moduleResolution/i.test(match[0]) && /NodeNext/i.test(line)) { + return false; + } + return true; + }, + }, + { + name: "npm-contributor-command", + message: "Repository contributor commands must use Bun.", + patterns: [/\bnpm\s+(?:ci|install|run|test)\b/i, /\bnpx\s+changeset\b/i], + shouldReport: ({ path }) => path === "CONTRIBUTING.md", + }, + { + name: "serialization-absolute", + message: "Serialization claims must be conditional on the complete value.", + patterns: [ + /\b(?:serialize|serializes|serialized)\s+(?:cleanly|exactly|intact|perfectly)\b/i, + /\b(?:lossless|perfect)\s+(?:JSON\s+)?(?:round[- ]trip|serialization)\b/i, + /\bround[- ]trips?\s+(?:exactly|intact|losslessly|perfectly)\b/i, + /\bsurvives?\s+(?:all|any|every)\s+serialization\b/i, + /\balways\s+survives?\s+(?:JSON\s+)?serialization\b/i, + /\bpreserves?\s+(?:the\s+)?full\s+(?:error\s+)?chain\b/i, + ], + shouldReport: ({ line, match }) => + !hasSerializationNegation(line, match.index, match[0].length), + }, + { + name: "serialization-type-enforcement", + message: "The current types do not enforce JSON-compatible error fields.", + patterns: [ + /\bdefineErrors\b.{0,100}\b(?:enforces?|guarantees?|requires?|rejects?|type-enforces?)\b.{0,80}\bJSON/i, + /\b(?:error\s+)?fields?\s+must\s+be\s+JSON[- ]serializable\b/i, + /\bJSON[- ](?:safe|serializable)\s+fields?\s+(?:are|is)\s+(?:enforced|required)\b/i, + ], + shouldReport: ({ line, match }) => + !hasSerializationNegation(line, match.index, match[0].length), + }, + { + name: "unsupported-metric", + message: + "Public documentation must not publish unsupported vanity metrics.", + patterns: [ + /\b\d[\d,]*(?:\.\d+)?\s+(?:tracked\s+)?(?:call sites?|importers?|importing files?|lines? of (?:production )?code)\b/i, + /\b\d[\d,]*(?:\.\d+)?\s+(?:active\s+|tracked\s+)?consumers?\b/i, + /\b\d[\d,]*\s+(?:files?\s+)?import(?:s|ed|ing)?\s+wellcrafted\b/i, + /\b\d[\d,]*\s+(?:production\s+)?(?:TypeScript\s+)?lines?\b/i, + /\b(?:bundle|library|package)\b.{0,60}(?:<|under|only)?\s*\d+(?:\.\d+)?\s*kB\b/i, + /\b\d+(?:\.\d+)?\s*kB\b.{0,60}\b(?:bundle|library|package)\b/i, + /\b\d+(?:\.\d+)?%\b.{0,60}\b(?:faster|reduction|smaller|sharing)\b/i, + /\b(?:hours?\s+to\s+minutes?|thousands? of hours?)\b/i, + ], + }, + { + name: "reliability-claim", + message: + "Public documentation must not make unsupported reliability claims.", + patterns: [ + /\b(?:battle[- ]tested|production[- ]tested|proven in production)\b/i, + /\bzero\s+(?:runtime\s+)?(?:crashes|unhandled exceptions)\b/i, + /\bnever\s+crashes?\b/i, + /\bno\s+(?:hidden failures|surprise exceptions)\b/i, + /\b\d{1,3}(?:\.\d+)?%\s+(?:uptime|availability)\b/i, + ], + }, + { + name: "uppercase-brand", + message: "Use lowercase wellcrafted in current public prose.", + patterns: [/\bWellCrafted\b/], + }, + { + name: "unsafe-result-check", + message: + "Use an exact Result check unless truthy object errors are explicit.", + patterns: [], + }, +] as const; + +const KNOWN_CLAIM_RULES = new Set( + CLAIM_RULES.map((rule) => rule.name), +); + +function toRepositoryPath(path: string): string { + return path.split(sep).join(posix.sep); +} + +async function pathExists(path: string): Promise { + try { + await stat(resolve(REPOSITORY_ROOT, path)); + return true; + } catch { + return false; + } +} + +function includePublicFile(root: string, path: string): boolean { + const extension = extname(path); + switch (root) { + case "docs": + return DOC_EXTENSIONS.has(extension); + case "examples": + return EXAMPLE_EXTENSIONS.has(extension); + case "skills": + return SKILL_EXTENSIONS.has(extension); + default: + return path === root; + } +} + +async function collectFiles(root: string): Promise { + const absoluteRoot = resolve(REPOSITORY_ROOT, root); + const rootStats = await stat(absoluteRoot); + if (rootStats.isFile()) return [root]; + + const files: string[] = []; + const entries = await readdir(absoluteRoot, { withFileTypes: true }); + for (const entry of entries) { + const child = posix.join(root, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectFiles(child))); + continue; + } + if (entry.isFile()) files.push(child); + } + return files; +} + +async function validateExclusions(findings: Finding[]): Promise> { + const approved = new Set(APPROVED_LEGACY_CLAIM_EXCLUSIONS); + const temporaryRewrites = new Set(TEMPORARY_REWRITE_CLAIM_EXCLUSIONS); + const allowed = new Set([...approved, ...temporaryRewrites]); + const configured = new Set(); + + for (const rawPath of LEGACY_CLAIM_EXCLUSIONS) { + const path = toRepositoryPath(rawPath); + if (configured.has(path)) { + findings.push({ + line: 1, + message: `Duplicate legacy exclusion: ${path}`, + path: "scripts/check-doc-claims.ts", + rule: "gate-config", + }); + continue; + } + configured.add(path); + + if (!path.startsWith("docs/") || !allowed.has(path)) { + findings.push({ + line: 1, + message: `Legacy exclusion is outside the exact claims exclusions: ${path}`, + path: "scripts/check-doc-claims.ts", + rule: "gate-config", + }); + continue; + } + if (!(await pathExists(path))) { + findings.push({ + line: 1, + message: `Legacy exclusion points to a missing file: ${path}`, + path: "scripts/check-doc-claims.ts", + rule: "gate-config", + }); + } + } + + for (const path of approved) { + if ((await pathExists(path)) && !configured.has(path)) { + findings.push({ + line: 1, + message: `Retained legacy file is missing its exact exclusion: ${path}`, + path: "scripts/check-doc-claims.ts", + rule: "gate-config", + }); + } + } + + for (const path of temporaryRewrites) { + if ((await pathExists(path)) && !configured.has(path)) { + findings.push({ + line: 1, + message: `Retained rewrite source is missing its exact exclusion: ${path}`, + path: "scripts/check-doc-claims.ts", + rule: "gate-config", + }); + } + } + + return configured; +} + +function parseAllowances( + path: string, + lines: readonly string[], + findings: Finding[], +): Allowance[] { + const allowances: Allowance[] = []; + let active: + | { reason: string; rule: ClaimRuleName; startLine: number } + | undefined; + + for (const [index, line] of lines.entries()) { + const lineNumber = index + 1; + const matches = [ + ...line.matchAll(new RegExp(CLAIM_ALLOWANCE_PATTERN.source, "g")), + ]; + if (matches.length === 0) { + if (line.includes("docs:claims:allow-")) { + findings.push({ + line: lineNumber, + message: "Malformed claims allowance marker.", + path, + rule: "gate-config", + }); + } + continue; + } + if (matches.length !== 1 || line.trim() !== matches[0]?.[0]) { + findings.push({ + line: lineNumber, + message: "A claims allowance marker must occupy its own line.", + path, + rule: "gate-config", + }); + continue; + } + + const [, direction, rawRule, rawReason] = matches[0]; + if (!KNOWN_CLAIM_RULES.has(rawRule as ClaimRuleName)) { + findings.push({ + line: lineNumber, + message: `Unknown claims allowance rule: ${rawRule}`, + path, + rule: "gate-config", + }); + continue; + } + const rule = rawRule as ClaimRuleName; + + if (!/^docs\/decisions\/[^/]+\.mdx$/.test(path)) { + findings.push({ + line: lineNumber, + message: "Claims allowances are permitted only in decision pages.", + path, + rule: "gate-config", + }); + continue; + } + if (!DECISION_ALLOWABLE_CLAIM_RULES.has(rule)) { + findings.push({ + line: lineNumber, + message: `Rule ${rule} cannot be allowed; only decision-scoped retired API history is eligible.`, + path, + rule: "gate-config", + }); + continue; + } + + if (direction === "start") { + const reason = rawReason?.trim() ?? ""; + if (reason.length === 0) { + findings.push({ + line: lineNumber, + message: "A claims allowance start marker requires a reason.", + path, + rule: "gate-config", + }); + continue; + } + if (active !== undefined) { + findings.push({ + line: lineNumber, + message: "Claims allowances cannot be nested.", + path, + rule: "gate-config", + }); + continue; + } + active = { reason, rule, startLine: lineNumber }; + continue; + } + + if (rawReason !== undefined) { + findings.push({ + line: lineNumber, + message: "A claims allowance end marker cannot include a reason.", + path, + rule: "gate-config", + }); + } + if (active === undefined) { + findings.push({ + line: lineNumber, + message: "Claims allowance end marker has no matching start.", + path, + rule: "gate-config", + }); + continue; + } + if (active.rule !== rule) { + findings.push({ + line: lineNumber, + message: `Claims allowance closes ${rule}, but ${active.rule} is active.`, + path, + rule: "gate-config", + }); + continue; + } + + allowances.push({ + endLine: lineNumber, + reason: active.reason, + rule, + startLine: active.startLine, + uses: 0, + }); + active = undefined; + } + + if (active !== undefined) { + findings.push({ + line: active.startLine, + message: `Claims allowance for ${active.rule} is not closed.`, + path, + rule: "gate-config", + }); + } + + return allowances; +} + +function findAllowance( + allowances: readonly Allowance[], + rule: ClaimRuleName, + line: number, +): Allowance | undefined { + return allowances.find( + (allowance) => + allowance.rule === rule && + line > allowance.startLine && + line < allowance.endLine, + ); +} + +function lineAtOffset(content: string, offset: number): number { + return content.slice(0, offset).split(/\r?\n/).length; +} + +function localClaimContext( + content: string, + matchIndex: number, + matchLength: number, +): string { + const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1; + const lineEnd = content.indexOf("\n", matchIndex + matchLength); + return content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd); +} + +function collectRetiredApiMatches( + content: string, +): Array<{ index: number; text: string }> { + const matches: Array<{ index: number; text: string }> = []; + const localNames = new Set(); + const importPattern = /\bimport\s*\{([\s\S]*?)\}\s*from\s*["'][^"']+["']/g; + const directCallPattern = + /\b(?:createTaggedError(?:Group|s)?|mutationOptions|queryOptions)\s*\(/g; + + for (const callMatch of content.matchAll(directCallPattern)) { + matches.push({ index: callMatch.index, text: callMatch[0] }); + } + + for (const importMatch of content.matchAll(importPattern)) { + const specifiers = importMatch[1] ?? ""; + for (const specifier of specifiers.split(",")) { + const retiredMatch = specifier + .trim() + .match( + /^(createTaggedError(?:Group|s)?|mutationOptions|queryOptions)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/, + ); + if (retiredMatch === null) continue; + + localNames.add(retiredMatch[2] ?? retiredMatch[1]); + matches.push({ + index: importMatch.index, + text: importMatch[0], + }); + } + } + + for (const localName of localNames) { + const callPattern = new RegExp( + String.raw`\b${localName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\s*\(`, + "g", + ); + for (const callMatch of content.matchAll(callPattern)) { + matches.push({ index: callMatch.index, text: callMatch[0] }); + } + } + + return matches; +} + +function scanMultilineClaims( + path: string, + content: string, + allowances: readonly Allowance[], + findings: Finding[], +): void { + const matchesByRule = [ + { + matches: collectRetiredApiMatches(content), + message: "Current guidance uses a retired API name.", + rule: "retired-api" as const, + }, + { + matches: UNSAFE_RESULT_PATTERNS.flatMap((pattern) => + [...content.matchAll(pattern)].map((match) => ({ + index: match.index, + text: match[0], + })), + ), + message: + "Use an exact Result check unless truthy object errors are explicit.", + rule: "unsafe-result-check" as const, + }, + ] as const; + + for (const entry of matchesByRule) { + for (const match of entry.matches) { + const line = lineAtOffset(content, match.index); + if ( + entry.rule === "unsafe-result-check" && + UNSAFE_CHECK_QUALIFICATION_PATTERN.test( + localClaimContext(content, match.index, match.text.length), + ) + ) { + continue; + } + if ( + findings.some( + (finding) => + finding.path === path && + finding.line === line && + finding.rule === entry.rule, + ) + ) { + continue; + } + + const allowance = findAllowance(allowances, entry.rule, line); + if (allowance !== undefined) { + allowance.uses += 1; + continue; + } + findings.push({ line, message: entry.message, path, rule: entry.rule }); + } + } +} + +export function findClaimFindings(path: string, content: string): Finding[] { + const findings: Finding[] = []; + const lines = content.split(/\r?\n/); + const allowances = parseAllowances(path, lines, findings); + scanClaimLines(path, lines, allowances, findings); + scanMultilineClaims(path, content, allowances, findings); + validateAllowanceUses(path, allowances, findings); + return findings; +} + +function scanClaimLines( + path: string, + lines: readonly string[], + allowances: readonly Allowance[], + findings: Finding[], +): void { + for (const [index, line] of lines.entries()) { + const lineNumber = index + 1; + if (line.includes("", + "```typescript", + "const value = 1;", + "```", + "", + ].join("\n"), + })), + }), + ).not.toThrow(); +}); + +test("accepts every required manifest pair and public owner", () => { + const { sourceFiles, targetFiles } = createManifestFiles(); + + expect(() => + checkDocSnippets({ sourceFiles, targetFiles, manifest }), + ).not.toThrow(); +}); + +test("rejects a missing required source-target pair", () => { + const { sourceFiles, targetFiles } = createManifestFiles(); + + expect(() => + checkDocSnippets({ + sourceFiles, + targetFiles: targetFiles.filter( + (file) => file.path !== "docs/snippets/example.mdx", + ), + manifest, + }), + ).toThrow( + 'Required target snippet "example" is missing from docs/snippets/example.mdx', + ); +}); + +test("rejects a missing public owner import", () => { + const { sourceFiles, targetFiles } = createManifestFiles(); + const owner = targetFiles.find( + (file) => file.path === "docs/start/example.mdx", + ); + if (!owner) throw new Error("Expected the owner fixture."); + owner.content = ""; + + expect(() => + checkDocSnippets({ sourceFiles, targetFiles, manifest }), + ).toThrow("is missing the required import"); +}); + +test("rejects duplicate sources", () => { + expect(() => + checkDocSnippets({ + sourceFiles: ["one.ts", "two.ts"].map((path) => ({ + path, + content: + "// docs:snippet example:start\nconst value = 1;\n// docs:snippet example:end", + })), + targetFiles: [], + }), + ).toThrow("multiple sources"); +}); + +test("rejects nested markers", () => { + expect(() => + checkDocSnippets({ + sourceFiles: [ + { + path: "example.ts", + content: [ + "// docs:snippet outer:start", + "// docs:snippet inner:start", + "// docs:snippet inner:end", + "// docs:snippet outer:end", + ].join("\n"), + }, + ], + targetFiles: [], + }), + ).toThrow("Nested snippet"); +}); + +test("rejects orphan and unused regions", () => { + expect(() => + checkDocSnippets({ + sourceFiles: [], + targetFiles: [ + { + path: "README.md", + content: + "\n```ts\nvalue\n```\n", + }, + ], + }), + ).toThrow("has no source"); + + expect(() => + checkDocSnippets({ + sourceFiles: [ + { + path: "example.ts", + content: + "// docs:snippet example:start\nvalue\n// docs:snippet example:end", + }, + ], + targetFiles: [], + }), + ).toThrow("has no target"); +}); + +test("rejects target prose and drift", () => { + const sourceFiles = [ + { + path: "example.ts", + content: + "// docs:snippet example:start\nconst value = 1;\n// docs:snippet example:end", + }, + ]; + + expect(() => + checkDocSnippets({ + sourceFiles, + targetFiles: [ + { + path: "README.md", + content: + "\nProse\n```ts\nconst value = 1;\n```\n", + }, + ], + }), + ).toThrow("exactly one fenced code block and no prose"); + + expect(() => + checkDocSnippets({ + sourceFiles, + targetFiles: [ + { + path: "README.md", + content: + "\n```ts\nconst value = 2;\n```\n", + }, + ], + }), + ).toThrow("has drifted"); +}); diff --git a/scripts/check-doc-snippets.ts b/scripts/check-doc-snippets.ts new file mode 100644 index 0000000..5e9b657 --- /dev/null +++ b/scripts/check-doc-snippets.ts @@ -0,0 +1,348 @@ +type TextFile = { + path: string; + content: string; +}; + +type Region = { + id: string; + path: string; + startLine: number; + content: string; +}; + +type Marker = { + id: string; + kind: "start" | "end"; +}; + +export type SnippetManifestEntry = { + id: string; + sourcePath: string; + targetPaths: readonly string[]; + ownerPath: string; + ownerImport: string; + ownerComponent: string; +}; + +export const DOC_SNIPPET_MANIFEST: readonly SnippetManifestEntry[] = [ + { + id: "quick-start", + sourcePath: "examples/quick-start.ts", + targetPaths: ["README.md", "docs/snippets/quick-start.mdx"], + ownerPath: "docs/start/quick-start.mdx", + ownerImport: 'import QuickStartExample from "/snippets/quick-start.mdx";', + ownerComponent: "", + }, + { + id: "service-boundary", + sourcePath: "examples/service-boundary.ts", + targetPaths: ["docs/snippets/service-boundary.mdx"], + ownerPath: "docs/guides/service-boundaries.mdx", + ownerImport: + 'import ServiceBoundaryExample from "/snippets/service-boundary.mdx";', + ownerComponent: "", + }, + { + id: "serialization-boundary", + sourcePath: "examples/serialization-boundary.ts", + targetPaths: ["docs/snippets/serialization-boundary.mdx"], + ownerPath: "docs/guides/serialization-boundaries.mdx", + ownerImport: + 'import SerializationBoundaryExample from "/snippets/serialization-boundary.mdx";', + ownerComponent: "", + }, + { + id: "tanstack-query", + sourcePath: "examples/tanstack-query.ts", + targetPaths: ["docs/snippets/tanstack-query.mdx"], + ownerPath: "docs/integrations/tanstack-query.mdx", + ownerImport: + 'import TanStackQueryExample from "/snippets/tanstack-query.mdx";', + ownerComponent: "", + }, +]; + +const SOURCE_MARKER = /^\s*\/\/\s*docs:snippet\s+([a-z0-9-]+):(start|end)\s*$/; +const HTML_TARGET_MARKER = + /^\s*\s*$/; +const MDX_TARGET_MARKER = + /^\s*\{\/\*\s*docs:snippet\s+([a-z0-9-]+):(start|end)\s*\*\/\}\s*$/; + +function normalize(content: string): string { + const lines = content + .replaceAll("\r\n", "\n") + .replaceAll("\r", "\n") + .split("\n") + .map((line) => line.trimEnd()); + + while (lines[0] === "") lines.shift(); + while (lines.at(-1) === "") lines.pop(); + return lines.join("\n"); +} + +function parseMarker({ + line, + path, + lineNumber, + type, +}: { + line: string; + path: string; + lineNumber: number; + type: "source" | "target"; +}): Marker | null { + const patterns = + type === "source" + ? [SOURCE_MARKER] + : [HTML_TARGET_MARKER, MDX_TARGET_MARKER]; + for (const pattern of patterns) { + const match = line.match(pattern); + if (match?.[1] && match[2] !== undefined) { + return { id: match[1], kind: match[2] as Marker["kind"] }; + } + } + + if (line.includes("docs:snippet")) { + throw new Error(`Malformed ${type} marker at ${path}:${lineNumber}.`); + } + return null; +} + +function extractRegions(file: TextFile, type: "source" | "target"): Region[] { + const lines = file.content + .replaceAll("\r\n", "\n") + .replaceAll("\r", "\n") + .split("\n"); + const regions: Region[] = []; + let open: { id: string; startLine: number; contentStart: number } | null = + null; + + for (const [index, line] of lines.entries()) { + const lineNumber = index + 1; + const marker = parseMarker({ line, path: file.path, lineNumber, type }); + if (!marker) continue; + + if (marker.kind === "start") { + if (open) { + throw new Error( + `Nested snippet "${marker.id}" at ${file.path}:${lineNumber}; "${open.id}" opened at line ${open.startLine}.`, + ); + } + open = { id: marker.id, startLine: lineNumber, contentStart: index + 1 }; + continue; + } + + if (!open) { + throw new Error( + `Snippet "${marker.id}" ends without a start at ${file.path}:${lineNumber}.`, + ); + } + if (open.id !== marker.id) { + throw new Error( + `Snippet "${marker.id}" ends at ${file.path}:${lineNumber}, but "${open.id}" opened at line ${open.startLine}.`, + ); + } + + regions.push({ + id: open.id, + path: file.path, + startLine: open.startLine, + content: lines.slice(open.contentStart, index).join("\n"), + }); + open = null; + } + + if (open) { + throw new Error( + `Snippet "${open.id}" starts at ${file.path}:${open.startLine} but never ends.`, + ); + } + + const ids = new Set(); + for (const region of regions) { + if (ids.has(region.id)) { + throw new Error( + `Snippet "${region.id}" is declared more than once in ${file.path}.`, + ); + } + ids.add(region.id); + } + + return regions; +} + +function extractTargetCode(region: Region): string { + const content = normalize(region.content); + const lines = content.split("\n"); + const fenceLines = lines + .map((line, index) => ({ line, index })) + .filter(({ line }) => line.trimStart().startsWith("```")); + + if ( + fenceLines.length !== 2 || + fenceLines[0]?.index !== 0 || + fenceLines[1]?.index !== lines.length - 1 || + !/^```[a-z0-9_-]+(?:\s+[^\s].*)?$/i.test(lines[0] ?? "") || + (lines.at(-1) ?? "").trim() !== "```" + ) { + throw new Error( + `Target snippet "${region.id}" at ${region.path}:${region.startLine} must contain exactly one fenced code block and no prose.`, + ); + } + + return normalize(lines.slice(1, -1).join("\n")); +} + +export function checkDocSnippets({ + sourceFiles, + targetFiles, + manifest = [], +}: { + sourceFiles: TextFile[]; + targetFiles: TextFile[]; + manifest?: readonly SnippetManifestEntry[]; +}): void { + const sources = sourceFiles.flatMap((file) => extractRegions(file, "source")); + const targets = targetFiles.flatMap((file) => extractRegions(file, "target")); + const sourceById = new Map(); + + for (const source of sources) { + const previous = sourceById.get(source.id); + if (previous) { + throw new Error( + `Snippet "${source.id}" has multiple sources: ${previous.path}:${previous.startLine} and ${source.path}:${source.startLine}.`, + ); + } + sourceById.set(source.id, source); + } + + if (manifest.length > 0) { + const manifestById = new Map(manifest.map((entry) => [entry.id, entry])); + const targetFilesByPath = new Map( + targetFiles.map((file) => [file.path, file]), + ); + + for (const source of sources) { + if (!manifestById.has(source.id)) { + throw new Error( + `Source snippet "${source.id}" at ${source.path}:${source.startLine} is not declared in the documentation snippet manifest.`, + ); + } + } + + for (const target of targets) { + if (!manifestById.has(target.id)) { + throw new Error( + `Target snippet "${target.id}" at ${target.path}:${target.startLine} is not declared in the documentation snippet manifest.`, + ); + } + } + + for (const entry of manifest) { + const source = sourceById.get(entry.id); + if (!source) { + throw new Error( + `Required source snippet "${entry.id}" is missing from ${entry.sourcePath}.`, + ); + } + if (source.path !== entry.sourcePath) { + throw new Error( + `Required source snippet "${entry.id}" moved from ${entry.sourcePath} to ${source.path}.`, + ); + } + + const actualTargetPaths = targets + .filter((target) => target.id === entry.id) + .map((target) => target.path); + for (const requiredPath of entry.targetPaths) { + if (!actualTargetPaths.includes(requiredPath)) { + throw new Error( + `Required target snippet "${entry.id}" is missing from ${requiredPath}.`, + ); + } + } + for (const actualPath of actualTargetPaths) { + if (!entry.targetPaths.includes(actualPath)) { + throw new Error( + `Target snippet "${entry.id}" is declared at unexpected path ${actualPath}.`, + ); + } + } + + const owner = targetFilesByPath.get(entry.ownerPath); + if (!owner) { + throw new Error( + `Required public owner for snippet "${entry.id}" is missing: ${entry.ownerPath}.`, + ); + } + if (!owner.content.includes(entry.ownerImport)) { + throw new Error( + `Public owner ${entry.ownerPath} is missing the required import for snippet "${entry.id}": ${entry.ownerImport}`, + ); + } + if (!owner.content.includes(entry.ownerComponent)) { + throw new Error( + `Public owner ${entry.ownerPath} is missing the required component for snippet "${entry.id}": ${entry.ownerComponent}`, + ); + } + } + } + + const targetIds = new Set(); + for (const target of targets) { + const source = sourceById.get(target.id); + if (!source) { + throw new Error( + `Target snippet "${target.id}" at ${target.path}:${target.startLine} has no source.`, + ); + } + targetIds.add(target.id); + + if (extractTargetCode(target) !== normalize(source.content)) { + throw new Error( + `Target snippet "${target.id}" at ${target.path}:${target.startLine} has drifted from ${source.path}:${source.startLine}.`, + ); + } + } + + for (const source of sources) { + if (!targetIds.has(source.id)) { + throw new Error( + `Source snippet "${source.id}" at ${source.path}:${source.startLine} has no target.`, + ); + } + } +} + +async function readFiles(paths: string[]): Promise { + return Promise.all( + paths.toSorted().map(async (path) => ({ + path, + content: await Bun.file(path).text(), + })), + ); +} + +async function scan(glob: string): Promise { + const paths: string[] = []; + for await (const path of new Bun.Glob(glob).scan({ onlyFiles: true })) { + paths.push(path); + } + return paths; +} + +if (import.meta.main) { + const sourcePaths = await scan("examples/**/*.ts"); + const targetPaths = [ + "README.md", + ...(await scan("docs/**/*.md")), + ...(await scan("docs/**/*.mdx")), + ]; + + checkDocSnippets({ + sourceFiles: await readFiles(sourcePaths), + targetFiles: await readFiles(targetPaths), + manifest: DOC_SNIPPET_MANIFEST, + }); + console.log("documentation snippets match their canonical examples"); +} diff --git a/scripts/check-quick-start-docs.ts b/scripts/check-quick-start-docs.ts deleted file mode 100644 index 6d2aba4..0000000 --- a/scripts/check-quick-start-docs.ts +++ /dev/null @@ -1,70 +0,0 @@ -const SOURCE_START = "// docs:quick-start:start"; -const SOURCE_END = "// docs:quick-start:end"; -const README_START = ""; -const README_END = ""; - -async function read(path: string): Promise { - return Bun.file(path).text(); -} - -function extractBetween({ - content, - start, - end, - path, -}: { - content: string; - start: string; - end: string; - path: string; -}): string { - const startIndex = content.indexOf(start); - const endIndex = content.indexOf(end); - if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) { - throw new Error(`Could not find the quick-start markers in ${path}.`); - } - - return content.slice(startIndex + start.length, endIndex).trim(); -} - -function extractCodeFence(content: string, path: string): string { - const match = content.match(/```typescript(?:[^\n]*)\n([\s\S]*?)\n```/); - if (!match?.[1]) { - throw new Error(`Could not find the TypeScript code fence in ${path}.`); - } - - return match[1].trim(); -} - -const source = extractBetween({ - content: await read("examples/quick-start.ts"), - start: SOURCE_START, - end: SOURCE_END, - path: "examples/quick-start.ts", -}); - -const readme = extractCodeFence( - extractBetween({ - content: await read("README.md"), - start: README_START, - end: README_END, - path: "README.md", - }), - "README.md", -); - -const snippet = extractCodeFence( - await read("docs/snippets/quick-start.mdx"), - "docs/snippets/quick-start.mdx", -); - -for (const [path, content] of [ - ["README.md", readme], - ["docs/snippets/quick-start.mdx", snippet], -] as const) { - if (content !== source) { - throw new Error(`${path} has drifted from examples/quick-start.ts.`); - } -} - -console.log("quick-start docs match the canonical example"); diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 2a98fd6..3634912 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -463,7 +463,7 @@ Pin the Mintlify CLI version rather than letting `bunx` silently change validati Strict content gates are designed and enabled only after the canonical content exists: - `docs:exports`: derive `(subpath, export kind, symbol)` tuples from the manifest and emitted declarations, then compare them with machine-readable markers on exactly one reference page. Searching prose does not count as coverage. -- `docs:claims`: scan `README.md`, `CONTRIBUTING.md`, `docs/`, `examples/`, and `skills/`; exclude `CHANGELOG.md`, historical specs, and explicitly marked historical quotations. Reject a documented list of retired names, unsupported root imports, conflicting requirements, npm contributor commands, serialization absolutes, unsupported metrics, and stale links. +- `docs:claims`: scan `README.md`, `CONTRIBUTING.md`, `docs/`, `examples/`, and `skills/`; exclude `CHANGELOG.md`, historical specs, and explicitly marked historical quotations. Reject a documented list of retired names, unsupported root imports, conflicting requirements, npm contributor commands, serialization absolutes, unsupported metrics, reliability claims, unsafe Result checks, and incorrect brand casing. The dedicated `docs:links` gate owns stale links instead of duplicating link resolution here. - `docs:snippets`: enable only after an include, extraction, or comparison prototype passes against the intended Markdown and MDX corpus. Canonical example drift is the first required case. Retained legacy files may have explicit temporary exclusions through the pre-deletion proof. The deletion wave removes each exclusion atomically with its approved legacy file, then reruns the strict gates. @@ -635,10 +635,22 @@ Verification on 2026-07-10: ### Wave 8: Prove and enable strict content gates -- [ ] Prototype machine-readable export markers, exact claims roots/exclusions, and canonical snippet comparison. -- [ ] Make each strict gate pass against canonical content. -- [ ] Add explicit temporary exclusions only for retained legacy files. -- [ ] Enable the passing gates in CI. +- [x] Prototype machine-readable export markers, exact claims roots/exclusions, and canonical snippet comparison. +- [x] Make each strict gate pass against canonical content. +- [x] Add explicit temporary exclusions only for retained legacy files. +- [x] Enable the passing gates in CI. + +Verification on 2026-07-10: + +- Added `docs:exports` as a build plus declaration checker. It derives `(subpath, export kind, symbol)` tuples from `package.json#exports` and emitted declarations, including namespace members, and requires an exact one-line marker of the form `{/* docs:export subpath="wellcrafted/result" kind="value" symbol="Ok" */}`. Every tuple must appear once in its sole `docs/reference/.mdx` owner, and the reference-file set must match the nine manifest subpaths exactly. The gate passed with 79 value, type, and namespace tuples across nine owners. +- Added `docs:claims` over exactly `README.md`, `CONTRIBUTING.md`, `docs/`, `examples/`, and `skills/`. It scans `.json`, `.md`, and `.mdx` under `docs/`; common JavaScript, TypeScript, Markdown, and MDX files under `examples/`; and Markdown or MDX under `skills/`. `CHANGELOG.md` and `specs/` are outside those roots. Decision pages may use an exact, reasoned, non-nested, and consumed allowance only for retired API history; no other claim class can be allowed inline. Stale links remain owned by `docs:links`. +- The claims gate has 20 exact deletion-approved legacy exclusions: `docs/core/brand-types.mdx`, `docs/core/error-system.mdx`, `docs/core/result-pattern.mdx`, `docs/getting-started/installation.mdx`, `docs/getting-started/quick-start.mdx`, `docs/integrations/testing.mdx`, `docs/migration/from-try-catch.mdx`, `docs/patterns/optional-keys.mdx`, `docs/patterns/real-world.mdx`, `docs/patterns/service-layer.mdx`, `docs/philosophy/brand-implementation.mdx`, `docs/philosophy/design-principles.mdx`, `docs/philosophy/developer-experience.mdx`, `docs/philosophy/err-null-is-ok-null.md`, `docs/philosophy/error-api-evolution.mdx`, `docs/philosophy/for-the-pragmatic-fp-developer.mdx`, `docs/philosophy/from-effect-to-pragmatic-errors.mdx`, `docs/philosophy/production-reliability.mdx`, `docs/philosophy/rust-inspiration.mdx`, and `docs/philosophy/why-name-and-message.mdx`. The gate passed across 41 current public files with those 20 files excluded. +- `docs/integrations/hono-serialization.mdx` is a separate temporary retained rewrite-source exclusion until the already-approved `docs/integrations/hono.mdx` cutover. This does not change its disposition to “Approved after proof,” does not add it to the Wave 11 deletion allowlist, and does not expand deletion approval. The claims gate requires this exclusion to be removed when the old rewrite source leaves the current path. +- Added `docs:snippets` with source markers `// docs:snippet :start|end` in `examples/**/*.ts` and HTML or MDX target markers in `README.md` and `docs/**/*.{md,mdx}`. Each ID has exactly one source and at least one target; multiple exact targets are allowed. A target contains one fenced code block and no prose. Newline style and trailing whitespace are normalized, while code content otherwise compares exactly. Malformed, nested, mismatched, duplicate, orphaned, unused, or drifted regions fail. The checked IDs are `quick-start`, `service-boundary`, `serialization-boundary`, and `tanstack-query`; the quick start has both README and reusable-snippet targets. +- Replaced the deleted focused quick-start checker in `docs:examples` with `docs:snippets` while preserving the package build, strict example typecheck, and the quick-start, service-boundary, and serialization-boundary runtime examples. The hardened snippet tests cover the explicit four-snippet source, target, and public-owner manifest so a whole pair or owner import cannot disappear silently. +- Added table-driven claims tests for multiline and aliased retired APIs, direct and coerced Result truthiness, consumer and uptime metrics, JSON-field enforcement, and unconditional serialization claims. Explicit limitations and current APIs remain accepted. The focused gate suites now cover 30 cases, and the full test suite passed 188 tests with no failures. +- Added `docs:exports`, `docs:claims`, and `docs:snippets` to the main CI job after the existing build, test, and documentation-example steps. `docs:exports`, `docs:claims`, `docs:snippets`, `format:check`, `typecheck`, `build`, `test`, `docs:examples`, `package:smoke`, `compat:types`, and `compat:runtime` passed. `lint:check` passed with 12 pre-existing warnings and no Wave 8 finding. +- With Node 24.14.0, `docs:validate` and `docs:links` passed. The main workflow parsed as YAML, and `git diff --check` passed. ### Wave 9: Cut over while the old path remains From 4a62cf8412d1e4042e533bae17767a69ee991f16 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:01:42 -0700 Subject: [PATCH 10/13] docs: cut over to canonical documentation owners Replace legacy navigation with the 24-route start, guide, reference, integration, and decision architecture. Preserve old files for proof while removing current-path dependence and recording the migration ledger. --- docs/docs.json | 55 ++++--- docs/integrations/hono-serialization.mdx | 140 +----------------- scripts/check-doc-claims.ts | 28 +--- ...10T012026-greenfield-documentation-pass.md | 47 +++++- 4 files changed, 77 insertions(+), 193 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 5451197..8df16bc 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -22,54 +22,53 @@ "tab": "Documentation", "groups": [ { - "group": "\ud83d\ude80 Getting Started", + "group": "Getting started", "pages": [ "index", - "getting-started/installation", - "getting-started/quick-start" + "start/installation", + "start/quick-start", + "start/migrating-from-try-catch" ] }, { - "group": "\u26a1 Core", + "group": "Guides", "pages": [ - "core/result-pattern", - "core/error-system", - "core/brand-types" + "guides/defining-error-vocabularies", + "guides/composing-results", + "guides/service-boundaries", + "guides/serialization-boundaries" ] }, { - "group": "\ud83d\udee0\ufe0f Patterns", + "group": "Reference", "pages": [ - "patterns/real-world", - "patterns/optional-keys", - "patterns/service-layer" + "reference/result", + "reference/error", + "reference/logger", + "reference/json", + "reference/brand", + "reference/function", + "reference/query", + "reference/standard-schema", + "reference/testing" ] }, { - "group": "\ud83d\udd0c Integrations", + "group": "Integrations", "pages": [ - "integrations/validation-libraries", "integrations/tanstack-query", - "integrations/hono-serialization", - "integrations/testing" + "integrations/validation-libraries", + "integrations/hono" ] }, { - "group": "\ud83d\udcad Philosophy", + "group": "Decisions", "pages": [ - "philosophy/design-principles", - "philosophy/why-name-and-message", - "philosophy/err-null-is-ok-null", - "philosophy/rust-inspiration", - "philosophy/error-api-evolution", - "philosophy/developer-experience", - "philosophy/production-reliability", - "philosophy/brand-implementation" + "decisions/result-shape", + "decisions/error-contract", + "decisions/brand-representation", + "decisions/pragmatic-tradeoffs" ] - }, - { - "group": "\ud83d\udd04 Migration", - "pages": ["migration/from-try-catch"] } ] } diff --git a/docs/integrations/hono-serialization.mdx b/docs/integrations/hono-serialization.mdx index 15d949f..5915aa8 100644 --- a/docs/integrations/hono-serialization.mdx +++ b/docs/integrations/hono-serialization.mdx @@ -1,140 +1,8 @@ --- -title: 'Hono: Result Types Across Serialization Boundaries' -description: 'Use Result types directly in HTTP responses with zero transformation' -icon: 'hono' +title: "Hono guide moved" +description: "Continue to the current Hono HTTP boundary guide" --- -# Hono: Result Types at Serialization Boundaries +# Hono guide moved -wellcrafted's Result types are plain objects that serialize perfectly over JSON. This means you can return `Ok()` and `Err()` directly from Hono endpoints, and they work on the client with zero transformation. - -## The Pattern - -```typescript -import { Hono } from 'hono'; -import { Ok, Err, type Result } from 'wellcrafted/result'; -import { defineErrors, type InferErrors } from 'wellcrafted/error'; - -const UserNotFoundError = defineErrors({ - NotFound: ({ userId }: { userId: string }) => ({ - message: `User ${userId} not found`, - userId, - }), -}); -type UserNotFoundError = InferErrors; - -async function getUser(id: string): Promise> { - if (!id) { - return UserNotFoundError.NotFound({ userId: id }); - } - return Ok({ id, name: 'Alice' }); -} - -const app = new Hono(); - -// Return Result directly from endpoint -app.get('/api/users/:id', async (c) => { - const id = c.req.param('id'); - const result = await getUser(id); - - // Error case - if (result.error) return c.json(Err(result.error), 500); - - // Success case - return c.json(Ok(result.data)); -}); -``` - -That's it. The HTTP response body contains the complete Result type: - -**Error response:** -```json -{ - "error": { - "name": "NotFound", - "message": "User 123 not found", - "userId": "123" - }, - "data": null -} -``` - -**Success response:** -```json -{ - "data": { "id": "123", "name": "Alice" }, - "error": null -} -``` - -## Client Side - -The client receives the same Result type structure: - -```typescript -// Fetch the endpoint -const response = await fetch('/api/users/123'); -const result = await response.json(); - -// TypeScript knows the shape -if (result.error) { - console.error(`${result.error.name}: ${result.error.message}`); - console.log('User ID:', result.error.userId); -} else { - console.log('User:', result.data); -} -``` - - -## Auto-Wrapping Results - -If your service functions return either Results or bare data, normalize them: - -```typescript -import { isResult } from 'wellcrafted/result'; - -app.post('/api/users', async (c) => { - const body = await c.req.json(); - const maybeResult = await createUser(body); - - // Extract data and error regardless of what was returned - const data = isResult(maybeResult) ? maybeResult.data : maybeResult; - const error = isResult(maybeResult) ? maybeResult.error : undefined; - - if (error) return c.json(Err(error), 500); - return c.json(Ok(data), 201); -}); -``` - -## Why This Works - -- **Plain objects**: Result types are just `{ data: T, error: null }` or `{ data: null, error: E }` - they serialize perfectly -- **No transformation needed**: The exact object created on the server arrives on the client -- **Type-safe across boundaries**: Client knows the exact error types the endpoint can return -- **Works anywhere**: Express, Next.js, Fastify, etc. - just return `res.json(Ok(...))` or `response.json(Err(...))` - -## Real-World Example - -Here's how Epicenter (used in Whispering) implements this across all endpoints: - -```typescript -// For every workspace action -forEachAction(client, ({ workspaceId, actionName, action }) => { - const path = `/${workspaceId}/${actionName}`; - - app.get(path, async (c) => { - const query = c.req.query(); - const input = Object.keys(query).length > 0 ? query : undefined; - - const maybeResult = await action(input) as Result | unknown; - - const data = isResult(maybeResult) ? maybeResult.data : maybeResult; - const error = isResult(maybeResult) ? maybeResult.error : undefined; - - if (error) return c.json(Err(error), 500); - return c.json(Ok(data)); - }); -}); -``` - -Every endpoint follows the same pattern. New endpoints automatically get consistent error handling. +This route remains as a compatibility notice. Continue to [Hono: preserve and validate Result-shaped responses](/integrations/hono) for the current HTTP boundary guide. diff --git a/scripts/check-doc-claims.ts b/scripts/check-doc-claims.ts index 4c002e2..c8e37c9 100644 --- a/scripts/check-doc-claims.ts +++ b/scripts/check-doc-claims.ts @@ -76,16 +76,7 @@ const APPROVED_LEGACY_CLAIM_EXCLUSIONS = [ "docs/philosophy/why-name-and-message.mdx", ] as const; -// This retained page is waiting for the already-approved rewrite/rename cutover. -// It is not part of the deletion allowlist approved for Wave 11. -const TEMPORARY_REWRITE_CLAIM_EXCLUSIONS = [ - "docs/integrations/hono-serialization.mdx", -] as const; - -const LEGACY_CLAIM_EXCLUSIONS = [ - ...APPROVED_LEGACY_CLAIM_EXCLUSIONS, - ...TEMPORARY_REWRITE_CLAIM_EXCLUSIONS, -]; +const LEGACY_CLAIM_EXCLUSIONS = APPROVED_LEGACY_CLAIM_EXCLUSIONS; const DOC_EXTENSIONS = new Set([".json", ".md", ".mdx"]); const EXAMPLE_EXTENSIONS = new Set([ @@ -300,8 +291,6 @@ async function collectFiles(root: string): Promise { async function validateExclusions(findings: Finding[]): Promise> { const approved = new Set(APPROVED_LEGACY_CLAIM_EXCLUSIONS); - const temporaryRewrites = new Set(TEMPORARY_REWRITE_CLAIM_EXCLUSIONS); - const allowed = new Set([...approved, ...temporaryRewrites]); const configured = new Set(); for (const rawPath of LEGACY_CLAIM_EXCLUSIONS) { @@ -317,7 +306,7 @@ async function validateExclusions(findings: Finding[]): Promise> { } configured.add(path); - if (!path.startsWith("docs/") || !allowed.has(path)) { + if (!path.startsWith("docs/") || !approved.has(path)) { findings.push({ line: 1, message: `Legacy exclusion is outside the exact claims exclusions: ${path}`, @@ -347,17 +336,6 @@ async function validateExclusions(findings: Finding[]): Promise> { } } - for (const path of temporaryRewrites) { - if ((await pathExists(path)) && !configured.has(path)) { - findings.push({ - line: 1, - message: `Retained rewrite source is missing its exact exclusion: ${path}`, - path: "scripts/check-doc-claims.ts", - rule: "gate-config", - }); - } - } - return configured; } @@ -731,7 +709,7 @@ async function main(): Promise { } console.log( - `documentation claims: ${publicFiles.length - exclusions.size} files checked, ${APPROVED_LEGACY_CLAIM_EXCLUSIONS.length} deletion-approved legacy files and ${TEMPORARY_REWRITE_CLAIM_EXCLUSIONS.length} retained rewrite source excluded`, + `documentation claims: ${publicFiles.length - exclusions.size} files checked, ${APPROVED_LEGACY_CLAIM_EXCLUSIONS.length} deletion-approved legacy files excluded`, ); } diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 3634912..5622a4a 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -654,10 +654,49 @@ Verification on 2026-07-10: ### Wave 9: Cut over while the old path remains -- [ ] Rewire navigation and every inbound link to the new owners. -- [ ] Confirm old pages and source READMEs have no remaining unique content. -- [ ] Remove current-path dependence on old files while leaving them on disk. -- [ ] Commit the cutover separately. +- [x] Rewire navigation and every inbound link to the new owners. +- [x] Confirm old pages and source READMEs have no remaining unique content. +- [x] Remove current-path dependence on old files while leaving them on disk. +- [x] Commit the cutover separately. + +Migration ledger completed on 2026-07-10: + +| Retained legacy owner | Replacement owner(s) | Disposition | Unique-content evidence | +| --- | --- | --- | --- | +| `docs/getting-started/installation.mdx` | `docs/start/installation.mdx` | Migrated and corrected | Consumer installation, subpath imports, compiler configuration, and runtime prerequisites are owned by the tested installation page. Unsupported root-import, legacy module-resolution, and contributor-setup guidance was corrected or dropped. | +| `docs/getting-started/quick-start.mdx` | `docs/start/quick-start.mdx`, `examples/quick-start.ts` | Migrated and corrected | The first Result loop is now explained by the start page and mechanically synchronized with the runnable canonical example. Undefined helpers and duplicate API tours were dropped. | +| `docs/migration/from-try-catch.mdx` | `docs/start/migrating-from-try-catch.mdx` | Migrated and corrected | The surgical adoption sequence, caller migration, and rollback adapter remain. Persona copy, combinator history, and stale links were dropped. | +| `docs/core/result-pattern.mdx` | `docs/guides/composing-results.mdx`, `docs/reference/result.mdx`, `docs/decisions/result-shape.mdx` | Split, migrated, and corrected | Task flow, the complete current API, and shape rationale have distinct owners. Falsy-error guards, `Ok(null)`/`Err(null)`, throwing edges, and current exports are corrected there; the legacy tutorial has no remaining unique fact. | +| `docs/core/error-system.mdx` | `docs/guides/defining-error-vocabularies.mdx`, `docs/guides/serialization-boundaries.mdx`, `docs/reference/error.mdx`, `docs/decisions/error-contract.mdx` | Split, migrated, and corrected | Variant design, boundary limits, current exports, and durable rationale are owned separately. Perfect-serialization, deep-immutability, and tagged-body-versus-`Err` mistakes were corrected. | +| `docs/core/brand-types.mdx` | `docs/reference/brand.mdx`, `docs/integrations/validation-libraries.mdx`, `docs/decisions/brand-representation.mdx` | Split, migrated, and corrected | Type behavior, runtime-validator recipes, and representation rationale have sole owners. Claims that branding validates or changes runtime data were dropped. | +| `docs/patterns/optional-keys.mdx` | `docs/guides/defining-error-vocabularies.mdx` | Migrated and corrected | Required-versus-optional variant field guidance and constructor ownership moved to the vocabulary guide. Repeated factory reference was dropped. | +| `docs/patterns/real-world.mdx` | `docs/guides/service-boundaries.mdx`, `examples/service-boundary.ts` | Migrated and corrected | The grounded service-to-caller pattern is adapted, attributed, and checked against the canonical example. Broad framework, production, and universal-use claims were dropped. | +| `docs/patterns/service-layer.mdx` | `docs/guides/service-boundaries.mdx`, `docs/guides/composing-results.mdx` | Migrated and corrected | Boundary ownership, manual propagation, classification, and caller handling remain in the two task guides. Prescriptive folder architecture and duplicate helper APIs were dropped. | +| `docs/integrations/testing.mdx` | `docs/reference/testing.mdx` | Migrated and corrected | `expectOk` and `expectErr`, narrowing, opposite-branch throws, and the null-error collision are complete in the sole testing reference. The null-unsafe custom matcher was dropped. | +| `docs/philosophy/err-null-is-ok-null.md` | `docs/decisions/result-shape.mdx`, `docs/guides/composing-results.mdx` | Migrated and corrected | The structural collision, non-null-error convention, truthy-error convention, and valid `Ok(null)` case are preserved without claiming the type enforces the convention. | +| `docs/philosophy/error-api-evolution.mdx` | `docs/decisions/error-contract.mdx` | Migrated and condensed | The durable progression from manual literals through brittle overloads and fluent modes to constructor functions remains. Dates, call-site counts, and historical tutorial detail were dropped. | +| `docs/philosophy/rust-inspiration.mdx` | `docs/decisions/error-contract.mdx` | Migrated and qualified | The limited `thiserror` analogy remains as rationale for namespaced variants and co-located messages. Claims of language-level enum equivalence were dropped. | +| `docs/philosophy/why-name-and-message.mdx` | `docs/decisions/error-contract.mdx` | Migrated and condensed | The JavaScript vocabulary rationale for `name` and human-facing `message` remains. Duplicate factory instructions moved to the guide and reference. | +| `docs/philosophy/brand-implementation.mdx` | `docs/decisions/brand-representation.mdx` | Migrated and corrected | Nested private-marker rationale and verified assignability cases remain. Runtime identity, validation, serialization-marker, and universal-composition claims were dropped. | +| `docs/philosophy/for-the-pragmatic-fp-developer.mdx` | `docs/decisions/pragmatic-tradeoffs.mdx` | Migrated and condensed | The explicit-propagation cost and the boundary between Results, exceptions, and an effect system remain. Persona framing and superiority claims were dropped. | +| `docs/philosophy/from-effect-to-pragmatic-errors.mdx` | `docs/decisions/pragmatic-tradeoffs.mdx` | Migrated and condensed | TypeScript control-flow constraints and the Effect escape hatch remain in the factual choice guide. Competitor size, performance, and universal-simplicity claims were dropped. | +| `docs/philosophy/production-reliability.mdx` | `docs/guides/service-boundaries.mdx`, `docs/guides/serialization-boundaries.mdx` | Grounded examples migrated; unsupported claims dropped | Boundary classification and serialization lessons survive in checked guides. Uptime, production-hours, importer, incident-prevention, and other reliability or vanity claims have no public replacement. | +| `docs/philosophy/developer-experience.mdx` | `docs/guides/composing-results.mdx`, `docs/reference/testing.mdx` | Exact behavior migrated; unsupported claims dropped | Ordinary narrowing, explicit branching, and test-helper behavior are owned by current guide/reference pages. Debugging-speed, productivity, editor-universality, and anecdotal claims were dropped. | +| `docs/philosophy/design-principles.mdx` | `README.md`, `docs/decisions/pragmatic-tradeoffs.mdx`, `docs/decisions/error-contract.mdx` | Durable principles migrated and corrected | Plain-data errors, ordinary control flow, explicit cost, and the larger-system escape hatch remain in concise owners. Perfect serialization, works-anywhere, never-throws, and other universal claims were dropped. | +| `src/README.md` | `README.md`, `docs/guides/`, `docs/reference/` | Split and migrated | The public package mental model, workflows, and exact APIs now live at the repository front door and public docs. A source-folder tutorial has no remaining unique reader job. | +| `src/error/README.md` | `docs/guides/defining-error-vocabularies.mdx`, `docs/guides/serialization-boundaries.mdx`, `docs/reference/error.mdx` | Split, migrated, and corrected | Variant rules, current signatures, shallow freeze, and conditional JSON behavior are complete in public owners. Type-enforced or perfect-serialization implications were corrected. | +| `src/query/README.md` | `docs/integrations/tanstack-query.mdx`, `docs/reference/query.mdx` | Split, migrated, and corrected | The two query families, exact returned handle shapes, `defineKeys`, reactive snapshots, cache ownership, and prerequisite are complete in public owners. Retired names and performance claims were dropped. | + +Cutover proof on 2026-07-10: + +- The Documentation tab contains exactly 24 current routes: four Start routes including the index, four guides, nine subpath references, three integrations, and four decisions. Navbar and footer configuration are unchanged. +- Current owners in `README.md`, `CONTRIBUTING.md`, `docs/index.mdx`, `docs/start/`, `docs/guides/`, `docs/reference/`, the three current integration pages, `docs/decisions/`, and `skills/` have no links to a legacy owner. Links already pointed at the split owner matching their intent, so no allowlisted legacy page needed link-only churn. +- `docs/integrations/hono-serialization.mdx` is now a compatibility notice pointing to `/integrations/hono`; it owns no HTTP guidance. Its separate temporary claims exclusion was removed, and the notice passes the same current-content claims gate as the Hono owner. +- All 20 deletion-approved legacy documentation files and all three source READMEs remain on disk. None is required by current navigation or current-owner links. +- `bun run format`, `bun run format:check`, `bun run lint:check`, `bun run typecheck`, `bun test`, and `bun run build` passed. The test run completed 188 tests with no failures; lint reported only the 12 pre-existing warnings recorded in earlier waves. +- `bun run docs:claims`, `bun run docs:snippets`, `bun run docs:exports`, and `bun run docs:examples` passed. The claims gate now checks the Hono compatibility route and reports 42 current files checked with only the 20 deletion-approved legacy pages excluded; export coverage remains 79 tuples across nine reference owners. +- `bun run package:smoke`, `bun run compat:types`, and `bun run compat:runtime` passed. Under bundled Node 24.14.0, `bun run docs:validate` and `bun run docs:links` passed. +- A first-reader structural navigation review confirmed a linear start path, task-oriented guides before reference, integrations separated from core usage, and rationale last. A rendered desktop and narrow-width review was not performed in this wave because no browser-backed local preview was available in this delegated task; it remains an explicit Wave 10 visual check and Wave 12 fresh-context review rather than an implied pass. ### Wave 10: Record the pre-deletion proof checkpoint From 82bb5b89bdcd4b4b11d4cf4608df999cacb083e2 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:58:32 -0700 Subject: [PATCH 11/13] docs: record pre-deletion proof --- docs/docs.json | 4 ---- docs/integrations/hono.mdx | 1 - ...10T012026-greenfield-documentation-pass.md | 20 ++++++++++++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 8df16bc..4ceb9a8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -12,10 +12,6 @@ "source": "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" }, "favicon": "/favicon.svg", - "logo": { - "light": "/logo/light.svg", - "dark": "/logo/dark.svg" - }, "navigation": { "tabs": [ { diff --git a/docs/integrations/hono.mdx b/docs/integrations/hono.mdx index de5a39f..2c48c88 100644 --- a/docs/integrations/hono.mdx +++ b/docs/integrations/hono.mdx @@ -1,7 +1,6 @@ --- title: Hono HTTP boundaries description: Preserve, validate, and type Result-shaped JSON across a Hono boundary without conflating those guarantees. -icon: hono --- # Hono HTTP boundaries diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 5622a4a..77ef5b4 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -700,9 +700,23 @@ Cutover proof on 2026-07-10: ### Wave 10: Record the pre-deletion proof checkpoint -- [ ] Run full type, test, build, packed-package, compatibility, runtime, docs, examples, exports, claims, snippets, and visual checks while old files remain. -- [ ] Record exact commands, versions, and results in this spec. -- [ ] Commit the proof checkpoint before deleting anything. +- [x] Run full type, test, build, packed-package, compatibility, runtime, docs, examples, exports, claims, snippets, and visual checks while old files remain. +- [x] Record exact commands, versions, and results for the completed nonvisual proof in this spec. +- [x] Commit the proof checkpoint before deleting anything. + +Nonvisual pre-deletion proof on 2026-07-10: + +- The checkpoint ran from commit `4a62cf8412d1e4042e533bae17767a69ee991f16` (`docs: cut over to canonical documentation owners`) with Bun 1.3.1, TypeScript 5.8.3, Biome 2.3.3, tsdown 0.12.9, pinned `mint` 4.2.684, and Node 24.14.0 for Mint. `PUPPETEER_SKIP_DOWNLOAD=true bun install --frozen-lockfile` passed and changed no installs. +- `bun run lint:check`, `bun run format:check`, `bun run typecheck`, `bun run build`, and `bun test` passed. Lint reported the same 12 pre-existing warnings and no errors; formatting changed nothing; the full suite completed 188 tests with no failures. +- `bun run docs:examples`, `bun run package:smoke`, `bun run compat:types`, and `bun run compat:runtime` passed. The example gate built the package, typechecked canonical examples, compared canonical snippets, and ran all three offline examples. Package smoke proved the tarball, nine subpaths, unsupported root, strict consumer typechecks, and explicit query prerequisite. The compatibility fixtures passed both type configurations and invoked every subpath while rejecting the root import. +- `bun run docs:exports`, `bun run docs:claims`, and `bun run docs:snippets` passed. Export coverage found 79 tuples across nine sole reference owners. Claims checked 42 current files with exactly 20 deletion-approved legacy exclusions. Snippets matched their canonical examples. +- `bun test scripts/check-doc-claims.test.ts scripts/check-doc-snippets.test.ts` passed 30 focused gate tests with no failures. +- With the Node 24.14.0 binary first on `PATH`, `bun run docs:validate` and `bun run docs:links` passed against pinned Mint 4.2.684. Build validation passed and no broken links were found. +- `bun -e 'import { parse } from "yaml"; const value=parse(await Bun.file(".github/workflows/main.yml").text()); if (!value || typeof value !== "object") throw new Error("workflow did not parse as an object"); console.log("workflow YAML parsed")'` passed. +- The explicit pre-deletion presence check found all 20 deletion-approved legacy documentation files and all three approved source READMEs. A direct count of `APPROVED_LEGACY_CLAIM_EXCLUSIONS` found exactly 20 entries. `git diff --check` passed, and status remained clean on `codex/docs-greenfield-pass`, ten commits ahead of `origin/main`, before this proof record was added. +- The first in-app browser attempt rejected localhost under its URL security policy, so no workaround was attempted through that surface. The later Computer Use review launched `bun run docs:dev`, opened the local preview in Chrome, and inspected the home page, quick start, serialization guide, Result reference, and Hono guide at the desktop viewport and a 400-pixel responsive viewport. Navigation, cards, prose, code blocks, and scrollable tables stayed readable and contained at both widths. +- The visual pass caught two template artifacts before approval: the configured logo SVGs rendered “Mint Starter Kit,” and the Hono frontmatter icon requested a remote `hono.svg` that returned 403. Removing the stale logo override made Mint render the lowercase `wellcrafted` name, and removing the broken Hono icon eliminated that request. Desktop and narrow rechecks passed with the corrected header and Hono page. +- After those two visual fixes, `bun run format:check`, `bun run lint:check`, `bun run typecheck`, `bun run build`, and `bun test` passed again; lint retained the same 12 warnings and the full suite retained 188 passing tests. `bun run docs:examples`, `bun run package:smoke`, `bun run compat:types`, `bun run compat:runtime`, `bun run docs:exports`, `bun run docs:claims`, and `bun run docs:snippets` passed again. Node 24.14.0 `bun run docs:validate` and `bun run docs:links` also passed against the final rendered files. ### Wave 11: Delete only approved obsolete owners From 50f37610c295bbf41abfec77163a9e303d11b7ad Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:01:59 -0700 Subject: [PATCH 12/13] docs: remove superseded documentation owners --- docs/core/brand-types.mdx | 467 -------- docs/core/error-system.mdx | 1004 ----------------- docs/core/result-pattern.mdx | 494 -------- docs/getting-started/installation.mdx | 262 ----- docs/getting-started/quick-start.mdx | 417 ------- docs/integrations/testing.mdx | 397 ------- docs/migration/from-try-catch.mdx | 790 ------------- docs/patterns/optional-keys.mdx | 262 ----- docs/patterns/real-world.mdx | 387 ------- docs/patterns/service-layer.mdx | 471 -------- docs/philosophy/brand-implementation.mdx | 147 --- docs/philosophy/design-principles.mdx | 329 ------ docs/philosophy/developer-experience.mdx | 633 ----------- docs/philosophy/err-null-is-ok-null.md | 184 --- docs/philosophy/error-api-evolution.mdx | 210 ---- .../for-the-pragmatic-fp-developer.mdx | 107 -- .../from-effect-to-pragmatic-errors.mdx | 175 --- docs/philosophy/production-reliability.mdx | 621 ---------- docs/philosophy/rust-inspiration.mdx | 319 ------ docs/philosophy/why-name-and-message.mdx | 172 --- scripts/check-doc-claims.ts | 90 +- ...10T012026-greenfield-documentation-pass.md | 17 +- src/README.md | 109 -- src/error/README.md | 328 ------ src/query/README.md | 849 -------------- src/result/result.test.ts | 2 +- 26 files changed, 15 insertions(+), 9228 deletions(-) delete mode 100644 docs/core/brand-types.mdx delete mode 100644 docs/core/error-system.mdx delete mode 100644 docs/core/result-pattern.mdx delete mode 100644 docs/getting-started/installation.mdx delete mode 100644 docs/getting-started/quick-start.mdx delete mode 100644 docs/integrations/testing.mdx delete mode 100644 docs/migration/from-try-catch.mdx delete mode 100644 docs/patterns/optional-keys.mdx delete mode 100644 docs/patterns/real-world.mdx delete mode 100644 docs/patterns/service-layer.mdx delete mode 100644 docs/philosophy/brand-implementation.mdx delete mode 100644 docs/philosophy/design-principles.mdx delete mode 100644 docs/philosophy/developer-experience.mdx delete mode 100644 docs/philosophy/err-null-is-ok-null.md delete mode 100644 docs/philosophy/error-api-evolution.mdx delete mode 100644 docs/philosophy/for-the-pragmatic-fp-developer.mdx delete mode 100644 docs/philosophy/from-effect-to-pragmatic-errors.mdx delete mode 100644 docs/philosophy/production-reliability.mdx delete mode 100644 docs/philosophy/rust-inspiration.mdx delete mode 100644 docs/philosophy/why-name-and-message.mdx delete mode 100644 src/README.md delete mode 100644 src/error/README.md delete mode 100644 src/query/README.md diff --git a/docs/core/brand-types.mdx b/docs/core/brand-types.mdx deleted file mode 100644 index f6ef5e8..0000000 --- a/docs/core/brand-types.mdx +++ /dev/null @@ -1,467 +0,0 @@ ---- -title: 'Brand Types' -description: 'Creating distinct types from primitives for compile-time safety' -icon: 'fingerprint' ---- - -# Brand Types: Nominal Typing in TypeScript - -Brand types (also known as opaque types or nominal types) allow you to create distinct types from primitive types, preventing accidental mixing of values that should be semantically different. This is a technique for making illegal states unrepresentable in your type system. - -## The Problem with Structural Typing - -TypeScript uses structural typing, which means types are compatible if they have the same shape: - -```typescript -// Without brand types - prone to errors -function transferMoney(fromId: string, toId: string, amount: number) { - // ... -} - -const userId = "user_123"; -const orderId = "order_456"; - -// This compiles but is semantically wrong! -transferMoney(orderId, userId, 100); // 💥 Accidentally swapped parameters -``` - -Both `userId` and `orderId` are just strings, so TypeScript can't catch this mistake. - -## Introducing Brand Types - -Brand types solve this by creating nominally distinct types: - -```typescript -import { type Brand } from "wellcrafted/brand"; - -// Create distinct branded types -type UserId = string & Brand<"UserId">; -type OrderId = string & Brand<"OrderId">; - -function transferMoney(fromId: UserId, toId: UserId, amount: number) { - // ... -} - -const userId = "user_123" as UserId; -const orderId = "order_456" as OrderId; - -// Now TypeScript catches the error! -transferMoney(orderId, userId, 100); // ❌ Type error: OrderId is not assignable to UserId -``` - -## How Brand Types Work - -The implementation is simple: - -```typescript -declare const brand: unique symbol; -export type Brand = { [brand]: { [K in T]: true } }; -``` - -This creates a phantom property using a unique symbol that exists only at the type level. The property doesn't exist at runtime, but TypeScript's type system treats each brand as a distinct type. - -The nested object structure `{ [K in T]: true }` enables brand stacking: when brands are intersected, the inner object properties merge rather than conflicting, allowing hierarchical brand relationships. - -### Why This Implementation? - - -**Source**: [`src/brand.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/src/brand.ts) • [`src/brand.test.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/src/brand.test.ts) - - -The boolean-marker pattern (`{ [K in T]: true }`) was chosen specifically to enable hierarchical brand relationships. A simpler implementation like `{ [brand]: T }` would cause intersections to collapse to `never`: - -```typescript -// ❌ Flat structure - intersections break -type Brand = { [brand]: T }; -type Parent = string & Brand<"Parent">; -type Child = Parent & Brand<"Child">; -// Child's brand becomes { [brand]: "Parent" } & { [brand]: "Child" } = never! - -// ✅ Nested structure - intersections merge -type Brand = { [brand]: { [K in T]: true } }; -type Parent = string & Brand<"Parent">; -type Child = Parent & Brand<"Child">; -// Child's brand is { [brand]: { Parent: true, Child: true } } - works! -``` - -This pattern follows similar approaches in Effect-TS, adapted with simpler boolean markers. For a deeper exploration of the design tradeoffs, see [Brand Type Implementation](/philosophy/brand-implementation). - -## Common Use Cases - -### 1. Preventing ID Mix-ups - -The most common use case is distinguishing between different types of identifiers: - -```typescript -type UserId = string & Brand<"UserId">; -type ProductId = string & Brand<"ProductId">; -type OrderId = string & Brand<"OrderId">; - -// API functions that are now type-safe -async function getUser(id: UserId): Promise { - return fetch(`/api/users/${id}`).then(r => r.json()); -} - -async function getProduct(id: ProductId): Promise { - return fetch(`/api/products/${id}`).then(r => r.json()); -} - -// Helper functions to create branded values -function toUserId(id: string): UserId { - return id as UserId; -} - -function toProductId(id: string): ProductId { - return id as ProductId; -} -``` - -### 2. Validated Data - -Use brands to mark data that has been validated or sanitized: - -```typescript -type SafeHtml = string & Brand<"SafeHtml">; -type ValidatedEmail = string & Brand<"ValidatedEmail">; -type SanitizedInput = string & Brand<"SanitizedInput">; - -// Only accepts sanitized HTML -function renderHtml(html: SafeHtml) { - element.innerHTML = html; // Safe because type guarantees sanitization -} - -// Sanitization function returns branded type -function sanitizeHtml(input: string): SafeHtml { - // ... sanitization logic ... - return sanitized as SafeHtml; -} - -// Usage -const userInput = ""; -// renderHtml(userInput); // ❌ Type error - must sanitize first -renderHtml(sanitizeHtml(userInput)); // ✅ Type-safe -``` - -### 3. Units of Measurement - -Prevent mixing incompatible units: - -```typescript -type Meters = number & Brand<"Meters">; -type Feet = number & Brand<"Feet">; -type Seconds = number & Brand<"Seconds">; - -function calculateSpeed(distance: Meters, time: Seconds): number { - return distance / time; -} - -const distance = 100 as Meters; -const wrongUnit = 328 as Feet; -const time = 10 as Seconds; - -calculateSpeed(distance, time); // ✅ Correct -// calculateSpeed(wrongUnit, time); // ❌ Type error - Feet not assignable to Meters -``` - -### 4. Secret Management - -Distinguish between sensitive and non-sensitive strings: - -```typescript -type ApiKey = string & Brand<"ApiKey">; -type DatabasePassword = string & Brand<"DatabasePassword">; -type PublicKey = string & Brand<"PublicKey">; - -function connectToApi(key: ApiKey) { - // ... -} - -function connectToDatabase(password: DatabasePassword) { - // ... -} - -// Prevents accidentally logging secrets -function logPublicInfo(info: string | PublicKey) { - console.log("Public info:", info); -} - -const apiKey = process.env.API_KEY as ApiKey; -const dbPass = process.env.DB_PASS as DatabasePassword; - -// logPublicInfo(apiKey); // ❌ Type error - prevents accidental secret exposure -``` - -## Creating Brand Type Utilities - -### Type Guards - -Create type guards for runtime validation: - -```typescript -type Email = string & Brand<"Email">; - -function isValidEmail(value: string): value is Email { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); -} - -function sendEmail(to: Email, subject: string) { - // Can safely assume 'to' is a valid email -} - -// Usage with validation -const input = "user@example.com"; -if (isValidEmail(input)) { - sendEmail(input, "Welcome!"); // TypeScript knows input is Email here -} -``` - -### Factory Functions - -Create factory functions that validate and brand values: - -```typescript -type PositiveNumber = number & Brand<"PositiveNumber">; -type NonEmptyString = string & Brand<"NonEmptyString">; - -function createPositiveNumber(value: number): Result { - if (value <= 0) { - return Err({ - name: "ValidationError", - message: "Value must be positive", - value, - }); - } - return Ok(value as PositiveNumber); -} - -function createNonEmptyString(value: string): Result { - if (value.trim().length === 0) { - return Err({ - name: "ValidationError", - message: "String cannot be empty", - value, - }); - } - return Ok(value as NonEmptyString); -} -``` - -### Conversion Functions - -Sometimes you need to convert between branded types: - -```typescript -type Meters = number & Brand<"Meters">; -type Kilometers = number & Brand<"Kilometers">; - -function metersToKilometers(meters: Meters): Kilometers { - return (meters / 1000) as Kilometers; -} - -function kilometersToMeters(km: Kilometers): Meters { - return (km * 1000) as Meters; -} -``` - -## Integration with Result Types - -Brand types work beautifully with Result types for validated data: - -```typescript -import { Result, Ok, Err } from "wellcrafted/result"; -import { type Brand } from "wellcrafted/brand"; - -type UserId = string & Brand<"UserId">; -type UserIdError = Readonly<{ name: "UserIdError"; message: string }>; - -function parseUserId(input: string): Result { - if (!input.startsWith("user_")) { - return Err({ - name: "UserIdError", - message: "User ID must start with 'user_'", - input, - }); - } - - if (input.length !== 12) { - return Err({ - name: "UserIdError", - message: "User ID must be exactly 12 characters", - input, - length: input.length, - }); - } - - return Ok(input as UserId); -} - -// Usage -const result = parseUserId("user_abc123"); -if (result.error) { - console.error("Invalid user ID:", result.error.message); -} else { - // result.data is typed as UserId - await getUser(result.data); -} -``` - -## Best Practices - -### 1. Use Descriptive Brand Names - -Choose brand names that clearly indicate the type's purpose: - -```typescript -// ❌ Too generic -type Id = string & Brand<"Id">; -type Key = string & Brand<"Key">; - -// ✅ Specific and clear -type CustomerId = string & Brand<"CustomerId">; -type EncryptionKey = string & Brand<"EncryptionKey">; -``` - -### 2. Document Brand Invariants - -Document what guarantees a branded type provides: - -```typescript -/** - * A string that has been validated to be a valid email address. - * Guaranteed to: - * - Contain exactly one @ symbol - * - Have at least one character before and after the @ - * - Have a domain with at least one dot - */ -type ValidatedEmail = string & Brand<"ValidatedEmail">; -``` - -### 3. Keep Brands at API Boundaries - -Use brands at the edges of your system where validation occurs: - -```typescript -// API endpoint validates and brands input -async function createUser(request: Request) { - const { email, password } = await request.json(); - - const emailResult = validateEmail(email); - if (emailResult.error) return badRequest(emailResult.error); - - const passwordResult = validatePassword(password); - if (passwordResult.error) return badRequest(passwordResult.error); - - // Now we have branded, validated data - return userService.create(emailResult.data, passwordResult.data); -} -``` - -### 4. Avoid Over-Branding - -Don't brand every string or number - use brands where they prevent real errors: - -```typescript -// ❌ Probably overkill -type FirstName = string & Brand<"FirstName">; -type LastName = string & Brand<"LastName">; - -// ✅ Prevents real mistakes -type UserId = string & Brand<"UserId">; -type OrderId = string & Brand<"OrderId">; -``` - -## Advanced Patterns - -### Composite Brands - -Create complex branded types from simpler ones: - -```typescript -type Latitude = number & Brand<"Latitude">; -type Longitude = number & Brand<"Longitude">; - -type GeoCoordinate = { - lat: Latitude; - lng: Longitude; -}; - -function createCoordinate(lat: number, lng: number): Result { - if (lat < -90 || lat > 90) { - return Err({ - name: "ValidationError", - message: "Latitude must be between -90 and 90", - lat, - }); - } - - if (lng < -180 || lng > 180) { - return Err({ - name: "ValidationError", - message: "Longitude must be between -180 and 180", - lng, - }); - } - - return Ok({ - lat: lat as Latitude, - lng: lng as Longitude - }); -} -``` - -### Brand Hierarchies - -Sometimes you want brands that are related but distinct: - -```typescript -type Id = string & Brand<"Id">; -type UserId = Id & Brand<"UserId">; -type AdminUserId = UserId & Brand<"AdminUserId">; - -function deleteUser(id: UserId) { - // Can delete any user -} - -function deleteSystem(id: AdminUserId) { - // Requires admin user ID -} - -const userId = "user_123" as UserId; -const adminId = "admin_456" as AdminUserId; - -deleteUser(userId); // ✅ Works -deleteUser(adminId); // ✅ Also works - AdminUserId extends UserId -// deleteSystem(userId); // ❌ Type error - needs AdminUserId -``` - -## Summary - -Brand types are a simple technique for adding nominal typing to TypeScript: - -- **Prevent errors**: Catch ID mix-ups and parameter swapping at compile time -- **Document guarantees**: Branded types communicate validation and invariants -- **Zero runtime cost**: Brands exist only in the type system -- **Composable**: Work with Result types and other patterns - -Use brand types strategically at API boundaries and for values that are easily confused but semantically different. They're one more tool in your toolkit for making illegal states unrepresentable. - -## See Also - - - - See brand types preventing bugs in authentication and file upload code - - - Learn how brand types work with Result discriminated unions - - - Use brand types for type-safe service APIs and dependency injection - - - How defineErrors and tagged errors work - - - - -Ready to prevent ID mix-ups in your code? Start with our [real-world examples](/patterns/real-world) to see brand types in action. - \ No newline at end of file diff --git a/docs/core/error-system.mdx b/docs/core/error-system.mdx deleted file mode 100644 index d771e87..0000000 --- a/docs/core/error-system.mdx +++ /dev/null @@ -1,1004 +0,0 @@ ---- -title: 'Error System Design' -description: "Understanding wellcrafted's structured, serializable error system" -icon: 'triangle-exclamation' ---- - -# Error System Design - -wellcrafted's error system is built on a simple principle: **errors should be data, not control flow**. This page explains the design philosophy, implementation details, and best practices for error handling in your applications. - - -**Why this API?** The `defineErrors` design is directly inspired by Rust's [`thiserror`](https://docs.rs/thiserror) crate: short variant names under a namespace, typed fields per variant, and a display message co-located with the definition. See [Rust's thiserror in TypeScript](/philosophy/rust-inspiration) for the full story. - - -## The TaggedError Pattern - -At the heart of wellcrafted's error system is the tagged error: a structured, serializable error representation that works with TypeScript's type system. - -### Why TaggedError? - -Traditional JavaScript errors have fundamental problems: - -1. **Serialization breaks them**: `JSON.stringify(new Error())` loses the message and stack trace -2. **No standardization**: Libraries throw strings, Error objects, custom classes, or even undefined -3. **Prototype chain complexity**: `instanceof` checks fail across different realms (iframes, workers) -4. **Poor TypeScript integration**: Can't discriminate between different error types in unions - -TaggedError solves all these issues: - -1. **JSON-serializable**: Plain objects that survive any serialization boundary -2. **Type-safe discrimination**: The `name` field acts as a discriminant for TypeScript -3. **Lightweight**: No overhead of class instantiation or prototype chains -4. **Flat structure**: Fields are spread directly on the error object, no nesting - -## The TaggedError Shape - -Every tagged error has a `name` and `message`, plus any additional fields spread flat on the object: - -```typescript -Readonly<{ name: TName; message: string } & TFields> -``` - -The minimal type for any tagged error is: - -```typescript -type AnyTaggedError = { name: string; message: string }; -``` - -### `name`: The Discriminant - -This is your error's unique identifier and the key to pattern matching, the same `.name` property every JavaScript `Error` already has. See [Why `name` and `message`](/philosophy/why-name-and-message) for why we follow this convention. Use it in `if` statements and `switch` statements to handle different error types: - -```typescript -const AppError = defineErrors({ - Validation: ({ field }: { field: string }) => ({ - message: `Validation failed for field "${field}"`, - field, - }), - Network: ({ url }: { url: string }) => ({ - message: `Network request to ${url} failed`, - url, - }), - File: ({ path }: { path: string }) => ({ - message: `File operation failed for ${path}`, - path, - }), -}); -type AppError = InferErrors; - -function handleError(error: AppError) { - switch (error.name) { - case "Validation": - // TypeScript knows this is the Validation variant - console.log("Invalid input:", error.field); - break; - case "Network": - // TypeScript knows this is the Network variant - console.log("Network failed:", error.url); - break; - case "File": - // TypeScript knows this is the File variant - console.log("File issue:", error.path); - break; - } -} -``` - -### `message`: Human-Readable Text - -When using `defineErrors`, the constructor function computes the message from the input fields; the caller never passes `message` directly: - -```typescript -import { defineErrors } from 'wellcrafted/error'; - -const AuthError = defineErrors({ - Validation: ({ email }: { email: string }) => ({ - message: `Email address "${email}" must contain an @ symbol`, - email, - }), -}); - -return AuthError.Validation({ email: userInput }); -``` - -### Fields: Flat on the Error Object - -Additional data is spread directly on the error object, not nested under a `context` property. This makes access natural and concise: - -```typescript -const ProcessingError = defineErrors({ - Process: ({ userId, timestamp, retryCount }: { userId: number; timestamp: string; retryCount: number }) => ({ - message: 'User processing failed', - userId, - timestamp, - retryCount, - }), -}); - -function processUser(id: number): Result> { - return ProcessingError.Process({ - userId: id, - timestamp: new Date().toISOString(), - retryCount: 3 - }); -} - -// Access fields directly: -// error.userId, error.timestamp, error.retryCount -``` - - -Fields should include the function's input parameters and any relevant debugging information. Since they are spread flat on the error object, you get direct access: `error.userId` instead of `error.context.userId`. - - -## The Three Tiers - -The `defineErrors` API supports three tiers of complexity: - -### Tier 1: Static Error (No Fields) - -For errors where the message is always the same and no additional data is needed: - -```typescript -const RecordingError = defineErrors({ - RecorderBusy: () => ({ - message: 'A recording is already in progress', - }), -}); - -// No arguments at the call site -RecordingError.RecorderBusy(); -// → Err({ name: 'RecorderBusy', message: 'A recording is already in progress' }) -``` - -### Tier 2: Cause-Wrapping Error - -For errors that wrap a caught error. Accept `cause: unknown` and call `extractErrorMessage` inside the message template, not at the call site: - -```typescript -const AudioError = defineErrors({ - PlaySound: ({ cause }: { cause: unknown }) => ({ - message: `Failed to play sound: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -AudioError.PlaySound({ cause: error }); -// → Err({ name: 'PlaySound', message: 'Failed to play sound: Audio context not initialized', cause: }) -``` - - -This keeps call sites clean (`{ cause: error }`) and centralizes message extraction where the message is composed. Avoid **string literal unions** like `reason: 'timeout' | 'refused' | 'dns_error'` acting as sub-discriminants inside a variant. If consumers would need to narrow on that field, each value should be its own variant. See [Avoid String Literal Unions as Sub-Discriminants](#avoid-string-literal-unions-as-sub-discriminants) below. - - -### Tier 3: Structured Data Error - -For errors that carry rich, typed debugging information: - -```typescript -const HttpError = defineErrors({ - // reason is genuinely optional enrichment: HTTP/2 dropped reason phrases, - // so many responses won't have one. - Response: ({ status, reason }: { status: number; reason?: string }) => ({ - message: `HTTP ${status}${reason ? `: ${reason}` : ''}`, - status, - reason, - }), -}); - -HttpError.Response({ status: 404 }); -// → Err({ name: 'Response', message: 'HTTP 404', status: 404 }) - -HttpError.Response({ status: 500, reason: 'Internal server error' }); -// → Err({ name: 'Response', message: 'HTTP 500: Internal server error', status: 500, reason: 'Internal server error' }) -``` - -## Wrapping Caught Errors with extractErrorMessage - -When a `catch` block hands you `unknown`, the constructor should own the conversion, not the call site. - -### Preferred: Transform Inside the Constructor - -Accept `cause: unknown`, call `extractErrorMessage(cause)` in the message template, and store the raw `cause` for programmatic access: - -```typescript -import { defineErrors, extractErrorMessage } from 'wellcrafted/error'; - -const AudioError = defineErrors({ - PlaySound: ({ cause }: { cause: unknown }) => ({ - message: `Failed to play sound: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -// Call site stays clean, just pass the raw error -try { - await audioContext.play(soundFile); -} catch (error) { - return AudioError.PlaySound({ cause: error }); -} -``` - -The resulting error carries both a human-readable `message` and the original `cause`: - -```json -{ - "name": "PlaySound", - "message": "Failed to play sound: The AudioContext was not allowed to start", - "cause": { /* original error object */ } -} -``` - -### Anti-Pattern: Transform at the Call Site - -```typescript -// Don't do this; every call site must remember to call extractErrorMessage -try { - await audioContext.play(soundFile); -} catch (error) { - return AudioError.PlaySound({ reason: extractErrorMessage(error) }); -} -``` - -This scatters presentation logic across every `catch` block instead of centralizing it in the one place that defines the message format. - -### Why - -- **Constructor owns the template.** It already decides the message format; it should also decide how raw inputs become strings. -- **Call sites pass raw data.** Callers hand over what they have; the constructor does the rest. -- **`cause` stays available.** Storing `cause: unknown` means consumers can inspect the original error programmatically, not just read a stringified version. -- **Same principle as Rust's `#[from]`.** In `thiserror`, the `#[from]` attribute tells the variant to handle conversion automatically. The call site just wraps; the definition owns the transform. - -## Error Chaining - -There is no dedicated cause step. If you need to chain errors, model `cause` as just another field: - -```typescript -import { defineErrors, type InferErrors } from 'wellcrafted/error'; - -// Define errors for each layer -const NetworkError = defineErrors({ - SocketTimeout: ({ host, port, timeout }: { host: string; port: number; timeout: number }) => ({ - message: `Socket timeout after ${timeout}ms`, - host, - port, - timeout, - }), -}); -type NetworkError = InferErrors; - -const DbError = defineErrors({ - Connection: ({ retries, cause }: { retries: number; cause: NetworkError }) => ({ - message: 'Failed to connect to database', - retries, - cause, - }), -}); -type DbError = InferErrors; - -const UserServiceError = defineErrors({ - FetchProfile: ({ userId, cause }: { userId: string; cause: DbError }) => ({ - message: 'Could not fetch user profile', - userId, - cause, - }), -}); -type UserServiceError = InferErrors; - -const ApiError = defineErrors({ - Internal: ({ endpoint, statusCode, cause }: { endpoint: string; statusCode: number; cause: UserServiceError }) => ({ - message: 'Internal server error', - endpoint, - statusCode, - cause, - }), -}); - -// Low-level network error -const networkError = NetworkError.SocketTimeout({ - host: "db.example.com", port: 5432, timeout: 5000 -}); - -// Wrapped by database layer -const dbError = DbError.Connection({ - retries: 3, cause: networkError.error -}); - -// Wrapped by service layer -const serviceError = UserServiceError.FetchProfile({ - userId: "123", cause: dbError.error -}); - -// Wrapped by API handler -const apiError = ApiError.Internal({ - endpoint: "/api/users/123", statusCode: 500, cause: serviceError.error -}); - -// The entire error chain is JSON-serializable! -console.log(JSON.stringify(apiError, null, 2)); -``` - -**The serialized output shows the complete chain:** - -```json -{ - "name": "Internal", - "message": "Internal server error", - "endpoint": "/api/users/123", - "statusCode": 500, - "cause": { - "name": "FetchProfile", - "message": "Could not fetch user profile", - "userId": "123", - "cause": { - "name": "Connection", - "message": "Failed to connect to database", - "retries": 3, - "cause": { - "name": "SocketTimeout", - "message": "Socket timeout after 5000ms", - "host": "db.example.com", - "port": 5432, - "timeout": 5000 - } - } - } -} -``` - -This chaining pattern provides: -- **Complete error trace**: See exactly how an error bubbled up through your stack -- **Layer-specific fields**: Each layer adds its own debugging information as flat fields -- **JSON-serializable**: Unlike JavaScript stack traces, this survives any serialization -- **Type-safe**: Each error in the chain maintains full TypeScript typing - -## Creating Domain-Specific Errors - -Define a set of possible errors for each domain in your application: - -```typescript -import { defineErrors, type InferError, type InferErrors } from 'wellcrafted/error'; - -// Define error factories for this domain -const FileError = defineErrors({ - NotFound: ({ path }: { path: string }) => ({ - message: `The file at path "${path}" was not found`, - path, - }), - PermissionDenied: ({ path, user }: { path: string; user: string }) => ({ - message: `Permission denied for "${path}"`, - path, - user, - }), - DiskFull: ({ volumeName }: { volumeName: string }) => ({ - message: `Disk full on volume "${volumeName}"`, - volumeName, - }), -}); - -// Union of all possible errors for this domain -type FileError = InferErrors; - -// Or extract a single variant's type -type FileNotFoundError = InferError; -``` - -## The defineErrors API - -wellcrafted provides `defineErrors`, a declarative API that eliminates boilerplate and enforces consistent error structure. Each key in the config object maps to a constructor function that returns `{ message, ...fields }`. The `name` is automatically stamped from the key, and each factory returns `Err<...>` directly, ready to use in a `Result` return. - -```typescript -import { defineErrors, type InferError, type InferErrors } from 'wellcrafted/error'; - -// Define errors with constructor functions -const AppError = defineErrors({ - // Static error: no input, fixed message - Network: () => ({ - message: 'Network request failed', - }), - - // Structured error: typed input, computed message - FileNotFound: ({ path }: { path: string }) => ({ - message: `File not found: ${path}`, - path, - }), -}); - -// Each variant is accessed via the namespace -// AppError.Network() returns Err<{ name: 'Network'; message: string }> -// AppError.FileNotFound({ path }) returns Err<{ name: 'FileNotFound'; message: string; path: string }> - -// Extract types -type AppError = InferErrors; -type FileNotFoundError = InferError; -``` - -### Constructor Functions: Define Message and Fields Together - -Each constructor function receives the input fields and returns an object with `message` plus any additional fields to spread on the error. The `name` is stamped automatically from the key. - -```typescript -const FileError = defineErrors({ - // Constructor computes message and returns fields - Write: ({ path }: { path: string }) => ({ - message: `Write failed for ${path}`, - path, - }), -}); - -FileError.Write({ path: '/etc/passwd' }); -// → Err({ name: 'Write', message: 'Write failed for /etc/passwd', path: '/etc/passwd' }) - -// For errors where the caller provides the message directly: -const GenericError = defineErrors({ - Generic: ({ message, path }: { message: string; path: string }) => ({ - message, - path, - }), -}); - -GenericError.Generic({ message: 'Failed to process file', path: '/etc/passwd' }); -``` - - -The constructor function is the single source of truth: it defines both the input shape and how the message is computed. No separate `.withFields()` or `.withMessage()` steps needed. - - -### Required Fields - -When you know what information is **always** needed: - -```typescript -const FileError = defineErrors({ - Write: ({ path }: { path: string }) => ({ - message: `Write failed for ${path}`, - path, - }), -}); - -// Fields are REQUIRED; TypeScript enforces it -FileError.Write({ path: '/etc/passwd' }); -// FileError.Write(); // Type error! fields are required -``` - -### Optional Fields (Enrichment) - -Use optional properties only for genuine enrichment: data that may not be available at the call site: - -```typescript -const ParseError = defineErrors({ - // line is genuinely optional enrichment: parsing may fail before - // a line number is identified (e.g., binary format mismatch) - Log: ({ file, line }: { file: string; line?: number }) => ({ - message: `Log parse failed: ${file}${line != null ? `:${line}` : ''}`, - file, - line, - }), -}); - -// file is always required; you always know what you're parsing -ParseError.Log({ file: 'app.ts' }); - -// line is optional enrichment; provide it when available -ParseError.Log({ file: 'app.ts', line: 42 }); -// ParseError.Log({ wrong: true }); // Type error! wrong shape -``` - -### Fields Must Be JSON-Serializable - -Fields must be JSON-serializable primitives, arrays, or nested objects. This ensures errors survive any serialization boundary. - -```typescript -// Valid JSON-serializable fields -FileError.Write({ path: '/etc/passwd' }); - -// Not allowed: Date, functions, class instances are not JSON-serializable -// FileError.Write({ createdAt: new Date(), handler: () => {} }); // Type error! -``` - -### Extracting the Error Type - -Use `InferError` to get the TypeScript type for a specific variant, or `InferErrors` to get the union of all variants: - -```typescript -import { defineErrors, type InferError, type InferErrors } from 'wellcrafted/error'; - -const HttpError = defineErrors({ - Request: ({ url, status }: { url: string; status: number }) => ({ - message: `Request to ${url} failed with ${status}`, - url, - status, - }), -}); - -// Union of all variants (useful for function signatures) -type HttpError = InferErrors; - -// Single variant type -type RequestError = InferError; -// RequestError = Readonly<{ name: 'Request'; message: string; url: string; status: number }> -``` - -### Quick Reference - -```typescript -import { defineErrors, type InferError, type InferErrors } from 'wellcrafted/error'; - -const AppError = defineErrors({ - // Tier 1: Static, no fields, no args at call site - Network: () => ({ - message: 'Network request failed', - }), - - // Tier 2: Cause-wrapping - Parse: ({ cause }: { cause: unknown }) => ({ - message: `Parse failed: ${extractErrorMessage(cause)}`, - cause, - }), - - // Tier 3: Structured data - FileNotFound: ({ path }: { path: string }) => ({ - message: `File not found: ${path}`, - path, - }), - - // Structured with optional enrichment: line number may not be available - // when parsing fails before a line is identified - Log: ({ file, line }: { file: string; line?: number }) => ({ - message: `Log parse failed: ${file}${line != null ? `:${line}` : ''}`, - file, - line, - }), -}); - -// Access variants via the namespace; each returns Err<...> directly -// AppError.Network() -// AppError.Parse({ cause: error }) -// AppError.FileNotFound({ path: '...' }) -// AppError.Log({ file: '...', line: 42 }) - -// Extract types -type AppError = InferErrors; -type FileNotFoundError = InferError; -``` - -### Using in tryAsync/trySync - -The factory pattern works with error mapping. Each variant returns `Err<...>` directly, making it a natural fit for the `catch` callback: - -```typescript -const ApiError = defineErrors({ - Fetch: ({ endpoint }: { endpoint: string }) => ({ - message: `Failed to fetch data from ${endpoint}`, - endpoint, - }), -}); - -const result = await tryAsync({ - try: () => fetch('/api/data').then(r => r.json()), - catch: () => ApiError.Fetch({ endpoint: '/api/data' }) -}); -``` - -## Serialization Benefits - -Unlike JavaScript's Error class, TaggedErrors are plain objects that serialize perfectly: - -```typescript -const ValidationError = defineErrors({ - Range: ({ field, value, min, max }: { field: string; value: number; min: number; max: number }) => ({ - message: `${field} must be between ${min} and ${max}`, - field, - value, - min, - max, - }), -}); - -const error = ValidationError.Range({ - field: "age", value: -5, min: 0, max: 150 -}); - -// Perfect serialization -const serialized = JSON.stringify(error); -console.log(serialized); -// {"name":"Range","message":"age must be between 0 and 150","field":"age","value":-5,"min":0,"max":150} - -// Perfect deserialization -const deserialized = JSON.parse(serialized); -// Still a valid TaggedError! -``` - -This enables: -- **API responses**: Send structured errors to clients -- **Logging**: Store complete error information -- **Worker communication**: Pass errors between threads -- **State persistence**: Save errors in localStorage or databases - -## Best Practices - -### 1. Include Meaningful Fields - -Always start with the function's input parameters: - -```typescript -const DbError = defineErrors({ - Query: ({ query, params, timestamp, connectionPool }: { query: string; params: unknown[]; timestamp: string; connectionPool: string }) => ({ - message: `Database query failed: ${query}`, - query, - params, - timestamp, - connectionPool, - }), -}); - -// At the call site: fields capture function inputs and debugging info -DbError.Query({ - query: 'SELECT * FROM users WHERE id = ?', // Function input - params: [userId], // Function input - timestamp: new Date().toISOString(), // Additional context - connectionPool: 'primary' // Debugging info -}); -``` - -### 2. Make Errors Specific - -Avoid generic error types: - -```typescript -// Too generic -Readonly<{ name: "Error"; message: string }> - -// Specific and actionable -Readonly<{ name: "UserNotFound"; message: string; userId: string }> -Readonly<{ name: "InvalidCredentials"; message: string; username: string }> -Readonly<{ name: "SessionExpired"; message: string; sessionId: string }> -``` - -### 3. Avoid String Literal Unions as Sub-Discriminants - -If you find yourself adding a field like `reason: 'timeout' | 'refused' | 'dns'` inside a single variant, that field is acting as a second discriminant, duplicating what variant names already do. Split into separate variants instead. - -This is a common mistake, especially if you are coming from a codebase where errors were loosely typed strings. The instinct to group related failures under one name is natural, but it forces consumers into double narrowing (`switch` on `name`, then `if` on `reason`) and often leads to dishonest optional fields. - -```typescript -// Avoid: sub-discriminant inside a variant -const NetworkError = defineErrors({ - Request: ({ reason }: { reason: 'timeout' | 'refused' | 'dns' }) => ({ - message: `Request failed: ${reason}`, - reason, - }), -}); - -// Prefer: each failure is its own variant -const NetworkError = defineErrors({ - Timeout: ({ duration }: { duration: number }) => ({ - message: `Request timed out after ${duration}ms`, - duration, - }), - ConnectionRefused: ({ host }: { host: string }) => ({ - message: `Connection refused by ${host}`, - host, - }), - DnsFailure: ({ hostname }: { hostname: string }) => ({ - message: `DNS lookup failed for ${hostname}`, - hostname, - }), -}); -``` - -Freeform `string` fields for metadata are fine; the anti-pattern is specifically **string literal unions** that consumers would switch on. For wrapping caught errors, prefer `cause: unknown` with `extractErrorMessage(cause)` inside the message template. - -### 3b. Avoid Conditional Logic on Factory Inputs - -A related smell: if your constructor uses `if`/`switch` on its own inputs to decide what message to produce, the variant is doing double duty. Each branch is really a separate error; flatten the keys. - -```typescript -// Avoid: if/switch inside the constructor signals multiple errors hiding in one variant -const FormError = defineErrors({ - Validation: ({ field, value, receivedType }: { field?: string; value?: string; receivedType?: string }) => ({ - message: (() => { - if (receivedType) return "Invalid form data"; - if (field === 'email') return "Please enter a valid email address"; - if (field === 'password') return "Password must be at least 8 characters"; - if (field === 'confirmPassword') return "Passwords do not match"; - return "Validation failed"; - })(), - field, - value, - receivedType, - }), -}); -``` - -The problems are the same as string literal unions, just harder to spot: -- **Dishonest optionals**: `field`, `value`, and `receivedType` are all optional because no single call site needs all of them. That's a sign they belong on different variants. -- **Hidden branching**: Consumers can't discriminate on `name` alone; they'd need to inspect `field` or `receivedType` to know which validation failed. -- **Untypeable messages**: TypeScript can't narrow the message or fields based on which branch ran. - -Flatten into separate variants with honest, required fields, and split into separate namespaces when errors serve different purposes: - -```typescript -// Prefer: each validation failure is its own variant with exactly the fields it needs. -// Validation and submission are separate concerns, so they get separate namespaces. -const ValidationError = defineErrors({ - InvalidEmail: ({ value }: { value: string }) => ({ - message: "Please enter a valid email address", - value, - }), - WeakPassword: ({ value }: { value: string }) => ({ - message: "Password must be at least 8 characters", - value, - }), - PasswordMismatch: () => ({ - message: "Passwords do not match", - }), - InvalidFormData: ({ receivedType }: { receivedType: string }) => ({ - message: "Invalid form data", - receivedType, - }), -}); -type ValidationError = InferErrors; - -const SubmissionError = defineErrors({ - SubmissionFailed: ({ email }: { email: string }) => ({ - message: `Failed to create account for ${email}`, - email, - }), -}); -type SubmissionError = InferErrors; - -// TypeScript unions compose them at the function boundary: -function handleForm(data: unknown): Result { ... } -``` - -**The rule of thumb**: if a constructor branches on its inputs to decide the message, each branch should be its own variant. The variant name *is* the discriminant; don't rebuild one inside the constructor body. - -### 4. Use Union Types for Function Signatures - -Make all possible errors visible: - -```typescript -type AuthError = InferErrors; - -function authenticateUser( - credentials: Credentials -): Result { - // Implementation makes it clear what can go wrong -} -``` - -### 5. Handle Errors at the Right Level - -Transform errors where you can add context: - -```typescript -const UserServiceError = defineErrors({ - FetchProfile: ({ userId, cause }: { userId: string; cause: DbError }) => ({ - message: 'Failed to fetch user profile', - userId, - cause, - }), -}); - -async function getUserProfile(userId: string): Result> { - const dbResult = await queryDatabase(`SELECT * FROM users WHERE id = ?`, [userId]); - - if (dbResult.error) { - return UserServiceError.FetchProfile({ - userId, - cause: dbResult.error - }); - } - - return Ok(transformToProfile(dbResult.data)); -} -``` - -### 6. Design for Debugging - -Structure your errors to answer these questions: -- **What went wrong?** (message) -- **Where did it happen?** (name) -- **What data caused it?** (fields) -- **What was the root cause?** (cause field if chained) - -```typescript -const PaymentError = defineErrors({ - Processing: ({ orderId, amount, cardLast4, cause }: { orderId: string; amount: number; cardLast4: string; cause: CardValidationError }) => ({ - message: 'Payment processing failed', - orderId, - amount, - cardLast4, - cause, - }), -}); - -function processPayment(order: Order, card: Card): Result> { - const validation = validateCard(card); - if (validation.error) { - return PaymentError.Processing({ - orderId: order.id, - amount: order.total, - cardLast4: card.number.slice(-4), - cause: validation.error - }); - } - // ... rest of implementation -} -``` - -## Error Handling Patterns - -### Early Return Pattern - -The most common and readable pattern: - -```typescript -async function createOrder(input: OrderInput): Promise> { - const userResult = await getUser(input.userId); - if (userResult.error) return userResult; - - const validationResult = validateOrderInput(input); - if (validationResult.error) return validationResult; - - const inventoryResult = await checkInventory(input.items); - if (inventoryResult.error) return inventoryResult; - - // All checks passed, create the order - return Ok(await saveOrder({ - user: userResult.data, - items: inventoryResult.data, - ...validationResult.data - })); -} -``` - -### Error Aggregation - -When you need to collect multiple errors: - -```typescript -const FormError = defineErrors({ - // value is genuinely optional enrichment: sensitive fields like - // passwords should not include the value in error output - Validation: ({ field, value }: { field: string; value?: string }) => ({ - message: `Validation failed for field "${field}"`, - field, - value, - }), -}); -type FormError = InferErrors; - -function validateForm(input: FormInput): Result { - const validationErrors: FormError[] = []; - - if (!input.email?.includes('@')) { - validationErrors.push(FormError.Validation({ field: 'email', value: input.email }).error!); - } - - if (!input.password || input.password.length < 8) { - validationErrors.push(FormError.Validation({ field: 'password' }).error!); - } - - if (validationErrors.length > 0) { - return Err(validationErrors); - } - - return Ok(input as ValidatedForm); -} -``` - -### Error Recovery - -Implement retry logic with typed errors: - -```typescript -const NetworkError = defineErrors({ - Request: ({ url, attempt, maxRetries }: { url: string; attempt: number; maxRetries: number }) => ({ - message: `Request failed (attempt ${attempt}/${maxRetries})`, - url, - attempt, - maxRetries, - }), -}); -type NetworkError = InferErrors; - -async function fetchWithRetry( - url: string, - maxRetries = 3 -): Promise> { - let lastError: NetworkError | null = null; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - const result = await tryAsync({ - try: (): Promise => fetch(url).then(r => r.json()), - catch: () => NetworkError.Request({ url, attempt, maxRetries }) - }); - - if (result.data) return result; - - lastError = result.error; - - // Exponential backoff - if (attempt < maxRetries) { - await new Promise(resolve => - setTimeout(resolve, Math.pow(2, attempt) * 1000) - ); - } - } - - return Err(lastError!); -} -``` - -## Integration with Result Type - -TaggedErrors are designed to work with the Result type: - -```typescript -import { Result, Ok, Err, tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferError, type InferErrors } from "wellcrafted/error"; - -const UserError = defineErrors({ - DatabaseFailure: ({ userId }: { userId: string }) => ({ - message: `Database lookup failed for user ${userId}`, - userId, - }), - NotFound: ({ userId }: { userId: string }) => ({ - message: `User with ID ${userId} not found`, - userId, - }), -}); -type UserError = InferErrors; - -async function getUser(id: string): Promise> { - const result = await tryAsync({ - try: () => database.users.findById(id), - catch: () => UserError.DatabaseFailure({ userId: id }) - }); - - if (result.error) return result; - - if (!result.data) { - return UserError.NotFound({ userId: id }); - } - - return Ok(result.data); -} -``` - -## Summary - -The TaggedError system transforms error handling from an afterthought to a first-class concern: - -- **Structured**: Every error has a consistent shape with `name` and `message` -- **Flat**: Fields are spread directly on the error object, no nesting -- **Serializable**: Works across all JavaScript boundaries -- **Type-safe**: Full TypeScript discrimination support -- **Debuggable**: Rich fields for troubleshooting -- **Composable**: Errors can be chained via cause fields - -By treating errors as data rather than control flow, you gain predictability, testability, and maintainability in your error handling. - -## See Also - - - - Understand how TaggedErrors work with the Result discriminated union - - - See TaggedErrors in production authentication, validation, and API code - - - Learn error transformation patterns in service architecture - - - How Rust's enum-based error handling inspired the defineErrors API - - - - -Ready to see TaggedErrors in action? Explore our [real-world patterns](/patterns/real-world) or check out the [service layer guide](/patterns/service-layer). - diff --git a/docs/core/result-pattern.mdx b/docs/core/result-pattern.mdx deleted file mode 100644 index 64eafa0..0000000 --- a/docs/core/result-pattern.mdx +++ /dev/null @@ -1,494 +0,0 @@ ---- -title: 'Result Pattern Deep Dive' -description: 'A comprehensive guide to the Result pattern and discriminated unions' -icon: 'code-branch' ---- - -# The Result Pattern: A Deep Dive - -The Result pattern is the heart of wellcrafted. This page provides a comprehensive understanding of how it works, why it's designed this way, and how to leverage its full power. - -## Understanding the Type System - -### The Discriminated Union - -At its core, a `Result` is a discriminated union of two possible states: - -```typescript -type Result = Ok | Err -``` - -But what makes this a *discriminated* union? Let's examine the structure: - -```typescript -type Ok = { data: T; error: null }; -type Err = { error: E; data: null }; -``` - -The key insight: **`error` is the reliable discriminant**. TypeScript narrows types when you check the error side: - -- `error === null` → **Ok**: TypeScript knows `data` is `T` -- `error !== null` → **Err**: TypeScript knows `error` is `E` - -The `data` side is convenient to read after narrowing, but it is not a safe discriminator in every case because `Ok(null)` is valid. - -### Pro Tip: Prefer Checking `error` - -In practice, we recommend checking `error` first. Here's why: - -```typescript -// Prefer the exact null check: -if (result.error !== null) { /* handle error */ } -if (result.data === null) { /* handle error */ } // Wrong for Ok - -// But what about Ok? -const result: Result = Ok(null); -// result = { data: null, error: null } - -if (!result.data) { - // ❌ We'd wrongly think this is an error! - // But it's actually a successful Ok -} - -if (result.error !== null) { - // ✅ This correctly identifies it as Ok - // error is null, so we skip this branch -} -``` - -The edge case is `Ok`, when your success value is intentionally `null`. In this case, `data === null` doesn't mean failure; it means success with a null value. - -**Bottom line**: Check `error` first; it works reliably in every scenario, including `Ok`. Checking `data` works great when you know your success type is non-nullable, but `error` is the safer, more consistent choice. - -### Control-Flow Analysis in Action - -TypeScript's control-flow analysis understands this pattern deeply: - -```typescript -function processResult(result: Result) { - // At this point, result could be either Ok or Err - - if (result.error === null) { - // TypeScript has narrowed: result is Ok - console.log(result.data); // ✅ data is definitely T - console.log(result.error); // ✅ error is definitely null - } else { - // TypeScript has narrowed: result is Err - console.log(result.error); // ✅ error is definitely E - console.log(result.data); // ✅ data is definitely null - } -} -``` - -This isn't just type checking - it's type *narrowing*. TypeScript eliminates impossible states from consideration. - -### The Tiebreaker Rule - -When validating external data (like API responses), you might encounter objects with both `data` and `error` present: - -```typescript -// External API returns this ambiguous shape: -const response = { data: "value", error: "Something went wrong" }; - -// wellcrafted treats this as Err - error takes precedence -// The error property is the sole discriminant, so: -// error !== null → it's an Err (regardless of data value) -``` - -This "error wins" behavior provides a consistent rule: if there's an error, treat it as a failure. This is especially useful when working with APIs that might populate both fields. - -```typescript -// The constructors enforce clean states: -const success = Ok("value"); // { data: "value", error: null } -const failure = Err("error"); // { data: null, error: "error" } - -// But when parsing external data, both-present is valid (treated as Err) -``` - -### The Both-Null Case - -What about `{ data: null, error: null }`? Following the "error is the sole discriminant" rule: - -```typescript -const bothNull = { data: null, error: null }; - -// error === null → this is Ok -// It's a successful result where the success value happens to be null -``` - -This is useful for operations that succeed but intentionally return nothing, like a cache lookup that found nothing (success, but null data) versus a cache lookup that failed (error). - -## Why Not Boolean Discriminators? - -You might wonder why we don't use a pattern like: - -```typescript -type Result = - | { success: true; value: T } - | { success: false; error: E }; -``` - -Our design has several advantages: - -### 1. Direct Property Access - -```typescript -// Our approach -const { data, error } = result; -if (error !== null) handleError(error); -else useData(data); - -// Boolean approach -if (result.success) { - useData(result.value); // Extra property access -} else { - handleError(result.error); // Extra property access -} -``` - -### 2. Familiar Destructuring - -Modern JavaScript developers are familiar with this pattern from libraries like Supabase: - -```typescript -const { data, error } = await supabase - .from('users') - .select('*'); -``` - -### 3. Simplified Mental Model - -"If there's an error, handle it; otherwise, use the data" is a natural way to think about operations. - -## Advanced Type Narrowing - -### Using Type Predicates - -While simple null checks work great, we also provide type guard functions: - -```typescript -import { isOk, isErr } from "wellcrafted/result"; - -function processUser(result: Result) { - if (isOk(result)) { - // result.data is User - console.log(`Hello, ${result.data.name}!`); - } else { - // result.error is ApiError - logError(result.error); - } -} -``` - -### Exhaustive Pattern Matching - -With discriminated unions, a `never` check in `default` turns a missing case into a compile error: - -```typescript -type AuthError = Readonly<{ name: "AuthError"; message: string }>; -type NetworkError = Readonly<{ name: "NetworkError"; message: string }>; -type ValidationError = Readonly<{ name: "ValidationError"; message: string; field: string }>; - -type AppError = AuthError | NetworkError | ValidationError; - -function handleError(error: AppError): string { - switch (error.name) { - case "AuthError": - return "Please log in again"; - case "NetworkError": - return "Check your internet connection"; - case "ValidationError": - return `Invalid ${error.field}`; - default: { - // add a variant without a case and this line stops compiling - const _exhaustive: never = error; - return _exhaustive; - } - } -} -``` - -A plain `switch` does not enforce this on its own; the `never` assignment in `default` is what opts you in. - -## Working with Results - -### Early Returns Pattern - -The most common pattern is early return on errors: - -```typescript -async function processOrder(orderId: string): Promise> { - const orderResult = await fetchOrder(orderId); - if (orderResult.error) return orderResult; - - const validationResult = validateOrder(orderResult.data); - if (validationResult.error) return validationResult; - - const paymentResult = await processPayment(validationResult.data); - if (paymentResult.error) return paymentResult; - - return Ok(paymentResult.data); -} -``` - -### Transforming Results - -Sometimes you need to transform the success value: - -```typescript -function getUserName(userId: string): Result { - const userResult = getUser(userId); - - if (userResult.error) { - return userResult; // Propagate the error - } - - return Ok(userResult.data.name); // Transform the success value -} -``` - -### Combining Results - -When you have multiple Results to combine: - -```typescript -function validateUserInput(input: { - email?: string; - age?: string; -}): Result { - const errors: ValidationError[] = []; - - const emailResult = validateEmail(input.email); - if (emailResult.error) errors.push(emailResult.error); - - const ageResult = validateAge(input.age); - if (ageResult.error) errors.push(ageResult.error); - - if (errors.length > 0) { - return Err(errors); - } - - return Ok({ - email: emailResult.data!, - age: ageResult.data! - }); -} -``` - -## Result Pattern Best Practices - -### 1. Make Errors Specific - -```typescript -// ❌ Too generic -Readonly<{ name: "Error"; message: string }> - -// ✅ Specific and actionable -Readonly<{ name: "UserNotFoundError"; message: string }> -Readonly<{ name: "InvalidCredentialsError"; message: string }> -Readonly<{ name: "SessionExpiredError"; message: string }> -``` - -### 2. Use Union Types for Multiple Errors - -```typescript -function authenticateUser( - credentials: Credentials -): Result { - // Implementation -} -``` - -### 3. Propagate Errors Naturally - -```typescript -async function createPost(userId: string, content: string) { - // Don't catch and re-throw - just propagate! - const userResult = await getUser(userId); - if (userResult.error) return userResult; - - const post = await savePost(userResult.data, content); - return post; -} -``` - -### 4. Transform Errors at Boundaries - -```typescript -// Low-level function returns specific error -async function queryDatabase(sql: string): Result { - // ... -} - -// Service layer transforms to domain error -async function getUser(id: string): Result { - const result = await queryDatabase(`SELECT * FROM users WHERE id = ${id}`); - - if (result.error !== null) { - return Err({ - name: "UserServiceError", - message: "Failed to fetch user", - userId: id, - }); - } - - return Ok(transformRowToUser(result.data[0])); -} -``` - -## The Philosophy Behind Results - -### Errors as Values - -In traditional JavaScript, errors are thrown - they're control flow: - -```javascript -try { - const user = getUser(id); // Might throw - updateUser(user); // Might throw -} catch (error) { - // What threw? What type is error? -} -``` - -With Results, errors are values - they're data: - -```typescript -const userResult = getUser(id); -if (userResult.error) { - // We know exactly what error this is - return userResult; -} - -const updateResult = updateUser(userResult.data); -if (updateResult.error) { - // And what error this is - return updateResult; -} -``` - -### Composition Over Exception Handling - -Results compose naturally: - -```typescript -const result = await fetchUser(id) - .then(r => r.error ? r : validateUser(r.data)) - .then(r => r.error ? r : saveUser(r.data)); -``` - -Compare this to try-catch composition: - -```javascript -try { - try { - const user = await fetchUser(id); - try { - const validated = validateUser(user); - return await saveUser(validated); - } catch (saveError) { - // Handle save error - } - } catch (validateError) { - // Handle validation error - } -} catch (fetchError) { - // Handle fetch error -} -``` - -## Advanced Patterns - -### Result Chaining - -While wellcrafted doesn't provide built-in chaining methods, you can build them: - -```typescript -class ResultChain { - constructor(private result: Result) {} - - map(fn: (value: T) => U): ResultChain { - if (this.result.error) { - return new ResultChain(Err(this.result.error)); - } - return new ResultChain(Ok(fn(this.result.data))); - } - - flatMap(fn: (value: T) => Result): ResultChain { - if (this.result.error) { - return new ResultChain(Err(this.result.error)); - } - return new ResultChain(fn(this.result.data)); - } - - unwrap(): Result { - return this.result; - } -} - -// Usage -const result = new ResultChain(fetchUser(id)) - .flatMap(user => validateUser(user)) - .map(user => user.name) - .unwrap(); -``` - -### Async Result Pipelines - -For complex async operations: - -```typescript -async function pipeline( - initial: T, - ...operations: Array<(value: T) => Promise>> -): Promise> { - let current = Ok(initial); - - for (const operation of operations) { - if (current.error) return current; - current = await operation(current.data); - } - - return current; -} - -// Usage -const result = await pipeline( - userData, - validateUserData, - enrichUserData, - saveUserData -); -``` - -## Summary - -The Result pattern transforms error handling from an afterthought to a first-class concern in your code. By making errors explicit in function signatures, you: - -- Eliminate surprise runtime errors -- Get compile-time guarantees about error handling -- Make your code's behavior more predictable -- Enable better tooling and autocomplete - -The discriminated union design with `null` as the discriminant provides the perfect balance of simplicity, performance, and type safety. - -## See Also - - - - Learn how TaggedErrors work with Results for structured error handling - - - See complete examples using Results in production applications - - - Build services using Result-returning functions - - - Combine Results with brand types for maximum type safety - - - - -Ready to see the Result pattern in action? Start with our [Quick Start guide](/getting-started/quick-start) for hands-on examples. - diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx deleted file mode 100644 index fe93954..0000000 --- a/docs/getting-started/installation.mdx +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: 'Installation' -description: 'Get wellcrafted up and running in your project' -icon: 'download' ---- - -# Installation - -wellcrafted is available on npm and works with any TypeScript project. It has zero dependencies and is designed to be lightweight and tree-shakeable. - -## Requirements - -- **TypeScript**: 5.0 or higher (for `const` type parameters) -- **Module System**: ES Modules only (ESM) - -## Package Manager Installation - - - - ```bash - npm install wellcrafted - ``` - - - ```bash - yarn add wellcrafted - ``` - - - ```bash - pnpm add wellcrafted - ``` - - - ```bash - bun add wellcrafted - ``` - - - -## TypeScript Configuration - -wellcrafted is written in TypeScript and includes all type definitions. For the best experience, ensure your `tsconfig.json` includes: - -```json -{ - "compilerOptions": { - "strict": true, // Recommended for best type safety - "strictNullChecks": true, // Required for discriminated unions - "esModuleInterop": true, // For clean imports - "moduleResolution": "node", // Standard resolution - "lib": ["ES2020"] // Or higher - } -} -``` - -## Importing - -wellcrafted uses subpath exports for optimal tree-shaking. Import only what you need: - -```typescript -// Result type utilities -import { Result, Ok, Err, tryAsync, trySync } from "wellcrafted/result"; - -// Error utilities -import { defineErrors, type InferErrors, extractErrorMessage } from "wellcrafted/error"; - -// Brand type utilities -import { type Brand } from "wellcrafted/brand"; -``` - - -Avoid importing from the package root (`wellcrafted`) as this will import all modules and prevent tree-shaking. - - -## Module System - -wellcrafted ships as ES Modules only. Import from the subpath exports: - -```typescript -import { Ok, Err } from "wellcrafted/result"; - -export async function fetchUser(id: string) { - try { - const user = await api.getUser(id); - return Ok(user); - } catch (error) { - return Err(error); - } -} -``` - -There is no `require()` entry point. From a CommonJS module, load wellcrafted with a dynamic `import()`. - -## Bundle Size - -wellcrafted is designed to be lightweight: - -- **Full library**: ~2KB minified + gzipped -- **Result module only**: ~0.8KB minified + gzipped -- **Error module only**: ~0.3KB minified + gzipped -- **Brand module only**: ~0.1KB minified + gzipped - -When using modern bundlers with tree-shaking, you only pay for what you use. - -## Framework Integration - -wellcrafted works with any JavaScript framework or runtime: - -### Next.js -```typescript -// app/actions.ts -"use server"; - -import { tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -const ServerError = defineErrors({ - CreateUser: ({ timestamp }: { timestamp: number }) => ({ - message: "Failed to create user", - timestamp, - }), -}); -type ServerError = InferErrors; - -export async function createUser(data: FormData) { - return tryAsync({ - try: async () => { - // Your server action logic - }, - catch: () => ServerError.CreateUser({ timestamp: Date.now() }) - }); -} -``` - -### React -```typescript -// hooks/useUser.ts -import { useState, useEffect } from "react"; -import { Result } from "wellcrafted/result"; - -type ApiError = Readonly<{ name: "ApiError"; message: string }>; - -export function useUser(id: string) { - const [result, setResult] = useState | null>(null); - - useEffect(() => { - fetchUser(id).then(setResult); - }, [id]); - - return result; -} -``` - -### Express/Node.js -```typescript -// routes/users.ts -import { Router } from "express"; -import { tryAsync } from "wellcrafted/result"; - -const router = Router(); - -router.get("/:id", async (req, res) => { - const result = await tryAsync({ - try: () => getUserById(req.params.id), - catch: (error) => Err({ - name: "DatabaseError", - message: "Failed to fetch user", - userId: req.params.id, - }) - }); - - if (result.error) { - return res.status(500).json({ error: result.error }); - } - - res.json(result.data); -}); -``` - -## Development Setup - -If you're contributing to wellcrafted or want to build from source: - -```bash -# Clone the repository -git clone https://github.com/wellcrafted-dev/wellcrafted.git -cd wellcrafted - -# Install dependencies -npm install - -# Run tests -npm test - -# Build the library -npm run build - -# Run in watch mode -npm run dev -``` - -## Verification - -After installation, you can verify everything is working: - -```typescript -// test-wellcrafted.ts -import { Ok, Err, isOk } from "wellcrafted/result"; - -const result = Ok("Hello, wellcrafted!"); -console.log(isOk(result)); // true -console.log(result.data); // "Hello, wellcrafted!" -``` - -Run with: -```bash -npx tsx test-wellcrafted.ts -``` - -## Next Steps - -Now that you have wellcrafted installed: - - - - Learn the basics in 5 minutes - - - How defineErrors and tagged errors work - - - See real-world implementations - - - -## Troubleshooting - -### TypeScript Errors - -If you see TypeScript errors, ensure: -- You're using TypeScript 4.5 or higher -- `strictNullChecks` is enabled in your tsconfig.json -- You're importing from the correct subpaths - -### Module Resolution Issues - -If imports aren't resolving: -- Check your `moduleResolution` is set to "node" or "bundler" -- Ensure you're using subpath imports like `wellcrafted/result` -- Try clearing your node_modules and reinstalling - -### Bundle Size Concerns - -If your bundle is larger than expected: -- Ensure you're importing from subpaths, not the root -- Check that your bundler has tree-shaking enabled -- Use bundle analyzer tools to verify what's included - - -Need help? Check our [GitHub issues](https://github.com/wellcrafted-dev/wellcrafted/issues) or start a [discussion](https://github.com/wellcrafted-dev/wellcrafted/discussions). - \ No newline at end of file diff --git a/docs/getting-started/quick-start.mdx b/docs/getting-started/quick-start.mdx deleted file mode 100644 index 4bcd38e..0000000 --- a/docs/getting-started/quick-start.mdx +++ /dev/null @@ -1,417 +0,0 @@ ---- -title: 'Quick Start' -description: 'Learn wellcrafted in 5 minutes with practical examples' -icon: 'rocket' ---- - -# Quick Start - -This guide will get you productive with wellcrafted in just 5 minutes. We'll cover the essential patterns you'll use every day. - -## Your First Result - -Let's start by replacing a throwing function with a Result-returning one: - -```typescript -import { Result, Ok } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -// Define your error with defineErrors -// The const name IS the namespace; variant names are short -const ParseError = defineErrors({ - Parse: () => ({ - message: 'Input is not a valid number', - }), -}); -type ParseError = InferErrors; - -// Instead of throwing... -function parseNumberOld(input: string): number { - const num = parseInt(input); - if (isNaN(num)) { - throw new Error("Invalid number"); - } - return num; -} - -// Return a Result! -function parseNumber(input: string): Result { - const num = parseInt(input); - - if (isNaN(num)) { - // Each factory returns Err<...> directly - return ParseError.Parse(); - } - - return Ok(num); -} -``` - -## Handling Results - -Now let's use our Result-returning function: - -```typescript -const result = parseNumber("42"); - -// Pattern 1: Destructuring (recommended) -const { data, error } = result; -if (error) { - console.error(`Error: ${error.message}`); -} else { - console.log(`The number is ${data}`); -} - -// Pattern 2: Type guards -import { isOk, isErr } from "wellcrafted/result"; - -if (isOk(result)) { - console.log(`Success: ${result.data}`); -} else { - console.error(`Failed: ${result.error.message}`); -} -``` - -## Wrapping Existing Code - -Most code you work with throws exceptions. Here's how to wrap it: - -### Synchronous Functions - -```typescript -import { trySync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -const JsonError = defineErrors({ - Parse: ({ text }: { text: string }) => ({ - message: `Failed to parse JSON: ${text}`, - text, - }), -}); -type JsonError = InferErrors; - -function parseJson(text: string): Result { - return trySync({ - try: () => JSON.parse(text), - catch: () => JsonError.Parse({ text: text.substring(0, 100) }) // Truncate for logging - }); -} - -// Usage -const { data, error } = parseJson<{ name: string }>('{"name": "Alice"}'); -if (error) { - console.error("Parse failed:", error.message); -} else { - console.log("Parsed:", data.name); // TypeScript knows data exists! -} -``` - -### Asynchronous Functions - -```typescript -import { Result, Ok } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -// Define specific error variants for different failure modes -const ApiError = defineErrors({ - NetworkError: ({ endpoint }: { endpoint: string }) => ({ - message: `Network request failed: ${endpoint}`, - endpoint, - }), - HttpError: ({ endpoint, statusCode }: { endpoint: string; statusCode: number }) => ({ - message: `HTTP ${statusCode} from ${endpoint}`, - endpoint, - statusCode, - }), -}); -type ApiError = InferErrors; - -interface User { - id: number; - name: string; - email: string; -} - -async function fetchUser(id: number): Promise> { - const endpoint = `/api/users/${id}`; - - try { - const response = await fetch(endpoint); - - if (!response.ok) { - // HTTP error: we have the status code - return ApiError.HttpError({ endpoint, statusCode: response.status }); - } - - return Ok(await response.json() as User); - } catch { - // fetch threw: network failure, no status code available - return ApiError.NetworkError({ endpoint }); - } -} - -// Usage -const userResult = await fetchUser(123); -if (userResult.error) { - console.error("API call failed:", userResult.error); -} else { - console.log("User:", userResult.data.name); -} -``` - -## Real-World Example: Form Validation - -Here's a complete example showing how Results improve a common task: - -```typescript -import { Result, Ok, tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -// Separate error namespaces for validation vs submission. -// Each namespace groups related errors; TypeScript unions -// compose them at function boundaries; no wrapper type needed. -const ValidationError = defineErrors({ - InvalidType: ({ receivedType }: { receivedType: string }) => ({ - message: `Expected form data, received ${receivedType}`, - receivedType, - }), - InvalidEmail: ({ value }: { value: string }) => ({ - message: "Please enter a valid email address", - field: "email" as const, - value, - }), - WeakPassword: () => ({ - message: "Password must be at least 8 characters", - field: "password" as const, - }), - PasswordMismatch: () => ({ - message: "Passwords do not match", - field: "confirmPassword" as const, - }), -}); -type ValidationError = InferErrors; - -const SubmissionError = defineErrors({ - Submission: ({ email }: { email: string }) => ({ - message: `Failed to create account for ${email}`, - email, - }), -}); -type SubmissionError = InferErrors; - -// Form data interface -interface SignupForm { - email: string; - password: string; - confirmPassword: string; -} - -// Validation function: only returns ValidationError -function validateSignupForm(data: unknown): Result { - if (!data || typeof data !== 'object') { - return ValidationError.InvalidType({ receivedType: typeof data }); - } - - const { email, password, confirmPassword } = data as any; - - if (!email || !email.includes('@')) { - return ValidationError.InvalidEmail({ value: email ?? '' }); - } - - if (!password || password.length < 8) { - return ValidationError.WeakPassword(); - } - - if (password !== confirmPassword) { - return ValidationError.PasswordMismatch(); - } - - return Ok({ email, password, confirmPassword }); -} - -// Submission function: only returns SubmissionError -async function submitSignup( - form: SignupForm -): Promise> { - return tryAsync({ - try: async () => { - const response = await fetch('/api/signup', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(form) - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(error); - } - - return response.json(); - }, - catch: () => SubmissionError.Submission({ email: form.email }) - }); -} - -// Using it all together: the union happens naturally at the call site -async function handleSignup(formData: unknown) { - const { data: validForm, error: validationError } = validateSignupForm(formData); - - if (validationError) { - console.error("Validation failed:", validationError.message); - if ('field' in validationError) { - highlightField(validationError.field); - } - return; - } - - const { data: signup, error: submitError } = await submitSignup(validForm); - - if (submitError) { - console.error("Signup failed:", submitError.message); - showToast("Unable to create account. Please try again."); - return; - } - - console.log("Account created:", signup.userId); - redirectToDashboard(); -} -``` - -## Using Brand Types - -Prevent common mistakes with branded types: - -```typescript -import { type Brand } from "wellcrafted/brand"; - -// Create branded ID types -type UserId = string & Brand<"UserId">; -type PostId = string & Brand<"PostId">; - -// Functions that require specific types -async function deletePost(userId: UserId, postId: PostId) { - // TypeScript ensures you can't mix up the parameters - return api.delete(`/users/${userId}/posts/${postId}`); -} - -// Create branded values -const userId = "user_123" as UserId; -const postId = "post_456" as PostId; - -// This works -await deletePost(userId, postId); - -// This fails at compile time! -// await deletePost(postId, userId); // ❌ Type error -``` - -## Chaining Operations - -Here's how to chain multiple Result-returning operations: - -```typescript -async function processOrder(orderId: string) { - // Fetch the order - const orderResult = await fetchOrder(orderId); - if (orderResult.error) return orderResult; - - // Validate the order - const validationResult = validateOrder(orderResult.data); - if (validationResult.error) return validationResult; - - // Process payment - const paymentResult = await processPayment(validationResult.data); - if (paymentResult.error) return paymentResult; - - // Ship the order - return shipOrder(paymentResult.data); -} - -// Usage -const result = await processOrder("order_123"); -if (result.error) { - // Handle any error from the chain - switch (result.error.name) { - case "OrderNotFound": - show404Page(); - break; - case "Validation": - showValidationMessage(result.error.message); - break; - case "Payment": - showPaymentRetry(); - break; - case "Shipping": - notifyWarehouse(result.error); - break; - } -} else { - showSuccessMessage("Order processed successfully!"); -} -``` - -## What You've Learned - -In just 5 minutes, you've learned how to: - -- Create Result-returning functions -- Handle Results with destructuring -- Wrap throwing code with trySync/tryAsync -- Build type-safe validation -- Use brand types to prevent errors -- Chain Result-returning operations - -## Next Steps - - - - Master advanced Result techniques - - - Learn about structured error design - - - See production-ready code - - - -## Quick Reference - -```typescript -// Import what you need -import { Result, Ok, Err, tryAsync, trySync, isOk, isErr } from "wellcrafted/result"; -import { defineErrors, type InferErrors, type InferError } from "wellcrafted/error"; -import { type Brand } from "wellcrafted/brand"; - -// Create Results -Ok(value) // Success -Err(error) // Failure - -// Handle Results -const { data, error } = result; -if (error) { /* handle */ } else { /* use data */ } - -// Wrap unsafe code -trySync({ try: () => risky(), catch: (e) => ... }) -tryAsync({ try: async () => risky(), catch: (e) => ... }) - -// Create tagged errors with defineErrors -// The const name IS the namespace; variant names are short -const MyError = defineErrors({ - Variant: ({ /* fields */ }: { /* types */ }) => ({ - message: "Something went wrong", - /* ...fields */ - }), -}); -type MyError = InferErrors; // union of all variants -type MyVariant = InferError; // single variant -const result = MyError.Variant({ /* fields */ }); // returns Err<...> - -// Create brand types -type MyId = string & Brand<"MyId">; -const id = "123" as MyId; -``` - - -Ready to dive deeper? Read the [error system](/core/error-system) design or explore the [TanStack Query integration](/integrations/tanstack-query). - diff --git a/docs/integrations/testing.mdx b/docs/integrations/testing.mdx deleted file mode 100644 index d3df89b..0000000 --- a/docs/integrations/testing.mdx +++ /dev/null @@ -1,397 +0,0 @@ ---- -title: 'Testing Strategies with wellcrafted' -description: 'Comprehensive testing patterns for Result-based code and error scenarios' -icon: 'test-tube' ---- - -# Testing wellcrafted Code - -This guide covers how to test functions that return `Result`, custom matchers for cleaner assertions, and query layer testing with TanStack Query. - -## Testing Services That Return Results - -Services returning Results are straightforward to test: assert on the `Ok` or `Err` value directly: - -```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { Ok, Err } from 'wellcrafted/result'; -import { createUserService } from '../users'; -import type { Database } from '../db'; - -const createMockDatabase = (): Database => ({ - user: { - findMany: vi.fn(), - findUnique: vi.fn(), - create: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - count: vi.fn(), - }, -}); - -describe('UserService', () => { - let mockDb: Database; - let userService: ReturnType; - - beforeEach(() => { - mockDb = createMockDatabase(); - userService = createUserService(mockDb); - }); - - const validInput = { - name: 'John Doe', - email: 'john@example.com', - password: 'password123', - }; - - it('creates user with valid input', async () => { - const expectedUser = { - id: '1', - name: 'John Doe', - email: 'john@example.com', - role: 'user' as const, - createdAt: new Date(), - updatedAt: new Date(), - }; - - mockDb.user.findUnique.mockResolvedValue(null); - mockDb.user.create.mockResolvedValue(expectedUser); - - const result = await userService.createUser(validInput); - - expect(result).toEqual(Ok(expectedUser)); - }); - - it('returns error for invalid input', async () => { - const result = await userService.createUser({ - ...validInput, - name: '', - }); - - expect(result).toEqual(Err({ - name: 'UserServiceError', - message: 'Name is required', - input: { ...validInput, name: '', password: '[REDACTED]' }, - })); - - // Verify no database calls were made - expect(mockDb.user.findUnique).not.toHaveBeenCalled(); - expect(mockDb.user.create).not.toHaveBeenCalled(); - }); - - it('returns error for duplicate email', async () => { - mockDb.user.findUnique.mockResolvedValue({ id: '2', email: 'john@example.com' }); - - const result = await userService.createUser(validInput); - - expect(result).toEqual(Err({ - name: 'UserServiceError', - message: 'Email already exists', - input: { ...validInput, password: '[REDACTED]' }, - })); - - expect(mockDb.user.create).not.toHaveBeenCalled(); - }); - - it('handles database errors', async () => { - mockDb.user.findUnique.mockRejectedValue(new Error('Connection timeout')); - - const result = await userService.createUser(validInput); - - expect(result).toEqual(Err({ - name: 'UserServiceError', - message: 'Failed to create user', - input: { ...validInput, password: '[REDACTED]' }, - })); - }); - - it('returns error when user not found', async () => { - mockDb.user.findUnique.mockResolvedValue(null); - - const result = await userService.getUserById('non-existent'); - - expect(result).toEqual(Err({ - name: 'UserServiceError', - message: 'User not found', - userId: 'non-existent', - })); - }); -}); -``` - -The key pattern: assert directly with `expect(result).toEqual(Ok(...))` or `expect(result).toEqual(Err(...))`. No try/catch needed. - -## Custom Result Matchers - -For cleaner assertions, register custom Vitest matchers: - -```typescript -// src/__tests__/setup.ts -import { expect } from 'vitest'; - -const ResultMatchers = { - toBeOk: (received: any, expected?: any) => { - const pass = received && received.error === null && received.data !== null; - - if (expected !== undefined) { - return { - pass: pass && JSON.stringify(received.data) === JSON.stringify(expected), - message: () => `Expected ${JSON.stringify(received)} to be Ok(${JSON.stringify(expected)})`, - }; - } - - return { - pass, - message: () => `Expected ${JSON.stringify(received)} to be an Ok result`, - }; - }, - - toBeErr: (received: any, expected?: any) => { - const pass = received && received.data === null && received.error !== null; - - if (expected !== undefined) { - return { - pass: pass && JSON.stringify(received.error) === JSON.stringify(expected), - message: () => `Expected ${JSON.stringify(received)} to be Err(${JSON.stringify(expected)})`, - }; - } - - return { - pass, - message: () => `Expected ${JSON.stringify(received)} to be an Err result`, - }; - }, -}; - -expect.extend(ResultMatchers); -``` - -Then use them in tests: - -```typescript -const result = await userService.createUser(validInput); -expect(result).toBeOk(expectedUser); - -const errorResult = await userService.createUser({ ...validInput, name: '' }); -expect(errorResult).toBeErr({ - name: 'UserServiceError', - message: 'Name is required', -}); -``` - -## Testing the Query Layer - -Use a test `QueryClient` with retries disabled to test query and mutation factories: - -```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { QueryClient } from '@tanstack/query-core'; -import { createQueryFactories } from 'wellcrafted/query'; -import { Ok, Err } from 'wellcrafted/result'; -import * as userService from '../../services/users'; - -vi.mock('../../services/users'); - -function createTestQueryClient() { - return new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }); -} - -describe('User Queries', () => { - let queryClient: QueryClient; - let defineQuery: ReturnType['defineQuery']; - let defineMutation: ReturnType['defineMutation']; - - beforeEach(() => { - queryClient = createTestQueryClient(); - ({ defineQuery, defineMutation } = createQueryFactories(queryClient)); - vi.clearAllMocks(); - }); - - it('fetches users successfully', async () => { - const mockUsers = [ - { id: '1', name: 'John', email: 'john@example.com', role: 'user' as const, createdAt: new Date(), updatedAt: new Date() }, - ]; - - vi.mocked(userService.getAllUsers).mockResolvedValue(Ok({ - users: mockUsers, - total: 1, - })); - - const userQuery = defineQuery({ - queryKey: ['users'], - queryFn: () => userService.getAllUsers(), - }); - - const result = await userQuery.fetch(); - - expect(result).toEqual(Ok({ users: mockUsers, total: 1 })); - }); - - it('handles service errors', async () => { - const serviceError = { - name: 'UserServiceError', - message: 'Database connection failed', - operation: 'getAllUsers', - }; - - vi.mocked(userService.getAllUsers).mockResolvedValue(Err(serviceError)); - - const userQuery = defineQuery({ - queryKey: ['users'], - queryFn: () => userService.getAllUsers(), - }); - - const result = await userQuery.fetch(); - - expect(result).toEqual(Err(serviceError)); - }); - - it('mutation updates cache on success', async () => { - const newUser = { - id: '3', - name: 'Bob', - email: 'bob@example.com', - role: 'user' as const, - createdAt: new Date(), - updatedAt: new Date(), - }; - - vi.mocked(userService.createUser).mockResolvedValue(Ok(newUser)); - - queryClient.setQueryData(['users'], { users: [], total: 0 }); - - const createUserMutation = defineMutation({ - mutationKey: ['users', 'create'], - mutationFn: async (input: any) => { - const result = await userService.createUser(input); - - if (result.error) return result; - - queryClient.setQueryData(['users'], (old: any) => - old ? { - users: [result.data, ...old.users], - total: old.total + 1, - } : { users: [result.data], total: 1 } - ); - - return result; - }, - }); - - const result = await createUserMutation({ - name: 'Bob', - email: 'bob@example.com', - password: 'password123', - }); - - expect(result).toEqual(Ok(newUser)); - - const cachedData = queryClient.getQueryData(['users']); - expect(cachedData).toEqual({ users: [newUser], total: 1 }); - }); - - it('mutation does not update cache on error', async () => { - const serviceError = { - name: 'UserServiceError', - message: 'Email already exists', - email: 'existing@example.com', - }; - - vi.mocked(userService.createUser).mockResolvedValue(Err(serviceError)); - - const initialData = { users: [], total: 0 }; - queryClient.setQueryData(['users'], initialData); - - const createUserMutation = defineMutation({ - mutationKey: ['users', 'create'], - mutationFn: async (input: any) => { - const result = await userService.createUser(input); - if (result.error) return result; - - queryClient.setQueryData(['users'], (old: any) => - old ? { - users: [result.data, ...old.users], - total: old.total + 1, - } : { users: [result.data], total: 1 } - ); - return result; - }, - }); - - const result = await createUserMutation({ - name: 'John', - email: 'existing@example.com', - password: 'password123', - }); - - expect(result).toEqual(Err(serviceError)); - expect(queryClient.getQueryData(['users'])).toEqual(initialData); - }); -}); -``` - -## Error Scenario Testing - -Test edge cases and boundary conditions using the custom matchers: - -```typescript -describe('Input validation edge cases', () => { - it('handles empty strings', async () => { - const result = await userService.createUser({ - name: '', - email: '', - password: '', - }); - - expect(result).toBeErr({ - name: 'UserServiceError', - message: 'Name is required', - }); - }); - - it('handles extremely long inputs', async () => { - const result = await userService.createUser({ - name: 'a'.repeat(1001), - email: 'test@example.com', - password: 'password123', - }); - - expect(result).toBeErr({ - name: 'UserServiceError', - message: 'Name must be less than 1000 characters', - }); - }); - - it('handles concurrent creation with same email', async () => { - const userData = { - name: 'Test User', - email: 'test@example.com', - password: 'password123', - }; - - mockDb.user.findUnique - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(null); - - mockDb.user.create - .mockResolvedValueOnce(TestDataFactory.user({ email: 'test@example.com' })) - .mockRejectedValueOnce(new Error('Unique constraint violation')); - - const [result1, result2] = await Promise.all([ - userService.createUser(userData), - userService.createUser(userData), - ]); - - expect(result1).toBeOk(); - expect(result2).toBeErr({ - name: 'UserServiceError', - message: 'Failed to create user', - }); - }); -}); -``` diff --git a/docs/migration/from-try-catch.mdx b/docs/migration/from-try-catch.mdx deleted file mode 100644 index 24f0356..0000000 --- a/docs/migration/from-try-catch.mdx +++ /dev/null @@ -1,790 +0,0 @@ ---- -title: 'Migrating from Try-Catch' -description: 'Transform exception-based code to Result-based error handling' -icon: 'arrows-rotate' ---- - -# Migrating from Try-Catch - -This guide helps you transform traditional exception-based code to wellcrafted's Result-based error handling. You'll learn patterns for gradual migration and see common scenarios transformed. - -## Why Migrate? - -### The Fundamental Problems with Try-Catch - -JavaScript's traditional error handling has critical flaws: - -> **A function signature `function doSomething(): User` doesn't tell you that it might throw a `NetworkError` or a `ValidationError`. Errors are invisible until they strike at runtime.** - -Even worse: **Error instances lose their prototype chain when crossing serialization boundaries** (JSON.stringify/parse, network requests, worker threads), breaking `instanceof` checks. - -### Problems with Try-Catch - -```typescript -// ❌ Traditional approach - full of issues -async function processUser(id: string): Promise { - try { - const user = await fetchUser(id); // What errors can this throw? - const validated = validateUser(user); // What about this? - const saved = await saveUser(validated); // Or this? - return saved; - } catch (error) { - // Is it a network error? Validation error? Database error? - // Is it even an Error object? - console.error("Something went wrong:", error); - throw error; // Lost stack trace, no context, breaks across boundaries - } -} -``` - -### Benefits of Results - -```typescript -// ✅ Result-based approach - explicit and type-safe -async function processUser( - id: string -): Promise> { - const fetchResult = await fetchUser(id); - if (fetchResult.error) return fetchResult; - - const validationResult = validateUser(fetchResult.data); - if (validationResult.error) return validationResult; - - return saveUser(validationResult.data); -} -``` - -Benefits: -- **Visible error types** in function signatures - replace `throw new Error()` with `return Err()` to make errors explicit -- **Forced error handling** by the type system -- **Preserved error context** for debugging - custom fields capture function inputs and state -- **Better IntelliSense** and autocomplete -- **Serialization-safe** - plain objects that work everywhere - -## Migration Strategies - -### Strategy 1: Wrap at the Boundaries - -Start by wrapping external APIs and third-party code: - -```typescript -import { tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -// Before: Throwing function -async function fetchUserOld(id: string): Promise { - const response = await fetch(`/api/users/${id}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - return response.json(); -} - -// After: Result-returning wrapper -type ApiError = Readonly<{ name: "ApiError"; message: string }>; - -async function fetchUser(id: string): Promise> { - return tryAsync({ - try: () => fetchUserOld(id), - catch: (error) => Err({ - name: "ApiError", - message: "Failed to fetch user", - userId: id, - endpoint: `/api/users/${id}`, - }) - }); -} -``` - -### Strategy 2: Bottom-Up Transformation - -Transform leaf functions first, then work your way up: - -```typescript -// Step 1: Transform the lowest-level function -function validateEmail(email: string): Result { - if (!email.includes('@')) { - return Err({ - name: "ValidationError", - message: "Email must contain @ symbol", - email, - }); - } - return Ok(email.toLowerCase()); -} - -// Step 2: Transform functions that use it -function validateUser(data: unknown): Result { - if (!data || typeof data !== 'object') { - return Err({ - name: "ValidationError", - message: "Invalid user data", - receivedType: typeof data, - }); - } - - const emailResult = validateEmail((data as any).email); - if (emailResult.error) return emailResult; - - return Ok({ - email: emailResult.data, - // ... other fields - } as User); -} -``` - -### Strategy 3: Gradual Function Migration - -Migrate one function at a time while maintaining compatibility: - -```typescript -// Original throwing function -async function saveUserThrowing(user: User): Promise { - await db.save(user); -} - -// New Result-based version -async function saveUser(user: User): Promise> { - return tryAsync({ - try: () => saveUserThrowing(user), - catch: (error) => Err({ - name: "DatabaseError", - message: "Failed to save user", - userId: user.id, - }) - }); -} - -// Compatibility wrapper for gradual migration -async function saveUserCompat(user: User): Promise { - const result = await saveUser(user); - if (result.error) { - throw result.error; // Convert back to exception if needed - } -} -``` - -## Common Patterns - -### Pattern 1: Simple Try-Catch - -```typescript -// Before -function parseConfig(json: string): Config { - try { - const parsed = JSON.parse(json); - return validateConfig(parsed); - } catch (error) { - throw new Error(`Invalid config: ${error.message}`); - } -} - -// After -function parseConfig(json: string): Result { - return trySync({ - try: () => { - const parsed = JSON.parse(json); - return validateConfig(parsed); - }, - catch: (error) => Err({ - name: "ConfigError", - message: "Invalid configuration", - rawJson: json.substring(0, 100), - }) - }); -} -``` - -### Pattern 2: Nested Try-Catch - -```typescript -// Before - nested error handling is messy -async function processOrder(orderId: string): Promise { - try { - const order = await fetchOrder(orderId); - try { - await validateInventory(order); - try { - await chargePayment(order); - return order; - } catch (paymentError) { - await refundOrder(order); - throw new Error(`Payment failed: ${paymentError.message}`); - } - } catch (inventoryError) { - throw new Error(`Inventory check failed: ${inventoryError.message}`); - } - } catch (fetchError) { - throw new Error(`Order not found: ${fetchError.message}`); - } -} - -// After - linear error handling -async function processOrder( - orderId: string -): Promise> { - const orderResult = await fetchOrder(orderId); - if (orderResult.error) return orderResult; - - const inventoryResult = await validateInventory(orderResult.data); - if (inventoryResult.error) return inventoryResult; - - const paymentResult = await chargePayment(orderResult.data); - if (paymentResult.error) { - await refundOrder(orderResult.data); // Clean up on error - return paymentResult; - } - - return Ok(orderResult.data); -} -``` - -### Pattern 3: Error Recovery - -```typescript -// Before - complex recovery logic -async function fetchWithFallback(url: string): Promise { - try { - return await fetchFromPrimary(url); - } catch (primaryError) { - console.warn("Primary failed, trying secondary:", primaryError); - try { - return await fetchFromSecondary(url); - } catch (secondaryError) { - console.warn("Secondary failed, using cache:", secondaryError); - try { - return await fetchFromCache(url); - } catch (cacheError) { - throw new Error("All sources failed"); - } - } - } -} - -// After - clear fallback chain -async function fetchWithFallback(url: string): Promise> { - // Try primary - const primaryResult = await fetchFromPrimary(url); - if (primaryResult.data) return primaryResult; - - console.warn("Primary failed:", primaryResult.error); - - // Try secondary - const secondaryResult = await fetchFromSecondary(url); - if (secondaryResult.data) return secondaryResult; - - console.warn("Secondary failed:", secondaryResult.error); - - // Try cache - const cacheResult = await fetchFromCache(url); - if (cacheResult.data) return cacheResult; - - // All failed - return comprehensive error - return Err({ - name: "FetchError", - message: "All data sources failed", - url, - attempts: [ - { source: 'primary', error: primaryResult.error }, - { source: 'secondary', error: secondaryResult.error }, - { source: 'cache', error: cacheResult.error } - ], - }); -} -``` - -### Pattern 4: Resource Cleanup - -```typescript -// Before - cleanup in finally block -async function processFile(path: string): Promise { - let file; - try { - file = await openFile(path); - const content = await readContent(file); - const processed = await processContent(content); - return processed; - } catch (error) { - throw new Error(`File processing failed: ${error.message}`); - } finally { - if (file) { - await closeFile(file); - } - } -} - -// After - explicit cleanup -async function processFile(path: string): Promise> { - const fileResult = await openFile(path); - if (fileResult.error) return fileResult; - - const file = fileResult.data; - - try { - const contentResult = await readContent(file); - if (contentResult.error) { - await closeFile(file); - return contentResult; - } - - const processedResult = await processContent(contentResult.data); - if (processedResult.error) { - await closeFile(file); - return processedResult; - } - - await closeFile(file); - return processedResult; - - } catch (error) { - // Ensure cleanup even if closeFile throws - try { - await closeFile(file); - } catch { } - - return Err({ - name: "FileError", - message: "Unexpected error during file processing", - path, - }); - } -} -``` - -## Step-by-Step Migration Example - -Let's migrate a complete user registration flow: - -### Before: Exception-Based - -```typescript -class UserService { - async register(input: RegistrationInput): Promise { - try { - // Validate input - if (!input.email || !input.email.includes('@')) { - throw new Error("Invalid email"); - } - - if (!input.password || input.password.length < 8) { - throw new Error("Password too short"); - } - - // Check if user exists - const existing = await this.db.findByEmail(input.email); - if (existing) { - throw new Error("Email already registered"); - } - - // Hash password - const hashedPassword = await bcrypt.hash(input.password, 10); - - // Create user - const user = await this.db.createUser({ - email: input.email, - password: hashedPassword - }); - - // Send welcome email - await this.emailService.sendWelcome(user.email); - - return user; - - } catch (error) { - console.error("Registration failed:", error); - throw error; - } - } -} -``` - -### After: Result-Based - -```typescript -// Step 1: Define specific error types -type ValidationError = Readonly<{ name: "ValidationError"; message: string }>; -type UserExistsError = Readonly<{ name: "UserExistsError"; message: string }>; -type DatabaseError = Readonly<{ name: "DatabaseError"; message: string }>; -type EmailError = Readonly<{ name: "EmailError"; message: string }>; - -type RegistrationError = - | ValidationError - | UserExistsError - | DatabaseError - | EmailError; - -class UserService { - async register( - input: RegistrationInput - ): Promise> { - // Step 2: Validate with Results - const validationResult = this.validateRegistration(input); - if (validationResult.error) return validationResult; - - // Step 3: Check existence with Results - const existsResult = await this.checkUserExists(input.email); - if (existsResult.error) return existsResult; - - // Step 4: Hash password with error handling - const hashResult = await tryAsync({ - try: () => bcrypt.hash(input.password, 10), - catch: (error) => Err({ - name: "DatabaseError", - message: "Failed to hash password", - operation: 'bcrypt.hash', - }) - }); - if (hashResult.error) return hashResult; - - // Step 5: Create user with error handling - const userResult = await this.createUser({ - email: input.email, - password: hashResult.data - }); - if (userResult.error) return userResult; - - // Step 6: Send email (non-critical, log but don't fail) - const emailResult = await this.sendWelcomeEmail(userResult.data.email); - if (emailResult.error) { - console.warn("Failed to send welcome email:", emailResult.error); - // Continue - email failure shouldn't block registration - } - - return Ok(userResult.data); - } - - private validateRegistration( - input: RegistrationInput - ): Result { - if (!input.email?.includes('@')) { - return Err({ - name: "ValidationError", - message: "Invalid email address", - field: 'email', - value: input.email, - }); - } - - if (!input.password || input.password.length < 8) { - return Err({ - name: "ValidationError", - message: "Password must be at least 8 characters", - field: 'password', - }); - } - - return Ok(input); - } - - private async checkUserExists( - email: string - ): Result { - const result = await tryAsync({ - try: () => this.db.findByEmail(email), - catch: (error) => Err({ - name: "DatabaseError", - message: "Failed to check user existence", - email, - operation: 'findByEmail', - }) - }); - - if (result.error) return result; - - if (result.data) { - return Err({ - name: "UserExistsError", - message: "Email already registered", - email, - }); - } - - return Ok(undefined); - } -} -``` - -## Testing Migration - -### Testing Exception-Based Code - -```typescript -// Before - complex error testing -describe('UserService', () => { - it('should throw on invalid email', async () => { - const service = new UserService(); - - await expect( - service.register({ email: 'invalid', password: 'password123' }) - ).rejects.toThrow('Invalid email'); - }); - - it('should throw on existing user', async () => { - const service = new UserService(); - mockDb.findByEmail.mockResolvedValue({ id: '123' }); - - await expect( - service.register({ email: 'test@example.com', password: 'password123' }) - ).rejects.toThrow('Email already registered'); - }); -}); -``` - -### Testing Result-Based Code - -```typescript -// After - explicit error testing -describe('UserService', () => { - it('should return ValidationError for invalid email', async () => { - const service = new UserService(); - - const result = await service.register({ - email: 'invalid', - password: 'password123' - }); - - expect(result.error).toBeDefined(); - expect(result.error?.name).toBe('ValidationError'); - expect(result.error?.field).toBe('email'); - }); - - it('should return UserExistsError for existing user', async () => { - const service = new UserService(); - mockDb.findByEmail.mockResolvedValue({ id: '123' }); - - const result = await service.register({ - email: 'test@example.com', - password: 'password123' - }); - - expect(result.error).toBeDefined(); - expect(result.error?.name).toBe('UserExistsError'); - expect(result.error?.email).toBe('test@example.com'); - }); - - it('should return user on success', async () => { - const service = new UserService(); - mockDb.findByEmail.mockResolvedValue(null); - mockDb.createUser.mockResolvedValue({ id: '123', email: 'test@example.com' }); - - const result = await service.register({ - email: 'test@example.com', - password: 'password123' - }); - - expect(result.data).toBeDefined(); - expect(result.data?.email).toBe('test@example.com'); - expect(result.error).toBeNull(); - }); -}); -``` - -## Best Practices for Migration - -### 1. Start Small - -Begin with leaf functions that don't depend on other code: -- Validation functions -- Parsing functions -- Simple calculations - -### 2. Wrap External APIs - -Use `trySync` and `tryAsync` to wrap third-party code: -- Database queries -- HTTP requests -- File system operations - -### 3. Define Clear Error Types - -Create specific error types for each failure mode: -```typescript -// ❌ Too generic -Readonly<{ name: "Error"; message: string }> - -// ✅ Specific -Readonly<{ name: "NetworkTimeoutError"; message: string }> -Readonly<{ name: "InvalidCredentialsError"; message: string }> -``` - -### 4. Preserve Error Context - -Always include relevant debugging information as flat fields: -```typescript -return Err({ - name: "DatabaseError", - message: "Query failed", - query: sql, - params, - duration: Date.now() - startTime, - connection: connectionId, -}); -``` - -### 5. Maintain Backwards Compatibility - -During migration, provide compatibility wrappers: -```typescript -// New Result-based implementation -async function fetchUserResult(id: string): Promise> { - // ... implementation -} - -// Compatibility wrapper for existing code -async function fetchUser(id: string): Promise { - const result = await fetchUserResult(id); - if (result.error) { - throw new Error(result.error.message); - } - return result.data; -} -``` - -### Pattern 4: Graceful Error Handling with Ok(undefined) - -One useful pattern with `tryAsync` is treating certain "errors" as successful outcomes. This is perfect for operations where the exception doesn't represent a true failure from a business logic perspective. - -```typescript -// Before - treating all exceptions as errors -async function cleanupProcess(pid: number): Promise { - try { - const process = getProcess(pid); - await process.kill(); - console.log(`Process ${pid} terminated`); - } catch (error) { - // Is this really an error if the process is already dead? - console.error(`Failed to kill process ${pid}:`, error); - throw error; // Propagating an "error" that might not be one - } -} - -// After - explicitly defining what success means -async function cleanupProcess(pid: number): Promise> { - return tryAsync({ - try: async () => { - const process = getProcess(pid); - await process.kill(); - console.log(`Process ${pid} terminated`); - }, - catch: (error) => { - // Process already dead? That's what we wanted! - console.log(`Process ${pid} was already terminated`); - return Ok(undefined); // Explicitly marking this as success - } - }); -} -``` - -**Key insight**: By returning `Ok(undefined)` in the catch block, you're saying "this exception is actually fine." The operation succeeded from the user's perspective. - -#### Common Use Cases for Ok(undefined) - -```typescript -// Deleting a file that might not exist -async function deleteFile(path: string): Promise> { - return tryAsync({ - try: () => unlink(path), - catch: () => { - // File doesn't exist? Goal achieved! - return Ok(undefined); - } - }); -} - -// Creating a directory that might already exist -async function ensureDirectory(path: string): Promise> { - return tryAsync({ - try: () => mkdir(path), - catch: (error) => { - // Directory exists? Perfect! - if (error.code === 'EEXIST') { - return Ok(undefined); - } - // Other errors are real failures - return Err({ - name: "FileSystemError", - message: "Failed to create directory", - path, - }); - } - }); -} - -// Graceful cleanup in service shutdown -async function cleanup(): Promise { - await tryAsync({ - try: async () => { - await session.close(); - await connection.end(); - }, - catch: () => { - // Already cleaned up? That's fine - return Ok(undefined); - } - }); -} -``` - -#### When to Use Ok(undefined) vs Err - -Use `Ok(undefined)` when: -- The end goal is achieved regardless of the exception -- The exception represents an acceptable alternative path -- You're doing cleanup or ensuring a state -- The operation is idempotent - -Return `Err` when: -- The exception represents actual failure -- The operation needs to be retried or escalated -- You need to preserve error context for debugging -- The caller needs to know specifically what went wrong - -```typescript -// Example showing both patterns -async function saveUserPreference( - userId: string, - pref: Preference -): Promise> { - return tryAsync({ - try: async () => { - await db.upsert('preferences', { userId, ...pref }); - }, - catch: (error) => { - // Network errors are real failures - if (error.code === 'ENETUNREACH') { - return Err({ - name: "SaveError", - message: "Network unreachable", - userId, - }); - } - - // Duplicate key? Upsert semantics mean that's success - if (error.code === 'DUPLICATE_KEY') { - return Ok(undefined); - } - - // Unknown errors should propagate - return Err({ - name: "SaveError", - message: "Failed to save preference", - userId, - }); - } - }); -} -``` - -## Summary - -Migrating from try-catch to Results is a journey that pays dividends in: -- **Type safety**: Catch errors at compile time -- **Clarity**: See all failure modes in signatures -- **Debugging**: Rich error context -- **Maintenance**: Easier refactoring and testing - -Start small, migrate gradually, and enjoy more reliable code! - - -Ready to continue your journey? See why wellcrafted moves away from chained combinators in [From Effect to Pragmatic Errors](/philosophy/from-effect-to-pragmatic-errors), or revisit the [Error System Design](/core/error-system). - \ No newline at end of file diff --git a/docs/patterns/optional-keys.mdx b/docs/patterns/optional-keys.mdx deleted file mode 100644 index ae60340..0000000 --- a/docs/patterns/optional-keys.mdx +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: 'Optional Keys in Error Definitions' -description: 'When to use optional fields in defineErrors variants, and when to split into separate variants instead' -icon: 'key' ---- - -# Optional Keys in Error Definitions - -Optional keys (`?:`) in `defineErrors` constructors are almost always a design smell. The default stance: **no optional keys**. Each variant should carry exactly the fields it needs, all required. - -But optionality isn't the only smell. A constructor parameter can also have the **wrong type** (accepting a pre-formatted string when it should accept raw data) or the **wrong granularity** (accepting decomposed fields when it should accept a whole object). These are orthogonal problems: a field can have one, two, or all three smells stacked on top of each other. - -## The Six Categories - -When you spot a suspicious key in a `defineErrors` variant, it falls into one of six buckets: - -### 1. "Always Passed" Optionals - -The key is marked `?` but every call site passes it. - -```typescript -// Anti-pattern: optional but always provided -const UserError = defineErrors({ - NotFound: ({ userId }: { userId?: string }) => ({ - message: userId ? `User ${userId} not found` : 'User not found', - userId, - }), -}); - -// Every call site: -UserError.NotFound({ userId: id }); // always passed -UserError.NotFound({ userId: other }); // always passed -``` - -**Fix: make it required.** - -```typescript -const UserError = defineErrors({ - NotFound: ({ userId }: { userId: string }) => ({ - message: `User ${userId} not found`, - userId, - }), -}); -``` - -### 2. Boolean Sub-Discriminant - -A boolean or string that changes the error message or behavior. This means you really have multiple error types sharing a trenchcoat. - -```typescript -// Anti-pattern: boolean changes the meaning -const PaymentError = defineErrors({ - Failed: ({ isRetryable, transactionId }: { - isRetryable?: boolean; transactionId: string; - }) => ({ - message: isRetryable - ? `Payment ${transactionId} failed (retryable)` - : `Payment ${transactionId} failed permanently`, - isRetryable, transactionId, - }), -}); -``` - -**Fix: split into separate variants.** - -```typescript -const PaymentError = defineErrors({ - Retryable: ({ transactionId }: { transactionId: string }) => ({ - message: `Payment ${transactionId} failed, please retry`, - transactionId, - }), - Permanent: ({ transactionId }: { transactionId: string }) => ({ - message: `Payment ${transactionId} failed permanently`, - transactionId, - }), -}); -``` - -Now consumers can handle them distinctly without inspecting a boolean: - -```typescript -switch (error.name) { - case 'Retryable': return scheduleRetry(error.transactionId); - case 'Permanent': return refundUser(error.transactionId); -} -``` - -### 3. "Grab Bag" All-Optional - -A single variant tries to cover many failure modes with all-optional context. Different call sites pass different subsets of the fields. - -```typescript -// Anti-pattern: one variant, many failure modes -const FileError = defineErrors({ - Operation: ({ operation, fileName, fileSize, maxSize, fileType }: { - operation: string; - fileName?: string; - fileSize?: number; - maxSize?: number; - fileType?: string; - }) => ({ - message: `File ${operation} failed`, - operation, fileName, fileSize, maxSize, fileType, - }), -}); - -// Call sites pass different subsets: -FileError.Operation({ operation: 'upload', fileName: f.name, fileSize: f.size, maxSize: limit }); -FileError.Operation({ operation: 'upload', fileName: f.name, fileType: f.type }); -FileError.Operation({ operation: 'upload', fileName: f.name }); -``` - -**Fix: split into specific variants with required fields.** - -```typescript -const FileError = defineErrors({ - FileTooLarge: ({ fileName, fileSize, maxSize }: { - fileName: string; fileSize: number; maxSize: number; - }) => ({ - message: `File "${fileName}" is ${fileSize} bytes, exceeding the ${maxSize} byte limit`, - fileName, fileSize, maxSize, - }), - UnsupportedType: ({ fileName, fileType }: { - fileName: string; fileType: string; - }) => ({ - message: `File type "${fileType}" is not supported`, - fileName, fileType, - }), - UploadFailed: ({ fileName }: { fileName: string }) => ({ - message: `Failed to upload "${fileName}"`, - fileName, - }), -}); -``` - -Each variant has exactly the fields that matter for that failure mode, all required. - -### 4. Pre-Formatted Input (Let the Constructor Own Formatting) - -The field's type is `string`, but the call site is calling `extractErrorMessage()` or `String()` to produce it. The constructor should accept the raw data and own the conversion. - -```typescript -// Anti-pattern: call site does the formatting -const HttpError = defineErrors({ - Response: ({ status, bodyMessage }: { status: number; bodyMessage: string }) => ({ - message: `HTTP ${status}: ${bodyMessage}`, - status, bodyMessage, - }), -}); - -// Call site extracts the message: -HttpError.Response({ - status: response.status, - bodyMessage: extractErrorMessage(await response.json()), -}); -``` - -The `bodyMessage: string` type is a lie: the real data is `unknown` (an unparsed response body), and the call site is doing work the constructor should own. - -**Fix: accept raw data, format in the constructor.** - -```typescript -const HttpError = defineErrors({ - Response: ({ response, body }: { response: { status: number }; body: unknown }) => ({ - message: `HTTP ${response.status}: ${extractErrorMessage(body)}`, - status: response.status, body, - }), -}); - -// Call site passes raw objects: -HttpError.Response({ response, body: await response.json() }); -``` - -**The principle:** Transform raw data in the constructor, not the call site. If a field's type is `string` but the call site is calling `extractErrorMessage()` or `String()` to produce it, the type should be `unknown` and the constructor should own that conversion. - -### 5. Decomposed Object (Pass the Whole Thing) - -The call site is extracting fields from an object and passing them individually. The constructor should accept the object and decompose it itself. - -```typescript -// Anti-pattern: call site decomposes the response -const HttpError = defineErrors({ - Response: ({ status, body }: { status: number; body: unknown }) => ({ - message: `HTTP ${status}: ${extractErrorMessage(body)}`, - status, body, - }), -}); - -// Call site pulls .status out of the response: -HttpError.Response({ status: response.status, body: await response.json() }); -``` - -**Fix: pass the object, let the constructor extract what it needs.** - -```typescript -const HttpError = defineErrors({ - Response: ({ response, body }: { response: { status: number }; body: unknown }) => ({ - message: `HTTP ${response.status}: ${extractErrorMessage(body)}`, - status: response.status, body, - }), -}); - -// Call site passes the raw response: -HttpError.Response({ response, body: await response.json() }); -``` - - -**Why `body` is separate from `response`:** The `.json()` parse stays at the call site because it's `async` and constructors are sync; the parsed body must be passed as its own parameter. The `response` parameter uses structural typing (`{ status: number }`) rather than a platform-specific `Response` type, so browser `Response`, Tauri responses, and test doubles all satisfy the constraint. - - -### 6. Genuine Enrichment (The Exception) - -Extra context that truly may not be available at the call site. This is the **only** case where optional keys are acceptable. - -```typescript -const ApiError = defineErrors({ - /** @param requestId - The server-assigned request ID from the - * `X-Request-Id` response header, if present. Not all servers - * return this header. */ - RequestFailed: ({ status, url, requestId }: { - status: number; - url: string; - requestId?: string; - }) => ({ - message: requestId - ? `API request to ${url} failed (${status}) [${requestId}]` - : `API request to ${url} failed (${status})`, - status, url, requestId, - }), -}); -``` - -**When this is OK:** - -- The data source genuinely may not provide the value (a response header that not all servers return, an external trace ID, a stack trace) -- The error is the same variant regardless: the optional field adds debugging context, not a different meaning -- A JSDoc comment explains **why** the field is optional - -## Orthogonal Problems - -These six categories are independent checks. A single field can have multiple smells stacked on top of each other. For example, a `bodyMessage?: string` parameter might have **three** problems at once: - -1. **Wrong type** (category 4): `string` instead of `unknown`, with the call site calling `extractErrorMessage()` -2. **Wrong decomposition** (category 5): extracting `.status` from a response instead of passing the response -3. **Wrong optionality** (category 1): marked `?` but always passed - -Fix each independently: change the type to `unknown`, pass the whole response object, and make it required. The categories in this doc are orthogonal; apply each check separately. - -## The Rule - -> If removing the optional key would force you to split the variant, **split the variant**. If the call site is doing work the constructor could own, **move it into the constructor**. Optional keys in `defineErrors` should be the exception, not the default. When they must exist, document why with JSDoc. - -## Quick Checklist - -| Question | If yes... | -|----------|-----------| -| Is the field always passed at every call site? | Make it required | -| Does the field change the error message or recovery path? | Split into separate variants | -| Do different call sites pass different subsets of fields? | You have N variants sharing a trenchcoat; split them | -| Is the field a pre-formatted string derived from raw data? | Accept raw data (`unknown`), format in constructor | -| Is the call site decomposing an object to extract the field? | Pass the object, let constructor extract | -| Is the field truly unavailable in some scenarios? | Keep optional, add JSDoc explaining why | \ No newline at end of file diff --git a/docs/patterns/real-world.mdx b/docs/patterns/real-world.mdx deleted file mode 100644 index 15e92fc..0000000 --- a/docs/patterns/real-world.mdx +++ /dev/null @@ -1,387 +0,0 @@ ---- -title: 'Real-World Examples' -description: 'Production-tested patterns and complete examples using wellcrafted' -icon: 'code' ---- - -# Real-World Examples - -Production-tested examples showing how `defineErrors`, `tryAsync`, and Result destructuring work together in real services. - -## User Authentication Service - -A complete auth service with error propagation and branded types: - -```typescript -import { Result, Ok, tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; -import { type Brand } from "wellcrafted/brand"; - -type UserId = string & Brand<"UserId">; -type SessionToken = string & Brand<"SessionToken">; - -const AuthServiceError = defineErrors({ - LoginFailed: ({ email }: { email: string }) => ({ - message: `Login failed for ${email}`, - email, - }), - SessionExpired: () => ({ - message: 'Session has expired', - }), - TokenGenerationFailed: ({ userId }: { userId: string }) => ({ - message: `Failed to generate token for user ${userId}`, - userId, - }), - SessionValidationFailed: ({ userId }: { userId: string }) => ({ - message: `Session validation failed for user ${userId}`, - userId, - }), -}); -type AuthServiceError = InferErrors; - -interface AuthenticatedUser { - id: UserId; - email: string; - name: string; - isVerified: boolean; -} - -export function createAuthService( - db: Database, - hashService: HashService, - tokenService: TokenService -) { - return { - async login( - email: string, - password: string - ): Promise> { - const maskedEmail = email.substring(0, email.indexOf('@')) + '@***'; - - const userResult = await tryAsync({ - try: () => db.users.findByEmail(email), - catch: () => AuthServiceError.LoginFailed({ email: maskedEmail }) - }); - if (userResult.error) return userResult; - if (!userResult.data) { - return AuthServiceError.LoginFailed({ email: maskedEmail }); - } - - const isValid = await hashService.verify(password, userResult.data.passwordHash); - if (!isValid) { - return AuthServiceError.LoginFailed({ email: maskedEmail }); - } - - const tokenResult = await tryAsync({ - try: () => tokenService.generate(userResult.data.id), - catch: () => AuthServiceError.TokenGenerationFailed({ userId: userResult.data.id }) - }); - if (tokenResult.error) return tokenResult; - - return Ok({ - user: { - id: userResult.data.id as UserId, - email: userResult.data.email, - name: userResult.data.name, - isVerified: userResult.data.isVerified - }, - token: tokenResult.data as SessionToken - }); - }, - - async validateSession( - token: SessionToken - ): Promise> { - const validationResult = await tryAsync({ - try: () => tokenService.validate(token), - catch: () => AuthServiceError.SessionExpired() - }); - if (validationResult.error) return validationResult; - if (!validationResult.data.isValid) { - return AuthServiceError.SessionExpired(); - } - - const userResult = await tryAsync({ - try: () => db.users.findById(validationResult.data.userId), - catch: () => AuthServiceError.SessionValidationFailed({ userId: validationResult.data.userId }) - }); - if (userResult.error) return userResult; - if (!userResult.data) { - return AuthServiceError.SessionValidationFailed({ userId: validationResult.data.userId }); - } - - return Ok({ - id: userResult.data.id as UserId, - email: userResult.data.email, - name: userResult.data.name, - isVerified: userResult.data.isVerified - }); - } - }; -} -``` - -## File Upload with Validation - -Size limits, type checking, and storage cleanup on failure: - -```typescript -import { Result, Ok, tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; -import { type Brand } from "wellcrafted/brand"; - -type FileId = string & Brand<"FileId">; -type UserId = string & Brand<"UserId">; - -const FileServiceError = defineErrors({ - FileTooLarge: ({ fileName, fileSize, maxSize }: { - fileName: string; fileSize: number; maxSize: number; - }) => ({ - message: `File "${fileName}" is ${fileSize} bytes, exceeding the ${maxSize} byte limit`, - fileName, fileSize, maxSize, - }), - UnsupportedType: ({ fileName, fileType }: { - fileName: string; fileType: string; - }) => ({ - message: `File type "${fileType}" is not supported`, - fileName, fileType, - }), - UploadFailed: ({ fileName }: { fileName: string }) => ({ - message: `Failed to upload "${fileName}"`, - fileName, - }), - MetadataSaveFailed: ({ fileName }: { fileName: string }) => ({ - message: `Failed to save metadata for "${fileName}"`, - fileName, - }), -}); -type FileServiceError = InferErrors; - -const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB -const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']; - -export function createFileService(storage: StorageService, db: Database) { - return { - async uploadFile( - userId: UserId, - file: File, - onProgress?: (percent: number) => void - ): Promise> { - if (file.size > MAX_FILE_SIZE) { - return FileServiceError.FileTooLarge({ - fileName: file.name, fileSize: file.size, maxSize: MAX_FILE_SIZE - }); - } - if (!ALLOWED_TYPES.includes(file.type)) { - return FileServiceError.UnsupportedType({ - fileName: file.name, fileType: file.type - }); - } - - const fileId = crypto.randomUUID() as FileId; - const fileKey = `${userId}/${fileId}/${file.name}`; - - const uploadResult = await tryAsync({ - try: () => storage.upload(fileKey, file, { onProgress }), - catch: () => FileServiceError.UploadFailed({ fileName: file.name }) - }); - if (uploadResult.error) return uploadResult; - - const metadataResult = await tryAsync({ - try: () => db.files.create({ - id: fileId, userId, originalName: file.name, - size: file.size, mimeType: file.type, - storageKey: fileKey, url: uploadResult.data.url - }), - catch: () => FileServiceError.MetadataSaveFailed({ fileName: file.name }) - }); - - if (metadataResult.error) { - await storage.delete(fileKey); // cleanup on DB failure - return metadataResult; - } - - return Ok({ id: fileId, url: uploadResult.data.url }); - } - }; -} -``` - -## Form Validation with Zod - -Combining wellcrafted with Zod for typed validation errors: - -```typescript -import { z } from 'zod'; -import { Result, Ok, trySync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -const ValidationError = defineErrors({ - InvalidFields: ({ field, allErrors }: { - field: string; - allErrors: Array<{ path: string; message: string }>; - }) => ({ - message: `Validation failed on ${field}`, - field, allErrors, - }), - Unexpected: () => ({ - message: 'Validation failed unexpectedly', - }), -}); -type ValidationError = InferErrors; - -const CreateUserSchema = z.object({ - email: z.string().email("Invalid email address"), - name: z.string().min(2, "Name must be at least 2 characters"), - age: z.number().min(18, "Must be 18 or older").max(120, "Invalid age"), - terms: z.boolean().refine(val => val === true, "Must accept terms") -}); - -export function validateCreateUserForm( - input: unknown -): Result, ValidationError> { - return trySync({ - try: () => CreateUserSchema.parse(input), - catch: (error) => { - if (error instanceof z.ZodError) { - const firstIssue = error.issues[0]; - return ValidationError.InvalidFields({ - field: firstIssue.path.join('.'), - allErrors: error.issues.map(issue => ({ - path: issue.path.join('.'), - message: issue.message - })) - }); - } - return ValidationError.Unexpected(); - } - }); -} - -// Usage -async function handleSubmit(formData: unknown) { - const result = validateCreateUserForm(formData); - - if (result.error) { - if (result.error.name === 'InvalidFields') { - showFieldError(result.error.field, result.error.message); - } else { - showToast(result.error.message); - } - return; - } - - const createResult = await userService.createUser(result.data); - if (createResult.error) { - showToast("Failed to create user: " + createResult.error.message); - return; - } - - redirect(`/users/${createResult.data.id}`); -} -``` - -## API Route with Error Transformation - -A Next.js API route transforming service errors into HTTP responses: - -```typescript -import { NextRequest, NextResponse } from 'next/server'; -import { isErr } from 'wellcrafted/result'; -import { type Brand } from 'wellcrafted/brand'; - -type SessionToken = string & Brand<"SessionToken">; -type UserId = string & Brand<"UserId">; - -function errorResponse(message: string, status: number, context?: Record) { - return NextResponse.json({ error: { message, context, timestamp: new Date().toISOString() } }, { status }); -} - -export async function GET( - request: NextRequest, - { params }: { params: { id: string } } -) { - const authHeader = request.headers.get('authorization'); - if (!authHeader?.startsWith('Bearer ')) { - return errorResponse('Missing or invalid authorization header', 401); - } - - const token = authHeader.substring(7) as SessionToken; - const sessionResult = await authService.validateSession(token); - - if (isErr(sessionResult)) { - return errorResponse('Authentication failed', 401, { reason: sessionResult.error.message }); - } - - const userId = params.id as UserId; - const userResult = await userService.getUser(userId); - - if (isErr(userResult)) { - switch (userResult.error.name) { - case 'UserNotFound': - return errorResponse('User not found', 404, { userId }); - default: - return errorResponse('Failed to fetch user', 500); - } - } - - if (userResult.data.id !== sessionResult.data.id) { - return errorResponse('Access denied', 403); - } - - return NextResponse.json({ data: userResult.data }); -} -``` - -## Service Composition - -Coordinating multiple services where each step returns a Result: - -```typescript -export function createApplicationService( - authService: AuthService, - userService: UserService, - fileService: FileService -) { - return { - async updateUserProfile( - token: SessionToken, - profileData: UpdateProfileInput, - avatarFile?: File - ): Promise> { - const authResult = await authService.validateSession(token); - if (authResult.error) return transformAuthError(authResult.error); - - let avatarUrl: string | undefined; - if (avatarFile) { - const uploadResult = await fileService.uploadFile(authResult.data.id, avatarFile); - if (uploadResult.error) return transformFileError(uploadResult.error); - avatarUrl = uploadResult.data.url; - } - - const updateResult = await userService.updateProfile( - authResult.data.id, - { ...profileData, avatarUrl } - ); - if (updateResult.error) return transformUserError(updateResult.error); - - return Ok({ user: updateResult.data, message: "Profile updated successfully" }); - } - }; -} -``` - -## Key Takeaways - -1. **Errors in signatures**: every example makes failures visible in the return type, no surprise runtime exceptions. -2. **Error transformation at boundaries**: database errors become service errors, service errors become HTTP responses. -3. **Branded types prevent mixups**: `UserId` and `SessionToken` can never be accidentally swapped. -4. **Framework agnostic**: these patterns work in Next.js, Svelte, plain Node, or anywhere TypeScript runs. - - -**Why split variants?** The examples above use specific variants like `LoginFailed`, `FileTooLarge`, and `InvalidFields` rather than a single catch-all `Operation` variant. This is the recommended pattern: each variant carries exactly the context it needs as required fields, and consumers can `switch` on `error.name` to handle each case distinctly. A single catch-all variant is only acceptable when *all* failures are handled identically by every consumer. For guidance on when optional keys are appropriate, see [Optional Keys in Error Definitions](/patterns/optional-keys). - - - -For larger-scale architecture, see the [service layer patterns](/patterns/service-layer). - diff --git a/docs/patterns/service-layer.mdx b/docs/patterns/service-layer.mdx deleted file mode 100644 index 76df0fb..0000000 --- a/docs/patterns/service-layer.mdx +++ /dev/null @@ -1,471 +0,0 @@ ---- -title: 'Service Layer Pattern' -description: 'Best practices for building services with wellcrafted' -icon: 'layer-group' ---- - -# Service Layer Pattern - -Build testable services using wellcrafted's Result types and error handling with the factory function pattern: no classes needed. - -## Core Principles - -1. **Factory Functions Over Classes**: Use functions that return objects -2. **Explicit Error Handling**: All errors visible in function signatures -3. **Pure Business Logic**: Services contain only business logic, no UI concerns -4. **Dependency Injection**: Pass dependencies as parameters - -## Basic Service Pattern - -```typescript -import { Result, Ok, tryAsync } from "wellcrafted/result"; -import { defineErrors, type InferErrors } from "wellcrafted/error"; - -// 1. Define service-specific errors with typed fields -const UserServiceError = defineErrors({ - GetFailed: ({ userId }: { userId: string }) => ({ - message: `Failed to get user ${userId}`, - userId, - }), - CreateFailed: ({ email }: { email: string }) => ({ - message: `Failed to create user with email ${email}`, - email, - }), -}); -type UserServiceError = InferErrors; - -// 2. Create service with factory function -export function createUserService(db: Database) { - const cache = new Map(); - - return { - async getUser(id: string): Promise> { - const cached = cache.get(id); - if (cached) return Ok(cached); - - const result = await tryAsync({ - try: () => db.users.findById(id), - catch: () => UserServiceError.GetFailed({ userId: id }) - }); - - if (result.data) { - cache.set(id, result.data); - } - - return result; - }, - - async createUser(input: CreateUserInput): Promise> { - if (!input.email.includes('@')) { - return UserServiceError.CreateFailed({ email: input.email }); - } - - return tryAsync({ - try: () => db.users.create(input), - catch: () => UserServiceError.CreateFailed({ email: input.email }) - }); - }, - - clearCache() { - cache.clear(); - } - }; -} - -// 3. Export the type -export type UserService = ReturnType; - -// 4. Create live instance with real dependencies -export const UserServiceLive = createUserService(databaseInstance); -``` - -## Service with Multiple Dependencies - -```typescript -import { defineErrors, type InferErrors } from 'wellcrafted/error'; - -// Separate namespaces: payment and persistence are different concerns -const PaymentError = defineErrors({ - PaymentFailed: ({ amount }: { amount: number }) => ({ - message: 'Payment processing failed', - amount, - }), -}); -type PaymentError = InferErrors; - -const OrderDbError = defineErrors({ - CreateFailed: ({ input }: { input: OrderInput }) => ({ - message: 'Order creation failed', - input, - }), -}); -type OrderDbError = InferErrors; - -export function createOrderService( - db: Database, - paymentGateway: PaymentGateway, - emailService: EmailService -) { - return { - async createOrder(input: OrderInput): Promise> { - // 1. Validate inventory - const inventoryCheck = await checkInventory(input.items); - if (inventoryCheck.error) return inventoryCheck; - - // 2. Process payment - const paymentResult = await tryAsync({ - try: () => paymentGateway.charge(input.payment), - catch: () => PaymentError.PaymentFailed({ amount: input.payment.amount }) - }); - - if (paymentResult.error) return paymentResult; - - // 3. Create order in database - const orderResult = await tryAsync({ - try: () => db.orders.create({ - ...input, - paymentId: paymentResult.data.id - }), - catch: () => OrderDbError.CreateFailed({ input }) - }); - - if (orderResult.error) { - // Rollback payment on failure - await paymentGateway.refund(paymentResult.data.id); - return orderResult; - } - - // 4. Send confirmation (don't fail order if email fails) - await tryAsync({ - try: () => emailService.sendOrderConfirmation(orderResult.data), - catch: () => { - console.warn('Email notification failed, but order is complete'); - return Ok(undefined); - } - }); - - return Ok(orderResult.data); - } - }; -} -``` - -## Platform-Specific Services - -For services that need different implementations per platform: - -```typescript -import { defineErrors, type InferErrors } from 'wellcrafted/error'; - -// types.ts - Define the interface -export type NotificationService = { - notify(message: string): Promise>; - requestPermission(): Promise>; -}; - -const NotificationError = defineErrors({ - SendFailed: ({ platform }: { platform: string }) => ({ - message: `Failed to send notification on ${platform}`, - platform, - }), -}); -type NotificationError = InferErrors; - -// desktop.ts -export function createNotificationServiceDesktop(): NotificationService { - return { - async notify(message) { - return tryAsync({ - try: () => { - new window.Notification("App Name", { body: message }); - }, - catch: () => NotificationError.SendFailed({ platform: "desktop" }) - }); - }, - - async requestPermission() { - return Ok(true); // Desktop apps don't need permission - } - }; -} - -// web.ts -export function createNotificationServiceWeb(): NotificationService { - return { - async notify(message) { - const permission = await Notification.requestPermission(); - if (permission !== 'granted') { - return NotificationError.SendFailed({ platform: "web" }); - } - - return tryAsync({ - try: () => { - new Notification("App Name", { body: message }); - }, - catch: () => NotificationError.SendFailed({ platform: "web" }) - }); - }, - - async requestPermission() { - const permission = await Notification.requestPermission(); - return Ok(permission === 'granted'); - } - }; -} - -// index.ts - Runtime platform detection -export const NotificationServiceLive = typeof window !== 'undefined' - ? createNotificationServiceWeb() - : createNotificationServiceDesktop(); -``` - -## The Flexible Catch Pattern - -The `catch` function in `tryAsync`/`trySync` can return either `Ok` for graceful recovery or `Err` for error propagation. - -### Graceful Degradation - -When an operation fails but shouldn't break the flow: - -```typescript -export function createAnalyticsService(endpoint: string) { - return { - async trackEvent(event: AnalyticsEvent): Promise> { - return tryAsync({ - try: async () => { - await fetch(endpoint, { - method: 'POST', - body: JSON.stringify(event) - }); - }, - catch: (error) => { - console.warn('Analytics failed:', error); - return Ok(undefined); // Analytics is non-critical - } - }); - } - }; -} -``` - -### Fallback Values - -Return a default value when the primary operation fails: - -```typescript -export function createConfigService() { - return { - async loadConfig(): Promise> { - return tryAsync({ - try: () => fetch('/api/config').then(r => r.json()), - catch: (error) => { - console.warn('Using default config:', error); - return Ok({ theme: 'light', language: 'en', features: ['basic'] }); - } - }); - } - }; -} -``` - -### Conditional Error Handling - -Decide whether to recover or propagate based on the error: - -```typescript -const FileServiceError = defineErrors({ - Operation: ({ path }: { path: string }) => ({ - message: `File operation failed: ${path}`, - path, - }), -}); -type FileServiceError = InferErrors; - -export function createFileService() { - return { - async ensureDirectory(path: string): Promise> { - return tryAsync({ - try: () => mkdir(path, { recursive: true }), - catch: (error: any) => { - if (error.code === 'EEXIST') return Ok(undefined); - return FileServiceError.Operation({ path }); - } - }); - } - }; -} -``` - -### Cleanup and Recovery - -Perform cleanup operations that should always succeed: - -```typescript -export function createSessionService(db: Database) { - return { - async cleanup(sessionId: string): Promise> { - await tryAsync({ - try: () => db.sessions.delete(sessionId), - catch: () => Ok(undefined) - }); - - await tryAsync({ - try: () => db.tempFiles.deleteBySession(sessionId), - catch: () => Ok(undefined) - }); - - await tryAsync({ - try: () => db.logs.archiveBySession(sessionId), - catch: () => Ok(undefined) - }); - - return Ok(undefined); - } - }; -} -``` - -### When to Use Each - -| Strategy | Use When | -|---|---| -| `Ok(undefined)` | Non-critical ops: analytics, cleanup, notifications | -| `Ok(fallback)` | Sensible default exists: config, preferences, cache | -| `Err(...)` | Critical to business logic, needs user feedback, caller must decide | - -## Service Composition - -Services can compose other services: - -```typescript -export function createAppService( - userService: UserService, - orderService: OrderService, - notificationService: NotificationService -) { - return { - async purchaseProduct(userId: string, productId: string): Promise> { - const userResult = await userService.getUser(userId); - if (userResult.error) return userResult; - - const orderResult = await orderService.createOrder({ - userId, - productId, - payment: userResult.data.defaultPaymentMethod - }); - - if (orderResult.error) return orderResult; - - // Don't fail purchase if notification fails - await notificationService.notify(`Order ${orderResult.data.id} confirmed!`); - - return Ok(orderResult.data); - } - }; -} -``` - -## Best Practices - -### Include Fields in Errors - -Always include relevant debugging information: - -```typescript -const UserServiceError = defineErrors({ - UpdateFailed: ({ userId }: { userId: string }) => ({ - message: `Failed to update user ${userId}`, - userId, - }), -}); - -return UserServiceError.UpdateFailed({ userId }); -``` - -### Handle Errors at the Right Level - -Transform low-level errors into domain-specific ones at service boundaries: - -```typescript -const DatabaseError = defineErrors({ - Query: ({ sql }: { sql: string }) => ({ - message: `Database query failed: ${sql}`, - sql, - }), -}); - -const UserServiceError = defineErrors({ - GetFailed: ({ userId }: { userId: string }) => ({ - message: `Failed to get user ${userId}`, - userId, - }), -}); - -// Low-level database error -const dbResult = await tryAsync({ - try: () => db.query(sql), - catch: () => DatabaseError.Query({ sql }) -}); - -if (dbResult.error) { - // Transform to service-level error - return UserServiceError.GetFailed({ userId }); -} -``` - -### Keep Services Pure - -Services should only contain business logic: - -```typescript -// ✅ Good - pure business logic -export function createUserService(db: Database) { - return { - async getUser(id: string): Promise> { - return tryAsync({ - try: () => db.users.findById(id), - catch: () => UserServiceError.GetFailed({ userId: id }) - }); - } - }; -} - -// ❌ Bad - UI concerns in service -export function createUserService(db: Database, toastNotifier: ToastNotifier) { - return { - async getUser(id: string) { - const result = await db.users.findById(id); - if (!result) { - toastNotifier.error("User not found!"); // UI concern! - } - return result; - } - }; -} -``` - -### Use the Live Suffix Convention - -```typescript -// Service factory -export function createUserService(db: Database) { /* ... */ } - -// Type export -export type UserService = ReturnType; - -// Live instance with real dependencies -export const UserServiceLive = createUserService(databaseInstance); - -// Test instance with mocks -export const UserServiceTest = createUserService(mockDatabase); -``` - -## Summary - -The factory function pattern with wellcrafted provides: - -- **Type Safety**: All errors visible in function signatures -- **Testability**: Easy dependency injection -- **Simplicity**: No class boilerplate -- **Flexibility**: Platform-specific implementations -- **Composability**: Services can build on each other diff --git a/docs/philosophy/brand-implementation.mdx b/docs/philosophy/brand-implementation.mdx deleted file mode 100644 index 4c374d5..0000000 --- a/docs/philosophy/brand-implementation.mdx +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: 'Brand Type Implementation' -description: 'Why wellcrafted uses nested boolean markers for brand types' -icon: 'code' ---- - -# Brand Type Implementation - -wellcrafted's `Brand` type uses a specific implementation pattern that enables hierarchical brand relationships. This page explains why we chose this approach and how it compares to alternatives. - - -**Source Code**: The implementation is in [`src/brand.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/src/brand.ts) (~50 lines). Tests demonstrating each behavior are in [`src/brand.test.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/src/brand.test.ts). - - -## The Problem: Flat Brands Don't Stack - -The simplest possible brand implementation stores the brand name directly: - -```typescript -// ❌ Naive implementation -declare const brand: unique symbol; -type Brand = { [brand]: T }; -``` - -This works for simple cases but breaks when you need hierarchical types: - -```typescript -type AbsolutePath = string & Brand<"AbsolutePath">; -type ProjectDir = AbsolutePath & Brand<"ProjectDir">; -type ProviderDir = ProjectDir & Brand<"ProviderDir">; - -// What happens when TypeScript intersects these? -// { [brand]: "AbsolutePath" } & { [brand]: "ProjectDir" } -// = { [brand]: "AbsolutePath" & "ProjectDir" } -// = { [brand]: never } -// = never ❌ -``` - -The intersection of two different string literals is `never`, which makes the entire type collapse. You can't create a value that satisfies both brands. - -## Our Solution: Nested Boolean Markers - -wellcrafted uses a nested object structure with boolean markers: - -```typescript -// ✅ wellcrafted implementation (src/brand.ts lines 45-47) -declare const brand: unique symbol; -type Brand = { [brand]: { [K in T]: true } }; -``` - -Now intersections merge instead of conflicting: - -```typescript -type AbsolutePath = string & Brand<"AbsolutePath">; -type ProjectDir = AbsolutePath & Brand<"ProjectDir">; -type ProviderDir = ProjectDir & Brand<"ProviderDir">; - -// When TypeScript intersects these: -// { [brand]: { AbsolutePath: true } } & { [brand]: { ProjectDir: true } } -// = { [brand]: { AbsolutePath: true, ProjectDir: true } } -// Works! ✅ -``` - -This enables proper subtyping: a `ProjectDir` is assignable to `AbsolutePath` because it has all the required brand markers plus additional ones. - -### Verified Behaviors - -The test suite ([`src/brand.test.ts`](https://github.com/wellcrafted-dev/wellcrafted/blob/main/src/brand.test.ts)) verifies: - -| Test Case | What It Proves | -|-----------|----------------| -| `stacked brands are not never` | Intersected brands remain valid types | -| `child brand assignable to parent brand` | `ProjectDir` → `AbsolutePath` works | -| `parent brand NOT assignable to child brand` | `AbsolutePath` → `ProjectDir` fails | -| `sibling brands are distinct` | `ProjectDir` ↔ `ProviderDir` fails | -| `three-level hierarchy` | Deep nesting works correctly | -| `multiple inheritance via intersection` | `A & B` traits combine properly | - -## Comparison with Other Libraries - -### Effect-TS - -Effect uses a similar nested structure with self-mapping: - -```typescript -// Effect-TS approach -// See: https://github.com/Effect-TS/effect/blob/main/packages/effect/src/Brand.ts -type Brand = { [k in K]: K }; - -// Usage -interface UserId extends Brand<"UserId"> {} -``` - -Effect maps `K` to itself (`{ UserId: "UserId" }`) rather than to `true`. Both approaches work for brand stacking; we chose boolean markers for simpler semantics: the marker's presence is what matters, not its value. - -### ArkType - -ArkType uses tuple-based branding: - -```typescript -// ArkType approach -// See: https://github.com/arktypeio/arktype/blob/main/ark/type/keywords/constructors/brand.ts -type Brand = [t, id]; -``` - -This is a fundamentally different pattern designed for ArkType's validation system. Tuples don't naturally support intersection-based stacking the way object types do. - -## Why Boolean Markers? - -We chose `true` as the marker value for several reasons: - -1. **Semantic clarity**: The marker's presence indicates the brand applies; `true` communicates this directly -2. **Conventional pattern**: Boolean flags are a common TypeScript idiom for feature detection -3. **Minimal surface**: `true` is the simplest possible "yes" value - -The alternative of self-mapping (`{ UserId: "UserId" }`) works identically at runtime but adds conceptual overhead: why store the name twice? - -## Practical Implications - -This implementation enables real-world hierarchical type systems: - -```typescript -// File system paths with progressive refinement -type AbsolutePath = string & Brand<"AbsolutePath">; -type ProjectDir = AbsolutePath & Brand<"ProjectDir">; -type ProviderDir = ProjectDir & Brand<"ProviderDir">; - -// A ProviderDir is valid anywhere an AbsolutePath is expected -function readFile(path: AbsolutePath): string { /* ... */ } - -const providerPath: ProviderDir = "/project/providers/openai" as ProviderDir; -readFile(providerPath); // ✅ Compiles - ProviderDir extends AbsolutePath - -// But an AbsolutePath is NOT valid where a ProviderDir is expected -function getProviderConfig(path: ProviderDir): Config { /* ... */ } - -const absolutePath: AbsolutePath = "/some/path" as AbsolutePath; -getProviderConfig(absolutePath); // ❌ Error - AbsolutePath doesn't have ProviderDir brand -``` - -This is the same subtyping relationship you'd expect from class inheritance, but achieved purely through type-level composition. - -## Further Reading - -- [Brand Types](/core/brand-types): Usage guide and common patterns -- [Effect-TS Brand module](https://github.com/Effect-TS/effect/blob/main/packages/effect/src/Brand.ts): Alternative implementation with self-mapping -- [TypeScript Handbook: Branded Types](https://www.typescriptlang.org/play/#example/nominal-typing): Official playground example diff --git a/docs/philosophy/design-principles.mdx b/docs/philosophy/design-principles.mdx deleted file mode 100644 index de0fe33..0000000 --- a/docs/philosophy/design-principles.mdx +++ /dev/null @@ -1,329 +0,0 @@ ---- -title: 'Design Principles' -description: 'The philosophical foundations behind wellcrafted' -icon: 'lightbulb' ---- - -# Design Principles - -> "The best programs are written not by adding features, but by removing them." -> Antoine de Saint-Exupéry (paraphrased) - -wellcrafted is built on four core principles that guide every design decision. These aren't abstract ideals; they're practical philosophies proven in 22,824 lines of production TypeScript code. - -## 1. Errors as Values, Not Control Flow - -### The Problem: Hidden Exceptions - -Most JavaScript code treats errors as invisible, exceptional events: - -```typescript -// What can go wrong here? 🤷 -async function saveUser(user: User): Promise { - await validateUser(user); // Throws ValidationError? - await checkPermissions(); // Throws AuthError? - await database.save(user); // Throws DatabaseError? - return user; -} - -// Callers are gambling -try { - const savedUser = await saveUser(userData); - // Success path - but what could have failed? -} catch (error) { - // Failure path - but which failure? What type? - console.error("Something went wrong:", error.message); -} -``` - -**The hidden cost**: Every function call is a potential landmine. Errors can bubble up through multiple layers, crashing your application in production when you least expect it. - -### The Solution: Explicit Error Types - -wellcrafted makes every possible failure visible in your function signatures: - -```typescript -// Every error is visible and typed ✨ -async function saveUser( - user: User -): Promise> { - const validation = await validateUser(user); - if (validation.error) return validation; - - const auth = await checkPermissions(); - if (auth.error) return auth; - - const saved = await database.save(user); - if (saved.error) return saved; - - return Ok(user); -} - -// Callers handle errors as data -const { data: savedUser, error } = await saveUser(userData); -if (error) { - // TypeScript knows exactly what errors are possible - switch (error.name) { - case "ValidationError": - showValidationMessages(error.fields); - break; - case "AuthError": - redirectToLogin(); - break; - case "DatabaseError": - showRetryButton(); - break; - } -} else { - // TypeScript knows savedUser exists here - console.log("User saved:", savedUser.id); -} -``` - -**Why this works better**: -- **No surprise exceptions**: You see all failure modes upfront -- **Type safety**: TypeScript lets you enforce that every error case is handled -- **Debuggable**: Error context shows exactly what went wrong -- **Serializable**: Errors are plain objects that work everywhere - -### The Mental Model Shift - -Traditional exception handling asks: "What might go wrong?" - -Result types ask: "What are all the possible outcomes?" - -This shift from exceptional cases to explicit cases transforms error handling from a defensive afterthought into a core part of your API design. - -## 2. Work With the Language, Not Against It - -### JavaScript's Hidden Strengths - -JavaScript gets a lot of criticism, but it has some genuinely useful features that wellcrafted embraces: - -**Plain Objects**: No classes, no prototypes, no inheritance complexity. Just `{ data, error }` objects that work everywhere. - -**Destructuring**: The familiar `const { data, error } = ...` pattern that's already used by Supabase, Astro Actions, and countless other libraries. - -**Discriminated Unions**: TypeScript's type system can automatically narrow types based on the `error` property being `null` or non-`null`. - -```typescript -// TypeScript automatically knows the types here -const result = await fetchUser(id); - -if (result.error) { - // TypeScript knows: result.error is non-null, result.data is null - console.error(result.error.message); -} else { - // TypeScript knows: result.error is null, result.data is User - console.log(result.data.name); -} -``` - -### What We Don't Do - -**No Method Chaining**: Rust's `.map()` and `.and_then()` read well in Rust, but feel foreign in JavaScript. We use standard JavaScript control flow instead. - -**No Classes**: Error objects are plain data structures, not class instances that lose their prototype when serialized. They use `name` and `message` because that's what JavaScript's `Error` class already uses; see [Why `name` and `message`](/philosophy/why-name-and-message) for the full reasoning. - -**No Complex Abstractions**: The entire Result core is ~50 lines of code you can read and understand in 5 minutes. - -**No Hidden Behavior**: Every pattern is explicit and visible. No concealed behavior, no surprising transformations. - -### The Unix Philosophy for TypeScript - -wellcrafted follows the Unix philosophy: do one thing well, and compose with other tools. - -- **Result types**: Handle success/failure states -- **Tagged errors**: Provide structured, discriminated error data -- **Brand types**: Add semantic meaning to primitives -- **tryAsync/trySync**: Bridge between throwing and non-throwing code - -Each primitive is simple, but they compose to handle complex scenarios: - -```typescript -// Compose with standard JavaScript patterns -const results = await Promise.all([ - tryAsync({ try: () => fetchUser(id1), catch: handleUserError }), - tryAsync({ try: () => fetchUser(id2), catch: handleUserError }), - tryAsync({ try: () => fetchUser(id3), catch: handleUserError }), -]); - -const errors = results.filter(r => r.error !== null); -if (errors.length > 0) { - return Err({ - name: "BatchError", - message: `Failed to fetch ${errors.length} users`, - errors: errors.map(e => e.error), - }); -} - -const users = results.map(r => r.data!); -``` - -## 3. Make the Implicit Explicit - -### Hidden Behavior is the Enemy - -The most dangerous code is code that hides its behavior. wellcrafted makes everything visible: - -**Visible Failure Modes**: Function signatures show exactly what can go wrong. - -```typescript -// Hidden: What errors can this throw? -function processPayment(amount: number): Promise - -// Explicit: You can see every possible outcome -function processPayment( - amount: number -): Promise> -``` - -**Visible Side Effects**: Error context shows exactly what data was involved. - -```typescript -// Hidden: Generic error message -throw new Error("Payment failed"); - -// Explicit: Rich context for debugging -return Err({ - name: "PaymentError", - message: "Payment processing failed", - amount, - paymentMethodId, - userId, - attemptTimestamp: new Date().toISOString(), - gatewayResponse: response.status, -}); -``` - -**Visible Error Flow**: You can trace exactly how errors propagate through your system. - -### The Onion Architecture Effect - -When errors are explicit, you naturally develop better separation of concerns: - -```typescript -// Service layer: Domain-specific errors -async function withdrawFunds( - account: Account, - amount: number -): Promise> { - // Pure business logic with explicit error types -} - -// API layer: Transform to HTTP responses -async function handleWithdraw(req: Request): Promise { - const { data: transaction, error } = await withdrawFunds(account, amount); - - if (error) { - switch (error.name) { - case "InsufficientFundsError": - return new Response(JSON.stringify(error), { status: 400 }); - case "AccountLockedError": - return new Response(JSON.stringify(error), { status: 403 }); - } - } - - return new Response(JSON.stringify(transaction)); -} -``` - -Each layer handles only the errors it should know about. No leaky abstractions, no generic "something failed" messages. - -## 4. Composition Over Complexity - -### Simple Primitives, Complex Solutions - -wellcrafted provides just enough primitives to handle any error scenario, but no more: - -**`Result`**: Represents success or failure -**`defineErrors`**: Define structured, discriminated error data -**`tryAsync`/`trySync`**: Convert throwing code to Result types -**`Brand`**: Add semantic meaning to primitive types - -That's it. ~50 lines of core code. - -Compare this to enterprise error handling libraries with hundreds of methods, complex inheritance hierarchies, and configuration objects. wellcrafted's simplicity is a feature, not a limitation. - -### Real-World Complexity, Simple Tools - -The Whispering application demonstrates how simple primitives handle complex real-world scenarios: - -- **22,824 lines of TypeScript** -- **97% code sharing** between desktop and web platforms -- **Multiple transcription providers** with unified error handling -- **Complex multi-step workflows** with comprehensive error recovery -- **Zero runtime crashes** in production - -All built with the same simple primitives you get in wellcrafted. - -### The Combinatorial Power of Simplicity - -When primitives compose well, you get exponential flexibility: - -```typescript -// Error aggregation -const errors = await Promise.all([ - validateName(data.name), - validateEmail(data.email), - validateAge(data.age) -]); - -// Error transformation -const apiError = mapDatabaseError(dbError); - -// Error recovery -const result = await retry(operation, 3); - -// Error monitoring -logStructuredError(error, traceId); -``` - -Each pattern is simple, but they combine to handle sophisticated error scenarios without additional framework complexity. - -## Why These Principles Matter - -### Predictability - -When errors are values and behavior is explicit, your code becomes predictable: -- No surprise exceptions crashing your app -- No "undefined is not a function" errors -- No lost error context across serialization boundaries - -### Maintainability - -Explicit errors and simple primitives make code easier to maintain: -- New team members can see all failure modes -- Refactoring is safer with compiler assistance -- Error handling patterns are consistent across the codebase - -### Debuggability - -Structured errors with rich context make debugging straightforward: -- Error context shows exactly what data caused the problem -- Error chains preserve the full failure story -- Serializable errors work in all environments (browser, Node.js, workers) - -### Scalability - -Simple primitives that compose well scale from scripts to applications: -- No framework lock-in or vendor dependencies -- Zero-dependency core that works everywhere -- Patterns that grow with your application complexity - -## Conclusion - -wellcrafted's design principles aren't academic abstractions; they're practical philosophies proven in production code. By treating errors as values, working with JavaScript's strengths, making behavior explicit, and favoring composition over complexity, we create code that is: - -- **Reliable**: No hidden failure modes or surprise exceptions -- **Maintainable**: Clear patterns that new developers can understand -- **Debuggable**: Rich error context that makes problems obvious -- **Scalable**: Simple primitives that handle complex scenarios - -These principles guide every decision in wellcrafted, from the shape of the Result type to the design of the error system. They're not rules to follow blindly, but insights to help you build better, more reliable software. - -The next time you're tempted to add a try-catch block or throw an exception, ask yourself: "How can I make this failure explicit and type-safe instead?" That's the wellcrafted way. - ---- - -**Next**: Learn how these principles prevent production failures in [Production Reliability](/philosophy/production-reliability), or see how they improve day-to-day development in [Developer Experience](/philosophy/developer-experience). diff --git a/docs/philosophy/developer-experience.mdx b/docs/philosophy/developer-experience.mdx deleted file mode 100644 index b9f535b..0000000 --- a/docs/philosophy/developer-experience.mdx +++ /dev/null @@ -1,633 +0,0 @@ ---- -title: 'Developer Experience' -description: 'How Result types transform the development process' -icon: 'code' ---- - -# Developer Experience - -> "The best error handling is the error handling you don't have to think about." - -Good developer experience isn't just about clean syntax or helpful error messages. It's about designing systems that make the right thing easy and the wrong thing hard. wellcrafted transforms error handling from a source of bugs into a source of confidence. - -## The Mental Model Shift - -### From "Catching" to "Handling" - -Traditional exception handling puts developers in a defensive mindset: - -```typescript -// Defensive programming: "What might go wrong?" -try { - const user = await getUser(id); - try { - const profile = await getProfile(user.id); - try { - const preferences = await getPreferences(profile.id); - return buildUserData(user, profile, preferences); - } catch (prefError) { - return buildUserData(user, profile, null); - } - } catch (profileError) { - throw new Error("Profile required"); - } -} catch (userError) { - throw new Error("User not found"); -} -``` - -**The cognitive load**: you're thinking about exceptions, not business logic. Each nested try-catch increases mental complexity. Error handling patterns are inconsistent. It's unclear which errors can be recovered from. - -Result types shift you to a constructive mindset: - -```typescript -// Constructive programming: "What are all the possible outcomes?" -async function loadUserData(id: string): Promise> { - const userResult = await getUser(id); - if (userResult.error) return userResult; - - const profileResult = await getProfile(userResult.data.id); - if (profileResult.error) return profileResult; - - const preferencesResult = await getPreferences(profileResult.data.id); - if (preferencesResult.error) { - // Explicit decision: preferences are optional - return Ok(buildUserData(userResult.data, profileResult.data, null)); - } - - return Ok(buildUserData(userResult.data, profileResult.data, preferencesResult.data)); -} -``` - -Now you're thinking about data flow, not control flow. Each step is explicit about what it needs and returns. Business logic decisions (like "preferences are optional") are visible in the code. - -### From "Hoping It Works" to "Knowing It Works" - -Exception-based code creates uncertainty: - -```typescript -// What errors can this throw? When? Under what conditions? -async function processOrder(orderId: string) { - const order = await fetchOrder(orderId); // Throws? What type? - await validateOrder(order); // Throws? What type? - const payment = await processPayment(order); // Throws? What type? - await updateInventory(order.items); // Throws? What type? - await sendConfirmationEmail(order.customerEmail); // Throws? What type? - return order; -} -``` - -Result types create certainty: - -```typescript -async function processOrder( - orderId: string -): Promise> { - const orderResult = await fetchOrder(orderId); - if (orderResult.error) return orderResult; - - const validationResult = await validateOrder(orderResult.data); - if (validationResult.error) return validationResult; - - const paymentResult = await processPayment(orderResult.data); - if (paymentResult.error) return paymentResult; - - const inventoryResult = await updateInventory(orderResult.data.items); - if (inventoryResult.error) return inventoryResult; - - const emailResult = await sendConfirmationEmail(orderResult.data.customerEmail); - if (emailResult.error) { - // Business decision: email failure doesn't fail the order - console.warn("Failed to send confirmation email:", emailResult.error); - } - - return Ok(orderResult.data); -} -``` - -You know exactly what can go wrong at each step. You make explicit decisions about error recovery. Future maintainers can understand the error behavior without running the code. - -## TypeScript Integration That Actually Helps - -### Discriminated Unions That Guide You - -TypeScript's type system becomes your error-handling assistant: - -```typescript -type ApiResult = Result; - -async function handleApiCall(result: ApiResult) { - if (result.error) { - switch (result.error.name) { - case "NetworkError": - if (result.error.status === 429) { - return scheduleRetry(result.error.retryAfter); - } - return showNetworkErrorDialog(); - - case "AuthError": - return redirectToLogin(result.error.returnUrl); - - case "ValidationError": - return highlightValidationErrors(result.error.fields); - - default: { - // add a variant without a case and this assignment stops compiling - const _exhaustive: never = result.error; - return _exhaustive; - } - } - } else { - return processSuccessfulResult(result.data); - } -} -``` - -**What TypeScript gives you**: -- **Exhaustive checking**: TypeScript warns if you miss an error case -- **Type narrowing**: TypeScript knows the exact error type in each branch -- **Autocompletion**: Your IDE shows the available properties for each error type -- **Refactoring safety**: Adding new error types causes compilation errors until you handle them - -### No More `any` or `unknown` Errors - -Traditional exception handling forces you to work with untyped errors: - -```typescript -try { - const result = await someAsyncOperation(); - return result; -} catch (error) { - // error is unknown - you have no type information - if (error instanceof NetworkError) { - // instanceof checks don't work across serialization boundaries - return handleNetworkError(error); - } - throw new Error("Unknown error occurred"); -} -``` - -Result types preserve full type information: - -```typescript -const { data, error } = await someAsyncOperation(); - -if (error) { - switch (error.name) { - case "NetworkError": - console.error(`Network request failed: ${error.message}`, { - url: error.url, - status: error.status, - }); - return handleNetworkError(error); - - case "ValidationError": - console.error(`Validation failed: ${error.message}`, { - fields: error.invalidFields, - }); - return handleValidationError(error); - } -} else { - return processResult(data); -} -``` - -**Type safety benefits**: -- **No runtime type errors**: You can't access properties that don't exist -- **Full IntelliSense support**: Your IDE knows exactly what properties are available -- **Serialization safety**: Error objects work across all boundaries (JSON, workers, network) -- **Refactoring confidence**: Changing error shapes causes compilation errors everywhere they're used - -## IDE Support That Actually Works - -### IntelliSense for Error Handling - -Your IDE becomes an error-handling guide: - -```typescript -// When you type result.error. your IDE shows: -result.error.name // "NetworkError" | "AuthError" | "ValidationError" -result.error.message // string -result.error.url // (on NetworkError) string -result.error.status // (on NetworkError) number - -// When you're in a switch statement, your IDE knows the specific type: -switch (result.error.name) { - case "NetworkError": - result.error. // IDE shows: url, status, retryAttempt, etc. - break; - case "AuthError": - result.error. // IDE shows: userId, requiredPermission, etc. - break; -} -``` - -### Jump-to-Definition for Error Types - -Click on any error name to see its definition: - -```typescript -// Click on "PaymentError" to see its inferred type: -type PaymentError = Readonly<{ - name: "PaymentError"; - message: string; - orderId: string; - amount: number; - paymentMethod: string; - gatewayResponse?: string; -}>; -``` - -**Navigation benefits**: -- **Error schema discovery**: You can see exactly what context each error provides -- **Consistent error handling**: You can see how other parts of the codebase handle the same errors -- **Error documentation**: Error types serve as their own documentation - -### Refactoring with Confidence - -When you add a new error type to a union: - -```typescript -// Before: only NetworkError and AuthError -type ApiError = NetworkError | AuthError; - -// After: add RateLimitError -type ApiError = NetworkError | AuthError | RateLimitError; -``` - -TypeScript immediately shows you every place that needs updating: - -```typescript -function handleApiError(error: ApiError) { - switch (error.name) { - case "NetworkError": - return handleNetworkError(error); - case "AuthError": - return handleAuthError(error); - default: { - // RateLimitError now reaches here, so `error` is not `never`: - const _exhaustive: never = error; // compile error until you add the case - return _exhaustive; - } - } -} -``` - -The `never` assignment in `default` is what makes this a compile error; a plain `switch` would let the new variant fall through silently. - -**Refactoring safety**: -- **No missed error cases**: TypeScript finds every location that needs updating -- **Gradual migration**: You can update one function at a time -- **Compilation-time verification**: You know your error handling is complete before running any code - -## Debugging That Makes Sense - -### Error Context That Tells a Story - -Traditional stack traces show you where the error occurred, but not why: - -``` -Error: Request failed - at fetch (http-client.js:45:12) - at getUserData (user-service.js:23:8) - at loadProfile (profile-page.js:67:15) -``` - -wellcrafted errors tell the complete story: - -```json -{ - "name": "NetworkError", - "message": "Failed to fetch user data after 3 retry attempts", - "userId": "user_12345", - "requestUrl": "https://api.example.com/users/user_12345", - "requestMethod": "GET", - "finalStatus": 503, - "retryAttempts": 3, - "totalRequestTime": 15432 -} -``` - -You can answer immediately: which user, what request, why it failed, how long we tried. - -### Error Layers That Preserve Context - -Complex operations create errors at each layer that maintain context: - -```typescript -// Database layer -const dbError = { - name: "DatabaseError", - message: "Connection pool exhausted", - poolSize: 10, - activeConnections: 10, - queueLength: 15, -}; - -// Service layer -const serviceError = { - name: "UserServiceError", - message: "Failed to load user data", - userId: "user_12345", - operation: "loadUserProfile", -}; - -// API layer -const apiError = { - name: "ApiError", - message: "User profile request failed", - endpoint: "/api/users/user_12345/profile", - requestId: "req_xyz789", -}; -``` - -**Debugging workflow**: start at the top (API request failed), drill down (which operation), find root cause (connection pool exhausted), understand impact (15 queued requests), plan solution (scale pool or add circuit breaker). - -### Source Maps That Actually Help - -Because wellcrafted errors are explicit in your code, source maps point to the actual error handling logic: - -```typescript -async function processPayment(order: Order): Promise> { - const { data: validation, error } = await validatePayment(order); - if (error) return Err(error); - - const { data: payment, error: paymentError } = await chargeCard(order.total); - if (paymentError) { - // Source map points here - you can see the exact error creation - return Err({ - name: "PaymentError", - message: "Credit card charge failed", - orderId: order.id, - amount: order.total, - cardLast4: order.paymentMethod.last4, - gatewayResponse: paymentError.gatewayResponse, - }); - } - - return Ok(payment); -} -``` - -Stack traces point to business logic, not exception handling. You can see exactly where each error type is created and what context is being captured. - -## Testing Benefits - -### No More Try-Catch in Tests - -Traditional exception testing is verbose and unclear: - -```typescript -describe("payment processing", () => { - it("should throw PaymentError for invalid card", async () => { - await expect(processPayment(invalidOrder)).rejects.toThrow(); - - try { - await processPayment(invalidOrder); - fail("Expected error to be thrown"); - } catch (error) { - expect(error).toBeInstanceOf(PaymentError); - expect(error.message).toContain("invalid card"); - } - }); -}); -``` - -Result testing is clear and explicit: - -```typescript -describe("payment processing", () => { - it("should return PaymentError for invalid card", async () => { - const result = await processPayment(invalidOrder); - - expect(result.error).toBeDefined(); - expect(result.error?.name).toBe("PaymentError"); - expect(result.error?.message).toContain("invalid card"); - expect(result.error?.cardLast4).toBe("1234"); - expect(result.data).toBeNull(); - }); - - it("should return payment data for valid order", async () => { - const result = await processPayment(validOrder); - - expect(result.error).toBeNull(); - expect(result.data).toBeDefined(); - expect(result.data?.amount).toBe(validOrder.total); - }); -}); -``` - -### Testing Both Success and Failure Paths - -Result types make it natural to test both outcomes: - -```typescript -describe("user registration", () => { - it("should create user with valid data", async () => { - const result = await registerUser(validUserData); - - expect(result.error).toBeNull(); - expect(result.data.id).toBeDefined(); - expect(result.data.email).toBe(validUserData.email); - }); - - it("should return ValidationError for invalid email", async () => { - const result = await registerUser({ ...validUserData, email: "invalid" }); - - expect(result.error?.name).toBe("ValidationError"); - expect(result.error?.field).toBe("email"); - }); - - it("should return ConflictError for duplicate email", async () => { - await registerUser(validUserData); - const result = await registerUser(validUserData); - - expect(result.error?.name).toBe("ConflictError"); - expect(result.error?.email).toBe(validUserData.email); - }); -}); -``` - -**Testing advantages**: -- Symmetric success/failure testing with the same assertion patterns -- Type-safe test assertions that match actual error types -- Clear test intent showing which outcome is being verified - -### Mock and Stub Simplification - -Mocking error scenarios becomes straightforward: - -```typescript -const mockUserService = { - async getUser(id: string): Promise> { - if (id === "user_404") { - return Err({ - name: "UserError", - message: "User not found", - userId: id, - }); - } - - if (id === "user_500") { - return Err({ - name: "UserError", - message: "Database connection failed", - userId: id, - errorCode: "DB_TIMEOUT", - }); - } - - return Ok({ id, name: "Test User", email: "test@example.com" }); - } -}; - -describe("profile page", () => { - it("should show 404 page for missing user", async () => { - const result = await loadProfilePage("user_404"); - expect(result.pageType).toBe("not-found"); - }); - - it("should show error message for system errors", async () => { - const result = await loadProfilePage("user_500"); - expect(result.pageType).toBe("error"); - expect(result.errorMessage).toContain("connection failed"); - }); -}); -``` - -Each mock clearly defines what error conditions it simulates, implementations must match actual Result types, and mocks can provide realistic error context for comprehensive testing. - -## Common Patterns That Become Natural - -These patterns quickly become intuitive: - -**Early return for errors**: -```typescript -const result = await someOperation(); -if (result.error) return result; -// Continue with result.data -``` - -**Error transformation**: -```typescript -const result = await lowLevelOperation(); -if (result.error) { - return mapToHighLevelError(result.error); -} -``` - -**Partial failure handling**: -```typescript -const results = await Promise.all([required(), optional(), optional()]); -if (results[0].error) return results[0]; // Required operation failed -// Continue with partial data from optional operations -``` - -**Error aggregation**: -```typescript -const errors = results.filter(r => r.error).map(r => r.error); -if (errors.length > 0) { - return Err(createAggregateError(errors)); -} -``` - -## Wrapping Legacy Code - -You don't need to rewrite your entire codebase. Use `tryAsync` with `defineErrors` to wrap exception-throwing functions: - -```typescript -const UserError = defineErrors({ - Fetch: ({ userId, cause }: { userId: string; cause: unknown }) => ({ - message: `Failed to fetch user ${userId}: ${extractErrorMessage(cause)}`, - userId, - cause, - }), -}); - -async function getUser(id: string) { - return tryAsync({ - try: () => legacyGetUser(id), - catch: (error) => UserError.Fetch({ userId: id, cause: error }), - }); -} -``` - -This lets you adopt Result types incrementally: new code uses Results directly, legacy code gets wrapped at the boundary. The factory owns the message template and the `extractErrorMessage` call, so call sites stay clean. - -### Framework Integration - -wellcrafted works naturally with existing libraries and frameworks: - -**Express.js**: -```typescript -async function handleUserRequest(req: Request, res: Response) { - const { data: user, error } = await getUser(req.params.id); - - if (error) { - switch (error.name) { - case "NotFound": - return res.status(404).json({ error: error.message }); - case "Database": - return res.status(500).json({ error: "Internal server error" }); - } - } - - res.json(user); -} -``` - -**React components**: -```typescript -function UserProfile({ userId }: { userId: string }) { - const [state, setState] = useState<{ - data: User | null; - error: UserError | null; - loading: boolean; - }>({ data: null, error: null, loading: true }); - - useEffect(() => { - getUser(userId).then(result => { - setState({ - data: result.data, - error: result.error, - loading: false - }); - }); - }, [userId]); - - if (state.loading) return
Loading...
; - if (state.error) return ; - return ; -} -``` - -## Conclusion - -wellcrafted transforms error handling from a source of bugs into a source of confidence. The developer experience improvements go beyond syntax; they fundamentally change how you think about and work with failure scenarios. - -**Mental model benefits**: -- Shift from defensive exception handling to constructive outcome handling -- Error handling becomes part of business logic, not an afterthought -- Confidence in what can go wrong and how to handle it - -**TypeScript integration benefits**: -- Full type information for both success and error cases -- Exhaustive error handling enforced by the compiler -- IntelliSense and refactoring support that actually helps - -**Debugging and testing benefits**: -- Rich error context that tells the complete failure story -- Clear, explicit testing patterns for both success and failure paths -- Source maps that point to meaningful business logic - -Once you experience error handling that actually helps you write better code, it's hard to go back to hoping exceptions don't happen. You're not just writing more reliable code; you're writing code that helps future developers (including yourself) understand and maintain complex error scenarios. - ---- - -**Related**: Explore the foundational philosophy in [Design Principles](/philosophy/design-principles), or see how these patterns prevent production failures in [Production Reliability](/philosophy/production-reliability). diff --git a/docs/philosophy/err-null-is-ok-null.md b/docs/philosophy/err-null-is-ok-null.md deleted file mode 100644 index 6351be8..0000000 --- a/docs/philosophy/err-null-is-ok-null.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: "Err(null) Is Ok(null)" -description: "Why wellcrafted can't tell a null-valued failure from a null-valued success, what we tried to do about it, and why we stopped trying" -icon: 'equal' ---- - -# Err(null) Is Ok(null) - -wellcrafted's Result shape has a blind spot. If you pass `null` to the `Err` constructor, the runtime can't tell the resulting value apart from `Ok(null)`. We noticed. We tried to fix it with a type-level ban. Then we reverted. This is the story. - -## The shape - -wellcrafted's headline feature is that a Result looks like what you already know: the same `{ data, error }` shape Supabase and SvelteKit load functions use: - -```typescript -type Ok = { data: T; error: null }; -type Err = { error: E; data: null }; -type Result = Ok | Err; -``` - -And the built-in discriminator checks the error side: - -```typescript -const isErr = (r: Result): r is Err => r.error !== null; -``` - -That shape is why you can destructure a Result the same way you destructure a Supabase response, and why the library fits any codebase that already uses this pattern. It's the trade that earns the library its keep. - -It's also why `Err(null)` breaks. - -## The collision - -Construct an Ok with a null payload and an Err with a null reason: - -```typescript -Ok(null) // { data: null, error: null } -Err(null) // { error: null, data: null } -``` - -Same runtime object. Property order doesn't matter in JavaScript. `isErr` checks `error !== null`, which returns `false` for both. So: - -```typescript -const result = Err(null); -isOk(result); // true, wrong -isErr(result); // false, wrong -``` - -Your failure became a success. The type system said the variable was `Err`; the runtime said it was `Ok`. The discriminator lied. - -This isn't a bug we can patch. It's the shape telling you what it can and can't represent. - -## Shape-based Result libraries have a structural limit: they can't distinguish "success with null payload" from "failure with null reason." - -A tagged-union Result can: - -```rust -// Rust: two variants with a discriminant byte -enum Result { Ok(T), Err(E) } - -let success: Result<(), ()> = Ok(()); -let failure: Result<(), ()> = Err(()); -matches!(failure, Err(_)) // true -``` - -`Ok(())` and `Err(())` are runtime-distinguishable even when `T` and `E` are both the unit type. The discriminant byte holds the tag. `match` reads the byte, not the payload. - -wellcrafted can't do this without giving up the destructuring shape. No discriminant byte lives in `{ data, error }`. The shape *is* the discriminator, and when both slots are `null`, the shape has nothing to say. - -This isn't a universal property of Results. It's a consequence of the shape we chose. Rust disagrees with wellcrafted because Rust chose differently. - -## The type-level ban we tried - -The obvious patch: ban `Err(null)` at the constructor. - -```typescript -// The attempt -export const Err = >(error: E): Err => - ({ error, data: null }); -``` - -`NonNullable` excludes `null` and `undefined`. `Err(null)` becomes a compile error. Problem solved. - -We shipped it. Reviewed it. Reverted it. Here's why. - -### The enforcement is shallow - -The ban catches the literal case: `Err(null)` as written. Everything else slips through. - -```typescript -Err(value as any) // bypassed: any cast defeats the constraint -Err(value as NonNullable) // bypassed: the cast is a lie if value is actually null -Err(value) // bypassed if typeof value permits null via a bad upstream type -({ error: null, data: null }) as Err // bypassed: direct object construction -``` - -Most damning: the migration for the ban itself used this pattern: - -```typescript -// From src/query/utils.ts in the ban PR -catch (error) { - return Err(error as NonNullable); -} -``` - -The `as NonNullable` cast silences TypeScript without preventing the runtime case. If `TError` includes null-thrown values (and `catch (e: unknown)` always does, since `throw null` is legal JavaScript), this cast *is* the bug it claims to fix. The ban's own migration produced unsafe casts. - -### The cost is wide - -The ban added friction at every `catch` boundary in every downstream codebase. The TC39/TS consensus is `catch (e): unknown`. That's the type every catch in every codebase has. When you write: - -```typescript -catch: (error) => Err(error) -``` - -...the ban triggers a type error because `unknown` includes `null | undefined`. You now have to reach for either a cast (which doesn't enforce the invariant) or a different pattern (which is the real answer, but the type error doesn't tell you so). - -Multiply that by every `tryAsync`/`trySync` catch in every service file in every consumer, and you've added a lot of friction for a nudge the type error can't clearly articulate. - -### The teaching value is replaceable - -What the ban *wanted* to teach: "don't pass raw `unknown` to `Err`; wrap it in a tagged error instead." - -What the ban *actually* taught, most of the time: "add `as NonNullable` to make the type error go away." - -The fix at hand is a cast. The cast is wrong. The type error doesn't explain the right fix. So the ban teaches the wrong lesson more often than the right one. - -The correct lesson (**use `defineErrors` and pass `{ cause: error }`**) is better taught by documentation than by a compile error. The tagged error is non-null by construction, so the shape's invariant holds even if the cause was `null`. Documentation + idiom enforces the rule more reliably than the constraint does. - -## What we shipped instead - -Errs can still be constructed with any value. `Err` has no `NonNullable` constraint. Pre-validation, like every Result library in the shape family, is a documentation and idiom problem, not a type-system problem. - -The documented rule: - -> `Err(null)` produces `{ data: null, error: null }`, structurally identical to `Ok(null)`. Under our shape, `isErr`/`isOk` read it as Ok, so `Err(null)` silently becomes success. `Err(undefined)` is also discouraged: the discriminator technically works (the error field is `undefined`, not `null`), but `undefined` is falsy so `if (error)` checks trip downstream, and the error carries no information. **Don't call `Err` with `null` or `undefined`.** Either: -> -> - Use `Ok(null)`/`Ok(undefined)` (if what you meant was success-with-no-payload). -> - Define a tagged error via `defineErrors` with a real name. -> - Wrap a caught exception as `TaggedError.Unexpected({ cause: error })`. - -And the deeper rule, which the tagged-errors idiom expresses automatically: - -> At every `catch (error: unknown)` boundary, don't pass the raw `unknown` to `Err`. Wrap it in a tagged error. The tagged error is non-null by construction, so the shape's invariant holds regardless of what was caught. - -```typescript -// The pattern -const Errors = defineErrors({ - Unexpected: ({ cause }: { cause: unknown }) => ({ - message: extractErrorMessage(cause), - cause, - }), -}); - -const result = await tryAsync({ - try: async () => fetchThing(), - catch: (error) => Errors.Unexpected({ cause: error }), -}); -``` - -The tagged error `{ name: 'Unexpected', message, cause, ... }` is always non-null; it's a constructed object. `Err(taggedError)` produces `{ error: taggedError, data: null }`, which has a non-null error side. `isErr` reads it correctly. The shape's invariant is preserved, and the author didn't have to know the invariant existed. - -## The meta-lesson - -Shape choices are invariant choices. When wellcrafted picked the destructure-friendly `{ data, error }` shape, it picked a discriminator (`error !== null`) that implicitly assumes error values are never null. That assumption isn't documented in the shape (the shape has no way to document it), so it has to live as a convention. - -We tried to promote the convention into a type-level constraint. The attempt failed because: - -1. The constraint only catches literal null errors, not the broader class of runtime-null errors coming through `unknown`-typed boundaries. -2. The friction is paid at every catch boundary in every consumer codebase. -3. The intended lesson (use tagged errors) is better delivered by documentation than by a type error whose natural fix is a cast. - -The honest move is to keep the shape, document the convention, and let the tagged-errors idiom carry the enforcement. The shape has a limit. We can't patch around it in the types. We can make the limit easy to avoid in practice. - -If you ever find yourself writing `Err(null)`, the shape is telling you something: either you meant `Ok(null)`, or you haven't defined the error type yet. Both are fixable by looking at the call site. Neither is fixable by the constructor. - -## What this does and doesn't change - -- **The shape:** unchanged. Still `{ data: T; error: null } | { data: null; error: E }`. -- **Discriminators:** unchanged. Still `isErr(r) === r.error !== null`. -- **The `Err` constructor:** accepts any `E`. No `NonNullable` constraint. -- **The skill:** stronger. The rule was implicit; now it's documented with examples. -- **Existing consumers:** nothing to migrate. Code that uses tagged errors (the recommended pattern) is unaffected either way. - -If you came here looking for a type-level guarantee that `Err(null)` won't happen, you won't find one. What you'll find is a library that makes the right pattern the path of least resistance, and a documentation page (this one) explaining why the alternative didn't work. diff --git a/docs/philosophy/error-api-evolution.mdx b/docs/philosophy/error-api-evolution.mdx deleted file mode 100644 index 245addd..0000000 --- a/docs/philosophy/error-api-evolution.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: "We Wrote a Builder Pattern, Then Deleted It" -description: "How wellcrafted's error API went from types-only to a fluent builder to the realization that errors are just functions" -icon: 'route' ---- - -# We Wrote a Builder Pattern, Then Deleted It - -Every error in wellcrafted started as a hand-written object literal. Nine months and seven rewrites later, the API is ten lines of runtime. The journey between those two points is a story about over-engineering, and the moment we realized the builder was hiding what errors actually are: a function that takes input and returns `{ name, message, ...data }`. - -## Hand-Written Error Objects Were Correct But Unusable - -The earliest API had no factory functions at all. You defined a `TaggedError` type and constructed errors by hand: - -```typescript -type TaggedError = Readonly<{ - name: T; - message: string; - context: Record; - cause: unknown; -}>; - -// Every. Single. Call site. -const error: TaggedError<'NetworkError'> = { - name: 'NetworkError', - message: 'Connection failed', - context: { url: '/api/data' }, - cause: undefined, -}; -``` - -`context` and `cause` were required. Even if you had nothing to put in them, you wrote `context: {}` and `cause: undefined`. The types were correct, but the ergonomics were brutal. - -## `createTaggedError` Automated the Boilerplate - -The first factory function arrived a month later. Give it a name, get back two factories: one for plain errors, one pre-wrapped in `Err`. - -```typescript -const { NetworkError, NetworkErr } = createTaggedError('NetworkError'); - -// Plain error object -NetworkError({ message: 'Connection failed', context: { url: '/api' }, cause: undefined }); - -// Same thing, wrapped in Err(...) -NetworkErr({ message: 'Connection failed', context: { url: '/api' }, cause: undefined }); -``` - -The name had to end in `Error`. The `Err` suffix variant was auto-generated by string manipulation: `NetworkError` became `NetworkErr`. This solved the repetition problem, but the required fields remained. You still couldn't create a simple error without passing empty context and an undefined cause. - -## Making Fields Optional Broke Type Safety - -We made `context` and `cause` optional. That fixed the simple case, but opened a new problem: how do you type-constrain the context shape? A `NetworkError` should require `{ url: string }` in its context. A `ParseError` should require `{ input: string }`. - -The answer was generic parameters. Then typed cause. Then function overloads to support every combination: - -```typescript -// Overload 1: no constraints -function createTaggedError(name: TName): FlexibleFactories; -// Overload 2: typed context -function createTaggedError(name: TName): ContextFactories; -// Overload 3: typed context + cause -function createTaggedError(name: TName): FullFactories; -``` - -This worked at call sites. But TypeScript's `ReturnType` picks the last overload. So `type NetworkError = ReturnType` resolved to the most constrained signature, requiring both context and cause even when you only wanted the flexible version. Users couldn't extract types from their own factories. - -## The Fluent Builder Made It Worse - -To escape the overload problem, we introduced a fluent builder. Instead of cramming everything into generic parameters, you chained method calls: - -```typescript -const { NetworkError, NetworkErr } = createTaggedError('NetworkError') - .withContext<{ url: string }>() - .withCause() - .withMessage(({ url }) => `Failed to connect to ${url}`); -``` - -Each method returned a new builder type with the constraint baked in. `.withContext()` locked the context shape. `.withCause()` locked the cause type. `.withMessage()` was the terminal step that produced the factories. - -This was the most "correct" version. It was also the most complex. The builder had four modes depending on which methods you called, three layers of type indirection, and about 60 lines of runtime. - -```typescript -// .withFields() was a phantom call: purely type-level, disguised as a method -const { FileError, FileErr } = createTaggedError('FileError') - .withFields<{ path: string; code: number }>() // does nothing at runtime - .withMessage(({ path, code }) => `File error ${code}: ${path}`); -``` - -`.withFields()` replaced the separate `.withContext()` and `.withCause()`, flattening fields directly onto the error. That required a `NoReservedKeys` constraint to prevent collisions with `name` and `message`, and `JsonObject` constraints for serializability that broke with optional fields. Each fix spawned a new edge case. - -## 321 Call Sites Killed Two Extreme Positions - -Before the final rewrite, we audited every error call site across the codebase. 321 of them. - -| Pattern | Frequency | Example | -|---|---|---| -| Static/predictable message | 59% | `() => ({ message: 'Session expired' })` | -| Dynamic call-site message | 41% | `({ message }) => ({ message })` | - -No error type mixed the two patterns. This killed two extreme positions: "always compute the message in the definition" and "always pass the message at the call site." A constructor function handles both cases naturally. If the message is static, compute it from fields. If it's dynamic, accept it as a parameter. - -## Rust's `thiserror` Reframed the Problem - -Around this time, we started looking at Rust's `thiserror` crate. In Rust, you write `enum HttpError { Connection, Response, Parse }`. The enum name is the namespace; the variant name is the discriminant. The `#[error("...")]` attribute co-locates the message template with each variant. You'd never write `HttpError::ConnectionError` because that's redundant. - -```rust -#[derive(Error, Debug)] -enum HttpError { - #[error("Failed to connect: {cause}")] - Connection { cause: String }, - - #[error("HTTP {status}")] - Response { status: u16 }, -} -``` - -This reframed the whole question. We'd been asking "how do we make the builder more flexible?" when the real question was "why do we have a builder at all?" Rust's error variants are just structs with a display format. No builder, no modes, no phantom type-level calls. Each variant is data in, message out. - -## The Builder Was Hiding a Plain Function - -That Rust framing made the next insight obvious: every error definition is just a constructor function. Take some input, produce `{ message, ...data }`. The four "modes" of the builder were four shapes of the same function: - -```typescript -// "Static message" mode -() => ({ message: 'Session expired' }) - -// "Call-site message" mode -({ message }: { message: string }) => ({ message }) - -// "Computed message" mode -({ cause }: { cause: unknown }) => ({ - message: `Failed: ${extractErrorMessage(cause)}`, - cause, -}) - -// "Structured data" mode -({ status }: { status: number; reason?: string }) => ({ - message: `HTTP ${status}`, - status, - reason, -}) -``` - -Four tiers of complexity. Zero modes. Just a function with different signatures. The builder was ceremony wrapping this. - -## `defineErrors` Combined Both Insights - -The Rust namespace pattern and the "just a function" insight converged into `defineErrors`. The entire runtime: - -```typescript -function defineErrors(config) { - const result = {}; - for (const [name, ctor] of Object.entries(config)) { - result[name] = (...args) => { - const body = ctor(...args); - return Err(Object.freeze({ ...body, name })); - }; - } - return result; -} -``` - -Iterate over the config. For each key, wrap the user's constructor: call it, stamp `name` from the key, freeze the object, wrap in `Err`. That's it. - -The config mirrors Rust's enum structure: the object name is the namespace, each key is a variant, each value is the constructor: - -```typescript -const HttpError = defineErrors({ - Connection: ({ cause }: { cause: unknown }) => ({ - message: `Failed to connect: ${extractErrorMessage(cause)}`, - cause, - }), - Response: ({ status }: { status: number }) => ({ - message: `HTTP ${status}`, - status, - }), - Parse: ({ cause }: { cause: unknown }) => ({ - message: `Failed to parse: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -type HttpError = InferErrors; -``` - -The early versions required keys ending in `Error`: `ConnectionError`, `ResponseError`, `ParseError`. Under a namespace already called `HttpError`, that's noise. Dropping the suffix gave us `HttpError.Connection`, `HttpError.Response`, `HttpError.Parse`: directly analogous to Rust's `HttpError::Connection`. - -``` -Rust: HttpError::Connection { cause: "timeout".into() } -TypeScript: HttpError.Connection({ cause: error }) -``` - -TypeScript's ability to share a name between a value and a type made the final piece work. `const HttpError` and `type HttpError` coexist, just like a Rust enum is both a type and a namespace: - -```typescript -const HttpError = defineErrors({ ... }); // value: namespace of factories -type HttpError = InferErrors; // type: union of all variants -``` - -## Every Fix Was Correct; the Aggregate Was Not - -| Version | Runtime | What it looked like | -|---|---|---| -| Hand-written objects | 0 lines | `{ name: 'X', message: '...', context: {}, cause: undefined }` | -| `createTaggedError` factory | ~15 lines | `createTaggedError('XError')` | -| Typed generics + overloads | ~25 lines | `createTaggedError(name)` | -| Fluent builder | ~60 lines | `createTaggedError('X').withFields().withMessage(fn)` | -| `defineErrors` | ~10 lines | `defineErrors({ X: (input) => ({ message, ...data }) })` | - -Overloads solved type safety. The builder solved overload limitations. `.withFields()` solved nesting. `.withMessage()` solved scattered formatting. Each fix was correct in isolation and added weight in aggregate. When your error definition is a plain function, there's nothing left to configure. diff --git a/docs/philosophy/for-the-pragmatic-fp-developer.mdx b/docs/philosophy/for-the-pragmatic-fp-developer.mdx deleted file mode 100644 index c38e33d..0000000 --- a/docs/philosophy/for-the-pragmatic-fp-developer.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: "wellcrafted Is for the Pragmatic Almost-Functional Programmer" -description: "You like Rust's error handling. You respect Effect. You just want something that works with TypeScript instead of against it." -icon: 'user-check' ---- - -# wellcrafted Is for the Pragmatic Almost-Functional Programmer - -You've read about Result types. You've seen Rust's `thiserror` and thought "I want that in TypeScript." You may have tried Effect, neverthrow, or fp-ts. You understood the ideas. You liked the ideas. But somewhere between the method chains and the generators, you went back to `try-catch` and moved on with your life. - -You're not alone, and you're not wrong. wellcrafted was built for exactly this gap. - ---- - -## The Profile - -You probably recognize yourself in a few of these: - -You think `Promise` is lying. A function that hits a database can fail, and the return type should say so. You've been bitten by unhandled promise rejections that crashed production at 2 AM because nothing in the type signature warned you. - -You've looked at Rust's error handling with genuine envy. `thiserror` defining error variants in five lines, the `?` operator propagating errors without ceremony, `match` expressions that the compiler checks exhaustively. You want that, but in TypeScript. - -You tried an FP error handling library and it felt like writing in a foreign language. The types were right, the code compiled, but every function became a wrapper around a wrapper. Your team's PRs slowed down. New contributors couldn't read the error handling code without a tutorial. - -You went back to `try-catch` not because you think it's better, but because the alternative was worse in practice. The cognitive tax wasn't paying off for your team. - -If that's you, here's the deal: you don't have to choose between "untyped try-catch" and "full functional runtime." There's a middle ground that gives you the one thing you actually wanted (typed, named errors) without the parts that didn't work. - ---- - -## What You Wanted vs. What You Got - -The core appeal of FP error handling has always been three things: errors in the type signature, exhaustive handling, and named variants you can branch on. Everything else (the method chains, the generators, the effect system) is machinery to support those three things in languages that have the features for it. - -TypeScript doesn't have those features. So the machinery becomes the product, and the original goals get buried under it. - -wellcrafted strips back to the goals: - -```typescript -const UserError = defineErrors({ - AlreadyExists: ({ email }: { email: string }) => ({ - message: `User ${email} already exists`, - email, - }), - CreateFailed: ({ email, cause }: { email: string; cause: unknown }) => ({ - message: `Failed to create user ${email}: ${extractErrorMessage(cause)}`, - email, - cause, - }), -}); -type UserError = InferErrors; -``` - -Errors in the type signature. Named variants. Exhaustive handling via `switch`. That's the whole pitch. - -```typescript -async function createUser(email: string): Promise> { - const existing = await db.findByEmail(email); - if (existing) return UserError.AlreadyExists({ email }); - - return tryAsync({ - try: () => db.users.create({ email }), - catch: (error) => UserError.CreateFailed({ email, cause: error }), - }); -} -``` - -No generators. No `.andThen()`. Just async/await with early returns, the same control flow you'd write anyway, except now the errors are typed. - ---- - -## The Compromises, Stated Plainly - -wellcrafted makes specific, deliberate trade-offs. You should know what they are. - -**No method chains.** There's no `.map()` or `.andThen()` on the Result type. Composition is `if (error) return error` and early returns. This is verbose compared to Rust's `?` operator. It's also immediately readable to anyone who knows TypeScript. - -**No dependency injection.** Effect's service system is genuinely well-designed. wellcrafted has nothing like it. Pass your dependencies as function arguments. It works. It's not as composable. - -**No runtime.** No fibers, no structured concurrency, no resource management. If you need those, Effect has them and wellcrafted doesn't. - -**Plain objects, not classes.** Errors are frozen plain objects. `instanceof` doesn't work. This is intentional: class instances don't survive `JSON.stringify`, and in any app that crosses a serialization boundary (Web Workers, IPC, message channels), that matters more than prototype chains. - -These are real things you give up. They're also things that most TypeScript applications don't need. The question isn't "which library has more features." It's "which features are you actually using, and what are you paying for the ones you're not." - ---- - -## Where This Fits in the Ecosystem - -| If you want... | Use | -|---|---| -| A full effect system with DI, fibers, and concurrency | Effect | -| Method chains on Result types | neverthrow | -| The full FP toolkit (Option, Either, pipe) | fp-ts | -| Typed errors that work with async/await and serialize cleanly | wellcrafted | - -wellcrafted is the smallest circle on that Venn diagram. It does one thing: gives you typed, named, serializable error variants that compose with the TypeScript you already write. If that's all you wanted from FP error handling (and for most teams, it is), this is the library that stops there instead of continuing into territory TypeScript can't support well. - ---- - -## The Bet - -wellcrafted makes a bet: most TypeScript teams don't need a functional programming framework. They need a way to define errors that the type system can see. Everything else (async/await, early returns, switch statements, destructuring) TypeScript already does well enough. - -If you tried typed errors and walked away, you weren't wrong about the goal. You were right that TypeScript's errors should be typed. The libraries you tried were right about the theory. The gap was in the execution: they needed language features that TypeScript doesn't have, and the workarounds cost more than they saved. - -wellcrafted is what's left when you remove the workarounds. diff --git a/docs/philosophy/from-effect-to-pragmatic-errors.mdx b/docs/philosophy/from-effect-to-pragmatic-errors.mdx deleted file mode 100644 index b239c63..0000000 --- a/docs/philosophy/from-effect-to-pragmatic-errors.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: "You Tried Typed Errors and Gave Up. The Problem Wasn't You." -description: "TypeScript lacks the features that make FP error handling ergonomic. Here's what works instead." -icon: 'rotate-left' ---- - -# You Tried Typed Errors and Gave Up. The Problem Wasn't You. - -There's a silent majority in the TypeScript world: developers who tried Effect, or neverthrow, or fp-ts, found it didn't stick, and quietly went back to try-catch. They don't write blog posts about it. They don't file issues. They just stop using the library and move on, vaguely feeling like they failed to "get it." - -They didn't fail. TypeScript failed them. - ---- - -## The Pattern That Doesn't Translate - -Every FP error handling library in TypeScript borrows the same idea from Rust and Haskell: wrap your values in a Result type, then chain operations with `.map()`, `.mapError()`, `.andThen()`, `.orElse()`. - -```typescript -// neverthrow -const result = await getUserById(id) - .andThen((user) => validateEmail(user.email)) - .mapErr((e) => new ValidationError(e.message)); -``` - -In Rust, this pattern is ergonomic because the language meets you halfway. Rust has the `?` operator that auto-propagates errors. It has `match` expressions that exhaustively check variants. It has derive macros like `thiserror` that generate error implementations at compile time. The method chains work because they're backed by real language features. - -```rust -// Rust: the ? operator does the unwrapping for you -let user = get_user_by_id(id)?; -let validated = validate_email(&user.email)?; -``` - -TypeScript has none of this. No `?` operator. No compile-time macros. No real pattern matching. No algebraic data types as language primitives. No trait system. When you port `.map().andThen()` chains to TypeScript, you're porting the surface syntax without the underlying machinery that makes it feel natural. - -The result is code that's technically type-safe but reads like a translation from another language. And that's exactly what it is. - ---- - -## What TypeScript Actually Has - -TypeScript is good at a few things that functional languages don't prioritize: destructuring, discriminated unions via literal types, and `switch` narrowing. These aren't glamorous features. They don't get conference talks. But they compose into something surprisingly effective. - -```typescript -// Destructuring: TypeScript's native "unwrap" -const { data, error } = await createUser(email); - -// Discriminated unions: TypeScript's native "pattern matching" -if (error) { - switch (error.name) { - case "AlreadyExists": - showToast(`${error.email} already has an account`); - break; - case "CreateFailed": - logError(error); - break; - } -} -``` - -This is not a step backward from `.map().andThen()`. It's what TypeScript is actually built to do. The type narrowing is automatic. The control flow is visible. A junior developer reads this and knows what it does. - -Compare that to the chain version: - -```typescript -const result = await createUser(email) - .map((user) => user) - .mapErr((e) => { - if (e instanceof AlreadyExistsError) showToast(e.message); - else logError(e); - return e; - }); -``` - -The chain obscures control flow behind method calls. You're doing the same branching, just hidden inside lambdas. In Rust, the compiler enforces exhaustiveness on `match` arms. In TypeScript, `.mapErr()` is just a callback. You get no exhaustiveness checking, no narrowing, no compiler help. You gave up `switch` for nothing. - ---- - -## The Features TypeScript Is Missing - -This isn't a matter of opinion. TypeScript literally lacks the language features that make FP error handling ergonomic in other languages: - -**1. No `?` operator or auto-propagation** - -Rust's `?` turns multi-line error handling into a single character. TypeScript has no equivalent. Every FP library compensates with generators (`yield*`), method chains, or wrapper functions. All of them are strictly worse than a language-level operator. - -**2. No compile-time macros** - -Rust's `thiserror` generates `Display`, `Error`, and `From` implementations at compile time from a single `#[derive]` annotation. TypeScript can't generate code at compile time. Libraries have to choose between verbose manual definitions or runtime overhead. - -**3. No real pattern matching** - -Rust's `match` is exhaustive by default and destructures in the same expression. TypeScript's `switch` on a discriminant field gets you close, but it's not the same. You can't destructure and match in one step. You can't match nested structures. - -**4. No algebraic data types as primitives** - -Rust enums are sum types with associated data, checked exhaustively by the compiler. TypeScript achieves something similar with discriminated unions, but they're a convention enforced by the type checker, not a language primitive. The ergonomics gap is real. - -**5. No trait system** - -Rust's `From` trait lets you define automatic error conversions: `impl From for AppError`. TypeScript has no equivalent. Every error transformation is manual. - -These aren't features TypeScript chose not to include. They're features that require a compiled language with a sophisticated type system. TypeScript compiles to JavaScript, which has none of this infrastructure. Porting patterns that depend on these features produces code that fights the host language. - ---- - -## The Compromise That Actually Works - -The insight behind wellcrafted: take the one idea from FP error handling that TypeScript can express well (typed, named error variants) and implement it using patterns the language is built for. - -```typescript -import { defineErrors, extractErrorMessage, type InferErrors } from "wellcrafted/error"; -import { tryAsync, type Result } from "wellcrafted/result"; - -const UserError = defineErrors({ - AlreadyExists: ({ email }: { email: string }) => ({ - message: `User ${email} already exists`, - email, - }), - CreateFailed: ({ email, cause }: { email: string; cause: unknown }) => ({ - message: `Failed to create user ${email}: ${extractErrorMessage(cause)}`, - email, - cause, - }), -}); -type UserError = InferErrors; -``` - -`defineErrors` is Rust's `thiserror` without macros. You define your error vocabulary once. Each variant is a factory that returns a plain frozen object with a `name` discriminant. No classes, no `instanceof`, no prototype chains. - -```typescript -async function createUser(email: string): Promise> { - const existing = await db.findByEmail(email); - if (existing) return UserError.AlreadyExists({ email }); - - return tryAsync({ - try: () => db.users.create({ email }), - catch: (error) => UserError.CreateFailed({ email, cause: error }), - }); -} -``` - -This is `async/await` with early returns. No generators. No method chains. No new programming model. The error handling is the same control flow you'd write in any TypeScript function; the only difference is that errors are typed and named. - -On the consuming side, `switch (error.name)` gives you exhaustive narrowing: - -```typescript -const { data, error } = await createUser("alice@example.com"); -if (error) { - switch (error.name) { - case "AlreadyExists": - // TypeScript knows: error.email exists - showToast(`${error.email} already has an account`); - break; - case "CreateFailed": - // TypeScript knows: error.email and error.cause exist - logError(error); - break; - } -} -``` - -The `{ data, error }` shape is already everywhere. Supabase returns it. SvelteKit load functions return it. TanStack Query has `data` and `error` on every query. There's no new mental model to learn. Your team already knows how to destructure this. - ---- - -## What This Doesn't Try to Be - -wellcrafted is not a functional programming library. It doesn't have `.map()` or `.andThen()` or `.orElse()`. It doesn't have dependency injection, fiber-based concurrency, or a runtime. It doesn't try to make TypeScript into something it's not. - -If your team has bought into Effect and it's working, keep using it. If you need structured concurrency or runtime dependency injection, wellcrafted doesn't have it. Those are real capabilities for teams that need them. - -But if you tried FP error handling, found it awkward, and went back to untyped `try-catch`, you don't have to stay there. The gap between "raw try-catch" and "full functional runtime" is wide, and there's a practical spot in the middle: typed errors, plain objects, patterns your team already knows. - -The problem was never that you couldn't learn generators or method chains. The problem was that TypeScript doesn't have the features that make those patterns worth the cost. diff --git a/docs/philosophy/production-reliability.mdx b/docs/philosophy/production-reliability.mdx deleted file mode 100644 index a4131c5..0000000 --- a/docs/philosophy/production-reliability.mdx +++ /dev/null @@ -1,621 +0,0 @@ ---- -title: 'Production Reliability' -description: 'How explicit error handling prevents production failures' -icon: 'shield-check' ---- - -# Production Reliability - -> "In production, every unhandled exception is a potential outage." - -Production systems have a different relationship with errors than development environments. In development, you can restart, debug, and iterate quickly. In production, every failure impacts real users, costs money, and damages trust. - -wellcrafted's error-handling patterns weren't designed in isolation; they evolved from real production pain points where traditional exception handling fell short. - -## The Hidden Cost of Exceptions - -### Scenario 1: The Midnight Page - -It's 2 AM. Your phone buzzes with an alert: "Payment processing service returning 500 errors." Users can't complete purchases. Revenue is dropping by the minute. - -You dig into the logs and find this: - -``` -Error: Cannot read property 'id' of undefined - at processPayment (payment-service.js:42:18) - at handleCheckout (checkout.js:127:25) - at Router.post (/api/checkout:15:9) -``` - -**What went wrong?** Somewhere in the payment flow, a function returned `undefined` instead of throwing an exception. The calling code assumed success and tried to access `.id` on `undefined`. The error is generic, the stack trace is shallow, and you have no context about what data caused the failure. - -**How long to fix?** Hours. You need to reproduce the exact conditions, add logging, deploy, and wait for it to happen again. - -### Scenario 2: The Silent Failure - -Your dashboard shows green metrics, but users are complaining that their profile updates aren't saving. No error logs, no alerts, no obvious failures. - -After investigation, you discover this pattern: - -```typescript -async function updateProfile(userId: string, data: ProfileData) { - try { - await validateProfileData(data); - await database.updateUser(userId, data); - // Success! ...or is it? - } catch (error) { - console.error("Profile update failed:", error.message); - // Error logged, but user never sees it - // Function returns normally - caller thinks it succeeded - } -} -``` - -**What went wrong?** The try-catch block silently swallowed errors. Callers had no way to know that the operation failed. Users saw "Profile updated successfully" while their data was never saved. - -**How long to detect?** Days or weeks. Silent failures are the worst kind: they break user trust without triggering your monitoring systems. - -### Scenario 3: The Cascade Failure - -A third-party API starts returning 429 (rate limit) errors. Your application doesn't handle this gracefully: - -```typescript -async function fetchUserData(id: string) { - const response = await fetch(`/api/users/${id}`); - return response.json(); // Throws on non-200 status -} - -// Callers throughout the codebase -try { - const user = await fetchUserData(id); - // Assume success -} catch (error) { - // Generic error handling - throw new Error("User fetch failed"); -} -``` - -**What went wrong?** The 429 errors propagated as generic "fetch failed" exceptions throughout your application. Rate limits require specific handling (backoff, retry), but your error system couldn't distinguish between rate limits, network failures, and data errors. - -**The cascade effect**: One API's rate limiting brought down multiple features across your application. - -## The wellcrafted Approach to Production Reliability - -### Scenario 1 Solved: Rich Error Context - -```typescript -type PaymentError = Readonly<{ name: "PaymentError"; message: string }>; -type ValidationError = Readonly<{ name: "ValidationError"; message: string }>; -type GatewayError = Readonly<{ - name: "GatewayError"; - message: string; - orderId: string; - amount: number; - currency: string; - paymentMethodId: string; - customerId: string; - gatewayResponse: number | undefined; - timestamp: string; -}>; - -async function processPayment( - order: Order, - paymentMethod: PaymentMethod -): Promise> { - - // Validate payment data - const validation = validatePaymentData(order, paymentMethod); - if (validation.error) return validation; - - // Process with payment gateway - const { data: result, error } = await tryAsync({ - try: () => paymentGateway.charge({ - amount: order.total, - currency: order.currency, - paymentMethodId: paymentMethod.id, - customerId: order.customerId - }), - catch: (error) => - Err({ - name: "GatewayError", - message: "Payment gateway request failed", - orderId: order.id, - amount: order.total, - currency: order.currency, - paymentMethodId: paymentMethod.id, - customerId: order.customerId, - gatewayResponse: (error as { response?: { status?: number } }).response?.status, - timestamp: new Date().toISOString(), - }) - }); - - if (error) return Err(error); - - return Ok(result); -} -``` - -**When this fails at 2 AM**: -```json -{ - "level": "error", - "name": "GatewayError", - "message": "Payment gateway request failed", - "orderId": "ord_abc123", - "amount": 2999, - "currency": "USD", - "paymentMethodId": "pm_xyz789", - "customerId": "cus_def456", - "gatewayResponse": 402, - "timestamp": "2024-03-15T02:14:33.127Z", - "traceId": "trace_ghi012" -} -``` - -**Time to diagnose**: Minutes. You have the exact order, payment method, gateway response, and timing. You can reproduce the failure immediately. - -### Scenario 2 Solved: Explicit Success/Failure - -```typescript -async function updateProfile( - userId: string, - data: ProfileData -): Promise> { - - // Validate profile data - const validation = validateProfileData(data); - if (validation.error) return validation; - - // Check permissions - const authCheck = await checkUpdatePermissions(userId); - if (authCheck.error) return authCheck; - - // Update database - const { data: updatedProfile, error } = await database.updateUser(userId, data); - if (error) { - return Err({ - name: "DatabaseError", - message: "Failed to update user profile", - userId, - updateFields: Object.keys(data), - timestamp: new Date().toISOString(), - }); - } - - return Ok(updatedProfile); -} - -// API endpoint with explicit error handling -async function handleProfileUpdate(req: Request): Promise { - const { data: profile, error } = await updateProfile(userId, profileData); - - if (error) { - // Log the detailed error for debugging - logger.error("Profile update failed", { - error, - userId, - endpoint: "/api/profile", - traceId: req.headers.get("x-trace-id") - }); - - // Return appropriate error response to client - switch (error.name) { - case "ValidationError": - return new Response(JSON.stringify({ - error: "Invalid profile data", - details: error - }), { status: 400 }); - case "AuthError": - return new Response(JSON.stringify({ - error: "Unauthorized" - }), { status: 403 }); - case "DatabaseError": - return new Response(JSON.stringify({ - error: "Profile update failed" - }), { status: 500 }); - } - } - - return new Response(JSON.stringify(profile)); -} -``` - -**What changed**: The calling code *must* handle the error case. TypeScript enforces this: you can't access `profile` without first checking if `error` is null. Silent failures become impossible. - -### Scenario 3 Solved: Discriminated Error Types - -```typescript -type ApiError = - | Readonly<{ name: "RateLimitError"; message: string; retryAfter: string | null }> - | Readonly<{ name: "NetworkError"; message: string }> - | Readonly<{ name: "AuthError"; message: string }> - | Readonly<{ name: "NotFoundError"; message: string }>; - -async function fetchUserData( - id: string -): Promise> { - - return await tryAsync({ - try: async () => { - const response = await fetch(`/api/users/${id}`); - - if (!response.ok) { - switch (response.status) { - case 429: - throw { type: "rate_limit", retryAfter: response.headers.get("retry-after") }; - case 401: - case 403: - throw { type: "auth", status: response.status }; - case 404: - throw { type: "not_found" }; - default: - throw { type: "network", status: response.status }; - } - } - - return response.json(); - }, - catch: (error) => { - const e = error as { type?: string; retryAfter?: string | null; status?: number }; - switch (e.type) { - case "rate_limit": - return Err({ - name: "RateLimitError" as const, - message: "API rate limit exceeded", - userId: id, - retryAfter: e.retryAfter ?? null, - timestamp: new Date().toISOString(), - }); - case "auth": - return Err({ - name: "AuthError" as const, - message: "Authentication failed", - userId: id, - status: e.status, - }); - case "not_found": - return Err({ - name: "NotFoundError" as const, - message: "User not found", - userId: id, - }); - default: - return Err({ - name: "NetworkError" as const, - message: e.type ? "Network request failed" : "Unexpected error", - userId: id, - status: e.status, - }); - } - } - }); -} - -// Now callers can handle each error type appropriately -async function handleUserRequest(userId: string) { - const { data: user, error } = await fetchUserData(userId); - - if (error) { - switch (error.name) { - case "RateLimitError": - // Specific handling for rate limits - const retryAfter = parseInt(error.retryAfter || "60"); - await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); - return handleUserRequest(userId); // Retry after delay - - case "AuthError": - // Redirect to login - throw new RedirectError("/login"); - - case "NotFoundError": - // Show 404 page - return notFoundResponse(); - - case "NetworkError": - // Show retry button - return networkErrorResponse(error); - } - } - - return successResponse(user); -} -``` - -**What changed**: Each error type gets appropriate handling. Rate limits trigger retries, auth errors redirect to login, and network errors show retry options. No more cascade failures from generic error handling. - -## Production Metrics: Whispering Case Study - -Whispering is a production desktop/web application built with wellcrafted patterns. After thousands of hours of user testing and real-world usage: - -### Zero Runtime Crashes -**Traditional approach (before wellcrafted)**: -- Unhandled promise rejections causing app crashes -- Generic error boundaries catching everything as "something went wrong" -- Users losing work due to unexpected exceptions - -**wellcrafted approach**: -- Every operation returns a Result type -- All error paths explicitly handled -- Graceful degradation instead of crashes - -**Result**: Zero unhandled exceptions in production. When things go wrong, users see helpful error messages with actionable next steps, not blank screens. - -### Improved Error Observability - -**Traditional logs**: -``` -[ERROR] TypeError: Cannot read property 'id' of undefined -[ERROR] Network request failed -[ERROR] Validation error -``` - -**wellcrafted logs**: -```json -{ - "level": "error", - "name": "TranscriptionError", - "message": "OpenAI API key invalid", - "provider": "openai", - "modelName": "whisper-1", - "audioLengthSeconds": 142, - "apiKeyLength": 8, - "timestamp": "2024-03-15T14:22:18.193Z", - "traceId": "trace_abc123" -} -``` - -**Impact**: Debugging time reduced from hours to minutes. Error context provides immediate insights into the failure conditions. - -### Better User Experience - -**Traditional error handling**: -- Generic "An error occurred" messages -- No guidance on how to resolve issues -- Users unable to distinguish between temporary and permanent failures - -**wellcrafted error handling**: -```typescript -// Real example from Whispering's transcription service -if (error.name === "Auth") { - return WhisperingError.ApiKeyMissing({ - title: "🔑 API Key Required", - description: "Please enter your OpenAI API key in settings to use Whisper transcription.", - action: { - type: "link", - label: "Add API key", - href: "/settings/transcription" - } - }); -} -``` - -**Impact**: Users see specific, actionable error messages that guide them toward resolution. - -## Team Collaboration Benefits - -### API Contracts That Actually Work - -Traditional function signatures hide their failure modes: - -```typescript -// What can go wrong? 🤷 -async function uploadFile(file: File): Promise -``` - -wellcrafted signatures make error handling part of the contract: - -```typescript -// Crystal clear about all possible outcomes -async function uploadFile( - file: File -): Promise> -``` - -**Team impact**: New developers can see exactly what error cases to handle. Code reviews can verify that all error paths are covered. Integration between teams becomes more predictable. - -### Self-Documenting Error Handling - -```typescript -// This function tells a story about what can go wrong and why -async function processPayment( - orderId: string, - paymentMethod: PaymentMethod -): Promise> { - // Implementation makes the error handling explicit at each step - - const orderValidation = await validateOrder(orderId); - if (orderValidation.error) return orderValidation; - - const fundsCheck = await checkSufficientFunds(paymentMethod, order.total); - if (fundsCheck.error) return fundsCheck; - - const fraudCheck = await runFraudDetection(order, paymentMethod); - if (fraudCheck.error) return fraudCheck; - - return await chargePaymentMethod(paymentMethod, order.total); -} -``` - -**Team impact**: The function signature serves as comprehensive documentation of all failure modes. New team members can understand the error handling without reading the implementation. - -### Exhaustive Error Handling in Code Reviews - -TypeScript's discriminated unions ensure exhaustive error handling: - -```typescript -function handleApiError(error: ApiError) { - switch (error.name) { - case "RateLimitError": - return showRetryAfterDelay(error.retryAfter); - case "AuthError": - return redirectToLogin(); - case "NetworkError": - return showNetworkErrorDialog(); - default: { - // every variant is handled, so `error` is `never` here; - // add a variant without a case and this line stops compiling - const _exhaustive: never = error; - return _exhaustive; - } - } -} -``` - -**Team impact**: Code reviews can mechanically verify that all error cases are handled. No more "did we handle the timeout case?" discussions: the `never` check in `default` enforces completeness. - -## The Debugging Advantage - -### Error Context That Actually Helps - -Traditional errors: -``` -Error: Request failed - at fetch (api.js:23:12) - at getUser (user.js:45:8) -``` - -wellcrafted errors: -```json -{ - "name": "ApiError", - "message": "Failed to fetch user data", - "userId": "user_123", - "endpoint": "/api/users/user_123", - "requestMethod": "GET", - "responseStatus": 503, - "responseTime": 5432, - "retryAttempt": 2, - "maxRetries": 3, - "requestHeaders": { - "authorization": "Bearer ***", - "user-agent": "MyApp/1.2.3" - }, - "timestamp": "2024-03-15T09:31:42.194Z" -} -``` - -**Debugging impact**: You know exactly what request failed, why it failed, what the user was trying to do, and what retry attempts were made. No guesswork, no "I can't reproduce this" responses. - -### Error Chains That Tell the Full Story - -```typescript -// Service layer error -const databaseError = { - name: "DatabaseError", - message: "Connection timeout", - query: "SELECT * FROM users WHERE id = ?", - timeout: 5000, -}; - -// API layer enrichment -const apiError = { - name: "ApiError", - message: "Failed to fetch user", - userId: "user_123", - endpoint: "/api/users/user_123", -}; - -// Application layer enrichment -const userError = { - name: "UserError", - message: "Profile load failed", - feature: "profile-page", - userId: "user_123", - userAgent: "Chrome/91.0", -}; -``` - -**Debugging impact**: You can trace the error from the database connection timeout, through the API layer, to the specific user action that triggered it. Every layer adds relevant context without losing the original failure details. - -## Monitoring and Alerting - -### Structured Error Metrics - -wellcrafted's consistent error structure enables structured monitoring: - -```typescript -// Automatic error categorization -function recordErrorMetrics(error: BaseError) { - metrics.increment("errors.total", { - errorType: error.name, - service: error.service, - endpoint: error.endpoint - }); - - // Different alert thresholds for different error types - switch (error.name) { - case "ValidationError": - // High volume expected, alert on unusual spikes - metrics.threshold("errors.validation", 100, "5m"); - break; - case "DatabaseError": - // Low tolerance, alert immediately - metrics.threshold("errors.database", 1, "1m"); - break; - case "RateLimitError": - // Expected during traffic spikes, alert on sustained rates - metrics.threshold("errors.rate_limit", 50, "10m"); - break; - } -} -``` - -**Monitoring impact**: Different error types get different treatment. Database errors trigger immediate alerts, while validation errors only alert on unusual patterns. - -### Error Rate SLIs (Service Level Indicators) - -```typescript -// Track error rates by category for SLA monitoring -const errorRateMetrics = { - "user_errors": ["ValidationError", "AuthError", "NotFoundError"], - "system_errors": ["DatabaseError", "NetworkError", "TimeoutError"], - "external_errors": ["PaymentGatewayError", "EmailServiceError"] -}; - -function updateSLI(error: BaseError) { - for (const [category, errorTypes] of Object.entries(errorRateMetrics)) { - if (errorTypes.includes(error.name)) { - metrics.increment(`sli.error_rate.${category}`); - } - } -} -``` - -**SLA impact**: You can set different reliability targets for different error categories. User errors don't count against system reliability SLAs, but database errors do. - -## The Production Reality Check - -Production systems taught us that error handling isn't just about catching exceptions: it's about building systems that fail gracefully, provide actionable feedback, and maintain observability under stress. - -wellcrafted's patterns emerged from these production lessons: - -- **Exceptions hide crucial debugging context** that you need at 3 AM -- **Silent failures are worse than loud ones** because they break trust without triggering alerts -- **Generic error handling causes cascade failures** when different error types need different responses -- **Error messages should guide users toward resolution**, not just report that something broke -- **Monitoring systems need structured error data** to provide meaningful insights - -These aren't theoretical concerns: they're the difference between a system that scales gracefully and one that requires constant manual intervention. - -## Conclusion - -Production reliability isn't achieved by adding more try-catch blocks or better error logging. It comes from designing systems where failures are explicit, predictable, and actionable. - -wellcrafted's error handling patterns provide: - -- **Rich error context** that accelerates debugging from hours to minutes -- **Explicit failure modes** that prevent silent failures and cascade errors -- **Structured error data** that enables sophisticated monitoring and alerting -- **Type-safe error handling** that ensures comprehensive error coverage -- **User-friendly error messages** that guide toward resolution - -The next time you're debugging a production incident at 2 AM, you'll appreciate having error context that shows exactly what went wrong, when, and with what data. That's the difference between reliable systems and systems that just happen to work most of the time. - ---- - -**Related**: Understand the foundational principles in [Design Principles](/philosophy/design-principles), or see how these patterns improve development workflows in [Developer Experience](/philosophy/developer-experience). \ No newline at end of file diff --git a/docs/philosophy/rust-inspiration.mdx b/docs/philosophy/rust-inspiration.mdx deleted file mode 100644 index 08c6c78..0000000 --- a/docs/philosophy/rust-inspiration.mdx +++ /dev/null @@ -1,319 +0,0 @@ ---- -title: "defineErrors Is thiserror Without Macros" -description: "How wellcrafted emulates Rust's thiserror in TypeScript using plain functions and type inference" -icon: 'rust' ---- - -# `defineErrors` Is `thiserror` Without Macros - -wellcrafted's `defineErrors` is directly modeled on Rust's [`thiserror`](https://docs.rs/thiserror) crate. The definition site looks almost identical. The construction syntax is nearly the same. The discrimination logic maps 1:1. The difference is that Rust uses compile-time procedural macros to generate code, and TypeScript doesn't have those. `defineErrors` gets the same result with plain functions and type inference. - -## The Rust Starting Point - -Here is an HTTP error type using `thiserror`: - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -enum HttpError { - #[error("Failed to connect: {cause}")] - Connection { cause: String }, - - #[error("HTTP {status}")] - Response { status: u16, body_message: Option }, - - #[error("Failed to parse response body: {cause}")] - Parse { cause: String }, -} -``` - -A few things to notice: - -- **`HttpError` is the namespace.** The variants (`Connection`, `Response`, `Parse`) live under it. They are short, one-word names because the enum name already provides the context. -- **Each variant is a struct with named fields.** `Connection` carries a `cause`. `Response` carries a `status` and an optional `body_message`. The fields are part of the type. -- **`#[error("...")]` defines the display string** for each variant, interpolating fields by name. -- **You construct** with `HttpError::Connection { cause: "timeout".into() }` and **discriminate** with `match`. - -A single type, short variant names under a descriptive namespace, typed fields per variant, and a display message co-located with the definition. That is the whole pattern. - -## The TypeScript Equivalent - -Here is the same type with `defineErrors`: - -```typescript -import { defineErrors, extractErrorMessage, type InferErrors } from 'wellcrafted/error'; - -const HttpError = defineErrors({ - Connection: ({ cause }: { cause: unknown }) => ({ - message: `Failed to connect: ${extractErrorMessage(cause)}`, - cause, - }), - - Response: ({ status }: { status: number; bodyMessage?: string }) => ({ - message: `HTTP ${status}`, - status, - }), - - Parse: ({ cause }: { cause: unknown }) => ({ - message: `Failed to parse response body: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -type HttpError = InferErrors; -``` - -Read it next to the Rust version. The structure is nearly identical. `HttpError` is the namespace. `Connection`, `Response`, `Parse` are short variant names. Each variant's fields are typed inline. The message sits right next to its definition. No compile-time code generation produced this; it's just a config object with constructor functions. TypeScript infers the full type from `const` generics, so `InferErrors` extracts the union of all variants without you writing a single type by hand. - -## The Definition Site Is Nearly Identical - -| Rust concept | TypeScript equivalent | -|---|---| -| `enum HttpError` | `const HttpError = defineErrors(...)` | -| `Connection { cause: String }` | `Connection: ({ cause }: { cause: unknown }) => (...)` | -| `#[error("Failed: {cause}")]` | `` message: `Failed: ${extractErrorMessage(cause)}` `` | -| `HttpError::Connection { cause: "timeout".into() }` | `HttpError.Connection({ cause: error })` | -| `match error { Connection { cause } => ... }` | `switch (error.name) { case 'Connection': ... }` | -| `fn handle(err: HttpError)` | `function handle(err: HttpError)` | - -### Construction - -``` -Rust: HttpError::Connection { cause: "timeout".into() } -TS: HttpError.Connection({ cause: error }) -``` - -### Discrimination - - -```rust Rust -match error { - HttpError::Connection { cause } => println!("Connection failed: {cause}"), - HttpError::Response { status, .. } => println!("HTTP {status}"), - HttpError::Parse { cause } => println!("Parse failed: {cause}"), -} -``` - -```typescript TypeScript -switch (error.name) { - case 'Connection': console.log(`Connection failed: ${error.cause}`); break; - case 'Response': console.log(`HTTP ${error.status}`); break; - case 'Parse': console.log(`Parse failed: ${error.cause}`); break; -} -``` - - -The `name` field on each error object is the discriminant. It is stamped automatically from the key you provide: `'Connection'`, `'Response'`, `'Parse'`. You never write it by hand; `defineErrors` handles it. That is directly analogous to how Rust stamps the variant identity into the enum value at construction time. - -## Five Places Where the Languages Diverge - -TypeScript doesn't have proc macros, ownership, struct literals, `match`, or the `?` operator. Each of these gaps has a JavaScript-native solution that preserves the ergonomics. - -### Factory Functions Instead of Struct Literals - -In Rust, `HttpError::Connection { cause: "timeout".into() }` is a struct literal. TypeScript has no struct syntax, so `defineErrors` gives you factory functions: `HttpError.Connection({ cause: error })`. The call site looks nearly identical; the only difference is the parentheses. - -### Template Literals Instead of Proc Macros - -This is the biggest adaptation. Rust's `#[error("Failed to connect: {cause}")]` is a compile-time format string powered by a procedural macro. The macro generates a `Display` impl at compile time with zero runtime cost. TypeScript has no compile-time code generation at all. - -Template literals are the pragmatic substitute. They run at construction time rather than compile time, but the definition-site experience is almost identical: - -```rust -// Rust: proc macro generates Display impl -#[error("Failed to connect: {cause}")] -Connection { cause: String }, -``` - -```typescript -// TypeScript: template literal runs at construction time -Connection: ({ cause }: { cause: unknown }) => ({ - message: `Failed to connect: ${extractErrorMessage(cause)}`, - cause, -}), -``` - -Both co-locate the message template with the variant definition. Both interpolate fields by name. The TypeScript version has one advantage Rust doesn't: `extractErrorMessage` can handle `unknown` caught errors at the definition site, so call sites never do string conversion. In Rust, `cause` must already be a `String`. In TypeScript, the constructor accepts the raw `unknown` and handles the conversion internally. - -### `Err<...>` Wrapping Instead of Direct Returns - -In Rust, a function returning `Result` returns the error variant directly. Rust's `?` operator and return type tell the compiler which side of the Result you are on. TypeScript does not have that. `defineErrors` factories always return `Err<...>` (an object shaped `{ data: null, error: ... }`) so that `trySync` and `tryAsync` can tell errors apart from successful values without ambiguity. - -### `Object.freeze` Instead of Ownership - -Rust's ownership model prevents mutation after construction. TypeScript has no ownership system. `defineErrors` freezes every error object at runtime and marks it `Readonly<...>` at the type level. Different mechanisms, same goal: errors are values, not mutable state. - -### Discriminated Unions Instead of `match` - -Rust's `match` is exhaustive by default; the compiler forces you to handle every variant. TypeScript has no native pattern matching yet, but discriminated unions on `error.name` get you most of the way there. A `switch` on a string literal union narrows the type in each branch, and you can use `never` checks for exhaustiveness if you want it. - -| Difference | Rust | TypeScript | Why | -|---|---|---|---| -| Construction | Struct literal | Factory call | TS has no struct literals | -| Message format | Compile-time proc macro | Runtime template literal | TS has no proc macros | -| Return type | Returns enum variant directly | Returns `Err<...>` wrapper | No `?` operator in TS | -| Immutability | Ownership model | `Object.freeze` + `Readonly` | No ownership in TS | -| Exhaustiveness | `match` is exhaustive by default | `switch` + discriminated unions | No pattern matching in TS (yet) | - -## The Core Insight - -The thing that unlocked the `defineErrors` design comes straight from Rust: **the enum name is the namespace, the variant name is the discriminant.** - -In Rust, you would never name a variant `ConnectionError` inside an enum called `HttpError`. That would be `HttpError::ConnectionError`, redundant. You name it `Connection`. The enum already tells you it is an `HttpError`. The variant tells you which kind. - -The same logic applies in TypeScript: - -```typescript -const HttpError = defineErrors({ - Connection: ..., - Response: ..., - Parse: ..., -}); -``` - -`HttpError` is the context. `Connection` is the discriminant. The `name` on the error object will be `'Connection'`, not `'HttpConnectionError'` or `'HttpError.Connection'`. Short, unambiguous, and exactly what you `switch` on. - -This is the pattern that was missing from TypeScript error handling. Not just a way to make errors with `name` fields, but a way to define a family of errors under a shared namespace with the same structural clarity that Rust's enum system provides. - -**The enum name is the namespace. The variant name is the discriminant. Everything else follows from there.** - -## Variant Names Replace Sub-Discriminants - -In Rust, you would never write this: - -```rust -enum NetworkError { - Request { reason: RequestReason }, -} - -enum RequestReason { - Timeout, - Refused, - DnsFailure, -} -``` - -That nesting is pointless. You would write: - -```rust -enum NetworkError { - Timeout { duration: Duration }, - Refused { host: String }, - DnsFailure { hostname: String }, -} -``` - -Each failure case gets its own variant. The enum's discriminant does the work. There is no inner enum to match on separately. - -The same principle applies in TypeScript. When a `defineErrors` variant has a string literal union field like `reason: 'timeout' | 'refused' | 'dns'`, that field is an inner enum in disguise. Consumers have to narrow twice (once on `error.name`, once on `error.reason`), which defeats the purpose of having a discriminated union in the first place. - -```typescript -// The Rust instinct is correct here: make each case a variant -const NetworkError = defineErrors({ - Timeout: ({ duration }: { duration: number }) => ({ - message: `Request timed out after ${duration}ms`, - duration, - }), - Refused: ({ host }: { host: string }) => ({ - message: `Connection refused by ${host}`, - host, - }), - DnsFailure: ({ hostname }: { hostname: string }) => ({ - message: `DNS lookup failed for ${hostname}`, - hostname, - }), -}); -``` - -If you are coming from Rust, trust your instincts on this. The pattern that feels right in Rust (one variant per failure case, fields specific to that case) is exactly right here too. - -## Constructor Owns the Conversion (Rust's `#[from]`) - -In Rust, `thiserror` has a `#[from]` attribute that tells a variant to own the conversion from another error type: - -```rust -#[derive(Error, Debug)] -enum AudioError { - #[error("Failed to play sound: {0}")] - PlaySound(#[from] IoError), -} -``` - -The call site never converts anything. You pass the raw `IoError` and the variant handles it: - -```rust -// The variant owns the conversion, not the call site -let raw: IoError = ...; -let err: AudioError = raw.into(); // From impl does the work -``` - -This is a deliberate design choice. The error definition knows how to present itself. The call site just hands over the raw material. - -The same principle applies with `defineErrors`. The constructor function is where conversion logic lives, not the call site: - -```typescript -import { defineErrors, extractErrorMessage } from 'wellcrafted/error'; - -const AudioError = defineErrors({ - PlaySound: ({ cause }: { cause: unknown }) => ({ - message: `Failed to play sound: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -// Call site: just pass the raw error, like Rust's From impl -try { - playSound(file); -} catch (error) { - return AudioError.PlaySound({ cause: error }); -} -``` - -The `extractErrorMessage` call lives inside the constructor, not at the call site. The variant knows how to turn an `unknown` cause into a human-readable message. The caller just passes `{ cause: error }` and moves on. - -The anti-pattern is doing the conversion at the call site: - -```typescript -// Don't do this: no Rust equivalent because Rust wouldn't let you -try { - playSound(file); -} catch (error) { - return AudioError.PlaySound({ reason: extractErrorMessage(error) }); -} -``` - -This scatters formatting logic across every `catch` block. If you later change how `PlaySound` presents its message, you have to find every call site. With `#[from]` in Rust, there is exactly one place where the conversion is defined. With `extractErrorMessage` inside the constructor, there is exactly one place in TypeScript too. - -The rule: **the constructor owns the conversion**. Call sites pass raw inputs. The variant decides how to present them. - -## TypeScript's Type System Does What Macros Would - -The pragmatic bet behind `defineErrors` is that TypeScript's type inference can replace Rust's compile-time code generation for this specific use case. Rust's `thiserror` uses proc macros to generate `Display`, `Error`, and `From` implementations. `defineErrors` uses `const` generic inference and mapped types to achieve the same outcomes: - -```typescript -// TypeScript infers every variant's type from the config object -const HttpError = defineErrors({ - Connection: ({ cause }: { cause: unknown }) => ({ - message: `Failed to connect: ${extractErrorMessage(cause)}`, - cause, - }), - Response: ({ status }: { status: number }) => ({ - message: `HTTP ${status}`, - status, - }), -}); - -// InferErrors extracts the discriminated union, no manual type definitions -type HttpError = InferErrors; -// = Readonly<{ name: 'Connection'; message: string; cause: unknown }> -// | Readonly<{ name: 'Response'; message: string; status: number }> -``` - -In Rust, the proc macro reads your enum definition and generates code. In TypeScript, `const` generic inference reads your config object and infers types. Both produce the same result: a namespace of constructors and a union type of all variants, derived from a single source of truth. The definition is the type. No duplication, no drift. - -The ten lines of `defineErrors` runtime do what `thiserror`'s proc macro does: iterate the variants, stamp each one with its name, and produce ready-to-use constructors. The type system does the rest. That's the whole trick: plain functions for runtime, type inference for compile time, and the definition site looks like `thiserror` because it *is* `thiserror`, just without the macro. - ---- - -**Next**: Read [Why `name` and `message`](/philosophy/why-name-and-message) for how these Rust patterns meet JavaScript conventions, see the full [Error System reference](/core/error-system) for API details, or read about the broader [Design Principles](/philosophy/design-principles) that guide wellcrafted. diff --git a/docs/philosophy/why-name-and-message.mdx b/docs/philosophy/why-name-and-message.mdx deleted file mode 100644 index e239266..0000000 --- a/docs/philosophy/why-name-and-message.mdx +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: "We Didn't Invent name and message: JavaScript Did" -description: "Why wellcrafted errors follow JavaScript's Error convention instead of inventing new terminology" -icon: 'js' ---- - -# We Didn't Invent `name` and `message`: JavaScript Did - -Every JavaScript `Error` has two properties: `.name` and `.message`. wellcrafted errors have the same two properties, for the same reasons, because we're not inventing a new error system. We're fixing the one JavaScript already has. - -```typescript -// JavaScript's Error class: -const err = new TypeError('Expected a string'); -err.name; // 'TypeError' -err.message; // 'Expected a string' - -// WellCrafted's defineErrors: -const HttpError = defineErrors({ - Connection: ({ cause }: { cause: unknown }) => ({ - message: `Failed to connect: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -const { error } = HttpError.Connection({ cause: 'timeout' }); -error.name; // 'Connection' -error.message; // 'Failed to connect: timeout' -``` - -Same shape. Same semantics. `name` identifies what kind of error it is; `message` explains what happened in plain English. The difference is that wellcrafted errors are plain objects that actually serialize, carry typed fields, and work with TypeScript's discriminated unions. - -## JavaScript's Error Gets Two Things Right and One Thing Wrong - -The `Error` class got `name` and `message` right. `name` tells you the category: `TypeError`, `RangeError`, `SyntaxError`. `message` tells you the specifics: "Cannot read properties of undefined." Every JavaScript developer already knows this mental model. - -What `Error` gets wrong is everything else. `JSON.stringify(new Error('oops'))` returns `'{}'`. Class instances lose their prototype across iframes, workers, and serialization boundaries. `instanceof` checks fail in ways that are invisible until production. Stack traces are expensive to capture and useless to serialize. - -```typescript -// JavaScript's Error: name and message are right, serialization is broken -const error = new TypeError('Expected a string'); -JSON.stringify(error); // '{}', name and message are gone - -// WellCrafted: same name and message, serialization works -const { error: wcError } = ValidationError.Type({ expected: 'string', received: 'number' }); -JSON.stringify(wcError); -// '{"name":"Type","message":"Expected string, got number","expected":"string","received":"number"}' -``` - -wellcrafted keeps what works (`name` and `message`) and replaces what doesn't (classes, prototypes, non-serializable state) with plain frozen objects. - -## JavaScript Already Named These Fields - -We could have called the discriminant `tag` and the human-readable field `description`. Some error handling libraries do exactly that. We didn't because every JavaScript developer already knows what `name` and `message` mean on an error. When you see `.name`, you know it identifies the error type. When you see `.message`, you know it's the human-readable explanation. That convention comes from JavaScript itself, not from us. - -```typescript -// This reads naturally because it follows what JS developers already know -switch (error.name) { - case 'Connection': console.log(error.message); break; - case 'Timeout': console.log(error.message); break; - case 'Parse': console.log(error.message); break; -} -``` - -Inventing new field names would buy us nothing and cost us familiarity. A developer reading wellcrafted code for the first time already knows what `error.name` and `error.message` are. - -## Rust's Patterns Map to JavaScript Without New Abstractions - -wellcrafted's `defineErrors` is directly inspired by Rust's `thiserror` crate: the namespace pattern, the variant naming, the co-located message templates. But Rust and JavaScript have different constraints, and pretending otherwise leads to APIs that fight the platform. - -Rust has compile-time procedural macros; JavaScript doesn't. Template literals are the natural equivalent: - -```rust -// Rust: compile-time format string via proc macro -#[error("Failed to connect: {cause}")] -Connection { cause: String }, -``` - -```typescript -// TypeScript: runtime template literal, same result, different mechanism -Connection: ({ cause }: { cause: unknown }) => ({ - message: `Failed to connect: ${extractErrorMessage(cause)}`, - cause, -}), -``` - -Both produce a human-readable message from structured fields. The Rust version runs at compile time; the TypeScript version runs at construction time. The developer experience is nearly identical. The implementation uses what each language actually provides. - -The same principle applies across every divergence: - -| Rust has | JavaScript doesn't | wellcrafted uses instead | -|---|---|---| -| Procedural macros | No compile-time codegen | Template literals | -| Ownership model | No ownership | `Object.freeze` + `Readonly` | -| `match` exhaustiveness | No pattern matching | `switch` on discriminated unions | -| Struct literals | No struct syntax | Factory functions with object args | -| `?` operator | No Result operator | Early return with `if (result.error)` | - -None of these are compromises. They're the JavaScript-native way to express the same ideas. A `switch` on `error.name` narrows types just as precisely as `match`. The patterns map; only the syntax differs. - -## JSON Serializability Is the Whole Point - -JavaScript errors cross boundaries that Rust errors never touch. A Rust error lives in one process, in one binary, on one machine. A JavaScript error might start in a browser tab, serialize to JSON, cross a network boundary to a server, get logged to a monitoring service, and end up in a Slack notification. - -```json -{ - "name": "Connection", - "message": "Failed to connect: timeout", - "cause": "timeout" -} -``` - -This is a valid JSON object, a valid wellcrafted error, and a valid thing to log, send over HTTP, store in a database, or display in a UI. No special serialization logic, no custom `toJSON` methods, no lossy transforms. The error is the JSON. - -That works as long as you keep every field a plain `JsonValue`: a string, number, boolean, or nested object or array. This is a convention the library leans on, not a constraint it enforces at the type level (the only required field is `message: string`). Nothing stops you from putting a `Date` or a class instance on an error, but it will not round-trip cleanly, so reach for a string or number instead. - -```typescript -const FileError = defineErrors({ - Write: ({ path, bytesWritten }: { path: string; bytesWritten: number }) => ({ - message: `Write failed at byte ${bytesWritten}`, - path, - bytesWritten, - }), -}); - -// Compiles, but a Date does not survive JSON.stringify; store a number instead: -// Write: ({ path, timestamp }: { path: string; timestamp: Date }) => ... -``` - -Error chains serialize too. When `cause` is another wellcrafted error, `JSON.stringify` produces a nested structure that preserves the full chain. No stack trace parsing, no custom serializers. This only works because the errors are plain objects, which brings us to the other half of the design. - -## Plain Objects Over Classes Is a JavaScript Decision - -Choosing plain objects over `class extends Error` isn't an abstract design preference. It solves a specific JavaScript problem: class instances don't survive serialization boundaries. - -```typescript -// Class-based error: breaks across boundaries -class ConnectionError extends Error { - constructor(public host: string) { - super(`Connection to ${host} failed`); - this.name = 'ConnectionError'; - } -} - -const err = new ConnectionError('api.example.com'); -const serialized = JSON.parse(JSON.stringify(err)); -serialized instanceof ConnectionError; // false: prototype is gone -serialized.name; // undefined: non-enumerable -serialized.message; // undefined: non-enumerable -serialized.host; // 'api.example.com': only enumerable props survive -``` - -`Error` properties `name` and `message` are non-enumerable by default. They vanish on serialization. Custom subclasses lose their prototype chain. `instanceof` checks fail across iframes, Web Workers, and server/client boundaries. - -wellcrafted errors don't have this problem because there's no prototype to lose. They're frozen plain objects where every property is enumerable. They work the same whether you're in a browser tab, a Web Worker, a Node.js server, or reading them back from a database. - -## The Shape Is the Contract - -wellcrafted errors make a simple bet: the shape of the object is the contract, not its class identity. If an object has `{ name: 'Connection', message: string, cause: unknown }`, it's a `Connection` error. You don't need `instanceof` to check. You don't need the original class definition in scope. You just read the `name` field. - -This is how discriminated unions work in TypeScript, and it's how data-oriented programming works in general. Identity lives in the data, not in the prototype chain. - -```typescript -// The shape IS the type: TypeScript narrows on name, not instanceof -function handle(error: HttpError) { - if (error.name === 'Connection') { - // TypeScript knows: error has cause field - retry(error.cause); - } -} -``` - -That's the whole philosophy. Use `name` because JavaScript errors use `name`. Use `message` because JavaScript errors use `message`. Make them plain objects because JavaScript classes don't serialize. Take Rust's patterns where they map; use JavaScript's idioms where they don't. Nobody has to learn a new vocabulary to read the code. diff --git a/scripts/check-doc-claims.ts b/scripts/check-doc-claims.ts index c8e37c9..2b7e786 100644 --- a/scripts/check-doc-claims.ts +++ b/scripts/check-doc-claims.ts @@ -53,31 +53,6 @@ const CLAIM_ALLOWANCE_PATTERN = //g; const DECISION_ALLOWABLE_CLAIM_RULES = new Set(["retired-api"]); -const APPROVED_LEGACY_CLAIM_EXCLUSIONS = [ - "docs/core/brand-types.mdx", - "docs/core/error-system.mdx", - "docs/core/result-pattern.mdx", - "docs/getting-started/installation.mdx", - "docs/getting-started/quick-start.mdx", - "docs/integrations/testing.mdx", - "docs/migration/from-try-catch.mdx", - "docs/patterns/optional-keys.mdx", - "docs/patterns/real-world.mdx", - "docs/patterns/service-layer.mdx", - "docs/philosophy/brand-implementation.mdx", - "docs/philosophy/design-principles.mdx", - "docs/philosophy/developer-experience.mdx", - "docs/philosophy/err-null-is-ok-null.md", - "docs/philosophy/error-api-evolution.mdx", - "docs/philosophy/for-the-pragmatic-fp-developer.mdx", - "docs/philosophy/from-effect-to-pragmatic-errors.mdx", - "docs/philosophy/production-reliability.mdx", - "docs/philosophy/rust-inspiration.mdx", - "docs/philosophy/why-name-and-message.mdx", -] as const; - -const LEGACY_CLAIM_EXCLUSIONS = APPROVED_LEGACY_CLAIM_EXCLUSIONS; - const DOC_EXTENSIONS = new Set([".json", ".md", ".mdx"]); const EXAMPLE_EXTENSIONS = new Set([ ".cjs", @@ -248,15 +223,6 @@ function toRepositoryPath(path: string): string { return path.split(sep).join(posix.sep); } -async function pathExists(path: string): Promise { - try { - await stat(resolve(REPOSITORY_ROOT, path)); - return true; - } catch { - return false; - } -} - function includePublicFile(root: string, path: string): boolean { const extension = extname(path); switch (root) { @@ -289,56 +255,6 @@ async function collectFiles(root: string): Promise { return files; } -async function validateExclusions(findings: Finding[]): Promise> { - const approved = new Set(APPROVED_LEGACY_CLAIM_EXCLUSIONS); - const configured = new Set(); - - for (const rawPath of LEGACY_CLAIM_EXCLUSIONS) { - const path = toRepositoryPath(rawPath); - if (configured.has(path)) { - findings.push({ - line: 1, - message: `Duplicate legacy exclusion: ${path}`, - path: "scripts/check-doc-claims.ts", - rule: "gate-config", - }); - continue; - } - configured.add(path); - - if (!path.startsWith("docs/") || !approved.has(path)) { - findings.push({ - line: 1, - message: `Legacy exclusion is outside the exact claims exclusions: ${path}`, - path: "scripts/check-doc-claims.ts", - rule: "gate-config", - }); - continue; - } - if (!(await pathExists(path))) { - findings.push({ - line: 1, - message: `Legacy exclusion points to a missing file: ${path}`, - path: "scripts/check-doc-claims.ts", - rule: "gate-config", - }); - } - } - - for (const path of approved) { - if ((await pathExists(path)) && !configured.has(path)) { - findings.push({ - line: 1, - message: `Retained legacy file is missing its exact exclusion: ${path}`, - path: "scripts/check-doc-claims.ts", - rule: "gate-config", - }); - } - } - - return configured; -} - function parseAllowances( path: string, lines: readonly string[], @@ -679,7 +595,6 @@ async function scanFile(path: string, findings: Finding[]): Promise { async function main(): Promise { const findings: Finding[] = []; - const exclusions = await validateExclusions(findings); const publicFiles = ( await Promise.all( PUBLIC_CLAIM_ROOTS.map(async (root) => { @@ -693,7 +608,6 @@ async function main(): Promise { .sort(); for (const path of publicFiles) { - if (exclusions.has(path)) continue; await scanFile(path, findings); } @@ -708,9 +622,7 @@ async function main(): Promise { ); } - console.log( - `documentation claims: ${publicFiles.length - exclusions.size} files checked, ${APPROVED_LEGACY_CLAIM_EXCLUSIONS.length} deletion-approved legacy files excluded`, - ); + console.log(`documentation claims: ${publicFiles.length} files checked`); } if (import.meta.main) await main(); diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 77ef5b4..0e6ae4f 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -720,10 +720,19 @@ Nonvisual pre-deletion proof on 2026-07-10: ### Wave 11: Delete only approved obsolete owners -- [ ] Delete only paths in the Wave 0 allowlist. -- [ ] Remove every temporary legacy exclusion. -- [ ] Sweep for stale names, imports, claims, and links. -- [ ] Rerun the full suite before committing the deletion, so the deletion commit is independently green. +- [x] Delete only paths in the Wave 0 allowlist. +- [x] Remove every temporary legacy exclusion. +- [x] Sweep for stale names, imports, claims, and links. +- [x] Rerun the full suite before committing the deletion, so the deletion commit is independently green. + +Deletion proof on 2026-07-10: + +- Deleted exactly the 20 approved legacy documentation owners and three approved source READMEs recorded in the Wave 0 disposition table. `docs/integrations/hono-serialization.mdx` remains as the approved compatibility notice and was not part of the deletion allowlist. +- Removed the complete temporary claims-exclusion mechanism. `bun run docs:claims` now scans all 42 remaining public files with no exclusions. +- The stale-path sweep found one source-test comment that still named `docs/philosophy/err-null-is-ok-null.md`; it now points to the canonical `docs/decisions/result-shape.mdx`. A second sweep found no current links or imports for the deleted paths. +- `bun run format:check`, `bun run lint:check`, `bun run typecheck`, `bun run build`, and `bun test` passed after deletion. Lint retained 12 pre-existing warnings and no errors; the full suite retained 188 passing tests. +- `bun run docs:examples`, `bun run package:smoke`, `bun run compat:types`, `bun run compat:runtime`, `bun run docs:exports`, `bun run docs:claims`, and `bun run docs:snippets` passed. Export coverage retained 79 tuples across nine owners, package and compatibility checks retained all nine supported subpaths, and canonical examples and snippets remained synchronized. +- With Node 24.14.0 first on `PATH`, `bun run docs:validate` and `bun run docs:links` passed against pinned Mint 4.2.684. The deletion change is independently green before commit. ### Wave 12: Independent final review diff --git a/src/README.md b/src/README.md deleted file mode 100644 index 29b2394..0000000 --- a/src/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# wellcrafted - -Delightful TypeScript utilities for elegant, type-safe applications. - -## Overview - -This library provides delightful TypeScript utilities including: - -- **Result Pattern**: Type-safe error handling with `Ok`/`Err` variants (`wellcrafted/result`) -- **Brand Types**: Nominal typing for creating distinct types from primitives (`wellcrafted/brand`) -- **Error Utilities**: Structured, serializable error handling (`wellcrafted/error`) - -## Modular Imports - -```typescript -// Result handling -import { Result, Ok, Err, isOk, isErr, trySync, tryAsync } from "wellcrafted/result"; - -// Error utilities -import { defineErrors, type InferError, type AnyTaggedError, extractErrorMessage } from "wellcrafted/error"; - -// Brand types -import { type Brand } from "wellcrafted/brand"; -``` - -The Result pattern represents either success (Ok) or failure (Err data structure) of an operation. This implementation uses the "exclusive" approach where `null` values indicate absence (success has `error: null`, failure has `data: null`). - -## Implementation - -### Exclusive Values with Null - -```typescript -// From result.ts -type Ok = { data: T; error: null }; -type Err = { error: E; data: null }; -type Result = Ok | Err; -``` - -This implementation uses `null` to indicate absence, ensuring that a success result always has `error: null` and a failure result always has `data: null`. This provides clear type discrimination and excellent TypeScript inference. - -## Error Type Naming Convention - -Error types should follow the convention of ending with "Error" suffix: - -- `ValidationError` - for input validation errors -- `NetworkError` - for network-related errors -- `DatabaseError` - for database operation errors -- `AuthenticationError` - for authentication failures - -## Usage - -The implementation provides utility functions: - -- `Ok(data: T)`: Create a success result -- `Err(error: E)`: Create an Err data structure containing an error type -- `isOk(result)`: Type guard to check for success -- `isErr(result)`: Type guard to check for Err data structure -- `trySync({ try, catch })`: Execute a synchronous operation safely -- `tryAsync({ try, catch })`: Execute an asynchronous operation safely - -### Basic Usage - -```typescript -const result = await fetchData(); -if (isOk(result)) { - // Success case: use result.data -} else { - // Err data structure case: use result.error (which contains the error type) -} -``` - -### Safe Operation Execution - -```typescript -// Define an error type with defineErrors -const { ValidationError } = defineErrors({ - ValidationError: ({ cause }: { cause: unknown }) => ({ - message: `JSON parsing failed: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -// Wrapping a potentially throwing operation -const result = trySync({ - try: () => JSON.parse(jsonString), - catch: (error) => ValidationError({ cause: error }), -}); - -if (isErr(result)) { - console.error("Parsing failed:", result.error); -} else { - console.log("Parsed successfully:", result.data); -} -``` - -## Type Inference - -The implementation provides type inference helpers: - -- `UnwrapOk`: Extract the success type from a Result type -- `UnwrapErr`: Extract the error type from a Result type - -## Best Practices - -- Always handle both success and Err data structure cases -- Use the `trySync` and `tryAsync` functions to safely execute operations that might throw exceptions -- Follow the error type naming convention with "Error" suffix -- Include meaningful context in error types for better debugging -- Use tagged unions for error types to enable exhaustive pattern matching \ No newline at end of file diff --git a/src/error/README.md b/src/error/README.md deleted file mode 100644 index 3e09c13..0000000 --- a/src/error/README.md +++ /dev/null @@ -1,328 +0,0 @@ -# Tagged Errors - -Type-safe error handling without throw/catch. Return errors as values with full type inference. - -## Why This Folder Exists - -Traditional error handling with `throw`/`catch` has a fundamental problem: you lose all type information. When you catch an error, TypeScript gives you `unknown`. You have to manually check types, guess at properties, and hope you didn't miss an error case. - -```typescript -try { - await apiCall(); -} catch (error) { - // What is error? Who knows! TypeScript can't help you. - if (error.statusCode === 404) { /* hope this property exists */ } -} -``` - -Tagged errors solve this by treating errors as plain data structures instead of exceptions: - -- **Discriminated unions**: Switch on `error.name` and TypeScript narrows the type automatically -- **Explicit in signatures**: `Result` tells you exactly what can go wrong -- **Flat properties**: All fields live directly on the error object — `error.status`, not `error.context.status` -- **JSON-serializable**: Plain objects that survive serialization without special handling - -## The Mental Model - -``` -┌─────────────────────────────────────────────────────────┐ -│ name → "What broke?" (for code / switch matching) │ -│ message → "What do I tell the user?" (for UI / logs) │ -│ ...rest → "What else matters?" (typed per error) │ -└─────────────────────────────────────────────────────────┘ -``` - -## The `defineErrors` API - -`defineErrors` takes an object where each key is a short variant name (the namespace provides context). Every factory returns `Err<...>` directly — ready for `trySync`/`tryAsync` catch handlers. The variant name is stamped as `name` on the error object. - -```typescript -import { defineErrors, type InferError, type InferErrors } from 'wellcrafted/error'; - -const HttpError = defineErrors({ - Network: () => ({ - message: 'Network request failed', - }), - /** reason is optional — HTTP/2 dropped reason phrases, so it may be absent. */ - Response: ({ status, reason }: { status: number; reason?: string }) => ({ - message: `HTTP ${status}${reason ? `: ${reason}` : ''}`, - status, - reason, - }), -}); - -type HttpError = InferErrors; - -const result = HttpError.Network(); // Err<{ name: 'Network'; message: string }> -const result2 = HttpError.Response({ status: 404 }); // Err<{ name: 'Response'; ... }> -``` - -## Tiers of Error Complexity - -### Tier 0: Minimal Errors — message at call site - -The constructor takes `message` as input and passes it through. The call site provides the message directly. - -```typescript -const AppError = defineErrors({ - Simple: ({ message }: { message: string }) => ({ message }), - - FsRead: ({ message, path }: { message: string; path: string }) => ({ - message, - path, - }), -}); - -AppError.Simple({ message: 'Something went wrong' }); -// → Err<{ name: 'Simple', message: 'Something went wrong' }> - -AppError.FsRead({ message: 'Failed to read config', path: '/etc/config' }); -// → Err<{ name: 'FsRead', message: 'Failed to read config', path: '/etc/config' }> -``` - -### Tier 1: Static Errors — no fields, no arguments - -Zero-arg constructor with a fixed message. Use when there's no dynamic content. - -```typescript -const RecorderError = defineErrors({ - Busy: () => ({ - message: 'A recording is already in progress', - }), -}); - -RecorderError.Busy(); -// → Err<{ name: 'Busy', message: 'A recording is already in progress' }> -``` - -### Tier 2: Cause-Wrapping — `cause` carries the raw caught error - -Use when wrapping a caught error. Accept `cause: unknown` and call `extractErrorMessage` inside the message template — not at the call site. - -```typescript -const SoundError = defineErrors({ - Play: ({ cause }: { cause: unknown }) => ({ - message: `Failed to play sound: ${extractErrorMessage(cause)}`, - cause, - }), -}); - -SoundError.Play({ cause: error }); -// → Err<{ name: 'Play', message: 'Failed to play sound: device busy', cause: }> -``` - -### Tier 3: Structured Data — domain-specific fields - -Use when there's data worth preserving as named fields that callers branch on. - -```typescript -const ApiError = defineErrors({ - /** reason is optional — HTTP/2 dropped reason phrases, so it may be absent. */ - Response: ({ status, reason }: { status: number; reason?: string }) => ({ - message: `HTTP ${status}${reason ? `: ${reason}` : ''}`, - status, - reason, - }), -}); - -ApiError.Response({ status: 404 }); -// → Err<{ name: 'Response', message: 'HTTP 404', status: 404 }> - -ApiError.Response({ status: 500, reason: 'Internal error' }); -// → Err<{ name: 'Response', message: 'HTTP 500: Internal error', status: 500, reason: 'Internal error' }> -``` - -## Mixing Error Shapes - -A single `defineErrors` call can contain any mix of tiers: - -```typescript -const AppError = defineErrors({ - // Tier 1: Static - RecorderBusy: () => ({ - message: 'A recording is already in progress', - }), - - // Tier 3: Structured - Response: ({ provider, status, model }: { provider: string; status: number; model: string }) => ({ - message: `HTTP ${status}`, - provider, - status, - model, - }), - - // Tier 2: Cause-wrapping - Upload: ({ cause }: { cause: unknown }) => ({ - message: `Upload failed: ${extractErrorMessage(cause)}`, - cause, - }), - - // Tier 0: Call-site message with fields - Export: ({ format, message }: { format: string; message: string }) => ({ - message, - format, - }), -}); -``` - -## Flat Fields — No Nesting - -Fields are spread directly on the error object. Access them as top-level properties: - -```typescript -const err = ApiError.Response({ status: 401, reason: 'Unauthorized' }); -// err.error → { name: 'Response', status: 401, reason: 'Unauthorized', message: 'HTTP 401: Unauthorized' } - -// Rest spread extracts just the extra fields -const { name, message, ...rest } = err.error; -// rest = { status: 401, reason: 'Unauthorized' } -``` - -## Reserved Keys - -Only `name` is reserved — `defineErrors` stamps it automatically from the key. `message` is always part of the constructor's return type. - -## JSON Serializability - -Fields must be JSON-serializable values (`JsonObject`). This ensures errors can round-trip through `JSON.stringify`/`JSON.parse` perfectly: - -```typescript -const result = ApiError.Response({ status: 401, reason: 'Unauthorized' }); -const parsed = JSON.parse(JSON.stringify(result.error)); -// parsed.status === 401, parsed.reason === 'Unauthorized' -``` - -**Allowed:** strings, numbers, booleans, null, plain objects, arrays of the above. - -**Not allowed:** `Date` instances, `Error` instances, class instances, functions, `undefined` values, symbols. - -## Wrapping Caught Errors with `cause: unknown` - -When an error type needs to wrap a caught error, accept `cause: unknown` and extract the message inside the factory: - -```typescript -const DbError = defineErrors({ - Backend: ({ backend, cause }: { backend: string; cause: unknown }) => ({ - message: `${backend} failed: ${extractErrorMessage(cause)}`, - backend, - cause, - }), -}); - -DbError.Backend({ backend: 'postgres', cause: error }); -``` - -Call sites stay clean — pass the raw caught error, no transformation needed: - -```typescript -try { - await db.query(sql); -} catch (error) { - return DbError.Backend({ backend: 'postgres', cause: error }); -} -``` - -The constructor owns the message template, so it should also own the cause-to-string transformation. This keeps call sites minimal and preserves the raw `cause` for programmatic access. - -## Avoid String Literal Unions in Variant Inputs - -When a variant's input includes a string literal union like `reason: 'timeout' | 'refused' | 'dns'`, that field is acting as a sub-discriminant — duplicating what variant names already provide. Split into separate variants instead: - -```typescript -// Avoid: consumers must narrow on name AND reason -const NetworkError = defineErrors({ - Request: ({ reason }: { reason: 'timeout' | 'refused' | 'dns' }) => ({ - message: `Request failed: ${reason}`, - reason, - }), -}); - -// Prefer: each failure is its own variant with honest types -const NetworkError = defineErrors({ - Timeout: ({ duration }: { duration: number }) => ({ - message: `Request timed out after ${duration}ms`, - duration, - }), - Refused: ({ host }: { host: string }) => ({ - message: `Connection refused by ${host}`, - host, - }), - DnsFailure: ({ hostname }: { hostname: string }) => ({ - message: `DNS lookup failed for ${hostname}`, - hostname, - }), -}); -``` - -Freeform `string` fields (like `reason: string` for a human-readable description) are fine. The anti-pattern is specifically **literal unions** that consumers would need to narrow on. If no consumer ever switches on a string literal field (it is purely metadata for logging), keeping it as a field is acceptable. - -## Type Annotations with `InferError` and `InferErrors` - -Use `InferError` to extract the error type from a single factory, and `InferErrors` for the union of all errors from a namespace: - -```typescript -import { defineErrors, type InferError, type InferErrors } from 'wellcrafted/error'; - -const AppError = defineErrors({ - Network: () => ({ - message: 'Network request failed', - }), - File: ({ path }: { path: string }) => ({ - message: `File not found: ${path}`, - path, - }), -}); - -type NetworkError = InferError; -// = Readonly<{ name: 'Network'; message: string }> - -type FileError = InferError; -// = Readonly<{ name: 'File'; message: string; path: string }> - -// Union of all errors defined in this namespace -type AppError = InferErrors; -// = NetworkError | FileError - -// Use in discriminated union switches -function handleErrors(error: AppError) { - switch (error.name) { - case 'Network': - console.log('Network failed:', error.message); - break; - case 'File': - console.log('File failed:', error.path); - break; - } -} -``` - -## Quick Reference - -```typescript -import { defineErrors, type InferError, type InferErrors, type AnyTaggedError, extractErrorMessage } from 'wellcrafted/error'; - -const AppError = defineErrors({ - // Call-site message - Simple: ({ message }: { message: string }) => ({ message }), - - // Static message - Network: () => ({ - message: 'Network request failed', - }), - - // Computed message from fields - Response: ({ status, provider }: { status: number; provider: string }) => ({ - message: `${provider}: HTTP ${status}`, - status, - provider, - }), -}); - -AppError.Simple({ message: 'Something went wrong' }); -AppError.Network(); // message: 'Network request failed' -AppError.Response({ status: 404, provider: 'openai' }); // message: "openai: HTTP 404" - -// Type extraction -type NetworkError = InferError; -type AppError = InferErrors; -``` diff --git a/src/query/README.md b/src/query/README.md deleted file mode 100644 index 1cace1b..0000000 --- a/src/query/README.md +++ /dev/null @@ -1,849 +0,0 @@ -# Query Utilities - -A set of utilities for integrating wellcrafted's Result types with TanStack Query, providing a clean pattern for organizing your data fetching layer. - -## Overview - -The query utilities solve a common integration challenge: your service functions return `Result` types, but TanStack Query expects functions that either return data or throw errors. These utilities bridge that gap while providing two convenient interfaces for each query and mutation. - -## Quick Start - -`wellcrafted/query` exposes two layers built on the same conversion path: - -1. **`resultQueryOptions` / `resultMutationOptions`**: platform-agnostic adapters that turn a Result-returning `queryFn` or `mutationFn` into normal TanStack Query options. No `QueryClient` needed. Compose them directly with any framework hook (`createQuery`, `useQuery`, `createMutation`, `useMutation`). - -2. **`createQueryFactories(queryClient)` -> `defineQuery` / `defineMutation`**: bind the same options to a specific `QueryClient` and attach imperative helpers. Queries expose explicit `.fetch()` and `.ensure()` methods because those choose different cache policies. Mutations are callable because there is one imperative action: run the mutation. - -```typescript -import { QueryClient } from '@tanstack/query-core'; -import { - createQueryFactories, - resultQueryOptions, - resultMutationOptions, -} from 'wellcrafted/query'; - -// Local options used directly inside a hook -const user = createQuery(() => - resultQueryOptions({ - queryKey: ['user', userId], - queryFn: () => services.getUser(userId), - }), -); - -const save = createMutation(() => - resultMutationOptions({ - mutationKey: ['saveUser'], - mutationFn: (input: SaveUserInput) => services.saveUser(input), - }), -); - -// Reusable definitions bound to a QueryClient -const queryClient = new QueryClient(); -const { defineQuery, defineMutation } = createQueryFactories(queryClient); -``` - -### `resultQueryOptions(input)` - -- Accepts a `queryKey` plus a `queryFn` that returns `Result` (sync or async). -- Returns standard `QueryObserverOptions` whose `queryFn` resolves `Ok(data)` with `data` and throws on `Err(error)`. -- Preserves literal `queryKey` tuples (no `as const` needed) and Result data/error inference. - -### `resultMutationOptions(input)` - -- Accepts a `mutationKey` plus a `mutationFn` that returns `Result` (sync or async). -- Returns standard mutation observer options whose `mutationFn` resolves `Ok(data)` with `data` and throws on `Err(error)`. -- Infers variables from the `mutationFn` parameter. - -### Which one do I reach for? - -Both families ride the same conversion path, so the choice is about *where the operation lives*, not about how Results are unwrapped. - -Reach for **`resultQueryOptions` / `resultMutationOptions`** when the operation is local to the hook call site — any of: - -- **Hook-local**: it is defined and used in one component, not shared. -- **Reactive options**: the `queryKey`, `enabled`, or `queryFn` closes over framework state (Svelte `$derived`, React state, props). These adapters run *inside* the reactive thunk (`createQuery(() => resultQueryOptions({ … }))`), so the options recompute every render. `defineQuery.options` is a static snapshot computed once and cannot carry a reactive key. -- **No global / a local `QueryClient`**: you are in a shared package with no app client to bind, or you want a purpose-built client with its own policy. These adapters are client-agnostic; the hook supplies the client. - -Reach for **`createQueryFactories(queryClient).defineQuery` / `defineMutation`** when the operation is a reusable, `QueryClient`-bound handle — any of: - -- **Shared / reusable identity**: one definition consumed from several call sites (an RPC/query layer). -- **Imperative execution**: you need `.fetch()` / `.ensure()` on a query, or a callable mutation handle, for preloaders, event handlers, and workflows — not just reactive `.options`. -- **Bound to the app client**: it lives at module scope against one long-lived `QueryClient`. - -> **Cache operations stay on the `QueryClient`.** Wellcrafted deliberately does **not** wrap `invalidateQueries`, `setQueryData`, `getQueryData`, `ensureQueryData`, `prefetchQuery`, or any other cache method. Those already have TanStack's own contract and no `Result` to unwrap, so wrapping them would add surface without value. Call them directly on the `queryClient` (see the mutation cache-update examples below). The two families above wrap exactly one thing: a Result-returning `queryFn` / `mutationFn`. - -> **Note on naming:** these adapters were previously called `queryOptions` / `mutationOptions`, which collided with TanStack Query's own identity helpers of the same name. The `result*` prefix removes that collision and says what they do — adapt a `Result`-returning function — so both can coexist in one file without aliasing. - -## Architecture Pattern: The RPC-like Approach - -The query utilities enable a powerful architectural pattern inspired by RPC (Remote Procedure Call), where your query layer acts as a bridge between UI components and pure service functions. This pattern is used successfully in production apps like [Whispering](https://github.com/braden-w/whispering). - -``` -┌─────────────┐ ┌─────────────┐ ┌──────────────┐ -│ UI │ --> │ Query/RPC │ --> │ Services │ -│ Components │ │ Layer │ │ (Pure) │ -└─────────────┘ └─────────────┘ └──────────────┘ - ↑ │ - └────────────────────┘ - Reactive Updates -``` - -### Layer Responsibilities - -1. **Services Layer**: Pure functions with no UI dependencies - - Business logic only - - Return `Result` types - - Platform-agnostic - - Easily testable - -2. **Query Layer**: Adds reactivity and caching - - Wraps service calls with TanStack Query - - Handles runtime dependency injection - - Transforms errors for UI consumption - - Manages cache updates - -3. **UI Layer**: Consumes queries reactively or imperatively - - Uses reactive options and imperative helpers - - Handles loading states - - Displays errors - -## Recommended Folder Structure - -Here's how to organize your code for maximum clarity: - -``` -src/ -├── services/ # Pure business logic -│ ├── api/ # External API calls -│ │ ├── users.ts -│ │ └── products.ts -│ ├── db/ # Database operations -│ │ └── index.ts -│ └── platform/ # Platform-specific code -│ ├── web.ts -│ └── desktop.ts -│ -├── query/ # Query layer with caching -│ ├── _factories.ts # createQueryFactories setup -│ ├── users.ts # User-related queries -│ ├── products.ts # Product-related queries -│ └── index.ts # RPC namespace export -│ -└── components/ # UI components - └── UserList.svelte -``` - -## Real-World Example: Building an RPC Namespace - -This example shows how to build a complete data fetching layer: - -### Step 1: Create Your Services - -```typescript -// services/api/users.ts -import { Ok, Err, type Result } from 'wellcrafted/result'; - -export type User = { - id: string; - name: string; - email: string; -}; - -export type UserServiceError = { - code: 'NOT_FOUND' | 'NETWORK_ERROR' | 'UNAUTHORIZED'; - message: string; -}; - -export async function getUser(id: string): Promise> { - try { - const response = await fetch(`/api/users/${id}`); - - if (!response.ok) { - if (response.status === 404) { - return Err({ code: 'NOT_FOUND', message: 'User not found' }); - } - return Err({ code: 'NETWORK_ERROR', message: 'Failed to fetch user' }); - } - - return Ok(await response.json()); - } catch (error) { - return Err({ code: 'NETWORK_ERROR', message: error.message }); - } -} - -export async function updateUser(user: User): Promise> { - // Implementation... -} -``` - -### Step 2: Create Query Definitions - -```typescript -// query/_factories.ts -import { QueryClient } from '@tanstack/query-core'; -import { createQueryFactories } from 'wellcrafted/query'; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - }, - }, -}); - -export const { defineQuery, defineMutation } = createQueryFactories(queryClient); -``` - -```typescript -// query/users.ts -import { defineQuery, defineMutation, queryClient } from './_factories'; -import * as userService from '../services/api/users'; - -export const users = { - // Query with parameters - getUser: (userId: string) => - defineQuery({ - queryKey: ['users', userId], - queryFn: () => userService.getUser(userId), - }), - - // Query all users - getAllUsers: defineQuery({ - queryKey: ['users'], - queryFn: () => userService.getAllUsers(), - }), - - // Mutation with optimistic updates - updateUser: defineMutation({ - mutationKey: ['users', 'update'], - mutationFn: async (user: User) => { - const result = await userService.updateUser(user); - - if (result.error) return result; - - // Optimistic cache update - queryClient.setQueryData(['users', user.id], user); - queryClient.invalidateQueries({ queryKey: ['users'] }); - - return result; - }, - onError: (error) => { - // Error is already in the right format for UI - console.error('Failed to update user:', error); - }, - }), -}; -``` - -### Step 3: Create the RPC Namespace - -```typescript -// query/index.ts -export { queryClient } from './_factories'; - -import { users } from './users'; -import { products } from './products'; -import { settings } from './settings'; - -// This creates your RPC-like interface -export const rpc = { - users, - products, - settings, -} as const; -``` - -### Step 4: Use in Components - -```svelte - - - -{#if userQuery.isPending} -
Loading user...
-{:else if userQuery.error} -
Error: {userQuery.error.message}
-{:else if userQuery.data} - updateMutation.mutate(user)} - isSaving={updateMutation.isPending} - /> -{/if} -``` - -**React equivalent:** - -```tsx -// components/UserProfile.tsx -import { useQuery, useMutation } from '@tanstack/react-query'; -import { rpc } from '../query'; -import { toast } from '../toast'; - -interface UserProfileProps { - userId: string; -} - -export function UserProfile({ userId }: UserProfileProps) { - // Reactive query - automatically updates UI (React passes options directly) - const userQuery = useQuery(rpc.users.getUser(userId).options); - - // Reactive mutation - provides loading states - const updateMutation = useMutation(rpc.users.updateUser.options); - - // Or use imperatively in event handlers - async function handleQuickUpdate(updates: Partial) { - const { data, error } = await rpc.users.updateUser({ - ...userQuery.data, - ...updates - }); - - if (error) { - toast.error(error.message); - } - } - - if (userQuery.isPending) return
Loading user...
; - if (userQuery.error) return
Error: {userQuery.error.message}
; - if (!userQuery.data) return null; - - return ( - updateMutation.mutate(user)} - isSaving={updateMutation.isPending} - /> - ); -} -``` - -## The Dual Interface Pattern - -Every query and mutation provides two ways to use it: - -### 1. Reactive Interface (`.options`) - -Best for UI components that need to track state: - -**Svelte 5** (wrap entire expression in accessor function): - -```typescript -// With reactive parameter - creates a reactive subscription -const userId = $state('abc-123'); -const query = createQuery(() => rpc.users.getUser(() => userId).options); -// Access: query.data, query.isPending, query.error -// Query automatically re-runs when userId changes - -// With static parameter - simpler when value never changes -const query = createQuery(() => rpc.users.getUser('static-id').options); -``` - -**React** (pass options directly): - -```tsx -// With reactive parameter - creates a reactive subscription -const [userId, setUserId] = useState('abc-123'); -const query = useQuery(rpc.users.getUser(userId).options); -// Access: query.data, query.isPending, query.error -// Query automatically re-runs when userId changes - -// With static parameter - simpler when value never changes -const query = useQuery(rpc.users.getUser('static-id').options); -``` - -**Important**: -- **Svelte 5**: Wrap in accessor function `() => ...options`. For reactive parameters inside, also use accessor functions `() => param`. -- **React**: Pass `.options` directly (it's a property, not a function). Pass reactive parameters directly (no accessor function needed). - -### 2. Imperative Interface - -Best for event handlers and workflows: - -```typescript -// Direct execution without subscriptions -const { data, error } = await rpc.users.updateUser(user); -// No reactive overhead, just the result - -// For queries, .ensure() prefers cached data -const { data, error } = await rpc.users.getUser('123').ensure(); - -// Use .fetch() when you need to check freshness -const { data, error } = await rpc.users.getUser('123').fetch(); -``` - -Queries are not directly callable. Choose `.fetch()` when you want TanStack's freshness policy, or `.ensure()` when you want cache-first data and only want to fetch if data is missing. - -Mutations are directly callable because there is only one imperative action: - -```typescript -const ensured = await rpc.users.getUser('123').ensure(); -const fetched = await rpc.users.getUser('123').fetch(); - -const updated = await rpc.users.updateUser(user); -``` - -## Common Mistakes with `.options` - -Understanding the `.options` pattern is crucial for proper reactive behavior. Here are the most common mistakes: - -### ❌ Mistake 1: Wrong `.options` pattern for your framework - -**Svelte 5:** - -```typescript -// WRONG: Not wrapping in accessor function -const userId = $state('abc-123'); -const query = createQuery(rpc.users.getUser(userId).options); -// ^^^^^^^^^^^^ ^ -// Missing outer accessor Svelte 5 requires () => ... - -// ALSO WRONG: Calling .options() with parentheses (.options is a property) -const query = createQuery(() => rpc.users.getUser(userId).options()); -// ^^ -// Don't call it! -``` - -```typescript -// CORRECT: Wrap in accessor, use inner accessor for reactive param -const userId = $state('abc-123'); -const query = createQuery(() => rpc.users.getUser(() => userId).options); -// ^^^^ ^^^^^^^^^^^ -// outer accessor inner accessor for reactive param -``` - -**React:** - -```tsx -// WRONG: Calling .options() with parentheses (.options is a property) -const [userId, setUserId] = useState('abc-123'); -const query = useQuery(rpc.users.getUser(userId).options()); -// ^^ -// Don't call it! - -// ALSO WRONG: Using unnecessary accessor function -const [userId, setUserId] = useState('abc-123'); -const query = useQuery(() => rpc.users.getUser(userId).options); -// ^^^^ -// Don't need accessor wrapper in React! -``` - -```tsx -// CORRECT: Pass .options directly (it's a property, not a function) -const [userId, setUserId] = useState('abc-123'); -const query = useQuery(rpc.users.getUser(userId).options); -// ^^^^^^^ direct param -// ^ no parentheses! -``` - -**Why it's wrong**: -- **Svelte 5**: Requires accessor wrapper `() => ...options` and inner accessors `() => param` for reactive values -- **React**: Pass `.options` directly (no parentheses, no wrapper) with direct parameter passing - -### ❌ Mistake 2: Passing reactive values directly (Svelte-specific) - -```typescript -// WRONG: Passes a snapshot, breaks reactivity (missing both accessors) -const id = $state('abc-123'); -const query = createQuery(rpc.users.getUser(id).options); -// ^ ^^ -// Missing outer accessor Direct value breaks reactivity -``` - -```typescript -// CORRECT: Use accessor functions for reactive tracking -const id = $state('abc-123'); -const query = createQuery(() => rpc.users.getUser(() => id).options); -// ^^^^ ^^^^^^^^ -// outer accessor inner accessor preserves reactivity -``` - -**Why it's wrong**: In Svelte 5, `createQuery` expects an accessor function that returns the options. Additionally, when you pass `id` directly to the RPC method, the query definition runs once with the current value. Changes to `id` won't trigger new queries. Using `() => id` creates a function that TanStack Query can call each time it needs the value, preserving reactivity. - -**Note**: This is Svelte-specific. React hooks automatically track dependencies, so you pass values directly without accessor wrappers. - -### ❌ Mistake 3: Wrong method call order - -**Svelte 5:** - -```typescript -// WRONG: Can't access .options before calling the method -const query = createQuery(() => rpc.users.getUser.options(() => userId)); -// ^^^^^^^^^^^^^^^^^^^^^^^ -// .options isn't a function -``` - -```typescript -// CORRECT: Call method with accessor, then access .options property -const query = createQuery(() => rpc.users.getUser(() => userId).options); -// ^^^^ ^^^^^^^^^^^^^ call with accessor -// outer accessor ^^^^^^^ then .options property -``` - -**React:** - -```tsx -// WRONG: Can't access .options before calling the method -const query = useQuery(rpc.users.getUser.options(userId)); -// ^^^^^^^^^^^^^^^^^ -// .options isn't a function -``` - -```tsx -// CORRECT: Call method with param, then access .options property -const query = useQuery(rpc.users.getUser(userId).options); -// ^^^^^^^^^^^^ call with param -// ^ then .options property (not function!) -``` - -**Why it's wrong**: The RPC method structure is: -- **Svelte 5**: `() => method(() => param).options` (outer accessor, inner accessor for param, then property) -- **React**: `method(param).options` (direct param, then property access) - -First call the method with its parameter, which returns an object containing the `.options` property. - -### When to Use Accessor Functions (Svelte 5-Specific) - -**Note**: Accessor functions are only needed in Svelte 5. React hooks automatically track dependencies, so you pass values directly. - -In Svelte 5, you always need the outer accessor wrapper `() => ...options`. Additionally, use inner accessor functions for **reactive values**: - -```typescript -// ✅ Props (Svelte 5) -let { userId } = $props<{ userId: string }>(); -const query = createQuery(() => rpc.users.getUser(() => userId).options); - -// ✅ $state variables -const searchTerm = $state(''); -const query = createQuery(() => rpc.products.search(() => searchTerm).options); - -// ✅ $derived values -const fullName = $derived(`${firstName} ${lastName}`); -const query = createQuery(() => rpc.users.searchByName(() => fullName).options); - -// ✅ Store values -const settings = getSettings(); // returns a store -const query = createQuery(() => rpc.api.getData(() => settings.apiKey).options); -``` - -For **static values**, you still need the outer accessor but can pass the value directly to the RPC method: - -```typescript -// ✅ String literals -const query = createQuery(() => rpc.users.getUser('user-123').options); - -// ✅ Numbers -const query = createQuery(() => rpc.products.getProduct(42).options); - -// ✅ Constants -const ADMIN_ID = 'admin-001'; -const query = createQuery(() => rpc.users.getUser(ADMIN_ID).options); -``` - -### Quick Reference - -| Pattern | Svelte 5 | React | Use Case | -|---------|----------|-------|----------| -| **No parameters** | `createQuery(() => rpc.users.getAll.options)` | `useQuery(rpc.users.getAll.options)` | Static query, no params | -| **Static parameter** | `createQuery(() => rpc.users.getUser('id-123').options)` | `useQuery(rpc.users.getUser('id-123').options)` | Non-reactive value | -| **Reactive parameter** | `createQuery(() => rpc.users.getUser(() => userId).options)` | `useQuery(rpc.users.getUser(userId).options)` | Props, state, derived | -| **Multiple parameters** | `createQuery(() => rpc.products.search(() => term, () => category).options)` | `useQuery(rpc.products.search(term, category).options)` | Multiple reactive values | - -**Key differences:** -- **Svelte 5**: Outer accessor wrapper `() => ...options`, inner accessor functions `() => param` for reactive values -- **React**: `.options` property directly (no wrapper, no parentheses), direct parameter passing - -## Advanced Patterns - -### Runtime Dependency Injection - -The query layer can handle dynamic service selection based on runtime conditions: - -```typescript -// query/transcription.ts -import { settings } from '../stores/settings'; - -export const transcription = { - transcribe: defineMutation({ - mutationFn: async (audio: Blob) => { - // Select service based on user settings - const provider = settings.value.transcriptionProvider; - - switch (provider) { - case 'openai': - return services.openai.transcribe(audio, { - apiKey: settings.value.openaiKey, - model: settings.value.openaiModel, - }); - case 'whisper': - return services.whisper.transcribe(audio); - default: - return Err({ code: 'INVALID_PROVIDER', message: `Unknown provider: ${provider}` }); - } - }, - }), -}; -``` - -### Error Transformation - -Transform service errors into UI-friendly formats: - -```typescript -// query/users.ts -export const users = { - createUser: defineMutation({ - mutationFn: async (userData: CreateUserInput) => { - const result = await userService.createUser(userData); - - if (result.error) { - // Transform service error to UI error - return Err({ - title: 'Failed to create user', - description: getErrorMessage(result.error.code), - action: { type: 'retry', data: userData }, - }); - } - - return result; - }, - }), -}; - -function getErrorMessage(code: string): string { - switch (code) { - case 'DUPLICATE_EMAIL': - return 'A user with this email already exists'; - case 'INVALID_DATA': - return 'Please check your input and try again'; - default: - return 'An unexpected error occurred'; - } -} -``` - -### Coordinating Multiple Services - -The query layer can orchestrate complex operations: - -```typescript -// query/orders.ts -export const orders = { - placeOrder: defineMutation({ - mutationFn: async (orderData: OrderInput) => { - // Step 1: Validate inventory - const inventory = await rpc.inventory.checkAvailability.ensure(); - if (!hasStock(orderData.items, inventory.data)) { - return Err({ code: 'OUT_OF_STOCK', message: 'Some items are out of stock' }); - } - - // Step 2: Process payment - const payment = await services.payment.charge(orderData.payment); - if (payment.error) return Err(payment.error); - - // Step 3: Create order - const order = await services.orders.create({ - ...orderData, - paymentId: payment.data.id, - }); - - // Step 4: Update caches - queryClient.invalidateQueries({ queryKey: ['inventory'] }); - queryClient.invalidateQueries({ queryKey: ['orders'] }); - - return order; - }, - }), -}; -``` - -## Testing - -The factory pattern makes testing straightforward: - -```typescript -// tests/users.test.ts -import { QueryClient } from '@tanstack/query-core'; -import { createQueryFactories } from 'wellcrafted/query'; -import { vi } from 'vitest'; - -describe('User Queries', () => { - let queryClient: QueryClient; - let defineQuery: ReturnType['defineQuery']; - - beforeEach(() => { - queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } } - }); - ({ defineQuery } = createQueryFactories(queryClient)); - }); - - it('should fetch user data', async () => { - const mockUser = { id: '1', name: 'Test User', email: 'test@example.com' }; - vi.mocked(userService.getUser).mockResolvedValue(Ok(mockUser)); - - const userQuery = defineQuery({ - queryKey: ['users', '1'], - queryFn: () => userService.getUser('1'), - }); - - const result = await userQuery.fetch(); - expect(result.data).toEqual(mockUser); - }); -}); -``` - -## Best Practices - -1. **Keep Services Pure**: Services should only contain business logic, no UI concerns -2. **Use Result Types**: Always return `Result` from services for consistent error handling -3. **Transform Errors in Query Layer**: Convert service errors to UI-friendly formats -4. **Leverage Both Interfaces**: Use reactive for UI state, imperative for workflows -5. **Organize by Feature**: Group related queries together (users, products, etc.) -6. **Cache Thoughtfully**: Update related caches after mutations - -## Migration Guide - -If you're migrating from a traditional setup: - -```typescript -// Before: Direct API calls in components -async function loadUser() { - try { - const response = await fetch(`/api/users/${userId}`); - user = await response.json(); - } catch (error) { - errorMessage = error.message; - } -} - -// After: Using the query pattern (Svelte 5) -const userQuery = createQuery(() => rpc.users.getUser(() => userId).options); -// Automatically handles loading, error, caching, and refetching -``` - -## Troubleshooting `.options` Errors - -If you're getting errors with `.options`, use this quick diagnostic guide: - -### Error: "Cannot read property 'options' of undefined" - -**Problem**: The RPC method itself is undefined. - -**Common causes**: -```typescript -// ❌ Typo in method name -createQuery(() => rpc.users.getUzer(id).options); // 'getUzer' doesn't exist - -// ❌ Method not exported from RPC namespace -createQuery(() => rpc.users.privateMethod(id).options); // not in exports -``` - -**Fix**: Check that the method exists and is properly exported in your RPC namespace. - -### Error: "options is not a function" (when calling it) - -**Problem**: You're trying to call `.options()` with parentheses. - -**Fix**: Remove the parentheses from `.options` (it's a property, not a function): -```typescript -// ❌ Wrong: calling .options() with parentheses -createQuery(() => rpc.users.getUser(id).options()) - -// ✅ Correct (Svelte 5): .options as property with accessor wrapper -const id = $state('abc-123'); -createQuery(() => rpc.users.getUser(() => id).options) - -// ✅ Correct (React): .options as property directly -useQuery(rpc.users.getUser(id).options) -``` - -### Query doesn't update when reactive value changes (Svelte 5) - -**Problem**: You're missing the outer accessor wrapper or passing the value directly instead of using an inner accessor function. - -**Symptoms**: Query runs once but doesn't re-run when `userId` changes. - -**Fix**: Use both outer and inner accessor functions: -```typescript -let { userId } = $props<{ userId: string }>(); - -// ❌ Wrong: Missing outer accessor -createQuery(rpc.users.getUser(() => userId).options) - -// ❌ Wrong: Missing inner accessor for reactive value -createQuery(() => rpc.users.getUser(userId).options) - -// ✅ Correct: Both outer and inner accessors -createQuery(() => rpc.users.getUser(() => userId).options) -``` - -### Type error: "Type 'string' is not assignable to type '() => string'" - -**Problem**: Your RPC method expects an accessor function but you're passing a static value. - -**When this happens**: The method was defined to always use accessors for reactivity. - -**Fix**: Either wrap in an accessor or update the RPC method definition: -```typescript -// Option 1: Wrap in accessor (if value might change) -createQuery(() => rpc.users.getUser(() => CONSTANT_ID).options) - -// Option 2: Update RPC definition to accept both -// In query/users.ts: -getUser: (userId: string | (() => string)) => defineQuery({ - queryKey: ['users', typeof userId === 'function' ? userId() : userId], - queryFn: () => services.getUser( - typeof userId === 'function' ? userId() : userId - ), -}), -``` - -### Query runs too many times - -**Problem**: Accessor function has side effects or creates new objects on each call. - -**Symptoms**: You see excessive network requests or query re-runs. - -**Fix**: Make accessor functions pure and stable: -```typescript -// ❌ Wrong: Creates new object every call -createQuery( - () => rpc.products.search(() => ({ term: searchTerm, category })).options -) - -// ✅ Correct: Pass primitive values separately or use $derived -const searchParams = $derived({ term: searchTerm, category }); -createQuery( - () => rpc.products.searchWithParams(() => searchParams).options -) -``` - -## Additional Resources - -- [Whispering App](https://github.com/braden-w/whispering) - A real-world example using this pattern -- [TanStack Query Docs](https://tanstack.com/query) - Learn more about the underlying query library -- [Result Type Pattern](/result/README.md) - Understanding Result types for error handling diff --git a/src/result/result.test.ts b/src/result/result.test.ts index 6db7893..2cbbbe4 100644 --- a/src/result/result.test.ts +++ b/src/result/result.test.ts @@ -44,7 +44,7 @@ describe("Ok / Err structural invariants", () => { // Documents the shape's known limit. `Err(null)` produces // `{ data: null, error: null }` which is structurally identical to `Ok(null)`, // so the isErr discriminator reads it as Ok. We do NOT ban this at the type - // level — see docs/philosophy/err-null-is-ok-null.md for why. Test pins the + // level — see docs/decisions/result-shape.mdx for why. Test pins the // runtime behavior so contributors can see the limit exists. test("Err(null) collides with Ok(null) — the shape's known limit", () => { const badErr = Err(null); From 0f18ee68a48f0beb7aab14c363add1e1e9308e1c Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:07:09 -0700 Subject: [PATCH 13/13] docs: complete independent documentation review --- .claude/skills/services-layer/SKILL.md | 2 +- docs/index.mdx | 12 ++++++++++++ docs/reference/query.mdx | 2 +- ...710T012026-greenfield-documentation-pass.md | 18 +++++++++++++----- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.claude/skills/services-layer/SKILL.md b/.claude/skills/services-layer/SKILL.md index 51227c2..924ac8b 100644 --- a/.claude/skills/services-layer/SKILL.md +++ b/.claude/skills/services-layer/SKILL.md @@ -298,7 +298,7 @@ This applies to: - **Ternary expressions** that pick between fundamentally different messages - **Lookup tables** keyed on input fields (covered by the string literal union rule above) -> See also: `docs/core/error-system.mdx` § "3b. Avoid Conditional Logic on Factory Inputs" for the canonical reference with full examples. +> See also: `docs/guides/defining-error-vocabularies.mdx` § "Keep the vocabulary closed" for the public guidance on keeping variants distinct. ## Service Implementation Pattern diff --git a/docs/index.mdx b/docs/index.mdx index f41c147..5dc45e5 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -26,6 +26,18 @@ wellcrafted defines expected errors as plain, boundary-friendly data that can mo ## Continue by task + + Design named variants and deliberate boundary-friendly fields. + + + Propagate success and expected failure with ordinary TypeScript control flow. + + + Classify infrastructure failures into application-owned error data. + + + Keep JSON shape, runtime validation, and static typing as separate guarantees. + Adapt Result-returning functions to TanStack's throwing contract. diff --git a/docs/reference/query.mdx b/docs/reference/query.mdx index fc84743..24fa416 100644 --- a/docs/reference/query.mdx +++ b/docs/reference/query.mdx @@ -98,4 +98,4 @@ const accountKeys = defineKeys({ Static entries preserve readonly literal tuples without `as const`. Factory entries preserve tuple shape but widen literal positions under contextual typing. Add `as const` inside a factory when those literal positions must remain exact. Empty arrays and values that are not key tuples are rejected. -For choosing between adapters and bound factories in UI code, see the TanStack Query integration guide. +For choosing between adapters and bound factories in UI code, see the [TanStack Query integration guide](/integrations/tanstack-query). diff --git a/specs/20260710T012026-greenfield-documentation-pass.md b/specs/20260710T012026-greenfield-documentation-pass.md index 0e6ae4f..69d55da 100644 --- a/specs/20260710T012026-greenfield-documentation-pass.md +++ b/specs/20260710T012026-greenfield-documentation-pass.md @@ -736,11 +736,19 @@ Deletion proof on 2026-07-10: ### Wave 12: Independent final review -- [ ] Run fresh-context first-reader review. -- [ ] Run skeptical API and claims review. -- [ ] Incorporate grounded findings or record why they were rejected. -- [ ] Add the review summary and final proof results to this spec. -- [ ] Stop on a local PR-ready branch. Do not push, publish, deploy, or open a PR without permission. +- [x] Run fresh-context first-reader review. +- [x] Run skeptical API and claims review. +- [x] Incorporate grounded findings or record why they were rejected. +- [x] Add the review summary and final proof results to this spec. +- [x] Stop on a local PR-ready branch. Do not push, publish, deploy, or open a PR without permission. + +Independent review and final proof on 2026-07-10: + +- A fresh-context first-reader reviewer found three grounded navigation/ownership issues: `.claude/skills/services-layer/SKILL.md` still linked a deleted error-system page, the home page's “Continue by task” section omitted all four core guides, and `reference/query` named the TanStack integration without linking it. The stale skill reference now points to the vocabulary guide, the home page exposes all four core tasks before secondary integrations, and the Query reference links the integration owner. +- A separate skeptical API and claims reviewer found no issues. That review independently checked serialization qualifications, falsy Result errors and the `Err(null)` collision, the Hono three-guarantee distinction, four pinned Epicenter attributions, metric and reliability prohibitions, the TanStack prerequisite, the 42-file claims scan with zero exclusions, and the exact Wave 11 deletion set. +- After incorporating the first-reader findings, `PUPPETEER_SKIP_DOWNLOAD=true bun install --frozen-lockfile` made no changes. Format, lint, typecheck, build, and all 188 tests passed; lint retained the same 12 pre-existing warnings and no errors. +- Examples, package smoke, both compatibility fixtures, 79 export tuples across nine reference owners, all 42 current-file claims, and canonical snippets passed. Node 24.14.0 Mint validation and broken-link checks passed against pinned Mint 4.2.684, and the workflow YAML parsed successfully. +- The branch is local and PR-ready. No push, publication, deployment, or pull request was performed. ## Edge Cases and Deferred API Questions