Conversation
…w routes, defensive Zod
Second pass on `contentrain serve` after Phase 13's auth + drift fixes.
Tight, surgical — additive routes and events, no behaviour regressions.
### File watcher coverage
- `.contentrain/meta/` now recognised: new `meta:changed` WS event
fires with `modelId`, optional `entryId`, `locale`. Covers both
per-model SEO layout (`meta/<model>/<locale>.json`) and per-entry
SEO layout (`meta/<model>/<entry>/<locale>.json`). Previously
editing a meta file left the Serve UI rendering stale metadata
until a full refresh.
- chokidar `'error'` handler was previously unhandled. Now broadcasts
`file-watch:error` with `message` + ISO `timestamp`. The UI can
render "watcher down, live updates paused" instead of silently
degrading (e.g. OS inotify limit on Linux).
### New HTTP routes
- `GET /api/describe-format` — thin wrapper around the
`contentrain_describe_format` MCP tool. The Serve UI's model
inspector can render the content-format spec as a reference panel.
- `GET /api/preview/merge?branch=cr/...` — side-effect-free preview
before approve. Returns `alreadyMerged`, `canFastForward`,
`conflicts` (best-effort via `git merge-tree`; `null` when unable
to run), `filesChanged`, `stat`. Complements the approve route,
which continues to surface runtime conflicts by throwing.
### Defensive Zod parity
- `/api/normalize/plan/reject` now parses an optional `{ reason? }`
body through a new `NormalizePlanRejectBodySchema`. Empty-body and
reason-only requests still work (backwards compatible); malformed
bodies return structured 400. Whole serve write surface now goes
through one `parseOrThrow()` gate.
### Explicitly out of scope
- `/api/doctor` — no `contentrain_doctor` MCP tool exists; only the
CLI's 540-line command. Rather than duplicate CLI logic into
serve, defer until doctor is extracted into a reusable MCP tool.
- `sdk:regenerated` WS event — `contentrain generate` runs outside
serve's process so the watcher can't observe it. Needs a different
hook; defer until the design is concrete.
### Verification
- `oxlint` cli src + tests → 0 warnings on 209 files.
- `contentrain` typecheck → 0 errors.
- `contentrain` vitest → **137/137** (was 130 on next-mcp). 7 new
tests cover `meta:changed` (with / without entryId),
`file-watch:error` payload, `/api/describe-format` tool call,
`/api/preview/merge` validation + happy FF path, plan/reject body
validation + backwards compat.
No MCP changes. No tool surface changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Summary
.contentrain/meta/now surfaced via newmeta:changedWS event; chokidar'error'no longer silently swallowed (newfile-watch:errorWS event with message + timestamp).GET /api/describe-format(wrapscontentrain_describe_format);GET /api/preview/merge?branch=cr/...(side-effect-free FF / conflict / already-merged preview before approve)./api/normalize/plan/rejectvalidates an optional{ reason? }body; whole serve write surface now parses through oneparseOrThrow()gate.Details
meta:changedCovers both real layouts agents produce:
meta/<model>/<locale>.json→{ type: 'meta:changed', modelId, locale }meta/<model>/<entry>/<locale>.json→{ type: 'meta:changed', modelId, entryId, locale }Previously editing any
.contentrain/meta/*.jsonfile left the Serve UI rendering stale metadata until a full refresh.file-watch:errorchokidar's
errorevent was unhandled. Now broadcasts{ type: 'file-watch:error', message, timestamp }. The UI can render a "watcher down, live updates paused" banner instead of silently degrading (e.g. hitting the OS inotify limit on Linux)./api/preview/mergeReturns
{ alreadyMerged, canFastForward, conflicts, filesChanged, stat }. Three signals:alreadyMerged—merge-base --is-ancestor <branch> <CONTENTRAIN_BRANCH>succeeds (approve would be a no-op)canFastForward—merge-base --is-ancestor <CONTENTRAIN_BRANCH> <branch>succeeds (FF possible)conflicts— best-effort list fromgit merge-tree.[]when clean;nullwhen the check couldn't run (older git, missing refs)Does NOT run the real merge. Complements the approve route, which continues to surface runtime conflicts by throwing.
Out of scope (explicit)
/api/doctor— nocontentrain_doctorMCP tool exists; only the CLI's 540-line command. Rather than duplicate CLI logic into serve, we defer until doctor is extracted into a reusable MCP tool.sdk:regeneratedWS event —contentrain generateruns outside serve's process so the watcher can't observe it cleanly. Needs a different hook; defer until the design is concrete.Test plan
oxlintacross cli src + tests → 0 warnings on 209 filescontentraintypecheck → 0 errorscontentrainvitest → 137/137 (was 130 on next-mcp); 7 new tests covering:meta:changedwith and without entryIdfile-watch:errorpayload shape (message + ISO timestamp)/api/describe-formatinvokes the correct MCP tool/api/preview/mergerejects non-cr branches with 400/api/preview/mergehappy path: FF-possible, no conflicts, correct filesChanged{ reason? }body AND empty body AND rejects malformedNo MCP or tool surface changes
Pure serve-backend pass on existing tools.
🤖 Generated with Claude Code