@contentrain/skills 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +65 -60
  2. package/dist/index.cjs +65 -1
  3. package/dist/index.d.cts +50 -2
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +50 -2
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +65 -2
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -1
  10. package/skills/contentrain/SKILL.md +149 -0
  11. package/skills/contentrain/references/architecture.md +212 -0
  12. package/skills/contentrain/references/content-formats.md +279 -0
  13. package/skills/contentrain/references/i18n.md +298 -0
  14. package/skills/contentrain/references/mcp-pipelines.md +195 -0
  15. package/skills/contentrain/references/mcp-tools.md +308 -0
  16. package/skills/contentrain/references/model-kinds.md +330 -0
  17. package/skills/contentrain/references/schema-types.md +177 -0
  18. package/skills/contentrain/references/security.md +146 -0
  19. package/skills/contentrain/references/workflow.md +285 -0
  20. package/skills/contentrain-bulk/SKILL.md +99 -0
  21. package/skills/contentrain-content/SKILL.md +162 -0
  22. package/skills/contentrain-diff/SKILL.md +62 -0
  23. package/skills/contentrain-doctor/SKILL.md +62 -0
  24. package/skills/contentrain-generate/SKILL.md +183 -0
  25. package/skills/contentrain-generate/references/generated-client.md +198 -0
  26. package/skills/contentrain-init/SKILL.md +99 -0
  27. package/skills/contentrain-model/SKILL.md +141 -0
  28. package/skills/contentrain-normalize/SKILL.md +185 -0
  29. package/skills/contentrain-normalize/references/extraction.md +164 -0
  30. package/skills/contentrain-normalize/references/reuse.md +146 -0
  31. package/skills/contentrain-normalize/references/what-is-content.md +115 -0
  32. package/skills/contentrain-quality/SKILL.md +180 -0
  33. package/skills/contentrain-quality/references/accessibility.md +160 -0
  34. package/skills/contentrain-quality/references/content-quality.md +299 -0
  35. package/skills/contentrain-quality/references/media.md +170 -0
  36. package/skills/contentrain-quality/references/seo.md +229 -0
  37. package/skills/contentrain-review/SKILL.md +170 -0
  38. package/skills/contentrain-sdk/SKILL.md +145 -0
  39. package/skills/contentrain-sdk/references/bundler-config.md +135 -0
  40. package/skills/contentrain-serve/SKILL.md +96 -0
  41. package/skills/contentrain-translate/SKILL.md +180 -0
  42. package/skills/contentrain-validate-fix/SKILL.md +92 -0
@@ -0,0 +1,198 @@
1
+ ---
2
+ name: generated-client
3
+ description: "Generated client structure, framework bundler configs, and API patterns"
4
+ ---
5
+
6
+ # Generated Client Reference
7
+
8
+ ## Bundler Alias Configuration
9
+
10
+ The `#contentrain` subpath import works natively in Node.js 22+ but **does NOT resolve in browser bundlers**. Configure an alias for your framework.
11
+
12
+ ### Vite (Vue, React, Svelte, Astro)
13
+
14
+ ```ts
15
+ // vite.config.ts
16
+ import { resolve } from 'node:path'
17
+ export default defineConfig({
18
+ resolve: {
19
+ alias: {
20
+ '#contentrain': resolve(__dirname, '.contentrain/client/index.mjs'),
21
+ },
22
+ },
23
+ })
24
+ ```
25
+
26
+ Also add a `paths` entry to `tsconfig.json` so the TypeScript language server resolves the alias:
27
+
28
+ ```json
29
+ {
30
+ "compilerOptions": {
31
+ "paths": {
32
+ "#contentrain": ["./.contentrain/client/index.d.ts"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### Next.js (webpack)
39
+
40
+ ```js
41
+ // next.config.js
42
+ const path = require('path')
43
+ module.exports = {
44
+ webpack: (config) => {
45
+ config.resolve.alias['#contentrain'] = path.resolve(__dirname, '.contentrain/client/index.mjs')
46
+ return config
47
+ },
48
+ }
49
+ ```
50
+
51
+ Add the same `tsconfig.json` paths entry:
52
+
53
+ ```json
54
+ {
55
+ "compilerOptions": {
56
+ "paths": {
57
+ "#contentrain": ["./.contentrain/client/index.d.ts"]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Nuxt 3
64
+
65
+ Nuxt provides a top-level `alias` option — no Vite config needed:
66
+
67
+ ```ts
68
+ // nuxt.config.ts
69
+ export default defineNuxtConfig({
70
+ alias: {
71
+ '#contentrain': './.contentrain/client/index.mjs',
72
+ },
73
+ })
74
+ ```
75
+
76
+ Add the `tsconfig.json` paths entry as above. Nuxt auto-extends `tsconfig.json` via `.nuxt/tsconfig.json`, so ensure the paths entry is in the project root `tsconfig.json`.
77
+
78
+ ### SvelteKit
79
+
80
+ SvelteKit uses Vite internally. Add the alias in `vite.config.ts` as shown in the Vite section above.
81
+
82
+ ### Expo / React Native (Metro)
83
+
84
+ ```js
85
+ // metro.config.js
86
+ const path = require('path')
87
+ module.exports = {
88
+ resolver: {
89
+ extraNodeModules: {
90
+ '#contentrain': path.resolve(__dirname, '.contentrain/client/index.mjs'),
91
+ },
92
+ },
93
+ }
94
+ ```
95
+
96
+ ### Pure Node.js / SSR-only
97
+
98
+ No alias needed. Node.js 22+ resolves `#contentrain` from `package.json` imports natively.
99
+
100
+ ---
101
+
102
+ ## Complete SDK API Reference
103
+
104
+ ```ts
105
+ import { query, singleton, dictionary, document } from '#contentrain'
106
+ ```
107
+
108
+ ### QueryBuilder (for collection models) -- SYNC, no await needed
109
+
110
+ ```ts
111
+ const posts = query('blog-post')
112
+ .locale('en') // set locale
113
+ .where('status', 'published') // exact match filter
114
+ .sort('date', 'desc') // sort by field
115
+ .limit(10) // limit results
116
+ .offset(5) // skip results
117
+ .include('author', 'tags') // resolve relation fields (1 level deep)
118
+ .all() // --> T[] (returns array)
119
+ .first() // --> T | undefined (first match)
120
+ ```
121
+
122
+ ### SingletonAccessor (for singleton models)
123
+
124
+ ```ts
125
+ const hero = singleton('hero')
126
+ .locale('en')
127
+ .include('featured_post') // resolve relations on singletons too
128
+ .get() // --> T (single object)
129
+ ```
130
+
131
+ ### DictionaryAccessor (for dictionary models)
132
+
133
+ ```ts
134
+ const allLabels = dictionary('ui-labels').locale('en').get() // --> Record<string, string>
135
+ const oneLabel = dictionary('ui-labels').locale('en').get('key') // --> string | undefined
136
+ const withParams = dictionary('ui-labels').locale('en').get('add-entry', { model: 'blog' }) // --> "Add a new entry to blog"
137
+ ```
138
+
139
+ Parameterized templates use `{placeholder}` syntax in dictionary values. The `get(key, params)` overload replaces `{name}` with the provided value. Unmatched placeholders are left as-is.
140
+
141
+ ### DocumentQuery (for document models -- markdown + frontmatter)
142
+
143
+ ```ts
144
+ const article = document('blog-article')
145
+ .locale('en')
146
+ .where('category', 'tech')
147
+ .include('author') // resolve relations in frontmatter
148
+ .bySlug('getting-started') // --> T | undefined (find by slug)
149
+
150
+ const docs = document('doc-page').locale('en').all() // --> T[]
151
+ const first = document('doc-page').locale('en').first() // --> T | undefined
152
+ ```
153
+
154
+ ### DOES NOT EXIST -- never use these
155
+
156
+ - `.filter()` -- use `.where(field, value)` instead
157
+ - `.byId()` -- use `.where('id', value).first()` instead
158
+ - `.count()` -- use `.all().length` instead
159
+ - `dictionary().all()` -- use `.get()` instead
160
+ - Queries are SYNC -- do not use `await` with `query()`, `singleton()`, `dictionary()`, or `document()`
161
+ - `.where('field', 'eq', value)` -- just `.where('field', value)`
162
+ - `.get()` on QueryBuilder -- use `.all()` or `.first()`
163
+
164
+ ---
165
+
166
+ ## With Relations
167
+
168
+ ```ts
169
+ // Relations are resolved 1 level deep via .include()
170
+ const posts = query('blog-post')
171
+ .locale('en')
172
+ .include('author', 'tags')
173
+ .all()
174
+ // posts[0].author --> { id: '...', name: 'John', ... } (resolved object)
175
+
176
+ // Without include:
177
+ const raw = query('blog-post').locale('en').all()
178
+ // raw[0].author --> 'author-id-123' (raw string ID, NOT resolved)
179
+
180
+ // Singletons support include too:
181
+ const hero = singleton('hero').locale('en').include('featured_post').get()
182
+
183
+ // Documents support include too:
184
+ const article = document('blog-article').locale('en').include('author').bySlug('my-post')
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Framework-Specific Patterns
190
+
191
+ | Stack | Usage Pattern | Alias Setup |
192
+ |---|---|---|
193
+ | Nuxt 3 | `useAsyncData(() => singleton('hero').locale(locale).get())` | `nuxt.config.ts` alias |
194
+ | Next.js | In RSC: `const data = singleton('hero').locale('en').get()` | `next.config.js` webpack alias |
195
+ | Astro | In frontmatter: `const posts = query('blog-post').locale('en').all()` | `vite.config.ts` alias |
196
+ | SvelteKit | In `+page.server.ts`: `export const load = () => ({ hero: singleton('hero').locale('en').get() })` | `vite.config.ts` alias |
197
+ | Expo / RN | `const hero = singleton('hero').locale('en').get()` | `metro.config.js` resolver |
198
+ | Node.js / SSR | Direct import — no alias needed | Native subpath imports |
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: contentrain-init
3
+ description: "Initialize a new Contentrain project. Use when setting up .contentrain/ directory, configuring stack and locales, or scaffolding templates."
4
+ metadata:
5
+ author: Contentrain
6
+ version: "1.0.0"
7
+ ---
8
+
9
+ # Skill: Initialize Contentrain in a Project
10
+
11
+ > Set up Contentrain content infrastructure in an existing project.
12
+
13
+ ---
14
+
15
+ ## When to Use
16
+
17
+ The user wants to add Contentrain to their project, or says something like "set up contentrain", "initialize contentrain", "add content management".
18
+
19
+ ---
20
+
21
+ ## Steps
22
+
23
+ ### 1. Detect the Tech Stack
24
+
25
+ Read `package.json` to identify the framework:
26
+
27
+ | Dependency | Stack |
28
+ |---|---|
29
+ | `nuxt` | Nuxt 3 |
30
+ | `next` | Next.js |
31
+ | `@astrojs/astro` or `astro` | Astro |
32
+ | `@sveltejs/kit` | SvelteKit |
33
+
34
+ If none match, treat as a generic Node.js project. Report the detected stack to the user before proceeding.
35
+
36
+ ### 2. Ask for Configuration
37
+
38
+ Ask the user for:
39
+
40
+ - **Supported locales:** Which languages will the project support? Default: `["en"]`. Use ISO 639-1 codes.
41
+ - **Source locale:** Which locale is the primary/source locale? Default: `"en"`.
42
+ - **Domain names:** Suggest domains based on the project structure (e.g., `marketing`, `blog`, `app`, `docs`). Ask the user to confirm or modify.
43
+
44
+ ### 3. Initialize the Project
45
+
46
+ Call the MCP tool:
47
+
48
+ ```
49
+ contentrain_init(stack: "<detected>", locales: ["en", ...], domains: ["marketing", ...])
50
+ ```
51
+
52
+ This creates the `.contentrain/` directory structure, `config.json`, and initial `context.json`.
53
+
54
+ ### 4. Analyze Project Conventions
55
+
56
+ After initialization, review the project for conventions to inform content creation:
57
+
58
+ - **Tone:** Analyze existing copy in the project (README, landing page, UI text) to determine the appropriate tone (e.g., `professional`, `casual`, `technical`).
59
+ - **Naming conventions:** Note any patterns in file naming, component naming, or content structure.
60
+
61
+ > **Note:** Do NOT manually edit `context.json`. It is managed automatically by MCP tools and updated after every write operation.
62
+
63
+ ### 5. Suggest Initial Models
64
+
65
+ Based on the project analysis, suggest models that would be useful:
66
+
67
+ - If the project has a landing page: suggest `hero` (singleton), `features` (singleton or collection).
68
+ - If the project has a blog section: suggest `blog-post` (document), `categories` (collection), `authors` (collection).
69
+ - If the project has UI text: suggest `ui-labels` (dictionary), `error-messages` (dictionary).
70
+
71
+ Present suggestions to the user and ask which to create. For selected models, call `contentrain_model_save` for each.
72
+
73
+ ### 6. Offer Scaffold Templates
74
+
75
+ If the project matches a common pattern, offer to use a scaffold template:
76
+
77
+ ```
78
+ contentrain_scaffold(template: "blog", locales: ["en"], with_sample_content: true)
79
+ ```
80
+
81
+ Available templates: `blog`, `landing`, `docs`, `ecommerce`, `saas`, `i18n`, `mobile`.
82
+
83
+ ### 7. Set Up Agent Rules
84
+
85
+ Copy the appropriate rules file to the project:
86
+
87
+ - For Claude Code projects: append Contentrain rules to the project's `CLAUDE.md` or create a new section.
88
+ - For Cursor projects: create or update `.cursorrules` with Contentrain conventions.
89
+
90
+ Include a reference to the framework-specific guide (nuxt.md, next.md, astro.md, sveltekit.md).
91
+
92
+ ### 8. Final Summary
93
+
94
+ Report to the user:
95
+
96
+ - What was created in `.contentrain/`.
97
+ - Which models were set up.
98
+ - How to import content in their code (show the `#contentrain` import).
99
+ - Next steps: create content, run `npx contentrain generate`, start the dev server.
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: contentrain-model
3
+ description: "Design and save Contentrain model definitions. Use when creating models, updating schemas, adding fields, or changing model structure."
4
+ metadata:
5
+ author: Contentrain
6
+ version: "1.0.0"
7
+ ---
8
+
9
+ # Skill: Design and Save Models
10
+
11
+ > Create or evolve Contentrain model definitions safely.
12
+
13
+ ---
14
+
15
+ ## When to Use
16
+
17
+ Use this when the user wants to:
18
+
19
+ - add a new model
20
+ - change fields on an existing model
21
+ - choose between `singleton`, `collection`, `document`, and `dictionary`
22
+ - define relations, locales, or custom content paths
23
+
24
+ ---
25
+
26
+ ## Steps
27
+
28
+ ### 1. Inspect Project State
29
+
30
+ Call `contentrain_status` first:
31
+
32
+ - confirm the project is initialized
33
+ - see existing model IDs and domains
34
+ - avoid creating duplicates
35
+
36
+ If the user is extending an existing model, call `contentrain_describe(model: "<model-id>")`.
37
+
38
+ ### 2. Confirm Storage Contract
39
+
40
+ Call `contentrain_describe_format` before proposing structure changes.
41
+
42
+ Use it to confirm:
43
+
44
+ - model kinds and storage expectations
45
+ - locale behavior
46
+ - document vs JSON model tradeoffs
47
+ - how custom `content_path` and `locale_strategy` affect files
48
+
49
+ ### 3. Choose the Right Kind
50
+
51
+ - `singleton`: one object per locale, e.g. hero, navigation, footer
52
+ - `collection`: repeated entries with IDs, e.g. testimonials, faq items, authors
53
+ - `document`: long-form markdown content with frontmatter, e.g. blog posts, docs pages
54
+ - `dictionary`: flat key-value strings, e.g. UI labels, error messages
55
+
56
+ Default rules:
57
+
58
+ - use `dictionary` for UI/system strings
59
+ - use `document` for markdown-heavy long-form content
60
+ - use `collection` for repeated structured items
61
+ - use `singleton` for one page/section config per locale
62
+
63
+ ### 4. Design Fields
64
+
65
+ Follow these rules:
66
+
67
+ - model IDs must be kebab-case
68
+ - field names must be snake_case
69
+ - relation fields must define a target `model`
70
+ - prefer small, explicit schemas over large generic `object` blobs
71
+ - only mark fields as `required` when the content truly cannot function without them
72
+
73
+ Relation guidance:
74
+
75
+ - `relation`: single reference
76
+ - `relations`: multiple references
77
+ - use `slug`-driven relations for document-like linking
78
+
79
+ ### 5. Decide i18n and Path Strategy
80
+
81
+ Ask or infer:
82
+
83
+ - should this model be localized?
84
+ - should it use the default `.contentrain/content/...` path or a custom `content_path`?
85
+ - if custom path is needed, which `locale_strategy` matches the project layout?
86
+
87
+ Use custom paths only when the framework or repo structure clearly benefits from it.
88
+
89
+ ### 6. Present the Proposed Model
90
+
91
+ Before writing, show:
92
+
93
+ - model ID
94
+ - kind
95
+ - domain
96
+ - `i18n` behavior
97
+ - field list with types
98
+ - relation targets
99
+ - any custom `content_path` / `locale_strategy`
100
+
101
+ Get user confirmation before saving.
102
+
103
+ ### 7. Save the Model
104
+
105
+ Call `contentrain_model_save` with the approved definition.
106
+
107
+ Example:
108
+
109
+ ```json
110
+ {
111
+ "id": "hero",
112
+ "name": "Hero",
113
+ "kind": "singleton",
114
+ "domain": "marketing",
115
+ "i18n": true,
116
+ "fields": {
117
+ "title": { "type": "string", "required": true },
118
+ "subtitle": { "type": "text" },
119
+ "cta_label": { "type": "string" },
120
+ "featured_post": { "type": "relation", "model": "blog-post" }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### 8. Validate Downstream Impact
126
+
127
+ After changing a model:
128
+
129
+ - review whether existing content now needs updates
130
+ - call `contentrain_validate`
131
+ - if the model is consumed by the SDK, recommend `contentrain generate`
132
+
133
+ ### 9. Final Summary
134
+
135
+ Report:
136
+
137
+ - model created or updated
138
+ - kind and domain
139
+ - field count
140
+ - any relation targets
141
+ - whether content migration is needed next
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: contentrain-normalize
3
+ description: "Two-phase normalize flow: extract hardcoded strings from source code into .contentrain/ (Phase 1) and patch source files with content references (Phase 2). Use when normalizing, extracting content, or replacing hardcoded strings."
4
+ metadata:
5
+ author: Contentrain
6
+ version: "1.0.0"
7
+ ---
8
+
9
+ # Contentrain Normalize
10
+
11
+ Normalize converts a codebase with hardcoded strings into a Contentrain-managed content architecture. It runs in two independent phases, each producing a separate branch for review.
12
+
13
+ - **Phase 1 (Extraction):** Pull content from source code into `.contentrain/` structure. Source files are NOT modified.
14
+ - **Phase 2 (Reuse):** Patch source files to replace hardcoded strings with content references. Requires completed extraction.
15
+
16
+ Phase 1 alone is valuable: content becomes manageable in Studio, translatable, and publishable without touching source code.
17
+
18
+ ---
19
+
20
+ ## Two-Phase Architecture
21
+
22
+ | Aspect | Phase 1: Extraction | Phase 2: Reuse |
23
+ |--------|-------------------|----------------|
24
+ | Purpose | Pull content from source to `.contentrain/` | Patch source files with content references |
25
+ | Scope | Full project scan | Per model or per domain |
26
+ | Source files modified | No | Yes |
27
+ | Branch pattern | `contentrain/normalize/extract/{domain}/{timestamp}` | `contentrain/normalize/reuse/{model}/{locale}/{timestamp}` |
28
+ | Prerequisite | Initialized `.contentrain/` | Completed extraction (content exists in `.contentrain/`) |
29
+ | Workflow mode | Always `review` | Always `review` |
30
+ | Standalone value | Yes -- content is manageable in Studio immediately | Depends on Phase 1 |
31
+
32
+ ---
33
+
34
+ ## Agent vs MCP Responsibilities
35
+
36
+ ### Agent (Intelligence Layer)
37
+
38
+ - **Decide what is content vs code.** Semantic judgment requiring context understanding.
39
+ - **Assign domain grouping.** Determine which domain each piece of content belongs to.
40
+ - **Determine model structure.** Group related content into models, choose the right kind.
41
+ - **Create replacement expressions.** Stack-specific, requires framework knowledge (Vue/Nuxt, React/Next, Svelte, Astro, SDK).
42
+ - **Filter false positives** from scan candidates.
43
+ - **Evaluate and group** candidates into logical models.
44
+
45
+ ### MCP (Deterministic Infrastructure)
46
+
47
+ - **Scan the file system** (build graph, find candidates).
48
+ - **Read and write files** in `.contentrain/`.
49
+ - **Patch source files** with exact string replacement (agent provides the replacement expression).
50
+ - **Create branches, commit, validate** all changes.
51
+ - **Enforce guardrails** (file limits, type restrictions, dry-run requirement).
52
+
53
+ MCP is framework-agnostic. It does NOT know how `{t('key')}` differs from `{{ $t('key') }}`. The agent provides all stack-specific logic.
54
+
55
+ ---
56
+
57
+ ## Guardrails
58
+
59
+ | Guardrail | Limit | Rationale |
60
+ |-----------|-------|-----------|
61
+ | Allowed source file types | `.vue`, `.tsx`, `.jsx`, `.ts`, `.js`, `.mjs`, `.astro`, `.svelte` | Only scan files that can contain UI content |
62
+ | Max files per scan | 500 | Prevent scanning entire monorepos |
63
+ | Max files per apply | 100 | Keep diffs reviewable |
64
+ | Dry-run before apply | **MANDATORY** | Preview all changes before executing |
65
+ | Workflow mode | Always `review` | Normalize changes are never auto-merged |
66
+ | Reuse scope | Per model or per domain | No whole-project reuse in one operation |
67
+ | Reuse prerequisite | Extraction must be complete | Content must exist in `.contentrain/` before patching source |
68
+
69
+ Attempting to apply without a prior dry-run will be rejected. Exceeding file limits will truncate results with a warning.
70
+
71
+ ---
72
+
73
+ ## Phase 1: Extraction (Step-by-Step)
74
+
75
+ ### 1. Check Project State
76
+
77
+ Call `contentrain_status` to confirm the project is initialized and note supported locales. Call `contentrain_describe_format` to understand the four storage formats (dictionary, collection, singleton, document) before deciding model structure.
78
+
79
+ ### 2. Build the Project Graph
80
+
81
+ Call `contentrain_scan(mode: "graph")` to build the import/component dependency graph. Use this to understand which components belong to which pages, identify shared vs page-specific components, and prioritize files.
82
+
83
+ ### 3. Find Candidates
84
+
85
+ Call `contentrain_scan(mode: "candidates")` to find hardcoded strings. The scan returns candidates with file paths, line numbers, string values, and surrounding context.
86
+
87
+ ### 4. Evaluate Candidates (Agent Intelligence)
88
+
89
+ - **Filter false positives:** Remove CSS values, technical identifiers, import paths, variable names, config values, log messages, test strings. See [What Is Content](references/what-is-content.md) for full heuristics.
90
+ - **Assign domains:** Group candidates by domain (e.g., `marketing`, `blog`, `ui`, `system`).
91
+ - **Determine model types:** Choose dictionary, singleton, collection, or document kind for each group.
92
+ - **Structure fields:** Define field names and assign candidate strings to fields.
93
+
94
+ ### 5. Preview Extraction
95
+
96
+ Call `contentrain_apply(mode: "extract", dry_run: true)` to generate a preview. Review the dry-run output: verify model definitions, content assignments, and check for missed or misclassified candidates. Show the preview to the user.
97
+
98
+ ### 6. Execute Extraction
99
+
100
+ 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.
101
+
102
+ ### 7. Validate and Submit
103
+
104
+ Call `contentrain_validate` to check schema compliance, i18n completeness, and duplicate entries. Call `contentrain_submit` to push the branch for review. Normalize operations always use `review` workflow mode.
105
+
106
+ Tell the user: "Phase 1 complete. Content is now in Contentrain and can be managed, translated, and published from Studio. When ready, proceed with Phase 2 to update source files."
107
+
108
+ ---
109
+
110
+ ## Phase 2: Reuse (Step-by-Step)
111
+
112
+ Only start after Phase 1 is reviewed and merged.
113
+
114
+ ### 1. Select Scope
115
+
116
+ List the extracted models and ask the user which model or domain to process first. Process one model or domain at a time to keep diffs small and reviewable.
117
+
118
+ ### 2. Determine Replacement Expressions (Agent Intelligence)
119
+
120
+ Based on the project's tech stack, determine the correct replacement pattern. See [Reuse Details](references/reuse.md) for the full replacement expressions table by stack. Also determine any necessary import statements for patched files.
121
+
122
+ ### 3. Preview Reuse
123
+
124
+ Call `contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: true)`. Review: verify each replacement is correct, import statements will be added, no non-content strings are being replaced, component structure is preserved.
125
+
126
+ ### 4. Execute Reuse
127
+
128
+ After user confirmation, call `contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: false)`. This patches source files and creates a `contentrain/normalize/reuse/{model}/{timestamp}` branch.
129
+
130
+ ### 5. Validate and Submit
131
+
132
+ Call `contentrain_validate` to verify source files parse correctly, content references resolve, and no strings were missed or double-replaced. Call `contentrain_submit` to push the branch for review.
133
+
134
+ ### 6. Repeat
135
+
136
+ Ask the user which model or domain to process next. Repeat steps 1-5 for each remaining model until all extracted content is referenced in source code.
137
+
138
+ ---
139
+
140
+ ## Domain and Model Grouping Guidelines
141
+
142
+ ### Domain Assignment
143
+
144
+ | Content Location | Suggested Domain |
145
+ |------------------|-----------------|
146
+ | Landing page, marketing sections | `marketing` |
147
+ | Blog, articles, posts | `blog` |
148
+ | Navigation, footer, header | `ui` |
149
+ | Error messages, validation | `system` |
150
+ | Product pages, e-commerce | `product` |
151
+ | Documentation, help | `docs` |
152
+ | User-facing app strings | `app` |
153
+
154
+ ### Model Kind Selection
155
+
156
+ | Content Pattern | Kind | Rationale |
157
+ |----------------|------|-----------|
158
+ | One set of fields per page section | `singleton` | Hero, features, pricing header |
159
+ | Multiple items of same type | `collection` | Team members, FAQs, testimonials |
160
+ | Long-form with metadata | `document` | Blog posts, documentation pages |
161
+ | Key-value UI strings | `dictionary` | Error messages, button labels, form labels |
162
+
163
+ ---
164
+
165
+ ## Common Mistakes
166
+
167
+ | Mistake | Correct Approach |
168
+ |---------|-----------------|
169
+ | Extracting CSS values as content | Only extract user-visible text |
170
+ | Creating one model per component | Group related content into shared models |
171
+ | Skipping dry-run | ALWAYS preview before apply |
172
+ | Auto-merging normalize changes | Normalize ALWAYS uses review mode |
173
+ | Reusing before extraction is merged | Wait for extraction review and merge first |
174
+ | Processing all models in one reuse | Scope reuse to one model/domain at a time |
175
+ | Ignoring project graph | Use graph output to understand component relationships |
176
+ | Hardcoding replacement patterns | Detect the project's i18n stack and use its conventions |
177
+ | Extracting strings with runtime variables | Split parameterized strings: extract the static part, leave interpolation in code |
178
+
179
+ ---
180
+
181
+ ## References
182
+
183
+ - [Extraction Details](references/extraction.md) -- Phase 1 extraction rules, parameters, and examples
184
+ - [Reuse Details](references/reuse.md) -- Phase 2 replacement expressions by stack and patching rules
185
+ - [What Is Content](references/what-is-content.md) -- Heuristics for identifying content vs code strings