From 6d5ff71770a3a4901462aca8c40d87f7712e6ca5 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 14:48:53 +0100 Subject: [PATCH 01/42] docs: design spec for production-grade concerns Captures the design for four scoped workstreams: 1. README positioning + tagline 2. Explicit error-code escape hatch (docs gap) 3. TaskResult production-grade pass (cancellation, pruning, docs) 4. ESLint plugin robustness via fixture corpus Sequenced 1 -> 2 -> 3 -> 4; small/safe items first. --- ...-05-22-production-grade-concerns-design.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-22-production-grade-concerns-design.md diff --git a/docs/superpowers/specs/2026-05-22-production-grade-concerns-design.md b/docs/superpowers/specs/2026-05-22-production-grade-concerns-design.md new file mode 100644 index 0000000..98510de --- /dev/null +++ b/docs/superpowers/specs/2026-05-22-production-grade-concerns-design.md @@ -0,0 +1,292 @@ +# Production-Grade Concerns: Design + +**Date:** 2026-05-22 +**Status:** Draft for review +**Scope:** Address four concerns surfaced during package review to bring faultline closer to production-grade: + +1. README positioning + tagline +2. Explicit error-code escape hatch (documentation gap) +3. TaskResult production-grade pass (cancellation, API pruning, docs) +4. ESLint plugin robustness via a comprehensive fixture suite + +These are sequenced from smallest/highest-confidence to largest/most-uncertain. + +## 1. README positioning + tagline + +### Problem + +The current tagline — *"The complete type-safe error system for TypeScript"* — invites direct comparison to Effect-ts, which faultline cannot win on breadth. The README also lacks any direct framing against the two libraries every reader will silently compare to: neverthrow and Effect. The existing `?=` proposal comparison is good but lives in isolation. + +### Approach + +**Tagline change.** Lead with the actual wedge: incrementality. + +- Current: *"The complete type-safe error system for TypeScript."* +- New: *"The incremental type-safe error system for TypeScript."* +- Sub-line (already strong, keep): *"Define your errors. Throw them like normal. The ESLint plugin tells you when you miss something. Adopt Result types and boundaries when you're ready — or don't. Either way, your app is better off."* + +**New "How faultline differs" section** in the README, placed after "Why" and before "Install". Prose only — no comparison table. Three short paragraphs: + +- **vs. neverthrow / ts-results / true-myth.** Acknowledge they're solid Result-type libraries. Faultline's difference: it doesn't require you to convert your throws first. Stage 1 (typed factories + ESLint) gives you most of the value without touching control flow. Result types are available when you want them at Stage 3. +- **vs. Effect.** Acknowledge Effect is more powerful and mature. Faultline's difference: it's narrowly focused on errors. No effect system, no runtime, no scheduler — just typed errors plus optional Results. The learning curve is hours, not weeks. +- **vs. typed Error subclasses.** Acknowledge custom Error classes work. Faultline's difference: consistent tags/codes/serialization derived from a single declaration, an ESLint plugin that catches drift, and a smooth path to Result types if you want them later. + +Move the existing `?=` proposal comparison into this section as a fourth paragraph. + +### Files touched + +- `README.md` — tagline (top), new "How faultline differs" section, relocate `?=` comparison. + +### Acceptance criteria + +- Tagline updated. +- "How faultline differs" section exists with four short paragraphs (neverthrow-family, Effect, custom Error subclasses, `?=` proposal). +- No comparison table. +- README still scans well — section is ≤ 400 words total. + +--- + +## 2. Explicit error-code escape hatch (documentation gap) + +### Problem + +Reviewers (including me) assumed faultline only supports auto-generated SCREAMING_SNAKE codes (`USER_NOT_FOUND`). It actually already supports user-supplied codes: + +- Runtime: `define-error.ts:327` — `const code = definition.code ?? autoCodeFromTag(tag);` +- Type-level: `defineError` has overloads that preserve the literal `Code` type when explicitly provided. +- `defineErrors` constraint (`ErrorDefConstraint`) accepts `code?: string` per definition. + +This is a documentation gap, not an API gap. Teams with pre-existing error-code conventions (e.g., `ERR_USER_404`, `auth.user.not_found`) need to know they can keep them. + +### Approach + +**Documentation.** Add a short example in the README's "Stage 1: Define and throw" section showing both forms: + +```ts +const UserErrors = defineErrors('User', { + // Auto-generated code: 'USER_NOT_FOUND' + NotFound: { + status: 404, + message: (data: { userId: string }) => `User ${data.userId} not found`, + }, + // Explicit code: keeps your existing convention + Unauthorized: { + code: 'ERR_AUTH_401', + status: 401, + }, +}); +``` + +One short paragraph: "Codes are auto-generated from the tag by default. Pass an explicit `code` to keep an existing convention — it's preserved at the type level too, so consumers of the factory see the literal." + +**Type-preservation regression test.** Add a `.test-d.ts`-style assertion (or a runtime test with `expectTypeOf` / equivalent — whatever pattern the repo already uses) proving the explicit code survives: + +```ts +const errors = defineErrors('User', { + NotFound: { code: 'CUSTOM_CODE', status: 404 }, +}); +// Type-level assertion: the factory's output type has code: 'CUSTOM_CODE' (literal, not string) +type Code = ReturnType['code']; +// expect Code to equal 'CUSTOM_CODE' +``` + +Plus a runtime assertion: `expect(errors.NotFound().code).toBe('CUSTOM_CODE');` + +### Files touched + +- `README.md` — example in Stage 1 section + one-line note. +- `packages/faultline/test/define-error.test.ts` (or wherever existing defineErrors tests live) — new test case for explicit code at both runtime and type levels. + +### Acceptance criteria + +- README shows an explicit-code example. +- A test asserts the literal type is preserved through `defineErrors`. +- A test asserts the runtime value matches the explicit code. + +--- + +## 3. TaskResult production-grade pass + +### Problem + +`TaskResult` is a third primitive alongside `Promise` and `Result`. It earns its place if and only if it does things `Promise` can't. Today: + +- **Cancellation is undercooked.** `signal` is passed via `TaskContext` to the executor, but chain methods (`.map`, `.andThen`, `.andThenTask`, `.catchTag`, `.tap`, `.tapError`, `.withContext`) never check `context.signal.aborted` between steps. A long pipeline keeps executing every step after abort — the signal only affects the leaf executor. +- **API surface has redundancy.** `toPromise()` aliases `run()`. The `match()` instance method overlaps with `await task.run().then(r => r.match(...))`. +- **No documented decision rule.** Readers don't know when to reach for TaskResult vs plain `Promise`. + +### Approach + +#### 3.1 Cancellation propagation + +Between every chain step, check `context.signal?.aborted` and short-circuit to `System.Cancelled` if so. Concretely, each chain method's executor should look like: + +```ts +return TaskResult.from(async (context) => { + if (context.signal?.aborted) { + return err(SystemErrors.Cancelled({ reason: context.signal.reason })) as Result; + } + const result = await this.executor(context); + if (context.signal?.aborted) { + return err(SystemErrors.Cancelled({ ... })); + } + // ... existing logic +}); +``` + +This adds `System.Cancelled` to the error union of every chained TaskResult. That's a correct widening — the chain can in fact produce cancellation at any step. + +**Note on type widening.** The current chain method signatures don't include `System.Cancelled` in their return error unions. We have two options: + +- **(a)** Widen all chain return types to `TaskResult` where `InferCancelled = ReturnType`. Most correct. Slight ergonomic cost — every TaskResult error union now includes `System.Cancelled` whether you use signals or not. +- **(b)** Only add `System.Cancelled` to the union when `.run({ signal })` is called with a non-undefined signal. Requires a type-level conditional — harder to implement cleanly. + +**Decision: (a).** It's honest. If a user runs a TaskResult without a signal, `System.Cancelled` simply never materializes at runtime; the type is correct in saying it *could* if a signal were passed. + +#### 3.2 API pruning + +- **Remove** `toPromise()` — it's a pure alias for `run()`. Anyone using it migrates by find-replace. +- **Keep** `tap` and `tapError` — they're the idiomatic side-effect insertion points (logging, metrics) and don't have a clean `Promise` equivalent. +- **Keep** `match()` — the convenience of `await task.match({ ok, 'User.NotFound': ... })` over `await task.run().then(r => r.match(...))` is meaningful at the call site. +- **Audit** the static constructors: `from`, `fromResult`, `fromPromise`, `ok`, `err`. All five are reasonable; `fromPromise` is the one to double-check — its current implementation just wraps `factory()`, but if `factory()` rejects, the rejection isn't caught. That's a real bug. Fix: catch and route to `SystemErrors.Unexpected` (matching `attemptAsync`'s mapUnknown semantics) or document explicitly that `fromPromise` factories must not throw. + +**Decision on `fromPromise`:** catch the rejection and route to `SystemErrors.Unexpected`. Document that behavior. Anyone wanting custom mapping should use `attemptAsync`. + +#### 3.3 Documentation + +New subsection in README under "Async Pipelines": **"When to use TaskResult vs `Promise`"**. + +> Reach for `TaskResult` when you need one of: +> - **Cancellation through a pipeline.** Abort one signal, every step short-circuits. +> - **Context frames across async steps.** Logging/observability annotations that survive composition. +> - **Lazy composition.** Build the pipeline, hand it off, run later. +> +> Otherwise, `Promise` is fine. It composes with `await` and plain `if (isErr)` checks just like the synchronous Stage 3 examples. + +#### 3.4 Tests + +- Cancellation propagation test: long chain (`.map.map.map.andThen.map`), abort the signal between step 2 and step 3, assert the result is `System.Cancelled` and steps 4+ never executed. +- `fromPromise` rejection test: factory that throws → assert result is `System.Unexpected` containing the original throw as `cause`. +- Type-level assertions that chain methods return `TaskResult` (using existing repo's type-test mechanism). + +### Files touched + +- `packages/faultline/src/task-result.ts` — cancellation propagation, drop `toPromise`, fix `fromPromise` rejection handling. +- `packages/faultline/src/index.ts` — remove `toPromise` from re-exports if present. +- `README.md` — new "When to use TaskResult vs `Promise`" subsection. +- `packages/faultline/test/task-result.test.ts` (or equivalent) — cancellation tests, fromPromise rejection test, type-level assertions. +- `CHANGELOG.md` — breaking change note (`toPromise` removal). + +### Acceptance criteria + +- Aborting a signal mid-pipeline short-circuits within one step. +- `fromPromise` rejection produces `System.Unexpected`, not an uncaught promise rejection. +- `toPromise` is removed. +- README documents the TaskResult vs Promise decision. +- Tests cover all three behaviors above. + +### Risks + +- Type-union widening to include `System.Cancelled` is mildly breaking for existing TaskResult consumers (their match statements may need a `default` or an explicit `System.Cancelled` handler). This is the right kind of breaking change — it forces callers to acknowledge a real outcome — but it must be in the changelog. + +--- + +## 4. ESLint plugin robustness via fixture suite + +### Problem + +The Stage 2 ESLint promise ("the linter ensures your catch blocks are complete") is only as good as the rules. From a code read I already see specific weak spots, but I don't know which patterns matter most in real codebases. We need a comprehensive fixture suite that exercises real-world patterns, then a triage of fix / document-as-limitation / out-of-scope. + +### Approach + +#### 4.1 Build the fixture corpus + +Create `packages/eslint-plugin-faultline/test/fixtures/torture/` with one subdirectory per pattern category. Each subdirectory contains: + +- `input.ts` — the code under test +- `expected.json` — declared expectations: `{ rule: 'no-raw-throw', expectedReports: [{ line: 12, messageId: 'rawThrow' }] }` and `{ shouldNotReport: [...] }` cases +- Optional `notes.md` — what's being tested and why + +**Categories:** + +| Dir | Patterns | +|---|---| +| `re-throws/` | `catch (e) { throw e }`; wrapping re-throw; conditional re-throw; re-throw inside `if/else` | +| `generics/` | `(fn: () => T) => T`; conditional return types; mapped types; generic helpers from libraries | +| `third-party/` | `JSON.parse`, `fetch`, library functions with no faultline annotation; node `fs` throws | +| `nested-try/` | try-in-try; try-in-catch; try inside a function literal inside try (relevant to `walkNode` scope) | +| `await-shapes/` | `await ternary`; `await x.method()` where x is dynamic; `await Promise.all([...])`; `await await x` | +| `overloads/` | Functions with multiple call signatures (current code picks `callSigs[0]`) | +| `re-exports/` | `narrowError` imported under aliases; error groups re-exported through barrels | +| `class-methods/` | `throw` in methods, getters, decorators, constructors | +| `boundaries/` | Throws inside `defineBoundary` map handlers (special — these are *intended* boundary mappings, not raw throws) | +| `baselines/` | Patterns that should NOT report — catch-and-handle, sync helpers without errors, plain promise chains | + +#### 4.2 Test runner + +A small harness that walks the corpus, runs each rule against `input.ts`, and asserts produced reports match `expected.json`. Use the existing rule-tester pattern in `@typescript-eslint/rule-tester` where it fits; build a thin wrapper for the structured-expectation format if needed. + +The harness must produce **three** outputs per run: + +- ✅ Passes (expected report observed) +- ❌ Failures: rule did/did not report when it should have +- ⚠️ Surprises: rule reported something the fixture didn't predict (potential false positive) + +#### 4.3 Triage the results + +Each ⚠️ or ❌ is triaged into: + +- **Fix.** A rule bug to patch. Candidates I've already identified from reading the source: + - `throw-type-mismatch` picks `callSigs[0]` on overloaded functions — should consider all signatures. + - `throw-type-mismatch` `undeclaredThrow` only fires for `NewExpression | Literal` — `throw someVariable` is silently ignored even when the variable's type is clearly mismatched. + - `throw-type-mismatch` transitive check only handles `AwaitExpression` whose argument is a direct `CallExpression` — ternaries, `Promise.all`, chained calls fall through. + - `uncovered-catch` `walkNode` descends into nested `FunctionExpression`/`ArrowFunctionExpression` inside the try block — calls inside a closure passed to `setTimeout` shouldn't count as throws from the try. + - `uncovered-catch` resolves `narrowError` covered tags via type lookup — re-exports through barrels or renamed imports may fail if the type doesn't preserve the `[ErrorOutput]` brand. (Verify with fixture.) +- **Document as limitation.** Patterns the rules legitimately can't handle (e.g., `throw fn()` where `fn` returns a dynamically computed error type). Publish a "Known limitations" doc page so adopters set expectations. +- **Out of scope.** Reflection, eval, dynamic imports — patterns we explicitly won't support. + +#### 4.4 Publish "Known limitations" + +A new doc page (`packages/eslint-plugin-faultline/LIMITATIONS.md` or website page) listing each documented limitation, with a minimal repro and the workaround. Linked from each rule's docs page and from the README's ESLint section. + +### Files touched + +- `packages/eslint-plugin-faultline/test/fixtures/torture/**` — new corpus. +- `packages/eslint-plugin-faultline/test/torture.test.ts` — new harness (or extend existing test setup). +- `packages/eslint-plugin-faultline/src/rules/*.ts` — fixes per triage results. +- `packages/eslint-plugin-faultline/src/utils/type-analysis.ts` — if root causes live here. +- `packages/eslint-plugin-faultline/LIMITATIONS.md` — new doc. +- `README.md` — link to limitations from the ESLint section. + +### Acceptance criteria + +- Torture corpus exists with ≥ 10 category subdirectories. +- Harness runs all fixtures in CI and fails on unexpected reports. +- Triage produces a written list: fixes applied, limitations documented, out-of-scope patterns called out. +- The four bugs I already identified are either fixed or explicitly documented as limitations with rationale. +- README's ESLint section links to `LIMITATIONS.md`. + +### Risks + +- **Triage volume.** A real torture corpus will likely surface more issues than initially expected. The triage discipline (fix vs document vs out-of-scope) is the throttle. If volume exceeds a reasonable session budget, prioritize: (1) false positives over false negatives (false positives drive users away from the plugin entirely), (2) common patterns over exotic ones. +- **Type-aware ESLint performance.** Some torture patterns (deeply generic helpers) can be slow under type-aware rules. If a fixture causes the test suite to take > 30s, mark it as a separate "slow" test and note the perf concern as a limitation. + +--- + +## Sequencing + +Tackle in order: **1 → 2 → 3 → 4.** + +- **1 + 2** are doc/test changes only. Land them together as a single PR — they make the README better immediately and unblock no other work. +- **3** is a focused code change with a clear acceptance contract. Land as its own PR with the changelog entry for the `toPromise` removal and type-union widening. +- **4** is the sustained effort. Land in phases: (a) corpus + harness scaffold, (b) first triage batch + first round of fixes, (c) limitations doc, (d) further triage passes as needed. + +Do not block 1/2/3 on 4. + +## Out of scope + +- New language features in the core library. +- Alternative Result-type ergonomics (`?=`-style tuples, etc. — already addressed in README, no further changes). +- Performance optimization beyond what the cancellation propagation change requires. +- VS Code extension changes (separate package, separate concerns). +- CLI changes. From 2e87b971048f59ffaa660857525cf99aa3ca46f9 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 14:58:20 +0100 Subject: [PATCH 02/42] docs: implementation plan for production-grade concerns 30 bite-sized tasks across four phases: - Phase 1 (T1-2): README tagline + positioning - Phase 2 (T3-5): explicit error-code escape hatch docs + tests - Phase 3 (T6-12): TaskResult cancellation, pruning, fromPromise fix, docs, changelog - Phase 4 (T13-28): ESLint torture corpus, triage, three rule fixes, LIMITATIONS.md, changelog - Wrap-up (T29-30): green-build verification + PR --- .../2026-05-22-production-grade-concerns.md | 2235 +++++++++++++++++ 1 file changed, 2235 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-22-production-grade-concerns.md diff --git a/docs/superpowers/plans/2026-05-22-production-grade-concerns.md b/docs/superpowers/plans/2026-05-22-production-grade-concerns.md new file mode 100644 index 0000000..ed7c131 --- /dev/null +++ b/docs/superpowers/plans/2026-05-22-production-grade-concerns.md @@ -0,0 +1,2235 @@ +# Production-Grade Concerns Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Address four production-grade concerns: README positioning, explicit-code escape hatch docs, TaskResult cancellation/pruning, and ESLint plugin robustness via fixture corpus. + +**Architecture:** Four sequenced phases on a single feature branch (`production-grade-concerns`). Phases 1–3 are bounded code/doc changes. Phase 4 establishes a torture-fixture test harness, lands the first round of rule fixes, and ships a `LIMITATIONS.md`. Further ESLint fixes beyond the first triage round are out of scope for this plan and become a follow-up. + +**Tech Stack:** TypeScript, Bun test runner (`bun:test`), tsdown for builds, `@typescript-eslint/utils` + `@typescript-eslint/rule-tester` for ESLint rules, oxlint + eslint for repo linting. + +**Branch:** `production-grade-concerns` (already created, spec already committed). + +**Spec:** `docs/superpowers/specs/2026-05-22-production-grade-concerns-design.md` + +--- + +## File Structure + +### Files created + +- `packages/eslint-plugin-faultline/test/torture/README.md` — explains corpus +- `packages/eslint-plugin-faultline/test/torture/{category}/input.ts` — one per category (10 categories) +- `packages/eslint-plugin-faultline/test/torture/{category}/expected.ts` — paired expectations +- `packages/eslint-plugin-faultline/test/torture/run-torture.test.ts` — harness +- `packages/eslint-plugin-faultline/test/torture/tsconfig.json` — TypeScript config for fixtures +- `packages/eslint-plugin-faultline/LIMITATIONS.md` — known limitations doc + +### Files modified + +- `README.md` — tagline + "How faultline differs" + explicit-code example + TaskResult-vs-Promise section + LIMITATIONS link +- `packages/faultline/package.json` — `description` field (matches new tagline) +- `packages/faultline/src/task-result.ts` — cancellation propagation, drop `toPromise`, fix `fromPromise` rejection +- `packages/faultline/src/index.ts` — verify `toPromise` not re-exported (it isn't — confirm only) +- `packages/faultline/test/task-result.test.ts` — cancellation tests, fromPromise rejection test +- `packages/faultline/test/typecheck.ts` — explicit-code type assertion + Cancelled-in-union assertion +- `CHANGELOG.md` — entries for TaskResult breaking changes + new ESLint test infrastructure +- `packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts` — fixes per triage +- `packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts` — fixes per triage +- `packages/eslint-plugin-faultline/src/utils/type-analysis.ts` — if root causes live here + +--- + +## Phase 1: README positioning + tagline (Workstream 1) + +### Task 1: Update tagline at top of README + +**Files:** +- Modify: `README.md:1-3` + +- [ ] **Step 1: Replace tagline line** + +In `README.md`, the current line is: +```markdown +# faultline + +The complete type-safe error system for TypeScript. +``` + +Replace with: +```markdown +# faultline + +The incremental type-safe error system for TypeScript. +``` + +- [ ] **Step 2: Update package.json description to match** + +In `packages/faultline/package.json`, find the line: +```json +"description": "The complete type-safe error system for TypeScript.", +``` + +Replace with: +```json +"description": "The incremental type-safe error system for TypeScript.", +``` + +- [ ] **Step 3: Commit** + +```bash +git add README.md packages/faultline/package.json +git commit -m "docs: lead with 'incremental' in tagline" +``` + +--- + +### Task 2: Add "How faultline differs" section to README + +**Files:** +- Modify: `README.md` (insert new section between "## Why" and "## Install") + +- [ ] **Step 1: Identify insertion point** + +Open `README.md`. Find the heading `## Install`. The new section will be inserted *immediately above* it, after the existing `## Why` content ends. + +- [ ] **Step 2: Insert new section** + +Insert this block directly above `## Install`: + +```markdown +## How faultline differs + +**vs. neverthrow, ts-results, true-myth.** These are solid Result-type libraries. Faultline's difference is that it doesn't ask you to convert your throws first. Stage 1 — typed factories plus the ESLint plugin — delivers most of the value while your code keeps using `throw` and `catch`. Result types are there when you want them at Stage 3. + +**vs. Effect.** Effect is more powerful and more mature. Faultline is narrowly focused on errors: no effect system, no runtime, no scheduler. Just typed errors plus optional Results. The learning curve is hours, not weeks. + +**vs. typed `Error` subclasses.** Custom Error classes work. Faultline derives consistent tags, codes, status values, and serialization from a single declaration, ships an ESLint plugin that catches drift, and gives you a smooth path to Result types if you want them later. + +**vs. the `?=` proposal.** You may have seen the TC39 [Safe Assignment Operator (`?=`) proposal](https://github.com/nicolo-ribaudo/tc39-proposal-safe-assignment-operator) or libraries that return `[error, value]` tuples. Faultline uses discriminated unions instead because: (1) TypeScript narrows discriminated unions more reliably than tuple truthiness — after `if (isErr(result))`, the compiler *guarantees* the error type; (2) the `?=` proposal doesn't type the error — it's still `unknown`; (3) a single `result` value composes with `match()`, returns cleanly from functions, and chains methods. If you prefer tuple syntax, a one-line helper gets you there and your errors stay fully typed (see "Why not `[err, value]` tuples?" below). +``` + +- [ ] **Step 3: Remove the now-duplicate `?=` section** + +The existing `### Why not `[err, value]` tuples?` section (under Stage 3) is now partially duplicated. Keep the section header and the helper code example, but delete the three numbered bullet points that explain *why* — those are now in the "How faultline differs" section. The retained content should read: + +```markdown +### Why not `[err, value]` tuples? + +We chose discriminated unions over tuples (see "How faultline differs" above). If you prefer the tuple syntax, a one-line helper gets you there — and your errors stay fully typed: + +```ts +function tryResult( + result: Result, +): [E, undefined] | [undefined, T] { + return isErr(result) ? [result.error, undefined] : [undefined, result.value]; +} + +const [err, user] = tryResult(getUser(id)); +if (err) { + err.data.userId; // still typed + return; +} +user.name; // still typed +``` +``` + +- [ ] **Step 4: Verify markdown renders cleanly** + +Run: `bun --filter '*' lint` (if it lints markdown, otherwise visually inspect with a markdown previewer or `cat README.md | head -100`). + +Expected: no broken links, headings render in the right hierarchy. + +- [ ] **Step 5: Commit** + +```bash +git add README.md +git commit -m "docs: add 'How faultline differs' section" +``` + +--- + +## Phase 2: Explicit error-code escape hatch (Workstream 2) + +### Task 3: Add explicit-code example to README + +**Files:** +- Modify: `README.md` — in the "Stage 1: Define and throw" section + +- [ ] **Step 1: Find the existing `UserErrors` example** + +In `README.md`, find the block under "Stage 1: Define and throw" that defines `UserErrors` with `NotFound` and `InvalidEmail` and `Unauthorized`. Currently: + +```ts +const UserErrors = defineErrors('User', { + NotFound: { + status: 404, + message: (data: { userId: string }) => `User ${data.userId} not found`, + }, + InvalidEmail: { + status: 400, + message: (data: { email: string; reason: string }) => `Invalid email: ${data.email}`, + }, + Unauthorized: { status: 401 }, +}); +``` + +- [ ] **Step 2: Add a paragraph and a second small example after the auto-generated tags/codes note** + +The existing follow-up paragraph reads: +```markdown +Tags and codes are auto-generated: +- `UserErrors.NotFound(...)._tag` → `'User.NotFound'` +- `UserErrors.NotFound(...).code` → `'USER_NOT_FOUND'` +``` + +Insert *after* that paragraph: + +```markdown +**Codes default to auto-generated `SCREAMING_SNAKE_CASE` from the tag.** If you have an existing error-code convention, pass an explicit `code` — it's preserved at the type level too, so downstream consumers see the literal string: + +```ts +const UserErrors = defineErrors('User', { + // Auto-generated: code === 'USER_NOT_FOUND' + NotFound: { + status: 404, + message: (data: { userId: string }) => `User ${data.userId} not found`, + }, + // Explicit: code === 'ERR_AUTH_401' (literal type preserved) + Unauthorized: { + code: 'ERR_AUTH_401', + status: 401, + }, +}); +``` +``` + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "docs: document explicit error-code escape hatch" +``` + +--- + +### Task 4: Add runtime test for explicit code + +**Files:** +- Modify: `packages/faultline/test/error-system.test.ts` (or create a new section) +- Test: `packages/faultline/test/error-system.test.ts` + +- [ ] **Step 1: Find a good insertion point** + +Open `packages/faultline/test/error-system.test.ts` and locate the existing `describe('defineErrors', ...)` block. The new tests will be added inside that block (or in a new `describe('defineErrors with explicit codes', ...)` block right after it). + +- [ ] **Step 2: Add the failing test** + +Add this `describe` block: + +```ts +describe('defineErrors with explicit codes', () => { + test('preserves explicit code at runtime', () => { + const errors = defineErrors('User', { + Unauthorized: { code: 'ERR_AUTH_401', status: 401 }, + }); + expect(errors.Unauthorized().code).toBe('ERR_AUTH_401'); + }); + + test('falls back to auto-generated code when not provided', () => { + const errors = defineErrors('User', { + NotFound: { status: 404 }, + }); + expect(errors.NotFound().code).toBe('USER_NOT_FOUND'); + }); + + test('mixing explicit and auto-generated codes in one group', () => { + const errors = defineErrors('User', { + NotFound: { status: 404 }, + Unauthorized: { code: 'ERR_AUTH_401', status: 401 }, + }); + expect(errors.NotFound().code).toBe('USER_NOT_FOUND'); + expect(errors.Unauthorized().code).toBe('ERR_AUTH_401'); + }); +}); +``` + +- [ ] **Step 3: Run the tests — they should pass already (existing runtime supports this)** + +Run: `cd packages/faultline && bun test test/error-system.test.ts` + +Expected: PASS (all three new tests). If any fail, that's a real bug — investigate before continuing. + +- [ ] **Step 4: Commit** + +```bash +git add packages/faultline/test/error-system.test.ts +git commit -m "test: cover explicit-code escape hatch in defineErrors" +``` + +--- + +### Task 5: Add type-level assertion for explicit code preservation + +**Files:** +- Modify: `packages/faultline/test/typecheck.ts` + +- [ ] **Step 1: Open typecheck.ts and locate where existing `Equal/Expect` assertions live** + +Open `packages/faultline/test/typecheck.ts`. Note the existing pattern: it uses `type Equal` and `type Expect` and asserts equality via `type _Foo = Expect>`. + +- [ ] **Step 2: Append type-level assertions for explicit code** + +At the bottom of `typecheck.ts`, add: + +```ts +// === Explicit code preservation === +const ExplicitCodeErrors = defineErrors('User', { + Unauthorized: { code: 'ERR_AUTH_401', status: 401 }, + NotFound: { status: 404 }, +}); + +// Explicit code is preserved as a literal type +type _ExplicitCodeLiteral = Expect< + Equal['code'], 'ERR_AUTH_401'> +>; + +// Auto-generated code is also preserved as a literal type +type _AutoCodeLiteral = Expect< + Equal['code'], 'USER_NOT_FOUND'> +>; +``` + +- [ ] **Step 3: Run typecheck** + +Run: `cd packages/faultline && bunx tsc --noEmit` + +Expected: no errors. The `_ExplicitCodeLiteral` and `_AutoCodeLiteral` assertions resolve to `true` because both codes are preserved as literal types. + +If you see a TS error like `'ERR_AUTH_401' does not satisfy true`, that's a real type-level bug in `defineErrors` — stop and investigate before continuing. + +- [ ] **Step 4: Commit** + +```bash +git add packages/faultline/test/typecheck.ts +git commit -m "test: assert explicit-code literal type preservation" +``` + +--- + +## Phase 3: TaskResult production-grade pass (Workstream 3) + +### Task 6: Add cancellation propagation test (failing) + +**Files:** +- Modify: `packages/faultline/test/task-result.test.ts` + +- [ ] **Step 1: Add a new describe block with the failing test** + +Append to `packages/faultline/test/task-result.test.ts`: + +```ts +describe('TaskResult cancellation propagation', () => { + test('aborting mid-chain short-circuits subsequent steps', async () => { + const stepsRun: number[] = []; + const controller = new AbortController(); + + const task = TaskResult.ok(1) + .map((v) => { stepsRun.push(1); return v + 1; }) + .map((v) => { stepsRun.push(2); controller.abort(); return v + 1; }) + .map((v) => { stepsRun.push(3); return v + 1; }) + .map((v) => { stepsRun.push(4); return v + 1; }); + + const result = await task.run({ signal: controller.signal }); + + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('System.Cancelled'); + } + // Steps 1 and 2 ran; 3 and 4 must not have + expect(stepsRun).toEqual([1, 2]); + }); + + test('no cancellation when signal never aborts', async () => { + const controller = new AbortController(); + const task = TaskResult.ok(1).map((v) => v + 1).map((v) => v + 1); + const result = await task.run({ signal: controller.signal }); + + expect(isOk(result)).toBe(true); + if (isOk(result)) { + expect(result.value).toBe(3); + } + }); + + test('pre-aborted signal short-circuits before first step', async () => { + const controller = new AbortController(); + controller.abort(); + let ran = false; + const task = TaskResult.ok(1).map(() => { ran = true; return 2; }); + const result = await task.run({ signal: controller.signal }); + + expect(isErr(result)).toBe(true); + expect(ran).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run the new tests to confirm they fail** + +Run: `cd packages/faultline && bun test test/task-result.test.ts -t "cancellation propagation"` + +Expected: FAIL. The current implementation does not check `context.signal.aborted` between chain steps, so all four `map` callbacks execute and the result is `ok(5)`. The assertions on `_tag === 'System.Cancelled'` and `stepsRun.toEqual([1, 2])` will fail. + +- [ ] **Step 3: Commit the failing test** + +```bash +git add packages/faultline/test/task-result.test.ts +git commit -m "test: failing cancellation propagation tests for TaskResult" +``` + +--- + +### Task 7: Implement cancellation propagation in chain methods + +**Files:** +- Modify: `packages/faultline/src/task-result.ts` + +- [ ] **Step 1: Add a cancellation helper at the top of the file** + +Open `packages/faultline/src/task-result.ts`. After the existing imports, add: + +```ts +import { SystemErrors } from './system-errors'; + +function checkCancelled(context: TaskContext): AppError | undefined { + if (context.signal?.aborted) { + const reason = context.signal.reason; + return SystemErrors.Cancelled({ + reason: typeof reason === 'string' ? reason : reason instanceof Error ? reason.message : 'aborted', + }); + } + return undefined; +} +``` + +Before continuing, **verify the shape of `SystemErrors.Cancelled`** by reading `packages/faultline/src/system-errors.ts`. If `Cancelled` does not accept a `{ reason: string }` payload, adjust this helper to match the actual signature. If `SystemErrors.Cancelled` is zero-arg, simply call `SystemErrors.Cancelled()` and drop the reason logic. + +- [ ] **Step 2: Wrap every chain method with cancellation checks** + +For each of these methods in `task-result.ts` — `map`, `mapErr`, `andThen`, `andThenTask`, `catchTag`, `tap`, `tapError`, `withContext` — modify the inner async function so it checks for cancellation **before** running the previous executor and **after** receiving its result, before running the user callback. + +Concrete pattern. For `map`: + +```ts +map(fn: (value: T) => U | Promise): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; + + return isOk(result) + ? ok(await fn(result.value)) + : (result as unknown as Result); + }); +} +``` + +Apply the same `preCancel` / `postCancel` pattern to: `mapErr`, `andThen`, `andThenTask`, `catchTag`, `tap`, `tapError`, `withContext`. + +Add the type alias near the top of the file: +```ts +type InferCancelled = ReturnType; +``` + +Update each method's return-type signature to widen the error union to `E | InferCancelled` (and the appropriate `E2` widening in `andThen`/`andThenTask`/`catchTag`). + +- [ ] **Step 3: Run the cancellation tests — expect PASS** + +Run: `cd packages/faultline && bun test test/task-result.test.ts -t "cancellation propagation"` + +Expected: PASS, all three tests. + +- [ ] **Step 4: Run the full task-result test file** + +Run: `cd packages/faultline && bun test test/task-result.test.ts` + +Expected: every existing test still passes. If any fail, the type widening or the cancel-check is breaking existing behavior — investigate. + +- [ ] **Step 5: Commit** + +```bash +git add packages/faultline/src/task-result.ts +git commit -m "feat(task-result): propagate cancellation through chain steps" +``` + +--- + +### Task 8: Update typecheck for widened error union + +**Files:** +- Modify: `packages/faultline/test/typecheck.ts` + +- [ ] **Step 1: Add type-level assertions for cancellation widening** + +Append to `packages/faultline/test/typecheck.ts`: + +```ts +// === Cancellation widening === +type CancelledTag = ReturnType['_tag']; +type _CancelledTagIsCorrect = Expect>; + +// After .map, the error union must include System.Cancelled +const _baseTask: TaskResult = TaskResult.ok(1); +const _mappedTask = _baseTask.map((v) => v + 1); +type MappedError = Awaited> extends Result ? E : never; + +// MappedError should be a union containing System.Cancelled (the AppError instance) +type _IncludesCancelled = Expect< + Equal +>; +``` + +- [ ] **Step 2: Run typecheck** + +Run: `cd packages/faultline && bunx tsc --noEmit` + +Expected: no errors. + +If the `_IncludesCancelled` assertion fails, the widening in Task 7 didn't take — return to Task 7 and verify the method signatures actually return `TaskResult`. + +- [ ] **Step 3: Run the entire faultline test suite** + +Run: `cd packages/faultline && bun test` + +Expected: all tests pass. + +- [ ] **Step 4: Run the repo-wide typecheck** + +Run: `bun --filter '*' typecheck` + +Expected: no type errors anywhere — including in `examples/` and `test/fixtures/`. If consumers of TaskResult in fixtures break because their match statements don't cover `System.Cancelled`, that's the *expected* breaking change. Fix the fixtures to acknowledge cancellation (or add a default branch) and note it in the CHANGELOG entry later. + +- [ ] **Step 5: Fix any broken fixtures/examples** + +If `bun --filter '*' typecheck` surfaced errors in `examples/` or `test/fixtures/`, fix them. The fix is usually adding `'System.Cancelled': (e) => ...` to a match block or adding a `default` branch. + +For each fix, the smallest change is: +```ts +match(result, { + ok: (value) => ..., + 'User.NotFound': (e) => ..., + 'System.Cancelled': (e) => ..., // ADD THIS +}); +``` + +After fixes, re-run `bun --filter '*' typecheck` to confirm clean. + +- [ ] **Step 6: Commit** + +```bash +git add packages/faultline/test/typecheck.ts examples test/fixtures +git commit -m "test: assert TaskResult chain widens error union with System.Cancelled" +``` + +--- + +### Task 9: Remove `toPromise()` alias + +**Files:** +- Modify: `packages/faultline/src/task-result.ts:190-192` + +- [ ] **Step 1: Check for callers** + +Run: `grep -rn "toPromise" /Users/danielfry/dev/faultline/packages /Users/danielfry/dev/faultline/examples /Users/danielfry/dev/faultline/test /Users/danielfry/dev/faultline/playground 2>/dev/null | grep -v node_modules` + +Note every caller (other than the definition in `task-result.ts`). Each one needs to be updated to use `run()`. + +- [ ] **Step 2: Update each caller** + +For each caller found in Step 1, replace `.toPromise(` with `.run(` (preserving any arguments). + +- [ ] **Step 3: Remove the method from `task-result.ts`** + +Delete these lines from `packages/faultline/src/task-result.ts`: + +```ts +toPromise(options: TaskRunOptions = {}): Promise> { + return this.run(options); +} +``` + +- [ ] **Step 4: Run the tests** + +Run: `cd packages/faultline && bun test` + +Expected: all tests pass. + +- [ ] **Step 5: Run the repo-wide typecheck** + +Run: `bun --filter '*' typecheck` + +Expected: no errors. If anything still references `toPromise`, you missed a caller in Step 2 — return to Step 1. + +- [ ] **Step 6: Commit** + +```bash +git add packages/faultline +git commit -m "refactor(task-result): remove toPromise alias (use run) + +BREAKING CHANGE: TaskResult.toPromise() has been removed. Call .run() instead — it has the same signature and behavior." +``` + +--- + +### Task 10: Fix `fromPromise` unhandled rejection + +**Files:** +- Modify: `packages/faultline/src/task-result.ts` (the `fromPromise` static method) +- Test: `packages/faultline/test/task-result.test.ts` + +- [ ] **Step 1: Add the failing test** + +Append to `packages/faultline/test/task-result.test.ts`: + +```ts +describe('TaskResult.fromPromise rejection handling', () => { + test('rejected factory becomes System.Unexpected with cause preserved', async () => { + const original = new Error('boom'); + const task = TaskResult.fromPromise(async () => { + throw original; + }); + const result = await task.run(); + + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('System.Unexpected'); + expect(result.error.cause).toBe(original); + } + }); + + test('successful factory passes through unchanged', async () => { + const task = TaskResult.fromPromise(async () => ok(42)); + const result = await task.run(); + + expect(isOk(result)).toBe(true); + if (isOk(result)) { + expect(result.value).toBe(42); + } + }); +}); +``` + +- [ ] **Step 2: Run the failing tests** + +Run: `cd packages/faultline && bun test test/task-result.test.ts -t "fromPromise rejection"` + +Expected: the first test FAILS (rejection bubbles up as an uncaught promise rejection / test failure). The second PASSES. + +- [ ] **Step 3: Read `SystemErrors.Unexpected` shape** + +Open `packages/faultline/src/system-errors.ts` and confirm `SystemErrors.Unexpected` accepts a cause / takes `{ cause: unknown }` or similar. If not, look at how `attempt()` (sync) wraps unknown throws and copy that approach. + +- [ ] **Step 4: Update `fromPromise`** + +In `packages/faultline/src/task-result.ts`, find: +```ts +static fromPromise( + factory: () => Promise>, +): TaskResult { + return new TaskResult(async () => factory()); +} +``` + +Replace with: +```ts +static fromPromise( + factory: () => Promise>, +): TaskResult { + return new TaskResult(async () => { + try { + return await factory(); + } catch (thrown) { + return err(SystemErrors.Unexpected({ cause: thrown })) as Result; + } + }); +} +``` + +And add the type alias near the top: +```ts +type InferUnexpected = ReturnType; +``` + +Adjust the `Unexpected` payload shape to match the actual signature you saw in Step 3. + +- [ ] **Step 5: Run the tests** + +Run: `cd packages/faultline && bun test test/task-result.test.ts -t "fromPromise rejection"` + +Expected: both tests PASS. + +- [ ] **Step 6: Run the full faultline test suite + typecheck** + +Run: `cd packages/faultline && bun test && bunx tsc --noEmit && cd ../.. && bun --filter '*' typecheck` + +Expected: all green. If `examples/` or `test/fixtures/` use `fromPromise` and now need to handle `System.Unexpected`, fix those fixtures the same way as Task 8 Step 5. + +- [ ] **Step 7: Commit** + +```bash +git add packages/faultline +git commit -m "fix(task-result): fromPromise catches rejections as System.Unexpected + +Previously, a rejection from the factory bubbled up as an uncaught promise rejection. +Now it's wrapped as System.Unexpected with the original throw preserved as .cause. + +BREAKING CHANGE: fromPromise return type now widens to include System.Unexpected." +``` + +--- + +### Task 11: Document "When to use TaskResult vs Promise" + +**Files:** +- Modify: `README.md` — under "Async Pipelines" + +- [ ] **Step 1: Find the existing "Async Pipelines" subsection** + +In `README.md`, locate `### Async Pipelines` under "## Going Deeper". + +- [ ] **Step 2: Add a new subsection immediately after `### Async Pipelines`** + +Insert this new heading and content after the existing Async Pipelines content (which ends after the `attempt` sync example), and before the next heading (`### Error Boundaries`): + +```markdown +### When to use TaskResult vs `Promise` + +Reach for `TaskResult` when you need one of: + +- **Cancellation through a pipeline.** Pass an `AbortSignal` to `.run({ signal })` and every chained step checks it. Abort once, and the rest of the chain short-circuits to `System.Cancelled` — no partial work continues. +- **Context frames across async steps.** Use `.withContext({ ... })` to attach observability metadata that survives composition. +- **Lazy composition.** Build the pipeline, hand it off, run it later. Plain Promises run eagerly the moment you construct them. + +Otherwise, `Promise` is fine. It composes with `await` and plain `if (isErr(result))` checks just like the synchronous Stage 3 examples. + +```ts +// Promise — perfectly good for most async work +async function getUser(id: string): Promise> { + const user = await db.users.find(id); + if (!user) return err(UserErrors.NotFound({ userId: id })); + return ok(user); +} + +// TaskResult — when you need cancellation or context propagation +const task = attemptAsync( + async (signal) => fetch(`/api/users/${id}`, { signal }), + { mapUnknown: () => UserErrors.NotFound({ userId: id }) }, +).withContext({ layer: 'service', operation: 'getUser' }); + +const result = await task.run({ signal: controller.signal }); +``` +``` + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "docs: explain when to use TaskResult vs Promise" +``` + +--- + +### Task 12: Update CHANGELOG for Phase 3 breaking changes + +**Files:** +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: Inspect existing CHANGELOG style** + +Read `CHANGELOG.md` to see the format used (Keep a Changelog? Conventional?). Match it. + +- [ ] **Step 2: Add a new unreleased section** + +At the top of `CHANGELOG.md` (under any "# Changelog" heading, above the latest version section), add an unreleased entry. Style approximate — match what's already there: + +```markdown +## [Unreleased] + +### Breaking changes + +- `TaskResult.toPromise()` removed. Use `.run()` instead — same signature. +- TaskResult chain methods (`.map`, `.andThen`, `.catchTag`, etc.) now widen their error union to include `System.Cancelled`. If you exhaustively `match()` against a TaskResult, add a `'System.Cancelled'` handler or a `default` branch. +- `TaskResult.fromPromise` return type now widens to include `System.Unexpected`. Factory rejections are no longer unhandled — they're caught and wrapped as `System.Unexpected` with the original throw preserved as `.cause`. + +### Added + +- TaskResult chain steps check `context.signal?.aborted` between every step. Aborting the signal mid-pipeline short-circuits the remaining steps to `System.Cancelled` instead of running them to completion. +- README: "How faultline differs" section comparing faultline to neverthrow, Effect, and typed Error subclasses. +- README: "When to use TaskResult vs Promise" decision guide. +- README: explicit error-code escape hatch is now documented (the feature already existed; this fills a docs gap). + +### Tests + +- Type-level assertion that explicit `code` values on `defineErrors` definitions are preserved as literal types. +- Cancellation propagation tests for TaskResult chains (pre-abort, mid-chain abort, no abort). +- `TaskResult.fromPromise` rejection-handling tests. +``` + +- [ ] **Step 3: Commit** + +```bash +git add CHANGELOG.md +git commit -m "docs: changelog entries for Phase 3 task-result changes" +``` + +--- + +## Phase 4: ESLint plugin robustness (Workstream 4) + +### Task 13: Create torture fixture directory structure + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/README.md` +- Create: `packages/eslint-plugin-faultline/test/torture/tsconfig.json` + +- [ ] **Step 1: Create the directory** + +```bash +mkdir -p /Users/danielfry/dev/faultline/packages/eslint-plugin-faultline/test/torture +``` + +- [ ] **Step 2: Write the README** + +Create `packages/eslint-plugin-faultline/test/torture/README.md`: + +```markdown +# Torture Fixtures + +Real-world ESLint rule stress tests. Each subdirectory exercises one pattern category against `faultline/no-raw-throw`, `faultline/throw-type-mismatch`, and `faultline/uncovered-catch`. + +## Layout + +Each category directory contains: + +- `input.ts` — the code under test +- `expected.ts` — exported expectations array, typed as `RuleExpectation[]` + +## Expectation shape + +```ts +export const expected: RuleExpectation[] = [ + { rule: 'throw-type-mismatch', line: 12, messageId: 'mismatch' }, + // ...or... + { rule: 'uncovered-catch', shouldNotReport: true, line: 20 }, +]; +``` + +The harness in `run-torture.test.ts` walks the directory, runs ESLint against each `input.ts` with the faultline plugin enabled, and asserts the produced reports match `expected.ts`. + +Surprises (rule reports a violation not in `expected.ts`) and misses (rule fails to report an expected violation) both fail the test. + +## Triage + +Findings from this corpus are triaged into: + +- **Fix** — bug in the rule, patched in this PR or a follow-up. +- **Limitation** — documented in `../../LIMITATIONS.md`. Fixture's `expected.ts` is annotated with a comment explaining why. +- **Out of scope** — fixture removed; rationale recorded in commit message. +``` + +- [ ] **Step 3: Write the tsconfig** + +Create `packages/eslint-plugin-faultline/test/torture/tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "types": ["bun-types"], + "paths": { + "faultline": ["../../../faultline/src/index.ts"] + } + }, + "include": ["./**/*.ts"] +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add packages/eslint-plugin-faultline/test/torture +git commit -m "test(eslint-plugin): scaffold torture fixture directory" +``` + +--- + +### Task 14: Write the torture harness skeleton + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/run-torture.test.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/types.ts` + +- [ ] **Step 1: Define the expectation type** + +Create `packages/eslint-plugin-faultline/test/torture/types.ts`: + +```ts +export type RuleName = 'no-raw-throw' | 'throw-type-mismatch' | 'uncovered-catch'; + +export interface RuleExpectation { + rule: RuleName; + line: number; + /** ESLint messageId expected. Mutually exclusive with shouldNotReport. */ + messageId?: string; + /** When true, asserts that no report is generated at `line` for `rule`. */ + shouldNotReport?: boolean; + /** Optional: human-readable note for triage. */ + note?: string; +} +``` + +- [ ] **Step 2: Write the harness** + +Create `packages/eslint-plugin-faultline/test/torture/run-torture.test.ts`: + +```ts +import { describe, expect, test } from 'bun:test'; +import { readdirSync, statSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { Linter } from 'eslint'; +import tsParser from '@typescript-eslint/parser'; +import faultline from '../../src/index'; +import type { RuleExpectation } from './types'; + +const TORTURE_ROOT = resolve(__dirname); + +function listCategories(): string[] { + return readdirSync(TORTURE_ROOT).filter((entry) => { + const full = join(TORTURE_ROOT, entry); + return statSync(full).isDirectory(); + }); +} + +async function loadExpectations(category: string): Promise { + const mod = await import(join(TORTURE_ROOT, category, 'expected.ts')); + return mod.expected as RuleExpectation[]; +} + +function lintFile(filePath: string) { + const linter = new Linter({ configType: 'flat' }); + return linter.verify( + require('node:fs').readFileSync(filePath, 'utf8'), + [ + { + files: ['**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: join(TORTURE_ROOT, 'tsconfig.json'), + tsconfigRootDir: TORTURE_ROOT, + }, + }, + plugins: { faultline }, + rules: { + 'faultline/no-raw-throw': ['warn', { allowAppErrors: true }], + 'faultline/throw-type-mismatch': 'error', + 'faultline/uncovered-catch': 'error', + }, + }, + ], + filePath, + ); +} + +describe('torture fixtures', () => { + const categories = listCategories(); + + for (const category of categories) { + test(category, async () => { + const inputPath = join(TORTURE_ROOT, category, 'input.ts'); + const expectations = await loadExpectations(category); + + const messages = lintFile(inputPath); + + // Group messages by (rule, line) for easier matching + const seen = new Set(); + for (const m of messages) { + const ruleId = m.ruleId?.replace(/^faultline\//, '') ?? ''; + seen.add(`${ruleId}:${m.line}:${m.messageId ?? ''}`); + } + + const missing: RuleExpectation[] = []; + const unexpected: typeof messages = []; + + for (const exp of expectations) { + if (exp.shouldNotReport) { + const key = `${exp.rule}:${exp.line}:`; + const matched = [...seen].some((s) => s.startsWith(key)); + if (matched) { + missing.push({ ...exp, note: `expected no report, but got one${exp.note ? `: ${exp.note}` : ''}` }); + } + } else { + const key = `${exp.rule}:${exp.line}:${exp.messageId ?? ''}`; + if (!seen.has(key)) { + missing.push(exp); + } + } + } + + // Find unexpected reports + for (const m of messages) { + const ruleId = m.ruleId?.replace(/^faultline\//, '') ?? ''; + const expected = expectations.some( + (e) => !e.shouldNotReport && e.rule === ruleId && e.line === m.line && e.messageId === m.messageId, + ); + if (!expected) unexpected.push(m); + } + + if (missing.length > 0 || unexpected.length > 0) { + const lines: string[] = [`Fixture: ${category}`]; + if (missing.length > 0) { + lines.push('MISSING expected reports:'); + for (const m of missing) lines.push(` ${JSON.stringify(m)}`); + } + if (unexpected.length > 0) { + lines.push('UNEXPECTED reports:'); + for (const m of unexpected) { + lines.push(` ${m.ruleId} @ line ${m.line} messageId=${m.messageId} — ${m.message}`); + } + } + throw new Error(lines.join('\n')); + } + + expect(true).toBe(true); // marker + }); + } +}); +``` + +- [ ] **Step 3: Verify the harness loads (no fixtures yet, so the test list is empty)** + +Run: `cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts` + +Expected: the test file loads without error. Output should show 0 tests run (no fixture directories yet). If the file fails to load, debug the import path or tsconfig setup before continuing. + +- [ ] **Step 4: Commit** + +```bash +git add packages/eslint-plugin-faultline/test/torture +git commit -m "test(eslint-plugin): scaffold torture harness" +``` + +--- + +### Task 15: Add `baselines/` fixture (negative controls) + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/baselines/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/baselines/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/baselines/input.ts`: + +```ts +import { defineErrors, isErrorTag, narrowError } from 'faultline'; + +const UserErrors = defineErrors('User', { + NotFound: { status: 404, message: (data: { userId: string }) => `not found ${data.userId}` }, +}); + +function getUser(id: string) { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; +} + +// 1. Properly handled with isErrorTag — should NOT report +function handled1(id: string) { + try { + return getUser(id); + } catch (e) { + if (isErrorTag(e, UserErrors.NotFound)) { + return { id: 'guest' }; + } + throw e; + } +} + +// 2. Properly handled with narrowError — should NOT report +function handled2(id: string) { + try { + return getUser(id); + } catch (e) { + const err = narrowError(e, [UserErrors]); + if (err._tag === 'User.NotFound') return { id: 'guest' }; + throw err; + } +} + +// 3. Pure sync helper, no throws — should NOT report any rule +function pure(a: number, b: number) { + return a + b; +} + +// 4. Plain promise chain, no throws — should NOT report +async function fetchAndLog(url: string) { + const res = await fetch(url); + console.log(res.status); + return res; +} +``` + +- [ ] **Step 2: Write expectations (all negative)** + +Create `packages/eslint-plugin-faultline/test/torture/baselines/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 16, shouldNotReport: true, note: 'isErrorTag covers tag' }, + { rule: 'uncovered-catch', line: 27, shouldNotReport: true, note: 'narrowError covers tag' }, +]; +``` + +- [ ] **Step 3: Run the harness** + +Run: `cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts` + +Expected: `baselines` test PASSES. If it fails, the harness has a bug — investigate before adding more fixtures. + +- [ ] **Step 4: Commit** + +```bash +git add packages/eslint-plugin-faultline/test/torture/baselines +git commit -m "test(eslint-plugin): baseline fixture (no-report negative controls)" +``` + +--- + +### Task 16: Add `re-throws/` fixture + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/re-throws/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/re-throws/input.ts`: + +```ts +import { defineErrors } from 'faultline'; + +const UserErrors = defineErrors('User', { + NotFound: { status: 404, message: (data: { userId: string }) => `not found ${data.userId}` }, + Unauthorized: { status: 401 }, +}); + +function getUser(id: string) { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; +} + +// 1. Bare re-throw — catch is uncovered (no narrowing) +function bareRethrow(id: string) { + try { + return getUser(id); + } catch (e) { + throw e; + } +} + +// 2. Conditional re-throw — partial handling, still uncovered for NotFound +function conditionalRethrow(id: string) { + try { + return getUser(id); + } catch (e) { + if (Math.random() > 0.5) throw e; + return { id: 'fallback' }; + } +} + +// 3. Wrap-and-rethrow with a known typed factory — should NOT report mismatch +function wrappedRethrow(id: string): never { + try { + return getUser(id) as never; + } catch (e) { + throw UserErrors.Unauthorized(); + } +} +``` + +- [ ] **Step 2: Write expectations** + +Create `packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 17, messageId: 'uncheckedCatch', note: 'bare rethrow does not constitute typed handling' }, + { rule: 'uncovered-catch', line: 26, messageId: 'uncheckedCatch', note: 'conditional rethrow without narrowing' }, + { rule: 'uncovered-catch', line: 37, shouldNotReport: true, note: 'throwing a typed factory is fine' }, +]; +``` + +- [ ] **Step 3: Run the fixture** + +Run: `cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "re-throws"` + +Expected: test runs. Record the actual output — pass or fail. If it fails, this is a finding for triage. **Do not fix yet** — continue adding fixtures first. Note the finding in the commit message. + +- [ ] **Step 4: Commit (regardless of pass/fail)** + +```bash +git add packages/eslint-plugin-faultline/test/torture/re-throws +git commit -m "test(eslint-plugin): re-throws torture fixture + +Records expected behavior. Triage results follow in Task 23." +``` + +--- + +### Task 17: Add `generics/` fixture + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/generics/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/generics/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/generics/input.ts`: + +```ts +import { defineErrors } from 'faultline'; + +const ApiErrors = defineErrors('Api', { + Timeout: { status: 408 }, +}); + +// Generic helper — runs an arbitrary function +function runWith(fn: () => T): T { + return fn(); +} + +// 1. Calling generic helper that calls a throwing function — uncovered-catch should detect +function genericCall() { + try { + return runWith(() => { + throw ApiErrors.Timeout(); + }); + } catch (e) { + return null; + } +} + +// 2. Mapped type return — known limitation, may not detect +type MapShape = { [K in keyof T]: T[K] }; +function mappedReturn(input: T): MapShape { + throw ApiErrors.Timeout(); +} + +function callMapped() { + try { + return mappedReturn({ a: 1 }); + } catch (e) { + return null; + } +} +``` + +- [ ] **Step 2: Write expectations (mark as triage candidates)** + +Create `packages/eslint-plugin-faultline/test/torture/generics/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // The plugin likely cannot detect this — generic helpers obscure return types. + // If the rule DOES report, that's good and we should update this expectation. + // If it does not report, document as a limitation. + { rule: 'uncovered-catch', line: 16, shouldNotReport: true, note: 'TRIAGE: generic helper hides throws' }, + { rule: 'uncovered-catch', line: 30, shouldNotReport: true, note: 'TRIAGE: mapped return type hides throws' }, +]; +``` + +- [ ] **Step 3: Run and commit** + +```bash +cd /Users/danielfry/dev/faultline && cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "generics" +``` + +Record output, then: + +```bash +git add packages/eslint-plugin-faultline/test/torture/generics +git commit -m "test(eslint-plugin): generics torture fixture (triage candidates)" +``` + +--- + +### Task 18: Add `third-party/` fixture + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/third-party/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/third-party/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/third-party/input.ts`: + +```ts +// 1. JSON.parse can throw SyntaxError — no faultline factory +function parseConfig(raw: string) { + try { + return JSON.parse(raw); + } catch (e) { + return {}; + } +} + +// 2. fetch can throw TypeError — no faultline factory +async function fetchJson(url: string) { + try { + const res = await fetch(url); + return await res.json(); + } catch (e) { + return null; + } +} + +// 3. Mixed — third-party throw + caught with no narrowing +function mixed() { + try { + const a = JSON.parse('not json'); + return a; + } catch (e) { + throw e; + } +} +``` + +- [ ] **Step 2: Write expectations** + +Create `packages/eslint-plugin-faultline/test/torture/third-party/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // Third-party throws have no faultline tags, so uncovered-catch correctly does not report. + { rule: 'uncovered-catch', line: 4, shouldNotReport: true, note: 'no faultline-typed throw in try' }, + { rule: 'uncovered-catch', line: 13, shouldNotReport: true, note: 'fetch is not faultline-typed' }, + { rule: 'uncovered-catch', line: 22, shouldNotReport: true, note: 'JSON.parse is not faultline-typed' }, +]; +``` + +- [ ] **Step 3: Run and commit** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "third-party" +git add packages/eslint-plugin-faultline/test/torture/third-party +git commit -m "test(eslint-plugin): third-party throws torture fixture" +``` + +--- + +### Task 19: Add `nested-try/` fixture + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/nested-try/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/nested-try/input.ts`: + +```ts +import { defineErrors, isErrorTag } from 'faultline'; + +const E = defineErrors('E', { + Outer: { status: 500 }, + Inner: { status: 500 }, +}); + +function outer() { throw E.Outer(); } +function inner() { throw E.Inner(); } + +// 1. Try inside try — inner catch handles Inner, outer must handle Outer +function nested() { + try { + outer(); + try { + inner(); + } catch (e) { + if (isErrorTag(e, E.Inner)) return; + } + } catch (e) { + // Should be flagged as uncovered for E.Outer (and possibly E.Inner if walkNode descends) + return; + } +} + +// 2. Try inside a function literal inside a try — outer try should NOT pick up the inner throw +function isolatedClosure() { + try { + setTimeout(() => { + try { inner(); } catch (e) {} + }, 0); + } catch (e) { + // Outer catch shouldn't even be triggered — but the rule may misattribute + } +} +``` + +- [ ] **Step 2: Write expectations** + +Create `packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 21, messageId: 'uncheckedCatch', note: 'outer catch must handle E.Outer (Inner is handled inside)' }, + { rule: 'uncovered-catch', line: 31, shouldNotReport: true, note: 'closure inside try does not propagate throws to outer catch' }, +]; +``` + +- [ ] **Step 3: Run and commit** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "nested-try" +git add packages/eslint-plugin-faultline/test/torture/nested-try +git commit -m "test(eslint-plugin): nested-try torture fixture" +``` + +--- + +### Task 20: Add `await-shapes/` fixture + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/await-shapes/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/await-shapes/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/await-shapes/input.ts`: + +```ts +import { defineErrors, type Infer } from 'faultline'; + +const E = defineErrors('E', { + A: { status: 500 }, + B: { status: 500 }, +}); + +import type { TypedPromise } from '../../../../faultline/src/typed-promise'; + +async function thrA(): TypedPromise> { throw E.A(); } +async function thrB(): TypedPromise> { throw E.B(); } + +// 1. await ternary — transitive check may miss this +async function awaitTernary(flag: boolean): TypedPromise> { + return await (flag ? thrA() : thrB()); +} + +// 2. await Promise.all — multiple sources of throw +async function awaitAll(): TypedPromise> { + return await Promise.all([thrA(), thrB()] as const); +} + +// 3. Plain await of typed call — should be detected as mismatch (declares only E.A but calls E.B) +async function plainMismatch(): TypedPromise> { + return await thrB(); +} +``` + +- [ ] **Step 2: Write expectations** + +Create `packages/eslint-plugin-faultline/test/torture/await-shapes/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // Likely missed by current rule — TRIAGE + { rule: 'throw-type-mismatch', line: 15, shouldNotReport: true, note: 'TRIAGE: await ternary not handled by transitive check' }, + { rule: 'throw-type-mismatch', line: 20, shouldNotReport: true, note: 'TRIAGE: await Promise.all not handled' }, + // This one SHOULD be flagged — plain await of mismatching call + { rule: 'throw-type-mismatch', line: 25, messageId: 'transitiveError', note: 'plain await of mismatched call' }, +]; +``` + +- [ ] **Step 3: Run and commit** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "await-shapes" +git add packages/eslint-plugin-faultline/test/torture/await-shapes +git commit -m "test(eslint-plugin): await-shapes torture fixture" +``` + +--- + +### Task 21: Add `overloads/` fixture + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/overloads/input.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/overloads/expected.ts` + +- [ ] **Step 1: Write the input** + +Create `packages/eslint-plugin-faultline/test/torture/overloads/input.ts`: + +```ts +import { defineErrors } from 'faultline'; + +const E = defineErrors('E', { + A: { status: 500 }, + B: { status: 500 }, +}); + +// Overloaded function — different return-type error signatures per overload +function ambiguous(a: string): { kind: 'A' }; +function ambiguous(a: number): { kind: 'B' }; +function ambiguous(a: string | number): { kind: 'A' | 'B' } { + if (typeof a === 'string') throw E.A(); + throw E.B(); +} + +// 1. Caller catches without narrowing — rule should detect throws from BOTH overloads +function callAmbiguous(input: string | number) { + try { + return ambiguous(input as any); + } catch (e) { + return null; + } +} +``` + +- [ ] **Step 2: Write expectations** + +Create `packages/eslint-plugin-faultline/test/torture/overloads/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // Current rule picks callSigs[0] — likely misses one of the overloads. TRIAGE candidate. + { rule: 'uncovered-catch', line: 20, shouldNotReport: true, note: 'TRIAGE: overloads — current code only inspects signature [0]' }, +]; +``` + +- [ ] **Step 3: Run and commit** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "overloads" +git add packages/eslint-plugin-faultline/test/torture/overloads +git commit -m "test(eslint-plugin): overloads torture fixture (triage candidate)" +``` + +--- + +### Task 22: Add remaining fixtures (`re-exports`, `class-methods`, `boundaries`) + +**Files:** +- Create: `packages/eslint-plugin-faultline/test/torture/re-exports/input.ts` + `expected.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/class-methods/input.ts` + `expected.ts` +- Create: `packages/eslint-plugin-faultline/test/torture/boundaries/input.ts` + `expected.ts` + +- [ ] **Step 1: Write `re-exports/input.ts`** + +```ts +// Aliased imports — does the rule recognize them? +import { narrowError as nE, defineErrors as dE } from 'faultline'; + +const Errs = dE('User', { + NotFound: { status: 404, message: (d: { id: string }) => `nf ${d.id}` }, +}); + +function get(id: string) { + if (!id) throw Errs.NotFound({ id }); + return { id }; +} + +function handled(id: string) { + try { + return get(id); + } catch (e) { + const err = nE(e, [Errs]); + if (err._tag === 'User.NotFound') return null; + throw err; + } +} +``` + +- [ ] **Step 2: Write `re-exports/expected.ts`** + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 14, shouldNotReport: true, note: 'aliased narrowError import should still cover' }, +]; +``` + +- [ ] **Step 3: Write `class-methods/input.ts`** + +```ts +import { defineErrors } from 'faultline'; + +const E = defineErrors('Svc', { + Bad: { status: 500 }, +}); + +class Service { + // 1. Throw in a method + doThing(): number { + throw E.Bad(); + } + + // 2. Throw in a getter + get value(): number { + throw E.Bad(); + } +} + +function callMethod() { + const s = new Service(); + try { + return s.doThing(); + } catch (e) { + return 0; + } +} + +function callGetter() { + const s = new Service(); + try { + return s.value; + } catch (e) { + return 0; + } +} +``` + +- [ ] **Step 4: Write `class-methods/expected.ts`** + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 22, messageId: 'uncheckedCatch', note: 'method throws Svc.Bad' }, + // Getter access is a MemberExpression, not a CallExpression — likely missed. TRIAGE. + { rule: 'uncovered-catch', line: 31, shouldNotReport: true, note: 'TRIAGE: getter access not detected as throwing' }, +]; +``` + +- [ ] **Step 5: Write `boundaries/input.ts`** + +```ts +import { defineErrors, defineBoundary } from 'faultline'; + +const SrcErrors = defineErrors('Src', { + A: { status: 500 }, +}); + +const DstErrors = defineErrors('Dst', { + X: { status: 500 }, +}); + +// Boundary map handler — `throw` here is structurally a typed factory call, should not trip no-raw-throw +const boundary = defineBoundary({ + name: 'src-to-dst', + from: SrcErrors, + map: { + 'Src.A': () => DstErrors.X(), + }, +}); + +void boundary; +``` + +- [ ] **Step 6: Write `boundaries/expected.ts`** + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // defineBoundary handlers return errors, they do not `throw`. No reports expected. +]; +``` + +- [ ] **Step 7: Run all three fixtures and commit** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts +git add packages/eslint-plugin-faultline/test/torture/re-exports packages/eslint-plugin-faultline/test/torture/class-methods packages/eslint-plugin-faultline/test/torture/boundaries +git commit -m "test(eslint-plugin): re-exports, class-methods, boundaries torture fixtures" +``` + +--- + +### Task 23: Run full corpus, collect triage findings + +**Files:** +- Modify: any `expected.ts` whose triage outcome flips from "shouldNotReport TRIAGE" → "this should be a fix" (no changes yet; just record) + +- [ ] **Step 1: Run the entire corpus** + +```bash +cd /Users/danielfry/dev/faultline/packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts 2>&1 | tee /tmp/torture-output.txt +``` + +- [ ] **Step 2: Read the output and categorize every test result** + +For each fixture category, record: + +- ✅ PASS — rule behavior matched expectations +- ❌ FAIL: missing — rule failed to report something `expected.ts` predicted +- ⚠️ FAIL: unexpected — rule reported something `expected.ts` did not predict + +- [ ] **Step 3: Write the triage doc** + +Create `/Users/danielfry/dev/faultline/docs/superpowers/notes/2026-05-22-eslint-triage.md` with this structure: + +```markdown +# ESLint Plugin Torture Triage — 2026-05-22 + +## Summary + +| Category | Status | Notes | +|---|---|---| +| baselines | (fill in) | | +| re-throws | | | +| generics | | | +| third-party | | | +| nested-try | | | +| await-shapes | | | +| overloads | | | +| re-exports | | | +| class-methods | | | +| boundaries | | | + +## Findings + +### Fix list (bugs to patch in this PR) + +- (e.g.) `throw-type-mismatch` picks `callSigs[0]` on overloaded functions — overloads/input.ts line 20 untested. Fix: iterate all signatures. + +### Document as limitation + +- (e.g.) Generic helpers obscure callee return types — rule cannot detect throws via `runWith(() => throw ...)`. Document in LIMITATIONS.md. + +### Out of scope + +- (e.g.) Reflection-based throws — Symbol.iterator side effects, etc. + +## Next steps + +Tasks 24+ in this plan handle the fix list. Tasks 26+ ship LIMITATIONS.md. +``` + +- [ ] **Step 4: Commit the triage doc** + +```bash +git add docs/superpowers/notes/2026-05-22-eslint-triage.md +git commit -m "docs: ESLint torture triage findings" +``` + +--- + +### Task 24: Fix `throw-type-mismatch` overload-signature bug + +**Files:** +- Modify: `packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts:21,98,129` + +> **Conditional:** only run this task if the triage in Task 23 confirms the overloads fixture failed because the rule picks `callSigs[0]`. If the triage shows a different root cause, adapt the fix accordingly. + +- [ ] **Step 1: Identify the three `callSigs[0]!` sites** + +Open `packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts`. There are three occurrences of `callSignatures[0]!` or `callSigs[0]!`. They sit in: `getDeclaredErrorTags`, the variable-typed enclosing function block, and `getCalleeErrorTags`. + +- [ ] **Step 2: Replace each with a multi-signature union** + +For each site, replace: + +```ts +const returnType = checker.getReturnTypeOfSignature(callSigs[0]!); +return extractErrorTagsFromType(checker, returnType); +``` + +with: + +```ts +const tagSet = new Set(); +for (const sig of callSigs) { + const returnType = checker.getReturnTypeOfSignature(sig); + for (const tag of extractErrorTagsFromType(checker, returnType)) { + tagSet.add(tag); + } +} +return [...tagSet]; +``` + +- [ ] **Step 3: Update the `overloads/expected.ts` fixture** + +Now that the rule examines all overloads, the `overloads` fixture should newly report. Update `packages/eslint-plugin-faultline/test/torture/overloads/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 20, messageId: 'uncheckedCatch', note: 'all overloads now inspected' }, +]; +``` + +- [ ] **Step 4: Run the corpus** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts +``` + +Expected: `overloads` test passes. No other fixtures regress. + +- [ ] **Step 5: Commit** + +```bash +git add packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts packages/eslint-plugin-faultline/test/torture/overloads/expected.ts +git commit -m "fix(eslint-plugin): inspect all overload signatures, not just first" +``` + +--- + +### Task 25: Fix `throw-type-mismatch` variable-throw blind spot + +**Files:** +- Modify: `packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts` (around line 160-175, the `undeclaredThrow` reporting block) + +> **Conditional:** run only if triage confirms `throw someVariable` of a known mismatching type is not reported. If a fixture proves this is already handled, skip and adapt accordingly. + +- [ ] **Step 1: Add a fixture demonstrating the blind spot** + +Create `packages/eslint-plugin-faultline/test/torture/variable-throw/input.ts`: + +```ts +import { defineErrors, type Infer } from 'faultline'; +import type { TypedPromise } from '../../../../faultline/src/typed-promise'; + +const E = defineErrors('E', { + A: { status: 500 }, + B: { status: 500 }, +}); + +async function declaresA(): TypedPromise> { + const wrong = E.B(); + throw wrong; +} +``` + +Create `packages/eslint-plugin-faultline/test/torture/variable-throw/expected.ts`: + +```ts +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'throw-type-mismatch', line: 11, messageId: 'mismatch', note: 'thrown variable of known mismatching type' }, +]; +``` + +- [ ] **Step 2: Run the fixture — should FAIL** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "variable-throw" +``` + +Expected: FAIL (the rule does not report `throw someVariable` even when type is known). + +- [ ] **Step 3: Update the rule to handle Identifier throws** + +In `throw-type-mismatch.ts`, find the block: + +```ts +if ( + node.argument.type === 'NewExpression' || + node.argument.type === 'Literal' +) { + context.report({ ... messageId: 'undeclaredThrow', ... }); +} +``` + +The condition is too narrow. The earlier `getThrowTag` returns `undefined` only when the thrown type has no literal `_tag`. But for `Identifier` throws whose *type* is a known AppError factory output, `getThrowTag` already returns the tag and the mismatch path fires. The actual blind spot is when the type has a `_tag` but the variable was reassigned to something untracked. + +The cleaner fix is to also report `undeclaredThrow` for `Identifier` throws where the type lacks a string-literal `_tag` (it's `string` or `unknown`): + +Replace the block with: + +```ts +if ( + node.argument.type === 'NewExpression' || + node.argument.type === 'Literal' || + node.argument.type === 'Identifier' +) { + context.report({ + node: node.argument, + messageId: 'undeclaredThrow', + data: { declaredTags: declaredTags.join(', ') }, + }); +} +``` + +This is correct because the surrounding `if (thrownTag)` branch already handled the case where the identifier's type *does* have a known `_tag`. So extending the `else` to include `Identifier` only fires when the type has no resolvable `_tag` — exactly the case we want to flag. + +- [ ] **Step 4: Run the fixture — should PASS** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts -t "variable-throw" +``` + +Expected: PASS, and no other fixtures regress. + +Run the full corpus to verify: +```bash +bun test test/torture/run-torture.test.ts +``` + +- [ ] **Step 5: Commit** + +```bash +git add packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts packages/eslint-plugin-faultline/test/torture/variable-throw +git commit -m "fix(eslint-plugin): report throw of untyped variable in TypedPromise scope" +``` + +--- + +### Task 26: Fix `uncovered-catch` closure-descent bug + +**Files:** +- Modify: `packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts` (the `walkNode` invocation, around line 75) + +> **Conditional:** only if triage confirms the `nested-try` fixture's second case (closure inside try) misattributes throws to the outer catch. + +- [ ] **Step 1: Update `walkNode` to skip function-literal subtrees** + +In `uncovered-catch.ts`, the `walkNode` helper descends into every child. For the try-block call collection step, we should *not* descend into nested function literals — their throws don't escape to the enclosing catch. + +Modify the `walkNode` callback inside the `TryStatement` handler (around line 75): + +```ts +walkNode(context.sourceCode, node.block, (astNode) => { + // Don't descend into function literals — their throws don't reach the enclosing catch + if ( + astNode.type === 'FunctionExpression' || + astNode.type === 'ArrowFunctionExpression' || + astNode.type === 'FunctionDeclaration' + ) { + return; + } + if (astNode.type !== 'CallExpression') return; + // ... existing body +}); +``` + +But `walkNode` doesn't currently have a way to skip subtrees from the callback — it always descends. Update `walkNode` to support a "skip" signal: + +```ts +function walkNode( + sourceCode: { visitorKeys: Record }, + node: TSESTree.Node, + callback: (n: TSESTree.Node) => boolean | void, // return false to skip children +): void { + const skip = callback(node); + if (skip === false) return; + + const keys = sourceCode.visitorKeys[node.type]; + if (!keys) return; + + for (const key of keys) { + const child = (node as unknown as Record)[key]; + if (Array.isArray(child)) { + for (const item of child) { + if (item && typeof item === 'object' && 'type' in item) { + walkNode(sourceCode, item as TSESTree.Node, callback); + } + } + } else if (child && typeof child === 'object' && 'type' in child) { + walkNode(sourceCode, child as TSESTree.Node, callback); + } + } +} +``` + +Then in the call-collection callback, return `false` for function literals: + +```ts +walkNode(context.sourceCode, node.block, (astNode) => { + if ( + astNode.type === 'FunctionExpression' || + astNode.type === 'ArrowFunctionExpression' || + astNode.type === 'FunctionDeclaration' + ) { + return false; // skip subtree + } + if (astNode.type !== 'CallExpression') return; + // ... existing body +}); +``` + +- [ ] **Step 2: Run the corpus** + +```bash +cd packages/eslint-plugin-faultline && bun test test/torture/run-torture.test.ts +``` + +Expected: `nested-try` test passes (closure case no longer misattributes), no regressions. + +- [ ] **Step 3: Commit** + +```bash +git add packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts +git commit -m "fix(eslint-plugin): don't descend into closures when collecting try-block throws" +``` + +--- + +### Task 27: Write LIMITATIONS.md + +**Files:** +- Create: `packages/eslint-plugin-faultline/LIMITATIONS.md` + +- [ ] **Step 1: Write the doc** + +Use the triage doc (Task 23) to populate. Template: + +```markdown +# ESLint Plugin: Known Limitations + +The `eslint-plugin-faultline` rules use type-aware analysis to verify error coverage. Some patterns are inherently difficult or impossible to analyze statically. This document lists what the rules currently miss, so you know where to apply manual review. + +## `uncovered-catch` + +### Generic helpers obscure callee throws + +```ts +function runWith(fn: () => T): T { return fn(); } + +try { + runWith(() => { throw UserErrors.NotFound({ userId: '1' }); }); +} catch (e) { + // Not flagged — the rule sees runWith's return type as , not the inner throw type +} +``` + +**Workaround:** Don't hide throws behind generic identity helpers. If you must, annotate the helper's return type explicitly with the error union. + +### Mapped-type returns hide throws + +```ts +function transform(x: T): { [K in keyof T]: T[K] } { + throw E.Whatever(); +} +``` + +Mapped types are opaque to the tag-extraction utility. + +**Workaround:** Use explicit return types when the function can throw faultline errors. + +### Getter access is not detected as throwing + +```ts +class Service { get value() { throw E.Bad(); return 0; } } +try { new Service().value; } catch (e) {} +``` + +The rule scans `CallExpression` nodes in the try block; getter access is a `MemberExpression`. + +**Workaround:** Convert getters to explicit methods, or be aware that getter-based throws bypass the rule. + +## `throw-type-mismatch` + +### Transitive errors through `await` ternaries and `Promise.all` + +```ts +async function fn(): TypedPromise> { + return await (flag ? throwsA() : throwsB()); // E.B not flagged +} +``` + +The transitive check inspects `await callExpression` directly; ternary expressions and `Promise.all` patterns are not unwrapped. + +**Workaround:** Use named intermediate calls so each `await` is a direct call: +```ts +const promise = flag ? throwsA() : throwsB(); +return await promise; // still missed +``` +The cleanest workaround is to handle errors at the call site of each branch. + +### Dynamic throws of unknown-type values + +```ts +function throwAnything(e: unknown) { throw e; } +``` + +If a thrown variable's type is `unknown` or `any`, the rule cannot verify the tag against declared TypedPromise errors. + +**Workaround:** Use typed factories. If you must throw an unknown, narrow it first. + +## Performance + +Type-aware rules require a full TypeScript program. Very large codebases or deeply generic patterns may slow lint runs. If you see runs > 30s on a single file, consider scoping the plugin to specific directories. + +## Out of scope + +- Reflection-based throws (`Reflect.construct`, etc.) +- Throws from `eval` +- Throws routed through Promises that the rule cannot trace (custom queue libraries, etc.) + +If you encounter a pattern not listed here that you believe the rule should detect, please file an issue with a minimal reproduction. +``` + +- [ ] **Step 2: Link from the plugin's README (if it has one) and from the main README** + +In the main `README.md`, find the "## Ecosystem" section and the "### ESLint Plugin" table. After the table, add: + +```markdown +**Known limitations:** see [`eslint-plugin-faultline/LIMITATIONS.md`](./packages/eslint-plugin-faultline/LIMITATIONS.md) for patterns the rules can't currently detect. +``` + +- [ ] **Step 3: Commit** + +```bash +git add packages/eslint-plugin-faultline/LIMITATIONS.md README.md +git commit -m "docs(eslint-plugin): publish known limitations" +``` + +--- + +### Task 28: Update CHANGELOG with Phase 4 entries + +**Files:** +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: Append to the `[Unreleased]` section** + +Add under "### Added": + +```markdown +- ESLint plugin: torture-fixture test corpus exercising 10+ real-world patterns (re-throws, generics, third-party throws, nested try, await shapes, overloads, re-exports, class methods, boundaries, baselines). +- ESLint plugin: `LIMITATIONS.md` documenting patterns the rules can't detect. +- ESLint plugin: `throw-type-mismatch` now inspects all overload signatures, not just the first. +- ESLint plugin: `throw-type-mismatch` reports thrown identifiers with no resolvable `_tag` when in a TypedPromise scope. +- ESLint plugin: `uncovered-catch` no longer misattributes throws inside nested function literals to the outer try's catch. +``` + +- [ ] **Step 2: Commit** + +```bash +git add CHANGELOG.md +git commit -m "docs: changelog entries for Phase 4 ESLint improvements" +``` + +--- + +## Final wrap-up + +### Task 29: Run full repo test + typecheck + lint + +**Files:** none + +- [ ] **Step 1: Run everything** + +```bash +cd /Users/danielfry/dev/faultline && bun --filter '*' test && bun --filter '*' typecheck && bun run lint +``` + +Expected: all green. + +- [ ] **Step 2: If anything is red** + +Stop. Fix the offending change. Re-run. + +- [ ] **Step 3: Final commit if any cleanup was needed** + +```bash +git add -u +git commit -m "chore: final cleanup before review" +``` + +--- + +### Task 30: Push branch and open PR + +**Files:** none + +- [ ] **Step 1: Push the branch** + +```bash +git push -u origin production-grade-concerns +``` + +- [ ] **Step 2: Open PR** + +```bash +gh pr create --title "Production-grade concerns: README, escape hatch, TaskResult, ESLint torture suite" --body "$(cat <<'EOF' +## Summary +- README repositioning + new "How faultline differs" section +- Documents explicit error-code escape hatch (feature already existed; docs gap closed) +- TaskResult: cancellation propagation through chain steps, `toPromise` removed, `fromPromise` catches rejections, new docs +- ESLint plugin: torture-fixture corpus (10+ categories), bug fixes for overloaded sigs / variable throws / closure descent, `LIMITATIONS.md` + +See `docs/superpowers/specs/2026-05-22-production-grade-concerns-design.md` for the spec and `docs/superpowers/plans/2026-05-22-production-grade-concerns.md` for the implementation plan. + +## Breaking changes +- `TaskResult.toPromise()` removed (use `.run()`) +- TaskResult chain error union widens to include `System.Cancelled` +- `TaskResult.fromPromise` return type widens to include `System.Unexpected` + +## Test plan +- [ ] All existing tests pass (`bun --filter '*' test`) +- [ ] Repo-wide typecheck clean (`bun --filter '*' typecheck`) +- [ ] Torture fixture corpus passes (`cd packages/eslint-plugin-faultline && bun test test/torture/`) +- [ ] CHANGELOG.md captures all breaking changes +- [ ] README renders cleanly +EOF +)" +``` + +--- + +## Out of scope for this plan (follow-ups) + +- Further ESLint rule fixes beyond the three identified in Tasks 24-26. Subsequent triage rounds become new plans. +- VS Code extension changes. +- CLI changes. +- Performance optimization beyond cancellation correctness. +- Adding `?=` proposal support (explicitly decided against in the README positioning). + +--- + +## Self-Review + +**Spec coverage:** + +- Workstream 1 (README positioning) → Tasks 1, 2 ✅ +- Workstream 2 (escape hatch docs + test) → Tasks 3, 4, 5 ✅ +- Workstream 3 (TaskResult production-grade pass) → Tasks 6, 7, 8, 9, 10, 11, 12 ✅ +- Workstream 4 (ESLint torture corpus + triage + fixes + LIMITATIONS) → Tasks 13–28 ✅ +- Spec's "Risks" section in §3 (type widening as breaking change) → addressed in Tasks 8 step 5 (fix fixtures) and Task 12 (changelog) ✅ +- Spec's "Risks" section in §4 (triage volume throttle) → addressed implicitly by limiting Phase 4 to three specific fixes (Tasks 24–26) with the rest → LIMITATIONS.md (Task 27) ✅ + +**Placeholder scan:** None of the disallowed patterns ("TBD", "implement later", "Similar to Task N", etc.) appear. All code blocks contain actual code. + +**Type consistency:** `InferCancelled` / `InferUnexpected` type aliases used consistently in Tasks 7 and 10. `RuleExpectation` shape consistent across all fixture `expected.ts` files. `walkNode`'s new boolean-return contract introduced in Task 26 — only affects that file and only after that task lands. + +**Note on Task 22:** The remaining-fixtures task bundles three fixtures (re-exports, class-methods, boundaries) into one task. This is borderline against bite-sized granularity but each is mechanically the same shape as Tasks 16–21 and the bundling avoids artificial inflation. If the executor prefers to split, each pair of files is independently committable. From 3d0d7d33e6ae4f5b54c499d7cf0f754ddf047392 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:03:57 +0100 Subject: [PATCH 03/42] docs: lead with 'incremental' in tagline --- README.md | 2 +- packages/faultline/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57d0423..ac0f212 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # faultline -The complete type-safe error system for TypeScript. +The incremental type-safe error system for TypeScript. Define your errors. Throw them like normal. The ESLint plugin tells you when you miss something. Adopt Result types and boundaries when you're ready — or don't. Either way, your app is better off. diff --git a/packages/faultline/package.json b/packages/faultline/package.json index 951d059..4885e2b 100644 --- a/packages/faultline/package.json +++ b/packages/faultline/package.json @@ -1,7 +1,7 @@ { "name": "faultline", "version": "0.2.0", - "description": "The complete type-safe error system for TypeScript.", + "description": "The incremental type-safe error system for TypeScript.", "author": "Daniel Fry", "license": "MIT", "type": "module", From 40b4c58600cb1344ac27d13f1f13222548a3361e Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:04:20 +0100 Subject: [PATCH 04/42] docs: add 'How faultline differs' section --- README.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ac0f212..fd677e6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ TypeScript tells you the shape of your data, but not the shape of your failures. Faultline fixes this incrementally. You don't need to rewrite your app. You don't need to learn a new paradigm. Start with what you already know — `throw` and `catch` — and let the tooling guide you forward. +## How faultline differs + +**vs. neverthrow, ts-results, true-myth.** These are solid Result-type libraries. Faultline's difference is that it doesn't ask you to convert your throws first. Stage 1 — typed factories plus the ESLint plugin — delivers most of the value while your code keeps using `throw` and `catch`. Result types are there when you want them at Stage 3. + +**vs. Effect.** Effect is more powerful and more mature. Faultline is narrowly focused on errors: no effect system, no runtime, no scheduler. Just typed errors plus optional Results. The learning curve is hours, not weeks. + +**vs. typed `Error` subclasses.** Custom Error classes work. Faultline derives consistent tags, codes, status values, and serialization from a single declaration, ships an ESLint plugin that catches drift, and gives you a smooth path to Result types if you want them later. + +**vs. the `?=` proposal.** You may have seen the TC39 [Safe Assignment Operator (`?=`) proposal](https://github.com/nicolo-ribaudo/tc39-proposal-safe-assignment-operator) or libraries that return `[error, value]` tuples. Faultline uses discriminated unions instead because: (1) TypeScript narrows discriminated unions more reliably than tuple truthiness — after `if (isErr(result))`, the compiler *guarantees* the error type; (2) the `?=` proposal doesn't type the error — it's still `unknown`; (3) a single `result` value composes with `match()`, returns cleanly from functions, and chains methods. If you prefer tuple syntax, a one-line helper gets you there and your errors stay fully typed (see "Why not `[err, value]` tuples?" below). + ## Install ```bash @@ -235,21 +245,7 @@ This errors on any raw `throw` and any uncovered catch — pushing you toward Re ### Why not `[err, value]` tuples? -You may have seen the TC39 [Safe Assignment Operator (`?=`) proposal](https://github.com/nicolo-ribaudo/tc39-proposal-safe-assignment-operator) or libraries that return `[error, value]` tuples: - -```ts -const [err, user] = safeTry(() => getUser(id)); -``` - -We considered this but chose discriminated unions (`result._type`) for a few reasons: - -1. **TypeScript narrows discriminated unions more reliably than tuple truthiness.** After `if (isErr(result))`, the compiler *guarantees* `result.error` is typed. With `if (err)`, you're relying on truthiness narrowing — which works but is a weaker contract and easier to get backwards. - -2. **The `?=` proposal doesn't type the error.** It's sugar for try/catch — `err` is still `unknown`. Faultline's value is that errors carry typed data, tags, and codes. Even if `?=` lands, you'd still want faultline underneath. - -3. **A single `result` value composes better.** You can pass it to `match()`, return it from functions, or chain methods on it. Two separate variables can't do that. - -If you prefer the tuple syntax, a one-line helper gets you there — and your errors stay fully typed: +We chose discriminated unions over tuples (see "How faultline differs" above). If you prefer the tuple syntax, a one-line helper gets you there — and your errors stay fully typed: ```ts function tryResult( From f92712f0f5d7240b216ee6f2bf0542ef3065fc0f Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:07:07 +0100 Subject: [PATCH 05/42] docs: document explicit error-code escape hatch --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index fd677e6..848af8f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,23 @@ Tags and codes are auto-generated: - `UserErrors.NotFound(...)._tag` → `'User.NotFound'` - `UserErrors.NotFound(...).code` → `'USER_NOT_FOUND'` +**Codes default to auto-generated `SCREAMING_SNAKE_CASE` from the tag.** If you have an existing error-code convention, pass an explicit `code` — it's preserved at the type level too, so downstream consumers see the literal string: + +```ts +const UserErrors = defineErrors('User', { + // Auto-generated: code === 'USER_NOT_FOUND' + NotFound: { + status: 404, + message: (data: { userId: string }) => `User ${data.userId} not found`, + }, + // Explicit: code === 'ERR_AUTH_401' (literal type preserved) + Unauthorized: { + code: 'ERR_AUTH_401', + status: 401, + }, +}); +``` + **Throw them like you already do:** ```ts From 3074895bc81bcb6418318e5ab1ad9600a7379fc7 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:07:31 +0100 Subject: [PATCH 06/42] test: cover explicit-code escape hatch in defineErrors --- packages/faultline/test/error-system.test.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/faultline/test/error-system.test.ts b/packages/faultline/test/error-system.test.ts index d0d6fff..02916d4 100644 --- a/packages/faultline/test/error-system.test.ts +++ b/packages/faultline/test/error-system.test.ts @@ -570,3 +570,28 @@ describe('attempt overloads', () => { } }); }); + +describe('defineErrors with explicit codes', () => { + test('preserves explicit code at runtime', () => { + const errors = defineErrors('User', { + Unauthorized: { code: 'ERR_AUTH_401', status: 401 }, + }); + expect(errors.Unauthorized().code).toBe('ERR_AUTH_401'); + }); + + test('falls back to auto-generated code when not provided', () => { + const errors = defineErrors('User', { + NotFound: { status: 404 }, + }); + expect(errors.NotFound().code).toBe('USER_NOT_FOUND'); + }); + + test('mixing explicit and auto-generated codes in one group', () => { + const errors = defineErrors('User', { + NotFound: { status: 404 }, + Unauthorized: { code: 'ERR_AUTH_401', status: 401 }, + }); + expect(errors.NotFound().code).toBe('USER_NOT_FOUND'); + expect(errors.Unauthorized().code).toBe('ERR_AUTH_401'); + }); +}); From 0efda35a2d914fa78ec6d252b158a92a66985175 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:07:51 +0100 Subject: [PATCH 07/42] test: assert explicit-code literal type preservation --- packages/faultline/test/typecheck.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/faultline/test/typecheck.ts b/packages/faultline/test/typecheck.ts index 2f0e157..7ca5e52 100644 --- a/packages/faultline/test/typecheck.ts +++ b/packages/faultline/test/typecheck.ts @@ -171,3 +171,19 @@ type _partialMatched = Expect>; const empty = all([] as const); const _emptyAll: Result = empty; + +// === Explicit code preservation === +const ExplicitCodeErrors = defineErrors('User', { + Unauthorized: { code: 'ERR_AUTH_401', status: 401 }, + NotFound: { status: 404 }, +}); + +// Explicit code is preserved as a literal type +type _ExplicitCodeLiteral = Expect< + Equal['code'], 'ERR_AUTH_401'> +>; + +// Auto-generated code is also preserved as a literal type +type _AutoCodeLiteral = Expect< + Equal['code'], 'USER_NOT_FOUND'> +>; From bc051f222baef21b636b5d35b27e3e59291d4a50 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:17:53 +0100 Subject: [PATCH 08/42] test: failing cancellation propagation tests for TaskResult --- packages/faultline/test/task-result.test.ts | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/faultline/test/task-result.test.ts b/packages/faultline/test/task-result.test.ts index f5b91eb..5922e90 100644 --- a/packages/faultline/test/task-result.test.ts +++ b/packages/faultline/test/task-result.test.ts @@ -118,3 +118,47 @@ describe('TaskResult.withContext', () => { } }); }); + +describe('TaskResult cancellation propagation', () => { + test('aborting mid-chain short-circuits subsequent steps', async () => { + const stepsRun: number[] = []; + const controller = new AbortController(); + + const task = TaskResult.ok(1) + .map((v) => { stepsRun.push(1); return v + 1; }) + .map((v) => { stepsRun.push(2); controller.abort(); return v + 1; }) + .map((v) => { stepsRun.push(3); return v + 1; }) + .map((v) => { stepsRun.push(4); return v + 1; }); + + const result = await task.run({ signal: controller.signal }); + + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('System.Cancelled'); + } + // Steps 1 and 2 ran; 3 and 4 must not have + expect(stepsRun).toEqual([1, 2]); + }); + + test('no cancellation when signal never aborts', async () => { + const controller = new AbortController(); + const task = TaskResult.ok(1).map((v) => v + 1).map((v) => v + 1); + const result = await task.run({ signal: controller.signal }); + + expect(isOk(result)).toBe(true); + if (isOk(result)) { + expect(result.value).toBe(3); + } + }); + + test('pre-aborted signal short-circuits before first step', async () => { + const controller = new AbortController(); + controller.abort(); + let ran = false; + const task = TaskResult.ok(1).map(() => { ran = true; return 2; }); + const result = await task.run({ signal: controller.signal }); + + expect(isErr(result)).toBe(true); + expect(ran).toBe(false); + }); +}); From 516b4ee843faf89afa27280dd924218b62415779 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:19:18 +0100 Subject: [PATCH 09/42] feat(task-result): propagate cancellation through chain steps --- packages/faultline/src/task-result.ts | 208 +++++++++++++++++--------- 1 file changed, 135 insertions(+), 73 deletions(-) diff --git a/packages/faultline/src/task-result.ts b/packages/faultline/src/task-result.ts index ec2ec08..d47781b 100644 --- a/packages/faultline/src/task-result.ts +++ b/packages/faultline/src/task-result.ts @@ -6,6 +6,19 @@ import { type TagsOf, ok, err, isOk, isErr, } from './result'; +import { SystemErrors } from './system-errors'; + +type InferCancelled = ReturnType; + +function checkCancelled(context: TaskContext): InferCancelled | undefined { + if (context.signal?.aborted) { + const reason = context.signal.reason; + return SystemErrors.Cancelled({ + reason: typeof reason === 'string' ? reason : reason instanceof Error ? reason.message : 'aborted', + }); + } + return undefined; +} export interface TaskContext { readonly signal?: AbortSignal; @@ -66,57 +79,80 @@ export class TaskResult { return TaskResult.fromResult(err(error)); } - map(fn: (value: T) => U | Promise): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => - isOk(result) - ? ok(await fn(result.value)) - : // Covariance: ErrImpl → Result — error path, T is unused - (result as unknown as Result), - ), - ); + map(fn: (value: T) => U | Promise): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; + + return isOk(result) + ? ok(await fn(result.value)) + : (result as unknown as Result); + }); } mapErr( fn: (error: E) => E2 | Promise, - ): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => - // Generic variance: err() returns ResultErr, widen to Result - isErr(result) ? (err(await fn(result.error)) as Result) : ok(result.value), - ), - ); + ): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; + + // Generic variance: err() returns ResultErr, widen to Result + return isErr(result) ? (err(await fn(result.error)) as Result) : ok(result.value); + }); } andThen( fn: (value: T) => Result | Promise>, - ): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => - isOk(result) - ? await fn(result.value) - : // Covariance: ErrImpl → Result — error path, T is unused - (result as unknown as Result), - ), - ); + ): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; + + return isOk(result) + ? await fn(result.value) + : // Covariance: ErrImpl → Result — error path, T is unused + (result as unknown as Result); + }); } andThenTask( fn: | ((value: T) => TaskResult) | ((value: T) => Promise>), - ): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => { - if (isErr(result)) { - // Covariance: ErrImpl → Result — error path, T is unused - return result as unknown as Result; - } + ): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; - const nextTask = await fn(result.value); - return nextTask.run(context); - }), - ); + if (isErr(result)) { + // Covariance: ErrImpl → Result — error path, T is unused + return result as unknown as Result; + } + + const nextTask = await fn(result.value); + return nextTask.run(context); + }); } catchTag, U = T, E2 extends AppError = never>( @@ -127,21 +163,27 @@ export class TaskResult { | Result | TaskResult | Promise | TaskResult>, - ): TaskResult | E2> { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => { - if (!isErr(result) || result.error._tag !== tag) { - // Covariance: error doesn't match tag, so Exclude is safe; widen T to T|U - return result as unknown as Result | E2>; - } - - return resolveTaskLike( - // Discriminated union narrowing: runtime _tag check guarantees Extract - handler(result.error as Extract), - context, - ); - }), - ); + ): TaskResult | E2 | InferCancelled> { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result | E2 | InferCancelled>; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result | E2 | InferCancelled>; + + if (!isErr(result) || result.error._tag !== tag) { + // Covariance: error doesn't match tag, so Exclude is safe; widen T to T|U + return result as unknown as Result | E2 | InferCancelled>; + } + + return resolveTaskLike( + // Discriminated union narrowing: runtime _tag check guarantees Extract + handler(result.error as Extract), + context, + ); + }); } async match( @@ -153,34 +195,54 @@ export class TaskResult { return result.match(handlers as ExhaustiveMatchHandlers>); } - tap(fn: (value: T) => void | Promise): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => { - if (isOk(result)) { - await fn(result.value); - } + tap(fn: (value: T) => void | Promise): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; - return result; - }), - ); + if (isOk(result)) { + await fn(result.value); + } + + return result; + }); } - tapError(fn: (error: E) => void | Promise): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then(async (result) => { - if (isErr(result)) { - await fn(result.error); - } + tapError(fn: (error: E) => void | Promise): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; + + if (isErr(result)) { + await fn(result.error); + } - return result; - }), - ); + return result; + }); } - withContext(frame: ContextFrame): TaskResult { - return TaskResult.from(async (context) => - this.executor(context).then((result) => result.withContext(frame)), - ); + withContext(frame: ContextFrame): TaskResult { + return TaskResult.from(async (context) => { + const preCancel = checkCancelled(context); + if (preCancel) return err(preCancel) as Result; + + const result = await this.executor(context); + + const postCancel = checkCancelled(context); + if (postCancel) return err(postCancel) as Result; + + return result.withContext(frame); + }); } run(options: TaskRunOptions = {}): Promise> { From 9ac0eb962f35ac8b5befd3aee16684f9046338e8 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:20:02 +0100 Subject: [PATCH 10/42] test: assert TaskResult chain widens error union with System.Cancelled --- packages/faultline/test/typecheck.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/faultline/test/typecheck.ts b/packages/faultline/test/typecheck.ts index 7ca5e52..8a35075 100644 --- a/packages/faultline/test/typecheck.ts +++ b/packages/faultline/test/typecheck.ts @@ -187,3 +187,17 @@ type _ExplicitCodeLiteral = Expect< type _AutoCodeLiteral = Expect< Equal['code'], 'USER_NOT_FOUND'> >; + +// === Cancellation widening === +type CancelledTag = ReturnType['_tag']; +type _CancelledTagIsCorrect = Expect>; + +// After .map, the error union must include System.Cancelled +const _baseTask: TaskResult = TaskResult.ok(1); +const _mappedTask = _baseTask.map((v) => v + 1); +type MappedError = Awaited> extends Result ? E : never; + +// MappedError should be a union containing System.Cancelled (the AppError instance) +type _IncludesCancelled = Expect< + Equal +>; From 18502a0ba7cd506e5bade5afbda62df0269a3a5d Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:27:42 +0100 Subject: [PATCH 11/42] chore(task-result): document mapErr+Cancelled, widen andThenTask typecheck --- packages/faultline/src/task-result.ts | 2 ++ packages/faultline/test/typecheck.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/faultline/src/task-result.ts b/packages/faultline/src/task-result.ts index d47781b..9ed6d67 100644 --- a/packages/faultline/src/task-result.ts +++ b/packages/faultline/src/task-result.ts @@ -95,6 +95,8 @@ export class TaskResult { }); } + // Note: if the upstream error union includes System.Cancelled, fn will receive it. + // Use catchTag('System.Cancelled', ...) upstream of mapErr to short-circuit before transformation. mapErr( fn: (error: E) => E2 | Promise, ): TaskResult { diff --git a/packages/faultline/test/typecheck.ts b/packages/faultline/test/typecheck.ts index 8a35075..81b631b 100644 --- a/packages/faultline/test/typecheck.ts +++ b/packages/faultline/test/typecheck.ts @@ -201,3 +201,11 @@ type MappedError = Awaited> extends Result >; + +// andThenTask also widens to include System.Cancelled +const _innerTask: TaskResult = TaskResult.ok(2); +const _chainedTask = _baseTask.andThenTask(() => _innerTask); +type ChainedError = Awaited> extends Result ? E : never; +type _ChainedIncludesCancelled = Expect< + Equal +>; From 54c1179d79d6fbe8195e0acff9745cb8d9091e48 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:28:51 +0100 Subject: [PATCH 12/42] refactor(task-result): remove toPromise alias (use run) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: TaskResult.toPromise() has been removed. Call .run() instead — it has the same signature and behavior. --- packages/faultline/src/task-result.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/faultline/src/task-result.ts b/packages/faultline/src/task-result.ts index 9ed6d67..f4d979c 100644 --- a/packages/faultline/src/task-result.ts +++ b/packages/faultline/src/task-result.ts @@ -250,8 +250,4 @@ export class TaskResult { run(options: TaskRunOptions = {}): Promise> { return this.executor(options); } - - toPromise(options: TaskRunOptions = {}): Promise> { - return this.run(options); - } } From 6d8fc47718fc2e9e5ffb4c1ca55c1fce1d1780d4 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:31:39 +0100 Subject: [PATCH 13/42] fix(task-result): fromPromise catches rejections as System.Unexpected Previously, a rejection from the factory bubbled up as an uncaught promise rejection. Now it's wrapped as System.Unexpected with the original throw preserved as .cause. BREAKING CHANGE: fromPromise return type now widens to include System.Unexpected. --- packages/faultline/src/task-result.ts | 17 +++++++++++--- packages/faultline/test/task-result.test.ts | 26 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/faultline/src/task-result.ts b/packages/faultline/src/task-result.ts index f4d979c..3d6b85f 100644 --- a/packages/faultline/src/task-result.ts +++ b/packages/faultline/src/task-result.ts @@ -9,6 +9,7 @@ import { import { SystemErrors } from './system-errors'; type InferCancelled = ReturnType; +type InferUnexpected = ReturnType; function checkCancelled(context: TaskContext): InferCancelled | undefined { if (context.signal?.aborted) { @@ -62,11 +63,21 @@ export class TaskResult { return TaskResult.from(async () => result); } - /** Creates a TaskResult from a factory that returns a Promise of Result. The factory is called on each `.run()`. */ + /** Creates a TaskResult from a factory that returns a Promise of Result. The factory is called on each `.run()`. Rejections are captured as System.Unexpected with the thrown value as `.cause`. */ static fromPromise( factory: () => Promise>, - ): TaskResult { - return new TaskResult(async () => factory()); + ): TaskResult { + return new TaskResult(async () => { + try { + return await factory(); + } catch (thrown) { + const message = thrown instanceof Error ? thrown.message : typeof thrown === 'string' ? thrown : 'Unexpected error'; + const name = thrown instanceof Error ? thrown.name : undefined; + const unexpected = SystemErrors.Unexpected({ name, message }); + // withCause returns AppError — narrowing to InferUnexpected is safe since we created it via SystemErrors.Unexpected + return err((thrown !== null && thrown !== undefined ? unexpected.withCause(thrown) : unexpected) as InferUnexpected) as Result; + } + }); } /** Creates a successful TaskResult. */ diff --git a/packages/faultline/test/task-result.test.ts b/packages/faultline/test/task-result.test.ts index 5922e90..88401bd 100644 --- a/packages/faultline/test/task-result.test.ts +++ b/packages/faultline/test/task-result.test.ts @@ -119,6 +119,32 @@ describe('TaskResult.withContext', () => { }); }); +describe('TaskResult.fromPromise rejection handling', () => { + test('rejected factory becomes System.Unexpected with cause preserved', async () => { + const original = new Error('boom'); + const task = TaskResult.fromPromise(async () => { + throw original; + }); + const result = await task.run(); + + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('System.Unexpected'); + expect(result.error.cause).toBe(original); + } + }); + + test('successful factory passes through unchanged', async () => { + const task = TaskResult.fromPromise(async () => ok(42)); + const result = await task.run(); + + expect(isOk(result)).toBe(true); + if (isOk(result)) { + expect(result.value).toBe(42); + } + }); +}); + describe('TaskResult cancellation propagation', () => { test('aborting mid-chain short-circuits subsequent steps', async () => { const stepsRun: number[] = []; From a17427a30a35e2dcf6a5fbaf48000c00fde1239b Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:33:25 +0100 Subject: [PATCH 14/42] test(task-result): cover non-Error throws through fromPromise --- packages/faultline/test/task-result.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/faultline/test/task-result.test.ts b/packages/faultline/test/task-result.test.ts index 88401bd..5cd56da 100644 --- a/packages/faultline/test/task-result.test.ts +++ b/packages/faultline/test/task-result.test.ts @@ -134,6 +134,19 @@ describe('TaskResult.fromPromise rejection handling', () => { } }); + test('non-Error throws are still wrapped as System.Unexpected', async () => { + const task = TaskResult.fromPromise(async () => { + throw 'string-thrown-instead-of-error'; + }); + const result = await task.run(); + + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('System.Unexpected'); + expect(result.error.cause).toBe('string-thrown-instead-of-error'); + } + }); + test('successful factory passes through unchanged', async () => { const task = TaskResult.fromPromise(async () => ok(42)); const result = await task.run(); From a6f7727e3df9217f377e0cee7dcb762afc7a8a68 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:34:21 +0100 Subject: [PATCH 15/42] docs: explain when to use TaskResult vs Promise --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 848af8f..e1b42e9 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,33 @@ const result = attempt(() => JSON.parse(raw)); // Result ``` +### When to use TaskResult vs `Promise` + +Reach for `TaskResult` when you need one of: + +- **Cancellation through a pipeline.** Pass an `AbortSignal` to `.run({ signal })` and every chained step checks it. Abort once, and the rest of the chain short-circuits to `System.Cancelled` — no partial work continues. +- **Context frames across async steps.** Use `.withContext({ ... })` to attach observability metadata that survives composition. +- **Lazy composition.** Build the pipeline, hand it off, run it later. Plain Promises run eagerly the moment you construct them. + +Otherwise, `Promise` is fine. It composes with `await` and plain `if (isErr(result))` checks just like the synchronous Stage 3 examples. + +```ts +// Promise — perfectly good for most async work +async function getUser(id: string): Promise> { + const user = await db.users.find(id); + if (!user) return err(UserErrors.NotFound({ userId: id })); + return ok(user); +} + +// TaskResult — when you need cancellation or context propagation +const task = attemptAsync( + async (signal) => fetch(`/api/users/${id}`, { signal }), + { mapUnknown: () => UserErrors.NotFound({ userId: id }) }, +).withContext({ layer: 'service', operation: 'getUser' }); + +const result = await task.run({ signal: controller.signal }); +``` + ### Error Boundaries Map domain errors to HTTP errors (or any other layer) with `defineBoundary`. The mapping is exhaustive — add a new domain error and the compiler tells you to add a handler. From dc6cd92b6af8cc661ae6d5342d2ea2acc3526642 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:34:21 +0100 Subject: [PATCH 16/42] docs: changelog entries for Phase 3 task-result changes --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83011ce..ad5c503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [Unreleased] + +### Breaking changes + +- `TaskResult.toPromise()` removed. Use `.run()` instead — same signature. +- TaskResult chain methods (`.map`, `.andThen`, `.catchTag`, etc.) now widen their error union to include `System.Cancelled`. If you exhaustively `match()` against a TaskResult, add a `'System.Cancelled'` handler or a `default` branch. +- `TaskResult.fromPromise` return type now widens to include `System.Unexpected`. Factory rejections are no longer unhandled — they're caught and wrapped as `System.Unexpected` with the original throw preserved as `.cause`. + +### Added + +- TaskResult chain steps check `context.signal?.aborted` between every step. Aborting the signal mid-pipeline short-circuits the remaining steps to `System.Cancelled` instead of running them to completion. +- README: "How faultline differs" section comparing faultline to neverthrow, Effect, and typed Error subclasses. +- README: "When to use TaskResult vs Promise" decision guide. +- README: explicit error-code escape hatch is now documented (the feature already existed; this fills a docs gap). + +### Tests + +- Type-level assertion that explicit `code` values on `defineErrors` definitions are preserved as literal types. +- Cancellation propagation tests for TaskResult chains (pre-abort, mid-chain abort, no abort). +- `TaskResult.fromPromise` rejection-handling tests (Error and non-Error throws). + ## 0.1.0 Initial release. From 685054930a9cd06b2c01e1396dd95a1c4caaba39 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:35:53 +0100 Subject: [PATCH 17/42] test(eslint-plugin): scaffold torture fixture directory --- .../test/torture/README.md | 32 +++++++++++++++++++ .../test/torture/tsconfig.json | 17 ++++++++++ 2 files changed, 49 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/README.md create mode 100644 packages/eslint-plugin-faultline/test/torture/tsconfig.json diff --git a/packages/eslint-plugin-faultline/test/torture/README.md b/packages/eslint-plugin-faultline/test/torture/README.md new file mode 100644 index 0000000..dcefa75 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/README.md @@ -0,0 +1,32 @@ +# Torture Fixtures + +Real-world ESLint rule stress tests. Each subdirectory exercises one pattern category against `faultline/no-raw-throw`, `faultline/throw-type-mismatch`, and `faultline/uncovered-catch`. + +## Layout + +Each category directory contains: + +- `input.ts` — the code under test +- `expected.ts` — exported expectations array, typed as `RuleExpectation[]` + +## Expectation shape + +```ts +export const expected: RuleExpectation[] = [ + { rule: 'throw-type-mismatch', line: 12, messageId: 'mismatch' }, + // ...or... + { rule: 'uncovered-catch', shouldNotReport: true, line: 20 }, +]; +``` + +The harness in `run-torture.test.ts` walks the directory, runs ESLint against each `input.ts` with the faultline plugin enabled, and asserts the produced reports match `expected.ts`. + +Surprises (rule reports a violation not in `expected.ts`) and misses (rule fails to report an expected violation) both fail the test. + +## Triage + +Findings from this corpus are triaged into: + +- **Fix** — bug in the rule, patched in this PR or a follow-up. +- **Limitation** — documented in `../../LIMITATIONS.md`. Fixture's `expected.ts` is annotated with a comment explaining why. +- **Out of scope** — fixture removed; rationale recorded in commit message. diff --git a/packages/eslint-plugin-faultline/test/torture/tsconfig.json b/packages/eslint-plugin-faultline/test/torture/tsconfig.json new file mode 100644 index 0000000..916b2bc --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "types": ["bun-types"], + "paths": { + "faultline": ["../../../faultline/src/index.ts"] + } + }, + "include": ["./**/*.ts"] +} From c13543b8110beeea8e70481089c15f8f11f866f0 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:36:24 +0100 Subject: [PATCH 18/42] test(eslint-plugin): scaffold torture harness --- .../test/torture/run-torture.test.ts | 111 ++++++++++++++++++ .../test/torture/types.ts | 12 ++ 2 files changed, 123 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/run-torture.test.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/types.ts diff --git a/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts b/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts new file mode 100644 index 0000000..75d06be --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, test } from 'bun:test'; +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { Linter } from 'eslint'; +import tsParser from '@typescript-eslint/parser'; +import faultline from '../../src/index'; +import type { RuleExpectation } from './types'; + +const TORTURE_ROOT = resolve(__dirname); + +function listCategories(): string[] { + return readdirSync(TORTURE_ROOT).filter((entry) => { + const full = join(TORTURE_ROOT, entry); + return statSync(full).isDirectory(); + }); +} + +async function loadExpectations(category: string): Promise { + const mod = await import(join(TORTURE_ROOT, category, 'expected.ts')); + return mod.expected as RuleExpectation[]; +} + +function lintFile(filePath: string) { + const linter = new Linter({ configType: 'flat' }); + return linter.verify( + readFileSync(filePath, 'utf8'), + [ + { + files: ['**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: join(TORTURE_ROOT, 'tsconfig.json'), + tsconfigRootDir: TORTURE_ROOT, + }, + }, + plugins: { faultline }, + rules: { + 'faultline/no-raw-throw': ['warn', { allowAppErrors: true }], + 'faultline/throw-type-mismatch': 'error', + 'faultline/uncovered-catch': 'error', + }, + }, + ], + filePath, + ); +} + +describe('torture fixtures', () => { + const categories = listCategories(); + + for (const category of categories) { + test(category, async () => { + const inputPath = join(TORTURE_ROOT, category, 'input.ts'); + const expectations = await loadExpectations(category); + + const messages = lintFile(inputPath); + + // Group messages by (rule, line) for easier matching + const seen = new Set(); + for (const m of messages) { + const ruleId = m.ruleId?.replace(/^faultline\//, '') ?? ''; + seen.add(`${ruleId}:${m.line}:${m.messageId ?? ''}`); + } + + const missing: RuleExpectation[] = []; + const unexpected: typeof messages = []; + + for (const exp of expectations) { + if (exp.shouldNotReport) { + const key = `${exp.rule}:${exp.line}:`; + const matched = [...seen].some((s) => s.startsWith(key)); + if (matched) { + missing.push({ ...exp, note: `expected no report, but got one${exp.note ? `: ${exp.note}` : ''}` }); + } + } else { + const key = `${exp.rule}:${exp.line}:${exp.messageId ?? ''}`; + if (!seen.has(key)) { + missing.push(exp); + } + } + } + + // Find unexpected reports + for (const m of messages) { + const ruleId = m.ruleId?.replace(/^faultline\//, '') ?? ''; + const expected = expectations.some( + (e) => !e.shouldNotReport && e.rule === ruleId && e.line === m.line && e.messageId === m.messageId, + ); + if (!expected) unexpected.push(m); + } + + if (missing.length > 0 || unexpected.length > 0) { + const lines: string[] = [`Fixture: ${category}`]; + if (missing.length > 0) { + lines.push('MISSING expected reports:'); + for (const m of missing) lines.push(` ${JSON.stringify(m)}`); + } + if (unexpected.length > 0) { + lines.push('UNEXPECTED reports:'); + for (const m of unexpected) { + lines.push(` ${m.ruleId} @ line ${m.line} messageId=${m.messageId} — ${m.message}`); + } + } + throw new Error(lines.join('\n')); + } + + expect(true).toBe(true); // marker + }); + } +}); diff --git a/packages/eslint-plugin-faultline/test/torture/types.ts b/packages/eslint-plugin-faultline/test/torture/types.ts new file mode 100644 index 0000000..adf9f3e --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/types.ts @@ -0,0 +1,12 @@ +export type RuleName = 'no-raw-throw' | 'throw-type-mismatch' | 'uncovered-catch'; + +export interface RuleExpectation { + rule: RuleName; + line: number; + /** ESLint messageId expected. Mutually exclusive with shouldNotReport. */ + messageId?: string; + /** When true, asserts that no report is generated at `line` for `rule`. */ + shouldNotReport?: boolean; + /** Optional: human-readable note for triage. */ + note?: string; +} From 58afe8aa90aec7238c491f31687f1341c4107b79 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:37:27 +0100 Subject: [PATCH 19/42] test(eslint-plugin): baseline fixture (no-report negative controls) --- .../test/torture/baselines/expected.ts | 8 ++++ .../test/torture/baselines/input.ts | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/baselines/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/baselines/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/baselines/expected.ts b/packages/eslint-plugin-faultline/test/torture/baselines/expected.ts new file mode 100644 index 0000000..aecdf4c --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/baselines/expected.ts @@ -0,0 +1,8 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 16, shouldNotReport: true, note: 'isErrorTag covers tag' }, + { rule: 'uncovered-catch', line: 27, shouldNotReport: true, note: 'narrowError covers tag' }, + { rule: 'no-raw-throw', line: 20, messageId: 'noRawThrowUseFactory', note: 're-throw of unknown after isErrorTag guard' }, + { rule: 'no-raw-throw', line: 31, messageId: 'noRawThrowUseFactory', note: 're-throw of unknown after narrowError guard' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/baselines/input.ts b/packages/eslint-plugin-faultline/test/torture/baselines/input.ts new file mode 100644 index 0000000..664313b --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/baselines/input.ts @@ -0,0 +1,45 @@ +import { defineErrors, isErrorTag, narrowError } from 'faultline'; + +const UserErrors = defineErrors('User', { + NotFound: { status: 404, message: (data: { userId: string }) => `not found ${data.userId}` }, +}); + +function getUser(id: string) { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; +} + +// 1. Properly handled with isErrorTag — should NOT report +function handled1(id: string) { + try { + return getUser(id); + } catch (e) { + if (isErrorTag(e, UserErrors.NotFound)) { + return { id: 'guest' }; + } + throw e; + } +} + +// 2. Properly handled with narrowError — should NOT report +function handled2(id: string) { + try { + return getUser(id); + } catch (e) { + const err = narrowError(e, [UserErrors]); + if (err._tag === 'User.NotFound') return { id: 'guest' }; + throw err; + } +} + +// 3. Pure sync helper, no throws — should NOT report any rule +function pure(a: number, b: number) { + return a + b; +} + +// 4. Plain promise chain, no throws — should NOT report +async function fetchAndLog(url: string) { + const res = await fetch(url); + console.log(res.status); + return res; +} From 99105aa0352113a6683bdc71ca321addb42a9b65 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:41:22 +0100 Subject: [PATCH 20/42] chore(eslint-plugin): polish torture harness ergonomics --- .../test/torture/run-torture.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts b/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts index 75d06be..49a6525 100644 --- a/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts +++ b/packages/eslint-plugin-faultline/test/torture/run-torture.test.ts @@ -10,6 +10,7 @@ const TORTURE_ROOT = resolve(__dirname); function listCategories(): string[] { return readdirSync(TORTURE_ROOT).filter((entry) => { + if (entry.startsWith('.')) return false; const full = join(TORTURE_ROOT, entry); return statSync(full).isDirectory(); }); @@ -94,7 +95,12 @@ describe('torture fixtures', () => { const lines: string[] = [`Fixture: ${category}`]; if (missing.length > 0) { lines.push('MISSING expected reports:'); - for (const m of missing) lines.push(` ${JSON.stringify(m)}`); + for (const m of missing) { + const kind = m.shouldNotReport ? 'unexpected report' : 'missing report'; + const idPart = m.messageId ? ` messageId=${m.messageId}` : ''; + const notePart = m.note ? ` — ${m.note}` : ''; + lines.push(` ${kind}: ${m.rule} @ line ${m.line}${idPart}${notePart}`); + } } if (unexpected.length > 0) { lines.push('UNEXPECTED reports:'); From 8952909d17477cc666fe1fb5106939ef29ab509f Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:43:22 +0100 Subject: [PATCH 21/42] test(eslint-plugin): re-throws torture fixture Records expected behavior. Triage results follow in Task 23. --- .../test/torture/re-throws/expected.ts | 11 ++++++ .../test/torture/re-throws/input.ts | 39 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/re-throws/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts b/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts new file mode 100644 index 0000000..15e8f67 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts @@ -0,0 +1,11 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 17, messageId: 'uncheckedCatch', note: 'bare rethrow does not constitute typed handling' }, + // no-raw-throw fires on `throw e` because e is typed `unknown` (rule does not infer narrowing through e/err) + { rule: 'no-raw-throw', line: 18, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw e even in a bare rethrow' }, + { rule: 'uncovered-catch', line: 26, messageId: 'uncheckedCatch', note: 'conditional rethrow without narrowing' }, + // no-raw-throw fires on `throw e` because e is typed `unknown` (rule does not infer narrowing through e/err) + { rule: 'no-raw-throw', line: 27, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw e in conditional rethrow' }, + { rule: 'uncovered-catch', line: 37, shouldNotReport: true, note: 'throwing a typed factory is fine' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/re-throws/input.ts b/packages/eslint-plugin-faultline/test/torture/re-throws/input.ts new file mode 100644 index 0000000..a76ac4a --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/re-throws/input.ts @@ -0,0 +1,39 @@ +import { defineErrors } from 'faultline'; + +const UserErrors = defineErrors('User', { + NotFound: { status: 404, message: (data: { userId: string }) => `not found ${data.userId}` }, + Unauthorized: { status: 401 }, +}); + +function getUser(id: string) { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; +} + +// 1. Bare re-throw — catch is uncovered (no narrowing) +function bareRethrow(id: string) { + try { + return getUser(id); + } catch (e) { + throw e; + } +} + +// 2. Conditional re-throw — partial handling, still uncovered for NotFound +function conditionalRethrow(id: string) { + try { + return getUser(id); + } catch (e) { + if (Math.random() > 0.5) throw e; + return { id: 'fallback' }; + } +} + +// 3. Wrap-and-rethrow with a known typed factory — should NOT report mismatch +function wrappedRethrow(id: string): never { + try { + return getUser(id) as never; + } catch (e) { + throw UserErrors.Unauthorized(); + } +} From cb1fa445529fc20591755403557696c617e5b62e Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:44:12 +0100 Subject: [PATCH 22/42] test(eslint-plugin): generics torture fixture (triage candidates) --- .../test/torture/generics/expected.ts | 9 +++++ .../test/torture/generics/input.ts | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/generics/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/generics/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/generics/expected.ts b/packages/eslint-plugin-faultline/test/torture/generics/expected.ts new file mode 100644 index 0000000..82dbe5b --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/generics/expected.ts @@ -0,0 +1,9 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // The plugin DOES detect throws through generic helpers — the rule reports on the catch clause line. + // The throw inside the generic lambda is detected because the rule descends into the arrow function argument. + { rule: 'uncovered-catch', line: 18, messageId: 'uncheckedCatch', note: 'rule descends into inline lambda passed to generic helper' }, + // Mapped type returns still hide throws — rule cannot trace through mapped type return + { rule: 'uncovered-catch', line: 30, shouldNotReport: true, note: 'TRIAGE: mapped return type hides throws' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/generics/input.ts b/packages/eslint-plugin-faultline/test/torture/generics/input.ts new file mode 100644 index 0000000..fece6fd --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/generics/input.ts @@ -0,0 +1,35 @@ +import { defineErrors } from 'faultline'; + +const ApiErrors = defineErrors('Api', { + Timeout: { status: 408 }, +}); + +// Generic helper — runs an arbitrary function +function runWith(fn: () => T): T { + return fn(); +} + +// 1. Calling generic helper that calls a throwing function — uncovered-catch should detect +function genericCall() { + try { + return runWith(() => { + throw ApiErrors.Timeout(); + }); + } catch (e) { + return null; + } +} + +// 2. Mapped type return — known limitation, may not detect +type MapShape = { [K in keyof T]: T[K] }; +function mappedReturn(input: T): MapShape { + throw ApiErrors.Timeout(); +} + +function callMapped() { + try { + return mappedReturn({ a: 1 }); + } catch (e) { + return null; + } +} From d4bf96aac25039f7c8f5ce7f1f3828ab217061df Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:44:45 +0100 Subject: [PATCH 23/42] test(eslint-plugin): third-party throws torture fixture --- .../test/torture/third-party/expected.ts | 10 +++++++ .../test/torture/third-party/input.ts | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/third-party/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/third-party/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/third-party/expected.ts b/packages/eslint-plugin-faultline/test/torture/third-party/expected.ts new file mode 100644 index 0000000..8b44381 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/third-party/expected.ts @@ -0,0 +1,10 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // Third-party throws have no faultline tags, so uncovered-catch correctly does not report. + { rule: 'uncovered-catch', line: 4, shouldNotReport: true, note: 'no faultline-typed throw in try' }, + { rule: 'uncovered-catch', line: 13, shouldNotReport: true, note: 'fetch is not faultline-typed' }, + { rule: 'uncovered-catch', line: 22, shouldNotReport: true, note: 'JSON.parse is not faultline-typed' }, + // no-raw-throw fires on `throw e` because e is typed `unknown` (rule does not infer narrowing through e/err) + { rule: 'no-raw-throw', line: 26, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw e even for third-party re-throws' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/third-party/input.ts b/packages/eslint-plugin-faultline/test/torture/third-party/input.ts new file mode 100644 index 0000000..e5f4731 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/third-party/input.ts @@ -0,0 +1,28 @@ +// 1. JSON.parse can throw SyntaxError — no faultline factory +function parseConfig(raw: string) { + try { + return JSON.parse(raw); + } catch (e) { + return {}; + } +} + +// 2. fetch can throw TypeError — no faultline factory +async function fetchJson(url: string) { + try { + const res = await fetch(url); + return await res.json(); + } catch (e) { + return null; + } +} + +// 3. Mixed — third-party throw + caught with no narrowing +function mixed() { + try { + const a = JSON.parse('not json'); + return a; + } catch (e) { + throw e; + } +} From da3719795c8f058701c94bc19f903ac2b646379e Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:48:55 +0100 Subject: [PATCH 24/42] test(eslint-plugin): nested-try torture fixture --- .../test/torture/nested-try/expected.ts | 6 ++++ .../test/torture/nested-try/input.ts | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/nested-try/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts b/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts new file mode 100644 index 0000000..c974b0b --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts @@ -0,0 +1,6 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 21, messageId: 'uncheckedCatch', note: 'outer catch must handle E.Outer (Inner is handled inside)' }, + { rule: 'uncovered-catch', line: 31, shouldNotReport: true, note: 'closure inside try does not propagate throws to outer catch' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts b/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts new file mode 100644 index 0000000..91899c8 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts @@ -0,0 +1,35 @@ +import { defineErrors, isErrorTag } from 'faultline'; + +const E = defineErrors('E', { + Outer: { status: 500 }, + Inner: { status: 500 }, +}); + +function outer() { throw E.Outer(); } +function inner() { throw E.Inner(); } + +// 1. Try inside try — inner catch handles Inner, outer must handle Outer +function nested() { + try { + outer(); + try { + inner(); + } catch (e) { + if (isErrorTag(e, E.Inner)) return; + } + } catch (e) { + // Should be flagged as uncovered for E.Outer (and possibly E.Inner if walkNode descends) + return; + } +} + +// 2. Try inside a function literal inside a try — outer try should NOT pick up the inner throw +function isolatedClosure() { + try { + setTimeout(() => { + try { inner(); } catch (e) {} + }, 0); + } catch (e) { + // Outer catch shouldn't even be triggered — but the rule may misattribute + } +} From ab1eefa319bb84e36819b50c08f0c49a1bc7be3e Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:49:24 +0100 Subject: [PATCH 25/42] test(eslint-plugin): await-shapes torture fixture --- .../test/torture/await-shapes/expected.ts | 9 +++++++ .../test/torture/await-shapes/input.ts | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/await-shapes/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/await-shapes/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/await-shapes/expected.ts b/packages/eslint-plugin-faultline/test/torture/await-shapes/expected.ts new file mode 100644 index 0000000..dd5280c --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/await-shapes/expected.ts @@ -0,0 +1,9 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // Likely missed by current rule — TRIAGE + { rule: 'throw-type-mismatch', line: 15, shouldNotReport: true, note: 'TRIAGE: await ternary not handled by transitive check' }, + { rule: 'throw-type-mismatch', line: 20, shouldNotReport: true, note: 'TRIAGE: await Promise.all not handled' }, + // This one SHOULD be flagged — plain await of mismatching call + { rule: 'throw-type-mismatch', line: 25, messageId: 'transitiveError', note: 'plain await of mismatched call' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/await-shapes/input.ts b/packages/eslint-plugin-faultline/test/torture/await-shapes/input.ts new file mode 100644 index 0000000..4d9d50a --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/await-shapes/input.ts @@ -0,0 +1,26 @@ +import { defineErrors, type Infer } from 'faultline'; + +const E = defineErrors('E', { + A: { status: 500 }, + B: { status: 500 }, +}); + +import type { TypedPromise } from '../../../../faultline/src/typed-promise'; + +async function thrA(): TypedPromise> { throw E.A(); } +async function thrB(): TypedPromise> { throw E.B(); } + +// 1. await ternary — transitive check may miss this +async function awaitTernary(flag: boolean): TypedPromise> { + return await (flag ? thrA() : thrB()); +} + +// 2. await Promise.all — multiple sources of throw +async function awaitAll(): TypedPromise> { + return await Promise.all([thrA(), thrB()] as const); +} + +// 3. Plain await of typed call — should be detected as mismatch (declares only E.A but calls E.B) +async function plainMismatch(): TypedPromise> { + return await thrB(); +} From c2b9a77abe147c7dc70495bd1b58e4789e578a94 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:49:45 +0100 Subject: [PATCH 26/42] test(eslint-plugin): overloads torture fixture (triage candidate) --- .../test/torture/overloads/expected.ts | 6 +++++ .../test/torture/overloads/input.ts | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/overloads/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/overloads/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts b/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts new file mode 100644 index 0000000..9db5bba --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts @@ -0,0 +1,6 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // Current rule picks callSigs[0] — likely misses one of the overloads. TRIAGE candidate. + { rule: 'uncovered-catch', line: 20, shouldNotReport: true, note: 'TRIAGE: overloads — current code only inspects signature [0]' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/overloads/input.ts b/packages/eslint-plugin-faultline/test/torture/overloads/input.ts new file mode 100644 index 0000000..7f7be9c --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/overloads/input.ts @@ -0,0 +1,23 @@ +import { defineErrors } from 'faultline'; + +const E = defineErrors('E', { + A: { status: 500 }, + B: { status: 500 }, +}); + +// Overloaded function — different return-type error signatures per overload +function ambiguous(a: string): { kind: 'A' }; +function ambiguous(a: number): { kind: 'B' }; +function ambiguous(a: string | number): { kind: 'A' | 'B' } { + if (typeof a === 'string') throw E.A(); + throw E.B(); +} + +// 1. Caller catches without narrowing — rule should detect throws from BOTH overloads +function callAmbiguous(input: string | number) { + try { + return ambiguous(input as any); + } catch (e) { + return null; + } +} From ccb8980fcae6f054eb18b1eb676b8c2d10bd41d3 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 15:51:16 +0100 Subject: [PATCH 27/42] test(eslint-plugin): re-exports, class-methods, boundaries torture fixtures --- .../test/torture/boundaries/expected.ts | 5 +++ .../test/torture/boundaries/input.ts | 20 +++++++++++ .../test/torture/class-methods/expected.ts | 7 ++++ .../test/torture/class-methods/input.ts | 35 +++++++++++++++++++ .../test/torture/re-exports/expected.ts | 7 ++++ .../test/torture/re-exports/input.ts | 21 +++++++++++ 6 files changed, 95 insertions(+) create mode 100644 packages/eslint-plugin-faultline/test/torture/boundaries/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/boundaries/input.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/class-methods/input.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/re-exports/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/re-exports/input.ts diff --git a/packages/eslint-plugin-faultline/test/torture/boundaries/expected.ts b/packages/eslint-plugin-faultline/test/torture/boundaries/expected.ts new file mode 100644 index 0000000..4fdaaf4 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/boundaries/expected.ts @@ -0,0 +1,5 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + // defineBoundary handlers return errors, they do not `throw`. No reports expected. +]; diff --git a/packages/eslint-plugin-faultline/test/torture/boundaries/input.ts b/packages/eslint-plugin-faultline/test/torture/boundaries/input.ts new file mode 100644 index 0000000..fb0c307 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/boundaries/input.ts @@ -0,0 +1,20 @@ +import { defineErrors, defineBoundary } from 'faultline'; + +const SrcErrors = defineErrors('Src', { + A: { status: 500 }, +}); + +const DstErrors = defineErrors('Dst', { + X: { status: 500 }, +}); + +// Boundary map handler — `throw` here is structurally a typed factory call, should not trip no-raw-throw +const boundary = defineBoundary({ + name: 'src-to-dst', + from: SrcErrors, + map: { + 'Src.A': () => DstErrors.X(), + }, +}); + +void boundary; diff --git a/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts b/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts new file mode 100644 index 0000000..ecd3770 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts @@ -0,0 +1,7 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 23, messageId: 'uncheckedCatch', note: 'method throws Svc.Bad (catch at line 23)' }, + // Getter access is a MemberExpression, not a CallExpression — likely missed. TRIAGE. + { rule: 'uncovered-catch', line: 32, shouldNotReport: true, note: 'TRIAGE: getter access not detected as throwing (catch at line 32)' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/class-methods/input.ts b/packages/eslint-plugin-faultline/test/torture/class-methods/input.ts new file mode 100644 index 0000000..9096368 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/class-methods/input.ts @@ -0,0 +1,35 @@ +import { defineErrors } from 'faultline'; + +const E = defineErrors('Svc', { + Bad: { status: 500 }, +}); + +class Service { + // 1. Throw in a method + doThing(): number { + throw E.Bad(); + } + + // 2. Throw in a getter + get value(): number { + throw E.Bad(); + } +} + +function callMethod() { + const s = new Service(); + try { + return s.doThing(); + } catch (e) { + return 0; + } +} + +function callGetter() { + const s = new Service(); + try { + return s.value; + } catch (e) { + return 0; + } +} diff --git a/packages/eslint-plugin-faultline/test/torture/re-exports/expected.ts b/packages/eslint-plugin-faultline/test/torture/re-exports/expected.ts new file mode 100644 index 0000000..cff22b4 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/re-exports/expected.ts @@ -0,0 +1,7 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'uncovered-catch', line: 14, shouldNotReport: true, note: 'aliased narrowError import should still cover' }, + // no-raw-throw fires on `throw err` because err is typed as unknown (rule does not infer narrowing) + { rule: 'no-raw-throw', line: 19, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw err even after narrowError guard (known limitation)' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/re-exports/input.ts b/packages/eslint-plugin-faultline/test/torture/re-exports/input.ts new file mode 100644 index 0000000..d4c63ef --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/re-exports/input.ts @@ -0,0 +1,21 @@ +// Aliased imports — does the rule recognize them? +import { narrowError as nE, defineErrors as dE } from 'faultline'; + +const Errs = dE('User', { + NotFound: { status: 404, message: (d: { id: string }) => `nf ${d.id}` }, +}); + +function get(id: string) { + if (!id) throw Errs.NotFound({ id }); + return { id }; +} + +function handled(id: string) { + try { + return get(id); + } catch (e) { + const err = nE(e, [Errs]); + if (err._tag === 'User.NotFound') return null; + throw err; + } +} From 0866b05c4c98b681b738214979145b10e6daf3ae Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:20:20 +0100 Subject: [PATCH 28/42] docs: ESLint torture triage findings --- .../notes/2026-05-22-eslint-triage.md | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/superpowers/notes/2026-05-22-eslint-triage.md diff --git a/docs/superpowers/notes/2026-05-22-eslint-triage.md b/docs/superpowers/notes/2026-05-22-eslint-triage.md new file mode 100644 index 0000000..17c4951 --- /dev/null +++ b/docs/superpowers/notes/2026-05-22-eslint-triage.md @@ -0,0 +1,194 @@ +# ESLint Plugin Torture Triage — 2026-05-22 + +## Summary + +| Category | Status | Notes | +|---|---|---| +| baselines | PASS | Negative controls all hold | +| re-throws | FAIL | `uncovered-catch` does not fire — `getUser` returns plain `{ id }`, not `TypedPromise`; rule sees no error tags in return type | +| generics | PASS | Inline arrow lambda in generic helper is correctly traced; mapped-type return correctly skipped (documented limitation) | +| third-party | PASS | No faultline-tagged return types → rule correctly silent | +| nested-try | FAIL | Same root cause as re-throws: `outer()` and `inner()` return `never` (no `TypedPromise` annotation), so `tryErrorTags` is empty and the rule short-circuits | +| await-shapes | PASS | `throw-type-mismatch` detects plain `await` mismatch; ternary and `Promise.all` shapes correctly NOT detected (documented limitations) | +| overloads | PASS w/ shouldNotReport | Rule calls `getCallSignatures()[0]` — misses second overload; fixture correctly marks this as a known miss; Group J Task 24 targets this fix | +| re-exports | PASS | Aliased `narrowError`/`defineErrors` imports still recognized | +| class-methods | FAIL | Same root cause as re-throws: `doThing(): number` returns `number`, not `TypedPromise`; `extractErrorTagsFromType(number)` returns `[]`; rule has no tag data to act on | +| boundaries | PASS | `defineBoundary` handlers return errors without `throw`; no reports, as expected | + +--- + +## Root Cause Summary + +Three fixtures fail with the **same underlying constraint**: `uncovered-catch` relies on +`extractErrorTagsFromType` to find `_tag` string-literal types in a called function's declared +return type. If the called function is typed as `void`, `never`, `number`, `{ id: string }`, or +any non-faultline concrete type, the utility returns `[]` and the rule exits at the +`if (tryErrorTags.length === 0) return;` guard (line 102 of `uncovered-catch.ts`). + +This is not a traversal bug — the `walkNode` loop correctly reaches every `CallExpression`, +and the `MemberExpression` callee path (`s.doThing()`) is handled. The gap is purely in +what `extractErrorTagsFromType` can see: it reads the declared return type, which for these +functions is a plain type with no faultline markers. + +--- + +## Findings + +### Fix list (bugs to patch in this PR) + +**1. `throw-type-mismatch` picks `callSignatures[0]` — overloaded functions miss remaining overloads** + +- The bug: `getDeclaredErrorTags` (line 21), the variable-typed function block (line 98), and + `getCalleeErrorTags` (line 131) all call `.getCallSignatures()[0]!`. For an overloaded function + with two signatures, only the first is inspected; error tags on the second signature are silently dropped. +- Fixture: `overloads/input.ts` — `ambiguous(a: string | number)` with two overload declarations; + the catch at line 20 is marked `shouldNotReport` precisely because the bug prevents detection. +- Intended fix: replace the `callSigs[0]` pattern in all three sites with a loop over `callSigs`, + collecting tags into a `Set` and returning the union. After the fix, update + `overloads/expected.ts` to expect `uncheckedCatch` at line 20. +- Estimated complexity: **small** — three identical one-liner replacements. + +**2. `throw-type-mismatch` does not report `throw someVariable` when the variable's type is + unresolvable (variable-throw blind spot)** + +- The bug: in the `ThrowStatement` handler (lines 163–174), the `else` branch (where `getThrowTag` + returned `undefined`) only reports for `NewExpression` and `Literal` argument types, not for + `Identifier` arguments. A `throw wrong` where `wrong` is an unresolved identifier inside a + `TypedPromise`-annotated function is silently skipped. +- Fixture: no Group-H fixture exercises this. Group K (Task 25) planned to add a `variable-throw` + fixture; this triage confirms the gap exists in the rule source and the fixture should be added + before the fix lands. After adding the fixture, the fix is extending the `else` condition to + include `Identifier`. +- Caveat: if the variable's type IS a known `AppError` (i.e., `getThrowTag` returns a value), + the `if (thrownTag)` branch already fires correctly. The blind spot is only when the type is + `unknown`, `Error`, or similar untagged types. +- Estimated complexity: **small** — one-line condition extension plus a new fixture. + +### Document as limitation + +**1. Functions that throw but are not annotated with `TypedPromise`/`Result` return types are + invisible to `uncovered-catch`** + +- Rationale: the rule reads declared return types at the call site. If a function's return type is + `void`, `never`, `number`, `{ id: string }`, or any plain concrete type, there are no faultline + `_tag` markers for `extractErrorTagsFromType` to find. The rule has no static data-flow analysis + to scan function bodies for throw statements. +- Affected patterns: bare `throw` functions (`function getUser() { throw Errors.NotFound(...); }`), + class methods with non-`TypedPromise` return types (`doThing(): number`), and nested-try helpers + with inferred-`never` return types. +- Workaround for users: annotate throwing functions with a `TypedPromise` return type: + ```ts + function getUser(id: string): TypedPromise<{ id: string }, Infer> { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; + } + ``` + Once annotated, `uncovered-catch` at the call site fires correctly. +- This explains the `re-throws`, `nested-try`, and `class-methods` failures. + +**2. `throw e` / `throw err` (re-throw of caught variable) is not treated as typed handling** + +- Rationale: caught variables are typed `unknown` in TypeScript. The rule cannot narrow the type + of `e` in `throw e` without full data-flow analysis. The `no-raw-throw` rule separately flags + these re-throws (since `e` is `unknown`), giving users a prompt to use `narrowError` instead. +- Workaround: replace bare re-throws with explicit narrowing: + ```ts + } catch (e) { + const err = narrowError(e, [UserErrors]); + throw err; // re-throw after narrowing — still flagged by no-raw-throw, expected + } + ``` + +**3. Generic helpers obscure callee return types** + +- Rationale: `runWith(() => { throw ApiErrors.Timeout(); })` — the return type of `runWith` + is generic `T`, resolved to the lambda's return type. The rule walks into the inline lambda + and does detect the throw (generics PASS), but only because the lambda body is syntactically + reachable. An external generic helper in a different file would be opaque to the rule. +- Workaround: annotate the generic helper's return type with a `TypedPromise` error union, or + inline the throwing logic where the try/catch is. + +**4. Getter access is not a `CallExpression` — getter throws are not detected** + +- Rationale: `s.value` (getter access) is a `MemberExpression`, not a `CallExpression`. The + `walkNode` collector only processes `CallExpression` nodes. TypeScript does resolve getter + signatures, but the ESTree visitor pattern used by the rule relies on the `CallExpression` AST + node type to trigger signature resolution. Extending the rule to handle getters would require + a separate `MemberExpression` visitor path. +- The `class-methods/expected.ts` already marks getter detection as `shouldNotReport` with a + `TRIAGE` note. +- Workaround: expose the getter as a regular method with a `TypedPromise` return type. + +**5. `await` ternary and `Promise.all` throw-source detection** + +- Rationale: `throw-type-mismatch` detects transitive errors only for direct `await callExpr()` + shapes. `await (flag ? thrA() : thrB())` and `await Promise.all([thrA(), thrB()])` are + not currently decomposed. +- Already documented in `await-shapes/expected.ts`. + +### Out of scope + +**1. Reflection-based or dynamic throws** (`eval`, `Symbol.iterator` side effects, proxy traps, + computed property access that invokes getters on unknown objects). Static analysis cannot + reason about these patterns. No workaround is appropriate. + +**2. Throws inside dynamically required/imported modules** — calls to functions loaded via + `require()` or dynamic `import()` where the module path is not statically known. TypeScript + resolves these as `any`; there are no declared signatures to inspect. + +**3. Inferred-`never` function bodies** — TypeScript correctly infers a function that always + throws as returning `never`. The rule could theoretically special-case `never` return types + and scan the function body for `throw` statements to extract tags. This is feasible but + requires full AST traversal of external function bodies at analysis time, which is expensive + and out of scope for this plan. Users should annotate with `TypedPromise` explicitly. + +--- + +## Recommendation: Pivot Group L to the `nested-try` scope bug (closure isolation) + +The original Group L plan (Task 26) targeted a "closure-descent bug in `walkNode`" — the concern +that function-literal subtrees inside a try block would have their throws attributed to the +enclosing catch. **Group H did not confirm this bug.** The `nested-try` fixture's second case +(closure isolation at line 31) passes as `shouldNotReport`, meaning the rule currently does +NOT misattribute closure throws. + +This happens by accident: `inner()` inside the closure returns `never`, so no tags are collected +and there is nothing to misattribute. If a developer writes a closure where the inner function +IS `TypedPromise`-annotated, `walkNode` WOULD incorrectly attribute those tags to the outer +catch — this is a latent bug but no fixture exercises it today. + +Meanwhile, **class-methods is an active failure** in the corpus. However, investigating the +root cause reveals it is the same design constraint as re-throws (missing `TypedPromise` +annotation), not a separate code bug. The "fix" for class-methods is a limitation document and +an `expected.ts` update — not a rule patch. + +**Recommendation for Group L (Task 26):** Pivot from "closure-descent fix" to +**"closure-descent latent bug hardening"** — add a `walkNode` skip-signal for +`FunctionExpression`/`ArrowFunctionExpression` subtrees. This prevents future false positives +when `TypedPromise`-annotated functions are used inside closures within a try block. Update +the `nested-try` fixture to include a `TypedPromise`-annotated inner function to confirm the +fix prevents misattribution. + +This is a proactive hardening, not a reactive bugfix, but it closes the latent gap before a +real user hits it and it is small scope (same estimated complexity: small). + +The `class-methods` failure should instead be resolved by: +1. Updating `class-methods/expected.ts` line 23 to `shouldNotReport: true` (limitation) +2. Documenting it in `LIMITATIONS.md` with the `TypedPromise` annotation workaround + +--- + +## Next steps + +Tasks 24–26 implement the three rule fixes in Groups J, K, and L respectively. **Task 24 +(Group J)** fixes the overload-signature bug by iterating all call signatures in +`throw-type-mismatch`. **Task 25 (Group K)** adds the `variable-throw` fixture (currently +absent from the corpus) and extends the `undeclaredThrow` condition to cover `Identifier` +arguments. **Task 26 (Group L)** is hereby pivoted from confirming an already-passing closure +case to hardening `walkNode` with an explicit function-literal skip-signal, paired with a +new `TypedPromise`-annotated inner-function sub-case in the `nested-try` fixture to exercise +and verify the protection. + +**Task 27** ships `LIMITATIONS.md` documenting patterns 1–5 above, with workarounds. +The `class-methods/expected.ts` update (line 23 → `shouldNotReport`) should land in the same +commit as Task 27 so the corpus and the limitations doc stay in sync. From 236e3a56eaa894875c03f425ac2cbaa6e058f85d Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:22:05 +0100 Subject: [PATCH 29/42] test(eslint-plugin): align failing fixtures to design constraint --- .../test/torture/class-methods/expected.ts | 7 ++++--- .../test/torture/nested-try/expected.ts | 4 +++- .../test/torture/re-throws/expected.ts | 7 +++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts b/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts index ecd3770..48a9a62 100644 --- a/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts +++ b/packages/eslint-plugin-faultline/test/torture/class-methods/expected.ts @@ -1,7 +1,8 @@ import type { RuleExpectation } from '../types'; export const expected: RuleExpectation[] = [ - { rule: 'uncovered-catch', line: 23, messageId: 'uncheckedCatch', note: 'method throws Svc.Bad (catch at line 23)' }, - // Getter access is a MemberExpression, not a CallExpression — likely missed. TRIAGE. - { rule: 'uncovered-catch', line: 32, shouldNotReport: true, note: 'TRIAGE: getter access not detected as throwing (catch at line 32)' }, + // Design constraint: uncovered-catch requires the called method/function to have a + // declared error return type. Class methods without such annotation are invisible to the rule. + { rule: 'uncovered-catch', line: 22, shouldNotReport: true, note: 'design: requires declared error return type' }, + { rule: 'uncovered-catch', line: 31, shouldNotReport: true, note: 'getter access is MemberExpression, not CallExpression' }, ]; diff --git a/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts b/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts index c974b0b..c9b5395 100644 --- a/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts +++ b/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts @@ -1,6 +1,8 @@ import type { RuleExpectation } from '../types'; export const expected: RuleExpectation[] = [ - { rule: 'uncovered-catch', line: 21, messageId: 'uncheckedCatch', note: 'outer catch must handle E.Outer (Inner is handled inside)' }, + // Design constraint: uncovered-catch requires the called function to have a declared + // error return type (TypedPromise/Result). `outer()` / `inner()` here have no such annotation. + { rule: 'uncovered-catch', line: 21, shouldNotReport: true, note: 'design: requires declared error return type' }, { rule: 'uncovered-catch', line: 31, shouldNotReport: true, note: 'closure inside try does not propagate throws to outer catch' }, ]; diff --git a/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts b/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts index 15e8f67..30798ef 100644 --- a/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts +++ b/packages/eslint-plugin-faultline/test/torture/re-throws/expected.ts @@ -1,10 +1,13 @@ import type { RuleExpectation } from '../types'; export const expected: RuleExpectation[] = [ - { rule: 'uncovered-catch', line: 17, messageId: 'uncheckedCatch', note: 'bare rethrow does not constitute typed handling' }, + // Design constraint: uncovered-catch requires the called function to have a declared + // error return type (TypedPromise/Result). `getUser` here has no such annotation, + // so the rule produces zero tags and does not report. See LIMITATIONS.md. + { rule: 'uncovered-catch', line: 17, shouldNotReport: true, note: 'design: requires declared error return type' }, // no-raw-throw fires on `throw e` because e is typed `unknown` (rule does not infer narrowing through e/err) { rule: 'no-raw-throw', line: 18, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw e even in a bare rethrow' }, - { rule: 'uncovered-catch', line: 26, messageId: 'uncheckedCatch', note: 'conditional rethrow without narrowing' }, + { rule: 'uncovered-catch', line: 26, shouldNotReport: true, note: 'design: requires declared error return type' }, // no-raw-throw fires on `throw e` because e is typed `unknown` (rule does not infer narrowing through e/err) { rule: 'no-raw-throw', line: 27, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw e in conditional rethrow' }, { rule: 'uncovered-catch', line: 37, shouldNotReport: true, note: 'throwing a typed factory is fine' }, From 8284a7d19e210b3778d17a5f611e8a809808e0c6 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:28:45 +0100 Subject: [PATCH 30/42] fix(eslint-plugin): inspect all overload signatures, not just first Replace the three `callSigs[0]!` sites in throw-type-mismatch with a union loop over all signatures so overloaded functions are fully inspected. Update the overloads fixture to use TypedPromise return types and expect a transitiveError report for the undeclared E.B error that was previously invisible. --- .../src/rules/throw-type-mismatch.ts | 30 +++++++++++++++---- .../test/torture/overloads/expected.ts | 8 +++-- .../test/torture/overloads/input.ts | 24 +++++++-------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts b/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts index 2ff67f6..8d6ccab 100644 --- a/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts +++ b/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts @@ -18,8 +18,14 @@ function getDeclaredErrorTags( const callSignatures = type.getCallSignatures(); if (callSignatures.length === 0) return []; - const returnType = checker.getReturnTypeOfSignature(callSignatures[0]!); - return extractErrorTagsFromType(checker, returnType); + const tagSet = new Set(); + for (const sig of callSignatures) { + const returnType = checker.getReturnTypeOfSignature(sig); + for (const tag of extractErrorTagsFromType(checker, returnType)) { + tagSet.add(tag); + } + } + return [...tagSet]; } /** @@ -95,8 +101,14 @@ export const throwTypeMismatch = createRule({ const varType = checker.getTypeAtLocation(varNode); const callSigs = varType.getCallSignatures(); if (callSigs.length > 0) { - const returnType = checker.getReturnTypeOfSignature(callSigs[0]!); - const tags = extractErrorTagsFromType(checker, returnType); + const tagSet = new Set(); + for (const sig of callSigs) { + const returnType = checker.getReturnTypeOfSignature(sig); + for (const tag of extractErrorTagsFromType(checker, returnType)) { + tagSet.add(tag); + } + } + const tags = [...tagSet]; if (tags.length > 0) return tags; } } @@ -128,8 +140,14 @@ export const throwTypeMismatch = createRule({ const callSigs = calleeType.getCallSignatures(); if (callSigs.length === 0) return []; - const returnType = checker.getReturnTypeOfSignature(callSigs[0]!); - return extractErrorTagsFromType(checker, returnType); + const tagSet = new Set(); + for (const sig of callSigs) { + const returnType = checker.getReturnTypeOfSignature(sig); + for (const tag of extractErrorTagsFromType(checker, returnType)) { + tagSet.add(tag); + } + } + return [...tagSet]; } return { diff --git a/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts b/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts index 9db5bba..9e1c6e3 100644 --- a/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts +++ b/packages/eslint-plugin-faultline/test/torture/overloads/expected.ts @@ -1,6 +1,10 @@ import type { RuleExpectation } from '../types'; export const expected: RuleExpectation[] = [ - // Current rule picks callSigs[0] — likely misses one of the overloads. TRIAGE candidate. - { rule: 'uncovered-catch', line: 20, shouldNotReport: true, note: 'TRIAGE: overloads — current code only inspects signature [0]' }, + { + rule: 'throw-type-mismatch', + line: 22, + messageId: 'transitiveError', + note: 'all overload signatures now inspected — E.B from overload 2 is detected as undeclared', + }, ]; diff --git a/packages/eslint-plugin-faultline/test/torture/overloads/input.ts b/packages/eslint-plugin-faultline/test/torture/overloads/input.ts index 7f7be9c..f6b3e35 100644 --- a/packages/eslint-plugin-faultline/test/torture/overloads/input.ts +++ b/packages/eslint-plugin-faultline/test/torture/overloads/input.ts @@ -1,23 +1,23 @@ -import { defineErrors } from 'faultline'; +import { defineErrors, type Infer } from 'faultline'; +import type { TypedPromise } from '../../../../faultline/src/typed-promise'; const E = defineErrors('E', { A: { status: 500 }, B: { status: 500 }, }); -// Overloaded function — different return-type error signatures per overload -function ambiguous(a: string): { kind: 'A' }; -function ambiguous(a: number): { kind: 'B' }; -function ambiguous(a: string | number): { kind: 'A' | 'B' } { +// Overloaded async function — different TypedPromise error types per overload. +// Before the fix, only signature[0] was inspected (E.A); E.B was invisible. +async function ambiguous(a: string): TypedPromise>; +async function ambiguous(a: number): TypedPromise>; +async function ambiguous(a: string | number): TypedPromise | Infer> { if (typeof a === 'string') throw E.A(); throw E.B(); } -// 1. Caller catches without narrowing — rule should detect throws from BOTH overloads -function callAmbiguous(input: string | number) { - try { - return ambiguous(input as any); - } catch (e) { - return null; - } +// Caller declares only E.A but awaits ambiguous — transitive E.B should be reported. +// Before the fix: no report (only E.A seen, which is declared). +// After the fix: transitiveError for E.B (seen from overload 2). +async function callAmbiguous(input: string | number): TypedPromise> { + return await ambiguous(input as any); } From 0c3e722e667ae3d65fdaab49fe98175ae4cf9c70 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:31:16 +0100 Subject: [PATCH 31/42] fix(eslint-plugin): report throw of untyped variable in TypedPromise scope Add Identifier to the undeclaredThrow filter in ThrowStatement handler. Previously, `throw someVar` where the variable's type was unknown/any was silently ignored even inside a TypedPromise-returning function. Now reports undeclaredThrow for Identifier throws alongside NewExpression and Literal. Adds variable-throw torture fixture demonstrating the blind spot. --- .../src/rules/throw-type-mismatch.ts | 9 +++++---- .../test/torture/variable-throw/expected.ts | 7 +++++++ .../test/torture/variable-throw/input.ts | 12 ++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin-faultline/test/torture/variable-throw/expected.ts create mode 100644 packages/eslint-plugin-faultline/test/torture/variable-throw/input.ts diff --git a/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts b/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts index 8d6ccab..a9275f3 100644 --- a/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts +++ b/packages/eslint-plugin-faultline/src/rules/throw-type-mismatch.ts @@ -175,12 +175,13 @@ export const throwTypeMismatch = createRule({ }); } } else { - // Can't determine the tag — might be `new Error()` or a variable - // Only report if the throw is a NewExpression or literal (not a call expression, - // which might be a factory we can't resolve) + // Can't determine the tag — might be `new Error()`, a variable, or a literal + // Report for NewExpression, Literal, and Identifier (variable whose type is unresolvable, + // e.g. typed as unknown/any). Exclude CallExpression — might be a factory we can't resolve. if ( node.argument.type === 'NewExpression' || - node.argument.type === 'Literal' + node.argument.type === 'Literal' || + node.argument.type === 'Identifier' ) { context.report({ node: node.argument, diff --git a/packages/eslint-plugin-faultline/test/torture/variable-throw/expected.ts b/packages/eslint-plugin-faultline/test/torture/variable-throw/expected.ts new file mode 100644 index 0000000..02abd17 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/variable-throw/expected.ts @@ -0,0 +1,7 @@ +import type { RuleExpectation } from '../types'; + +export const expected: RuleExpectation[] = [ + { rule: 'throw-type-mismatch', line: 11, messageId: 'undeclaredThrow', note: 'thrown variable typed as unknown cannot be verified — should warn' }, + // no-raw-throw fires because wrong is typed unknown (rule does not infer narrowing through variable assignment) + { rule: 'no-raw-throw', line: 11, messageId: 'noRawThrowUseFactory', note: 'expected: rule flags throw of unknown-typed variable' }, +]; diff --git a/packages/eslint-plugin-faultline/test/torture/variable-throw/input.ts b/packages/eslint-plugin-faultline/test/torture/variable-throw/input.ts new file mode 100644 index 0000000..74beba4 --- /dev/null +++ b/packages/eslint-plugin-faultline/test/torture/variable-throw/input.ts @@ -0,0 +1,12 @@ +import { defineErrors, type Infer } from 'faultline'; +import type { TypedPromise } from '../../../../faultline/src/typed-promise'; + +const E = defineErrors('E', { + A: { status: 500 }, + B: { status: 500 }, +}); + +async function declaresA(): TypedPromise> { + const wrong: unknown = E.B(); + throw wrong; +} From 3e29d5518468a6d2d0fd9045b49741201bdbb6d7 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:34:15 +0100 Subject: [PATCH 32/42] fix(eslint-plugin): don't descend into async closures when collecting try-block throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Throws inside async function literals (e.g. setTimeout callbacks, .map handlers) are wrapped in rejected promises and cannot synchronously escape to the enclosing catch block. Previously, walkNode would descend into these async closures and misattribute their TypedPromise-annotated calls as throws that the outer catch must handle. Fix: update walkNode's callback signature to support a boolean skip signal (return false halts descent), then return false when the try-block walk encounters an async FunctionExpression or ArrowFunctionExpression. Synchronous inline lambdas are intentionally NOT skipped — their throws can propagate synchronously (see the generics fixture). Adds a forward-looking torture fixture (nested-try case 3) that confirms async-closure throws are not attributed to the outer catch. --- .../src/rules/uncovered-catch.ts | 17 +++++++++++++++-- .../test/torture/nested-try/expected.ts | 2 ++ .../test/torture/nested-try/input.ts | 19 ++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts b/packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts index 7167799..8d20a9f 100644 --- a/packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts +++ b/packages/eslint-plugin-faultline/src/rules/uncovered-catch.ts @@ -16,9 +16,10 @@ const createRule = ESLintUtils.RuleCreator( function walkNode( sourceCode: { visitorKeys: Record }, node: TSESTree.Node, - callback: (n: TSESTree.Node) => void, + callback: (n: TSESTree.Node) => boolean | void, ): void { - callback(node); + const skip = callback(node); + if (skip === false) return; const keys = sourceCode.visitorKeys[node.type]; if (!keys) return; @@ -73,6 +74,18 @@ export const uncoveredCatch = createRule({ const tryErrorTags: Array<{ tag: string; functionName: string }> = []; walkNode(context.sourceCode, node.block, (astNode) => { + // Don't descend into async function literals — throws inside async closures + // are wrapped in rejected promises and cannot synchronously escape to the + // enclosing catch block (e.g., setTimeout callbacks, .map handlers, etc.). + // Synchronous closures (e.g., inline lambdas passed to generic helpers) are + // intentionally NOT skipped because their throws can propagate synchronously. + if ( + (astNode.type === 'FunctionExpression' || astNode.type === 'ArrowFunctionExpression') && + (astNode as TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression).async + ) { + return false; + } + if (astNode.type !== 'CallExpression') return; const tsNode = services.esTreeNodeToTSNodeMap.get(astNode); diff --git a/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts b/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts index c9b5395..dbf8965 100644 --- a/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts +++ b/packages/eslint-plugin-faultline/test/torture/nested-try/expected.ts @@ -5,4 +5,6 @@ export const expected: RuleExpectation[] = [ // error return type (TypedPromise/Result). `outer()` / `inner()` here have no such annotation. { rule: 'uncovered-catch', line: 21, shouldNotReport: true, note: 'design: requires declared error return type' }, { rule: 'uncovered-catch', line: 31, shouldNotReport: true, note: 'closure inside try does not propagate throws to outer catch' }, + // TypedPromise-annotated call inside a closure — closure throws must not be attributed to outer catch. + { rule: 'uncovered-catch', line: 49, shouldNotReport: true, note: 'TypedPromise call inside async closure does not escape to outer catch' }, ]; diff --git a/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts b/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts index 91899c8..2801845 100644 --- a/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts +++ b/packages/eslint-plugin-faultline/test/torture/nested-try/input.ts @@ -1,4 +1,5 @@ -import { defineErrors, isErrorTag } from 'faultline'; +import { defineErrors, isErrorTag, type Infer } from 'faultline'; +import type { TypedPromise } from '../../../../faultline/src/typed-promise'; const E = defineErrors('E', { Outer: { status: 500 }, @@ -33,3 +34,19 @@ function isolatedClosure() { // Outer catch shouldn't even be triggered — but the rule may misattribute } } + +async function throwsTypedA(): TypedPromise> { + throw E.Outer(); +} + +// 3. Try containing a closure that calls a TypedPromise-annotated function. +// The closure's throw does NOT escape to the outer catch — outer must not be flagged. +async function typedClosureInsideTry() { + try { + setTimeout(async () => { + await throwsTypedA(); + }, 0); + } catch (e) { + // The outer catch never sees throwsTypedA's errors. Rule must not flag this. + } +} From d13a54b35f833a4d1dce4f354f3cee47828af3ec Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:38:51 +0100 Subject: [PATCH 33/42] docs(eslint-plugin): publish known limitations --- README.md | 2 + .../eslint-plugin-faultline/LIMITATIONS.md | 94 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 packages/eslint-plugin-faultline/LIMITATIONS.md diff --git a/README.md b/README.md index e1b42e9..42b0ddb 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,8 @@ Three preset configs matching the adoption stages: | `faultline/uncovered-catch` | Ensure catch blocks handle all throwable error types | | `faultline/throw-type-mismatch` | Detect drift between thrown errors and TypedPromise declarations | +**Known limitations:** see [`eslint-plugin-faultline/LIMITATIONS.md`](./packages/eslint-plugin-faultline/LIMITATIONS.md) for patterns the rules can't currently detect. + ### CLI ```bash diff --git a/packages/eslint-plugin-faultline/LIMITATIONS.md b/packages/eslint-plugin-faultline/LIMITATIONS.md new file mode 100644 index 0000000..d0b5387 --- /dev/null +++ b/packages/eslint-plugin-faultline/LIMITATIONS.md @@ -0,0 +1,94 @@ +# ESLint Plugin: Known Limitations + +The `eslint-plugin-faultline` rules use type-aware analysis. Some patterns are inherently difficult or impossible to analyze statically. This document captures what the rules currently miss, so adopters know where to apply manual review. + +## `uncovered-catch` + +### Design constraint: requires declared error return types + +The rule extracts error tags from the return types of functions called inside the try block. Functions without an explicit error-typed return (e.g., `TypedPromise`, `Result`, an async function returning a Result) produce zero tags, so the rule reports nothing — even if those functions throw faultline errors at runtime. + +Affected patterns: +- Functions with implicit/inferred return types (`function getUser(id: string) { throw UserErrors.NotFound(...); }` — no `TypedPromise` annotation) +- Class methods without typed-return annotations (`doThing(): number` — `number` carries no faultline markers) +- Nested-try helpers whose return type is inferred as `never` by TypeScript (TypeScript correctly infers a function that always throws as returning `never`; `extractErrorTagsFromType(never)` returns `[]`) +- Re-throws of caught variables (`throw e`) — the catch variable is typed as `unknown`, even after `isErrorTag` / `narrowError` guards + +**Workaround:** annotate functions that throw faultline errors with their error-typed return (`TypedPromise` for async, or `Result` if you've adopted Stage 3). The rule then catches missed cases at every call site. + +```ts +// Before — invisible to uncovered-catch +function getUser(id: string) { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; +} + +// After — uncovered-catch fires at every unhandled call site +function getUser(id: string): TypedPromise<{ id: string }, Infer> { + if (!id) throw UserErrors.NotFound({ userId: id }); + return { id }; +} +``` + +### Getter access is not a CallExpression + +```ts +class Service { get value() { throw E.Bad(); return 0; } } +try { new Service().value; } catch (e) {} +``` + +The rule scans `CallExpression` nodes; getter access is a `MemberExpression`. TypeScript resolves getter signatures, but the ESTree visitor pattern used by the rule relies on `CallExpression` to trigger signature resolution. The `class-methods` torture fixture marks getter detection as `shouldNotReport` for this reason. + +**Workaround:** expose getters as regular methods with a `TypedPromise` return type annotation. + +### Generic helpers with external definitions + +Inline arrow lambdas passed to generic helpers (`runWith(() => { throw E.Foo(); })`) are detected — the rule descends into the arrow expression, and the `generics` fixture confirms this works. But if the generic helper is defined in a different file with an opaque `(fn: () => T) => T` signature, the type system cannot recover the inner throws — the helper's return type resolves to `T`, not to a faultline-typed union. + +**Workaround:** annotate the helper's return type explicitly, or refactor away from generic identity wrappers when faultline errors are involved. + +### `walkNode` does not descend into async closures + +Throws inside `setTimeout(async () => ...)` or similar deferred async callbacks happen on a future tick and don't propagate to the surrounding try. The rule skips `async` function literals to avoid misattributing those throws to the outer catch. + +Synchronous inline lambdas (e.g., `runWith(() => ...)`) ARE descended into because their throws DO propagate synchronously to the enclosing try block. + +This is intentional design, not a bug — listed here so the asymmetry is clear. + +## `throw-type-mismatch` + +### Transitive errors through `await` ternaries and `Promise.all` + +```ts +async function fn(): TypedPromise> { + return await (flag ? throwsA() : throwsB()); +} +``` + +The transitive check inspects `await callExpr()` directly. Ternaries and `Promise.all` patterns are not unwrapped — the `await-shapes` fixture marks these as `shouldNotReport`. + +**Workaround:** await each branch separately, so each `await` is a direct call. The rule then catches transitive errors from both. + +### Dynamic throws of unknown-type values + +```ts +async function declaresA(): TypedPromise> { + const wrong: unknown = E.B(); + throw wrong; // reported as undeclaredThrow after the Group K fix +} +``` + +After the Group K fix (`throw ` is now reported when the surrounding function declares a TypedPromise error and the thrown variable's type has no resolvable `_tag`), this IS detected and flagged as `undeclaredThrow`. This is no longer a limitation — but worth knowing that the variable's actual runtime value is not inspected. If `wrong` happened to carry an `E.A` error at runtime, the rule would still report it as undeclared because the declared type is `unknown`. + +## Performance + +Type-aware rules require a full TypeScript program. Very large codebases or deeply generic patterns may slow lint runs. If you see runs > 30s on a single file, consider scoping the plugin to specific directories via your ESLint `files` globs. + +## Out of scope + +- Reflection-based throws (`Reflect.construct`, proxy traps, `Symbol.iterator` side effects) +- Throws from `eval` +- Throws from dynamically required/imported modules where the module path is not statically known (`require()` with a computed path resolves to `any`; there are no declared signatures to inspect) +- Throws routed through Promises that the rule cannot trace (custom queue libraries, etc.) + +If you encounter a pattern not listed here that you believe a rule should detect, please file an issue with a minimal reproduction. From 8807a49523a27faa2c90d65f8ae49114eb2770c1 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Fri, 22 May 2026 16:38:53 +0100 Subject: [PATCH 34/42] docs: changelog entries for Phase 4 ESLint improvements --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5c503..74b93e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ - README: "When to use TaskResult vs Promise" decision guide. - README: explicit error-code escape hatch is now documented (the feature already existed; this fills a docs gap). +### Added (Phase 4 — ESLint plugin) + +- ESLint plugin: torture-fixture test corpus exercising 11 real-world patterns (baselines, re-throws, generics, third-party throws, nested-try with closure-isolation case, await shapes, overloads with TypedPromise, re-exports with aliased imports, class methods, boundary handlers, variable throws). +- ESLint plugin: `LIMITATIONS.md` documenting patterns the rules can't detect, including the design constraint that `uncovered-catch` requires declared error return types. +- ESLint plugin: `throw-type-mismatch` now inspects all overload signatures, not just the first — overloaded functions are fully checked for transitive error propagation. +- ESLint plugin: `throw-type-mismatch` reports `throw ` when the surrounding function declares a TypedPromise error type and the thrown variable's type has no resolvable `_tag`. +- ESLint plugin: `uncovered-catch` no longer misattributes throws inside async closures (e.g., `setTimeout(async () => ...)`) to the outer try's catch. + ### Tests - Type-level assertion that explicit `code` values on `defineErrors` definitions are preserved as literal types. From e1fe7d11a56c5aed694f429e8d2a76d16413db24 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Sat, 13 Jun 2026 21:46:22 +0100 Subject: [PATCH 35/42] feat(cli): make faultline-cli npm-publishable - build to dist/ via tsdown (ESM, shebang preserved, typescript external) - bin -> ./dist/index.js with #!/usr/bin/env node (was raw .ts + bun shebang) - declare typescript as a runtime dependency; drop unused faultline dep - add files, repository, homepage, author, keywords, engines, publishConfig - add README and LICENSE --- packages/faultline-cli/LICENSE | 21 ++++++++ packages/faultline-cli/README.md | 76 +++++++++++++++++++++++++++++ packages/faultline-cli/package.json | 38 +++++++++++++-- packages/faultline-cli/src/index.ts | 2 +- 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 packages/faultline-cli/LICENSE create mode 100644 packages/faultline-cli/README.md diff --git a/packages/faultline-cli/LICENSE b/packages/faultline-cli/LICENSE new file mode 100644 index 0000000..799a7cb --- /dev/null +++ b/packages/faultline-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Daniel Fry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/faultline-cli/README.md b/packages/faultline-cli/README.md new file mode 100644 index 0000000..dc4a213 --- /dev/null +++ b/packages/faultline-cli/README.md @@ -0,0 +1,76 @@ +# faultline-cli + +Command-line tooling for [faultline](https://github.com/danfry1/faultline) — inspect your error catalog, lint for raw throws, run structural diagnostics, and visualize error flow across boundaries. + +The CLI statically analyzes your TypeScript project (via the TypeScript compiler API) — no code execution, no runtime instrumentation. + +## Install + +```bash +npm install -D faultline-cli +# or run without installing +npx faultline-cli catalog . +``` + +Requires Node.js >= 18. The TypeScript compiler is bundled as a dependency, so the CLI works even in projects that don't depend on `faultline` itself. + +## Usage + +```bash +faultline [path] [--json] +``` + +`path` defaults to the current working directory. Pass `--json` to any command for machine-readable output (useful in CI). + +### `catalog` + +Lists every error defined via `defineErrors` / `defineError`, with its tag, code, and HTTP status. + +```bash +faultline catalog ./src +``` + +``` +Error Catalog +- User.NotFound | USER_NOT_FOUND | status=404 | file=errors.ts +- User.Unauthorized | USER_UNAUTHORIZED | status=401 | file=errors.ts +``` + +### `lint` + +Reports raw `throw new Error(...)` sites that should be typed error factories. Exits non-zero when violations are found, so it doubles as a CI gate. + +```bash +faultline lint ./src +``` + +> The ESLint plugin (`eslint-plugin-faultline`) is the richer, editor-integrated way to enforce this. Use the CLI `lint` when you want a quick, dependency-free check in a script. + +### `doctor` + +Structural diagnostics: duplicate tags, boundary/handler mismatches, and other catalog-consistency problems. Exits non-zero on `error`-severity findings. + +```bash +faultline doctor . --json +``` + +### `graph` + +Prints the error-flow graph — which functions throw or return which errors, and how boundaries remap them. + +```bash +faultline graph ./apps/api +``` + +## Exit codes + +| Command | Non-zero exit when | +|---|---| +| `lint` | any raw-throw violation is found | +| `doctor` | any `error`-severity diagnostic is found | +| `catalog`, `graph` | only on internal failure | +| _no command_ | usage printed, exit 1 | + +## License + +MIT © Daniel Fry diff --git a/packages/faultline-cli/package.json b/packages/faultline-cli/package.json index 7bd337c..0fc3d4d 100644 --- a/packages/faultline-cli/package.json +++ b/packages/faultline-cli/package.json @@ -4,18 +4,48 @@ "description": "CLI for Faultline — error catalog, lint, doctor, and flow graph.", "type": "module", "bin": { - "faultline": "./src/index.ts" + "faultline": "./dist/index.js" }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], "scripts": { + "build": "tsdown src/index.ts --format esm", "test": "bun test", - "typecheck": "bunx tsc --noEmit" + "typecheck": "bunx tsc --noEmit", + "prepublishOnly": "bun run typecheck && bun run test && bun run build" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "typescript", + "errors", + "error-handling", + "type-safe", + "cli", + "static-analysis", + "faultline" + ], + "author": "Daniel Fry", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/danfry1/faultline.git", + "directory": "packages/faultline-cli" + }, + "homepage": "https://danfry1.github.io/faultline", + "publishConfig": { + "access": "public" }, "dependencies": { - "faultline": "workspace:*" + "typescript": "^5.9.3" }, "devDependencies": { "@types/bun": "^1.3.10", "@types/node": "^25.5.0", - "typescript": "^5.9.3" + "tsdown": "^0.9.0" } } diff --git a/packages/faultline-cli/src/index.ts b/packages/faultline-cli/src/index.ts index 6ed984a..2cbf8a3 100644 --- a/packages/faultline-cli/src/index.ts +++ b/packages/faultline-cli/src/index.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env bun +#!/usr/bin/env node import * as path from 'node:path'; From 9725c09f232ed5810289e0dce7efaf9df9fcdc38 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Sat, 13 Jun 2026 21:46:22 +0100 Subject: [PATCH 36/42] fix(eslint-plugin): publish readiness - add README.md (was referenced by files field but missing -> broken publish) - inline meta.version from package.json at build time (was hardcoded 0.1.0, drifted from 0.2.0) - run tests in prepublishOnly (was typecheck + build only) --- packages/eslint-plugin-faultline/README.md | 86 +++++++++++++++++++ packages/eslint-plugin-faultline/package.json | 3 +- packages/eslint-plugin-faultline/src/index.ts | 3 +- .../eslint-plugin-faultline/tsconfig.json | 1 + 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin-faultline/README.md diff --git a/packages/eslint-plugin-faultline/README.md b/packages/eslint-plugin-faultline/README.md new file mode 100644 index 0000000..a88d651 --- /dev/null +++ b/packages/eslint-plugin-faultline/README.md @@ -0,0 +1,86 @@ +# eslint-plugin-faultline + +Type-aware ESLint rules for [faultline](https://github.com/danfry1/faultline) — the incremental type-safe error system for TypeScript. + +The plugin is Stage 2 of faultline's adoption path: once your errors are typed factories, these rules tell you when a `catch` block misses an error type, when a `throw` drifts from a function's declared error union, or when a raw `new Error()` should be a typed factory. + +## Install + +```bash +npm install -D eslint-plugin-faultline +``` + +The rules are **type-aware**, so they require `@typescript-eslint/parser` with type information enabled. + +```bash +npm install -D @typescript-eslint/parser typescript-eslint +``` + +## Setup (flat config) + +```js +// eslint.config.js +import tseslint from 'typescript-eslint'; +import faultline from 'eslint-plugin-faultline'; + +export default tseslint.config({ + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + projectService: true, // type-aware linting + }, + }, + // Pick one preset (see below) + ...faultline.configs.strict, +}); +``` + +## Presets + +Faultline is adopted in stages — the presets mirror that path. Start with `recommended` and tighten as you go. + +| Preset | Stage | `no-raw-throw` | `uncovered-catch` | `throw-type-mismatch` | +|---|---|---|---|---| +| `recommended` | 1 — typed factories | `warn` (allows `throw AppError`) | off | off | +| `strict` | 2 — handle errors in catches | `warn` | `warn` | `error` | +| `all` | 3 — Result types everywhere | `error` (no throws) | `error` | `error` | + +Apply a preset by spreading it into your config (`...faultline.configs.strict`), or wire rules individually: + +```js +import faultline from 'eslint-plugin-faultline'; + +export default [ + { + files: ['**/*.ts'], + plugins: { faultline }, + rules: { + 'faultline/no-raw-throw': ['warn', { allowAppErrors: true }], + 'faultline/uncovered-catch': 'warn', + 'faultline/throw-type-mismatch': 'error', + }, + }, +]; +``` + +## Rules + +### `no-raw-throw` + +Flags `throw new Error(...)` (and other raw throws) in favor of typed faultline factories. With `{ allowAppErrors: true }`, throwing an existing `AppError` factory result is allowed — only un-typed throws are reported. At Stage 3 (`all`), all throws are reported in favor of `Result`/`attempt`. + +### `uncovered-catch` + +Ensures a `catch` block accounts for every faultline error type the `try` block can produce. It extracts error tags from the error-typed return signatures of functions called inside the `try`, then checks that `narrowError` / `isErrorTag` handling covers them. Surfaces the case where a new error type is added upstream and an existing catch silently fails to handle it. + +### `throw-type-mismatch` + +Inside functions that declare an error-typed return (`TypedPromise`, `Result`), ensures every `throw` — direct or transitively propagated through awaited calls — matches the declared error union. Catches drift between what a function says it can fail with and what it actually throws. + +## Known limitations + +These rules use static type analysis, and some patterns can't be analyzed reliably (implicit return types, generic helpers, dynamic throws, getters, etc.). Each is documented — with the rationale and a workaround — in [LIMITATIONS.md](./LIMITATIONS.md). Read it before filing a false-negative report; the gap may be a documented constraint with a one-line fix on your side (usually: annotate the throwing function's error-typed return). + +## License + +MIT © Daniel Fry diff --git a/packages/eslint-plugin-faultline/package.json b/packages/eslint-plugin-faultline/package.json index 607b7de..055d563 100644 --- a/packages/eslint-plugin-faultline/package.json +++ b/packages/eslint-plugin-faultline/package.json @@ -15,8 +15,9 @@ "files": ["dist", "README.md", "LICENSE"], "scripts": { "build": "tsdown src/index.ts --format cjs --dts", + "test": "bun test", "typecheck": "bunx tsc --noEmit", - "prepublishOnly": "bun run typecheck && bun run build" + "prepublishOnly": "bun run typecheck && bun run test && bun run build" }, "publishConfig": { "access": "public" }, "sideEffects": false, diff --git a/packages/eslint-plugin-faultline/src/index.ts b/packages/eslint-plugin-faultline/src/index.ts index 728b6ac..40226da 100644 --- a/packages/eslint-plugin-faultline/src/index.ts +++ b/packages/eslint-plugin-faultline/src/index.ts @@ -1,6 +1,7 @@ import { uncoveredCatch } from './rules/uncovered-catch'; import { noRawThrow } from './rules/no-raw-throw'; import { throwTypeMismatch } from './rules/throw-type-mismatch'; +import { version } from '../package.json'; const rules = { 'uncovered-catch': uncoveredCatch, @@ -25,7 +26,7 @@ const rules = { const plugin = { meta: { name: 'eslint-plugin-faultline', - version: '0.1.0', + version, }, rules, configs: { diff --git a/packages/eslint-plugin-faultline/tsconfig.json b/packages/eslint-plugin-faultline/tsconfig.json index 7dd19b5..ce118fd 100644 --- a/packages/eslint-plugin-faultline/tsconfig.json +++ b/packages/eslint-plugin-faultline/tsconfig.json @@ -6,6 +6,7 @@ "strict": true, "noEmit": true, "skipLibCheck": true, + "resolveJsonModule": true, "lib": ["ES2022"], "declaration": true, "outDir": "dist", From b1b495e08c006d083173a447dce63748a75284b0 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Sat, 13 Jun 2026 21:46:22 +0100 Subject: [PATCH 37/42] test(core): cover Result methods, attempt, fromUnknown; add coverage gate - result.test.ts: every Ok/Err method, match/catchTag/all combinators (result.ts 47%->100% funcs) - attempt.test.ts: async mapUnknown/mapAbort, pre-abort race, happy paths - from-unknown.test.ts: full context frames, AppError passthrough, detail preservation - serialize.test.ts: standalone AppError JSON.stringify uses toJSON - bunfig.toml: enforce 90% line/function coverage gate --- packages/faultline/bunfig.toml | 3 + packages/faultline/test/attempt.test.ts | 101 ++++++++++ packages/faultline/test/from-unknown.test.ts | 71 +++++++ packages/faultline/test/result.test.ts | 195 +++++++++++++++++++ packages/faultline/test/serialize.test.ts | 21 ++ 5 files changed, 391 insertions(+) create mode 100644 packages/faultline/bunfig.toml create mode 100644 packages/faultline/test/attempt.test.ts create mode 100644 packages/faultline/test/from-unknown.test.ts create mode 100644 packages/faultline/test/result.test.ts diff --git a/packages/faultline/bunfig.toml b/packages/faultline/bunfig.toml new file mode 100644 index 0000000..8cdd9d0 --- /dev/null +++ b/packages/faultline/bunfig.toml @@ -0,0 +1,3 @@ +[test] +coverage = true +coverageThreshold = { line = 0.9, function = 0.9 } diff --git a/packages/faultline/test/attempt.test.ts b/packages/faultline/test/attempt.test.ts new file mode 100644 index 0000000..030bf74 --- /dev/null +++ b/packages/faultline/test/attempt.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, test } from 'bun:test'; + +import { attempt, attemptAsync, defineErrors, isErr, isOk } from '../src/index'; + +const NetworkErrors = defineErrors('Network', { + Down: { status: 503 }, +}); + +describe('attempt (sync)', () => { + test('returns ok for a non-throwing function', () => { + const result = attempt(() => 42); + expect(isOk(result)).toBe(true); + if (isOk(result)) expect(result.value).toBe(42); + }); + + test('custom mapUnknown routes to a domain error', () => { + const result = attempt( + () => { + throw new Error('boom'); + }, + { mapUnknown: () => NetworkErrors.Down() }, + ); + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('Network.Down'); + expect(result.error.status).toBe(503); + } + }); +}); + +describe('attemptAsync (async)', () => { + test('returns ok for a resolving function', async () => { + const result = await attemptAsync(async () => 'value').run(); + expect(isOk(result)).toBe(true); + if (isOk(result)) expect(result.value).toBe('value'); + }); + + test('custom mapUnknown routes rejections to a domain error', async () => { + const result = await attemptAsync( + async () => { + throw new Error('socket reset'); + }, + { mapUnknown: () => NetworkErrors.Down() }, + ).run(); + + expect(isErr(result)).toBe(true); + if (isErr(result)) expect(result.error._tag).toBe('Network.Down'); + }); + + test('default mapAbort produces System.Cancelled with the abort reason', async () => { + const controller = new AbortController(); + const task = attemptAsync(async (signal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal?.aborted) throw signal.reason; + return 'never'; + }); + + const promise = task.run({ signal: controller.signal }); + controller.abort('user navigated away'); + const result = await promise; + + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error._tag).toBe('System.Cancelled'); + expect(result.error.message).toContain('user navigated away'); + } + }); + + test('custom mapAbort overrides the cancellation error', async () => { + const controller = new AbortController(); + const task = attemptAsync( + async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 'never'; + }, + { mapAbort: () => NetworkErrors.Down() }, + ); + + const promise = task.run({ signal: controller.signal }); + controller.abort(); + const result = await promise; + + expect(isErr(result)).toBe(true); + if (isErr(result)) expect(result.error._tag).toBe('Network.Down'); + }); + + test('pre-aborted signal short-circuits via the abort race (fn need not cooperate)', async () => { + const controller = new AbortController(); + controller.abort('already gone'); + + // fn does not inspect the signal — the abort race must reject for us. + const task = attemptAsync(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 'never'; + }); + + const result = await task.run({ signal: controller.signal }); + expect(isErr(result)).toBe(true); + if (isErr(result)) expect(result.error._tag).toBe('System.Cancelled'); + }); +}); diff --git a/packages/faultline/test/from-unknown.test.ts b/packages/faultline/test/from-unknown.test.ts new file mode 100644 index 0000000..c5c7bf9 --- /dev/null +++ b/packages/faultline/test/from-unknown.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from 'bun:test'; + +import { defineErrors, fromUnknown, isAppError } from '../src/index'; + +const UserErrors = defineErrors('User', { + NotFound: { status: 404, message: (data: { userId: string }) => `no user ${data.userId}` }, +}); + +describe('fromUnknown', () => { + test('attaches a full context frame from all options', () => { + const error = fromUnknown('boom', { + layer: 'service', + operation: 'load-user', + component: 'UserService', + requestId: 'req-1', + traceId: 'trace-1', + meta: { attempt: 2 }, + }); + + expect(error.context).toHaveLength(1); + expect(error.context[0]).toMatchObject({ + layer: 'service', + operation: 'load-user', + component: 'UserService', + requestId: 'req-1', + traceId: 'trace-1', + meta: { attempt: 2 }, + }); + }); + + test('does not attach a context frame when no context options are given', () => { + const error = fromUnknown(new Error('bare')); + expect(error.context).toHaveLength(0); + }); + + test('adds context to an existing AppError without changing its tag', () => { + const original = UserErrors.NotFound({ userId: '42' }); + const wrapped = fromUnknown(original, { layer: 'service', operation: 'reload' }); + + expect(wrapped._tag).toBe('User.NotFound'); + expect(wrapped.data.userId).toBe('42'); + expect(wrapped.context.at(-1)).toMatchObject({ layer: 'service', operation: 'reload' }); + }); + + test('returns the same AppError instance when no context is added', () => { + const original = UserErrors.NotFound({ userId: '42' }); + expect(fromUnknown(original)).toBe(original); + }); + + test('message option overrides the derived message', () => { + const error = fromUnknown('raw detail', { message: 'friendly message' }); + expect(error.message).toBe('friendly message'); + }); + + test('non-error, non-string values are preserved as cause and detail', () => { + const thrown = { code: 7, nested: { ok: false } }; + const error = fromUnknown(thrown); + + expect(error._tag).toBe('System.Unexpected'); + expect(error.cause).toBe(thrown); + expect(error.data.detail).toBe(thrown); + expect(isAppError(thrown)).toBe(false); + }); + + test('string throws are tagged NonErrorThrown via the name field', () => { + const error = fromUnknown('plain string'); + expect(error._tag).toBe('System.Unexpected'); + expect(error.data.name).toBe('NonErrorThrown'); + expect(error.message).toBe('plain string'); + }); +}); diff --git a/packages/faultline/test/result.test.ts b/packages/faultline/test/result.test.ts new file mode 100644 index 0000000..a4bd7f6 --- /dev/null +++ b/packages/faultline/test/result.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, test } from 'bun:test'; + +import { + type Infer, + type Result, + TaskResult, + all, + catchTag, + defineErrors, + err, + isErr, + isErrTag, + isOk, + match, + ok, +} from '../src/index'; + +const E = defineErrors('Test', { + NotFound: { status: 404, message: (d: { id: string }) => `missing ${d.id}` }, + Denied: { status: 403 }, +}); + +const notFound = () => E.NotFound({ id: '1' }); + +describe('Result — Ok variant methods', () => { + test('map transforms the value', () => { + const r = ok(2).map((v) => v * 5); + expect(isOk(r) && r.value).toBe(10); + }); + + test('mapErr is a no-op on Ok', () => { + const r = ok(2).mapErr(() => notFound()); + expect(isOk(r) && r.value).toBe(2); + }); + + test('andThen chains into the next Result', () => { + const r = ok(2).andThen((v) => ok(v + 1)); + expect(isOk(r) && r.value).toBe(3); + }); + + test('catchTag is a no-op on Ok', () => { + const r = ok(2).catchTag('Test.NotFound' as never, () => ok(99)); + expect(isOk(r) && r.value).toBe(2); + }); + + test('tap runs the side effect and passes the value through', () => { + let seen = 0; + const r = ok(7).tap((v) => { seen = v; }); + expect(seen).toBe(7); + expect(isOk(r) && r.value).toBe(7); + }); + + test('tapError does not run on Ok', () => { + let ran = false; + ok(7).tapError(() => { ran = true; }); + expect(ran).toBe(false); + }); + + test('withContext is a no-op on Ok', () => { + const r = ok(7).withContext({ operation: 'x' }); + expect(isOk(r) && r.value).toBe(7); + }); + + test('unwrap returns the value; unwrapOr returns the value (not fallback)', () => { + expect(ok(7).unwrap()).toBe(7); + expect(ok(7).unwrapOr(0)).toBe(7); + }); + + test('toTask produces a TaskResult that resolves Ok', async () => { + const task = ok(7).toTask(); + expect(task).toBeInstanceOf(TaskResult); + const r = await task.run(); + expect(isOk(r) && r.value).toBe(7); + }); +}); + +describe('Result — Err variant methods', () => { + test('map is a no-op and preserves the error', () => { + const r = err(notFound()).map((v: number) => v * 2); + expect(isErr(r) && r.error._tag).toBe('Test.NotFound'); + }); + + test('mapErr transforms the error', () => { + const r = err(notFound()).mapErr(() => E.Denied()); + expect(isErr(r) && r.error._tag).toBe('Test.Denied'); + }); + + test('andThen short-circuits, keeping the error', () => { + const r = err(notFound()).andThen(() => ok(1)); + expect(isErr(r) && r.error._tag).toBe('Test.NotFound'); + }); + + test('catchTag recovers a matching tag', () => { + const r = err(notFound()).catchTag('Test.NotFound', () => ok('recovered')); + expect(isOk(r) && r.value).toBe('recovered'); + }); + + test('catchTag leaves a non-matching tag untouched', () => { + const r = err(notFound()).catchTag('Test.Denied' as never, () => ok('nope')); + expect(isErr(r) && r.error._tag).toBe('Test.NotFound'); + }); + + test('tap does not run on Err; tapError runs', () => { + let tapped = false; + let tappedErr = ''; + const r = err(notFound()) + .tap(() => { tapped = true; }) + .tapError((e) => { tappedErr = e._tag; }); + expect(tapped).toBe(false); + expect(tappedErr).toBe('Test.NotFound'); + expect(isErr(r)).toBe(true); + }); + + test('withContext appends a context frame to the error', () => { + const r = err(notFound()).withContext({ layer: 'service', operation: 'load' }); + expect(isErr(r) && r.error.context.at(-1)?.operation).toBe('load'); + }); + + test('unwrap throws the error; unwrapOr returns the fallback', () => { + expect(() => err(notFound()).unwrap()).toThrow(); + expect(err(notFound()).unwrapOr('fallback')).toBe('fallback'); + }); + + test('toTask produces a TaskResult that resolves Err', async () => { + const r = await err(notFound()).toTask().run(); + expect(isErr(r) && r.error._tag).toBe('Test.NotFound'); + }); +}); + +type TestError = Infer | Infer; + +describe('match', () => { + test('exhaustive match dispatches to the ok branch', () => { + const value: Result = ok(3); + const out = match(value, { ok: (v) => `v${v}`, 'Test.NotFound': () => 'nf', 'Test.Denied': () => 'd' }); + expect(out).toBe('v3'); + }); + + test('exhaustive match dispatches to the tag branch', () => { + const value: Result = err(notFound()); + const out = match(value, { ok: () => 'ok', 'Test.NotFound': (e) => e.data.id, 'Test.Denied': () => 'd' }); + expect(out).toBe('1'); + }); + + test('partial match falls through to the wildcard handler', () => { + const out = match(err(E.Denied()), { ok: () => 'ok', _: (e) => `wild:${e._tag}` }); + expect(out).toBe('wild:Test.Denied'); + }); + + test('match without a handler or wildcard throws MatchExhaustion', () => { + // Force a missing handler at runtime to exercise the exhaustion guard. + expect(() => match(err(notFound()), { ok: () => 'ok' } as never)).toThrow(); + }); +}); + +describe('standalone catchTag', () => { + test('recovers a matching tag', () => { + const r = catchTag(err(notFound()), 'Test.NotFound', () => ok('ok')); + expect(isOk(r) && r.value).toBe('ok'); + }); +}); + +describe('isErrTag', () => { + test('true only for an Err with the given tag', () => { + expect(isErrTag(err(notFound()), 'Test.NotFound')).toBe(true); + expect(isErrTag(err(notFound()), 'Test.Denied' as never)).toBe(false); + expect(isErrTag(ok(1), 'Test.NotFound' as never)).toBe(false); + }); +}); + +describe('all', () => { + test('empty array returns Ok with empty tuple', () => { + const r = all([]); + expect(isOk(r) && r.value).toEqual([]); + }); + + test('all-ok returns the value tuple', () => { + const r = all([ok(1), ok('two'), ok(true)] as const); + expect(isOk(r) && r.value).toEqual([1, 'two', true]); + }); + + test('any-err returns a CombinedAppError with indexed errors', () => { + const r = all([ok(1), err(notFound()), err(E.Denied())] as const); + expect(isErr(r)).toBe(true); + if (isErr(r)) { + expect(r.error._tag).toBe('System.Combined'); + expect(r.error.data.errors.map((e) => e.index)).toEqual([1, 2]); + } + }); + + test('over TaskResults resolves to a combined Result', async () => { + const r = await all([TaskResult.ok(1), TaskResult.err(notFound())] as const).run(); + expect(isErr(r) && r.error._tag).toBe('System.Combined'); + }); +}); diff --git a/packages/faultline/test/serialize.test.ts b/packages/faultline/test/serialize.test.ts index bcc6db4..0671452 100644 --- a/packages/faultline/test/serialize.test.ts +++ b/packages/faultline/test/serialize.test.ts @@ -210,6 +210,27 @@ describe('Result toJSON format', () => { }); }); +describe('AppError toJSON format', () => { + test('JSON.stringify on a standalone AppError uses toJSON (no Result wrapper)', () => { + const error = TestErrors.NotFound({ id: '7' }).withCause(new TypeError('bad')); + const json = JSON.parse(JSON.stringify(error)); + + expect(json._format).toBe('faultline'); + expect(json._version).toBe(1); + expect(json._tag).toBe('Test.NotFound'); + expect(json.data.id).toBe('7'); + // cause is recursively serialized, not dropped to [Object]/[Function] + expect(json.cause).toBeDefined(); + }); + + test('nesting an AppError inside another object still serializes via toJSON', () => { + const error = TestErrors.NotFound({ id: '9' }); + const json = JSON.parse(JSON.stringify({ wrapped: error })); + expect(json.wrapped._format).toBe('faultline'); + expect(json.wrapped._tag).toBe('Test.NotFound'); + }); +}); + describe('JSON safety', () => { test('serializeError handles circular data in AppError', () => { const data: Record = { id: '1' }; From 86492a6e3406c993ea2da558d83e04f95f1e5d2c Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Sat, 13 Jun 2026 21:50:56 +0100 Subject: [PATCH 38/42] build: adopt Changesets for versioning and releases - add @changesets/cli + .changeset config (public access, vscode ignored) - rewrite release.yml: Changesets action opens a version PR, then publishes to npm with provenance (replaces manual tag-triggered publish) - root scripts: changeset, version-packages, release (builds the 3 publishable packages then changeset publish) - commit bun.lock (was gitignored despite CI using --frozen-lockfile) - mark faultline-vscode private (ships to VS Code marketplace, not npm) - changesets capturing unreleased work: core TaskResult changes (minor), eslint-plugin rule fixes + packaging (minor), CLI publishability (minor) - root CHANGELOG now points to per-package changelogs owned by Changesets --- .changeset/README.md | 8 + .changeset/cli-node-publishable.md | 5 + .changeset/config.json | 11 + .changeset/eslint-plugin-publish-fixes.md | 16 + .changeset/taskresult-cancellation.md | 13 + .github/workflows/release.yml | 62 +- .gitignore | 1 - CHANGELOG.md | 31 +- bun.lock | 731 ++++++++++++++++++++++ package.json | 7 +- packages/faultline-vscode/package.json | 1 + 11 files changed, 816 insertions(+), 70 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/cli-node-publishable.md create mode 100644 .changeset/config.json create mode 100644 .changeset/eslint-plugin-publish-fixes.md create mode 100644 .changeset/taskresult-cancellation.md create mode 100644 bun.lock diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..654c6d4 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets). + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/.changeset/cli-node-publishable.md b/.changeset/cli-node-publishable.md new file mode 100644 index 0000000..884818f --- /dev/null +++ b/.changeset/cli-node-publishable.md @@ -0,0 +1,5 @@ +--- +"faultline-cli": minor +--- + +The CLI is now publishable to npm and runs on plain Node.js (>=18). The `bin` points at a built `dist/index.js` with a Node shebang (previously a raw `.ts` file with a `bun` shebang), `typescript` is declared as a runtime dependency, and the package ships a `files` allowlist, README, LICENSE, and full repository metadata. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..ff76529 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": ["faultline-vscode"] +} diff --git a/.changeset/eslint-plugin-publish-fixes.md b/.changeset/eslint-plugin-publish-fixes.md new file mode 100644 index 0000000..792fcff --- /dev/null +++ b/.changeset/eslint-plugin-publish-fixes.md @@ -0,0 +1,16 @@ +--- +"eslint-plugin-faultline": minor +--- + +Rule improvements and publish-readiness fixes. + +Rules: +- `throw-type-mismatch` now inspects all overload signatures, not just the first — overloaded functions are fully checked for transitive error propagation. +- `throw-type-mismatch` reports `throw ` when the surrounding function declares a `TypedPromise` error type and the thrown variable's type has no resolvable `_tag`. +- `uncovered-catch` no longer misattributes throws inside async closures (e.g. `setTimeout(async () => ...)`) to the outer try's catch. +- Published `LIMITATIONS.md` documenting patterns the rules can't detect, plus an 11-pattern torture-fixture corpus guarding the rules. + +Packaging: +- Ship the previously-missing `README.md` (it was already listed in `files`). +- Derive `meta.version` from `package.json` at build time so it can no longer drift from the published version (was hardcoded `0.1.0`). +- Run tests as part of `prepublishOnly`. diff --git a/.changeset/taskresult-cancellation.md b/.changeset/taskresult-cancellation.md new file mode 100644 index 0000000..eee3754 --- /dev/null +++ b/.changeset/taskresult-cancellation.md @@ -0,0 +1,13 @@ +--- +"faultline": minor +--- + +TaskResult cancellation, rejection safety, and API pruning. + +Breaking changes: +- `TaskResult.toPromise()` removed. Use `.run()` instead — same signature. +- TaskResult chain methods (`.map`, `.andThen`, `.catchTag`, etc.) now widen their error union to include `System.Cancelled`. Exhaustive `match()` over a TaskResult needs a `'System.Cancelled'` handler or a `_` branch. +- `TaskResult.fromPromise` return type now widens to include `System.Unexpected`. Factory rejections are no longer unhandled — they are caught and wrapped as `System.Unexpected` with the original throw preserved as `.cause`. + +Added: +- TaskResult chain steps check `context.signal?.aborted` between every step. Aborting the signal mid-pipeline short-circuits the remaining steps to `System.Cancelled` instead of running them to completion. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe577e7..5b3972c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,21 @@ name: Release +# Changesets flow: +# - A push to main with pending changesets opens (or updates) a "Version Packages" PR. +# - Merging that PR bumps versions + changelogs, then this workflow publishes to npm. on: push: - tags: ['v*', 'eslint-plugin-v*'] + branches: [main] + +concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write + pull-requests: write id-token: write jobs: - publish-core: - if: startsWith(github.ref, 'refs/tags/v') && !startsWith(github.ref, 'refs/tags/eslint-plugin-v') + release: runs-on: ubuntu-latest environment: npm steps: @@ -24,43 +29,20 @@ jobs: - run: bun install --frozen-lockfile - run: bun run typecheck - run: bun run test - - name: Build - run: bun --filter 'faultline' build - - name: Validate package exports - working-directory: packages/faultline - run: npm pack && npx @arethetypeswrong/cli ./faultline-*.tgz --ignore-rules no-resolution - - name: Publish to npm - working-directory: packages/faultline - run: npm publish --provenance --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + - name: Validate package exports (faultline) + run: | + bun --filter 'faultline' build + cd packages/faultline + npm pack + npx @arethetypeswrong/cli ./faultline-*.tgz --ignore-rules no-resolution + - name: Create Release PR or Publish + uses: changesets/action@v1 with: - generate_release_notes: true - - publish-eslint-plugin: - if: startsWith(github.ref, 'refs/tags/eslint-plugin-v') - runs-on: ubuntu-latest - environment: npm - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - run: npm i -g npm@latest - - run: bun install --frozen-lockfile - - run: bun run typecheck - - name: Build - run: bun --filter 'eslint-plugin-faultline' build - - name: Publish to npm - working-directory: packages/eslint-plugin-faultline - run: npm publish --provenance --access public + version: bun run version-packages + publish: bun run release + commit: 'chore: version packages' + title: 'chore: version packages' env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true + NPM_CONFIG_PROVENANCE: 'true' diff --git a/.gitignore b/.gitignore index e751e60..64d67ff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ node_modules dist out *.tsbuildinfo -bun.lock .claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b93e5..e9683e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,8 @@ # Changelog -## [Unreleased] - -### Breaking changes - -- `TaskResult.toPromise()` removed. Use `.run()` instead — same signature. -- TaskResult chain methods (`.map`, `.andThen`, `.catchTag`, etc.) now widen their error union to include `System.Cancelled`. If you exhaustively `match()` against a TaskResult, add a `'System.Cancelled'` handler or a `default` branch. -- `TaskResult.fromPromise` return type now widens to include `System.Unexpected`. Factory rejections are no longer unhandled — they're caught and wrapped as `System.Unexpected` with the original throw preserved as `.cause`. - -### Added - -- TaskResult chain steps check `context.signal?.aborted` between every step. Aborting the signal mid-pipeline short-circuits the remaining steps to `System.Cancelled` instead of running them to completion. -- README: "How faultline differs" section comparing faultline to neverthrow, Effect, and typed Error subclasses. -- README: "When to use TaskResult vs Promise" decision guide. -- README: explicit error-code escape hatch is now documented (the feature already existed; this fills a docs gap). - -### Added (Phase 4 — ESLint plugin) - -- ESLint plugin: torture-fixture test corpus exercising 11 real-world patterns (baselines, re-throws, generics, third-party throws, nested-try with closure-isolation case, await shapes, overloads with TypedPromise, re-exports with aliased imports, class methods, boundary handlers, variable throws). -- ESLint plugin: `LIMITATIONS.md` documenting patterns the rules can't detect, including the design constraint that `uncovered-catch` requires declared error return types. -- ESLint plugin: `throw-type-mismatch` now inspects all overload signatures, not just the first — overloaded functions are fully checked for transitive error propagation. -- ESLint plugin: `throw-type-mismatch` reports `throw ` when the surrounding function declares a TypedPromise error type and the thrown variable's type has no resolvable `_tag`. -- ESLint plugin: `uncovered-catch` no longer misattributes throws inside async closures (e.g., `setTimeout(async () => ...)`) to the outer try's catch. - -### Tests - -- Type-level assertion that explicit `code` values on `defineErrors` definitions are preserved as literal types. -- Cancellation propagation tests for TaskResult chains (pre-abort, mid-chain abort, no abort). -- `TaskResult.fromPromise` rejection-handling tests (Error and non-Error throws). +> Versioning and per-package changelogs are managed with [Changesets](https://github.com/changesets/changesets). +> Each published package keeps its own `CHANGELOG.md` (`packages/*/CHANGELOG.md`), generated at release time. +> Unreleased changes live as files under [`.changeset/`](./.changeset). This file retains the pre-Changesets history. ## 0.1.0 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..add97b9 --- /dev/null +++ b/bun.lock @@ -0,0 +1,731 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "typescript-error-system", + "devDependencies": { + "@changesets/cli": "^2.31.0", + "@types/bun": "^1.3.10", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^10.0.3", + "oxlint": "^0.16.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + }, + }, + "packages/eslint-plugin-faultline": { + "name": "eslint-plugin-faultline", + "version": "0.2.0", + "devDependencies": { + "@typescript-eslint/rule-tester": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0", + "tsdown": "^0.9.0", + "typescript": "^5.9.3", + }, + "peerDependencies": { + "@typescript-eslint/utils": ">=6.0.0", + "eslint": ">=8.0.0", + "typescript": ">=5.0.0", + }, + }, + "packages/faultline": { + "name": "faultline", + "version": "0.2.0", + "devDependencies": { + "@types/bun": "^1.3.10", + "tsdown": "^0.9.0", + "typescript": "^5.9.3", + }, + }, + "packages/faultline-cli": { + "name": "faultline-cli", + "version": "0.1.0", + "bin": { + "faultline": "./dist/index.js", + }, + "dependencies": { + "typescript": "^5.9.3", + }, + "devDependencies": { + "@types/bun": "^1.3.10", + "@types/node": "^25.5.0", + "tsdown": "^0.9.0", + }, + }, + "packages/faultline-vscode": { + "name": "faultline-vscode", + "version": "0.1.0", + "dependencies": { + "typescript": "^5.9.3", + }, + "devDependencies": { + "@types/vscode": "^1.85.0", + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@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.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + + "@changesets/cli": ["@changesets/cli@2.31.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg=="], + + "@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="], + + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.4", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg=="], + + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.16", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.10", "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g=="], + + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + + "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], + + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + + "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + + "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + + "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + + "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="], + + "@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@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=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "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.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-project/types": ["@oxc-project/types@0.66.0", "", {}, "sha512-KF5Wlo2KzQ+jmuCtrGISZoUfdHom7qHavNfPLW2KkeYJfYMGwtiia8KjwtsvNJ49qRiXImOCkPeVPd4bMlbR7w=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@9.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MVyRgP2gzJJtAowjG/cHN3VQXwNLWnY+FpOEsyvDepJki1SdAX/8XDijM1yN6ESD1kr9uhBKjGelC6h3qtT+rA=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@9.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7kV0EOFEZ3sk5Hjy4+bfA6XOQpCwbDiDkkHN4BHHyrBHsXxUR05EcEJPPL1WjItefg+9+8hrBmoK0xRoDs41+A=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@9.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6OvkEtRXrt8sJ4aVfxHRikjain9nV1clIsWtJ1J3J8NG1ZhjyJFgT00SCvqxbK+pzeWJq6XzHyTCN78ML+lY2w=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-aYpNL6o5IRAUIdoweW21TyLt54Hy/ZS9tvzNzF6ya1ckOQ8DLaGVPjGpmzxdNja9j/bbV6aIzBH7lNcBtiOTkQ=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGFW4vCfKMFEIzb9VCY0oWyyY9tR1/o+wDdNePhiUXZU4SVniRPQaZ1SJ0sUFI1k25pXZmzQmIP6cBmazi/Dew=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lxx/PibBfzqYvut2Y8N2D0Ritg9H8pKO+7NUSJb9YjR/bfk2KRmP8iaUz3zB0JhPtf/W3REs65oKpWxgflGToA=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@9.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-yD28ptS/OuNhwkpXRPNf+/FvrO7lwURLsEbRVcL1kIE0GxNJNMtKgIE4xQvtKDzkhk6ZRpLho5VSrkkF+3ARTQ=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@9.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-WBwEJdspoga2w+aly6JVZeHnxuPVuztw3fPfWrei2P6rNM5hcKxBGWKKT6zO1fPMCB4sdDkFohGKkMHVV1eryQ=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-a2z3/cbOOTUq0UTBG8f3EO/usFcdwwXnCejfXv42HmV/G8GjrT4fp5+5mVDoMByH3Ce3iVPxj1LmS6OvItKMYQ=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bHZF+WShYQWpuswB9fyxcgMIWVk4sZQT0wnwpnZgQuvGTZLkYJ1JTCXJMtaX5mIFHf69ngvawnwPIUA4Feil0g=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@9.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-I5cSgCCh5nFozGSHz+PjIOfrqW99eUszlxKLgoNNzQ1xQ2ou9ZJGzcZ94BHsM9SpyYHLtgHljmOZxCT9bgxYNA=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@9.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5IhoOpPr38YWDWRCA5kP30xlUxbIJyLAEsAK7EMyUgqygBHEYLkElaKGgS0X5jRXUQ6l5yNxuW73caogb2FYaw=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@9.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Qc40GDkaad9rZksSQr2l/V9UubigIHsW69g94Gswc2sKYB3XfJXfIfyV8WTJ67u6ZMXsZ7BH1msSC6Aen75mCg=="], + + "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P3zBMhpOQceNSys3/ZqvrjuRvcIbVzfGFN/tH34HlVkOjOmfGK1mOWjORsGAZtbgh1muXrF6mQETLzFjfYndXQ=="], + + "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-B52aeo/C3spYHcwFQ4nAbDkwbMKf0K6ncWM8GrVUgGu8PPECLBhjPCW11kPW/lt9FxwrdgVYVzPYlZ6wmJmpEA=="], + + "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-5Ir1eQrC9lvj/rR1TJVGwOR4yLgXTLmfKHIfpVH7GGSQrzK7VMUfHWX+dAsX1VutaeE8puXIqtYvf9cHLw78dw=="], + + "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zTqfPET5+hZfJ3/dMqJboKxrpXMXk+j2HVdvX0wVhW2MI7n7hwELl+In6Yu20nXuEyJkNQlWHbNPCUfpM+cBWw=="], + + "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jzz/ATUhZ8wetb4gm5GwzheZns3Qj1CZ+DIMmD8nBxQXszmTS/fqnAPpgzruyLqkXBUuUfF3pHv5f/UmuHReuQ=="], + + "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Qy2+tfglJ8yX6guC1EDAnuuzRZIXciXO9UwOewxyiahLxwuTpj/wvvZN3Cb1SA3c14zrwb2TNMZvaXS1/OS5Pg=="], + + "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tHoYgDIRhgvh+/wIrzAk3cUoj/LSSoJAdsZW9XRlaixFW/TF2puxRyaS1hRco0bcKTwotXl/eDYqZmhIfUyGRQ=="], + + "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.67.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-ZPT+1HECf7WUnotodIuS8tvSkwaiCdC2DDw8HVRmlerbS6iPYIPKyBCvkSM4RyUx0kljZtB9AciLCkEbwy5/zA=="], + + "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+E3lOHCk4EuIk6IjshBAARknAUpgH+gHTtZxCPqK4AWYA+Tls2J6C0FVM48uZ4m3rZpAq8ZszM9JZVAkOaynBQ=="], + + "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3pIIFb9g5aFrAODTQVJYitq+ONHgDJ4IYk/7pk+jsG6JpKUkURd0auUlxvriO11fFit5hdwy+wIbU4kBvyRUkg=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.16.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G7phYhlIA4ke2nW7tHLl+E5+rvdzgGA6830D+e+y1RGllT0w2ONGdKcVTj+2pXGCw6yPmCC5fDsDEn2+RPTfxg=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.16.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-P/LSOgJ6SzQ3OKEIf3HsebgokZiZ5nDuTgIL4LpNCHlkOLDu/fT8XL9pSkR5y+60v0SOxUF/+aN0Q8EmxblrCw=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.16.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-0N/ZsW+cL7ZAUvOHbzMp3iApt5b/Q81q2e9RgEzkI6gUDCJK8/blWg0se/i6y9e24WH0ZC4bcxY1+Qz4ZQ+mFw=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.16.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-MoG1SIw4RGowsOsPjm5HjRWymisRZWBea7ewMoXA5xIVQ3eqECifG0KJW0OZp96Ad8DFBEavdlNuImB2uXsMwg=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.16.12", "", { "os": "linux", "cpu": "x64" }, "sha512-STho8QdMLfn/0lqRU94tGPaYX8lGJccPbqeUcEr3eK5gZ5ZBdXmiHlvkcngXFEXksYC8/5VoJN7Vf3HsmkEskw=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.16.12", "", { "os": "linux", "cpu": "x64" }, "sha512-i7pzSoj9nCg/ZzOe8dCZeFWyRRWDylR9tIX04xRTq3G6PBLm6i9VrOdEkxbgM9+pCkRzUc0a9D7rbtCF34TQUA=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.16.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-wcxq3IBJ7ZlONlXJxQM+7EMx+LX1nkz3ZS3R0EtDM76EOZaqe8BMkW5cXVhF8jarZTZC18oKAckW4Ng9d8adBg=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@0.16.12", "", { "os": "win32", "cpu": "x64" }, "sha512-Ae1fx7wmAcMVqzS8rLINaFRpAdh29QzHh133bEYMHzfWBYyK/hLu9g4GLwC/lEIVQu9884b8qutGfdOk6Qia3w=="], + + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.8-commit.151352b", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F4bhDtV6CHBx7JMiT9xvmxkcZLHFmonfbli36RyfvgThDOAu92bis28zDTdguDY85lN/jBRKX/eOvX+T5hMkg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.8-commit.151352b", "", { "os": "darwin", "cpu": "x64" }, "sha512-8VMChhFLeD/oOAQUspFtxZaV7ctDob63w626kwvBBIHtlpY2Ohw4rsfjjtGckyrTCI/RROgZv/TVVEsG3GkgLw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.8-commit.151352b", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4W28EgaIidbWIpwB3hESMBfiOSs7LBFpJGa8JIV488qLEnTR/pqzxDEoOPobhRSJ1lJlv0vUgA8+DKBIldo2gw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.8-commit.151352b", "", { "os": "linux", "cpu": "arm" }, "sha512-1ECtyzIKlAHikR7BhS4hk7Hxw8xCH6W3S+Sb74EM0vy5AqPvWSbgLfAwagYC7gNDcMMby3I757X7qih5fIrGiw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.8-commit.151352b", "", { "os": "linux", "cpu": "arm64" }, "sha512-wU1kp8qPRUKC8N82dNs3F5+UyKRww9TUEO5dQ5mxCb0cG+y4l5rVaXpMgvL0VuQahPVvTMs577QPhJGb4iDONw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.8-commit.151352b", "", { "os": "linux", "cpu": "arm64" }, "sha512-odDjO2UtEEMAzwmLHEOKylJjQa+em1REAO9H19PA+O+lPu6evVbre5bqu8qCjEtHG1Q034LpZR86imCP2arb/w=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.8-commit.151352b", "", { "os": "linux", "cpu": "x64" }, "sha512-Ty2T67t2Oj1lg417ATRENxdk8Jkkksc/YQdCJyvkGqteHe60pSU2GGP/tLWGB+I0Ox+u387bzU/SmfmrHZk9aw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.8-commit.151352b", "", { "os": "linux", "cpu": "x64" }, "sha512-Fm1TxyeVE+gy74HM26CwbEOUndIoWAMgWkVDxYBD64tayvp5JvltpGHaqCg6x5i+X2F5XCDCItqwVlC7/mTxIw=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.8-commit.151352b", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-AEZzTyGerfkffXmtv7kFJbHWkryNeolk0Br+yhH1wZyN6Tt6aebqICDL8KNRO2iExoEWzyYS6dPxh0QmvNTfUQ=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.8-commit.151352b", "", { "os": "win32", "cpu": "arm64" }, "sha512-0lskDFKQwf5PMjl17qHAroU6oVU0Zn8NbAH/PdM9QB1emOzyFDGa20d4kESGeo3Uq7xOKXcTORJV/JwKIBORqw=="], + + "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.8-commit.151352b", "", { "os": "win32", "cpu": "ia32" }, "sha512-DfG1S0zGKnUfr95cNCmR4YPiZ/moS7Tob5eV+9r5JGeHZVWFHWwvJdR0jArj6Ty0LbBFDTVVB3iAvqRSji+l0Q=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.8-commit.151352b", "", { "os": "win32", "cpu": "x64" }, "sha512-5HZEtc8U2I1O903hXBynWtWaf+qzAFj66h5B7gOtVcvqIk+lKRVSupA85OdIvR7emrsYU25ikpfiU5Jhg9kTbQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@types/vscode": ["@types/vscode@1.110.0", "", {}, "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], + + "@typescript-eslint/rule-tester": ["@typescript-eslint/rule-tester@8.57.0", "", { "dependencies": { "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", "semver": "^7.7.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-qs4OapXmAIj3so85/20lQG1WrBSSvDE/3b42Orl3lpZkaOlNXtbfKzL+9EPaY5wSEgdlhKEpympAMFHPG9i72Q=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], + + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.0.0", "", { "peerDependencies": { "valibot": "^1.0.0" } }, "sha512-/9crJgPptVsGCL6X+JPDQyaJwkalSZ/52WuF8DiRUxJgcmpNdzYRfZ+gqMEP8W3CTVfuMWPqqvIgfwJ97f9Etw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "ast-kit": ["ast-kit@1.4.3", "", { "dependencies": { "@babel/parser": "^7.27.0", "pathe": "^2.0.3" } }, "sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dts-resolver": ["dts-resolver@1.2.0", "", { "dependencies": { "oxc-resolver": "^9.0.0", "pathe": "^2.0.3" } }, "sha512-+xNF7raXYI1E3IFB+f3JqvoKYFI8R+1Mh9mpI75yNm3F5XuiC6ErEXe2Lqh9ach+4MQ1tOefzjxulhWGVclYbg=="], + + "empathic": ["empathic@1.1.0", "", {}, "sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.0.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ=="], + + "eslint-plugin-faultline": ["eslint-plugin-faultline@workspace:packages/eslint-plugin-faultline"], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "faultline": ["faultline@workspace:packages/faultline"], + + "faultline-cli": ["faultline-cli@workspace:packages/faultline-cli"], + + "faultline-vscode": ["faultline-vscode@workspace:packages/faultline-vscode"], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "human-id": ["human-id@4.2.0", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "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-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + + "oxc-resolver": ["oxc-resolver@9.0.2", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "9.0.2", "@oxc-resolver/binding-darwin-x64": "9.0.2", "@oxc-resolver/binding-freebsd-x64": "9.0.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "9.0.2", "@oxc-resolver/binding-linux-arm64-gnu": "9.0.2", "@oxc-resolver/binding-linux-arm64-musl": "9.0.2", "@oxc-resolver/binding-linux-riscv64-gnu": "9.0.2", "@oxc-resolver/binding-linux-s390x-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-musl": "9.0.2", "@oxc-resolver/binding-wasm32-wasi": "9.0.2", "@oxc-resolver/binding-win32-arm64-msvc": "9.0.2", "@oxc-resolver/binding-win32-x64-msvc": "9.0.2" } }, "sha512-w838ygc1p7rF+7+h5vR9A+Y9Fc4imy6C3xPthCMkdFUgFvUWkmABeNB8RBDQ6+afk44Q60/UMMQ+gfDUW99fBA=="], + + "oxc-transform": ["oxc-transform@0.67.0", "", { "optionalDependencies": { "@oxc-transform/binding-darwin-arm64": "0.67.0", "@oxc-transform/binding-darwin-x64": "0.67.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.67.0", "@oxc-transform/binding-linux-arm64-gnu": "0.67.0", "@oxc-transform/binding-linux-arm64-musl": "0.67.0", "@oxc-transform/binding-linux-x64-gnu": "0.67.0", "@oxc-transform/binding-linux-x64-musl": "0.67.0", "@oxc-transform/binding-wasm32-wasi": "0.67.0", "@oxc-transform/binding-win32-arm64-msvc": "0.67.0", "@oxc-transform/binding-win32-x64-msvc": "0.67.0" } }, "sha512-QXwmpLfNrXZoHgIjEtDEf6lhwmvHouNtstNgg/UveczVIjo8VSzd5h25Ea96PoX9KzReJUY/qYa4QSNkJpZGfA=="], + + "oxlint": ["oxlint@0.16.12", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.16.12", "@oxlint/darwin-x64": "0.16.12", "@oxlint/linux-arm64-gnu": "0.16.12", "@oxlint/linux-arm64-musl": "0.16.12", "@oxlint/linux-x64-gnu": "0.16.12", "@oxlint/linux-x64-musl": "0.16.12", "@oxlint/win32-arm64": "0.16.12", "@oxlint/win32-x64": "0.16.12" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-1oN3P9bzE90zkbjLTc+uICVLwSR+eQaDaYVipS0BtmtmEd3ccQue0y7npCinb35YqKzIv1LZxhoU9nm5fgmQuw=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.0.0-beta.8-commit.151352b", "", { "dependencies": { "@oxc-project/types": "0.66.0", "@valibot/to-json-schema": "1.0.0", "ansis": "^3.17.0", "valibot": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-darwin-x64": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-freebsd-x64": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.8-commit.151352b", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.8-commit.151352b" }, "peerDependencies": { "@oxc-project/runtime": "0.66.0" }, "optionalPeers": ["@oxc-project/runtime"], "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-TCb6GVaFBk4wB0LERofFDxTO5X1/Sgahr7Yn5UA9XjuFtCwL1CyEhUHX5lUIstcMxjbkLjn2z4TAGwisr6Blvw=="], + + "rolldown-plugin-dts": ["rolldown-plugin-dts@0.9.11", "", { "dependencies": { "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "ast-kit": "^1.4.3", "debug": "^4.4.0", "dts-resolver": "^1.0.1", "get-tsconfig": "^4.10.0", "oxc-transform": "^0.67.0" }, "peerDependencies": { "rolldown": "^1.0.0-beta.7", "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-iCIRKmvPLwRV4UKSxhaBo+5wDkvc3+MFiqYYvu7sGLSohzxoDn9WEsjN3y7A6xg3aCuxHh6rlRp8xbX98r1rSg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "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=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "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@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "tsdown": ["tsdown@0.9.9", "", { "dependencies": { "ansis": "^3.17.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.2", "debug": "^4.4.0", "diff": "^7.0.0", "empathic": "^1.0.0", "hookable": "^5.5.3", "lightningcss": "^1.29.3", "rolldown": "1.0.0-beta.8-commit.151352b", "rolldown-plugin-dts": "^0.9.4", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.13", "unconfig": "^7.3.2", "unplugin-lightningcss": "^0.3.3" }, "peerDependencies": { "publint": "^0.3.0", "unplugin-unused": "^0.4.0" }, "optionalPeers": ["publint", "unplugin-unused"], "bin": { "tsdown": "dist/run.js" } }, "sha512-IIGX55rkhaPomNSVrIbA58DRBwTO4ehlDTsw20XSooGqoEZbwpunDc1dRE73wKb1rHdwwBO6NMLOcgV2n1qhpA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="], + + "unconfig": ["unconfig@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "defu": "^6.1.4", "jiti": "^2.6.1", "quansync": "^1.0.0", "unconfig-core": "7.5.0" } }, "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA=="], + + "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unplugin-lightningcss": ["unplugin-lightningcss@0.3.3", "", { "dependencies": { "lightningcss": "^1.29.3", "magic-string": "^0.30.17", "unplugin": "^2.3.2" } }, "sha512-mMNRCNIcxc/3410w7sJdXcPxn0IGZdEpq42OBDyckdGkhOeWYZCG9RkHs72TFyBsS82a4agFDOFU8VrFKF2ZvA=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "valibot": ["valibot@1.0.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-1Hc0ihzWxBar6NGeZv7fPLY0QuxFMyxwYR2sF1Blu7Wq7EnremwY2W02tit2ij2VJT8HcSkHAQqmFfl77f73Yw=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "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.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "eslint-plugin-faultline/eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "unconfig/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "eslint-plugin-faultline/eslint/@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "eslint-plugin-faultline/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "eslint-plugin-faultline/eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "eslint-plugin-faultline/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "eslint-plugin-faultline/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-plugin-faultline/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "eslint-plugin-faultline/eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "eslint-plugin-faultline/eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "eslint-plugin-faultline/eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "eslint-plugin-faultline/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "eslint-plugin-faultline/eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "eslint-plugin-faultline/eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/package.json b/package.json index 1abf720..ca00bc8 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,14 @@ "lint": "oxlint .", "lint:eslint": "eslint examples/", "format": "oxlint --fix .", - "cli": "bun run packages/faultline-cli/src/index.ts" + "cli": "bun run packages/faultline-cli/src/index.ts", + "changeset": "changeset", + "changeset:status": "changeset status --since=main", + "version-packages": "changeset version", + "release": "bun --filter 'faultline' --filter 'eslint-plugin-faultline' --filter 'faultline-cli' build && changeset publish" }, "devDependencies": { + "@changesets/cli": "^2.31.0", "@types/bun": "^1.3.10", "@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/parser": "^8.57.0", diff --git a/packages/faultline-vscode/package.json b/packages/faultline-vscode/package.json index 58e94fc..1bac8fc 100644 --- a/packages/faultline-vscode/package.json +++ b/packages/faultline-vscode/package.json @@ -3,6 +3,7 @@ "displayName": "Faultline", "description": "Type-safe error handling diagnostics for TypeScript — surfaces missing error coverage, shows throwable errors on hover, and provides quick fixes.", "version": "0.1.0", + "private": true, "publisher": "faultline", "engines": { "vscode": "^1.85.0" From 40485adf0d767b731713aa6ee7f68a7618e71ec0 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Sat, 13 Jun 2026 21:52:39 +0100 Subject: [PATCH 39/42] ci: add Node 18/20/22 compat matrix and build all publishable packages - quality job now builds faultline + eslint-plugin + cli (was faultline only) - test step enforces the coverage gate - new node-compat matrix builds artifacts and smoke-loads them under real Node 18/20/22: faultline CJS+ESM, eslint-plugin require, cli bin execution - keeps the are-the-types-wrong export validation for faultline --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e747e35..8e0581c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,5 +15,33 @@ jobs: - run: bun install --frozen-lockfile - run: bun run lint - run: bun run typecheck - - run: bun run test - - run: bun --filter 'faultline' build + - run: bun run test # enforces the coverage gate in packages/faultline/bunfig.toml + - name: Build publishable packages + run: bun --filter 'faultline' --filter 'eslint-plugin-faultline' --filter 'faultline-cli' build + - name: Validate faultline package exports + working-directory: packages/faultline + run: npm pack && npx @arethetypeswrong/cli ./faultline-*.tgz --ignore-rules no-resolution + + node-compat: + needs: quality + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - name: Build publishable packages + run: bun --filter 'faultline' --filter 'eslint-plugin-faultline' --filter 'faultline-cli' build + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Smoke-test built artifacts on Node ${{ matrix.node-version }} + run: | + set -euo pipefail + node -e "const f=require('./packages/faultline/dist/index.cjs'); if(typeof f.defineErrors!=='function') throw new Error('CJS load failed'); console.log('faultline CJS ok')" + node --input-type=module -e "const f=await import('./packages/faultline/dist/index.js'); if(typeof f.defineErrors!=='function') throw new Error('ESM load failed'); console.log('faultline ESM ok')" + node -e "const p=require('./packages/eslint-plugin-faultline/dist/index.js'); if(!p.rules||!p.rules['no-raw-throw']) throw new Error('plugin load failed'); console.log('eslint-plugin ok')" + node packages/faultline-cli/dist/index.js catalog test/fixtures/sample-app | grep -q 'User.NotFound' && echo 'faultline-cli ok' From cbe987cf7d53323f7fad44de170f62b04a753713 Mon Sep 17 00:00:00 2001 From: Daniel Fry Date: Sat, 13 Jun 2026 21:55:20 +0100 Subject: [PATCH 40/42] docs: add repo hygiene and README trust signals - README: npm/CI/license/zero-deps badges, Contributing section with package status table and dev commands, mark VS Code extension experimental - CONTRIBUTING.md: monorepo workflow, test/coverage/changeset expectations - SECURITY.md: private vulnerability reporting policy - CODE_OF_CONDUCT.md: Contributor Covenant 2.1 - issue forms (bug/feature) + config, pull request template --- .github/ISSUE_TEMPLATE/bug_report.yml | 54 +++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/feature_request.yml | 25 +++++++ .github/pull_request_template.md | 27 ++++++++ CODE_OF_CONDUCT.md | 41 ++++++++++++ CONTRIBUTING.md | 76 ++++++++++++++++++++++ README.md | 35 +++++++++- SECURITY.md | 28 ++++++++ 8 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a2c8a91 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,54 @@ +name: Bug report +description: Report a bug in a faultline package +labels: [bug] +body: + - type: dropdown + id: package + attributes: + label: Package + description: Which package is affected? + options: + - faultline + - eslint-plugin-faultline + - faultline-cli + - faultline-vscode + - not sure + validations: + required: true + - type: input + id: version + attributes: + label: Package version + placeholder: e.g. 0.2.0 + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug and what you expected instead. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Minimal reproduction + description: The smallest code snippet (or repo link) that reproduces the issue. For ESLint rules, include the source and your ESLint config. + render: ts + validations: + required: true + - type: input + id: env + attributes: + label: Environment + description: Node/Bun version, OS, TypeScript version, ESLint version (if relevant). + placeholder: e.g. Node 20.11, macOS 14, TypeScript 5.9 + validations: + required: false + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: For an ESLint false negative/positive, I checked LIMITATIONS.md first. + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..88e4b91 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/danfry1/faultline/security/advisories/new + about: Please report security issues privately, not as public issues. See SECURITY.md. + - name: Question / discussion + url: https://github.com/danfry1/faultline/discussions + about: Ask questions and discuss ideas here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..318d621 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,25 @@ +name: Feature request +description: Suggest an idea or improvement +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What problem are you trying to solve? What's the use case? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What would you like to happen? API sketches welcome. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you've thought about, including existing workarounds. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..39f6f2f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ + + +## Summary + + + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Docs / internal only (no changeset needed) + +## Affected packages + +- [ ] `faultline` +- [ ] `eslint-plugin-faultline` +- [ ] `faultline-cli` +- [ ] `faultline-vscode` + +## Checklist + +- [ ] `bun run typecheck` passes +- [ ] `bun run test` passes (new code paths are covered) +- [ ] `bun run lint` passes +- [ ] Added a changeset (`bun run changeset`) for user-facing changes +- [ ] Updated docs / `LIMITATIONS.md` / type assertions where relevant diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d9e9615 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,41 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes +- Focusing on what is best for the overall community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the maintainers responsible for enforcement via the contact on the maintainer's GitHub profile. All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e2e3456 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing to faultline + +Thanks for your interest in improving faultline! This document covers the workflow for the monorepo. + +## Prerequisites + +- [Bun](https://bun.sh) (the version is pinned in `package.json` under `packageManager`) +- Node.js >= 18 (the published packages target Node 18+; CI verifies on 18/20/22) + +## Getting started + +```bash +git clone https://github.com/danfry1/faultline.git +cd faultline +bun install +``` + +## Repository layout + +| Package | Published as | Status | +|---|---|---| +| `packages/faultline` | `faultline` | stable | +| `packages/eslint-plugin-faultline` | `eslint-plugin-faultline` | stable | +| `packages/faultline-cli` | `faultline-cli` | beta | +| `packages/faultline-vscode` | — (Marketplace, not npm) | experimental | + +`examples/`, `playground/`, and `test/fixtures/` are development-only and are not published. + +## Development commands + +Run from the repo root: + +```bash +bun run typecheck # tsc --noEmit across every package +bun run test # all tests; packages/faultline enforces a coverage gate +bun run lint # oxlint +bun run build # build all packages +``` + +Per-package: `bun --filter ''