diff --git a/.changeset/phase-r1-rules-skills-mcp-parity.md b/.changeset/phase-r1-rules-skills-mcp-parity.md new file mode 100644 index 0000000..59a4b13 --- /dev/null +++ b/.changeset/phase-r1-rules-skills-mcp-parity.md @@ -0,0 +1,86 @@ +--- +"@contentrain/mcp": minor +"@contentrain/rules": minor +"@contentrain/skills": minor +--- + +fix(rules,skills,mcp): align rules/skills catalogs with MCP tool surface + `cr/*` branches, lock with parity tests + +Closes the two P1 drift findings and installs a drift-detection +mechanism so they don't come back: + +1. **Missing `contentrain_merge`** — `@contentrain/rules` public + `MCP_TOOLS` listed 15 tools. `@contentrain/mcp` registers 17 + (including `merge` and the new `doctor`). `@contentrain/skills` + tool reference also jumped from `submit` straight to `bulk`. +2. **Legacy `contentrain/{operation}/...` branch namespace** — + MCP's `buildBranchName()` returns `cr/...` (Phase 7 migration) + and `checkBranchHealth` filters on `cr/`, but essential rules, + review/normalize prompts, and multiple skills still taught the + old prefix. Agents following the shipped guidance would look + for branches that don't exist. + +### `@contentrain/mcp` + +- New public export `TOOL_NAMES: readonly string[]` in + `./tools/annotations`, frozen and derived from `TOOL_ANNOTATIONS`. + Single source of truth — parity tests in sibling packages now + import this instead of hardcoding. +- New `./tools/annotations` subpath export in `package.json`. +- Build script now emits the new subpath. + +### `@contentrain/rules` + +- `MCP_TOOLS` extended to **17 tools** (`contentrain_merge`, + `contentrain_doctor` added in catalog order). +- `essential/contentrain-essentials.md` — tool table gains `doctor` + row; feature-branch pattern rewritten to `cr/{operation}/...`; + health-threshold language mentions `cr/*`. +- `prompts/review-mode.md` — every legacy `contentrain//...` + reference → `cr//...` (pattern + type examples). +- `prompts/normalize-mode.md` — branch pattern table rewritten. +- `shared/workflow-rules.md` — branch pattern spec rewritten. +- `tests/mcp-parity.test.ts` (new) — 4 tests: + - `MCP_TOOLS` ↔ `TOOL_NAMES` exact match + - Essential guardrails mention every MCP tool + - `buildBranchName()` output starts with `cr/` (sampled across scopes) + - Rules docs do not contain the legacy `contentrain//...` + branch prefix (false-positive filter excludes `.contentrain/` paths) +- `package.json` — `@contentrain/mcp: workspace:*` added as devDep + for the parity test. + +### `@contentrain/skills` + +- `skills/contentrain/references/mcp-tools.md` — new sections for + `contentrain_merge` (after submit) and `contentrain_doctor` + (new Doctor Tools subsection). Submit description updated to + `cr/*` branches. +- `skills/contentrain/references/mcp-pipelines.md` + `workflow.md` + — branch-naming spec + examples rewritten to `cr/*`. +- `skills/contentrain-normalize/SKILL.md` + `references/extraction.md` + + `references/reuse.md` — 4 stale `contentrain/normalize/*` + references → `cr/normalize/*`. +- `skills/contentrain-translate/SKILL.md` — translate branch pattern + updated. +- `tests/mcp-parity.test.ts` (new) — 2 tests: + - `references/mcp-tools.md` has an `### ` heading for every + MCP tool + - 7 key skill docs do not contain the legacy branch prefix +- `package.json` — `@contentrain/mcp: workspace:*` devDep. + +### Monorepo + +- `tsconfig.json` paths — `@contentrain/mcp/tools/*` alias added so + TypeScript + vitest resolve the new subpath from source during dev. + +### Verification + +- `oxlint` across rules + skills + mcp/tools → 0 warnings. +- `tsc --noEmit` across rules, skills, mcp → 0 errors. +- `@contentrain/rules` vitest → 16/16 (was 12 — 4 new parity tests). +- `@contentrain/skills` vitest → 85/85 (was 83 — 2 new parity tests). + +### Tool surface + +No MCP tool behaviour changes. The new `TOOL_NAMES` export is +additive; everything else is documentation + tests. diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 0b099be..fd2bb38 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -116,6 +116,10 @@ "types": "./dist/git/branch-lifecycle.d.mts", "import": "./dist/git/branch-lifecycle.mjs" }, + "./tools/annotations": { + "types": "./dist/tools/annotations.d.mts", + "import": "./dist/tools/annotations.mjs" + }, "./templates": { "types": "./dist/templates/index.d.mts", "import": "./dist/templates/index.mjs" @@ -139,8 +143,8 @@ "dist" ], "scripts": { - "build": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript", - "dev": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript --watch", + "build": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript", + "dev": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript --watch", "test": "vitest run", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" diff --git a/packages/mcp/src/tools/annotations.ts b/packages/mcp/src/tools/annotations.ts index b148bad..b6afb04 100644 --- a/packages/mcp/src/tools/annotations.ts +++ b/packages/mcp/src/tools/annotations.ts @@ -3,6 +3,11 @@ import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js' /** * Centralized tool annotations registry. * MCP clients use these hints to distinguish read-only vs. write vs. destructive tools. + * + * Also serves as the **single source of truth for the tool name list**. Consumers + * that need to enumerate every registered tool (e.g. parity tests in + * `@contentrain/rules` / `@contentrain/skills`) should import `TOOL_NAMES` below + * rather than hardcoding the list. */ export const TOOL_ANNOTATIONS: Record = { // ─── Context (read-only) ─── @@ -121,3 +126,12 @@ export const TOOL_ANNOTATIONS: Record = { idempotentHint: false, }, } + +/** + * Canonical list of every registered MCP tool name, derived from the + * single source of truth above. Re-exported here with a stable name so + * parity tests in sibling packages (`@contentrain/rules`, + * `@contentrain/skills`) can assert against it without depending on + * `TOOL_ANNOTATIONS` internals. + */ +export const TOOL_NAMES: readonly string[] = Object.freeze(Object.keys(TOOL_ANNOTATIONS)) diff --git a/packages/rules/essential/contentrain-essentials.md b/packages/rules/essential/contentrain-essentials.md index ac6c77d..b0a0e40 100644 --- a/packages/rules/essential/contentrain-essentials.md +++ b/packages/rules/essential/contentrain-essentials.md @@ -54,6 +54,7 @@ MCP is **deterministic infrastructure**. The agent (you) is the **intelligence l | `contentrain_submit` | Push branches to remote | | `contentrain_merge` | Merge a review-mode branch into contentrain locally | | `contentrain_bulk` | Batch operations (copy_locale/update_status/delete_entries) | +| `contentrain_doctor` | Project health report (env + structure + orphan content + branch pressure + SDK freshness) | ## Mandatory Protocols @@ -68,7 +69,7 @@ MCP is **deterministic infrastructure**. The agent (you) is the **intelligence l - A dedicated `contentrain` branch is the single source of truth for content state, created at init - Every write operation creates a temporary worktree on the `contentrain` branch automatically -- Feature branches (`contentrain/{operation}/{model}/{locale}/{timestamp}`) are created from `contentrain` for each operation +- Feature branches (`cr/{operation}/{model}/{locale}/{timestamp}`) are created from `contentrain` for each operation - **auto-merge** mode: feature branch merges into `contentrain`, then `contentrain` advances baseBranch via update-ref (fast-forward), then `.contentrain/` files are selectively synced to the developer's working tree - **review** mode: feature branch pushed to remote for team review - Developer's working tree is never mutated during MCP git operations (no stash, no checkout, no merge on the developer's tree) @@ -76,7 +77,7 @@ MCP is **deterministic infrastructure**. The agent (you) is the **intelligence l - The `contentrain` branch is protected from deletion - context.json is committed together with content changes, not as a separate commit - Never create branches manually, never commit directly to main or the `contentrain` branch -- 50+ active branches = warning, 80+ = blocked +- 50+ active `cr/*` branches = warning, 80+ = blocked ## CLI Serve — Review & Approval diff --git a/packages/rules/package.json b/packages/rules/package.json index 8def6f5..047f5a9 100644 --- a/packages/rules/package.json +++ b/packages/rules/package.json @@ -56,6 +56,7 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { + "@contentrain/mcp": "workspace:*", "@types/node": "^22.0.0", "tsdown": "^0.21.0", "typescript": "^5.7.0", diff --git a/packages/rules/prompts/normalize-mode.md b/packages/rules/prompts/normalize-mode.md index 1b8a762..f204d03 100644 --- a/packages/rules/prompts/normalize-mode.md +++ b/packages/rules/prompts/normalize-mode.md @@ -12,7 +12,7 @@ This mode converts a codebase with hardcoded strings into a Contentrain-managed |--------|---------------------|----------------| | Purpose | Pull content from source code into `.contentrain/` | Patch source files to reference extracted content | | Source files modified | No | Yes | -| Branch pattern | `contentrain/normalize/extract/{domain}/{timestamp}` | `contentrain/normalize/reuse/{model}/{locale}/{timestamp}` | +| Branch pattern | `cr/normalize/extract/{domain}/{timestamp}` | `cr/normalize/reuse/{model}/{locale}/{timestamp}` | | Prerequisite | Initialized `.contentrain/` | Completed extraction (content exists) | | Workflow mode | Always `review` | Always `review` | | Standalone value | Yes — content is manageable in Studio immediately | Depends on Phase 1 | diff --git a/packages/rules/prompts/review-mode.md b/packages/rules/prompts/review-mode.md index f3f69d5..b31a33d 100644 --- a/packages/rules/prompts/review-mode.md +++ b/packages/rules/prompts/review-mode.md @@ -2,14 +2,14 @@ > **Prerequisites:** Read `prompts/common.md` first. All shared rules apply. -This mode is for reviewing content changes on pending `contentrain/*` branches before they are merged. You act as a content quality reviewer, applying all Contentrain rules systematically. +This mode is for reviewing content changes on pending `cr/*` branches before they are merged. You act as a content quality reviewer, applying all Contentrain rules systematically. --- ## Pipeline ``` -Step 1: List open contentrain/* branches +Step 1: List open cr/* branches Step 2: Show diffs for selected branch Step 3: Apply review checklist Step 4: Recommend action @@ -19,19 +19,19 @@ Step 4: Recommend action ## Step 1: List Pending Branches -Call `contentrain_status` to see pending changes and open branches. Identify branches with the `contentrain/` prefix that are awaiting review. +Call `contentrain_status` to see pending changes and open branches. Identify branches with the `cr/` prefix that are awaiting review. Branch naming convention: ``` -contentrain/{operation}/{model}/{locale}/{timestamp} +cr/{operation}/{model}/{locale}/{timestamp} ``` Common branch types: -- `contentrain/content/...` — content updates -- `contentrain/model/...` — model changes -- `contentrain/normalize/extract/...` — normalize extraction -- `contentrain/normalize/reuse/...` — normalize reuse (source patching) -- `contentrain/new/scaffold-...` — scaffold operations +- `cr/content/...` — content updates +- `cr/model/...` — model changes +- `cr/normalize/extract/...` — normalize extraction +- `cr/normalize/reuse/...` — normalize reuse (source patching) +- `cr/new/scaffold-...` — scaffold operations --- diff --git a/packages/rules/shared/workflow-rules.md b/packages/rules/shared/workflow-rules.md index 7b6fa53..0cfd1f1 100644 --- a/packages/rules/shared/workflow-rules.md +++ b/packages/rules/shared/workflow-rules.md @@ -43,7 +43,7 @@ Contentrain supports two workflow modes, configured in `.contentrain/config.json All Contentrain branches follow a strict naming pattern: ``` -contentrain/{operation}/{model}/{locale}/{timestamp} +cr/{operation}/{model}/{locale}/{timestamp} ``` ### Examples diff --git a/packages/rules/src/index.ts b/packages/rules/src/index.ts index 6887f7d..77dfbf9 100644 --- a/packages/rules/src/index.ts +++ b/packages/rules/src/index.ts @@ -23,7 +23,7 @@ export type FieldType = (typeof FIELD_TYPES)[number] export const MODEL_KINDS = ['singleton', 'collection', 'document', 'dictionary'] as const export type ModelKind = (typeof MODEL_KINDS)[number] -// ─── MCP Tools (15 tools) ─── +// ─── MCP Tools (17 tools) ─── export const MCP_TOOLS = [ 'contentrain_status', 'contentrain_describe', 'contentrain_describe_format', @@ -32,7 +32,9 @@ export const MCP_TOOLS = [ 'contentrain_content_save', 'contentrain_content_delete', 'contentrain_content_list', 'contentrain_scan', 'contentrain_apply', 'contentrain_validate', 'contentrain_submit', + 'contentrain_merge', 'contentrain_bulk', + 'contentrain_doctor', ] as const export type McpTool = (typeof MCP_TOOLS)[number] diff --git a/packages/rules/tests/mcp-parity.test.ts b/packages/rules/tests/mcp-parity.test.ts new file mode 100644 index 0000000..7d7828e --- /dev/null +++ b/packages/rules/tests/mcp-parity.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { TOOL_NAMES } from '@contentrain/mcp/tools/annotations' +import { buildBranchName } from '@contentrain/mcp/git/transaction' +import { MCP_TOOLS, ESSENTIAL_RULES_FILE } from '../src/index.js' + +/** + * Cross-package parity tests. + * + * `@contentrain/rules` publishes a public catalog of tools and branch + * conventions that agents rely on. `@contentrain/mcp` is the runtime + * authority. Without this file, the two can (and historically did) drift: + * the rules catalog sat at 15 tools while MCP advertised 16; the + * essential rules kept teaching the legacy `contentrain/*` branch + * namespace after MCP switched to `cr/*`. + * + * The tests below fail loudly whenever either side moves without the + * other. Fix by aligning — not by muting the test. + */ + +const PKG_ROOT = join(import.meta.dirname, '..') + +describe('MCP parity — tool registry', () => { + it('MCP_TOOLS matches the MCP annotations registry exactly', () => { + const rulesSet = new Set(MCP_TOOLS) + const mcpSet = new Set(TOOL_NAMES) + + const missingFromRules = [...mcpSet].filter(t => !rulesSet.has(t)) + const missingFromMcp = [...rulesSet].filter(t => !mcpSet.has(t)) + + expect(missingFromRules, 'tools registered in @contentrain/mcp but missing from @contentrain/rules MCP_TOOLS').toEqual([]) + expect(missingFromMcp, 'tools in @contentrain/rules MCP_TOOLS but not registered in @contentrain/mcp').toEqual([]) + expect(MCP_TOOLS.length).toBe(TOOL_NAMES.length) + }) + + it('essential guardrails document every MCP tool', () => { + const content = readFileSync(join(PKG_ROOT, ESSENTIAL_RULES_FILE), 'utf-8') + for (const tool of TOOL_NAMES) { + expect(content, `essential rules do not mention ${tool}`).toContain(tool) + } + }) +}) + +describe('MCP parity — branch naming', () => { + it('buildBranchName() emits the `cr/` prefix that rules + skills document', () => { + const samples = [ + buildBranchName('content', 'blog-post', 'en'), + buildBranchName('model', 'team-member'), + buildBranchName('normalize/extract', 'marketing'), + buildBranchName('new', 'scaffold-landing', 'en'), + ] + for (const branch of samples) { + expect(branch, `branch name should start with "cr/": ${branch}`).toMatch(/^cr\//u) + } + }) + + it('rules docs do not reference the legacy `contentrain/{operation}/` branch prefix', () => { + // The `.contentrain/` directory path is correct — only the branch + // prefix is stale. Filter accordingly so the test doesn't + // false-positive on real filesystem paths. + const filesToScan = [ + 'essential/contentrain-essentials.md', + 'prompts/review-mode.md', + 'prompts/normalize-mode.md', + 'shared/workflow-rules.md', + ] + const legacyPattern = /(^|[^.])contentrain\/(content|model|normalize|new|fix|review)\b/gmu + for (const rel of filesToScan) { + const content = readFileSync(join(PKG_ROOT, rel), 'utf-8') + const matches = [...content.matchAll(legacyPattern)] + expect(matches.length, `legacy "contentrain//" branch prefix in ${rel}: ${matches.map(m => m[0]).join(', ')}`).toBe(0) + } + }) +}) diff --git a/packages/rules/tests/validate-rules.test.ts b/packages/rules/tests/validate-rules.test.ts index 276eb35..bea3cc7 100644 --- a/packages/rules/tests/validate-rules.test.ts +++ b/packages/rules/tests/validate-rules.test.ts @@ -25,7 +25,7 @@ describe('essential rules', () => { describe('constants', () => { it('FIELD_TYPES has 27 entries', () => { expect(FIELD_TYPES).toHaveLength(27) }) it('MODEL_KINDS has 4 entries', () => { expect(MODEL_KINDS).toHaveLength(4) }) - it('MCP_TOOLS has 15 entries', () => { expect(MCP_TOOLS).toHaveLength(15) }) + it('MCP_TOOLS has 17 entries', () => { expect(MCP_TOOLS).toHaveLength(17) }) it('all MCP tools match pattern', () => { for (const t of MCP_TOOLS) expect(t).toMatch(/^contentrain_/) }) diff --git a/packages/skills/package.json b/packages/skills/package.json index 5ad86f8..833a3b7 100644 --- a/packages/skills/package.json +++ b/packages/skills/package.json @@ -62,6 +62,7 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { + "@contentrain/mcp": "workspace:*", "@types/node": "^22.0.0", "tsdown": "^0.21.0", "typescript": "^5.7.0", diff --git a/packages/skills/skills/contentrain-normalize/SKILL.md b/packages/skills/skills/contentrain-normalize/SKILL.md index 391bec7..870b72d 100644 --- a/packages/skills/skills/contentrain-normalize/SKILL.md +++ b/packages/skills/skills/contentrain-normalize/SKILL.md @@ -60,7 +60,7 @@ branch is pushed. | Purpose | Pull content from source to `.contentrain/` | Patch source files with content references | | Scope | Full project scan | Per model or per domain | | Source files modified | No | Yes | -| Branch pattern | `contentrain/normalize/extract/{domain}/{timestamp}` | `contentrain/normalize/reuse/{model}/{locale}/{timestamp}` | +| Branch pattern | `cr/normalize/extract/{domain}/{timestamp}` | `cr/normalize/reuse/{model}/{locale}/{timestamp}` | | Prerequisite | Initialized `.contentrain/` | Completed extraction (content exists in `.contentrain/`) | | Workflow mode | Always `review` | Always `review` | | Standalone value | Yes -- content is manageable in Studio immediately | Depends on Phase 1 | @@ -133,7 +133,7 @@ Call `contentrain_apply(mode: "extract", dry_run: true)` to generate a preview. ### 6. Execute Extraction -After user approval, call `contentrain_apply(mode: "extract", dry_run: false)`. This creates model definitions and content files in `.contentrain/` on a `contentrain/normalize/extract/{timestamp}` branch. Source files are NOT modified. +After user approval, call `contentrain_apply(mode: "extract", dry_run: false)`. This creates model definitions and content files in `.contentrain/` on a `cr/normalize/extract/{timestamp}` branch. Source files are NOT modified. ### 7. Validate and Submit @@ -207,7 +207,7 @@ Call `contentrain_apply(mode: "reuse", scope: { model: "" }, patches: ### 4. Execute Reuse -After user confirmation, call `contentrain_apply(mode: "reuse", scope: { model: "" }, patches: [...], dry_run: false)`. This patches source files and creates a `contentrain/normalize/reuse/{model}/{timestamp}` branch. +After user confirmation, call `contentrain_apply(mode: "reuse", scope: { model: "" }, patches: [...], dry_run: false)`. This patches source files and creates a `cr/normalize/reuse/{model}/{timestamp}` branch. ### 5. Validate and Submit diff --git a/packages/skills/skills/contentrain-normalize/references/extraction.md b/packages/skills/skills/contentrain-normalize/references/extraction.md index 228118c..02c9bfd 100644 --- a/packages/skills/skills/contentrain-normalize/references/extraction.md +++ b/packages/skills/skills/contentrain-normalize/references/extraction.md @@ -113,7 +113,7 @@ Returns a preview of what will be created in `.contentrain/` without making chan contentrain_apply(mode: "extract", dry_run: false) ``` -Creates model definitions and content files in `.contentrain/` on a `contentrain/normalize/extract/{timestamp}` branch. `dry_run` defaults to `true`, so you MUST explicitly set `dry_run: false` to execute. +Creates model definitions and content files in `.contentrain/` on a `cr/normalize/extract/{timestamp}` branch. `dry_run` defaults to `true`, so you MUST explicitly set `dry_run: false` to execute. ### 7. Validate and Submit diff --git a/packages/skills/skills/contentrain-normalize/references/reuse.md b/packages/skills/skills/contentrain-normalize/references/reuse.md index 6a1861e..c59538f 100644 --- a/packages/skills/skills/contentrain-normalize/references/reuse.md +++ b/packages/skills/skills/contentrain-normalize/references/reuse.md @@ -42,7 +42,7 @@ Review the dry-run output: contentrain_apply(mode: "reuse", scope: { model: "" }, patches: [...], dry_run: false) ``` -`dry_run` defaults to `true`, so you MUST explicitly set `dry_run: false` to execute. This patches source files and creates a `contentrain/normalize/reuse/{model}/{timestamp}` branch. +`dry_run` defaults to `true`, so you MUST explicitly set `dry_run: false` to execute. This patches source files and creates a `cr/normalize/reuse/{model}/{timestamp}` branch. ### 5. Validate and Submit diff --git a/packages/skills/skills/contentrain-translate/SKILL.md b/packages/skills/skills/contentrain-translate/SKILL.md index 6d78590..8eed9b1 100644 --- a/packages/skills/skills/contentrain-translate/SKILL.md +++ b/packages/skills/skills/contentrain-translate/SKILL.md @@ -170,7 +170,7 @@ If validation fails, fix issues and re-save. Call `contentrain_submit` to commit the translations: -- Branch: `contentrain/content/{model}/{targetLocale}/{timestamp}`. +- Branch: `cr/content/{model}/{targetLocale}/{timestamp}`. - Each locale can be submitted independently. ### 11. Final Summary diff --git a/packages/skills/skills/contentrain/references/mcp-pipelines.md b/packages/skills/skills/contentrain/references/mcp-pipelines.md index a833675..6c04b2a 100644 --- a/packages/skills/skills/contentrain/references/mcp-pipelines.md +++ b/packages/skills/skills/contentrain/references/mcp-pipelines.md @@ -147,7 +147,7 @@ contentrain_submit # Push (always review mode) - A dedicated `contentrain` branch is the single source of truth for content state, created at init and protected from deletion - Every write operation creates a temporary worktree on a new feature branch forked from `contentrain` -- Branch naming: `contentrain/{operation}/{model}/{timestamp}` (locale included when applicable) +- Branch naming: `cr/{operation}/{model}/{timestamp}` (locale included when applicable) - Do not create branches manually. MCP handles Git transactions - Developer's working tree is never mutated during MCP operations (no stash, no checkout, no merge on the developer's tree) - context.json is committed together with content changes, not as a separate commit diff --git a/packages/skills/skills/contentrain/references/mcp-tools.md b/packages/skills/skills/contentrain/references/mcp-tools.md index 5f2b2bb..c300b98 100644 --- a/packages/skills/skills/contentrain/references/mcp-tools.md +++ b/packages/skills/skills/contentrain/references/mcp-tools.md @@ -269,13 +269,35 @@ Validate project content against model schemas. ### contentrain_submit -Push contentrain/* branches to remote. +Push `cr/*` feature branches to remote. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `branches` | string[] | No | Specific branches to push (omit for all contentrain/* branches) | +| `branches` | string[] | No | Specific branches to push (omit for all `cr/*` branches) | | `message` | string | No | Optional message for the push operation | +### contentrain_merge + +Merge a single review-mode `cr/*` branch into the content-tracking `contentrain` branch and advance the base branch via `update-ref`. Runs the worktree transaction with selective sync — dirty files in the developer's working tree are preserved rather than overwritten. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `branch` | string | Yes | Feature branch name (must start with `cr/`) | + +Returns `{ action, commit, sync }` — `sync.skipped[]` lists files the selective sync skipped because the developer has uncommitted changes. The CLI surfaces this as a warning. + +## Doctor Tools + +### contentrain_doctor + +Structured project health report: git install, Node version, `.contentrain/` structure, model parse, orphan content, pending branch pressure, SDK client freshness. Read-only. Local-filesystem only (`localWorktree` capability — unavailable over remote providers). + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `usage` | boolean | No | Run the heavier usage-analysis branch (unused content keys, duplicate dictionary values, locale coverage). Default `false` | + +Returns `{ checks[], summary: { total, passed, failed, warnings }, usage? }`. Each check carries `name`, `pass`, `detail`, optional `severity: 'error' | 'warning' | 'info'`. + ## Bulk Tools ### contentrain_bulk diff --git a/packages/skills/skills/contentrain/references/workflow.md b/packages/skills/skills/contentrain/references/workflow.md index b619728..b5931a7 100644 --- a/packages/skills/skills/contentrain/references/workflow.md +++ b/packages/skills/skills/contentrain/references/workflow.md @@ -45,21 +45,21 @@ Contentrain supports two workflow modes, configured in `.contentrain/config.json ## 2. Branch Naming Convention -All Contentrain branches follow a strict naming pattern: +All Contentrain feature branches follow a strict naming pattern: ``` -contentrain/{operation}/{model}/{locale}/{timestamp} +cr/{operation}/{model}/{locale}/{timestamp}-{suffix} ``` ### Examples | Scenario | Branch Name | |----------|-------------| -| Content update | `contentrain/content/blog-post/en/1710300000` | -| Model creation | `contentrain/model/team-member/1710300000` | -| Normalize extraction | `contentrain/normalize/extract/blog/1710300000` | -| Normalize reuse | `contentrain/normalize/reuse/marketing-hero/en/1710300000` | -| Scaffold | `contentrain/new/scaffold-landing/en/1710300000` | +| Content update | `cr/content/blog-post/en/1710300000-a1b2` | +| Model creation | `cr/model/team-member/1710300000-c3d4` | +| Normalize extraction | `cr/normalize/extract/blog/1710300000-e5f6` | +| Normalize reuse | `cr/normalize/reuse/marketing-hero/en/1710300000-0789` | +| Scaffold | `cr/new/scaffold-landing/en/1710300000-1234` | ### Rules diff --git a/packages/skills/tests/mcp-parity.test.ts b/packages/skills/tests/mcp-parity.test.ts new file mode 100644 index 0000000..d6f45b0 --- /dev/null +++ b/packages/skills/tests/mcp-parity.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { TOOL_NAMES } from '@contentrain/mcp/tools/annotations' + +/** + * Cross-package parity tests. + * + * `@contentrain/skills` ships the canonical MCP tool reference and the + * workflow / normalize guides that agents load on demand. `@contentrain/mcp` + * is the runtime authority. Without these tests, drift creeps in: for a + * while, the reference jumped from `contentrain_submit` straight to + * `contentrain_bulk` with no `contentrain_merge` section, and normalize + * SKILL.md taught the legacy `contentrain/normalize/*` branch pattern + * after MCP switched to `cr/*`. + * + * The tests below fail loudly whenever either side moves without the + * other. Fix by aligning — not by muting the test. + */ + +const PKG_ROOT = join(import.meta.dirname, '..') +const TOOL_REF = join(PKG_ROOT, 'skills', 'contentrain', 'references', 'mcp-tools.md') + +describe('MCP parity — tool reference coverage', () => { + it('references/mcp-tools.md has a section for every MCP tool', () => { + const content = readFileSync(TOOL_REF, 'utf-8') + const missing: string[] = [] + for (const tool of TOOL_NAMES) { + const header = new RegExp(`^###\\s+${tool}\\b`, 'mu') + if (!header.test(content)) missing.push(tool) + } + expect(missing, `missing heading "### " in references/mcp-tools.md for: ${missing.join(', ')}`).toEqual([]) + }) +}) + +describe('MCP parity — branch naming', () => { + it('skills docs do not reference the legacy `contentrain//` branch prefix', () => { + // The `.contentrain/` directory path is correct — only the branch + // prefix is stale. Filter accordingly so we don't false-positive on + // real filesystem paths. + const filesToScan = [ + 'skills/contentrain/references/mcp-pipelines.md', + 'skills/contentrain/references/workflow.md', + 'skills/contentrain/references/mcp-tools.md', + 'skills/contentrain-normalize/SKILL.md', + 'skills/contentrain-normalize/references/extraction.md', + 'skills/contentrain-normalize/references/reuse.md', + 'skills/contentrain-translate/SKILL.md', + ] + const legacyPattern = /(^|[^.])contentrain\/(content|model|normalize|new|fix|review)\b/gmu + const violations: Record = {} + for (const rel of filesToScan) { + const content = readFileSync(join(PKG_ROOT, rel), 'utf-8') + const matches = [...content.matchAll(legacyPattern)].map(m => m[0].trim()) + if (matches.length > 0) violations[rel] = matches + } + expect(violations, `legacy branch prefix found: ${JSON.stringify(violations, null, 2)}`).toEqual({}) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 531bc24..c0d5075 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: packages/rules: devDependencies: + '@contentrain/mcp': + specifier: workspace:* + version: link:../mcp '@types/node': specifier: ^22.0.0 version: 22.19.15 @@ -238,6 +241,9 @@ importers: packages/skills: devDependencies: + '@contentrain/mcp': + specifier: workspace:* + version: link:../mcp '@types/node': specifier: ^22.0.0 version: 22.19.15 diff --git a/tsconfig.json b/tsconfig.json index f7f4ae0..82c8efc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "@contentrain/mcp/util/*": ["packages/mcp/src/util/*.ts"], "@contentrain/mcp/git/*": ["packages/mcp/src/git/*.ts"], "@contentrain/mcp/providers/*": ["packages/mcp/src/providers/*.ts", "packages/mcp/src/providers/*/index.ts"], + "@contentrain/mcp/tools/*": ["packages/mcp/src/tools/*.ts"], "@contentrain/mcp/templates": ["packages/mcp/src/templates/index.ts"], "@contentrain/query": ["packages/sdk/js/src/index.ts"], "@contentrain/query/generate": ["packages/sdk/js/src/generator/generate.ts"]