Skip to content

fix(mcp): make i18n:false delete and meta cleanup safe#86

Merged
ABB65 merged 1 commit into
mainfrom
fix/mcp-i18n-false-meta-safety
Jul 16, 2026
Merged

fix(mcp): make i18n:false delete and meta cleanup safe#86
ABB65 merged 1 commit into
mainfrom
fix/mcp-i18n-false-meta-safety

Conversation

@ABB65

@ABB65 ABB65 commented Jul 16, 2026

Copy link
Copy Markdown
Member

What & why

Two bugs a project hit while cleaning up an i18n: false collection (reported against @contentrain/[email protected]), plus a source-hygiene fix found along the way.

Base branch note: this was originally cut from fix/field-constraints, but that branch has since merged (PR #84) and 2.0.0 released from main, so it now targets main directly. The commit rebases cleanly — the diff is exactly the 13 files below.

Issue 1 — content_delete(locale=<non-default>) on i18n: false was destructive

On a non-i18n model, content is locale-agnostic (data.json) with a single meta record pinned to the default locale. Passing a non-default locale silently mapped onto data.json and the default-locale meta, so the call:

  • emptied the shared data.json (deleted the real, published entry),
  • deleted the default-locale meta (e.g. tr.json),
  • left the meta for the locale actually named (e.g. en.json) untouched,

and reported a files_removed path (.../en.json#<id>) that never existed — the opposite of the request.

Fix: planContentDelete (and the legacy deleteContent) now reject a locale-scoped delete on an i18n: false model with a clear error. Omit locale to delete the entry.

Issue 2 — validate fix:true would not remove the stray meta it warns about

The "Meta layout mismatch" warning had no remediation path, so fixed stayed 0. checkStrayNonI18nMeta now consolidates deterministically and never decides a status:

  • default-locale meta present → the strays are redundant, so prune them (the published record stays authoritative — no status is merged or downgraded);
  • default-locale meta absent, one stray → migrate it to the default path so the record is preserved;
  • several strays, no default → ambiguous, so leave the warning for the author to resolve.

Subtle interaction handled: the orphan-content pass fabricates a draft default-locale meta for content lacking meta at the default locale. Consolidation now runs before that pass and gates its fabrication while a stray is unresolved — otherwise a lone published stray would be shadowed by a fabricated draft and then deleted on a later run. The remediation is idempotent.

Issue 3 (found while investigating) — raw NUL bytes in validator/project.ts

The document validator used two literal NUL bytes as a Map-key separator, which made file/grep/diff classify the source as binary (and silently broke the first grep of this investigation). Replaced with a unicode escape — identical at runtime, plain-text source.

Tests & gates

  • New: planContentDelete guard (non-i18n reject / i18n allow / locale-free allow), legacy deleteContent guard, disk-backed validate fix:true stray-meta cases (prune, migrate, ambiguous no-op, idempotency).
  • tsc --noEmit: 0 errors · oxlint on all changed files: 0 warnings.
  • Targeted + validator suites pass; contentrain_content_delete tool test passes in isolation (17/17).
  • The full git-heavy suite was not run to green here — those files (branch-lifecycle, bulk, identity, …) time out under load from serial-git slowness, unrelated to this change (bulk delete_entries passes no locale, so the guard never fires).

Docs, rules, and skills updated (docs/packages/mcp.md, rules/shared/mcp-usage.md, contentrain-validate-fix skill + workflow, mcp-tools.md, tool description string).

Two bugs on i18n:false collections, plus a source-hygiene fix.

content_delete: passing a non-default `locale` to an i18n:false model was
destructive. The locale mapped onto data.json and the default-locale meta,
so it emptied the shared content and deleted the wrong meta while the named
locale's stray meta survived — the opposite of the request. planContentDelete
and the legacy deleteContent now reject a locale-scoped delete on an
i18n:false model with a clear error (omit locale; content is locale-agnostic).

validate fix:true: the "Meta layout mismatch" warning had no remediation, so
fixed stayed 0. checkStrayNonI18nMeta now consolidates deterministically —
prune redundant strays when the default-locale meta is authoritative, migrate
a lone stray to the default path when no default exists, and leave ambiguous
multi-stray cases for a human. It runs before the orphan-content pass and
gates that pass's draft fabrication, so a real published record is never
replaced by a fabricated draft and then deleted on a later run.

validator/project.ts held two raw NUL bytes (a Map-key separator) that made
tools classify the file as binary and broke grep/diff. Replaced with a
unicode escape: identical runtime, plain-text source.

Tests: planContentDelete guard (non-i18n reject / i18n allow), legacy delete
guard, and disk-backed validate fix:true stray-meta cases (prune, migrate,
ambiguous no-op, idempotency). Docs, rules, and skills updated.
@netlify

netlify Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy Preview for contentrain-ai ready!

Name Link
🔨 Latest commit a0d5bfe
🔍 Latest deploy log https://app.netlify.com/projects/contentrain-ai/deploys/6a5948018dac750008adf961
😎 Deploy Preview https://deploy-preview-86--contentrain-ai.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@ABB65
ABB65 merged commit 846d7a3 into main Jul 16, 2026
6 checks passed
@ABB65
ABB65 deleted the fix/mcp-i18n-false-meta-safety branch July 16, 2026 21:10
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant