feat: NavigationError class + public-export tidy#5
Conversation
…exports - Add NavigationError (extends Error) in src/error-handling.ts and throw it from runtime-metadata.ts and navigate.ts wherever an unknown link name or missing navigable metadata was previously throwing plain Error. Gives callers a precise `instanceof NavigationError` catch path while keeping `instanceof Error` compatibility for broad-catch code. - Export NavigationError from src/index.ts. - Remove LinkedResource from src/index.ts public exports (no external callers in test/ or examples/). - Add ApiDefinition, RootNavigable, and NavigationError to the AGENTS.md public-API list. - Fix docs/how-it-works.md:254 (TypeError -> NavigationError) and docs/roadmap.md §8 (link-extraction.ts -> runtime-metadata.ts; remove the stale TypeError claim; mark NavigationError entry as DONE). - Update CHANGELOG under [Unreleased]. Addresses FINDING-02, FINDING-04, Quick Fix 2, Quick Fix 12 from code review. Implements entry-point of roadmap §8. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feffef
left a comment
There was a problem hiding this comment.
Opus Reviewer (on behalf of @feffef): LGTM — no blocking concerns.
Thorough, well-scoped implementation of Approach C for FINDING-02 plus the FINDING-04 public-surface tidy. The blast radius is as expected but nothing leaked beyond it.
Verified
NavigationError class (src/error-handling.ts:20-25)
- Extends
Error,this.name = 'NavigationError'set correctly — serialization andinstanceofboth work. - Single-arg constructor is ES2020-compatible (correctly drops
ErrorOptions, which is ES2022).tsconfig.jsontarget ises2020, confirmed. - JSDoc
@publicmatches thesrc/index.tsexport.
Throw-site conversions (exactly the 3 requested, no more)
src/runtime-metadata.ts:175— named-link miss:Error→NavigationError, message preserved verbatim.src/runtime-metadata.ts:190— "No links are available":Error→NavigationError, message preserved verbatim.src/navigate.ts:200— metadata-not-found:Error→NavigationError, message preserved verbatim.- Invariant "Internal library bug" throws at
runtime-metadata.ts:163,:198,:275,:313,:325correctly remain plainError— these are library bugs, not navigation errors. Good discipline.
LinkedResource removal safety
src/index.ts:39no longer exportsLinkedResource;src/index.ts:23-25exportsNavigationError.LinkedResourcestill exists insrc/type-system.ts:379(internal), correctly imported from./type-system(not./index) inapi-client.ts,error-handling.ts,navigate.ts. Internal boundaries preserved.- Zero references in
test/orexamples/— confirmed viagit grep.
AGENTS.md:9 — public API list now includes ApiDefinition, RootNavigable, NavigationError; drops LinkedResource. "Deliberately minimal" framing still reads cleanly.
docs/how-it-works.md:254 — TypeError → NavigationError; instanceof Error aside added correctly (Quick Fix 12 landed).
docs/roadmap.md §8 — link-extraction.ts → runtime-metadata.ts (Quick Fix 2); no longer claims the code "currently throws TypeError"; NavigationError entry marked DONE with file/site references; HypermediaError/ValidationError/ConfigurationError correctly still "(still open)"; constraint updated to reference remaining subclasses.
CHANGELOG.md — correctly structured under [Unreleased]:
Added:NavigationErrorexport.Changed: throw-class change, notesinstanceof Errorcontinuity.Removed:LinkedResource, correctly prefixed**BREAKING**:with migration sentence (Resource<L['Target'], L['Api']>).
Tests (test/integration/runtime-guards.spec.ts:363-413)
- New
NavigationError class contractdescribe block present. - Named-link-miss test asserts
instanceof NavigationError,instanceof Error,name === 'NavigationError', and message shape. Dual-instance check included as requested. - Metadata-not-found test mirrors the same dual-instance pattern.
- Pre-existing tests at lines 124, 140, 156 still use regex/string matchers against
rejects.toThrow(...)— these pass for anyErrorsubclass, no regression. No tests asserted exact-class constructor ore.name === 'Error', so no updates were required.
Scope cleanliness — 9 files, 109 insertions, 19 deletions. No collateral refactors, no rename creep. Nothing touching PR #3's scope (how-it-works.md section renumbering, roadmap §1 map names).
CI — all green: Typecheck, Build, Tests, Coverage, Playwright (1m33s) all passed. Agent's claims of 393/393 jest, 100% coverage, 82/82 playwright are verified via the CI jobs.
Nit (non-blocking)
- The PR body's "Breaking changes" section correctly notes that
Error → NavigationErroris breaking for code asserting exact class but not forinstanceof Errorcode. The CHANGELOGChangedentry omits the**BREAKING**:prefix. Arguably correct (it's a subclass,instanceof Errorstill works), and matches how roadmap §8 framed the "breaking change fromErrortoNavigationError" as informational rather than a contract break. Not worth changing.
Verdict
Ship it. This is the cleanest of the batch — surgical class swap, invariant throws correctly preserved, test contract pinned on both instanceof axes, docs synchronized with code, and CHANGELOG accurate. Directly lands the entry-point of roadmap §8 without over-reaching into the remaining subclasses.
(GitHub blocks self-approval on PRs authored by @feffef — this comment is the functional LGTM.)
…lans - Rewrite housekeeping item #3's DONE blurb to describe the actual landed architecture (single fetchResource + per-builder inlined formatting + failureToError boundary converter). The previous text referenced fetchSafe/fetchProne/formatErrorMessage — none of which exist in src/. They were symbol names from an intermediate refactor plan that was superseded before landing. - Delete docs/pipeline-refactor-plan.md and docs/pipeline-refactor-plan-alt.md. Both carry "Proposed — awaiting review" headers despite describing superseded designs. They are the root cause of the housekeeping drift. - Drop dangling references to missing docs/integration-test-split.md in housekeeping items #5 and #7. - Reorder housekeeping items into numerical sequence (no content changes). Addresses FINDING-05 from code review plus Quick Fixes 3, 13, 14. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Findings
FINDING-02 | WARNING | Style / Documentation + Architecture
Location:
src/runtime-metadata.ts:~174,src/navigate.ts:~200,docs/how-it-works.md:254,docs/roadmap.md §8Docs claimed
navigatethrowsTypeError; the code always threw plainError. Consumers writingcatch (e) { if (e instanceof TypeError) ... }following the documented contract would never enter the branch.FINDING-04 | WARNING | Architecture / Documentation
Location:
AGENTS.md:9vssrc/index.ts:10,35,36AGENTS.md listed a "deliberately minimal" public surface that did not match reality:
ApiDefinition,RootNavigable, andLinkedResourcewere all exported but not listed.LinkedResourcehad zero external callers, making it a candidate for removal; the other two are load-bearing for type inference (return type oflinkTo, linked-resource type used bynavigateconsumers).Concrete impact: (1) Consumers catching
TypeErrorper the docs silently miss link-not-found errors. (2) The minimal-public-surface promise in AGENTS.md was misleading, and a type that never stabilized was frozen into the public API because nobody noticed it was exported.Approach chosen
NavigationError extends Errorrather than either fixing docs alone (A) or switching toTypeError(B). ATypeErroris semantically wrong — the argument is the right type (a string), just the wrong value.NavigationErrordirectly delivers the entry-point of roadmap §8's typed error hierarchy and is the smallest precise catch class consumers could ask for.LinkedResource(pre-release project, zero external callers), promoteApiDefinitionandRootNavigablein AGENTS.md, and now alsoNavigationError. Makes the "deliberately minimal" claim true.Alternatives considered
Error. Rejected because consumers are left with no discriminable catch class for this specific programming error, and the roadmap already plans typed error hierarchy.TypeError): Match the old docs by switching toTypeError. Rejected on semantics (TypeErroris for type-mismatch, not value-mismatch) and because it contradicts the roadmap's plan (NavigationError extends Error, notextends TypeError).LinkedResourcespecifically — committing to a public API surface for a type with no external callers is gratuitous.@remarksonLinkedResource): Same as A but add a JSDoc markingLinkedResourceas "advanced/potentially removable." Rejected because marking something as removable while keeping it public is worst-of-both-worlds; act or don't.Bundled quick fixes
docs/roadmap.md §8link-extraction.ts→runtime-metadata.ts(link-extraction lives inside runtime-metadata.ts).docs/how-it-works.md:254TypeError→NavigationError.Test plan
npx jest— all 393 tests pass. Existing tests that assertedErrorfor link-not-found still pass (NavigationError extends Error); new assertions pin the subclass contract.test/integration/runtime-guards.spec.ts(NavigationError class contractdescribe block):navigatewith unknown link throwsNavigationErrorANDinstanceof Errorstill holds; navigate on non-navigable object throwsNavigationError.npm run test:coverage— 100% statements/branches/functions/lines across all files includingerror-handling.ts.npm run typecheck— passes.npm run test:playwright— all 82 Playwright tests pass.Breaking changes
LinkedResourcetype no longer exported fromsrc/index.ts. The library is pre-release (see AGENTS.md public-API note) and has no known external callers. Migration: inlineResource<L['Target'], L['Api']>.navigateandlinkTonow throwNavigationError(extendsError) instead ofErrorfor unknown-link errors. Not strictly breaking forinstanceof Error/ broad-catch code; is breaking for anyone asserting the exact class.Both changes documented in
CHANGELOG.mdunder[Unreleased].Review checklist