Skip to content

feat: graceful entry/error screen for chant precondition failures (#72) - #81

Merged
lex00 merged 1 commit into
mainfrom
feat/72-precondition-errors
Jul 22, 2026
Merged

feat: graceful entry/error screen for chant precondition failures (#72)#81
lex00 merged 1 commit into
mainfrom
feat/72-precondition-errors

Conversation

@lex00

@lex00 lex00 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Closes #72

Summary

behold shells the served project's own chant binary — 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's classifyChantFailure() off chant's own stderr text (chant exits 1 for every failure alike, so the message is the only signal):

code trigger (chant's own wording) remedy
lint Refusing to emit graph: source has lint errors... (chant's own graph.ts lint gate) Run chant lint in the project to see the errors, fix them, then reload.
not-installed Cannot find package '...' / Cannot find module '...' (Node's own resolution error — behold fell back to its own pinned chant, missing the project's lexicons) Run npm install && chant typegen in the project directory, then reload.
eval anything else non-zero Check the project's chant source and environment — see the message above for chant's own error.
tier any of the above while a non-default ?tier= was picked (server.ts generalizes the old one-off tierErrorNote/tierNote) Pick a different tier, or provide the credentials/parameters that tier needs. A not-installed failure 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 formatError colours 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 real createApp() HTTP layer (not mocks) end-to-end:

  • Happy pathGET /api/graph on the clean, installed example project → 200, full IR + SVG, unchanged.
  • Lint gate — appended an EVL003 (dynamic-property-access) violation to example/src/network.ts, reverted after → 500, {code: "lint", remedy: "Run \chant lint`..."}`.
  • Not installed — copied example/ without node_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/creds — unit + route tests simulate a lint/eval failure under ?tier=production{code: "tier", remedy: "...pick a different tier..."}; not-installed under a picked tier still reports not-installed, not tier.

Unit tests in src/chant.test.ts feed classifyChantFailure() the actual stderr strings captured from the real runs above (lint gate, missing-package, and a generic thrown-error case), plus ChantCliError/runChantJson wiring tests (spawn-mocked, matching the file's existing pattern). src/server.test.ts adds HTTP-level tests for /api/graph and /api/overlay covering all four codes, mocking at the node:child_process spawn layer (not the exported async function) so the real graphIr/runChantJson/ChantCliError chain runs — mocking a plain Promise.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 — clean
  • npm test — 218/218 passing (28 files)
  • npm run build — clean, dist/cli.js 104.9kb

Files changed

  • src/chant.tsclassifyChantFailure, stripAnsi, ChantCliError (carries .failure); runChantJson/ciPipeline now throw ChantCliError instead of a plain Error.
  • src/server.tserrorResponse generalized to return {error, code, remedy} for every classified failure; tierErrorNote replaced by tierFailure() (same narration, now part of the shared taxonomy); /api/overlay's catch now goes through the same errorResponse (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.jsrenderTierNoterenderPreconditionError, handling all four codes; load()'s error branch and loadReconcile()'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-note CSS replaced with .precondition-error/-title/-message/-remedy.

Anticipated conflicts

🤖 Generated with Claude Code

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>
@lex00
lex00 merged commit e8bdd66 into main Jul 22, 2026
1 check passed
@lex00
lex00 deleted the feat/72-precondition-errors branch July 22, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Graceful entry/error screen for precondition failures (lint gate, not installed, creds)

1 participant