@contentrain/types 1.0.2 → 1.1.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/dist/index.d.mts +15 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +23 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -724,7 +724,21 @@ interface ContentrainError {
|
|
|
724
724
|
/** Command or action the developer should take */
|
|
725
725
|
developer_action: string;
|
|
726
726
|
}
|
|
727
|
-
/**
|
|
727
|
+
/**
|
|
728
|
+
* Patterns for detecting a credential that has been pasted into content.
|
|
729
|
+
*
|
|
730
|
+
* Each one matches a credential-shaped *value*, not prose that mentions
|
|
731
|
+
* credentials. That distinction is the whole design: the earlier set matched
|
|
732
|
+
* bare prefixes, so `task_id` and `risk_level` tripped `sk_`, every page
|
|
733
|
+
* documenting `Authorization: Bearer <token>` tripped `Bearer `, and
|
|
734
|
+
* `postgres://localhost:5432/mydb` in a getting-started guide tripped the
|
|
735
|
+
* connection-string rule. On this repository — which stores its own
|
|
736
|
+
* documentation as content — that was 7 of 12 validation errors, and the real
|
|
737
|
+
* findings were buried under them.
|
|
738
|
+
*
|
|
739
|
+
* A check that fires on the wrong thing is not a strict check; it is a check
|
|
740
|
+
* people learn to scroll past.
|
|
741
|
+
*/
|
|
728
742
|
declare const SECRET_PATTERNS: ReadonlyArray<RegExp>;
|
|
729
743
|
/** Validate a slug — returns error message or null if valid */
|
|
730
744
|
declare function validateSlug(slug: string): string | null;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.ts","../src/index.ts"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -80,21 +80,30 @@ const MODEL_FIELD_ORDER = [
|
|
|
80
80
|
];
|
|
81
81
|
/** Standard name for the dedicated content branch */
|
|
82
82
|
const CONTENTRAIN_BRANCH = "contentrain";
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Patterns for detecting a credential that has been pasted into content.
|
|
85
|
+
*
|
|
86
|
+
* Each one matches a credential-shaped *value*, not prose that mentions
|
|
87
|
+
* credentials. That distinction is the whole design: the earlier set matched
|
|
88
|
+
* bare prefixes, so `task_id` and `risk_level` tripped `sk_`, every page
|
|
89
|
+
* documenting `Authorization: Bearer <token>` tripped `Bearer `, and
|
|
90
|
+
* `postgres://localhost:5432/mydb` in a getting-started guide tripped the
|
|
91
|
+
* connection-string rule. On this repository — which stores its own
|
|
92
|
+
* documentation as content — that was 7 of 12 validation errors, and the real
|
|
93
|
+
* findings were buried under them.
|
|
94
|
+
*
|
|
95
|
+
* A check that fires on the wrong thing is not a strict check; it is a check
|
|
96
|
+
* people learn to scroll past.
|
|
97
|
+
*/
|
|
84
98
|
const SECRET_PATTERNS = [
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/
|
|
89
|
-
/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/mysql:\/\//,
|
|
94
|
-
/mongodb:\/\//,
|
|
95
|
-
/-----BEGIN/,
|
|
96
|
-
/AKIA/,
|
|
97
|
-
/aws_secret/i
|
|
99
|
+
/\b[sp]k_(?:live|test)_[A-Za-z0-9]{6,}/,
|
|
100
|
+
/\bgh[pousr]_[A-Za-z0-9]{10,}/,
|
|
101
|
+
/\bBearer\s+(?![<${{]|YOUR[_-]|your[_-]|xxx|XXX|\.{3})[A-Za-z0-9._~+/-]{20,}/,
|
|
102
|
+
/\bapi[_-]?key\s*[:=]\s*['"`]?[A-Za-z0-9_-]{16,}/i,
|
|
103
|
+
/\baws_secret[_a-z]*\s*[:=]\s*['"`]?[A-Za-z0-9/+=]{20,}/i,
|
|
104
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
105
|
+
/\b(?:postgres(?:ql)?|mysql|mongodb)(?:\+srv)?:\/\/[^\s:@/]+:[^\s@/]+@/,
|
|
106
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/
|
|
98
107
|
];
|
|
99
108
|
/** Validate a slug — returns error message or null if valid */
|
|
100
109
|
function validateSlug(slug) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.ts","../src/index.ts"],"sourcesContent":["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\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}\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\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// ─── 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","// ─── 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/** Common patterns for detecting potential secrets in content values */\nexport const SECRET_PATTERNS: ReadonlyArray<RegExp> = [\n /sk_/, /pk_/, /api_key/i, /apikey/i,\n /ghp_/, /gho_/, /Bearer /,\n /postgres:\\/\\//, /mysql:\\/\\//, /mongodb:\\/\\//,\n /-----BEGIN/, /AKIA/, /aws_secret/i,\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// ─── 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// ─── 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 Branch,\n Commit,\n CommitAuthor,\n FileChange,\n FileDiff,\n MediaAsset,\n MediaIngestInput,\n MediaListOptions,\n MediaListResult,\n MediaProvider,\n MediaUpdateInput,\n MergeResult,\n ProviderCapabilities,\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/provider.ts","../src/index.ts"],"sourcesContent":["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\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}\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\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// ─── 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","// ─── 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// ─── 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// ─── 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 Branch,\n Commit,\n CommitAuthor,\n FileChange,\n FileDiff,\n MediaAsset,\n MediaIngestInput,\n MediaListOptions,\n MediaListResult,\n MediaProvider,\n MediaUpdateInput,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n RepoReader,\n RepoWriter,\n WriteReadiness,\n} from './provider.js'\nexport { LOCAL_CAPABILITIES } from './provider.js'\n"],"mappings":"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"}
|