feat: graceful entry/error screen for chant precondition failures (#72) - #81
Merged
Conversation
Classify chant's own stderr in the shell-out layer (lint gate / not-
installed / generic eval — the tier-needs-creds case generalizes further
in server.ts) and return a structured {error, code, remedy} from every
graph/facet route instead of an opaque 500. web/app.js renders a readable
entry/error card with the remedy, replacing the old tier-only tierNote
special case with the same treatment for every classified failure. Happy
path is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #72
Summary
behold shells the served project's own
chantbinary — once users open their own chant projects (not just the bundled, always-lint-clean Loom demo), precondition failures start surfacing routinely, and previously degraded to an opaque HTTP 500 or a raw stack trace instead of telling the user what's wrong and how to fix it.This classifies chant's own stderr in the shell-out layer, returns a structured
{error, code, remedy}from every graph/facet route, and renders a readable entry/error card in the SPA instead of a blank canvas.Error taxonomy + remedies
Classified in
src/chant.ts'sclassifyChantFailure()off chant's own stderr text (chant exits 1 for every failure alike, so the message is the only signal):lintRefusing to emit graph: source has lint errors...(chant's owngraph.tslint gate)chant lintin the project to see the errors, fix them, then reload.not-installedCannot find package '...'/Cannot find module '...'(Node's own resolution error — behold fell back to its own pinned chant, missing the project's lexicons)npm install && chant typegenin the project directory, then reload.evaltier?tier=was picked (server.ts generalizes the old one-offtierErrorNote/tierNote)not-installedfailure keeps its own code even under a picked tier — that's unrelated to which tier was asked for.ANSI colour codes are stripped before classification and before the message reaches the JSON/UI — chant's own
formatErrorcolours output by default even for a piped, non-interactive spawn.How I tested each class
Against
example/(installed, lint-clean) and a temporary uninstalled copy, exercised through the realcreateApp()HTTP layer (not mocks) end-to-end:GET /api/graphon the clean, installed example project → 200, full IR + SVG, unchanged.example/src/network.ts, reverted after → 500,{code: "lint", remedy: "Run \chant lint`..."}`.example/withoutnode_modules→ behold's bin resolution falls back to its own pinned chant, which can't resolve the project's@intentius/chant-lexicon-aws→ 500,{code: "not-installed", remedy: "Run \npm install && chant typegen`..."}`.?tier=production→{code: "tier", remedy: "...pick a different tier..."};not-installedunder a picked tier still reportsnot-installed, nottier.Unit tests in
src/chant.test.tsfeedclassifyChantFailure()the actual stderr strings captured from the real runs above (lint gate, missing-package, and a generic thrown-error case), plusChantCliError/runChantJsonwiring tests (spawn-mocked, matching the file's existing pattern).src/server.test.tsadds HTTP-level tests for/api/graphand/api/overlaycovering all four codes, mocking at thenode:child_processspawnlayer (not the exported async function) so the realgraphIr/runChantJson/ChantCliErrorchain runs — mocking a plainPromise.reject(...)in place of an exported function tripped Node's unhandled-rejection tracking as a false positive through Hono's own internal promise chaining.Gate
npm run tsc— cleannpm test— 218/218 passing (28 files)npm run build— clean,dist/cli.js104.9kbFiles changed
src/chant.ts—classifyChantFailure,stripAnsi,ChantCliError(carries.failure);runChantJson/ciPipelinenow throwChantCliErrorinstead of a plainError.src/server.ts—errorResponsegeneralized to return{error, code, remedy}for every classified failure;tierErrorNotereplaced bytierFailure()(same narration, now part of the shared taxonomy);/api/overlay's catch now goes through the sameerrorResponse(it's actually where a tier/creds failure usually surfaces in practice, since the SPA routes an env pick there, not/api/graph).web/app.js—renderTierNote→renderPreconditionError, handling all four codes;load()'s error branch andloadReconcile()'s error message updated to the new shape. Two small, separated regions touched — should minimize overlap with Add a ⌘K command palette and thin the on-screen chrome (port spicypath pattern) #73's command-palette work in the same file.web/index.html—.tier-noteCSS replaced with.precondition-error/-title/-message/-remedy.Anticipated conflicts
src/chant.ts/src/server.ts— no known concurrent PRs touch these; Open your own project: remove auto-Loom defaulting from the CLI #69 (CLI Loom defaulting), Source deploy tiers from chant.config.ts instead of hardcoded LOOM_TIER #70 (tier config source), and Honor chant.config.ts sourceDir/stacks in graph resolution (graphPath) #71 (graphPath) are explicitly out of scope here and untouched.web/app.js— Add a ⌘K command palette and thin the on-screen chrome (port spicypath pattern) #73 adds a command palette to the same file. This PR's edits are confined toloadReconcile()'s one-line error message and therenderTierNote/load()region; a normal 3-way merge should apply cleanly unless Add a ⌘K command palette and thin the on-screen chrome (port spicypath pattern) #73 also touches those specific lines.🤖 Generated with Claude Code