Skip to content

Phase 14b: serve backend — meta watcher, watcher errors, new routes, defensive Zod#40

Merged
ABB65 merged 1 commit intonext-mcpfrom
fix/serve-backend-zod-watchers-routes
Apr 17, 2026
Merged

Phase 14b: serve backend — meta watcher, watcher errors, new routes, defensive Zod#40
ABB65 merged 1 commit intonext-mcpfrom
fix/serve-backend-zod-watchers-routes

Conversation

@ABB65
Copy link
Copy Markdown
Member

@ABB65 ABB65 commented Apr 17, 2026

Summary

  • Watchers.contentrain/meta/ now surfaced via new meta:changed WS event; chokidar 'error' no longer silently swallowed (new file-watch:error WS event with message + timestamp).
  • New routesGET /api/describe-format (wraps contentrain_describe_format); GET /api/preview/merge?branch=cr/... (side-effect-free FF / conflict / already-merged preview before approve).
  • Defensive Zod/api/normalize/plan/reject validates an optional { reason? } body; whole serve write surface now parses through one parseOrThrow() gate.

Details

meta:changed

Covers 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/*.json file left the Serve UI rendering stale metadata until a full refresh.

file-watch:error

chokidar's error event 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/merge

Returns { alreadyMerged, canFastForward, conflicts, filesChanged, stat }. Three signals:

  • alreadyMergedmerge-base --is-ancestor <branch> <CONTENTRAIN_BRANCH> succeeds (approve would be a no-op)
  • canFastForwardmerge-base --is-ancestor <CONTENTRAIN_BRANCH> <branch> succeeds (FF possible)
  • conflicts — best-effort list from git merge-tree. [] when clean; null when 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 — no contentrain_doctor MCP 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:regenerated WS eventcontentrain generate runs outside serve's process so the watcher can't observe it cleanly. Needs a different hook; defer until the design is concrete.

Test plan

  • oxlint across cli src + tests → 0 warnings on 209 files
  • contentrain typecheck → 0 errors
  • contentrain vitest → 137/137 (was 130 on next-mcp); 7 new tests covering:
    • meta:changed with and without entryId
    • file-watch:error payload shape (message + ISO timestamp)
    • /api/describe-format invokes the correct MCP tool
    • /api/preview/merge rejects non-cr branches with 400
    • /api/preview/merge happy path: FF-possible, no conflicts, correct filesChanged
    • plan/reject accepts { reason? } body AND empty body AND rejects malformed

No MCP or tool surface changes

Pure serve-backend pass on existing tools.

🤖 Generated with Claude Code

…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>
@ABB65 ABB65 merged commit 3cf7f33 into next-mcp Apr 17, 2026
1 check passed
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.

1 participant