@contentrain/types 1.9.0 → 1.10.0
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.
- package/README.md +3 -2
- package/dist/index.d.mts +45 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +37 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Validate functions (pure, dependency-free):
|
|
|
117
117
|
- `validateSlug(slug)` — kebab-case slug validation
|
|
118
118
|
- `validateEntryId(id)` — entry ID format validation
|
|
119
119
|
- `validateLocale(locale, config)` — locale format + config support check
|
|
120
|
-
- `detectSecrets(value)` — detect potential secrets in field values
|
|
120
|
+
- `detectSecrets(value)` — detect potential secrets in field values (provider-shaped patterns, plus an `api_key = …` assignment whose tail passes `looksLikeCredential`)
|
|
121
121
|
- `validateFieldValue(value, fieldDef)` — full field schema validation (type, required, min/max, pattern, select)
|
|
122
122
|
|
|
123
123
|
Serialize functions (pure, dependency-free):
|
|
@@ -137,7 +137,7 @@ Constants:
|
|
|
137
137
|
- `ENTRY_ID_PATTERN` — regex for valid entry IDs
|
|
138
138
|
- `LOCALE_PATTERN` — regex for valid locale codes
|
|
139
139
|
- `CANONICAL_JSON` — serialization rules (indent, encoding, trailing newline, key sort)
|
|
140
|
-
- `SECRET_PATTERNS` — regex patterns for secret detection
|
|
140
|
+
- `SECRET_PATTERNS` — provider-shaped regex patterns for secret detection (the generic `api_key` rule lives in `detectSecrets`, gated by `looksLikeCredential`)
|
|
141
141
|
|
|
142
142
|
## Stability
|
|
143
143
|
|
|
@@ -246,6 +246,7 @@ Studio (Nuxt 4, web) cannot import `@contentrain/mcp` directly because MCP depen
|
|
|
246
246
|
| `serializeMarkdownFrontmatter(data, body)` | Document editor serialization |
|
|
247
247
|
| `generateEntryId()` | Client-side entry ID generation |
|
|
248
248
|
| `SECRET_PATTERNS` | Extend or customize secret detection |
|
|
249
|
+
| `looksLikeCredential(tail)` | Decide whether a value assigned to an API-key setting is a credential or documentation |
|
|
249
250
|
|
|
250
251
|
### What stays in MCP (not available to Studio directly)
|
|
251
252
|
|
package/dist/index.d.mts
CHANGED
|
@@ -427,11 +427,37 @@ declare const CHROME_BODY_SLOT = "<!--@@body@@-->";
|
|
|
427
427
|
* `CHROME_BODY_SLOT` where content goes (preferred — nesting-safe); the legacy
|
|
428
428
|
* `before_body`/`after_body` pair is composed into a single body with the slot
|
|
429
429
|
* between them (only correct when the content container is top-level).
|
|
430
|
+
*
|
|
431
|
+
* `header` and `footer` lift a region OUT of the body chrome into its own Astro
|
|
432
|
+
* component, rendered as a sibling of the body fragment. They are for the two
|
|
433
|
+
* regions a site actually shares — the masthead and the colophon — and they buy
|
|
434
|
+
* two things one blob cannot: families that carry the same header emit ONE
|
|
435
|
+
* component instead of N copies of the markup, and the nav lives at a single
|
|
436
|
+
* address, which is where a jQuery-free menu replaces the theme's.
|
|
437
|
+
*
|
|
438
|
+
* The producer emits them ONLY after verifying both properties:
|
|
439
|
+
*
|
|
440
|
+
* 1. **Balanced.** The fragment closes every element it opens. Splitting chrome
|
|
441
|
+
* at an arbitrary point produces halves the parser silently repairs — the
|
|
442
|
+
* failure that cost a page 36 against 100 and the reason `body` exists.
|
|
443
|
+
* 2. **Outside the content path.** The region sits wholly before (header) or
|
|
444
|
+
* after (footer) `CHROME_BODY_SLOT` and is not an ancestor of it. A region
|
|
445
|
+
* that wraps the content cannot be lifted; it belongs in `body`.
|
|
446
|
+
*
|
|
447
|
+
* When either is in doubt, keep everything in one `body` chunk: a single blob
|
|
448
|
+
* is always correct, and this split is an optimisation on top of it.
|
|
430
449
|
*/
|
|
431
450
|
interface ChromeChunk {
|
|
432
451
|
id: string;
|
|
433
|
-
position: 'head' | 'body' | 'before_body' | 'after_body';
|
|
452
|
+
position: 'head' | 'body' | 'before_body' | 'after_body' | 'header' | 'footer';
|
|
434
453
|
html: string;
|
|
454
|
+
/**
|
|
455
|
+
* Shared component name for a `header`/`footer` chunk — `SiteHeader`,
|
|
456
|
+
* `BlogFooter`. Chunks that carry the same name AND the same html collapse
|
|
457
|
+
* into one emitted component; same name with different html gets a suffixed
|
|
458
|
+
* name and a warning, never a silent swap. Default: `SiteHeader`/`SiteFooter`.
|
|
459
|
+
*/
|
|
460
|
+
component?: string;
|
|
435
461
|
}
|
|
436
462
|
interface ComponentPlacement {
|
|
437
463
|
/** ComponentDef id. */
|
|
@@ -1539,6 +1565,11 @@ interface ContentrainError {
|
|
|
1539
1565
|
*
|
|
1540
1566
|
* A check that fires on the wrong thing is not a strict check; it is a check
|
|
1541
1567
|
* people learn to scroll past.
|
|
1568
|
+
*
|
|
1569
|
+
* Every pattern here carries a provider-specific shape — a fixed prefix, a
|
|
1570
|
+
* credential-only URL form, a PEM header — and is trusted on its own. The one
|
|
1571
|
+
* generic rule, `api_key = …`, has no such shape and lives in
|
|
1572
|
+
* {@link API_KEY_ASSIGNMENT}, where what it captures is checked before it fires.
|
|
1542
1573
|
*/
|
|
1543
1574
|
declare const SECRET_PATTERNS: ReadonlyArray<RegExp>;
|
|
1544
1575
|
/** Validate a slug — returns error message or null if valid */
|
|
@@ -1547,6 +1578,18 @@ declare function validateSlug(slug: string): string | null;
|
|
|
1547
1578
|
declare function validateEntryId(id: string): string | null;
|
|
1548
1579
|
/** Validate a locale code against pattern and config — returns error message or null if valid */
|
|
1549
1580
|
declare function validateLocale(locale: string, config: ContentrainConfig): string | null;
|
|
1581
|
+
/**
|
|
1582
|
+
* Does a value assigned to an API-key setting look like a credential rather
|
|
1583
|
+
* than prose about one?
|
|
1584
|
+
*
|
|
1585
|
+
* Two shapes are documentation, not secrets: a tail with no digit at all
|
|
1586
|
+
* (`your_project_api_key_here`, `xxxxxxxxxxxxxxxxxxxx`), and a tail whose
|
|
1587
|
+
* `_`/`-` separated tokens are each purely letters or purely digits
|
|
1588
|
+
* (`my_api_key_2024`, `REPLACE-WITH-KEY-1`) — words with a version number. An
|
|
1589
|
+
* issued key mixes letters and digits inside one token (`8f14e45fceea167a`,
|
|
1590
|
+
* `k1p8-q7x2m9v4z3w6`), which no sentence does by accident.
|
|
1591
|
+
*/
|
|
1592
|
+
declare function looksLikeCredential(tail: string): boolean;
|
|
1550
1593
|
/** Detect potential secrets in a value — returns validation errors if found */
|
|
1551
1594
|
declare function detectSecrets(value: unknown): ValidationError[];
|
|
1552
1595
|
/**
|
|
@@ -1660,5 +1703,5 @@ declare function parseMarkdownFrontmatter(content: string): {
|
|
|
1660
1703
|
/** Serialize data + body into markdown frontmatter format */
|
|
1661
1704
|
declare function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string;
|
|
1662
1705
|
//#endregion
|
|
1663
|
-
export { type ApplyPlanInput, AssetEntry, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, CommentsExport, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DICTIONARY_TITLE_FIELD, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EntryMeta, EntrySourceMap, EntrySourceRef, ExcerptSource, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GraphNode, HandoffCapability, HandoffComments, HandoffOffer, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PATH_PATTERNS, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, type ProviderCapabilities, QueryBinding, RAW_MARK_SUFFIX, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawIR, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawSite, RawTerm, RawTermRef, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RootAttrs, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, StackType, StringContext, SyncResult, TITLE_FIELD_TYPES, ValidationError, ValidationResult, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, canonicalStringify, conflictId, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
|
|
1706
|
+
export { type ApplyPlanInput, AssetEntry, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, CommentsExport, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DICTIONARY_TITLE_FIELD, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EntryMeta, EntrySourceMap, EntrySourceRef, ExcerptSource, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GraphNode, HandoffCapability, HandoffComments, HandoffOffer, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PATH_PATTERNS, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, type ProviderCapabilities, QueryBinding, RAW_MARK_SUFFIX, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawIR, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawSite, RawTerm, RawTermRef, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RootAttrs, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, StackType, StringContext, SyncResult, TITLE_FIELD_TYPES, ValidationError, ValidationResult, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, canonicalStringify, conflictId, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
|
|
1664
1707
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/migration.ts","../src/provider.ts","../src/index.ts"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/migration.ts","../src/provider.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -209,12 +209,16 @@ const CONTENTRAIN_BRANCH = "contentrain";
|
|
|
209
209
|
*
|
|
210
210
|
* A check that fires on the wrong thing is not a strict check; it is a check
|
|
211
211
|
* people learn to scroll past.
|
|
212
|
+
*
|
|
213
|
+
* Every pattern here carries a provider-specific shape — a fixed prefix, a
|
|
214
|
+
* credential-only URL form, a PEM header — and is trusted on its own. The one
|
|
215
|
+
* generic rule, `api_key = …`, has no such shape and lives in
|
|
216
|
+
* {@link API_KEY_ASSIGNMENT}, where what it captures is checked before it fires.
|
|
212
217
|
*/
|
|
213
218
|
const SECRET_PATTERNS = [
|
|
214
219
|
/\b[sp]k_(?:live|test)_[A-Za-z0-9]{6,}/,
|
|
215
220
|
/\bgh[pousr]_[A-Za-z0-9]{10,}/,
|
|
216
221
|
/\bBearer\s+(?![<${{]|YOUR[_-]|your[_-]|xxx|XXX|\.{3})[A-Za-z0-9._~+/-]{20,}/,
|
|
217
|
-
/\bapi[_-]?key\s*[:=]\s*['"`]?[A-Za-z0-9_-]{16,}/i,
|
|
218
222
|
/\baws_secret[_a-z]*\s*[:=]\s*['"`]?[A-Za-z0-9/+=]{20,}/i,
|
|
219
223
|
/\bAKIA[0-9A-Z]{16}\b/,
|
|
220
224
|
/\b(?:postgres(?:ql)?|mysql|mongodb)(?:\+srv)?:\/\/[^\s:@/]+:[^\s@/]+@/,
|
|
@@ -238,10 +242,40 @@ function validateLocale(locale, config) {
|
|
|
238
242
|
if (!config.locales.supported.includes(locale)) return `Locale "${locale}" is not in supported locales [${config.locales.supported.join(", ")}]. Update config first.`;
|
|
239
243
|
return null;
|
|
240
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* An API key being assigned a value: `api_key = <tail>`.
|
|
247
|
+
*
|
|
248
|
+
* Generic by nature — there is no provider prefix, so the word plus a separator
|
|
249
|
+
* is the only structural signal — and therefore the one rule that also has to
|
|
250
|
+
* look at what it captured before it fires; see {@link looksLikeCredential}.
|
|
251
|
+
* Without that, `api_key = your_project_api_key_here` in a setup guide was a
|
|
252
|
+
* blocking error, and once `contentrain validate` exited non-zero, a permanent
|
|
253
|
+
* CI failure for any project whose content discusses API keys — which, for a
|
|
254
|
+
* CMS, is ordinary content. Placeholders documentation uses (`<key>`, `$KEY`,
|
|
255
|
+
* `{{key}}`, `YOUR_KEY`, `xxx`, `…`) are excluded up front, as for `Bearer`.
|
|
256
|
+
*/
|
|
257
|
+
const API_KEY_ASSIGNMENT = /\bapi[_-]?key\s*[:=]\s*['"`]?(?![<${{]|YOUR[_-]|your[_-]|xxx|XXX|\.{3})([A-Za-z0-9_-]{16,})/gi;
|
|
258
|
+
/**
|
|
259
|
+
* Does a value assigned to an API-key setting look like a credential rather
|
|
260
|
+
* than prose about one?
|
|
261
|
+
*
|
|
262
|
+
* Two shapes are documentation, not secrets: a tail with no digit at all
|
|
263
|
+
* (`your_project_api_key_here`, `xxxxxxxxxxxxxxxxxxxx`), and a tail whose
|
|
264
|
+
* `_`/`-` separated tokens are each purely letters or purely digits
|
|
265
|
+
* (`my_api_key_2024`, `REPLACE-WITH-KEY-1`) — words with a version number. An
|
|
266
|
+
* issued key mixes letters and digits inside one token (`8f14e45fceea167a`,
|
|
267
|
+
* `k1p8-q7x2m9v4z3w6`), which no sentence does by accident.
|
|
268
|
+
*/
|
|
269
|
+
function looksLikeCredential(tail) {
|
|
270
|
+
if (!/\d/.test(tail)) return false;
|
|
271
|
+
const tokens = tail.split(/[_-]+/).filter(Boolean);
|
|
272
|
+
if (tokens.length > 1 && tokens.every((t) => /^[A-Za-z]+$/.test(t) || /^\d+$/.test(t))) return false;
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
241
275
|
/** Detect potential secrets in a value — returns validation errors if found */
|
|
242
276
|
function detectSecrets(value) {
|
|
243
277
|
if (typeof value !== "string") return [];
|
|
244
|
-
if (SECRET_PATTERNS.some((p) => p.test(value))) return [{
|
|
278
|
+
if (SECRET_PATTERNS.some((p) => p.test(value)) || [...value.matchAll(API_KEY_ASSIGNMENT)].some((m) => looksLikeCredential(m[1]))) return [{
|
|
245
279
|
severity: "error",
|
|
246
280
|
message: "Potential secret detected in value"
|
|
247
281
|
}];
|
|
@@ -757,6 +791,6 @@ function serializeMarkdownFrontmatter(data, body) {
|
|
|
757
791
|
return lines.join("\n");
|
|
758
792
|
}
|
|
759
793
|
//#endregion
|
|
760
|
-
export { CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, DICTIONARY_TITLE_FIELD, ENTRY_ID_PATTERN, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, MIGRATION_CONTRACT_VERSION, MODEL_FIELD_ORDER, PATH_PATTERNS, RAW_MARK_SUFFIX, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, TITLE_FIELD_TYPES, canonicalStringify, conflictId, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
|
|
794
|
+
export { CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, DICTIONARY_TITLE_FIELD, ENTRY_ID_PATTERN, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, MIGRATION_CONTRACT_VERSION, MODEL_FIELD_ORDER, PATH_PATTERNS, RAW_MARK_SUFFIX, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, TITLE_FIELD_TYPES, canonicalStringify, conflictId, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
|
|
761
795
|
|
|
762
796
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/migration.ts","../src/provider.ts","../src/index.ts"],"sourcesContent":["// ─── Migration Contracts ───\n//\n// Shared, MIT-licensed shapes for the WordPress → static-site migration pipeline.\n// Producers: the WordPress Bridge plugin (GPL, separate repo), REST/WXR importers.\n// Consumers: the migration engine, Studio, and the open Astro emitter.\n//\n// Every shape here is plain JSON — snake_case keys, no class instances, no\n// functions — because these documents cross process, repository, and license\n// boundaries. A GPL plugin writes RawIR; a proprietary service reads it; an MIT\n// emitter consumes ProjectIR. The contract is the only thing they share, so it\n// lives in the one package all of them may depend on.\n\n/**\n * Version stamped into every migration document (`version` field on each root).\n * The four contracts evolve together with this package; a reader that sees a\n * higher version than it knows should refuse rather than guess.\n */\nexport const MIGRATION_CONTRACT_VERSION = 1\n\n// ─── Source access ───\n\n/**\n * How the WordPress site's data was reached, ordered from least to most\n * complete. Measured field coverage rises with each rung (roughly 34% for\n * public REST, 57% with an Application Password, 82% from a WXR export, 100%\n * via the Bridge plugin) — which is why provenance is recorded on every RawIR:\n * two documents for the same site are only comparable if their rungs are.\n */\nexport type SourceAccessKind = 'rest_public' | 'rest_auth' | 'wxr' | 'bridge'\n\n/** The ladder in ascending completeness order. */\nexport const SOURCE_ACCESS_LADDER = [\n 'rest_public',\n 'rest_auth',\n 'wxr',\n 'bridge',\n] as const satisfies readonly SourceAccessKind[]\n\n// ─── RawIR ───\n//\n// Source-faithful extraction of a WordPress site: what the source said, before\n// any interpretation. Unresolved references are kept and *marked* (`resolved`\n// flags) instead of dropped — deciding what a broken reference means is the\n// consumer's job, not the extractor's.\n\nexport interface RawProvenance {\n kind: SourceAccessKind\n /** ISO 8601 UTC time the extraction ran. */\n fetched_at?: string\n /** Name/version of the producing tool (e.g. \"wordpress-bridge/1.0\"). */\n tool?: string\n}\n\nexport interface RawSite {\n url: string\n title?: string\n description?: string\n /** WXR distinguishes the WP install URL from the public site URL. */\n base_site_url?: string\n base_blog_url?: string\n /** Site language as reported (e.g. \"en-US\"); multilingual detail lives in `language_pairs`. */\n language?: string | null\n /** Generator string (WordPress version) when the source exposed it. */\n generator?: string | null\n /** WXR export timestamp, when the source was a WXR file. */\n export_date?: string | null\n wxr_version?: string | null\n}\n\nexport interface RawAuthor {\n id: number | null\n login: string\n email?: string | null\n display_name: string\n first_name?: string | null\n last_name?: string | null\n}\n\nexport interface RawTerm {\n id: number | null\n taxonomy: string\n slug: string\n name: string\n /** Parent term slug within the same taxonomy, when hierarchical. */\n parent?: string | null\n /** Whether `parent` names a term present in this document. */\n parent_resolved?: boolean | null\n description?: string\n}\n\n/** A post's reference to a term — kept even when the term itself is absent. */\nexport interface RawTermRef {\n taxonomy: string\n slug: string\n name: string\n /** Whether the referenced term exists in this document's `terms`. */\n resolved: boolean\n}\n\n/** An ACF field value paired with its field-definition key (`field_…`). */\nexport interface RawAcfValue {\n value: unknown\n field_key: string\n}\n\nexport interface RawPost {\n id: number\n /** WordPress post type — \"post\", \"page\", or a custom post type slug. */\n type: string\n /** WordPress status verbatim (\"publish\", \"draft\", \"future\", \"pending\", …). */\n status: string\n slug: string\n title: string\n /** Public permalink, when known. Permalink *structure* is a site setting; the link is the fact. */\n link?: string | null\n guid?: string | null\n /** Author login; resolve against `RawIR.authors`. */\n author: string | null\n /** ISO 8601 UTC, null when the source carried no usable date. */\n date: string | null\n modified: string | null\n /** Rendered/exported HTML body, untransformed. */\n content: string\n excerpt: string\n parent?: number | null\n menu_order?: number\n sticky?: boolean\n password?: string | null\n comment_status?: string | null\n ping_status?: string | null\n terms: RawTermRef[]\n /**\n * Post meta, PHP-serialized values already decoded where possible.\n * Core WP keys (underscore-prefixed) are included: which keys matter is a\n * downstream decision.\n */\n meta: Record<string, unknown>\n /** Meta keys whose values were PHP-serialized in the source (decoded above). */\n serialized_keys?: string[]\n /** ACF fields recovered by pairing `foo` with `_foo = \"field_…\"`. */\n acf?: Record<string, RawAcfValue>\n}\n\nexport interface RawAttachment {\n id: number\n title: string\n slug: string\n /** Original file URL. Downloading/re-hosting is a consumer concern. */\n url: string | null\n alt?: string\n caption?: string\n description?: string\n /** Relative upload path (`_wp_attached_file`), when present. */\n file?: string | null\n /** `_wp_attachment_metadata` verbatim (sizes, EXIF, …). */\n image_meta?: unknown\n mime?: string | null\n parent?: number | null\n parent_resolved?: boolean | null\n author?: string | null\n date?: string | null\n status?: string\n meta?: Record<string, unknown>\n}\n\n/** Where a menu item points. `resolved` says whether the target exists in this document. */\nexport type RawMenuTarget =\n | { kind: 'url'; url: string; resolved: true }\n | { kind: 'post'; post_type: string; id: number | null; slug: string | null; resolved: boolean }\n | { kind: 'term'; taxonomy: string; id: number | null; slug: string | null; resolved: boolean }\n | { kind: 'archive'; post_type: string; resolved: true }\n | { kind: 'unknown'; resolved: false }\n\nexport interface RawMenuItem {\n id: number\n title: string\n order?: number\n /** Parent menu-item id for nested menus. */\n parent?: number | null\n parent_unresolved?: boolean\n url?: string | null\n target: RawMenuTarget\n /** `target` attribute for the rendered link (e.g. \"_blank\"). */\n target_attr?: string | null\n classes?: string[]\n description?: string\n status?: string\n}\n\nexport interface RawMenu {\n id: number | null\n slug: string\n name: string\n items: RawMenuItem[]\n}\n\nexport interface RawComment {\n id: number\n /** Post id this comment belongs to. */\n post: number\n post_type?: string\n parent?: number | null\n parent_resolved?: boolean | null\n author: string\n email?: string | null\n url?: string | null\n /**\n * ISO 8601 UTC. Producers MUST normalize to UTC — comment import fidelity\n * (\"zero record and parent loss\") includes `created_at`, and a site-local\n * date silently corrupts it. When normalization is impossible, ship the\n * source's GMT column in `date_gmt` and leave `date` null.\n */\n date: string | null\n date_gmt?: string | null\n content: string\n /**\n * WordPress approval flag. Fixed vocabulary: `'1'` (approved), `'0'`\n * (pending), `'spam'`, `'trash'` — consumers map these; unknown strings\n * pass through for forward compatibility, never dropped.\n */\n approved?: '1' | '0' | 'spam' | 'trash' | (string & {})\n type?: string\n user_id?: number | null\n meta?: Record<string, unknown>\n}\n\n/** A redirect rule (e.g. from the Redirection plugin — visible from the authenticated rung up). */\nexport interface RawRedirect {\n from: string\n to: string\n status?: number\n /** Which plugin/table produced the rule. */\n source?: string\n}\n\n/** Translation grouping for multilingual sites (Polylang/WPML — bridge rung). */\nexport interface RawLanguagePair {\n post: number\n /** locale → post id of the translation. */\n translations: Record<string, number>\n}\n\n/**\n * The complete raw extraction of one WordPress site at one access rung.\n * This is the boundary document between extraction (Bridge/importers) and\n * everything downstream — content conversion, capability analysis, migration.\n */\nexport interface RawIR {\n version: number\n provenance: RawProvenance\n site: RawSite\n authors: RawAuthor[]\n terms: RawTerm[]\n posts: RawPost[]\n attachments: RawAttachment[]\n menus?: RawMenu[]\n comments?: RawComment[]\n redirects?: RawRedirect[]\n language_pairs?: RawLanguagePair[]\n /** Site options (bridge rung), verbatim. */\n options?: Record<string, unknown>\n}\n\n// ─── CapabilityManifest ───\n//\n// What the site *uses*, as detected — the input for migration planning,\n// effort/pricing, and the \"what happens to X\" conversation with the user.\n// Detection is evidence-based and never certain; `present: true` with evidence\n// beats a silent guess.\n\nexport const CAPABILITY_KEYS = [\n 'seo',\n 'forms',\n 'comments',\n 'search',\n 'newsletter',\n 'analytics',\n 'consent',\n 'share',\n 'video',\n 'ads',\n 'builder',\n 'blocks',\n 'i18n',\n 'cache',\n 'security',\n 'acf',\n 'ecommerce',\n 'jetpack',\n 'redirects',\n 'membership',\n 'accessibility',\n 'media',\n 'scheduling',\n] as const\n\nexport type CapabilityKey = (typeof CAPABILITY_KEYS)[number]\n\nexport interface CapabilityDetection {\n present: boolean\n /** Identified plugin/provider, when known (e.g. \"yoast\", \"contact-form-7\"). */\n plugin?: string | null\n /** How it was detected: \"rest\", \"dom\", \"slug\", \"headers\", \"meta\", … */\n evidence?: string[]\n detail?: string\n}\n\nexport interface CapabilityManifest {\n version: number\n site_url: string\n access: {\n html_status?: number | null\n rest_status?: number | null\n /** Highest source-access rung actually achieved for this site. */\n achieved: SourceAccessKind\n }\n generator?: string | null\n theme?: string | null\n plugins?: string[]\n rest_namespaces?: string[]\n custom_post_types?: Array<{\n slug: string\n /** Whether the CPT is exposed in public REST (`rest: false` CPTs need a higher rung). */\n rest_visible: boolean\n count?: number | null\n }>\n /** Comments get first-class treatment: they are a launch-critical capability. */\n comments: {\n active: boolean\n form_status?: 'open' | 'closed' | 'mixed' | null\n rest_total?: number | null\n plugin?: string | null\n }\n capabilities: Partial<Record<CapabilityKey, CapabilityDetection>>\n /** Dynamic front-end behaviors observed in the DOM (\"carousel\", \"modal\", \"search-overlay\", …). */\n behaviors?: string[]\n languages?: string[]\n}\n\n// ─── ProjectIR ───\n//\n// The reproducible model of the site: not \"this page's HTML\" but the design\n// system, component architecture, route model, and content queries that let\n// *unseen* pages — tomorrow's posts, another category — be generated correctly.\n// Produced by analysis; consumed by the open emitter. Three layers are kept\n// together on purpose: visual structure (families/chrome/tokens), content\n// binding (slots), and business rules (routes/queries). A page family exists\n// per genuinely different behavior, not per page.\n\n/**\n * How the legacy CSS for a family was packaged.\n * - `purge_set`: rules kept if used by any page in the family's page set —\n * small file, safe against per-page rule loss (single-page purging is not).\n * - `localcss`: the site's stylesheets localized wholesale — larger, zero risk.\n */\nexport type CssStrategy = 'purge_set' | 'localcss'\n\n/**\n * Cascade layer name the emitter quarantines legacy CSS under, so a modern\n * utility layer (e.g. Tailwind) can coexist with pixel-faithful migrated pages.\n */\nexport const LEGACY_CSS_LAYER = 'legacy'\n\n/** Design tokens extracted from the source site, e.g. for a Tailwind `@theme` block. */\nexport interface DesignTokens {\n colors?: Record<string, string>\n font_families?: Record<string, string>\n font_sizes?: Record<string, string>\n spacing?: Record<string, string>\n breakpoints?: Record<string, string>\n}\n\nexport type RouteKind =\n | 'front'\n | 'single'\n | 'page'\n | 'archive'\n | 'term'\n | 'author'\n | 'date'\n | 'search'\n | 'not_found'\n | 'custom'\n\nexport interface RouteParamDef {\n name: string\n /**\n * Where the value comes from. The date parts exist because\n * `/%year%/%monthnum%/%day%/%postname%/` is a common WordPress permalink\n * structure: treating the template post's date as a fixed prefix sends every\n * other post to a wrong address, and SEO continuity is the point of a\n * migration. Posts carry their own values (`EmitPost.params`).\n */\n source:\n | 'post_slug'\n | 'post_id'\n | 'post_year'\n | 'post_month'\n | 'post_day'\n | 'term_slug'\n | 'author_slug'\n | 'page_number'\n | 'custom'\n}\n\n/** Selects a family/component variant when a route parameter matches. */\nexport interface RouteVariantRule {\n param: string\n in: string[]\n variant: string\n}\n\n/**\n * One route pattern → one layout family. Pagination is a parameter of a route\n * (`/news/page/2` is `/news` with `page_number = 2`), never a separate family —\n * across a 30-site corpus, page 2 never produced a new layout.\n */\nexport interface RouteModel {\n id: string\n /**\n * URL pattern with `:param` placeholders — `/category/:term`, `/news/page/:page`.\n *\n * A trailing `*` marks a **multi-segment (rest) parameter**: `/category/:term*`\n * matches `/category/about-cc/events/` with `term = \"about-cc/events\"`.\n * Hierarchical taxonomies and nested pages need this — a single-segment\n * parameter silently flattens `/category/about-cc/events/` to\n * `/category/events/`, breaking link continuity for every nested address.\n */\n pattern: string\n kind: RouteKind\n /** LayoutFamily id. */\n family: string\n params?: RouteParamDef[]\n /**\n * Content collection this route generates one page per entry from. `single`\n * routes default to `posts`; naming a collection makes any route\n * collection-driven — a site's pages and custom post types are per-entry\n * routes too, they just are not called \"single\".\n *\n * Without a name per route they would all write to (and overwrite) the same\n * collection.\n */\n collection?: string\n /** QueryBinding id feeding this route's list, when it renders one. */\n query?: string\n /**\n * Document title for routes whose pages have no per-page title of their own\n * (a static page, an archive without `QueryPage.title`).\n */\n title?: string\n /**\n * Locale this route serves. A multilingual site has a route per language\n * (`/:slug`, `/en/:slug`) with its own family and query; the emitted page's\n * `lang` comes from here rather than from the project default.\n */\n locale?: string\n variant_rules?: RouteVariantRule[]\n}\n\nexport type SlotKind =\n | 'body'\n | 'title'\n | 'date'\n | 'excerpt'\n | 'author_name'\n | 'author_link'\n | 'featured_image'\n | 'term_list'\n | 'self_link'\n | 'custom'\n\n/** Where a content field lands in a family's markup. */\nexport interface SlotBinding {\n kind: SlotKind\n /** The region is printed only on posts that have this slot filled — see `CHROME_IF_OPEN`. */\n optional?: boolean\n /** The region repeats per list item (terms, authors) — see `CHROME_REPEAT_OPEN`. */\n repeat?: boolean\n /** CSS selector of the slot's container, when one could be determined. */\n selector?: string\n /** Content-model field backing a `custom` slot. */\n field?: string\n /** Date rendering format for `date` slots, as observed on the source site. */\n date_format?: string\n}\n\n/**\n * Marks whose name ends in this suffix are inserted as raw HTML instead of\n * being escaped. Escaping stays the default — content-derived text must never\n * be able to break the page — but some themes render a post's full content or\n * a link-bearing excerpt inside a list card, and escaping those prints markup\n * as text (measured on one theme's category page: 48.6).\n */\nexport const RAW_MARK_SUFFIX = '_html'\n\n/**\n * Repeat block: `<!--@@repeat:NAME@@-->…<!--@@/repeat@@-->`, optionally with a\n * separator — `<!--@@repeat:NAME|, @@-->`. The inner fragment renders once per\n * item of the list named NAME, with per-item marks `item` / `item_index` (and\n * `item_<key>` for object items).\n *\n * Fixed marks (`term0…termN`) cannot express a list whose length varies per\n * post: a template built from a 3-term post leaves stray separators on a\n * 6-term one (measured symptoms: `\"Business,\"`, `\"Releases, Events,\"`,\n * `\"Automattic, ,\"`), and the same class shows up with multiple authors.\n */\nexport const CHROME_REPEAT_OPEN = '<!--@@repeat:'\nexport const CHROME_REPEAT_CLOSE = '<!--@@/repeat@@-->'\n\n/**\n * Conditional block: `<!--@@if:NAME@@-->…<!--@@/if@@-->`, negated with\n * `<!--@@if:!NAME@@-->`. The fragment survives only when NAME has a value\n * (non-empty string, non-empty list).\n *\n * Route-parameter variants (`RouteVariantRule`) cannot express this: two posts\n * on the SAME route differ by whether the theme printed a region at all\n * (measured: five posts of one family scored 3.5–84.5, the only difference\n * being a featured block that some posts render and others do not).\n */\nexport const CHROME_IF_OPEN = '<!--@@if:'\nexport const CHROME_IF_CLOSE = '<!--@@/if@@-->'\n\n/**\n * Marker a list section's wrapper carries where its items go —\n * `<div class=\"grid\"><!--@@items@@--></div>`.\n */\nexport const LIST_ITEMS_SLOT = '<!--@@items@@-->'\n\n/**\n * Marker a `body`-position chrome chunk carries where the page content goes.\n * A comment survives serialization, renders as nothing if ever left behind,\n * and — critically — can sit at ANY nesting depth: real themes put the content\n * container deep inside the chrome (`article > div.entry-content`), so the\n * chrome cannot be split into balanced before/after fragments. The emitter\n * splices content in at this marker and injects the result as ONE fragment.\n */\nexport const CHROME_BODY_SLOT = '<!--@@body@@-->'\n\n/**\n * A rendered, asset-rewritten chunk of site chrome the emitter injects verbatim.\n * Positions: `head` lands in `<head>`; `body` is the whole body chrome carrying\n * `CHROME_BODY_SLOT` where content goes (preferred — nesting-safe); the legacy\n * `before_body`/`after_body` pair is composed into a single body with the slot\n * between them (only correct when the content container is top-level).\n */\nexport interface ChromeChunk {\n id: string\n position: 'head' | 'body' | 'before_body' | 'after_body'\n html: string\n}\n\nexport interface ComponentPlacement {\n /** ComponentDef id. */\n component: string\n variant?: string\n selector?: string\n}\n\nexport interface FamilyVariant {\n key: string\n description?: string\n}\n\n/**\n * Attributes on the document's root elements, carried verbatim from the source\n * page. Themes hang layout on them — WordPress writes `<body class=\"wp-singular\n * post-template-default single …\">` and scripts add `<html class=\"js wf-…\">`,\n * and the stylesheet's container rules key off exactly those classes. Dropping\n * them costs a correct-content page its entire layout (measured: 36.4 vs 100).\n *\n * Values may carry `@@mark@@` placeholders, so per-page classes (`postid-123`)\n * survive the same filling as chrome.\n *\n * Whether a site needs them is not knowable from one page: the same test that\n * moved 10up 36.4 → 100 moved wptavern 36 → 36, because that theme does not key\n * off body classes. Carry them always.\n */\nexport interface RootAttrs {\n html?: Record<string, string>\n body?: Record<string, string>\n}\n\nexport interface LayoutFamily {\n id: string\n name?: string\n kind?: RouteKind\n chrome?: ChromeChunk[]\n /** `<html>` / `<body>` attributes from the source page — see `RootAttrs`. */\n root_attrs?: RootAttrs\n slots?: SlotBinding[]\n components?: ComponentPlacement[]\n css: {\n strategy: CssStrategy\n /**\n * Stylesheets every page of the family loads.\n *\n * Page-builder sites emit CSS per page (`post-11368.css`,\n * `local/global-11368-frontend-*`), so one member's stylesheet set is not\n * the family's: building from a single template page scored 35.6, while\n * the UNION of the members' stylesheets scored 100. Producers put the\n * union here and the per-page remainder on the page itself.\n */\n files?: string[]\n }\n /** Observed column counts per viewport class (e.g. desktop 4 → mobile 1). */\n columns?: Partial<Record<'desktop' | 'mobile', number>>\n variants?: FamilyVariant[]\n /** Verification trail: which pages formed the family, held-out fidelity score. */\n evidence?: {\n pages?: string[]\n holdout_score?: number\n }\n}\n\n/**\n * Component semantic vocabulary. `source` records where its content can come\n * from: `rest` (derivable from content data — author box, term list),\n * `runtime` (needs a live service — comments form, search), `chrome` (carried\n * as rendered markup — nav, ads).\n */\nexport const COMPONENT_TYPES = [\n 'nav',\n 'related',\n 'comments',\n 'ads',\n 'chrome',\n 'taxonomy',\n 'author',\n 'share',\n 'breadcrumb',\n 'meta',\n 'pagination',\n 'card',\n 'custom',\n] as const\n\nexport type ComponentType = (typeof COMPONENT_TYPES)[number]\n\nexport type ComponentSource = 'rest' | 'runtime' | 'chrome'\n\nexport interface ComponentVariantDef {\n key: string\n conditions?: RouteVariantRule[]\n description?: string\n}\n\nexport interface ComponentDef {\n id: string\n type: ComponentType\n source: ComponentSource\n name?: string\n selector?: string\n /** e.g. a card component with `news`, `compact`, `featured` variants. */\n variants?: ComponentVariantDef[]\n}\n\nexport type ExcerptSource = 'excerpt' | 'content_first_paragraph' | 'none'\nexport type PaginationKind = 'numbered' | 'infinite' | 'none'\n\n/**\n * The query behind a rendered list: which content, filtered how, in what\n * order, how many. Inferred by matching rendered items against candidate\n * queries — order matters, so the sequence is part of the contract.\n */\nexport interface QueryBinding {\n id: string\n /** Content source: \"posts\", \"pages\", a CPT slug, or a content-model id. */\n source: string\n taxonomy?: {\n taxonomy: string\n /** Fixed term slug, or the route param carrying it. */\n term?: string\n term_param?: string\n }\n author?: {\n slug?: string\n param?: string\n }\n order: {\n by: 'date' | 'title' | 'menu_order' | 'custom'\n direction: 'asc' | 'desc'\n }\n per_page: number | null\n pagination: PaginationKind\n excerpt_source?: ExcerptSource\n /** WordPress image size name the list renders (e.g. \"medium_large\"). */\n image_size?: string\n /** Locale this query is filtered to, on a multilingual site. */\n locale?: string\n}\n\n/**\n * - `responsive`: one build, CSS handles viewports.\n * - `split`: desktop and mobile variants are produced separately and served\n * by device — for sites whose mobile DOM differs beyond CSS.\n */\nexport type ViewportStrategy = 'responsive' | 'split'\n\nexport interface ProjectIR {\n version: number\n site: {\n url: string\n title?: string\n locales?: string[]\n }\n routes: RouteModel[]\n families: LayoutFamily[]\n components?: ComponentDef[]\n queries?: QueryBinding[]\n tokens?: DesignTokens\n /** Default CSS packaging for families that don't override it. */\n css_default: CssStrategy\n viewport_strategy?: ViewportStrategy\n /** Content-model ids this project's queries and slots refer to. */\n content_models?: string[]\n}\n\n// ─── Comments export ───\n//\n// Comments cross one more boundary than the rest of RawIR: they leave the\n// generated static site entirely and land in a live service's database,\n// addressed by content entry — not by WordPress post id. This export is that\n// bridge: the source map translates WP ids to entry addresses, and the\n// comments ride along unmodified.\n\nexport const COMMENTS_EXPORT_FORMAT = 'contentrain-comments@1'\n\n/** Where one WordPress post's content ended up. */\nexport interface EntrySourceRef {\n model_id: string\n entry_id: string\n locale?: string\n}\n\n/**\n * WordPress post id (stringified) → content entry address. Only the tool that\n * wrote the content store can produce this; nothing downstream can recover it.\n */\nexport type EntrySourceMap = Record<string, EntrySourceRef>\n\nexport interface CommentsExport {\n version: number\n format: typeof COMMENTS_EXPORT_FORMAT\n source: RawProvenance\n site_url?: string\n /** ISO 8601 UTC. */\n generated_at: string\n entries: EntrySourceMap\n /** WP post ids whose comment form was closed — the receiving side opens those threads closed. */\n threads_closed?: number[]\n /** Verbatim `RawComment`s; parents are re-linked via `RawComment.parent` in a second pass. */\n comments: RawComment[]\n}\n\n/** Comments summary + payload pointer on the handoff (capability counts are not enough for intake). */\nexport interface HandoffComments {\n total: number\n by_status?: Record<string, number>\n types?: Record<string, number>\n export?: {\n format: typeof COMMENTS_EXPORT_FORMAT\n /** Where the full export can be fetched… */\n url?: string\n /** …or the export itself, inline, for small sites. */\n inline?: CommentsExport\n }\n threads_closed?: number[]\n unresolved?: Array<{ comment_id: number; post: number; reason: string }>\n}\n\n// ─── MigrationHandoff ───\n//\n// What the migration hands the user: where the generated project lives, what\n// happened to each detected capability, and — for capabilities that need a\n// runtime — the offers. Offering is this document's job; fulfilling an offer\n// is the receiving product's. The migration itself is complete without any\n// offer being accepted.\n\nexport type CapabilityDisposition =\n | 'migrated_static'\n | 'archived'\n | 'needs_runtime'\n | 'external_adapter'\n | 'kept_on_wordpress'\n | 'dropped'\n\nexport interface HandoffCapability {\n key: CapabilityKey\n disposition: CapabilityDisposition\n detail?: string\n /** e.g. comments: { migrated: 412, spam_skipped: 60 } */\n counts?: Record<string, number>\n}\n\nexport type OfferProvider = 'studio_managed' | 'adapter' | 'keep_wordpress'\n\nexport interface CostEstimate {\n currency: string\n monthly: number\n assumptions?: string[]\n}\n\n/**\n * The comparison shown when a runtime capability is offered: what running the\n * old server costs (maintenance, security, hosting) against the managed\n * option — so declining is an informed choice, not a default.\n */\nexport interface CostComparison {\n self_host?: CostEstimate\n managed?: CostEstimate\n}\n\nexport interface HandoffOffer {\n capability: CapabilityKey\n provider: OfferProvider\n /** Adapter identifier when `provider` is `adapter`. */\n adapter?: string\n cost_comparison?: CostComparison\n /** e.g. \"keeping comments on WordPress means the WordPress server stays live\". */\n warning?: string\n}\n\nexport interface MigrationHandoff {\n version: number\n site_url: string\n /** ISO 8601 UTC. */\n generated_at: string\n repository?: {\n provider: 'github' | 'gitlab'\n owner: string\n name: string\n default_branch: string\n }\n preview_url?: string\n content_summary?: {\n models: number\n entries: number\n locales?: string[]\n }\n capabilities: HandoffCapability[]\n /** Present whenever the source had comments — see `HandoffComments`. */\n comments?: HandoffComments\n offers?: HandoffOffer[]\n notes?: string[]\n}\n","import type { SyncResult } from './index.js'\n\n// ─── Repository Provider Contracts ───\n//\n// Shared interfaces for the provider-agnostic content repository model used\n// by @contentrain/mcp. They live in @contentrain/types so third-party tools\n// can implement a custom RepoProvider (e.g. for a private git host, an\n// internal service, a mock in a test suite) without depending on MCP\n// internals.\n//\n// @contentrain/mcp re-exports every symbol here from\n// @contentrain/mcp/core/contracts so existing consumers do not have to\n// migrate imports.\n\n// ─── File change ───\n\n/**\n * A single file change within a plan.\n *\n * - `content: string` — write or overwrite the file with this UTF-8 content.\n * - `content: null` — delete the file.\n *\n * Paths are content-root relative, use forward slashes, and must not contain\n * `..` segments or absolute anchors. Providers are responsible for resolving\n * paths against their backing store (worktree, git tree, etc.).\n */\nexport interface FileChange {\n path: string\n content: string | null\n}\n\n// ─── Capabilities ───\n\n/**\n * Capabilities describe what a provider can and cannot do. Operations check\n * required capabilities before running. Normalize extract needs `sourceRead`;\n * normalize reuse needs `sourceWrite`; submit needs `pushRemote`; AST scans\n * need `astScan` (which implies a local working tree).\n *\n * The consistent position is: all git hosts are commodity MIT providers; the\n * distinction between providers is operational (how they read/write), not\n * commercial. Enterprise features live in Studio, not in capability gates.\n */\nexport interface ProviderCapabilities {\n /** Provider backs onto a local worktree and can selectively sync changes into the developer's working tree. */\n localWorktree: boolean\n /** Provider can read arbitrary source files outside `.contentrain/`. Required for normalize extract. */\n sourceRead: boolean\n /** Provider can write arbitrary source files outside `.contentrain/`. Required for normalize reuse. */\n sourceWrite: boolean\n /** Provider can push commits to a remote. Required for submit. */\n pushRemote: boolean\n /** Provider detects branch protection rules on the remote. */\n branchProtection: boolean\n /** Provider can open a pull request as a merge fallback when branch protection blocks direct merge. */\n pullRequestFallback: boolean\n /** Provider can execute AST scanners against source files. Implies local disk access. */\n astScan: boolean\n /**\n * Provider can write a two-parent merge commit (see\n * `RepoProvider.createMergeCommit`). Absent = false. Optional so that\n * existing external `implements` code keeps compiling: a required member\n * here would be a breaking change for every custom provider.\n */\n mergeCommit?: boolean\n}\n\n/** Capability set for the LocalProvider (simple-git + worktree). */\nexport const LOCAL_CAPABILITIES: ProviderCapabilities = {\n localWorktree: true,\n sourceRead: true,\n sourceWrite: true,\n pushRemote: true,\n branchProtection: false,\n pullRequestFallback: false,\n astScan: true,\n mergeCommit: true,\n}\n\n// ─── Reader ───\n\n/**\n * Read-only interface to a content repository.\n *\n * Paths are relative to the repository's content root (e.g.\n * `.contentrain/config.json`). The `ref` parameter is a branch name, tag,\n * or commit SHA. Providers that operate on a single working tree\n * (LocalReader) ignore `ref`; API-backed providers use it to resolve the\n * correct revision.\n *\n * `readFile` and `listDirectory` deliberately have different error semantics:\n * - `readFile` THROWS when the file is missing so callers must opt into\n * tolerance explicitly (typically with a try/catch returning a default).\n * - `listDirectory` returns `[]` for a missing directory because the empty\n * case is the common, uninteresting one.\n */\nexport interface RepoReader {\n /**\n * Read a file's contents as UTF-8.\n * @throws when the file does not exist or cannot be read.\n */\n readFile(path: string, ref?: string): Promise<string>\n\n /**\n * List file and directory names directly under `path`. Does not recurse.\n * Returns an empty array when the directory does not exist.\n */\n listDirectory(path: string, ref?: string): Promise<string[]>\n\n /** Check whether a file or directory exists at `path`. */\n fileExists(path: string, ref?: string): Promise<boolean>\n}\n\n// ─── Writer ───\n\nexport interface CommitAuthor {\n name: string\n email: string\n}\n\nexport interface Commit {\n sha: string\n message: string\n author: CommitAuthor\n timestamp: string\n /**\n * What the provider did with the branch. Providers that own the merge\n * decision (a local worktree honouring the project's `workflow` setting)\n * report it here; providers that hand the branch off to an external\n * orchestrator leave it undefined, and the caller treats that as\n * `pending-review`.\n */\n workflowAction?: 'auto-merged' | 'pending-review'\n /**\n * Selective-sync bookkeeping — which files were copied into the developer's\n * working tree and which were skipped because of local edits. Only\n * providers backed by a local worktree populate it.\n */\n sync?: SyncResult\n /** Non-fatal problem encountered while completing the write. */\n warning?: string\n /**\n * Whether the base branch advanced to the contentrain tip after an\n * auto-merge. Only providers that own the base advance (a local worktree)\n * report it. `blocked_diverged` is a partial success: the content is on\n * the contentrain branch; only the fast-forward is pending.\n */\n base_advance?: BaseAdvance\n /** Outcome of pushing the contentrain branch, when the provider pushes. */\n remote_push?: RemotePush\n}\n\n/**\n * Input to `applyPlan`. Represents a single atomic commit: all changes land\n * in one commit on `branch`, created from `base` if it does not yet exist.\n */\nexport interface ApplyPlanInput {\n /** Branch name to commit to. Created from `base` if missing. */\n branch: string\n /** File additions, modifications and deletions to apply in a single commit. */\n changes: FileChange[]\n /** Commit message. */\n message: string\n /** Commit author. */\n author: CommitAuthor\n /**\n * Optional base branch. Defaults to the Contentrain content-tracking\n * branch (`CONTENTRAIN_BRANCH` — the `contentrain` ref) — NOT the\n * repository's default branch. This is the single source of truth for\n * content state; every feature branch forks from it. Pass an explicit\n * `base` only when you know you want to bypass the invariant.\n */\n base?: string\n /**\n * Describes the operation that produced these changes. A provider that\n * regenerates `.contentrain/context.json` itself (the local worktree flow\n * does, after merge, single-threaded) uses it; others ignore it. It is\n * never committed on the feature branch — context.json embeds timestamps,\n * so two branches forked from the same commit would always conflict on it.\n */\n context?: { tool: string; model: string; locale?: string; entries?: string[] }\n /** Override the project's configured workflow for this write. */\n workflowOverride?: 'auto-merge' | 'review'\n}\n\n/**\n * Write-side interface. Providers implement this to persist a set of file\n * changes as a single atomic commit. LocalProvider writes through a worktree\n * and `git commit`; API-backed providers post to the Git Data API or\n * equivalent.\n */\nexport interface RepoWriter {\n applyPlan(input: ApplyPlanInput): Promise<Commit>\n}\n\n// ─── Branch / diff / merge ───\n\nexport interface Branch {\n name: string\n sha: string\n protected?: boolean\n}\n\nexport interface FileDiff {\n path: string\n status: 'added' | 'modified' | 'removed'\n before: string | null\n after: string | null\n}\n\nexport interface MergeResult {\n merged: boolean\n sha: string | null\n pullRequestUrl: string | null\n /**\n * Selective-sync bookkeeping — only populated by providers that back onto\n * a local worktree (LocalProvider). Remote-API providers (GitHub, GitLab,\n * etc.) omit it because they do not touch a developer's working tree.\n */\n sync?: SyncResult\n /**\n * Outcome of the post-merge source-branch cleanup on the remote.\n * `deleted: false` with `skipped` is an expected no-op (cleanup disabled,\n * no remote, ref already gone); `warning` is a real failure (offline,\n * auth) that did NOT fail the merge itself.\n */\n remote?: {\n deleted: boolean\n skipped?: string\n warning?: string\n }\n}\n\n// ─── Base-branch advance / remote push outcomes ───\n\n/**\n * What happened to the base branch after content landed on the\n * content-tracking branch.\n *\n * `advanced` — the base branch fast-forwarded to the contentrain tip.\n * `blocked_diverged` — the base branch holds commits that are not in\n * contentrain, so fast-forwarding was impossible. The content itself is\n * safe on the contentrain branch; only the advance is pending until the\n * branches are reconciled.\n *\n * These two values are shared vocabulary across the ecosystem — Studio\n * reports the same enum for its main-advance state. A pull request is an\n * attachment (`pullRequestUrl`), never a third state: \"PR opened\" is\n * `blocked_diverged` with a non-null URL.\n */\nexport type BaseAdvance = 'advanced' | 'blocked_diverged'\n\n/**\n * Outcome of pushing the content-tracking branch to the configured remote.\n *\n * `pushed` — the remote accepted the push (possibly after one\n * fetch-merge-retry). `rejected` — the push failed even after the retry;\n * local and remote have diverged and need reconciling. `no-remote` — the\n * repository has no configured remote, so nothing was attempted.\n */\nexport type RemotePush = 'pushed' | 'rejected' | 'no-remote'\n\n// ─── Reconcile / conflicts ───\n\n/**\n * Machine-readable conflict kinds produced by the content-aware three-way\n * reconcile planner. CLOSED SET: consumers (Studio) key their localized\n * editor questions on these values, so a free string would drift. Adding a\n * value here is a minor version bump and MUST carry a changelog entry;\n * renaming or removing one is breaking.\n */\nexport type ConflictCode =\n | 'field_value_conflict' // collection/singleton: same field changed differently on both sides\n | 'dictionary_value_conflict' // same dictionary key, two different values\n | 'vocabulary_value_conflict' // same term + locale, two different translations\n | 'model_key_conflict' // same model schema key changed differently\n | 'meta_status_conflict' // both sides moved an entry's publish status differently\n | 'document_body_conflict' // both sides edited a document body (never text-merged)\n | 'frontmatter_value_conflict' // same frontmatter key, two different values\n | 'delete_edit_conflict' // one side deleted what the other edited\n | 'file_conflict' // unrecognized file changed on both sides — resolved by choosing a side\n\n/**\n * One surviving conflict from a three-way reconcile: base (merge-base),\n * ours (content branch), theirs (base branch) disagree in a way the policy\n * table cannot resolve mechanically. Everything mechanical has already been\n * merged; only these items need a decision from an editor or agent.\n *\n * `id` is derived from the position AND the three values (see\n * `conflictId`), so a resolution made against a stale dry-run no longer\n * matches once any side changes — compare-and-set for free.\n */\nexport interface ConflictItem {\n id: string\n /** Content-root-relative path of the file the conflict lives in. */\n path: string\n model?: string\n kind: 'collection' | 'singleton' | 'document' | 'dictionary'\n | 'vocabulary' | 'model' | 'meta' | 'file'\n /** Entry ID, dictionary key, vocabulary term, or model schema key. */\n key?: string\n /** Field inside the entry / frontmatter key, when the conflict is field-level. */\n field?: string\n locale?: string\n /** The three values in question — for display; omitted for `kind: 'file'`. */\n base?: unknown\n ours?: unknown\n theirs?: unknown\n code: ConflictCode\n /** Ready-to-display English sentence naming the conflict. */\n message: string\n /**\n * Policy-table suggestion, never auto-applied. Present only where the\n * approved policy names a preferred side (model schema keys → `theirs`).\n */\n suggested?: 'ours' | 'theirs'\n}\n\n/**\n * A decision for one conflict, keyed by `ConflictItem.id`. `choose` picks a\n * side (absence of the item on that side means deletion); `value` supplies\n * a hand-authored replacement. A resolution whose `id` no longer matches a\n * live conflict is dropped and the conflict re-reported with a fresh id.\n */\nexport type ConflictResolution =\n | { id: string; choose: 'ours' | 'theirs' }\n | { id: string; value: unknown }\n\n// ─── Media (optional provider facet) ───\n\n/**\n * One media asset as the provider's media stack reports it. `path` is the\n * repo-relative storage path content fields reference (`media/...`); `url`\n * is the absolute delivery URL when the provider can resolve one (see\n * `RepoProvider.mediaBaseUrl`). `meta` carries provider-defined extras\n * (dimensions, blurhash, variants) without widening this contract.\n */\nexport interface MediaAsset {\n id: string\n path: string\n url?: string\n mime?: string\n size?: number\n alt?: string\n tags?: string[]\n createdAt?: string\n meta?: Record<string, unknown>\n}\n\nexport interface MediaListOptions {\n /** Substring match on filename/path/alt. */\n search?: string\n /** Filter to assets carrying this tag. */\n tag?: string\n /** Page size — implementations may clamp. */\n limit?: number\n /** Opaque cursor from a previous `MediaListResult.nextCursor`. */\n cursor?: string\n}\n\nexport interface MediaListResult {\n assets: MediaAsset[]\n nextCursor?: string\n total?: number\n}\n\n/**\n * URL-based ingest — MCP has no binary channel, so the source is always a\n * URL the provider fetches server-side. Implementations MUST enforce their\n * own SSRF, MIME, and size policies before fetching; MCP passes the input\n * through verbatim and never fetches the URL itself.\n */\nexport interface MediaIngestInput {\n url: string\n filename?: string\n alt?: string\n tags?: string[]\n}\n\nexport interface MediaUpdateInput {\n alt?: string\n tags?: string[]\n filename?: string\n}\n\n/**\n * Optional media facet of a provider. When present on `RepoProvider.media`,\n * the MCP server registers the `contentrain_media_*` tools; when absent\n * (LocalProvider, plain GitHub/GitLab providers), those tools are not\n * listed at all. Hosted providers (Studio MCP Cloud) implement this by\n * delegating to their media stack — deterministic passthrough, no content\n * decisions in MCP.\n */\nexport interface MediaProvider {\n list(opts?: MediaListOptions): Promise<MediaListResult>\n get(id: string): Promise<MediaAsset | null>\n ingest(input: MediaIngestInput): Promise<MediaAsset>\n update(id: string, patch: MediaUpdateInput): Promise<MediaAsset>\n delete(id: string): Promise<void>\n}\n\n// ─── Provider (full surface) ───\n\n/**\n * A content repository provider — the unified surface that MCP tools drive.\n *\n * Implementations wrap a git backend:\n *\n * - `LocalProvider` — simple-git + temp worktree + selective sync\n * - `GitHubProvider` — Octokit Git Data API (no clone)\n * - `GitLabProvider` — gitbeaker REST client (no clone)\n * - `BitbucketProvider` — planned, coming soon\n *\n * Providers are commodity and all live in MIT.\n */\n/** Result of {@link RepoProvider.checkWriteReadiness}. */\nexport interface WriteReadiness {\n blocked: boolean\n /** Human-readable reason, present when `blocked`. */\n message?: string\n}\n\nexport interface RepoProvider extends RepoReader, RepoWriter {\n readonly capabilities: ProviderCapabilities\n\n /**\n * Per-project public media delivery base, e.g.\n * `https://studio.example/api/cdn/v1/{projectId}` — already includes the\n * project segment, so the content-write path only joins `{base}/{path}`.\n *\n * Set by hosted providers (Studio's MCP Cloud loopback resolves it from a\n * request header and attaches it here). When present, content writes\n * normalize relative `media/...` references — in media/image/file fields and\n * markdown bodies — to absolute delivery URLs, so external-agent writes\n * render anywhere without an SDK. Undefined for local/CLI providers, where\n * media stays a relative path (the OSS file model). Never affects reads.\n */\n readonly mediaBaseUrl?: string\n\n /**\n * Optional media facet. Present only on providers whose backend exposes a\n * media stack (Studio MCP Cloud); drives the `contentrain_media_*` MCP\n * tools, which are not registered when this is absent.\n */\n readonly media?: MediaProvider\n\n /**\n * Whether the provider will accept a write right now.\n *\n * A local worktree accumulates unmerged `cr/*` branches and eventually has\n * to refuse new ones; a hosted provider manages that server-side. Asking the\n * provider keeps the tool layer out of the business of counting git branches\n * — it previously reached through an `instanceof` check to a `projectRoot`\n * only one implementation has.\n *\n * Optional: a provider that omits it is always ready.\n */\n checkWriteReadiness?(): Promise<WriteReadiness>\n\n listBranches(prefix?: string): Promise<Branch[]>\n createBranch(name: string, fromRef?: string): Promise<void>\n deleteBranch(name: string): Promise<void>\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]>\n /**\n * Merge `branch` into `into`. Like `git merge` and the platform merge APIs,\n * the source branch is left in place by default. Pass\n * `opts.removeSourceBranch: true` to also delete it after a successful merge\n * (best-effort — reported via `MergeResult.remote`, never a thrown error).\n * Even when opted in, a long-lived branch is never deleted: not `into`, not\n * the `contentrain` content branch, and not the repo's default branch.\n * LocalProvider ignores the option: its cleanup is governed by\n * `config.remoteBranchCleanup` and its own `cr/*`-only guard.\n */\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult>\n isMerged(branch: string, into?: string): Promise<boolean>\n getDefaultBranch(): Promise<string>\n\n /**\n * The merge-base commit of two refs — the `base` input of a three-way\n * reconcile.\n *\n * Reconciling a diverged content branch needs the last common ancestor,\n * and every backend can name it (git `merge-base`, GitHub's compare API,\n * GitLab's merge_base endpoint) — but not every existing implementation\n * does yet, and demanding it would break external `implements` code.\n *\n * Optional: a provider that omits it cannot drive a reconcile; the caller\n * falls back to a pull-request flow. Returns `null` when the refs share\n * no history.\n */\n getMergeBase?(refA: string, refB: string): Promise<string | null>\n\n /**\n * Write a set of resolved changes as a TWO-PARENT merge commit on\n * `branch`, joining `ours` and `theirs` so that git history records the\n * reconcile — after it, `theirs` is an ancestor of `branch` and a plain\n * fast-forward advance works again. `changes` are applied on top of the\n * `ours` tree; for content-owned paths the planner's output is\n * authoritative, never a textual auto-merge.\n *\n * Optional, mirrored by `capabilities.mergeCommit`: a backend whose\n * commits API cannot express two parents (GitLab) omits it and the\n * caller falls back to a merge-request flow.\n */\n createMergeCommit?(input: {\n /** Branch whose tip becomes the merge commit (the content branch). */\n branch: string\n /** First parent — the current tip of `branch`. */\n ours: string\n /** Second parent — the ref being reconciled in. */\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit>\n}\n","// ─── Type System ───\n\nexport type FieldType =\n | 'string' | 'text' | 'email' | 'url' | 'slug' | 'color' | 'phone' | 'code' | 'icon'\n | 'markdown' | 'richtext'\n | 'number' | 'integer' | 'decimal' | 'percent' | 'rating'\n | 'boolean'\n | 'date' | 'datetime'\n | 'image' | 'video' | 'file'\n | 'relation' | 'relations'\n | 'select' | 'array' | 'object'\n\nexport type ModelKind = 'singleton' | 'collection' | 'document' | 'dictionary'\nexport type ContentStatus = 'draft' | 'in_review' | 'published' | 'rejected' | 'archived'\nexport type ContentSource = 'agent' | 'human' | 'import'\nexport type WorkflowMode = 'auto-merge' | 'review'\n// Meta-frameworks\nexport type StackType =\n | 'nuxt' | 'next' | 'astro' | 'sveltekit' | 'remix' | 'analog'\n // Plain frameworks\n | 'vue' | 'react' | 'svelte' | 'solid' | 'angular'\n // Mobile\n | 'react-native' | 'expo' | 'flutter'\n // Backend\n | 'node' | 'express' | 'fastify' | 'nestjs' | 'django' | 'rails' | 'laravel' | 'go' | 'rust' | 'dotnet'\n // Static site generators\n | 'hugo' | 'jekyll' | 'eleventy'\n // Desktop\n | 'electron' | 'tauri'\n // Catch-all\n | 'other'\n\nexport type Platform = 'web' | 'mobile' | 'api' | 'desktop' | 'static' | 'other'\nexport type ContextSource = 'mcp-local' | 'mcp-studio' | 'studio-ui'\nexport type CollectionRuntimeFormat = 'map' | 'array'\n\n// ─── Field Definition ───\n\nexport interface FieldDef {\n type: FieldType\n required?: boolean\n unique?: boolean\n default?: unknown\n min?: number\n max?: number\n pattern?: string\n options?: string[]\n model?: string | string[]\n items?: string | FieldDef\n fields?: Record<string, FieldDef>\n accept?: string\n maxSize?: number\n description?: string\n /**\n * Human-readable name for this field in an editing UI.\n *\n * A plain string is one label for every locale; an object keyed by locale\n * carries a translation per locale. The union exists now rather than later\n * because widening `string` to it afterwards would break every consumer\n * that treats the value as a string.\n *\n * Absent means the field name itself is the label — which is why an editor\n * shows `body_public` and `is_category_hero` today.\n */\n label?: string | Record<string, string>\n /**\n * Display position in an editing UI, ascending.\n *\n * Fields are stored in canonical alphabetical order, so without this an\n * editor shows them alphabetically: on a 16-field article model `author`\n * comes first and `title` fifteenth. Fractional values are allowed so a\n * field can be inserted between two others without renumbering the rest.\n *\n * Fields without one sort after every field that has one, alphabetically\n * among themselves — so a model that has not been given an order behaves\n * exactly as it does today.\n */\n order?: number\n}\n\n// ─── Model Definition ───\n\nexport type LocaleStrategy = 'file' | 'suffix' | 'directory' | 'none'\n\nexport interface ModelDefinition {\n id: string\n name: string\n kind: ModelKind\n domain: string\n i18n: boolean\n /**\n * Name of the field whose value is shown as an entry's title — in a Studio row,\n * a picker, a relation reference. Declared, never guessed: a consumer that infers\n * the title from field order or length picks the icon over the headline.\n *\n * Must name a field that exists on this model and whose type can render as text\n * (see `isTitleFieldType`). Dictionary models have no fields, so their only legal\n * value is `DICTIONARY_TITLE_FIELD` — the entry key is the title.\n */\n title_field: string\n description?: string\n fields?: Record<string, FieldDef>\n /** Framework-relative path for content files (e.g. \"content/blog\", \"locales\"). When set, content is written here instead of .contentrain/content/ */\n content_path?: string\n /** How locale is encoded in file names. Default: \"file\" ({dir}/{locale}.json or {dir}/{slug}/{locale}.md) */\n locale_strategy?: LocaleStrategy\n}\n\n// ─── Config ───\n\nexport interface ContentrainConfig {\n version: number\n platform?: Platform\n stack: StackType\n workflow: WorkflowMode\n repository?: {\n /**\n * Git host backing this project's remote. Widens as new providers ship\n * in @contentrain/mcp — `github` and `gitlab` are currently supported.\n * The value is informational for tooling: the concrete provider the\n * MCP server talks to is chosen at server construction time, not from\n * this config.\n */\n provider: 'github' | 'gitlab'\n owner: string\n name: string\n default_branch: string\n }\n locales: {\n default: string\n supported: string[]\n }\n domains: string[]\n assets_path?: string\n /**\n * Public media delivery base for resolving relative `media/...` references to\n * absolute URLs (e.g. a deployed CDN delivery endpoint). When set,\n * `@contentrain/query`'s `generate` bakes a `media()` resolver into the\n * generated client. The base may already include any project segment — the\n * resolver only joins `{cdn.url}/{path}`. Distinct from `assets_path` (the\n * local on-disk asset directory).\n */\n cdn?: {\n url?: string\n }\n /** Days a merged `cr/*` branch is kept before lazy cleanup. Default: 30. */\n branchRetention?: number\n /** Unmerged `cr/*` branch count that triggers a warning. Default: 50. */\n branchWarnLimit?: number\n /** Unmerged `cr/*` branch count that blocks new writes. Default: 80. */\n branchBlockLimit?: number\n /**\n * Delete the remote copy of a `cr/*` branch when it is merged or deleted\n * locally, and allow lazy pruning of already-merged remote branches.\n * Read-only remote visibility (doctor, branch listing) is not affected.\n * Default: true.\n */\n remoteBranchCleanup?: boolean\n}\n\n// ─── Vocabulary ───\n\nexport interface Vocabulary {\n version: number\n terms: Record<string, Record<string, string>>\n}\n\n// ─── Metadata ───\n\nexport interface EntryMeta {\n status: ContentStatus\n source: ContentSource\n updated_by: string\n /**\n * When this entry was last written, ISO 8601 UTC.\n *\n * Optional, and deliberately not backfilled. It is a fact about the past,\n * and an entry written before the field existed has no recoverable value —\n * a fabricated timestamp would be worse than an absent one, because it\n * would sort. Absent means unknown; the first write mints the real value.\n */\n updated_at?: string\n approved_by?: string | null\n version?: string\n publish_at?: string\n expire_at?: string\n}\n\nexport type SingletonMeta = EntryMeta\nexport type CollectionMeta = Record<string, EntryMeta>\nexport type DocumentMeta = EntryMeta\nexport type DictionaryMeta = EntryMeta\n\n// ─── Asset ───\n\nexport interface AssetEntry {\n path: string\n type: string\n size: number\n alt?: string\n}\n\n// ─── Validation ───\n\nexport interface ValidationError {\n severity: 'error' | 'warning' | 'notice'\n model?: string\n locale?: string\n entry?: string\n slug?: string\n field?: string\n message: string\n}\n\nexport interface ValidationResult {\n valid: boolean\n errors: ValidationError[]\n}\n\n// ─── Content Types (Storage vs Output) ───\n\n// Storage formats — how content is written to disk (canonical JSON)\nexport type SingletonContentFile = Record<string, unknown>\nexport type CollectionContentFile = Record<string, Record<string, unknown>>\nexport type DictionaryContentFile = Record<string, string>\n// Document storage = markdown file (frontmatter + body), not JSON\n\n// Output formats — how MCP/SDK returns content to consumers\nexport type CollectionEntry = { id: string } & Record<string, unknown>\nexport type CollectionContentOutput = CollectionEntry[]\n\n/** Document entry as returned by MCP/SDK — parsed markdown with frontmatter */\nexport interface DocumentEntry {\n slug: string\n frontmatter: Record<string, unknown>\n body: string\n}\n\nexport type DocumentContentOutput = DocumentEntry[]\n\n/** Polymorphic relation storage — used when model field is string[] (multiple targets) */\nexport interface PolymorphicRelationRef {\n model: string\n ref: string\n}\n\n// ─── Model Summary ───\n\n/** Lightweight model info for listing and status operations */\nexport interface ModelSummary {\n id: string\n kind: ModelKind\n domain: string\n i18n: boolean\n fields: number\n}\n\n// ─── File Framework ───\n\n/** Source file framework — detected from extension for normalize/apply operations */\nexport type FileFramework = 'vue' | 'svelte' | 'jsx' | 'astro' | 'script'\n\n// ─── Path Conventions ───\n\n/** Root directory name for all Contentrain data */\nexport const CONTENTRAIN_DIR = '.contentrain' as const\n\n/**\n * Default path patterns for Contentrain projects.\n * Variables: {domain}, {modelId}, {locale}, {slug}\n *\n * When a model has `content_path` set, content files are written to\n * `{projectRoot}/{content_path}/` instead — but meta files always\n * remain under `.contentrain/meta/`.\n */\nexport const PATH_PATTERNS = {\n config: '.contentrain/config.json',\n context: '.contentrain/context.json',\n vocabulary: '.contentrain/vocabulary.json',\n model: '.contentrain/models/{modelId}.json',\n content: {\n singleton: '.contentrain/content/{domain}/{modelId}/{locale}.json',\n collection: '.contentrain/content/{domain}/{modelId}/{locale}.json',\n document: '.contentrain/content/{domain}/{slug}/{locale}.md',\n dictionary: '.contentrain/content/{domain}/{modelId}/{locale}.json',\n /** Non-i18n content (i18n: false) */\n noLocale: '.contentrain/content/{domain}/{modelId}/data.json',\n },\n meta: {\n singleton: '.contentrain/meta/{modelId}/{locale}.json',\n collection: '.contentrain/meta/{modelId}/{locale}.json',\n document: '.contentrain/meta/{modelId}/{slug}/{locale}.json',\n dictionary: '.contentrain/meta/{modelId}/{locale}.json',\n },\n} as const\n\n// ─── Validation Patterns ───\n\n/** Slug: lowercase alphanumeric with hyphens */\nexport const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n/** Entry ID: alphanumeric, 1-40 chars, starts with alphanumeric */\nexport const ENTRY_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,39}$/\n/** Locale: ISO 639-1 with optional region code */\nexport const LOCALE_PATTERN = /^[a-z]{2}(?:-[A-Z]{2})?$/\n\n// ─── Canonical Serialization ───\n\n/** Rules for deterministic JSON output — ensures stable git diffs */\nexport const CANONICAL_JSON = {\n indent: 2,\n encoding: 'utf-8',\n trailingNewline: true,\n omitNull: true,\n omitDefaults: true,\n sortKeys: true,\n} as const\n\n/**\n * Key order for canonical model JSON — the required keys first, then the optional\n * ones, then `fields` last so a reader sees the model's identity before scrolling\n * past its schema.\n *\n * This lives here, and only here, because `canonicalStringify` falls back to\n * alphabetical for any key absent from the order: a forgotten entry does not error,\n * it silently relocates. `satisfies` catches a key that is not on `ModelDefinition`;\n * the `keyof` assertion in index.test.ts catches a `ModelDefinition` key missing\n * from this array. Between them, drift fails `tsc`, which runs on every commit.\n */\nexport const MODEL_FIELD_ORDER = [\n 'id',\n 'name',\n 'kind',\n 'domain',\n 'i18n',\n 'title_field',\n 'description',\n 'content_path',\n 'locale_strategy',\n 'fields',\n] as const satisfies readonly (keyof ModelDefinition)[]\n\n// ─── Scaffold ───\n\nexport interface ScaffoldTemplate {\n id: string\n models: ModelDefinition[]\n sample_content?: Record<string, Record<string, unknown>>\n vocabulary?: Record<string, Record<string, string>>\n}\n\n// ─── Scan Types ───\n\nexport type StringContext =\n | 'jsx_text' | 'jsx_attribute'\n | 'template_text' | 'template_attribute'\n | 'variable_assignment' | 'function_argument'\n | 'object_value' | 'other'\n\nexport type FileCategory = 'page' | 'component' | 'layout' | 'other'\n\nexport interface ScanCandidate {\n file: string\n line: number\n column: number\n value: string\n context: StringContext\n surrounding: string\n /** Content confidence score (0-1). Higher = more likely user-visible content. */\n contentScore: number\n /** All locations where this value appears (populated when dedup is enabled). */\n occurrences: Array<{ file: string; line: number }>\n}\n\nexport interface DuplicateGroup {\n value: string\n count: number\n occurrences: Array<{ file: string; line: number }>\n}\n\nexport interface GraphNode {\n file: string\n category: FileCategory\n components?: string[]\n imports: string[]\n used_by: string[]\n strings: number\n}\n\nexport interface ProjectGraph {\n pages: GraphNode[]\n components: GraphNode[]\n layouts: GraphNode[]\n orphan_files: string[]\n stats: {\n total_files: number\n total_components: number\n total_pages: number\n total_strings_estimate: number\n }\n}\n\nexport interface ScanCandidatesResult {\n candidates: ScanCandidate[]\n duplicates: DuplicateGroup[]\n stats: {\n files_scanned: number\n raw_strings_found: number\n skipped: number\n low_confidence: number\n unique_candidates: number\n candidates_returned: number\n has_more: boolean\n skip_reasons: Record<string, number>\n }\n}\n\nexport interface ScanSummaryResult {\n total_files: number\n total_candidates_estimate: number\n by_directory: Record<string, { files: number; candidates: number }>\n top_repeated: Array<{ value: string; count: number }>\n sampling_note?: string\n file_types: Record<string, number>\n}\n\n// ─── Normalize Plan ───\n\nexport interface NormalizePlan {\n version: number\n status: 'pending' | 'approved' | 'rejected'\n created_at: string\n approved_at?: string\n agent: string\n scan_stats: {\n files_scanned: number\n raw_strings: number\n candidates_sent: number\n extracted: number\n skipped: number\n }\n models: NormalizePlanModel[]\n extractions: NormalizePlanExtraction[]\n patches: NormalizePlanPatch[]\n approved_models?: string[]\n}\n\nexport interface NormalizePlanModel {\n id: string\n kind: ModelKind\n domain: string\n i18n?: boolean\n fields: Record<string, FieldDef>\n}\n\nexport interface NormalizePlanExtraction {\n value: string\n file: string\n line: number\n model: string\n field: string\n locale?: string\n}\n\nexport interface NormalizePlanPatch {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\n// ─── Context ───\n\nexport interface ContextJson {\n version: string\n lastOperation: {\n tool: string\n model: string\n locale: string\n entries?: string[]\n timestamp: string\n source: ContextSource\n }\n stats: {\n models: number\n entries: number\n locales: string[]\n lastSync: string\n }\n}\n\n// ─── Git Transaction Types ───\n\n/** Standard name for the dedicated content branch */\nexport const CONTENTRAIN_BRANCH = 'contentrain' as const\n\n/** Result of selective file sync to developer's working tree */\nexport interface SyncResult {\n /** Files successfully synced to working tree */\n synced: string[]\n /** Files skipped due to uncommitted local changes */\n skipped: string[]\n /** Human-readable warning if files were skipped */\n warning?: string\n}\n\n/** Structured error for git operations with actionable guidance */\nexport interface ContentrainError {\n /** Machine-readable error code */\n code: string\n /** Human-readable error message */\n message: string\n /** Guidance for AI agents on how to handle this error */\n agent_hint: string\n /** Command or action the developer should take */\n developer_action: string\n}\n\n// ─── Validate: Pure Functions ───\n\n/**\n * Patterns for detecting a credential that has been pasted into content.\n *\n * Each one matches a credential-shaped *value*, not prose that mentions\n * credentials. That distinction is the whole design: the earlier set matched\n * bare prefixes, so `task_id` and `risk_level` tripped `sk_`, every page\n * documenting `Authorization: Bearer <token>` tripped `Bearer `, and\n * `postgres://localhost:5432/mydb` in a getting-started guide tripped the\n * connection-string rule. On this repository — which stores its own\n * documentation as content — that was 7 of 12 validation errors, and the real\n * findings were buried under them.\n *\n * A check that fires on the wrong thing is not a strict check; it is a check\n * people learn to scroll past.\n */\nexport const SECRET_PATTERNS: ReadonlyArray<RegExp> = [\n // Stripe-style keys: the prefix must start a word and carry a long body,\n // so `task_id` and `risk_level` no longer match.\n /\\b[sp]k_(?:live|test)_[A-Za-z0-9]{6,}/,\n // GitHub tokens are a fixed prefix plus a long body.\n /\\bgh[pousr]_[A-Za-z0-9]{10,}/,\n // A bearer token, not the word. Placeholders — <token>, $TOKEN, {{token}},\n // YOUR_TOKEN — are how documentation writes it, and are not secrets.\n /\\bBearer\\s+(?![<${{]|YOUR[_-]|your[_-]|xxx|XXX|\\.{3})[A-Za-z0-9._~+/-]{20,}/,\n // An API key being assigned a value, not the word in a sentence.\n /\\bapi[_-]?key\\s*[:=]\\s*['\"`]?[A-Za-z0-9_-]{16,}/i,\n /\\baws_secret[_a-z]*\\s*[:=]\\s*['\"`]?[A-Za-z0-9/+=]{20,}/i,\n // AWS access key IDs have a fixed shape.\n /\\bAKIA[0-9A-Z]{16}\\b/,\n // A connection string only carries a secret when it carries credentials;\n // `postgres://localhost:5432/mydb` in a guide does not.\n /\\b(?:postgres(?:ql)?|mysql|mongodb)(?:\\+srv)?:\\/\\/[^\\s:@/]+:[^\\s@/]+@/,\n // A pasted private key is unambiguous.\n /-----BEGIN [A-Z ]*PRIVATE KEY-----/,\n]\n\n/** Validate a slug — returns error message or null if valid */\nexport function validateSlug(slug: string): string | null {\n if (!slug) return 'Slug is required'\n if (!SLUG_PATTERN.test(slug)) return `Invalid slug \"${slug}\": must be kebab-case alphanumeric (a-z, 0-9, hyphens)`\n if (slug.startsWith('.') || slug.includes('..')) return `Invalid slug \"${slug}\": path traversal not allowed`\n return null\n}\n\n/** Validate an entry ID — returns error message or null if valid */\nexport function validateEntryId(id: string): string | null {\n if (!ENTRY_ID_PATTERN.test(id)) return `Invalid entry ID \"${id}\": must be 1-40 alphanumeric characters (hyphens and underscores allowed)`\n return null\n}\n\n/** Validate a locale code against pattern and config — returns error message or null if valid */\nexport function validateLocale(locale: string, config: ContentrainConfig): string | null {\n if (!LOCALE_PATTERN.test(locale)) return `Invalid locale \"${locale}\": must be ISO 639-1 format (e.g. \"en\", \"tr\", \"pt-BR\")`\n if (!config.locales.supported.includes(locale)) {\n return `Locale \"${locale}\" is not in supported locales [${config.locales.supported.join(', ')}]. Update config first.`\n }\n return null\n}\n\n/** Detect potential secrets in a value — returns validation errors if found */\nexport function detectSecrets(value: unknown): ValidationError[] {\n if (typeof value !== 'string') return []\n if (SECRET_PATTERNS.some(p => p.test(value))) {\n return [{ severity: 'error', message: 'Potential secret detected in value' }]\n }\n return []\n}\n\nfunction fieldTypeMatches(value: unknown, fieldDef: FieldDef): boolean {\n if (value === null || value === undefined) return true\n\n switch (fieldDef.type) {\n case 'string':\n case 'text':\n case 'email':\n case 'url':\n case 'slug':\n case 'color':\n case 'phone':\n case 'code':\n case 'icon':\n case 'markdown':\n case 'richtext':\n case 'date':\n case 'datetime':\n case 'image':\n case 'video':\n case 'file':\n case 'select':\n return typeof value === 'string'\n\n case 'number':\n case 'integer':\n case 'decimal':\n case 'percent':\n case 'rating':\n return typeof value === 'number'\n\n case 'boolean':\n return typeof value === 'boolean'\n\n case 'relation':\n return typeof value === 'string'\n\n case 'relations':\n case 'array':\n return Array.isArray(value)\n\n case 'object':\n return typeof value === 'object' && !Array.isArray(value)\n\n default:\n return true\n }\n}\n\nfunction checkMinMax(value: unknown, fieldDef: FieldDef): string | null {\n if (fieldDef.min !== undefined) {\n if (typeof value === 'number' && value < fieldDef.min) {\n return `Value ${value} is below minimum ${fieldDef.min}`\n }\n if (typeof value === 'string' && value.length < fieldDef.min) {\n return `String length ${value.length} is below minimum ${fieldDef.min}`\n }\n if (Array.isArray(value) && value.length < fieldDef.min) {\n return `Array length ${value.length} is below minimum ${fieldDef.min}`\n }\n }\n if (fieldDef.max !== undefined) {\n if (typeof value === 'number' && value > fieldDef.max) {\n return `Value ${value} is above maximum ${fieldDef.max}`\n }\n if (typeof value === 'string' && value.length > fieldDef.max) {\n return `String length ${value.length} is above maximum ${fieldDef.max}`\n }\n if (Array.isArray(value) && value.length > fieldDef.max) {\n return `Array length ${value.length} is above maximum ${fieldDef.max}`\n }\n }\n return null\n}\n\n// ─── Semantic type validation ───\n\n/** Hex (#rgb/#rrggbb/#rrggbbaa), rgb()/rgba(), hsl()/hsla(), or a bare CSS keyword. */\nconst COLOR_PATTERN = /^(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgb|hsl)a?\\([^)]*\\)|[a-zA-Z]+)$/\n/** Deliberately loose: digits, spaces, and the usual separators, 4-20 digits total. */\nconst PHONE_PATTERN = /^\\+?[\\d\\s().-]{4,}$/\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\nfunction isParsableDate(value: string): boolean {\n return !Number.isNaN(new Date(value).getTime())\n}\n\n/**\n * Rules a field type implies beyond its runtime `typeof`.\n *\n * Severity follows one rule: a check that is *definitional* is an error — a `slug`\n * that does not match `SLUG_PATTERN` is not a slug. A check that is a *heuristic* is\n * a warning, because the pattern is an approximation and a legitimate value may sit\n * outside it (international phone formats, CSS colour keywords).\n *\n * `rating` is deliberately absent: its scale is never declared, so any range here\n * would be invented. Declare `min`/`max` on the field instead.\n *\n * Returns `[]` for types whose only contract is their `typeof` (`string`, `text`,\n * `code`, `icon`, `markdown`, `richtext`, `number`, `decimal`, `boolean`, media).\n */\nexport function validateSemanticType(value: unknown, type: FieldType): ValidationError[] {\n if (typeof value === 'number') {\n if (type === 'integer' && !Number.isInteger(value)) {\n return [{ severity: 'error', message: `Value ${value} is not an integer` }]\n }\n if (type === 'percent' && (value < 0 || value > 100)) {\n return [{ severity: 'error', message: `Percent ${value} is outside 0-100` }]\n }\n return []\n }\n\n if (typeof value !== 'string' || value === '') return []\n\n switch (type) {\n case 'slug': {\n const err = validateSlug(value)\n return err ? [{ severity: 'error', message: err }] : []\n }\n case 'date':\n return DATE_PATTERN.test(value) && isParsableDate(value)\n ? []\n : [{ severity: 'error', message: `Invalid date \"${value}\": must be YYYY-MM-DD` }]\n case 'datetime':\n return isParsableDate(value)\n ? []\n : [{ severity: 'error', message: `Invalid datetime \"${value}\": must be an ISO 8601 date-time` }]\n case 'email':\n return EMAIL_PATTERN.test(value)\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid email` }]\n case 'url':\n return /^https?:\\/\\/.+/.test(value) || value.startsWith('/')\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid URL` }]\n case 'color':\n return COLOR_PATTERN.test(value)\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid color (expected hex, rgb(), hsl() or a keyword)` }]\n case 'phone':\n return PHONE_PATTERN.test(value) && (value.match(/\\d/g)?.length ?? 0) >= 4\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid phone number` }]\n default:\n return []\n }\n}\n\n/** Media field types — the ones whose value is a path/URL to an asset. */\nconst MEDIA_TYPES = new Set<FieldType>(['image', 'video', 'file'])\n\nexport function isMediaType(type: FieldType): boolean {\n return MEDIA_TYPES.has(type)\n}\n\n// ─── Field presentation ───\n\n/**\n * Resolve a field's label for a locale.\n *\n * Falls back in the order a reader would expect: the requested locale, the\n * default locale, any locale present, then the field name. A field with no\n * label resolves to its own name, which is what an editor shows today.\n */\nexport function resolveFieldLabel(\n fieldName: string,\n field: Pick<FieldDef, 'label'>,\n locale?: string,\n defaultLocale?: string,\n): string {\n const { label } = field\n if (typeof label === 'string') return label\n if (label && typeof label === 'object') {\n const byLocale = (locale && label[locale])\n ?? (defaultLocale && label[defaultLocale])\n ?? Object.values(label)[0]\n if (byLocale) return byLocale\n }\n return fieldName\n}\n\n/**\n * Field names in display order.\n *\n * Exported so every consumer orders identically instead of each reimplementing\n * the fallback. Ascending `order`; fields without one come last, alphabetically\n * among themselves, which is the behaviour a model that has no `order` already\n * has.\n */\nexport function orderedFieldNames(fields: Record<string, FieldDef> | undefined): string[] {\n if (!fields) return []\n return Object.keys(fields).toSorted((a, b) => {\n const oa = fields[a]?.order\n const ob = fields[b]?.order\n if (oa !== undefined && ob !== undefined && oa !== ob) return oa - ob\n if (oa !== undefined && ob === undefined) return -1\n if (oa === undefined && ob !== undefined) return 1\n return a.localeCompare(b, 'en')\n })\n}\n\n// ─── Title field ───\n\n/**\n * The reserved `title_field` value for dictionary models. A dictionary has no\n * fields — it is a flat key→string map — so there is nothing to point at. Its key\n * *is* the title, and this names that fact rather than leaving the property blank.\n * Only dictionaries may use it; on any other kind `title_field` must name a real field.\n */\nexport const DICTIONARY_TITLE_FIELD = 'key'\n\n/**\n * Field types whose value can stand in as a human-readable title.\n *\n * Narrower than \"types that hold a string\": `color`, `phone`, `icon`, `select`,\n * `date`, `datetime`, `relation` and the media types all store strings, but a row\n * labelled `#ff0000` or `f3a81c09d24e` is exactly the failure this contract exists\n * to prevent.\n */\nexport const TITLE_FIELD_TYPES = [\n 'string', 'text', 'slug', 'email', 'url', 'code', 'markdown', 'richtext',\n] as const satisfies readonly FieldType[]\n\nconst TITLE_FIELD_TYPE_SET = new Set<FieldType>(TITLE_FIELD_TYPES)\n\n/** Whether a field of this type may be named as a model's `title_field`. */\nexport function isTitleFieldType(type: FieldType): boolean {\n return TITLE_FIELD_TYPE_SET.has(type)\n}\n\n/** Extensions we can name a MIME type for. Deliberately small — this is a sniff. */\nconst EXTENSION_MIME: Record<string, string> = {\n jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',\n webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml', ico: 'image/x-icon',\n mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',\n pdf: 'application/pdf', zip: 'application/zip', json: 'application/json',\n csv: 'text/csv', txt: 'text/plain',\n}\n\n/**\n * Check a media value's file extension against an `accept` list.\n *\n * This is an approximation and says so: the value is a path or URL, so the real\n * MIME type is only knowable where the bytes are — the provider, at ingest\n * (`provider.ts:MediaProvider.ingest` owns that policy). An extension can lie, and\n * an unknown extension is not evidence of a violation. Hence: warning, never error,\n * and silence when we cannot tell.\n *\n * `accept` follows the HTML input syntax: `image/jpeg`, `image/*`, `.jpg`, comma-separated.\n */\nexport function validateAccept(value: string, accept: string): ValidationError[] {\n const path = value.split('?')[0]?.split('#')[0] ?? value\n const ext = path.split('.').pop()?.toLowerCase()\n if (!ext || ext === path.toLowerCase()) return []\n\n const mime = EXTENSION_MIME[ext]\n const patterns = accept.split(',').map(a => a.trim().toLowerCase()).filter(Boolean)\n if (patterns.length === 0) return []\n\n const matches = patterns.some((p) => {\n if (p.startsWith('.')) return p.slice(1) === ext\n if (p.endsWith('/*')) return mime ? mime.startsWith(p.slice(0, -1)) : false\n return mime === p\n })\n if (matches) return []\n\n // An extension we have no MIME for is not evidence of a violation.\n if (!mime && !patterns.some(p => p.startsWith('.'))) return []\n\n return [{\n severity: 'warning',\n message: `File extension \".${ext}\" does not match accept \"${accept}\" (extension check — the provider enforces the real MIME type at ingest)`,\n }]\n}\n\n/**\n * Validate a field value against its FieldDef schema.\n * Checks: required, type match, semantic type rules, min/max, pattern, select\n * options, and `accept` on media fields (by extension).\n * Does NOT check: secrets (use detectSecrets), uniqueness (stateful), relations (I/O),\n * or `maxSize` — byte length is not knowable from a stored path.\n */\nexport function validateFieldValue(value: unknown, fieldDef: FieldDef): ValidationError[] {\n const issues: ValidationError[] = []\n\n if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({ severity: 'error', message: 'Required field is missing or empty' })\n return issues\n }\n\n if (value === undefined || value === null) return issues\n\n if (!fieldTypeMatches(value, fieldDef)) {\n issues.push({ severity: 'error', message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}` })\n return issues\n }\n\n issues.push(...validateSemanticType(value, fieldDef.type))\n\n if (fieldDef.accept && isMediaType(fieldDef.type) && typeof value === 'string') {\n issues.push(...validateAccept(value, fieldDef.accept))\n }\n\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({ severity: 'error', message: minMaxErr })\n }\n\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({ severity: 'error', message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/` })\n }\n } catch {\n issues.push({ severity: 'warning', message: `Invalid pattern regex: ${fieldDef.pattern}` })\n }\n }\n\n if (fieldDef.type === 'select' && fieldDef.options && typeof value === 'string') {\n if (!fieldDef.options.includes(value)) {\n issues.push({ severity: 'error', message: `Value \"${value}\" is not in allowed options: [${fieldDef.options.join(', ')}]` })\n }\n }\n\n return issues\n}\n\n// ─── Serialize: Pure Functions ───\n\n/** Recursively sort object keys — respects optional fieldOrder for top-level keys */\nexport function sortKeys(obj: unknown, fieldOrder?: readonly string[]): unknown {\n if (obj === null || obj === undefined) return undefined\n if (Array.isArray(obj)) return obj.map(item => sortKeys(item, fieldOrder))\n if (typeof obj !== 'object') return obj\n\n const record = obj as Record<string, unknown>\n const sorted: Record<string, unknown> = {}\n\n const keys = fieldOrder\n ? [...new Set([...fieldOrder, ...Object.keys(record).toSorted()])]\n : Object.keys(record).toSorted()\n\n for (const key of keys) {\n if (!(key in record)) continue\n const val = record[key]\n if (val === null || val === undefined) continue\n sorted[key] = sortKeys(val)\n }\n\n return sorted\n}\n\n/** Canonical JSON serialization — deterministic output for stable git diffs */\nexport function canonicalStringify(data: unknown, fieldOrder?: readonly string[]): string {\n const sorted = sortKeys(data, fieldOrder)\n return `${JSON.stringify(sorted, null, 2)}\\n`\n}\n\n/** Generate a 12-character hex entry ID */\nexport function generateEntryId(): string {\n const hex = '0123456789abcdef'\n let id = ''\n for (let i = 0; i < 12; i++) {\n id += hex[Math.floor(Math.random() * 16)]\n }\n return id\n}\n\n// ─── Reconcile: conflict id derivation ───\n\n/**\n * FNV-1a 64-bit over the input's UTF-8 code points, returned as 16 hex\n * chars. Deliberately dependency-free (no `node:crypto` — this package has\n * zero runtime imports and runs anywhere). Not cryptographic; the use case\n * is change detection, not tamper resistance.\n */\nexport function stableHash(input: string): string {\n const PRIME = 0x100000001B3n\n const MASK = 0xFFFFFFFFFFFFFFFFn\n let hash = 0xCBF29CE484222325n\n const bytes = new TextEncoder().encode(input)\n for (const byte of bytes) {\n hash ^= BigInt(byte)\n hash = (hash * PRIME) & MASK\n }\n return hash.toString(16).padStart(16, '0')\n}\n\n/**\n * Identity of a reconcile conflict: position AND values. Hashing the three\n * values into the id makes every resolution a compare-and-set — if any side\n * changes between the dry-run that produced the conflict and the apply that\n * carries the decision, the id no longer matches, the stale resolution is\n * dropped, and the conflict is re-reported with a fresh id. Values are\n * normalized through `sortKeys` first so semantically equal objects hash\n * equal regardless of key order; the positional array keeps absent and\n * null distinguishable per slot.\n */\nexport function conflictId(conflict: {\n path: string\n key?: string\n field?: string\n locale?: string\n base?: unknown\n ours?: unknown\n theirs?: unknown\n}): string {\n return stableHash(JSON.stringify([\n conflict.path,\n conflict.key ?? null,\n conflict.field ?? null,\n conflict.locale ?? null,\n sortKeys(conflict.base) ?? null,\n sortKeys(conflict.ours) ?? null,\n sortKeys(conflict.theirs) ?? null,\n ]))\n}\n\n// ─── Markdown Frontmatter ───\n\nfunction parseYamlValue(raw: string): unknown {\n if (raw === 'true') return true\n if (raw === 'false') return false\n if (raw === 'null') return null\n if (/^-?\\d+$/.test(raw)) return parseInt(raw, 10)\n if (/^-?\\d+\\.\\d+$/.test(raw)) return parseFloat(raw)\n if ((raw.startsWith('\"') && raw.endsWith('\"')) || (raw.startsWith(\"'\") && raw.endsWith(\"'\"))) {\n return raw.slice(1, -1)\n }\n return raw\n}\n\nfunction yamlScalar(value: unknown): string {\n const str = String(value)\n if (str.includes(':') || str.includes('#') || str.includes('{') || str.includes('}')\n || str.includes('[') || str.includes(']') || str.includes('*') || str.includes('&')\n || str.includes('!') || str.includes('|') || str.includes('>') || str.includes(\"'\")\n || str.includes('\"') || str.includes('%') || str.includes('@') || str.includes('`')\n || str.startsWith('-') || str.startsWith('?')\n || str === 'true' || str === 'false' || str === 'null' || str === 'yes' || str === 'no'\n || str === '') {\n return `\"${str.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`\n }\n return str\n}\n\nfunction serializeYamlValue(value: unknown, indent: number): string[] {\n const pad = ' '.repeat(indent)\n if (value === null || value === undefined) return []\n if (typeof value === 'object' && !Array.isArray(value)) {\n const lines: string[] = []\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === null || v === undefined) continue\n if (typeof v === 'object' && !Array.isArray(v)) {\n lines.push(`${pad}${k}:`)\n lines.push(...serializeYamlValue(v, indent + 1))\n } else if (Array.isArray(v)) {\n lines.push(`${pad}${k}:`)\n for (const item of v) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, indent + 2)\n if (nested.length > 0) {\n lines.push(`${pad} - ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(`${pad} - ${yamlScalar(item)}`)\n }\n }\n } else {\n lines.push(`${pad}${k}: ${yamlScalar(v)}`)\n }\n }\n return lines\n }\n if (Array.isArray(value)) {\n const lines: string[] = []\n for (const item of value) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, indent + 1)\n if (nested.length > 0) {\n lines.push(`${pad}- ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(`${pad}- ${yamlScalar(item)}`)\n }\n }\n return lines\n }\n return [`${pad}${yamlScalar(value)}`]\n}\n\nfunction serializeYamlFields(data: Record<string, unknown>): string[] {\n const lines: string[] = []\n for (const [key, value] of Object.entries(data)) {\n if (key === 'body') continue\n if (value === null || value === undefined) continue\n if (typeof value === 'object' && !Array.isArray(value)) {\n lines.push(`${key}:`)\n lines.push(...serializeYamlValue(value, 1))\n } else if (Array.isArray(value)) {\n lines.push(`${key}:`)\n for (const item of value) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, 2)\n if (nested.length > 0) {\n lines.push(` - ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(` - ${yamlScalar(item)}`)\n }\n }\n } else {\n lines.push(`${key}: ${yamlScalar(value)}`)\n }\n }\n return lines\n}\n\n/** Parse markdown frontmatter — extracts YAML frontmatter and body from a markdown string */\nexport function parseMarkdownFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {\n const normalized = content.replace(/\\r\\n/g, '\\n')\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/)\n if (!match) return { frontmatter: {}, body: normalized }\n\n const frontmatterStr = match[1]!\n const body = match[2]!.trim()\n const frontmatter: Record<string, unknown> = {}\n\n let currentKey: string | null = null\n let currentArray: string[] | null = null\n\n for (const line of frontmatterStr.split('\\n')) {\n if (/^\\s+-\\s+/.test(line) && currentKey) {\n const value = line.replace(/^\\s+-\\s+/, '').trim()\n if (!currentArray) currentArray = []\n currentArray.push(value)\n continue\n }\n\n if (currentKey && currentArray) {\n frontmatter[currentKey] = currentArray\n currentKey = null\n currentArray = null\n }\n\n const kvMatch = line.match(/^([\\w][\\w.-]*)\\s*:\\s*(.*)$/)\n if (!kvMatch) continue\n\n const key = kvMatch[1]!\n const rawValue = kvMatch[2]!.trim()\n\n if (rawValue === '') {\n currentKey = key\n currentArray = []\n continue\n }\n\n if (rawValue.startsWith('[') && rawValue.endsWith(']')) {\n const items = rawValue.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean)\n frontmatter[key] = items\n continue\n }\n\n frontmatter[key] = parseYamlValue(rawValue)\n }\n\n if (currentKey && currentArray) {\n frontmatter[currentKey] = currentArray\n }\n\n return { frontmatter, body }\n}\n\n/** Serialize data + body into markdown frontmatter format */\nexport function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string {\n const trimmedBody = body.trimStart()\n if (trimmedBody.startsWith('---')) {\n const endIdx = trimmedBody.indexOf('---', 3)\n if (endIdx > 0) {\n const bodyFmContent = trimmedBody.slice(3, endIdx).trim()\n const afterFm = trimmedBody.slice(endIdx + 3)\n\n const lines: string[] = ['---']\n lines.push(...serializeYamlFields(data))\n if (bodyFmContent) {\n const modelKeys = new Set(Object.keys(data))\n for (const line of bodyFmContent.split('\\n')) {\n if (line.length > 0 && line[0] !== ' ' && line[0] !== '-') {\n const colonIdx = line.indexOf(':')\n const lineKey = colonIdx > 0 ? line.slice(0, colonIdx).trim() : ''\n if (lineKey && modelKeys.has(lineKey)) continue\n }\n lines.push(line)\n }\n }\n lines.push('---')\n if (afterFm.trim()) {\n lines.push(afterFm.trimStart())\n }\n lines.push('')\n return lines.join('\\n')\n }\n }\n\n const lines: string[] = ['---']\n lines.push(...serializeYamlFields(data))\n lines.push('---')\n lines.push('')\n if (body) {\n lines.push(body)\n lines.push('')\n }\n return lines.join('\\n')\n}\n\n// ─── Migration contracts ───\n\nexport * from './migration.js'\n\n// ─── Repository provider contracts ───\n//\n// Provider-agnostic engine contracts used by @contentrain/mcp. Exposed from\n// @contentrain/types so third-party tools can implement a custom\n// RepoProvider without taking a dependency on @contentrain/mcp.\nexport type {\n ApplyPlanInput,\n BaseAdvance,\n Branch,\n Commit,\n CommitAuthor,\n ConflictCode,\n ConflictItem,\n ConflictResolution,\n FileChange,\n FileDiff,\n MediaAsset,\n MediaIngestInput,\n MediaListOptions,\n MediaListResult,\n MediaProvider,\n MediaUpdateInput,\n MergeResult,\n ProviderCapabilities,\n RemotePush,\n RepoProvider,\n RepoReader,\n RepoWriter,\n WriteReadiness,\n} from './provider.js'\nexport { LOCAL_CAPABILITIES } from './provider.js'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/migration.ts","../src/provider.ts","../src/index.ts"],"sourcesContent":["// ─── Migration Contracts ───\n//\n// Shared, MIT-licensed shapes for the WordPress → static-site migration pipeline.\n// Producers: the WordPress Bridge plugin (GPL, separate repo), REST/WXR importers.\n// Consumers: the migration engine, Studio, and the open Astro emitter.\n//\n// Every shape here is plain JSON — snake_case keys, no class instances, no\n// functions — because these documents cross process, repository, and license\n// boundaries. A GPL plugin writes RawIR; a proprietary service reads it; an MIT\n// emitter consumes ProjectIR. The contract is the only thing they share, so it\n// lives in the one package all of them may depend on.\n\n/**\n * Version stamped into every migration document (`version` field on each root).\n * The four contracts evolve together with this package; a reader that sees a\n * higher version than it knows should refuse rather than guess.\n */\nexport const MIGRATION_CONTRACT_VERSION = 1\n\n// ─── Source access ───\n\n/**\n * How the WordPress site's data was reached, ordered from least to most\n * complete. Measured field coverage rises with each rung (roughly 34% for\n * public REST, 57% with an Application Password, 82% from a WXR export, 100%\n * via the Bridge plugin) — which is why provenance is recorded on every RawIR:\n * two documents for the same site are only comparable if their rungs are.\n */\nexport type SourceAccessKind = 'rest_public' | 'rest_auth' | 'wxr' | 'bridge'\n\n/** The ladder in ascending completeness order. */\nexport const SOURCE_ACCESS_LADDER = [\n 'rest_public',\n 'rest_auth',\n 'wxr',\n 'bridge',\n] as const satisfies readonly SourceAccessKind[]\n\n// ─── RawIR ───\n//\n// Source-faithful extraction of a WordPress site: what the source said, before\n// any interpretation. Unresolved references are kept and *marked* (`resolved`\n// flags) instead of dropped — deciding what a broken reference means is the\n// consumer's job, not the extractor's.\n\nexport interface RawProvenance {\n kind: SourceAccessKind\n /** ISO 8601 UTC time the extraction ran. */\n fetched_at?: string\n /** Name/version of the producing tool (e.g. \"wordpress-bridge/1.0\"). */\n tool?: string\n}\n\nexport interface RawSite {\n url: string\n title?: string\n description?: string\n /** WXR distinguishes the WP install URL from the public site URL. */\n base_site_url?: string\n base_blog_url?: string\n /** Site language as reported (e.g. \"en-US\"); multilingual detail lives in `language_pairs`. */\n language?: string | null\n /** Generator string (WordPress version) when the source exposed it. */\n generator?: string | null\n /** WXR export timestamp, when the source was a WXR file. */\n export_date?: string | null\n wxr_version?: string | null\n}\n\nexport interface RawAuthor {\n id: number | null\n login: string\n email?: string | null\n display_name: string\n first_name?: string | null\n last_name?: string | null\n}\n\nexport interface RawTerm {\n id: number | null\n taxonomy: string\n slug: string\n name: string\n /** Parent term slug within the same taxonomy, when hierarchical. */\n parent?: string | null\n /** Whether `parent` names a term present in this document. */\n parent_resolved?: boolean | null\n description?: string\n}\n\n/** A post's reference to a term — kept even when the term itself is absent. */\nexport interface RawTermRef {\n taxonomy: string\n slug: string\n name: string\n /** Whether the referenced term exists in this document's `terms`. */\n resolved: boolean\n}\n\n/** An ACF field value paired with its field-definition key (`field_…`). */\nexport interface RawAcfValue {\n value: unknown\n field_key: string\n}\n\nexport interface RawPost {\n id: number\n /** WordPress post type — \"post\", \"page\", or a custom post type slug. */\n type: string\n /** WordPress status verbatim (\"publish\", \"draft\", \"future\", \"pending\", …). */\n status: string\n slug: string\n title: string\n /** Public permalink, when known. Permalink *structure* is a site setting; the link is the fact. */\n link?: string | null\n guid?: string | null\n /** Author login; resolve against `RawIR.authors`. */\n author: string | null\n /** ISO 8601 UTC, null when the source carried no usable date. */\n date: string | null\n modified: string | null\n /** Rendered/exported HTML body, untransformed. */\n content: string\n excerpt: string\n parent?: number | null\n menu_order?: number\n sticky?: boolean\n password?: string | null\n comment_status?: string | null\n ping_status?: string | null\n terms: RawTermRef[]\n /**\n * Post meta, PHP-serialized values already decoded where possible.\n * Core WP keys (underscore-prefixed) are included: which keys matter is a\n * downstream decision.\n */\n meta: Record<string, unknown>\n /** Meta keys whose values were PHP-serialized in the source (decoded above). */\n serialized_keys?: string[]\n /** ACF fields recovered by pairing `foo` with `_foo = \"field_…\"`. */\n acf?: Record<string, RawAcfValue>\n}\n\nexport interface RawAttachment {\n id: number\n title: string\n slug: string\n /** Original file URL. Downloading/re-hosting is a consumer concern. */\n url: string | null\n alt?: string\n caption?: string\n description?: string\n /** Relative upload path (`_wp_attached_file`), when present. */\n file?: string | null\n /** `_wp_attachment_metadata` verbatim (sizes, EXIF, …). */\n image_meta?: unknown\n mime?: string | null\n parent?: number | null\n parent_resolved?: boolean | null\n author?: string | null\n date?: string | null\n status?: string\n meta?: Record<string, unknown>\n}\n\n/** Where a menu item points. `resolved` says whether the target exists in this document. */\nexport type RawMenuTarget =\n | { kind: 'url'; url: string; resolved: true }\n | { kind: 'post'; post_type: string; id: number | null; slug: string | null; resolved: boolean }\n | { kind: 'term'; taxonomy: string; id: number | null; slug: string | null; resolved: boolean }\n | { kind: 'archive'; post_type: string; resolved: true }\n | { kind: 'unknown'; resolved: false }\n\nexport interface RawMenuItem {\n id: number\n title: string\n order?: number\n /** Parent menu-item id for nested menus. */\n parent?: number | null\n parent_unresolved?: boolean\n url?: string | null\n target: RawMenuTarget\n /** `target` attribute for the rendered link (e.g. \"_blank\"). */\n target_attr?: string | null\n classes?: string[]\n description?: string\n status?: string\n}\n\nexport interface RawMenu {\n id: number | null\n slug: string\n name: string\n items: RawMenuItem[]\n}\n\nexport interface RawComment {\n id: number\n /** Post id this comment belongs to. */\n post: number\n post_type?: string\n parent?: number | null\n parent_resolved?: boolean | null\n author: string\n email?: string | null\n url?: string | null\n /**\n * ISO 8601 UTC. Producers MUST normalize to UTC — comment import fidelity\n * (\"zero record and parent loss\") includes `created_at`, and a site-local\n * date silently corrupts it. When normalization is impossible, ship the\n * source's GMT column in `date_gmt` and leave `date` null.\n */\n date: string | null\n date_gmt?: string | null\n content: string\n /**\n * WordPress approval flag. Fixed vocabulary: `'1'` (approved), `'0'`\n * (pending), `'spam'`, `'trash'` — consumers map these; unknown strings\n * pass through for forward compatibility, never dropped.\n */\n approved?: '1' | '0' | 'spam' | 'trash' | (string & {})\n type?: string\n user_id?: number | null\n meta?: Record<string, unknown>\n}\n\n/** A redirect rule (e.g. from the Redirection plugin — visible from the authenticated rung up). */\nexport interface RawRedirect {\n from: string\n to: string\n status?: number\n /** Which plugin/table produced the rule. */\n source?: string\n}\n\n/** Translation grouping for multilingual sites (Polylang/WPML — bridge rung). */\nexport interface RawLanguagePair {\n post: number\n /** locale → post id of the translation. */\n translations: Record<string, number>\n}\n\n/**\n * The complete raw extraction of one WordPress site at one access rung.\n * This is the boundary document between extraction (Bridge/importers) and\n * everything downstream — content conversion, capability analysis, migration.\n */\nexport interface RawIR {\n version: number\n provenance: RawProvenance\n site: RawSite\n authors: RawAuthor[]\n terms: RawTerm[]\n posts: RawPost[]\n attachments: RawAttachment[]\n menus?: RawMenu[]\n comments?: RawComment[]\n redirects?: RawRedirect[]\n language_pairs?: RawLanguagePair[]\n /** Site options (bridge rung), verbatim. */\n options?: Record<string, unknown>\n}\n\n// ─── CapabilityManifest ───\n//\n// What the site *uses*, as detected — the input for migration planning,\n// effort/pricing, and the \"what happens to X\" conversation with the user.\n// Detection is evidence-based and never certain; `present: true` with evidence\n// beats a silent guess.\n\nexport const CAPABILITY_KEYS = [\n 'seo',\n 'forms',\n 'comments',\n 'search',\n 'newsletter',\n 'analytics',\n 'consent',\n 'share',\n 'video',\n 'ads',\n 'builder',\n 'blocks',\n 'i18n',\n 'cache',\n 'security',\n 'acf',\n 'ecommerce',\n 'jetpack',\n 'redirects',\n 'membership',\n 'accessibility',\n 'media',\n 'scheduling',\n] as const\n\nexport type CapabilityKey = (typeof CAPABILITY_KEYS)[number]\n\nexport interface CapabilityDetection {\n present: boolean\n /** Identified plugin/provider, when known (e.g. \"yoast\", \"contact-form-7\"). */\n plugin?: string | null\n /** How it was detected: \"rest\", \"dom\", \"slug\", \"headers\", \"meta\", … */\n evidence?: string[]\n detail?: string\n}\n\nexport interface CapabilityManifest {\n version: number\n site_url: string\n access: {\n html_status?: number | null\n rest_status?: number | null\n /** Highest source-access rung actually achieved for this site. */\n achieved: SourceAccessKind\n }\n generator?: string | null\n theme?: string | null\n plugins?: string[]\n rest_namespaces?: string[]\n custom_post_types?: Array<{\n slug: string\n /** Whether the CPT is exposed in public REST (`rest: false` CPTs need a higher rung). */\n rest_visible: boolean\n count?: number | null\n }>\n /** Comments get first-class treatment: they are a launch-critical capability. */\n comments: {\n active: boolean\n form_status?: 'open' | 'closed' | 'mixed' | null\n rest_total?: number | null\n plugin?: string | null\n }\n capabilities: Partial<Record<CapabilityKey, CapabilityDetection>>\n /** Dynamic front-end behaviors observed in the DOM (\"carousel\", \"modal\", \"search-overlay\", …). */\n behaviors?: string[]\n languages?: string[]\n}\n\n// ─── ProjectIR ───\n//\n// The reproducible model of the site: not \"this page's HTML\" but the design\n// system, component architecture, route model, and content queries that let\n// *unseen* pages — tomorrow's posts, another category — be generated correctly.\n// Produced by analysis; consumed by the open emitter. Three layers are kept\n// together on purpose: visual structure (families/chrome/tokens), content\n// binding (slots), and business rules (routes/queries). A page family exists\n// per genuinely different behavior, not per page.\n\n/**\n * How the legacy CSS for a family was packaged.\n * - `purge_set`: rules kept if used by any page in the family's page set —\n * small file, safe against per-page rule loss (single-page purging is not).\n * - `localcss`: the site's stylesheets localized wholesale — larger, zero risk.\n */\nexport type CssStrategy = 'purge_set' | 'localcss'\n\n/**\n * Cascade layer name the emitter quarantines legacy CSS under, so a modern\n * utility layer (e.g. Tailwind) can coexist with pixel-faithful migrated pages.\n */\nexport const LEGACY_CSS_LAYER = 'legacy'\n\n/** Design tokens extracted from the source site, e.g. for a Tailwind `@theme` block. */\nexport interface DesignTokens {\n colors?: Record<string, string>\n font_families?: Record<string, string>\n font_sizes?: Record<string, string>\n spacing?: Record<string, string>\n breakpoints?: Record<string, string>\n}\n\nexport type RouteKind =\n | 'front'\n | 'single'\n | 'page'\n | 'archive'\n | 'term'\n | 'author'\n | 'date'\n | 'search'\n | 'not_found'\n | 'custom'\n\nexport interface RouteParamDef {\n name: string\n /**\n * Where the value comes from. The date parts exist because\n * `/%year%/%monthnum%/%day%/%postname%/` is a common WordPress permalink\n * structure: treating the template post's date as a fixed prefix sends every\n * other post to a wrong address, and SEO continuity is the point of a\n * migration. Posts carry their own values (`EmitPost.params`).\n */\n source:\n | 'post_slug'\n | 'post_id'\n | 'post_year'\n | 'post_month'\n | 'post_day'\n | 'term_slug'\n | 'author_slug'\n | 'page_number'\n | 'custom'\n}\n\n/** Selects a family/component variant when a route parameter matches. */\nexport interface RouteVariantRule {\n param: string\n in: string[]\n variant: string\n}\n\n/**\n * One route pattern → one layout family. Pagination is a parameter of a route\n * (`/news/page/2` is `/news` with `page_number = 2`), never a separate family —\n * across a 30-site corpus, page 2 never produced a new layout.\n */\nexport interface RouteModel {\n id: string\n /**\n * URL pattern with `:param` placeholders — `/category/:term`, `/news/page/:page`.\n *\n * A trailing `*` marks a **multi-segment (rest) parameter**: `/category/:term*`\n * matches `/category/about-cc/events/` with `term = \"about-cc/events\"`.\n * Hierarchical taxonomies and nested pages need this — a single-segment\n * parameter silently flattens `/category/about-cc/events/` to\n * `/category/events/`, breaking link continuity for every nested address.\n */\n pattern: string\n kind: RouteKind\n /** LayoutFamily id. */\n family: string\n params?: RouteParamDef[]\n /**\n * Content collection this route generates one page per entry from. `single`\n * routes default to `posts`; naming a collection makes any route\n * collection-driven — a site's pages and custom post types are per-entry\n * routes too, they just are not called \"single\".\n *\n * Without a name per route they would all write to (and overwrite) the same\n * collection.\n */\n collection?: string\n /** QueryBinding id feeding this route's list, when it renders one. */\n query?: string\n /**\n * Document title for routes whose pages have no per-page title of their own\n * (a static page, an archive without `QueryPage.title`).\n */\n title?: string\n /**\n * Locale this route serves. A multilingual site has a route per language\n * (`/:slug`, `/en/:slug`) with its own family and query; the emitted page's\n * `lang` comes from here rather than from the project default.\n */\n locale?: string\n variant_rules?: RouteVariantRule[]\n}\n\nexport type SlotKind =\n | 'body'\n | 'title'\n | 'date'\n | 'excerpt'\n | 'author_name'\n | 'author_link'\n | 'featured_image'\n | 'term_list'\n | 'self_link'\n | 'custom'\n\n/** Where a content field lands in a family's markup. */\nexport interface SlotBinding {\n kind: SlotKind\n /** The region is printed only on posts that have this slot filled — see `CHROME_IF_OPEN`. */\n optional?: boolean\n /** The region repeats per list item (terms, authors) — see `CHROME_REPEAT_OPEN`. */\n repeat?: boolean\n /** CSS selector of the slot's container, when one could be determined. */\n selector?: string\n /** Content-model field backing a `custom` slot. */\n field?: string\n /** Date rendering format for `date` slots, as observed on the source site. */\n date_format?: string\n}\n\n/**\n * Marks whose name ends in this suffix are inserted as raw HTML instead of\n * being escaped. Escaping stays the default — content-derived text must never\n * be able to break the page — but some themes render a post's full content or\n * a link-bearing excerpt inside a list card, and escaping those prints markup\n * as text (measured on one theme's category page: 48.6).\n */\nexport const RAW_MARK_SUFFIX = '_html'\n\n/**\n * Repeat block: `<!--@@repeat:NAME@@-->…<!--@@/repeat@@-->`, optionally with a\n * separator — `<!--@@repeat:NAME|, @@-->`. The inner fragment renders once per\n * item of the list named NAME, with per-item marks `item` / `item_index` (and\n * `item_<key>` for object items).\n *\n * Fixed marks (`term0…termN`) cannot express a list whose length varies per\n * post: a template built from a 3-term post leaves stray separators on a\n * 6-term one (measured symptoms: `\"Business,\"`, `\"Releases, Events,\"`,\n * `\"Automattic, ,\"`), and the same class shows up with multiple authors.\n */\nexport const CHROME_REPEAT_OPEN = '<!--@@repeat:'\nexport const CHROME_REPEAT_CLOSE = '<!--@@/repeat@@-->'\n\n/**\n * Conditional block: `<!--@@if:NAME@@-->…<!--@@/if@@-->`, negated with\n * `<!--@@if:!NAME@@-->`. The fragment survives only when NAME has a value\n * (non-empty string, non-empty list).\n *\n * Route-parameter variants (`RouteVariantRule`) cannot express this: two posts\n * on the SAME route differ by whether the theme printed a region at all\n * (measured: five posts of one family scored 3.5–84.5, the only difference\n * being a featured block that some posts render and others do not).\n */\nexport const CHROME_IF_OPEN = '<!--@@if:'\nexport const CHROME_IF_CLOSE = '<!--@@/if@@-->'\n\n/**\n * Marker a list section's wrapper carries where its items go —\n * `<div class=\"grid\"><!--@@items@@--></div>`.\n */\nexport const LIST_ITEMS_SLOT = '<!--@@items@@-->'\n\n/**\n * Marker a `body`-position chrome chunk carries where the page content goes.\n * A comment survives serialization, renders as nothing if ever left behind,\n * and — critically — can sit at ANY nesting depth: real themes put the content\n * container deep inside the chrome (`article > div.entry-content`), so the\n * chrome cannot be split into balanced before/after fragments. The emitter\n * splices content in at this marker and injects the result as ONE fragment.\n */\nexport const CHROME_BODY_SLOT = '<!--@@body@@-->'\n\n/**\n * A rendered, asset-rewritten chunk of site chrome the emitter injects verbatim.\n * Positions: `head` lands in `<head>`; `body` is the whole body chrome carrying\n * `CHROME_BODY_SLOT` where content goes (preferred — nesting-safe); the legacy\n * `before_body`/`after_body` pair is composed into a single body with the slot\n * between them (only correct when the content container is top-level).\n *\n * `header` and `footer` lift a region OUT of the body chrome into its own Astro\n * component, rendered as a sibling of the body fragment. They are for the two\n * regions a site actually shares — the masthead and the colophon — and they buy\n * two things one blob cannot: families that carry the same header emit ONE\n * component instead of N copies of the markup, and the nav lives at a single\n * address, which is where a jQuery-free menu replaces the theme's.\n *\n * The producer emits them ONLY after verifying both properties:\n *\n * 1. **Balanced.** The fragment closes every element it opens. Splitting chrome\n * at an arbitrary point produces halves the parser silently repairs — the\n * failure that cost a page 36 against 100 and the reason `body` exists.\n * 2. **Outside the content path.** The region sits wholly before (header) or\n * after (footer) `CHROME_BODY_SLOT` and is not an ancestor of it. A region\n * that wraps the content cannot be lifted; it belongs in `body`.\n *\n * When either is in doubt, keep everything in one `body` chunk: a single blob\n * is always correct, and this split is an optimisation on top of it.\n */\nexport interface ChromeChunk {\n id: string\n position: 'head' | 'body' | 'before_body' | 'after_body' | 'header' | 'footer'\n html: string\n /**\n * Shared component name for a `header`/`footer` chunk — `SiteHeader`,\n * `BlogFooter`. Chunks that carry the same name AND the same html collapse\n * into one emitted component; same name with different html gets a suffixed\n * name and a warning, never a silent swap. Default: `SiteHeader`/`SiteFooter`.\n */\n component?: string\n}\n\nexport interface ComponentPlacement {\n /** ComponentDef id. */\n component: string\n variant?: string\n selector?: string\n}\n\nexport interface FamilyVariant {\n key: string\n description?: string\n}\n\n/**\n * Attributes on the document's root elements, carried verbatim from the source\n * page. Themes hang layout on them — WordPress writes `<body class=\"wp-singular\n * post-template-default single …\">` and scripts add `<html class=\"js wf-…\">`,\n * and the stylesheet's container rules key off exactly those classes. Dropping\n * them costs a correct-content page its entire layout (measured: 36.4 vs 100).\n *\n * Values may carry `@@mark@@` placeholders, so per-page classes (`postid-123`)\n * survive the same filling as chrome.\n *\n * Whether a site needs them is not knowable from one page: the same test that\n * moved 10up 36.4 → 100 moved wptavern 36 → 36, because that theme does not key\n * off body classes. Carry them always.\n */\nexport interface RootAttrs {\n html?: Record<string, string>\n body?: Record<string, string>\n}\n\nexport interface LayoutFamily {\n id: string\n name?: string\n kind?: RouteKind\n chrome?: ChromeChunk[]\n /** `<html>` / `<body>` attributes from the source page — see `RootAttrs`. */\n root_attrs?: RootAttrs\n slots?: SlotBinding[]\n components?: ComponentPlacement[]\n css: {\n strategy: CssStrategy\n /**\n * Stylesheets every page of the family loads.\n *\n * Page-builder sites emit CSS per page (`post-11368.css`,\n * `local/global-11368-frontend-*`), so one member's stylesheet set is not\n * the family's: building from a single template page scored 35.6, while\n * the UNION of the members' stylesheets scored 100. Producers put the\n * union here and the per-page remainder on the page itself.\n */\n files?: string[]\n }\n /** Observed column counts per viewport class (e.g. desktop 4 → mobile 1). */\n columns?: Partial<Record<'desktop' | 'mobile', number>>\n variants?: FamilyVariant[]\n /** Verification trail: which pages formed the family, held-out fidelity score. */\n evidence?: {\n pages?: string[]\n holdout_score?: number\n }\n}\n\n/**\n * Component semantic vocabulary. `source` records where its content can come\n * from: `rest` (derivable from content data — author box, term list),\n * `runtime` (needs a live service — comments form, search), `chrome` (carried\n * as rendered markup — nav, ads).\n */\nexport const COMPONENT_TYPES = [\n 'nav',\n 'related',\n 'comments',\n 'ads',\n 'chrome',\n 'taxonomy',\n 'author',\n 'share',\n 'breadcrumb',\n 'meta',\n 'pagination',\n 'card',\n 'custom',\n] as const\n\nexport type ComponentType = (typeof COMPONENT_TYPES)[number]\n\nexport type ComponentSource = 'rest' | 'runtime' | 'chrome'\n\nexport interface ComponentVariantDef {\n key: string\n conditions?: RouteVariantRule[]\n description?: string\n}\n\nexport interface ComponentDef {\n id: string\n type: ComponentType\n source: ComponentSource\n name?: string\n selector?: string\n /** e.g. a card component with `news`, `compact`, `featured` variants. */\n variants?: ComponentVariantDef[]\n}\n\nexport type ExcerptSource = 'excerpt' | 'content_first_paragraph' | 'none'\nexport type PaginationKind = 'numbered' | 'infinite' | 'none'\n\n/**\n * The query behind a rendered list: which content, filtered how, in what\n * order, how many. Inferred by matching rendered items against candidate\n * queries — order matters, so the sequence is part of the contract.\n */\nexport interface QueryBinding {\n id: string\n /** Content source: \"posts\", \"pages\", a CPT slug, or a content-model id. */\n source: string\n taxonomy?: {\n taxonomy: string\n /** Fixed term slug, or the route param carrying it. */\n term?: string\n term_param?: string\n }\n author?: {\n slug?: string\n param?: string\n }\n order: {\n by: 'date' | 'title' | 'menu_order' | 'custom'\n direction: 'asc' | 'desc'\n }\n per_page: number | null\n pagination: PaginationKind\n excerpt_source?: ExcerptSource\n /** WordPress image size name the list renders (e.g. \"medium_large\"). */\n image_size?: string\n /** Locale this query is filtered to, on a multilingual site. */\n locale?: string\n}\n\n/**\n * - `responsive`: one build, CSS handles viewports.\n * - `split`: desktop and mobile variants are produced separately and served\n * by device — for sites whose mobile DOM differs beyond CSS.\n */\nexport type ViewportStrategy = 'responsive' | 'split'\n\nexport interface ProjectIR {\n version: number\n site: {\n url: string\n title?: string\n locales?: string[]\n }\n routes: RouteModel[]\n families: LayoutFamily[]\n components?: ComponentDef[]\n queries?: QueryBinding[]\n tokens?: DesignTokens\n /** Default CSS packaging for families that don't override it. */\n css_default: CssStrategy\n viewport_strategy?: ViewportStrategy\n /** Content-model ids this project's queries and slots refer to. */\n content_models?: string[]\n}\n\n// ─── Comments export ───\n//\n// Comments cross one more boundary than the rest of RawIR: they leave the\n// generated static site entirely and land in a live service's database,\n// addressed by content entry — not by WordPress post id. This export is that\n// bridge: the source map translates WP ids to entry addresses, and the\n// comments ride along unmodified.\n\nexport const COMMENTS_EXPORT_FORMAT = 'contentrain-comments@1'\n\n/** Where one WordPress post's content ended up. */\nexport interface EntrySourceRef {\n model_id: string\n entry_id: string\n locale?: string\n}\n\n/**\n * WordPress post id (stringified) → content entry address. Only the tool that\n * wrote the content store can produce this; nothing downstream can recover it.\n */\nexport type EntrySourceMap = Record<string, EntrySourceRef>\n\nexport interface CommentsExport {\n version: number\n format: typeof COMMENTS_EXPORT_FORMAT\n source: RawProvenance\n site_url?: string\n /** ISO 8601 UTC. */\n generated_at: string\n entries: EntrySourceMap\n /** WP post ids whose comment form was closed — the receiving side opens those threads closed. */\n threads_closed?: number[]\n /** Verbatim `RawComment`s; parents are re-linked via `RawComment.parent` in a second pass. */\n comments: RawComment[]\n}\n\n/** Comments summary + payload pointer on the handoff (capability counts are not enough for intake). */\nexport interface HandoffComments {\n total: number\n by_status?: Record<string, number>\n types?: Record<string, number>\n export?: {\n format: typeof COMMENTS_EXPORT_FORMAT\n /** Where the full export can be fetched… */\n url?: string\n /** …or the export itself, inline, for small sites. */\n inline?: CommentsExport\n }\n threads_closed?: number[]\n unresolved?: Array<{ comment_id: number; post: number; reason: string }>\n}\n\n// ─── MigrationHandoff ───\n//\n// What the migration hands the user: where the generated project lives, what\n// happened to each detected capability, and — for capabilities that need a\n// runtime — the offers. Offering is this document's job; fulfilling an offer\n// is the receiving product's. The migration itself is complete without any\n// offer being accepted.\n\nexport type CapabilityDisposition =\n | 'migrated_static'\n | 'archived'\n | 'needs_runtime'\n | 'external_adapter'\n | 'kept_on_wordpress'\n | 'dropped'\n\nexport interface HandoffCapability {\n key: CapabilityKey\n disposition: CapabilityDisposition\n detail?: string\n /** e.g. comments: { migrated: 412, spam_skipped: 60 } */\n counts?: Record<string, number>\n}\n\nexport type OfferProvider = 'studio_managed' | 'adapter' | 'keep_wordpress'\n\nexport interface CostEstimate {\n currency: string\n monthly: number\n assumptions?: string[]\n}\n\n/**\n * The comparison shown when a runtime capability is offered: what running the\n * old server costs (maintenance, security, hosting) against the managed\n * option — so declining is an informed choice, not a default.\n */\nexport interface CostComparison {\n self_host?: CostEstimate\n managed?: CostEstimate\n}\n\nexport interface HandoffOffer {\n capability: CapabilityKey\n provider: OfferProvider\n /** Adapter identifier when `provider` is `adapter`. */\n adapter?: string\n cost_comparison?: CostComparison\n /** e.g. \"keeping comments on WordPress means the WordPress server stays live\". */\n warning?: string\n}\n\nexport interface MigrationHandoff {\n version: number\n site_url: string\n /** ISO 8601 UTC. */\n generated_at: string\n repository?: {\n provider: 'github' | 'gitlab'\n owner: string\n name: string\n default_branch: string\n }\n preview_url?: string\n content_summary?: {\n models: number\n entries: number\n locales?: string[]\n }\n capabilities: HandoffCapability[]\n /** Present whenever the source had comments — see `HandoffComments`. */\n comments?: HandoffComments\n offers?: HandoffOffer[]\n notes?: string[]\n}\n","import type { SyncResult } from './index.js'\n\n// ─── Repository Provider Contracts ───\n//\n// Shared interfaces for the provider-agnostic content repository model used\n// by @contentrain/mcp. They live in @contentrain/types so third-party tools\n// can implement a custom RepoProvider (e.g. for a private git host, an\n// internal service, a mock in a test suite) without depending on MCP\n// internals.\n//\n// @contentrain/mcp re-exports every symbol here from\n// @contentrain/mcp/core/contracts so existing consumers do not have to\n// migrate imports.\n\n// ─── File change ───\n\n/**\n * A single file change within a plan.\n *\n * - `content: string` — write or overwrite the file with this UTF-8 content.\n * - `content: null` — delete the file.\n *\n * Paths are content-root relative, use forward slashes, and must not contain\n * `..` segments or absolute anchors. Providers are responsible for resolving\n * paths against their backing store (worktree, git tree, etc.).\n */\nexport interface FileChange {\n path: string\n content: string | null\n}\n\n// ─── Capabilities ───\n\n/**\n * Capabilities describe what a provider can and cannot do. Operations check\n * required capabilities before running. Normalize extract needs `sourceRead`;\n * normalize reuse needs `sourceWrite`; submit needs `pushRemote`; AST scans\n * need `astScan` (which implies a local working tree).\n *\n * The consistent position is: all git hosts are commodity MIT providers; the\n * distinction between providers is operational (how they read/write), not\n * commercial. Enterprise features live in Studio, not in capability gates.\n */\nexport interface ProviderCapabilities {\n /** Provider backs onto a local worktree and can selectively sync changes into the developer's working tree. */\n localWorktree: boolean\n /** Provider can read arbitrary source files outside `.contentrain/`. Required for normalize extract. */\n sourceRead: boolean\n /** Provider can write arbitrary source files outside `.contentrain/`. Required for normalize reuse. */\n sourceWrite: boolean\n /** Provider can push commits to a remote. Required for submit. */\n pushRemote: boolean\n /** Provider detects branch protection rules on the remote. */\n branchProtection: boolean\n /** Provider can open a pull request as a merge fallback when branch protection blocks direct merge. */\n pullRequestFallback: boolean\n /** Provider can execute AST scanners against source files. Implies local disk access. */\n astScan: boolean\n /**\n * Provider can write a two-parent merge commit (see\n * `RepoProvider.createMergeCommit`). Absent = false. Optional so that\n * existing external `implements` code keeps compiling: a required member\n * here would be a breaking change for every custom provider.\n */\n mergeCommit?: boolean\n}\n\n/** Capability set for the LocalProvider (simple-git + worktree). */\nexport const LOCAL_CAPABILITIES: ProviderCapabilities = {\n localWorktree: true,\n sourceRead: true,\n sourceWrite: true,\n pushRemote: true,\n branchProtection: false,\n pullRequestFallback: false,\n astScan: true,\n mergeCommit: true,\n}\n\n// ─── Reader ───\n\n/**\n * Read-only interface to a content repository.\n *\n * Paths are relative to the repository's content root (e.g.\n * `.contentrain/config.json`). The `ref` parameter is a branch name, tag,\n * or commit SHA. Providers that operate on a single working tree\n * (LocalReader) ignore `ref`; API-backed providers use it to resolve the\n * correct revision.\n *\n * `readFile` and `listDirectory` deliberately have different error semantics:\n * - `readFile` THROWS when the file is missing so callers must opt into\n * tolerance explicitly (typically with a try/catch returning a default).\n * - `listDirectory` returns `[]` for a missing directory because the empty\n * case is the common, uninteresting one.\n */\nexport interface RepoReader {\n /**\n * Read a file's contents as UTF-8.\n * @throws when the file does not exist or cannot be read.\n */\n readFile(path: string, ref?: string): Promise<string>\n\n /**\n * List file and directory names directly under `path`. Does not recurse.\n * Returns an empty array when the directory does not exist.\n */\n listDirectory(path: string, ref?: string): Promise<string[]>\n\n /** Check whether a file or directory exists at `path`. */\n fileExists(path: string, ref?: string): Promise<boolean>\n}\n\n// ─── Writer ───\n\nexport interface CommitAuthor {\n name: string\n email: string\n}\n\nexport interface Commit {\n sha: string\n message: string\n author: CommitAuthor\n timestamp: string\n /**\n * What the provider did with the branch. Providers that own the merge\n * decision (a local worktree honouring the project's `workflow` setting)\n * report it here; providers that hand the branch off to an external\n * orchestrator leave it undefined, and the caller treats that as\n * `pending-review`.\n */\n workflowAction?: 'auto-merged' | 'pending-review'\n /**\n * Selective-sync bookkeeping — which files were copied into the developer's\n * working tree and which were skipped because of local edits. Only\n * providers backed by a local worktree populate it.\n */\n sync?: SyncResult\n /** Non-fatal problem encountered while completing the write. */\n warning?: string\n /**\n * Whether the base branch advanced to the contentrain tip after an\n * auto-merge. Only providers that own the base advance (a local worktree)\n * report it. `blocked_diverged` is a partial success: the content is on\n * the contentrain branch; only the fast-forward is pending.\n */\n base_advance?: BaseAdvance\n /** Outcome of pushing the contentrain branch, when the provider pushes. */\n remote_push?: RemotePush\n}\n\n/**\n * Input to `applyPlan`. Represents a single atomic commit: all changes land\n * in one commit on `branch`, created from `base` if it does not yet exist.\n */\nexport interface ApplyPlanInput {\n /** Branch name to commit to. Created from `base` if missing. */\n branch: string\n /** File additions, modifications and deletions to apply in a single commit. */\n changes: FileChange[]\n /** Commit message. */\n message: string\n /** Commit author. */\n author: CommitAuthor\n /**\n * Optional base branch. Defaults to the Contentrain content-tracking\n * branch (`CONTENTRAIN_BRANCH` — the `contentrain` ref) — NOT the\n * repository's default branch. This is the single source of truth for\n * content state; every feature branch forks from it. Pass an explicit\n * `base` only when you know you want to bypass the invariant.\n */\n base?: string\n /**\n * Describes the operation that produced these changes. A provider that\n * regenerates `.contentrain/context.json` itself (the local worktree flow\n * does, after merge, single-threaded) uses it; others ignore it. It is\n * never committed on the feature branch — context.json embeds timestamps,\n * so two branches forked from the same commit would always conflict on it.\n */\n context?: { tool: string; model: string; locale?: string; entries?: string[] }\n /** Override the project's configured workflow for this write. */\n workflowOverride?: 'auto-merge' | 'review'\n}\n\n/**\n * Write-side interface. Providers implement this to persist a set of file\n * changes as a single atomic commit. LocalProvider writes through a worktree\n * and `git commit`; API-backed providers post to the Git Data API or\n * equivalent.\n */\nexport interface RepoWriter {\n applyPlan(input: ApplyPlanInput): Promise<Commit>\n}\n\n// ─── Branch / diff / merge ───\n\nexport interface Branch {\n name: string\n sha: string\n protected?: boolean\n}\n\nexport interface FileDiff {\n path: string\n status: 'added' | 'modified' | 'removed'\n before: string | null\n after: string | null\n}\n\nexport interface MergeResult {\n merged: boolean\n sha: string | null\n pullRequestUrl: string | null\n /**\n * Selective-sync bookkeeping — only populated by providers that back onto\n * a local worktree (LocalProvider). Remote-API providers (GitHub, GitLab,\n * etc.) omit it because they do not touch a developer's working tree.\n */\n sync?: SyncResult\n /**\n * Outcome of the post-merge source-branch cleanup on the remote.\n * `deleted: false` with `skipped` is an expected no-op (cleanup disabled,\n * no remote, ref already gone); `warning` is a real failure (offline,\n * auth) that did NOT fail the merge itself.\n */\n remote?: {\n deleted: boolean\n skipped?: string\n warning?: string\n }\n}\n\n// ─── Base-branch advance / remote push outcomes ───\n\n/**\n * What happened to the base branch after content landed on the\n * content-tracking branch.\n *\n * `advanced` — the base branch fast-forwarded to the contentrain tip.\n * `blocked_diverged` — the base branch holds commits that are not in\n * contentrain, so fast-forwarding was impossible. The content itself is\n * safe on the contentrain branch; only the advance is pending until the\n * branches are reconciled.\n *\n * These two values are shared vocabulary across the ecosystem — Studio\n * reports the same enum for its main-advance state. A pull request is an\n * attachment (`pullRequestUrl`), never a third state: \"PR opened\" is\n * `blocked_diverged` with a non-null URL.\n */\nexport type BaseAdvance = 'advanced' | 'blocked_diverged'\n\n/**\n * Outcome of pushing the content-tracking branch to the configured remote.\n *\n * `pushed` — the remote accepted the push (possibly after one\n * fetch-merge-retry). `rejected` — the push failed even after the retry;\n * local and remote have diverged and need reconciling. `no-remote` — the\n * repository has no configured remote, so nothing was attempted.\n */\nexport type RemotePush = 'pushed' | 'rejected' | 'no-remote'\n\n// ─── Reconcile / conflicts ───\n\n/**\n * Machine-readable conflict kinds produced by the content-aware three-way\n * reconcile planner. CLOSED SET: consumers (Studio) key their localized\n * editor questions on these values, so a free string would drift. Adding a\n * value here is a minor version bump and MUST carry a changelog entry;\n * renaming or removing one is breaking.\n */\nexport type ConflictCode =\n | 'field_value_conflict' // collection/singleton: same field changed differently on both sides\n | 'dictionary_value_conflict' // same dictionary key, two different values\n | 'vocabulary_value_conflict' // same term + locale, two different translations\n | 'model_key_conflict' // same model schema key changed differently\n | 'meta_status_conflict' // both sides moved an entry's publish status differently\n | 'document_body_conflict' // both sides edited a document body (never text-merged)\n | 'frontmatter_value_conflict' // same frontmatter key, two different values\n | 'delete_edit_conflict' // one side deleted what the other edited\n | 'file_conflict' // unrecognized file changed on both sides — resolved by choosing a side\n\n/**\n * One surviving conflict from a three-way reconcile: base (merge-base),\n * ours (content branch), theirs (base branch) disagree in a way the policy\n * table cannot resolve mechanically. Everything mechanical has already been\n * merged; only these items need a decision from an editor or agent.\n *\n * `id` is derived from the position AND the three values (see\n * `conflictId`), so a resolution made against a stale dry-run no longer\n * matches once any side changes — compare-and-set for free.\n */\nexport interface ConflictItem {\n id: string\n /** Content-root-relative path of the file the conflict lives in. */\n path: string\n model?: string\n kind: 'collection' | 'singleton' | 'document' | 'dictionary'\n | 'vocabulary' | 'model' | 'meta' | 'file'\n /** Entry ID, dictionary key, vocabulary term, or model schema key. */\n key?: string\n /** Field inside the entry / frontmatter key, when the conflict is field-level. */\n field?: string\n locale?: string\n /** The three values in question — for display; omitted for `kind: 'file'`. */\n base?: unknown\n ours?: unknown\n theirs?: unknown\n code: ConflictCode\n /** Ready-to-display English sentence naming the conflict. */\n message: string\n /**\n * Policy-table suggestion, never auto-applied. Present only where the\n * approved policy names a preferred side (model schema keys → `theirs`).\n */\n suggested?: 'ours' | 'theirs'\n}\n\n/**\n * A decision for one conflict, keyed by `ConflictItem.id`. `choose` picks a\n * side (absence of the item on that side means deletion); `value` supplies\n * a hand-authored replacement. A resolution whose `id` no longer matches a\n * live conflict is dropped and the conflict re-reported with a fresh id.\n */\nexport type ConflictResolution =\n | { id: string; choose: 'ours' | 'theirs' }\n | { id: string; value: unknown }\n\n// ─── Media (optional provider facet) ───\n\n/**\n * One media asset as the provider's media stack reports it. `path` is the\n * repo-relative storage path content fields reference (`media/...`); `url`\n * is the absolute delivery URL when the provider can resolve one (see\n * `RepoProvider.mediaBaseUrl`). `meta` carries provider-defined extras\n * (dimensions, blurhash, variants) without widening this contract.\n */\nexport interface MediaAsset {\n id: string\n path: string\n url?: string\n mime?: string\n size?: number\n alt?: string\n tags?: string[]\n createdAt?: string\n meta?: Record<string, unknown>\n}\n\nexport interface MediaListOptions {\n /** Substring match on filename/path/alt. */\n search?: string\n /** Filter to assets carrying this tag. */\n tag?: string\n /** Page size — implementations may clamp. */\n limit?: number\n /** Opaque cursor from a previous `MediaListResult.nextCursor`. */\n cursor?: string\n}\n\nexport interface MediaListResult {\n assets: MediaAsset[]\n nextCursor?: string\n total?: number\n}\n\n/**\n * URL-based ingest — MCP has no binary channel, so the source is always a\n * URL the provider fetches server-side. Implementations MUST enforce their\n * own SSRF, MIME, and size policies before fetching; MCP passes the input\n * through verbatim and never fetches the URL itself.\n */\nexport interface MediaIngestInput {\n url: string\n filename?: string\n alt?: string\n tags?: string[]\n}\n\nexport interface MediaUpdateInput {\n alt?: string\n tags?: string[]\n filename?: string\n}\n\n/**\n * Optional media facet of a provider. When present on `RepoProvider.media`,\n * the MCP server registers the `contentrain_media_*` tools; when absent\n * (LocalProvider, plain GitHub/GitLab providers), those tools are not\n * listed at all. Hosted providers (Studio MCP Cloud) implement this by\n * delegating to their media stack — deterministic passthrough, no content\n * decisions in MCP.\n */\nexport interface MediaProvider {\n list(opts?: MediaListOptions): Promise<MediaListResult>\n get(id: string): Promise<MediaAsset | null>\n ingest(input: MediaIngestInput): Promise<MediaAsset>\n update(id: string, patch: MediaUpdateInput): Promise<MediaAsset>\n delete(id: string): Promise<void>\n}\n\n// ─── Provider (full surface) ───\n\n/**\n * A content repository provider — the unified surface that MCP tools drive.\n *\n * Implementations wrap a git backend:\n *\n * - `LocalProvider` — simple-git + temp worktree + selective sync\n * - `GitHubProvider` — Octokit Git Data API (no clone)\n * - `GitLabProvider` — gitbeaker REST client (no clone)\n * - `BitbucketProvider` — planned, coming soon\n *\n * Providers are commodity and all live in MIT.\n */\n/** Result of {@link RepoProvider.checkWriteReadiness}. */\nexport interface WriteReadiness {\n blocked: boolean\n /** Human-readable reason, present when `blocked`. */\n message?: string\n}\n\nexport interface RepoProvider extends RepoReader, RepoWriter {\n readonly capabilities: ProviderCapabilities\n\n /**\n * Per-project public media delivery base, e.g.\n * `https://studio.example/api/cdn/v1/{projectId}` — already includes the\n * project segment, so the content-write path only joins `{base}/{path}`.\n *\n * Set by hosted providers (Studio's MCP Cloud loopback resolves it from a\n * request header and attaches it here). When present, content writes\n * normalize relative `media/...` references — in media/image/file fields and\n * markdown bodies — to absolute delivery URLs, so external-agent writes\n * render anywhere without an SDK. Undefined for local/CLI providers, where\n * media stays a relative path (the OSS file model). Never affects reads.\n */\n readonly mediaBaseUrl?: string\n\n /**\n * Optional media facet. Present only on providers whose backend exposes a\n * media stack (Studio MCP Cloud); drives the `contentrain_media_*` MCP\n * tools, which are not registered when this is absent.\n */\n readonly media?: MediaProvider\n\n /**\n * Whether the provider will accept a write right now.\n *\n * A local worktree accumulates unmerged `cr/*` branches and eventually has\n * to refuse new ones; a hosted provider manages that server-side. Asking the\n * provider keeps the tool layer out of the business of counting git branches\n * — it previously reached through an `instanceof` check to a `projectRoot`\n * only one implementation has.\n *\n * Optional: a provider that omits it is always ready.\n */\n checkWriteReadiness?(): Promise<WriteReadiness>\n\n listBranches(prefix?: string): Promise<Branch[]>\n createBranch(name: string, fromRef?: string): Promise<void>\n deleteBranch(name: string): Promise<void>\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]>\n /**\n * Merge `branch` into `into`. Like `git merge` and the platform merge APIs,\n * the source branch is left in place by default. Pass\n * `opts.removeSourceBranch: true` to also delete it after a successful merge\n * (best-effort — reported via `MergeResult.remote`, never a thrown error).\n * Even when opted in, a long-lived branch is never deleted: not `into`, not\n * the `contentrain` content branch, and not the repo's default branch.\n * LocalProvider ignores the option: its cleanup is governed by\n * `config.remoteBranchCleanup` and its own `cr/*`-only guard.\n */\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult>\n isMerged(branch: string, into?: string): Promise<boolean>\n getDefaultBranch(): Promise<string>\n\n /**\n * The merge-base commit of two refs — the `base` input of a three-way\n * reconcile.\n *\n * Reconciling a diverged content branch needs the last common ancestor,\n * and every backend can name it (git `merge-base`, GitHub's compare API,\n * GitLab's merge_base endpoint) — but not every existing implementation\n * does yet, and demanding it would break external `implements` code.\n *\n * Optional: a provider that omits it cannot drive a reconcile; the caller\n * falls back to a pull-request flow. Returns `null` when the refs share\n * no history.\n */\n getMergeBase?(refA: string, refB: string): Promise<string | null>\n\n /**\n * Write a set of resolved changes as a TWO-PARENT merge commit on\n * `branch`, joining `ours` and `theirs` so that git history records the\n * reconcile — after it, `theirs` is an ancestor of `branch` and a plain\n * fast-forward advance works again. `changes` are applied on top of the\n * `ours` tree; for content-owned paths the planner's output is\n * authoritative, never a textual auto-merge.\n *\n * Optional, mirrored by `capabilities.mergeCommit`: a backend whose\n * commits API cannot express two parents (GitLab) omits it and the\n * caller falls back to a merge-request flow.\n */\n createMergeCommit?(input: {\n /** Branch whose tip becomes the merge commit (the content branch). */\n branch: string\n /** First parent — the current tip of `branch`. */\n ours: string\n /** Second parent — the ref being reconciled in. */\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit>\n}\n","// ─── Type System ───\n\nexport type FieldType =\n | 'string' | 'text' | 'email' | 'url' | 'slug' | 'color' | 'phone' | 'code' | 'icon'\n | 'markdown' | 'richtext'\n | 'number' | 'integer' | 'decimal' | 'percent' | 'rating'\n | 'boolean'\n | 'date' | 'datetime'\n | 'image' | 'video' | 'file'\n | 'relation' | 'relations'\n | 'select' | 'array' | 'object'\n\nexport type ModelKind = 'singleton' | 'collection' | 'document' | 'dictionary'\nexport type ContentStatus = 'draft' | 'in_review' | 'published' | 'rejected' | 'archived'\nexport type ContentSource = 'agent' | 'human' | 'import'\nexport type WorkflowMode = 'auto-merge' | 'review'\n// Meta-frameworks\nexport type StackType =\n | 'nuxt' | 'next' | 'astro' | 'sveltekit' | 'remix' | 'analog'\n // Plain frameworks\n | 'vue' | 'react' | 'svelte' | 'solid' | 'angular'\n // Mobile\n | 'react-native' | 'expo' | 'flutter'\n // Backend\n | 'node' | 'express' | 'fastify' | 'nestjs' | 'django' | 'rails' | 'laravel' | 'go' | 'rust' | 'dotnet'\n // Static site generators\n | 'hugo' | 'jekyll' | 'eleventy'\n // Desktop\n | 'electron' | 'tauri'\n // Catch-all\n | 'other'\n\nexport type Platform = 'web' | 'mobile' | 'api' | 'desktop' | 'static' | 'other'\nexport type ContextSource = 'mcp-local' | 'mcp-studio' | 'studio-ui'\nexport type CollectionRuntimeFormat = 'map' | 'array'\n\n// ─── Field Definition ───\n\nexport interface FieldDef {\n type: FieldType\n required?: boolean\n unique?: boolean\n default?: unknown\n min?: number\n max?: number\n pattern?: string\n options?: string[]\n model?: string | string[]\n items?: string | FieldDef\n fields?: Record<string, FieldDef>\n accept?: string\n maxSize?: number\n description?: string\n /**\n * Human-readable name for this field in an editing UI.\n *\n * A plain string is one label for every locale; an object keyed by locale\n * carries a translation per locale. The union exists now rather than later\n * because widening `string` to it afterwards would break every consumer\n * that treats the value as a string.\n *\n * Absent means the field name itself is the label — which is why an editor\n * shows `body_public` and `is_category_hero` today.\n */\n label?: string | Record<string, string>\n /**\n * Display position in an editing UI, ascending.\n *\n * Fields are stored in canonical alphabetical order, so without this an\n * editor shows them alphabetically: on a 16-field article model `author`\n * comes first and `title` fifteenth. Fractional values are allowed so a\n * field can be inserted between two others without renumbering the rest.\n *\n * Fields without one sort after every field that has one, alphabetically\n * among themselves — so a model that has not been given an order behaves\n * exactly as it does today.\n */\n order?: number\n}\n\n// ─── Model Definition ───\n\nexport type LocaleStrategy = 'file' | 'suffix' | 'directory' | 'none'\n\nexport interface ModelDefinition {\n id: string\n name: string\n kind: ModelKind\n domain: string\n i18n: boolean\n /**\n * Name of the field whose value is shown as an entry's title — in a Studio row,\n * a picker, a relation reference. Declared, never guessed: a consumer that infers\n * the title from field order or length picks the icon over the headline.\n *\n * Must name a field that exists on this model and whose type can render as text\n * (see `isTitleFieldType`). Dictionary models have no fields, so their only legal\n * value is `DICTIONARY_TITLE_FIELD` — the entry key is the title.\n */\n title_field: string\n description?: string\n fields?: Record<string, FieldDef>\n /** Framework-relative path for content files (e.g. \"content/blog\", \"locales\"). When set, content is written here instead of .contentrain/content/ */\n content_path?: string\n /** How locale is encoded in file names. Default: \"file\" ({dir}/{locale}.json or {dir}/{slug}/{locale}.md) */\n locale_strategy?: LocaleStrategy\n}\n\n// ─── Config ───\n\nexport interface ContentrainConfig {\n version: number\n platform?: Platform\n stack: StackType\n workflow: WorkflowMode\n repository?: {\n /**\n * Git host backing this project's remote. Widens as new providers ship\n * in @contentrain/mcp — `github` and `gitlab` are currently supported.\n * The value is informational for tooling: the concrete provider the\n * MCP server talks to is chosen at server construction time, not from\n * this config.\n */\n provider: 'github' | 'gitlab'\n owner: string\n name: string\n default_branch: string\n }\n locales: {\n default: string\n supported: string[]\n }\n domains: string[]\n assets_path?: string\n /**\n * Public media delivery base for resolving relative `media/...` references to\n * absolute URLs (e.g. a deployed CDN delivery endpoint). When set,\n * `@contentrain/query`'s `generate` bakes a `media()` resolver into the\n * generated client. The base may already include any project segment — the\n * resolver only joins `{cdn.url}/{path}`. Distinct from `assets_path` (the\n * local on-disk asset directory).\n */\n cdn?: {\n url?: string\n }\n /** Days a merged `cr/*` branch is kept before lazy cleanup. Default: 30. */\n branchRetention?: number\n /** Unmerged `cr/*` branch count that triggers a warning. Default: 50. */\n branchWarnLimit?: number\n /** Unmerged `cr/*` branch count that blocks new writes. Default: 80. */\n branchBlockLimit?: number\n /**\n * Delete the remote copy of a `cr/*` branch when it is merged or deleted\n * locally, and allow lazy pruning of already-merged remote branches.\n * Read-only remote visibility (doctor, branch listing) is not affected.\n * Default: true.\n */\n remoteBranchCleanup?: boolean\n}\n\n// ─── Vocabulary ───\n\nexport interface Vocabulary {\n version: number\n terms: Record<string, Record<string, string>>\n}\n\n// ─── Metadata ───\n\nexport interface EntryMeta {\n status: ContentStatus\n source: ContentSource\n updated_by: string\n /**\n * When this entry was last written, ISO 8601 UTC.\n *\n * Optional, and deliberately not backfilled. It is a fact about the past,\n * and an entry written before the field existed has no recoverable value —\n * a fabricated timestamp would be worse than an absent one, because it\n * would sort. Absent means unknown; the first write mints the real value.\n */\n updated_at?: string\n approved_by?: string | null\n version?: string\n publish_at?: string\n expire_at?: string\n}\n\nexport type SingletonMeta = EntryMeta\nexport type CollectionMeta = Record<string, EntryMeta>\nexport type DocumentMeta = EntryMeta\nexport type DictionaryMeta = EntryMeta\n\n// ─── Asset ───\n\nexport interface AssetEntry {\n path: string\n type: string\n size: number\n alt?: string\n}\n\n// ─── Validation ───\n\nexport interface ValidationError {\n severity: 'error' | 'warning' | 'notice'\n model?: string\n locale?: string\n entry?: string\n slug?: string\n field?: string\n message: string\n}\n\nexport interface ValidationResult {\n valid: boolean\n errors: ValidationError[]\n}\n\n// ─── Content Types (Storage vs Output) ───\n\n// Storage formats — how content is written to disk (canonical JSON)\nexport type SingletonContentFile = Record<string, unknown>\nexport type CollectionContentFile = Record<string, Record<string, unknown>>\nexport type DictionaryContentFile = Record<string, string>\n// Document storage = markdown file (frontmatter + body), not JSON\n\n// Output formats — how MCP/SDK returns content to consumers\nexport type CollectionEntry = { id: string } & Record<string, unknown>\nexport type CollectionContentOutput = CollectionEntry[]\n\n/** Document entry as returned by MCP/SDK — parsed markdown with frontmatter */\nexport interface DocumentEntry {\n slug: string\n frontmatter: Record<string, unknown>\n body: string\n}\n\nexport type DocumentContentOutput = DocumentEntry[]\n\n/** Polymorphic relation storage — used when model field is string[] (multiple targets) */\nexport interface PolymorphicRelationRef {\n model: string\n ref: string\n}\n\n// ─── Model Summary ───\n\n/** Lightweight model info for listing and status operations */\nexport interface ModelSummary {\n id: string\n kind: ModelKind\n domain: string\n i18n: boolean\n fields: number\n}\n\n// ─── File Framework ───\n\n/** Source file framework — detected from extension for normalize/apply operations */\nexport type FileFramework = 'vue' | 'svelte' | 'jsx' | 'astro' | 'script'\n\n// ─── Path Conventions ───\n\n/** Root directory name for all Contentrain data */\nexport const CONTENTRAIN_DIR = '.contentrain' as const\n\n/**\n * Default path patterns for Contentrain projects.\n * Variables: {domain}, {modelId}, {locale}, {slug}\n *\n * When a model has `content_path` set, content files are written to\n * `{projectRoot}/{content_path}/` instead — but meta files always\n * remain under `.contentrain/meta/`.\n */\nexport const PATH_PATTERNS = {\n config: '.contentrain/config.json',\n context: '.contentrain/context.json',\n vocabulary: '.contentrain/vocabulary.json',\n model: '.contentrain/models/{modelId}.json',\n content: {\n singleton: '.contentrain/content/{domain}/{modelId}/{locale}.json',\n collection: '.contentrain/content/{domain}/{modelId}/{locale}.json',\n document: '.contentrain/content/{domain}/{slug}/{locale}.md',\n dictionary: '.contentrain/content/{domain}/{modelId}/{locale}.json',\n /** Non-i18n content (i18n: false) */\n noLocale: '.contentrain/content/{domain}/{modelId}/data.json',\n },\n meta: {\n singleton: '.contentrain/meta/{modelId}/{locale}.json',\n collection: '.contentrain/meta/{modelId}/{locale}.json',\n document: '.contentrain/meta/{modelId}/{slug}/{locale}.json',\n dictionary: '.contentrain/meta/{modelId}/{locale}.json',\n },\n} as const\n\n// ─── Validation Patterns ───\n\n/** Slug: lowercase alphanumeric with hyphens */\nexport const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n/** Entry ID: alphanumeric, 1-40 chars, starts with alphanumeric */\nexport const ENTRY_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,39}$/\n/** Locale: ISO 639-1 with optional region code */\nexport const LOCALE_PATTERN = /^[a-z]{2}(?:-[A-Z]{2})?$/\n\n// ─── Canonical Serialization ───\n\n/** Rules for deterministic JSON output — ensures stable git diffs */\nexport const CANONICAL_JSON = {\n indent: 2,\n encoding: 'utf-8',\n trailingNewline: true,\n omitNull: true,\n omitDefaults: true,\n sortKeys: true,\n} as const\n\n/**\n * Key order for canonical model JSON — the required keys first, then the optional\n * ones, then `fields` last so a reader sees the model's identity before scrolling\n * past its schema.\n *\n * This lives here, and only here, because `canonicalStringify` falls back to\n * alphabetical for any key absent from the order: a forgotten entry does not error,\n * it silently relocates. `satisfies` catches a key that is not on `ModelDefinition`;\n * the `keyof` assertion in index.test.ts catches a `ModelDefinition` key missing\n * from this array. Between them, drift fails `tsc`, which runs on every commit.\n */\nexport const MODEL_FIELD_ORDER = [\n 'id',\n 'name',\n 'kind',\n 'domain',\n 'i18n',\n 'title_field',\n 'description',\n 'content_path',\n 'locale_strategy',\n 'fields',\n] as const satisfies readonly (keyof ModelDefinition)[]\n\n// ─── Scaffold ───\n\nexport interface ScaffoldTemplate {\n id: string\n models: ModelDefinition[]\n sample_content?: Record<string, Record<string, unknown>>\n vocabulary?: Record<string, Record<string, string>>\n}\n\n// ─── Scan Types ───\n\nexport type StringContext =\n | 'jsx_text' | 'jsx_attribute'\n | 'template_text' | 'template_attribute'\n | 'variable_assignment' | 'function_argument'\n | 'object_value' | 'other'\n\nexport type FileCategory = 'page' | 'component' | 'layout' | 'other'\n\nexport interface ScanCandidate {\n file: string\n line: number\n column: number\n value: string\n context: StringContext\n surrounding: string\n /** Content confidence score (0-1). Higher = more likely user-visible content. */\n contentScore: number\n /** All locations where this value appears (populated when dedup is enabled). */\n occurrences: Array<{ file: string; line: number }>\n}\n\nexport interface DuplicateGroup {\n value: string\n count: number\n occurrences: Array<{ file: string; line: number }>\n}\n\nexport interface GraphNode {\n file: string\n category: FileCategory\n components?: string[]\n imports: string[]\n used_by: string[]\n strings: number\n}\n\nexport interface ProjectGraph {\n pages: GraphNode[]\n components: GraphNode[]\n layouts: GraphNode[]\n orphan_files: string[]\n stats: {\n total_files: number\n total_components: number\n total_pages: number\n total_strings_estimate: number\n }\n}\n\nexport interface ScanCandidatesResult {\n candidates: ScanCandidate[]\n duplicates: DuplicateGroup[]\n stats: {\n files_scanned: number\n raw_strings_found: number\n skipped: number\n low_confidence: number\n unique_candidates: number\n candidates_returned: number\n has_more: boolean\n skip_reasons: Record<string, number>\n }\n}\n\nexport interface ScanSummaryResult {\n total_files: number\n total_candidates_estimate: number\n by_directory: Record<string, { files: number; candidates: number }>\n top_repeated: Array<{ value: string; count: number }>\n sampling_note?: string\n file_types: Record<string, number>\n}\n\n// ─── Normalize Plan ───\n\nexport interface NormalizePlan {\n version: number\n status: 'pending' | 'approved' | 'rejected'\n created_at: string\n approved_at?: string\n agent: string\n scan_stats: {\n files_scanned: number\n raw_strings: number\n candidates_sent: number\n extracted: number\n skipped: number\n }\n models: NormalizePlanModel[]\n extractions: NormalizePlanExtraction[]\n patches: NormalizePlanPatch[]\n approved_models?: string[]\n}\n\nexport interface NormalizePlanModel {\n id: string\n kind: ModelKind\n domain: string\n i18n?: boolean\n fields: Record<string, FieldDef>\n}\n\nexport interface NormalizePlanExtraction {\n value: string\n file: string\n line: number\n model: string\n field: string\n locale?: string\n}\n\nexport interface NormalizePlanPatch {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\n// ─── Context ───\n\nexport interface ContextJson {\n version: string\n lastOperation: {\n tool: string\n model: string\n locale: string\n entries?: string[]\n timestamp: string\n source: ContextSource\n }\n stats: {\n models: number\n entries: number\n locales: string[]\n lastSync: string\n }\n}\n\n// ─── Git Transaction Types ───\n\n/** Standard name for the dedicated content branch */\nexport const CONTENTRAIN_BRANCH = 'contentrain' as const\n\n/** Result of selective file sync to developer's working tree */\nexport interface SyncResult {\n /** Files successfully synced to working tree */\n synced: string[]\n /** Files skipped due to uncommitted local changes */\n skipped: string[]\n /** Human-readable warning if files were skipped */\n warning?: string\n}\n\n/** Structured error for git operations with actionable guidance */\nexport interface ContentrainError {\n /** Machine-readable error code */\n code: string\n /** Human-readable error message */\n message: string\n /** Guidance for AI agents on how to handle this error */\n agent_hint: string\n /** Command or action the developer should take */\n developer_action: string\n}\n\n// ─── Validate: Pure Functions ───\n\n/**\n * Patterns for detecting a credential that has been pasted into content.\n *\n * Each one matches a credential-shaped *value*, not prose that mentions\n * credentials. That distinction is the whole design: the earlier set matched\n * bare prefixes, so `task_id` and `risk_level` tripped `sk_`, every page\n * documenting `Authorization: Bearer <token>` tripped `Bearer `, and\n * `postgres://localhost:5432/mydb` in a getting-started guide tripped the\n * connection-string rule. On this repository — which stores its own\n * documentation as content — that was 7 of 12 validation errors, and the real\n * findings were buried under them.\n *\n * A check that fires on the wrong thing is not a strict check; it is a check\n * people learn to scroll past.\n *\n * Every pattern here carries a provider-specific shape — a fixed prefix, a\n * credential-only URL form, a PEM header — and is trusted on its own. The one\n * generic rule, `api_key = …`, has no such shape and lives in\n * {@link API_KEY_ASSIGNMENT}, where what it captures is checked before it fires.\n */\nexport const SECRET_PATTERNS: ReadonlyArray<RegExp> = [\n // Stripe-style keys: the prefix must start a word and carry a long body,\n // so `task_id` and `risk_level` no longer match.\n /\\b[sp]k_(?:live|test)_[A-Za-z0-9]{6,}/,\n // GitHub tokens are a fixed prefix plus a long body.\n /\\bgh[pousr]_[A-Za-z0-9]{10,}/,\n // A bearer token, not the word. Placeholders — <token>, $TOKEN, {{token}},\n // YOUR_TOKEN — are how documentation writes it, and are not secrets.\n /\\bBearer\\s+(?![<${{]|YOUR[_-]|your[_-]|xxx|XXX|\\.{3})[A-Za-z0-9._~+/-]{20,}/,\n /\\baws_secret[_a-z]*\\s*[:=]\\s*['\"`]?[A-Za-z0-9/+=]{20,}/i,\n // AWS access key IDs have a fixed shape.\n /\\bAKIA[0-9A-Z]{16}\\b/,\n // A connection string only carries a secret when it carries credentials;\n // `postgres://localhost:5432/mydb` in a guide does not.\n /\\b(?:postgres(?:ql)?|mysql|mongodb)(?:\\+srv)?:\\/\\/[^\\s:@/]+:[^\\s@/]+@/,\n // A pasted private key is unambiguous.\n /-----BEGIN [A-Z ]*PRIVATE KEY-----/,\n]\n\n/** Validate a slug — returns error message or null if valid */\nexport function validateSlug(slug: string): string | null {\n if (!slug) return 'Slug is required'\n if (!SLUG_PATTERN.test(slug)) return `Invalid slug \"${slug}\": must be kebab-case alphanumeric (a-z, 0-9, hyphens)`\n if (slug.startsWith('.') || slug.includes('..')) return `Invalid slug \"${slug}\": path traversal not allowed`\n return null\n}\n\n/** Validate an entry ID — returns error message or null if valid */\nexport function validateEntryId(id: string): string | null {\n if (!ENTRY_ID_PATTERN.test(id)) return `Invalid entry ID \"${id}\": must be 1-40 alphanumeric characters (hyphens and underscores allowed)`\n return null\n}\n\n/** Validate a locale code against pattern and config — returns error message or null if valid */\nexport function validateLocale(locale: string, config: ContentrainConfig): string | null {\n if (!LOCALE_PATTERN.test(locale)) return `Invalid locale \"${locale}\": must be ISO 639-1 format (e.g. \"en\", \"tr\", \"pt-BR\")`\n if (!config.locales.supported.includes(locale)) {\n return `Locale \"${locale}\" is not in supported locales [${config.locales.supported.join(', ')}]. Update config first.`\n }\n return null\n}\n\n/**\n * An API key being assigned a value: `api_key = <tail>`.\n *\n * Generic by nature — there is no provider prefix, so the word plus a separator\n * is the only structural signal — and therefore the one rule that also has to\n * look at what it captured before it fires; see {@link looksLikeCredential}.\n * Without that, `api_key = your_project_api_key_here` in a setup guide was a\n * blocking error, and once `contentrain validate` exited non-zero, a permanent\n * CI failure for any project whose content discusses API keys — which, for a\n * CMS, is ordinary content. Placeholders documentation uses (`<key>`, `$KEY`,\n * `{{key}}`, `YOUR_KEY`, `xxx`, `…`) are excluded up front, as for `Bearer`.\n */\nconst API_KEY_ASSIGNMENT = /\\bapi[_-]?key\\s*[:=]\\s*['\"`]?(?![<${{]|YOUR[_-]|your[_-]|xxx|XXX|\\.{3})([A-Za-z0-9_-]{16,})/gi\n\n/**\n * Does a value assigned to an API-key setting look like a credential rather\n * than prose about one?\n *\n * Two shapes are documentation, not secrets: a tail with no digit at all\n * (`your_project_api_key_here`, `xxxxxxxxxxxxxxxxxxxx`), and a tail whose\n * `_`/`-` separated tokens are each purely letters or purely digits\n * (`my_api_key_2024`, `REPLACE-WITH-KEY-1`) — words with a version number. An\n * issued key mixes letters and digits inside one token (`8f14e45fceea167a`,\n * `k1p8-q7x2m9v4z3w6`), which no sentence does by accident.\n */\nexport function looksLikeCredential(tail: string): boolean {\n if (!/\\d/.test(tail)) return false\n const tokens = tail.split(/[_-]+/).filter(Boolean)\n if (tokens.length > 1 && tokens.every(t => /^[A-Za-z]+$/.test(t) || /^\\d+$/.test(t))) return false\n return true\n}\n\n/** Detect potential secrets in a value — returns validation errors if found */\nexport function detectSecrets(value: unknown): ValidationError[] {\n if (typeof value !== 'string') return []\n const found = SECRET_PATTERNS.some(p => p.test(value))\n || [...value.matchAll(API_KEY_ASSIGNMENT)].some(m => looksLikeCredential(m[1]!))\n if (found) {\n return [{ severity: 'error', message: 'Potential secret detected in value' }]\n }\n return []\n}\n\nfunction fieldTypeMatches(value: unknown, fieldDef: FieldDef): boolean {\n if (value === null || value === undefined) return true\n\n switch (fieldDef.type) {\n case 'string':\n case 'text':\n case 'email':\n case 'url':\n case 'slug':\n case 'color':\n case 'phone':\n case 'code':\n case 'icon':\n case 'markdown':\n case 'richtext':\n case 'date':\n case 'datetime':\n case 'image':\n case 'video':\n case 'file':\n case 'select':\n return typeof value === 'string'\n\n case 'number':\n case 'integer':\n case 'decimal':\n case 'percent':\n case 'rating':\n return typeof value === 'number'\n\n case 'boolean':\n return typeof value === 'boolean'\n\n case 'relation':\n return typeof value === 'string'\n\n case 'relations':\n case 'array':\n return Array.isArray(value)\n\n case 'object':\n return typeof value === 'object' && !Array.isArray(value)\n\n default:\n return true\n }\n}\n\nfunction checkMinMax(value: unknown, fieldDef: FieldDef): string | null {\n if (fieldDef.min !== undefined) {\n if (typeof value === 'number' && value < fieldDef.min) {\n return `Value ${value} is below minimum ${fieldDef.min}`\n }\n if (typeof value === 'string' && value.length < fieldDef.min) {\n return `String length ${value.length} is below minimum ${fieldDef.min}`\n }\n if (Array.isArray(value) && value.length < fieldDef.min) {\n return `Array length ${value.length} is below minimum ${fieldDef.min}`\n }\n }\n if (fieldDef.max !== undefined) {\n if (typeof value === 'number' && value > fieldDef.max) {\n return `Value ${value} is above maximum ${fieldDef.max}`\n }\n if (typeof value === 'string' && value.length > fieldDef.max) {\n return `String length ${value.length} is above maximum ${fieldDef.max}`\n }\n if (Array.isArray(value) && value.length > fieldDef.max) {\n return `Array length ${value.length} is above maximum ${fieldDef.max}`\n }\n }\n return null\n}\n\n// ─── Semantic type validation ───\n\n/** Hex (#rgb/#rrggbb/#rrggbbaa), rgb()/rgba(), hsl()/hsla(), or a bare CSS keyword. */\nconst COLOR_PATTERN = /^(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgb|hsl)a?\\([^)]*\\)|[a-zA-Z]+)$/\n/** Deliberately loose: digits, spaces, and the usual separators, 4-20 digits total. */\nconst PHONE_PATTERN = /^\\+?[\\d\\s().-]{4,}$/\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\nfunction isParsableDate(value: string): boolean {\n return !Number.isNaN(new Date(value).getTime())\n}\n\n/**\n * Rules a field type implies beyond its runtime `typeof`.\n *\n * Severity follows one rule: a check that is *definitional* is an error — a `slug`\n * that does not match `SLUG_PATTERN` is not a slug. A check that is a *heuristic* is\n * a warning, because the pattern is an approximation and a legitimate value may sit\n * outside it (international phone formats, CSS colour keywords).\n *\n * `rating` is deliberately absent: its scale is never declared, so any range here\n * would be invented. Declare `min`/`max` on the field instead.\n *\n * Returns `[]` for types whose only contract is their `typeof` (`string`, `text`,\n * `code`, `icon`, `markdown`, `richtext`, `number`, `decimal`, `boolean`, media).\n */\nexport function validateSemanticType(value: unknown, type: FieldType): ValidationError[] {\n if (typeof value === 'number') {\n if (type === 'integer' && !Number.isInteger(value)) {\n return [{ severity: 'error', message: `Value ${value} is not an integer` }]\n }\n if (type === 'percent' && (value < 0 || value > 100)) {\n return [{ severity: 'error', message: `Percent ${value} is outside 0-100` }]\n }\n return []\n }\n\n if (typeof value !== 'string' || value === '') return []\n\n switch (type) {\n case 'slug': {\n const err = validateSlug(value)\n return err ? [{ severity: 'error', message: err }] : []\n }\n case 'date':\n return DATE_PATTERN.test(value) && isParsableDate(value)\n ? []\n : [{ severity: 'error', message: `Invalid date \"${value}\": must be YYYY-MM-DD` }]\n case 'datetime':\n return isParsableDate(value)\n ? []\n : [{ severity: 'error', message: `Invalid datetime \"${value}\": must be an ISO 8601 date-time` }]\n case 'email':\n return EMAIL_PATTERN.test(value)\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid email` }]\n case 'url':\n return /^https?:\\/\\/.+/.test(value) || value.startsWith('/')\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid URL` }]\n case 'color':\n return COLOR_PATTERN.test(value)\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid color (expected hex, rgb(), hsl() or a keyword)` }]\n case 'phone':\n return PHONE_PATTERN.test(value) && (value.match(/\\d/g)?.length ?? 0) >= 4\n ? []\n : [{ severity: 'warning', message: `\"${value}\" may not be a valid phone number` }]\n default:\n return []\n }\n}\n\n/** Media field types — the ones whose value is a path/URL to an asset. */\nconst MEDIA_TYPES = new Set<FieldType>(['image', 'video', 'file'])\n\nexport function isMediaType(type: FieldType): boolean {\n return MEDIA_TYPES.has(type)\n}\n\n// ─── Field presentation ───\n\n/**\n * Resolve a field's label for a locale.\n *\n * Falls back in the order a reader would expect: the requested locale, the\n * default locale, any locale present, then the field name. A field with no\n * label resolves to its own name, which is what an editor shows today.\n */\nexport function resolveFieldLabel(\n fieldName: string,\n field: Pick<FieldDef, 'label'>,\n locale?: string,\n defaultLocale?: string,\n): string {\n const { label } = field\n if (typeof label === 'string') return label\n if (label && typeof label === 'object') {\n const byLocale = (locale && label[locale])\n ?? (defaultLocale && label[defaultLocale])\n ?? Object.values(label)[0]\n if (byLocale) return byLocale\n }\n return fieldName\n}\n\n/**\n * Field names in display order.\n *\n * Exported so every consumer orders identically instead of each reimplementing\n * the fallback. Ascending `order`; fields without one come last, alphabetically\n * among themselves, which is the behaviour a model that has no `order` already\n * has.\n */\nexport function orderedFieldNames(fields: Record<string, FieldDef> | undefined): string[] {\n if (!fields) return []\n return Object.keys(fields).toSorted((a, b) => {\n const oa = fields[a]?.order\n const ob = fields[b]?.order\n if (oa !== undefined && ob !== undefined && oa !== ob) return oa - ob\n if (oa !== undefined && ob === undefined) return -1\n if (oa === undefined && ob !== undefined) return 1\n return a.localeCompare(b, 'en')\n })\n}\n\n// ─── Title field ───\n\n/**\n * The reserved `title_field` value for dictionary models. A dictionary has no\n * fields — it is a flat key→string map — so there is nothing to point at. Its key\n * *is* the title, and this names that fact rather than leaving the property blank.\n * Only dictionaries may use it; on any other kind `title_field` must name a real field.\n */\nexport const DICTIONARY_TITLE_FIELD = 'key'\n\n/**\n * Field types whose value can stand in as a human-readable title.\n *\n * Narrower than \"types that hold a string\": `color`, `phone`, `icon`, `select`,\n * `date`, `datetime`, `relation` and the media types all store strings, but a row\n * labelled `#ff0000` or `f3a81c09d24e` is exactly the failure this contract exists\n * to prevent.\n */\nexport const TITLE_FIELD_TYPES = [\n 'string', 'text', 'slug', 'email', 'url', 'code', 'markdown', 'richtext',\n] as const satisfies readonly FieldType[]\n\nconst TITLE_FIELD_TYPE_SET = new Set<FieldType>(TITLE_FIELD_TYPES)\n\n/** Whether a field of this type may be named as a model's `title_field`. */\nexport function isTitleFieldType(type: FieldType): boolean {\n return TITLE_FIELD_TYPE_SET.has(type)\n}\n\n/** Extensions we can name a MIME type for. Deliberately small — this is a sniff. */\nconst EXTENSION_MIME: Record<string, string> = {\n jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',\n webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml', ico: 'image/x-icon',\n mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',\n pdf: 'application/pdf', zip: 'application/zip', json: 'application/json',\n csv: 'text/csv', txt: 'text/plain',\n}\n\n/**\n * Check a media value's file extension against an `accept` list.\n *\n * This is an approximation and says so: the value is a path or URL, so the real\n * MIME type is only knowable where the bytes are — the provider, at ingest\n * (`provider.ts:MediaProvider.ingest` owns that policy). An extension can lie, and\n * an unknown extension is not evidence of a violation. Hence: warning, never error,\n * and silence when we cannot tell.\n *\n * `accept` follows the HTML input syntax: `image/jpeg`, `image/*`, `.jpg`, comma-separated.\n */\nexport function validateAccept(value: string, accept: string): ValidationError[] {\n const path = value.split('?')[0]?.split('#')[0] ?? value\n const ext = path.split('.').pop()?.toLowerCase()\n if (!ext || ext === path.toLowerCase()) return []\n\n const mime = EXTENSION_MIME[ext]\n const patterns = accept.split(',').map(a => a.trim().toLowerCase()).filter(Boolean)\n if (patterns.length === 0) return []\n\n const matches = patterns.some((p) => {\n if (p.startsWith('.')) return p.slice(1) === ext\n if (p.endsWith('/*')) return mime ? mime.startsWith(p.slice(0, -1)) : false\n return mime === p\n })\n if (matches) return []\n\n // An extension we have no MIME for is not evidence of a violation.\n if (!mime && !patterns.some(p => p.startsWith('.'))) return []\n\n return [{\n severity: 'warning',\n message: `File extension \".${ext}\" does not match accept \"${accept}\" (extension check — the provider enforces the real MIME type at ingest)`,\n }]\n}\n\n/**\n * Validate a field value against its FieldDef schema.\n * Checks: required, type match, semantic type rules, min/max, pattern, select\n * options, and `accept` on media fields (by extension).\n * Does NOT check: secrets (use detectSecrets), uniqueness (stateful), relations (I/O),\n * or `maxSize` — byte length is not knowable from a stored path.\n */\nexport function validateFieldValue(value: unknown, fieldDef: FieldDef): ValidationError[] {\n const issues: ValidationError[] = []\n\n if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({ severity: 'error', message: 'Required field is missing or empty' })\n return issues\n }\n\n if (value === undefined || value === null) return issues\n\n if (!fieldTypeMatches(value, fieldDef)) {\n issues.push({ severity: 'error', message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}` })\n return issues\n }\n\n issues.push(...validateSemanticType(value, fieldDef.type))\n\n if (fieldDef.accept && isMediaType(fieldDef.type) && typeof value === 'string') {\n issues.push(...validateAccept(value, fieldDef.accept))\n }\n\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({ severity: 'error', message: minMaxErr })\n }\n\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({ severity: 'error', message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/` })\n }\n } catch {\n issues.push({ severity: 'warning', message: `Invalid pattern regex: ${fieldDef.pattern}` })\n }\n }\n\n if (fieldDef.type === 'select' && fieldDef.options && typeof value === 'string') {\n if (!fieldDef.options.includes(value)) {\n issues.push({ severity: 'error', message: `Value \"${value}\" is not in allowed options: [${fieldDef.options.join(', ')}]` })\n }\n }\n\n return issues\n}\n\n// ─── Serialize: Pure Functions ───\n\n/** Recursively sort object keys — respects optional fieldOrder for top-level keys */\nexport function sortKeys(obj: unknown, fieldOrder?: readonly string[]): unknown {\n if (obj === null || obj === undefined) return undefined\n if (Array.isArray(obj)) return obj.map(item => sortKeys(item, fieldOrder))\n if (typeof obj !== 'object') return obj\n\n const record = obj as Record<string, unknown>\n const sorted: Record<string, unknown> = {}\n\n const keys = fieldOrder\n ? [...new Set([...fieldOrder, ...Object.keys(record).toSorted()])]\n : Object.keys(record).toSorted()\n\n for (const key of keys) {\n if (!(key in record)) continue\n const val = record[key]\n if (val === null || val === undefined) continue\n sorted[key] = sortKeys(val)\n }\n\n return sorted\n}\n\n/** Canonical JSON serialization — deterministic output for stable git diffs */\nexport function canonicalStringify(data: unknown, fieldOrder?: readonly string[]): string {\n const sorted = sortKeys(data, fieldOrder)\n return `${JSON.stringify(sorted, null, 2)}\\n`\n}\n\n/** Generate a 12-character hex entry ID */\nexport function generateEntryId(): string {\n const hex = '0123456789abcdef'\n let id = ''\n for (let i = 0; i < 12; i++) {\n id += hex[Math.floor(Math.random() * 16)]\n }\n return id\n}\n\n// ─── Reconcile: conflict id derivation ───\n\n/**\n * FNV-1a 64-bit over the input's UTF-8 code points, returned as 16 hex\n * chars. Deliberately dependency-free (no `node:crypto` — this package has\n * zero runtime imports and runs anywhere). Not cryptographic; the use case\n * is change detection, not tamper resistance.\n */\nexport function stableHash(input: string): string {\n const PRIME = 0x100000001B3n\n const MASK = 0xFFFFFFFFFFFFFFFFn\n let hash = 0xCBF29CE484222325n\n const bytes = new TextEncoder().encode(input)\n for (const byte of bytes) {\n hash ^= BigInt(byte)\n hash = (hash * PRIME) & MASK\n }\n return hash.toString(16).padStart(16, '0')\n}\n\n/**\n * Identity of a reconcile conflict: position AND values. Hashing the three\n * values into the id makes every resolution a compare-and-set — if any side\n * changes between the dry-run that produced the conflict and the apply that\n * carries the decision, the id no longer matches, the stale resolution is\n * dropped, and the conflict is re-reported with a fresh id. Values are\n * normalized through `sortKeys` first so semantically equal objects hash\n * equal regardless of key order; the positional array keeps absent and\n * null distinguishable per slot.\n */\nexport function conflictId(conflict: {\n path: string\n key?: string\n field?: string\n locale?: string\n base?: unknown\n ours?: unknown\n theirs?: unknown\n}): string {\n return stableHash(JSON.stringify([\n conflict.path,\n conflict.key ?? null,\n conflict.field ?? null,\n conflict.locale ?? null,\n sortKeys(conflict.base) ?? null,\n sortKeys(conflict.ours) ?? null,\n sortKeys(conflict.theirs) ?? null,\n ]))\n}\n\n// ─── Markdown Frontmatter ───\n\nfunction parseYamlValue(raw: string): unknown {\n if (raw === 'true') return true\n if (raw === 'false') return false\n if (raw === 'null') return null\n if (/^-?\\d+$/.test(raw)) return parseInt(raw, 10)\n if (/^-?\\d+\\.\\d+$/.test(raw)) return parseFloat(raw)\n if ((raw.startsWith('\"') && raw.endsWith('\"')) || (raw.startsWith(\"'\") && raw.endsWith(\"'\"))) {\n return raw.slice(1, -1)\n }\n return raw\n}\n\nfunction yamlScalar(value: unknown): string {\n const str = String(value)\n if (str.includes(':') || str.includes('#') || str.includes('{') || str.includes('}')\n || str.includes('[') || str.includes(']') || str.includes('*') || str.includes('&')\n || str.includes('!') || str.includes('|') || str.includes('>') || str.includes(\"'\")\n || str.includes('\"') || str.includes('%') || str.includes('@') || str.includes('`')\n || str.startsWith('-') || str.startsWith('?')\n || str === 'true' || str === 'false' || str === 'null' || str === 'yes' || str === 'no'\n || str === '') {\n return `\"${str.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`\n }\n return str\n}\n\nfunction serializeYamlValue(value: unknown, indent: number): string[] {\n const pad = ' '.repeat(indent)\n if (value === null || value === undefined) return []\n if (typeof value === 'object' && !Array.isArray(value)) {\n const lines: string[] = []\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === null || v === undefined) continue\n if (typeof v === 'object' && !Array.isArray(v)) {\n lines.push(`${pad}${k}:`)\n lines.push(...serializeYamlValue(v, indent + 1))\n } else if (Array.isArray(v)) {\n lines.push(`${pad}${k}:`)\n for (const item of v) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, indent + 2)\n if (nested.length > 0) {\n lines.push(`${pad} - ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(`${pad} - ${yamlScalar(item)}`)\n }\n }\n } else {\n lines.push(`${pad}${k}: ${yamlScalar(v)}`)\n }\n }\n return lines\n }\n if (Array.isArray(value)) {\n const lines: string[] = []\n for (const item of value) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, indent + 1)\n if (nested.length > 0) {\n lines.push(`${pad}- ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(`${pad}- ${yamlScalar(item)}`)\n }\n }\n return lines\n }\n return [`${pad}${yamlScalar(value)}`]\n}\n\nfunction serializeYamlFields(data: Record<string, unknown>): string[] {\n const lines: string[] = []\n for (const [key, value] of Object.entries(data)) {\n if (key === 'body') continue\n if (value === null || value === undefined) continue\n if (typeof value === 'object' && !Array.isArray(value)) {\n lines.push(`${key}:`)\n lines.push(...serializeYamlValue(value, 1))\n } else if (Array.isArray(value)) {\n lines.push(`${key}:`)\n for (const item of value) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, 2)\n if (nested.length > 0) {\n lines.push(` - ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(` - ${yamlScalar(item)}`)\n }\n }\n } else {\n lines.push(`${key}: ${yamlScalar(value)}`)\n }\n }\n return lines\n}\n\n/** Parse markdown frontmatter — extracts YAML frontmatter and body from a markdown string */\nexport function parseMarkdownFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {\n const normalized = content.replace(/\\r\\n/g, '\\n')\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/)\n if (!match) return { frontmatter: {}, body: normalized }\n\n const frontmatterStr = match[1]!\n const body = match[2]!.trim()\n const frontmatter: Record<string, unknown> = {}\n\n let currentKey: string | null = null\n let currentArray: string[] | null = null\n\n for (const line of frontmatterStr.split('\\n')) {\n if (/^\\s+-\\s+/.test(line) && currentKey) {\n const value = line.replace(/^\\s+-\\s+/, '').trim()\n if (!currentArray) currentArray = []\n currentArray.push(value)\n continue\n }\n\n if (currentKey && currentArray) {\n frontmatter[currentKey] = currentArray\n currentKey = null\n currentArray = null\n }\n\n const kvMatch = line.match(/^([\\w][\\w.-]*)\\s*:\\s*(.*)$/)\n if (!kvMatch) continue\n\n const key = kvMatch[1]!\n const rawValue = kvMatch[2]!.trim()\n\n if (rawValue === '') {\n currentKey = key\n currentArray = []\n continue\n }\n\n if (rawValue.startsWith('[') && rawValue.endsWith(']')) {\n const items = rawValue.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean)\n frontmatter[key] = items\n continue\n }\n\n frontmatter[key] = parseYamlValue(rawValue)\n }\n\n if (currentKey && currentArray) {\n frontmatter[currentKey] = currentArray\n }\n\n return { frontmatter, body }\n}\n\n/** Serialize data + body into markdown frontmatter format */\nexport function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string {\n const trimmedBody = body.trimStart()\n if (trimmedBody.startsWith('---')) {\n const endIdx = trimmedBody.indexOf('---', 3)\n if (endIdx > 0) {\n const bodyFmContent = trimmedBody.slice(3, endIdx).trim()\n const afterFm = trimmedBody.slice(endIdx + 3)\n\n const lines: string[] = ['---']\n lines.push(...serializeYamlFields(data))\n if (bodyFmContent) {\n const modelKeys = new Set(Object.keys(data))\n for (const line of bodyFmContent.split('\\n')) {\n if (line.length > 0 && line[0] !== ' ' && line[0] !== '-') {\n const colonIdx = line.indexOf(':')\n const lineKey = colonIdx > 0 ? line.slice(0, colonIdx).trim() : ''\n if (lineKey && modelKeys.has(lineKey)) continue\n }\n lines.push(line)\n }\n }\n lines.push('---')\n if (afterFm.trim()) {\n lines.push(afterFm.trimStart())\n }\n lines.push('')\n return lines.join('\\n')\n }\n }\n\n const lines: string[] = ['---']\n lines.push(...serializeYamlFields(data))\n lines.push('---')\n lines.push('')\n if (body) {\n lines.push(body)\n lines.push('')\n }\n return lines.join('\\n')\n}\n\n// ─── Migration contracts ───\n\nexport * from './migration.js'\n\n// ─── Repository provider contracts ───\n//\n// Provider-agnostic engine contracts used by @contentrain/mcp. Exposed from\n// @contentrain/types so third-party tools can implement a custom\n// RepoProvider without taking a dependency on @contentrain/mcp.\nexport type {\n ApplyPlanInput,\n BaseAdvance,\n Branch,\n Commit,\n CommitAuthor,\n ConflictCode,\n ConflictItem,\n ConflictResolution,\n FileChange,\n FileDiff,\n MediaAsset,\n MediaIngestInput,\n MediaListOptions,\n MediaListResult,\n MediaProvider,\n MediaUpdateInput,\n MergeResult,\n ProviderCapabilities,\n RemotePush,\n RepoProvider,\n RepoReader,\n RepoWriter,\n WriteReadiness,\n} from './provider.js'\nexport { LOCAL_CAPABILITIES } from './provider.js'\n"],"mappings":";;;;;;AAiBA,MAAa,6BAA6B;;AAc1C,MAAa,uBAAuB;CAClC;CACA;CACA;CACA;CACD;AA0OD,MAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;AAmED,MAAa,mBAAmB;;;;;;;;AAoIhC,MAAa,kBAAkB;;;;;;;;;;;;AAa/B,MAAa,qBAAqB;AAClC,MAAa,sBAAsB;;;;;;;;;;;AAYnC,MAAa,iBAAiB;AAC9B,MAAa,kBAAkB;;;;;AAM/B,MAAa,kBAAkB;;;;;;;;;AAU/B,MAAa,mBAAmB;;;;;;;AA8GhC,MAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AA2FD,MAAa,yBAAyB;;;;AC3qBtC,MAAa,qBAA2C;CACtD,eAAe;CACf,YAAY;CACZ,aAAa;CACb,YAAY;CACZ,kBAAkB;CAClB,qBAAqB;CACrB,SAAS;CACT,aAAa;CACd;;;;AC4LD,MAAa,kBAAkB;;;;;;;;;AAU/B,MAAa,gBAAgB;CAC3B,QAAQ;CACR,SAAS;CACT,YAAY;CACZ,OAAO;CACP,SAAS;EACP,WAAW;EACX,YAAY;EACZ,UAAU;EACV,YAAY;EAEZ,UAAU;EACX;CACD,MAAM;EACJ,WAAW;EACX,YAAY;EACZ,UAAU;EACV,YAAY;EACb;CACF;;AAKD,MAAa,eAAe;;AAE5B,MAAa,mBAAmB;;AAEhC,MAAa,iBAAiB;;AAK9B,MAAa,iBAAiB;CAC5B,QAAQ;CACR,UAAU;CACV,iBAAiB;CACjB,UAAU;CACV,cAAc;CACd,UAAU;CACX;;;;;;;;;;;;AAaD,MAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;AA2JD,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;AA8ClC,MAAa,kBAAyC;CAGpD;CAEA;CAGA;CACA;CAEA;CAGA;CAEA;CACD;;AAGD,SAAgB,aAAa,MAA6B;AACxD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,CAAC,aAAa,KAAK,KAAK,CAAE,QAAO,iBAAiB,KAAK;AAC3D,KAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS,KAAK,CAAE,QAAO,iBAAiB,KAAK;AAC9E,QAAO;;;AAIT,SAAgB,gBAAgB,IAA2B;AACzD,KAAI,CAAC,iBAAiB,KAAK,GAAG,CAAE,QAAO,qBAAqB,GAAG;AAC/D,QAAO;;;AAIT,SAAgB,eAAe,QAAgB,QAA0C;AACvF,KAAI,CAAC,eAAe,KAAK,OAAO,CAAE,QAAO,mBAAmB,OAAO;AACnE,KAAI,CAAC,OAAO,QAAQ,UAAU,SAAS,OAAO,CAC5C,QAAO,WAAW,OAAO,iCAAiC,OAAO,QAAQ,UAAU,KAAK,KAAK,CAAC;AAEhG,QAAO;;;;;;;;;;;;;;AAeT,MAAM,qBAAqB;;;;;;;;;;;;AAa3B,SAAgB,oBAAoB,MAAuB;AACzD,KAAI,CAAC,KAAK,KAAK,KAAK,CAAE,QAAO;CAC7B,MAAM,SAAS,KAAK,MAAM,QAAQ,CAAC,OAAO,QAAQ;AAClD,KAAI,OAAO,SAAS,KAAK,OAAO,OAAM,MAAK,cAAc,KAAK,EAAE,IAAI,QAAQ,KAAK,EAAE,CAAC,CAAE,QAAO;AAC7F,QAAO;;;AAIT,SAAgB,cAAc,OAAmC;AAC/D,KAAI,OAAO,UAAU,SAAU,QAAO,EAAE;AAGxC,KAFc,gBAAgB,MAAK,MAAK,EAAE,KAAK,MAAM,CAAC,IACjD,CAAC,GAAG,MAAM,SAAS,mBAAmB,CAAC,CAAC,MAAK,MAAK,oBAAoB,EAAE,GAAI,CAAC,CAEhF,QAAO,CAAC;EAAE,UAAU;EAAS,SAAS;EAAsC,CAAC;AAE/E,QAAO,EAAE;;AAGX,SAAS,iBAAiB,OAAgB,UAA6B;AACrE,KAAI,UAAU,QAAQ,UAAU,KAAA,EAAW,QAAO;AAElD,SAAQ,SAAS,MAAjB;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO,OAAO,UAAU;EAE1B,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO,OAAO,UAAU;EAE1B,KAAK,UACH,QAAO,OAAO,UAAU;EAE1B,KAAK,WACH,QAAO,OAAO,UAAU;EAE1B,KAAK;EACL,KAAK,QACH,QAAO,MAAM,QAAQ,MAAM;EAE7B,KAAK,SACH,QAAO,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM;EAE3D,QACE,QAAO;;;AAIb,SAAS,YAAY,OAAgB,UAAmC;AACtE,KAAI,SAAS,QAAQ,KAAA,GAAW;AAC9B,MAAI,OAAO,UAAU,YAAY,QAAQ,SAAS,IAChD,QAAO,SAAS,MAAM,oBAAoB,SAAS;AAErD,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,SAAS,IACvD,QAAO,iBAAiB,MAAM,OAAO,oBAAoB,SAAS;AAEpE,MAAI,MAAM,QAAQ,MAAM,IAAI,MAAM,SAAS,SAAS,IAClD,QAAO,gBAAgB,MAAM,OAAO,oBAAoB,SAAS;;AAGrE,KAAI,SAAS,QAAQ,KAAA,GAAW;AAC9B,MAAI,OAAO,UAAU,YAAY,QAAQ,SAAS,IAChD,QAAO,SAAS,MAAM,oBAAoB,SAAS;AAErD,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,SAAS,IACvD,QAAO,iBAAiB,MAAM,OAAO,oBAAoB,SAAS;AAEpE,MAAI,MAAM,QAAQ,MAAM,IAAI,MAAM,SAAS,SAAS,IAClD,QAAO,gBAAgB,MAAM,OAAO,oBAAoB,SAAS;;AAGrE,QAAO;;;AAMT,MAAM,gBAAgB;;AAEtB,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AACtB,MAAM,eAAe;AAErB,SAAS,eAAe,OAAwB;AAC9C,QAAO,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;AAiBjD,SAAgB,qBAAqB,OAAgB,MAAoC;AACvF,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,SAAS,aAAa,CAAC,OAAO,UAAU,MAAM,CAChD,QAAO,CAAC;GAAE,UAAU;GAAS,SAAS,SAAS,MAAM;GAAqB,CAAC;AAE7E,MAAI,SAAS,cAAc,QAAQ,KAAK,QAAQ,KAC9C,QAAO,CAAC;GAAE,UAAU;GAAS,SAAS,WAAW,MAAM;GAAoB,CAAC;AAE9E,SAAO,EAAE;;AAGX,KAAI,OAAO,UAAU,YAAY,UAAU,GAAI,QAAO,EAAE;AAExD,SAAQ,MAAR;EACE,KAAK,QAAQ;GACX,MAAM,MAAM,aAAa,MAAM;AAC/B,UAAO,MAAM,CAAC;IAAE,UAAU;IAAS,SAAS;IAAK,CAAC,GAAG,EAAE;;EAEzD,KAAK,OACH,QAAO,aAAa,KAAK,MAAM,IAAI,eAAe,MAAM,GACpD,EAAE,GACF,CAAC;GAAE,UAAU;GAAS,SAAS,iBAAiB,MAAM;GAAwB,CAAC;EACrF,KAAK,WACH,QAAO,eAAe,MAAM,GACxB,EAAE,GACF,CAAC;GAAE,UAAU;GAAS,SAAS,qBAAqB,MAAM;GAAmC,CAAC;EACpG,KAAK,QACH,QAAO,cAAc,KAAK,MAAM,GAC5B,EAAE,GACF,CAAC;GAAE,UAAU;GAAW,SAAS,IAAI,MAAM;GAA6B,CAAC;EAC/E,KAAK,MACH,QAAO,iBAAiB,KAAK,MAAM,IAAI,MAAM,WAAW,IAAI,GACxD,EAAE,GACF,CAAC;GAAE,UAAU;GAAW,SAAS,IAAI,MAAM;GAA2B,CAAC;EAC7E,KAAK,QACH,QAAO,cAAc,KAAK,MAAM,GAC5B,EAAE,GACF,CAAC;GAAE,UAAU;GAAW,SAAS,IAAI,MAAM;GAAuE,CAAC;EACzH,KAAK,QACH,QAAO,cAAc,KAAK,MAAM,KAAK,MAAM,MAAM,MAAM,EAAE,UAAU,MAAM,IACrE,EAAE,GACF,CAAC;GAAE,UAAU;GAAW,SAAS,IAAI,MAAM;GAAoC,CAAC;EACtF,QACE,QAAO,EAAE;;;;AAKf,MAAM,cAAc,IAAI,IAAe;CAAC;CAAS;CAAS;CAAO,CAAC;AAElE,SAAgB,YAAY,MAA0B;AACpD,QAAO,YAAY,IAAI,KAAK;;;;;;;;;AAY9B,SAAgB,kBACd,WACA,OACA,QACA,eACQ;CACR,MAAM,EAAE,UAAU;AAClB,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,YAAY,UAAU,MAAM,aAC5B,iBAAiB,MAAM,mBACxB,OAAO,OAAO,MAAM,CAAC;AAC1B,MAAI,SAAU,QAAO;;AAEvB,QAAO;;;;;;;;;;AAWT,SAAgB,kBAAkB,QAAwD;AACxF,KAAI,CAAC,OAAQ,QAAO,EAAE;AACtB,QAAO,OAAO,KAAK,OAAO,CAAC,UAAU,GAAG,MAAM;EAC5C,MAAM,KAAK,OAAO,IAAI;EACtB,MAAM,KAAK,OAAO,IAAI;AACtB,MAAI,OAAO,KAAA,KAAa,OAAO,KAAA,KAAa,OAAO,GAAI,QAAO,KAAK;AACnE,MAAI,OAAO,KAAA,KAAa,OAAO,KAAA,EAAW,QAAO;AACjD,MAAI,OAAO,KAAA,KAAa,OAAO,KAAA,EAAW,QAAO;AACjD,SAAO,EAAE,cAAc,GAAG,KAAK;GAC/B;;;;;;;;AAWJ,MAAa,yBAAyB;;;;;;;;;AAUtC,MAAa,oBAAoB;CAC/B;CAAU;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAAY;CAC/D;AAED,MAAM,uBAAuB,IAAI,IAAe,kBAAkB;;AAGlE,SAAgB,iBAAiB,MAA0B;AACzD,QAAO,qBAAqB,IAAI,KAAK;;;AAIvC,MAAM,iBAAyC;CAC7C,KAAK;CAAc,MAAM;CAAc,KAAK;CAAa,KAAK;CAC9D,MAAM;CAAc,MAAM;CAAc,KAAK;CAAiB,KAAK;CACnE,KAAK;CAAa,MAAM;CAAc,KAAK;CAC3C,KAAK;CAAmB,KAAK;CAAmB,MAAM;CACtD,KAAK;CAAY,KAAK;CACvB;;;;;;;;;;;;AAaD,SAAgB,eAAe,OAAe,QAAmC;CAC/E,MAAM,OAAO,MAAM,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM;CACnD,MAAM,MAAM,KAAK,MAAM,IAAI,CAAC,KAAK,EAAE,aAAa;AAChD,KAAI,CAAC,OAAO,QAAQ,KAAK,aAAa,CAAE,QAAO,EAAE;CAEjD,MAAM,OAAO,eAAe;CAC5B,MAAM,WAAW,OAAO,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,QAAQ;AACnF,KAAI,SAAS,WAAW,EAAG,QAAO,EAAE;AAOpC,KALgB,SAAS,MAAM,MAAM;AACnC,MAAI,EAAE,WAAW,IAAI,CAAE,QAAO,EAAE,MAAM,EAAE,KAAK;AAC7C,MAAI,EAAE,SAAS,KAAK,CAAE,QAAO,OAAO,KAAK,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,GAAG;AACtE,SAAO,SAAS;GAChB,CACW,QAAO,EAAE;AAGtB,KAAI,CAAC,QAAQ,CAAC,SAAS,MAAK,MAAK,EAAE,WAAW,IAAI,CAAC,CAAE,QAAO,EAAE;AAE9D,QAAO,CAAC;EACN,UAAU;EACV,SAAS,oBAAoB,IAAI,2BAA2B,OAAO;EACpE,CAAC;;;;;;;;;AAUJ,SAAgB,mBAAmB,OAAgB,UAAuC;CACxF,MAAM,SAA4B,EAAE;AAEpC,KAAI,SAAS,aAAa,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,KAAK;AAChF,SAAO,KAAK;GAAE,UAAU;GAAS,SAAS;GAAsC,CAAC;AACjF,SAAO;;AAGT,KAAI,UAAU,KAAA,KAAa,UAAU,KAAM,QAAO;AAElD,KAAI,CAAC,iBAAiB,OAAO,SAAS,EAAE;AACtC,SAAO,KAAK;GAAE,UAAU;GAAS,SAAS,2BAA2B,SAAS,KAAK,QAAQ,OAAO;GAAS,CAAC;AAC5G,SAAO;;AAGT,QAAO,KAAK,GAAG,qBAAqB,OAAO,SAAS,KAAK,CAAC;AAE1D,KAAI,SAAS,UAAU,YAAY,SAAS,KAAK,IAAI,OAAO,UAAU,SACpE,QAAO,KAAK,GAAG,eAAe,OAAO,SAAS,OAAO,CAAC;CAGxD,MAAM,YAAY,YAAY,OAAO,SAAS;AAC9C,KAAI,UACF,QAAO,KAAK;EAAE,UAAU;EAAS,SAAS;EAAW,CAAC;AAGxD,KAAI,SAAS,WAAW,OAAO,UAAU,SACvC,KAAI;AAEF,MAAI,CADU,IAAI,OAAO,SAAS,QAAQ,CAC/B,KAAK,MAAM,CACpB,QAAO,KAAK;GAAE,UAAU;GAAS,SAAS,UAAU,MAAM,4BAA4B,SAAS,QAAQ;GAAI,CAAC;SAExG;AACN,SAAO,KAAK;GAAE,UAAU;GAAW,SAAS,0BAA0B,SAAS;GAAW,CAAC;;AAI/F,KAAI,SAAS,SAAS,YAAY,SAAS,WAAW,OAAO,UAAU;MACjE,CAAC,SAAS,QAAQ,SAAS,MAAM,CACnC,QAAO,KAAK;GAAE,UAAU;GAAS,SAAS,UAAU,MAAM,gCAAgC,SAAS,QAAQ,KAAK,KAAK,CAAC;GAAI,CAAC;;AAI/H,QAAO;;;AAMT,SAAgB,SAAS,KAAc,YAAyC;AAC9E,KAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW,QAAO,KAAA;AAC9C,KAAI,MAAM,QAAQ,IAAI,CAAE,QAAO,IAAI,KAAI,SAAQ,SAAS,MAAM,WAAW,CAAC;AAC1E,KAAI,OAAO,QAAQ,SAAU,QAAO;CAEpC,MAAM,SAAS;CACf,MAAM,SAAkC,EAAE;CAE1C,MAAM,OAAO,aACT,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,YAAY,GAAG,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAChE,OAAO,KAAK,OAAO,CAAC,UAAU;AAElC,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,EAAE,OAAO,QAAS;EACtB,MAAM,MAAM,OAAO;AACnB,MAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW;AACvC,SAAO,OAAO,SAAS,IAAI;;AAG7B,QAAO;;;AAIT,SAAgB,mBAAmB,MAAe,YAAwC;CACxF,MAAM,SAAS,SAAS,MAAM,WAAW;AACzC,QAAO,GAAG,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;;;AAI5C,SAAgB,kBAA0B;CACxC,MAAM,MAAM;CACZ,IAAI,KAAK;AACT,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,IACtB,OAAM,IAAI,KAAK,MAAM,KAAK,QAAQ,GAAG,GAAG;AAE1C,QAAO;;;;;;;;AAWT,SAAgB,WAAW,OAAuB;CAChD,MAAM,QAAQ;CACd,MAAM,OAAO;CACb,IAAI,OAAO;CACX,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,MAAM;AAC7C,MAAK,MAAM,QAAQ,OAAO;AACxB,UAAQ,OAAO,KAAK;AACpB,SAAQ,OAAO,QAAS;;AAE1B,QAAO,KAAK,SAAS,GAAG,CAAC,SAAS,IAAI,IAAI;;;;;;;;;;;;AAa5C,SAAgB,WAAW,UAQhB;AACT,QAAO,WAAW,KAAK,UAAU;EAC/B,SAAS;EACT,SAAS,OAAO;EAChB,SAAS,SAAS;EAClB,SAAS,UAAU;EACnB,SAAS,SAAS,KAAK,IAAI;EAC3B,SAAS,SAAS,KAAK,IAAI;EAC3B,SAAS,SAAS,OAAO,IAAI;EAC9B,CAAC,CAAC;;AAKL,SAAS,eAAe,KAAsB;AAC5C,KAAI,QAAQ,OAAQ,QAAO;AAC3B,KAAI,QAAQ,QAAS,QAAO;AAC5B,KAAI,QAAQ,OAAQ,QAAO;AAC3B,KAAI,UAAU,KAAK,IAAI,CAAE,QAAO,SAAS,KAAK,GAAG;AACjD,KAAI,eAAe,KAAK,IAAI,CAAE,QAAO,WAAW,IAAI;AACpD,KAAK,IAAI,WAAW,KAAI,IAAI,IAAI,SAAS,KAAI,IAAM,IAAI,WAAW,IAAI,IAAI,IAAI,SAAS,IAAI,CACzF,QAAO,IAAI,MAAM,GAAG,GAAG;AAEzB,QAAO;;AAGT,SAAS,WAAW,OAAwB;CAC1C,MAAM,MAAM,OAAO,MAAM;AACzB,KAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAC/E,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAChF,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAChF,IAAI,SAAS,KAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,IAChF,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,IAC1C,QAAQ,UAAU,QAAQ,WAAW,QAAQ,UAAU,QAAQ,SAAS,QAAQ,QAChF,QAAQ,GACX,QAAO,IAAI,IAAI,QAAQ,OAAO,OAAO,CAAC,QAAQ,MAAM,OAAM,CAAC;AAE7D,QAAO;;AAGT,SAAS,mBAAmB,OAAgB,QAA0B;CACpE,MAAM,MAAM,KAAK,OAAO,OAAO;AAC/B,KAAI,UAAU,QAAQ,UAAU,KAAA,EAAW,QAAO,EAAE;AACpD,KAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,EAAE;EACtD,MAAM,QAAkB,EAAE;AAC1B,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAiC,EAAE;AACrE,OAAI,MAAM,QAAQ,MAAM,KAAA,EAAW;AACnC,OAAI,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,EAAE,EAAE;AAC9C,UAAM,KAAK,GAAG,MAAM,EAAE,GAAG;AACzB,UAAM,KAAK,GAAG,mBAAmB,GAAG,SAAS,EAAE,CAAC;cACvC,MAAM,QAAQ,EAAE,EAAE;AAC3B,UAAM,KAAK,GAAG,MAAM,EAAE,GAAG;AACzB,SAAK,MAAM,QAAQ,EACjB,KAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,KAAK,EAAE;KACrE,MAAM,SAAS,mBAAmB,MAAM,SAAS,EAAE;AACnD,SAAI,OAAO,SAAS,GAAG;AACrB,YAAM,KAAK,GAAG,IAAI,MAAM,OAAO,GAAI,WAAW,GAAG;AACjD,YAAM,KAAK,GAAG,OAAO,MAAM,EAAE,CAAC;;UAGhC,OAAM,KAAK,GAAG,IAAI,MAAM,WAAW,KAAK,GAAG;SAI/C,OAAM,KAAK,GAAG,MAAM,EAAE,IAAI,WAAW,EAAE,GAAG;;AAG9C,SAAO;;AAET,KAAI,MAAM,QAAQ,MAAM,EAAE;EACxB,MAAM,QAAkB,EAAE;AAC1B,OAAK,MAAM,QAAQ,MACjB,KAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,KAAK,EAAE;GACrE,MAAM,SAAS,mBAAmB,MAAM,SAAS,EAAE;AACnD,OAAI,OAAO,SAAS,GAAG;AACrB,UAAM,KAAK,GAAG,IAAI,IAAI,OAAO,GAAI,WAAW,GAAG;AAC/C,UAAM,KAAK,GAAG,OAAO,MAAM,EAAE,CAAC;;QAGhC,OAAM,KAAK,GAAG,IAAI,IAAI,WAAW,KAAK,GAAG;AAG7C,SAAO;;AAET,QAAO,CAAC,GAAG,MAAM,WAAW,MAAM,GAAG;;AAGvC,SAAS,oBAAoB,MAAyC;CACpE,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;AAC/C,MAAI,QAAQ,OAAQ;AACpB,MAAI,UAAU,QAAQ,UAAU,KAAA,EAAW;AAC3C,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,EAAE;AACtD,SAAM,KAAK,GAAG,IAAI,GAAG;AACrB,SAAM,KAAK,GAAG,mBAAmB,OAAO,EAAE,CAAC;aAClC,MAAM,QAAQ,MAAM,EAAE;AAC/B,SAAM,KAAK,GAAG,IAAI,GAAG;AACrB,QAAK,MAAM,QAAQ,MACjB,KAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,KAAK,EAAE;IACrE,MAAM,SAAS,mBAAmB,MAAM,EAAE;AAC1C,QAAI,OAAO,SAAS,GAAG;AACrB,WAAM,KAAK,OAAO,OAAO,GAAI,WAAW,GAAG;AAC3C,WAAM,KAAK,GAAG,OAAO,MAAM,EAAE,CAAC;;SAGhC,OAAM,KAAK,OAAO,WAAW,KAAK,GAAG;QAIzC,OAAM,KAAK,GAAG,IAAI,IAAI,WAAW,MAAM,GAAG;;AAG9C,QAAO;;;AAIT,SAAgB,yBAAyB,SAAyE;CAChH,MAAM,aAAa,QAAQ,QAAQ,SAAS,KAAK;CACjD,MAAM,QAAQ,WAAW,MAAM,qCAAqC;AACpE,KAAI,CAAC,MAAO,QAAO;EAAE,aAAa,EAAE;EAAE,MAAM;EAAY;CAExD,MAAM,iBAAiB,MAAM;CAC7B,MAAM,OAAO,MAAM,GAAI,MAAM;CAC7B,MAAM,cAAuC,EAAE;CAE/C,IAAI,aAA4B;CAChC,IAAI,eAAgC;AAEpC,MAAK,MAAM,QAAQ,eAAe,MAAM,KAAK,EAAE;AAC7C,MAAI,WAAW,KAAK,KAAK,IAAI,YAAY;GACvC,MAAM,QAAQ,KAAK,QAAQ,YAAY,GAAG,CAAC,MAAM;AACjD,OAAI,CAAC,aAAc,gBAAe,EAAE;AACpC,gBAAa,KAAK,MAAM;AACxB;;AAGF,MAAI,cAAc,cAAc;AAC9B,eAAY,cAAc;AAC1B,gBAAa;AACb,kBAAe;;EAGjB,MAAM,UAAU,KAAK,MAAM,6BAA6B;AACxD,MAAI,CAAC,QAAS;EAEd,MAAM,MAAM,QAAQ;EACpB,MAAM,WAAW,QAAQ,GAAI,MAAM;AAEnC,MAAI,aAAa,IAAI;AACnB,gBAAa;AACb,kBAAe,EAAE;AACjB;;AAGF,MAAI,SAAS,WAAW,IAAI,IAAI,SAAS,SAAS,IAAI,EAAE;AAEtD,eAAY,OADE,SAAS,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,OAAO,QAAQ;AAEjF;;AAGF,cAAY,OAAO,eAAe,SAAS;;AAG7C,KAAI,cAAc,aAChB,aAAY,cAAc;AAG5B,QAAO;EAAE;EAAa;EAAM;;;AAI9B,SAAgB,6BAA6B,MAA+B,MAAsB;CAChG,MAAM,cAAc,KAAK,WAAW;AACpC,KAAI,YAAY,WAAW,MAAM,EAAE;EACjC,MAAM,SAAS,YAAY,QAAQ,OAAO,EAAE;AAC5C,MAAI,SAAS,GAAG;GACd,MAAM,gBAAgB,YAAY,MAAM,GAAG,OAAO,CAAC,MAAM;GACzD,MAAM,UAAU,YAAY,MAAM,SAAS,EAAE;GAE7C,MAAM,QAAkB,CAAC,MAAM;AAC/B,SAAM,KAAK,GAAG,oBAAoB,KAAK,CAAC;AACxC,OAAI,eAAe;IACjB,MAAM,YAAY,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC;AAC5C,SAAK,MAAM,QAAQ,cAAc,MAAM,KAAK,EAAE;AAC5C,SAAI,KAAK,SAAS,KAAK,KAAK,OAAO,OAAO,KAAK,OAAO,KAAK;MACzD,MAAM,WAAW,KAAK,QAAQ,IAAI;MAClC,MAAM,UAAU,WAAW,IAAI,KAAK,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG;AAChE,UAAI,WAAW,UAAU,IAAI,QAAQ,CAAE;;AAEzC,WAAM,KAAK,KAAK;;;AAGpB,SAAM,KAAK,MAAM;AACjB,OAAI,QAAQ,MAAM,CAChB,OAAM,KAAK,QAAQ,WAAW,CAAC;AAEjC,SAAM,KAAK,GAAG;AACd,UAAO,MAAM,KAAK,KAAK;;;CAI3B,MAAM,QAAkB,CAAC,MAAM;AAC/B,OAAM,KAAK,GAAG,oBAAoB,KAAK,CAAC;AACxC,OAAM,KAAK,MAAM;AACjB,OAAM,KAAK,GAAG;AACd,KAAI,MAAM;AACR,QAAM,KAAK,KAAK;AAChB,QAAM,KAAK,GAAG;;AAEhB,QAAO,MAAM,KAAK,KAAK"}
|