@contentrain/types 1.1.0 → 1.3.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 CHANGED
@@ -1,3 +1,495 @@
1
+ //#region src/migration.d.ts
2
+ /**
3
+ * Version stamped into every migration document (`version` field on each root).
4
+ * The four contracts evolve together with this package; a reader that sees a
5
+ * higher version than it knows should refuse rather than guess.
6
+ */
7
+ declare const MIGRATION_CONTRACT_VERSION = 1;
8
+ /**
9
+ * How the WordPress site's data was reached, ordered from least to most
10
+ * complete. Measured field coverage rises with each rung (roughly 34% for
11
+ * public REST, 57% with an Application Password, 82% from a WXR export, 100%
12
+ * via the Bridge plugin) — which is why provenance is recorded on every RawIR:
13
+ * two documents for the same site are only comparable if their rungs are.
14
+ */
15
+ type SourceAccessKind = 'rest_public' | 'rest_auth' | 'wxr' | 'bridge';
16
+ /** The ladder in ascending completeness order. */
17
+ declare const SOURCE_ACCESS_LADDER: readonly ["rest_public", "rest_auth", "wxr", "bridge"];
18
+ interface RawProvenance {
19
+ kind: SourceAccessKind;
20
+ /** ISO 8601 UTC time the extraction ran. */
21
+ fetched_at?: string;
22
+ /** Name/version of the producing tool (e.g. "wordpress-bridge/1.0"). */
23
+ tool?: string;
24
+ }
25
+ interface RawSite {
26
+ url: string;
27
+ title?: string;
28
+ description?: string;
29
+ /** WXR distinguishes the WP install URL from the public site URL. */
30
+ base_site_url?: string;
31
+ base_blog_url?: string;
32
+ /** Site language as reported (e.g. "en-US"); multilingual detail lives in `language_pairs`. */
33
+ language?: string | null;
34
+ /** Generator string (WordPress version) when the source exposed it. */
35
+ generator?: string | null;
36
+ /** WXR export timestamp, when the source was a WXR file. */
37
+ export_date?: string | null;
38
+ wxr_version?: string | null;
39
+ }
40
+ interface RawAuthor {
41
+ id: number | null;
42
+ login: string;
43
+ email?: string | null;
44
+ display_name: string;
45
+ first_name?: string | null;
46
+ last_name?: string | null;
47
+ }
48
+ interface RawTerm {
49
+ id: number | null;
50
+ taxonomy: string;
51
+ slug: string;
52
+ name: string;
53
+ /** Parent term slug within the same taxonomy, when hierarchical. */
54
+ parent?: string | null;
55
+ /** Whether `parent` names a term present in this document. */
56
+ parent_resolved?: boolean | null;
57
+ description?: string;
58
+ }
59
+ /** A post's reference to a term — kept even when the term itself is absent. */
60
+ interface RawTermRef {
61
+ taxonomy: string;
62
+ slug: string;
63
+ name: string;
64
+ /** Whether the referenced term exists in this document's `terms`. */
65
+ resolved: boolean;
66
+ }
67
+ /** An ACF field value paired with its field-definition key (`field_…`). */
68
+ interface RawAcfValue {
69
+ value: unknown;
70
+ field_key: string;
71
+ }
72
+ interface RawPost {
73
+ id: number;
74
+ /** WordPress post type — "post", "page", or a custom post type slug. */
75
+ type: string;
76
+ /** WordPress status verbatim ("publish", "draft", "future", "pending", …). */
77
+ status: string;
78
+ slug: string;
79
+ title: string;
80
+ /** Public permalink, when known. Permalink *structure* is a site setting; the link is the fact. */
81
+ link?: string | null;
82
+ guid?: string | null;
83
+ /** Author login; resolve against `RawIR.authors`. */
84
+ author: string | null;
85
+ /** ISO 8601 UTC, null when the source carried no usable date. */
86
+ date: string | null;
87
+ modified: string | null;
88
+ /** Rendered/exported HTML body, untransformed. */
89
+ content: string;
90
+ excerpt: string;
91
+ parent?: number | null;
92
+ menu_order?: number;
93
+ sticky?: boolean;
94
+ password?: string | null;
95
+ comment_status?: string | null;
96
+ ping_status?: string | null;
97
+ terms: RawTermRef[];
98
+ /**
99
+ * Post meta, PHP-serialized values already decoded where possible.
100
+ * Core WP keys (underscore-prefixed) are included: which keys matter is a
101
+ * downstream decision.
102
+ */
103
+ meta: Record<string, unknown>;
104
+ /** Meta keys whose values were PHP-serialized in the source (decoded above). */
105
+ serialized_keys?: string[];
106
+ /** ACF fields recovered by pairing `foo` with `_foo = "field_…"`. */
107
+ acf?: Record<string, RawAcfValue>;
108
+ }
109
+ interface RawAttachment {
110
+ id: number;
111
+ title: string;
112
+ slug: string;
113
+ /** Original file URL. Downloading/re-hosting is a consumer concern. */
114
+ url: string | null;
115
+ alt?: string;
116
+ caption?: string;
117
+ description?: string;
118
+ /** Relative upload path (`_wp_attached_file`), when present. */
119
+ file?: string | null;
120
+ /** `_wp_attachment_metadata` verbatim (sizes, EXIF, …). */
121
+ image_meta?: unknown;
122
+ mime?: string | null;
123
+ parent?: number | null;
124
+ parent_resolved?: boolean | null;
125
+ author?: string | null;
126
+ date?: string | null;
127
+ status?: string;
128
+ meta?: Record<string, unknown>;
129
+ }
130
+ /** Where a menu item points. `resolved` says whether the target exists in this document. */
131
+ type RawMenuTarget = {
132
+ kind: 'url';
133
+ url: string;
134
+ resolved: true;
135
+ } | {
136
+ kind: 'post';
137
+ post_type: string;
138
+ id: number | null;
139
+ slug: string | null;
140
+ resolved: boolean;
141
+ } | {
142
+ kind: 'term';
143
+ taxonomy: string;
144
+ id: number | null;
145
+ slug: string | null;
146
+ resolved: boolean;
147
+ } | {
148
+ kind: 'archive';
149
+ post_type: string;
150
+ resolved: true;
151
+ } | {
152
+ kind: 'unknown';
153
+ resolved: false;
154
+ };
155
+ interface RawMenuItem {
156
+ id: number;
157
+ title: string;
158
+ order?: number;
159
+ /** Parent menu-item id for nested menus. */
160
+ parent?: number | null;
161
+ parent_unresolved?: boolean;
162
+ url?: string | null;
163
+ target: RawMenuTarget;
164
+ /** `target` attribute for the rendered link (e.g. "_blank"). */
165
+ target_attr?: string | null;
166
+ classes?: string[];
167
+ description?: string;
168
+ status?: string;
169
+ }
170
+ interface RawMenu {
171
+ id: number | null;
172
+ slug: string;
173
+ name: string;
174
+ items: RawMenuItem[];
175
+ }
176
+ interface RawComment {
177
+ id: number;
178
+ /** Post id this comment belongs to. */
179
+ post: number;
180
+ post_type?: string;
181
+ parent?: number | null;
182
+ parent_resolved?: boolean | null;
183
+ author: string;
184
+ email?: string | null;
185
+ url?: string | null;
186
+ date: string | null;
187
+ content: string;
188
+ /** WordPress approval flag verbatim ("1", "0", "spam", …). */
189
+ approved?: string;
190
+ type?: string;
191
+ user_id?: number | null;
192
+ meta?: Record<string, unknown>;
193
+ }
194
+ /** A redirect rule (e.g. from the Redirection plugin — visible from the authenticated rung up). */
195
+ interface RawRedirect {
196
+ from: string;
197
+ to: string;
198
+ status?: number;
199
+ /** Which plugin/table produced the rule. */
200
+ source?: string;
201
+ }
202
+ /** Translation grouping for multilingual sites (Polylang/WPML — bridge rung). */
203
+ interface RawLanguagePair {
204
+ post: number;
205
+ /** locale → post id of the translation. */
206
+ translations: Record<string, number>;
207
+ }
208
+ /**
209
+ * The complete raw extraction of one WordPress site at one access rung.
210
+ * This is the boundary document between extraction (Bridge/importers) and
211
+ * everything downstream — content conversion, capability analysis, migration.
212
+ */
213
+ interface RawIR {
214
+ version: number;
215
+ provenance: RawProvenance;
216
+ site: RawSite;
217
+ authors: RawAuthor[];
218
+ terms: RawTerm[];
219
+ posts: RawPost[];
220
+ attachments: RawAttachment[];
221
+ menus?: RawMenu[];
222
+ comments?: RawComment[];
223
+ redirects?: RawRedirect[];
224
+ language_pairs?: RawLanguagePair[];
225
+ /** Site options (bridge rung), verbatim. */
226
+ options?: Record<string, unknown>;
227
+ }
228
+ declare const CAPABILITY_KEYS: readonly ["seo", "forms", "comments", "search", "newsletter", "analytics", "consent", "share", "video", "ads", "builder", "blocks", "i18n", "cache", "security", "acf", "ecommerce", "jetpack", "redirects", "membership", "accessibility", "media", "scheduling"];
229
+ type CapabilityKey = (typeof CAPABILITY_KEYS)[number];
230
+ interface CapabilityDetection {
231
+ present: boolean;
232
+ /** Identified plugin/provider, when known (e.g. "yoast", "contact-form-7"). */
233
+ plugin?: string | null;
234
+ /** How it was detected: "rest", "dom", "slug", "headers", "meta", … */
235
+ evidence?: string[];
236
+ detail?: string;
237
+ }
238
+ interface CapabilityManifest {
239
+ version: number;
240
+ site_url: string;
241
+ access: {
242
+ html_status?: number | null;
243
+ rest_status?: number | null; /** Highest source-access rung actually achieved for this site. */
244
+ achieved: SourceAccessKind;
245
+ };
246
+ generator?: string | null;
247
+ theme?: string | null;
248
+ plugins?: string[];
249
+ rest_namespaces?: string[];
250
+ custom_post_types?: Array<{
251
+ slug: string; /** Whether the CPT is exposed in public REST (`rest: false` CPTs need a higher rung). */
252
+ rest_visible: boolean;
253
+ count?: number | null;
254
+ }>;
255
+ /** Comments get first-class treatment: they are a launch-critical capability. */
256
+ comments: {
257
+ active: boolean;
258
+ form_status?: 'open' | 'closed' | 'mixed' | null;
259
+ rest_total?: number | null;
260
+ plugin?: string | null;
261
+ };
262
+ capabilities: Partial<Record<CapabilityKey, CapabilityDetection>>;
263
+ /** Dynamic front-end behaviors observed in the DOM ("carousel", "modal", "search-overlay", …). */
264
+ behaviors?: string[];
265
+ languages?: string[];
266
+ }
267
+ /**
268
+ * How the legacy CSS for a family was packaged.
269
+ * - `purge_set`: rules kept if used by any page in the family's page set —
270
+ * small file, safe against per-page rule loss (single-page purging is not).
271
+ * - `localcss`: the site's stylesheets localized wholesale — larger, zero risk.
272
+ */
273
+ type CssStrategy = 'purge_set' | 'localcss';
274
+ /**
275
+ * Cascade layer name the emitter quarantines legacy CSS under, so a modern
276
+ * utility layer (e.g. Tailwind) can coexist with pixel-faithful migrated pages.
277
+ */
278
+ declare const LEGACY_CSS_LAYER = "legacy";
279
+ /** Design tokens extracted from the source site, e.g. for a Tailwind `@theme` block. */
280
+ interface DesignTokens {
281
+ colors?: Record<string, string>;
282
+ font_families?: Record<string, string>;
283
+ font_sizes?: Record<string, string>;
284
+ spacing?: Record<string, string>;
285
+ breakpoints?: Record<string, string>;
286
+ }
287
+ type RouteKind = 'front' | 'single' | 'page' | 'archive' | 'term' | 'author' | 'date' | 'search' | 'not_found' | 'custom';
288
+ interface RouteParamDef {
289
+ name: string;
290
+ source: 'post_slug' | 'term_slug' | 'author_slug' | 'page_number' | 'custom';
291
+ }
292
+ /** Selects a family/component variant when a route parameter matches. */
293
+ interface RouteVariantRule {
294
+ param: string;
295
+ in: string[];
296
+ variant: string;
297
+ }
298
+ /**
299
+ * One route pattern → one layout family. Pagination is a parameter of a route
300
+ * (`/news/page/2` is `/news` with `page_number = 2`), never a separate family —
301
+ * across a 30-site corpus, page 2 never produced a new layout.
302
+ */
303
+ interface RouteModel {
304
+ id: string;
305
+ /** URL pattern with `:param` placeholders, e.g. "/category/:term", "/news/page/:page". */
306
+ pattern: string;
307
+ kind: RouteKind;
308
+ /** LayoutFamily id. */
309
+ family: string;
310
+ params?: RouteParamDef[];
311
+ /** QueryBinding id feeding this route's list, when it renders one. */
312
+ query?: string;
313
+ variant_rules?: RouteVariantRule[];
314
+ }
315
+ type SlotKind = 'body' | 'title' | 'date' | 'excerpt' | 'author_name' | 'author_link' | 'featured_image' | 'term_list' | 'self_link' | 'custom';
316
+ /** Where a content field lands in a family's markup. */
317
+ interface SlotBinding {
318
+ kind: SlotKind;
319
+ /** CSS selector of the slot's container, when one could be determined. */
320
+ selector?: string;
321
+ /** Content-model field backing a `custom` slot. */
322
+ field?: string;
323
+ /** Date rendering format for `date` slots, as observed on the source site. */
324
+ date_format?: string;
325
+ }
326
+ /** A rendered, asset-rewritten chunk of site chrome the emitter injects verbatim. */
327
+ interface ChromeChunk {
328
+ id: string;
329
+ position: 'head' | 'before_body' | 'after_body';
330
+ html: string;
331
+ }
332
+ interface ComponentPlacement {
333
+ /** ComponentDef id. */
334
+ component: string;
335
+ variant?: string;
336
+ selector?: string;
337
+ }
338
+ interface FamilyVariant {
339
+ key: string;
340
+ description?: string;
341
+ }
342
+ interface LayoutFamily {
343
+ id: string;
344
+ name?: string;
345
+ kind?: RouteKind;
346
+ chrome?: ChromeChunk[];
347
+ slots?: SlotBinding[];
348
+ components?: ComponentPlacement[];
349
+ css: {
350
+ strategy: CssStrategy; /** Emitted stylesheet paths, relative to the generated project. */
351
+ files?: string[];
352
+ };
353
+ /** Observed column counts per viewport class (e.g. desktop 4 → mobile 1). */
354
+ columns?: Partial<Record<'desktop' | 'mobile', number>>;
355
+ variants?: FamilyVariant[];
356
+ /** Verification trail: which pages formed the family, held-out fidelity score. */
357
+ evidence?: {
358
+ pages?: string[];
359
+ holdout_score?: number;
360
+ };
361
+ }
362
+ /**
363
+ * Component semantic vocabulary. `source` records where its content can come
364
+ * from: `rest` (derivable from content data — author box, term list),
365
+ * `runtime` (needs a live service — comments form, search), `chrome` (carried
366
+ * as rendered markup — nav, ads).
367
+ */
368
+ declare const COMPONENT_TYPES: readonly ["nav", "related", "comments", "ads", "chrome", "taxonomy", "author", "share", "breadcrumb", "meta", "pagination", "card", "custom"];
369
+ type ComponentType = (typeof COMPONENT_TYPES)[number];
370
+ type ComponentSource = 'rest' | 'runtime' | 'chrome';
371
+ interface ComponentVariantDef {
372
+ key: string;
373
+ conditions?: RouteVariantRule[];
374
+ description?: string;
375
+ }
376
+ interface ComponentDef {
377
+ id: string;
378
+ type: ComponentType;
379
+ source: ComponentSource;
380
+ name?: string;
381
+ selector?: string;
382
+ /** e.g. a card component with `news`, `compact`, `featured` variants. */
383
+ variants?: ComponentVariantDef[];
384
+ }
385
+ type ExcerptSource = 'excerpt' | 'content_first_paragraph' | 'none';
386
+ type PaginationKind = 'numbered' | 'infinite' | 'none';
387
+ /**
388
+ * The query behind a rendered list: which content, filtered how, in what
389
+ * order, how many. Inferred by matching rendered items against candidate
390
+ * queries — order matters, so the sequence is part of the contract.
391
+ */
392
+ interface QueryBinding {
393
+ id: string;
394
+ /** Content source: "posts", "pages", a CPT slug, or a content-model id. */
395
+ source: string;
396
+ taxonomy?: {
397
+ taxonomy: string; /** Fixed term slug, or the route param carrying it. */
398
+ term?: string;
399
+ term_param?: string;
400
+ };
401
+ author?: {
402
+ slug?: string;
403
+ param?: string;
404
+ };
405
+ order: {
406
+ by: 'date' | 'title' | 'menu_order' | 'custom';
407
+ direction: 'asc' | 'desc';
408
+ };
409
+ per_page: number | null;
410
+ pagination: PaginationKind;
411
+ excerpt_source?: ExcerptSource;
412
+ /** WordPress image size name the list renders (e.g. "medium_large"). */
413
+ image_size?: string;
414
+ }
415
+ /**
416
+ * - `responsive`: one build, CSS handles viewports.
417
+ * - `split`: desktop and mobile variants are produced separately and served
418
+ * by device — for sites whose mobile DOM differs beyond CSS.
419
+ */
420
+ type ViewportStrategy = 'responsive' | 'split';
421
+ interface ProjectIR {
422
+ version: number;
423
+ site: {
424
+ url: string;
425
+ title?: string;
426
+ locales?: string[];
427
+ };
428
+ routes: RouteModel[];
429
+ families: LayoutFamily[];
430
+ components?: ComponentDef[];
431
+ queries?: QueryBinding[];
432
+ tokens?: DesignTokens;
433
+ /** Default CSS packaging for families that don't override it. */
434
+ css_default: CssStrategy;
435
+ viewport_strategy?: ViewportStrategy;
436
+ /** Content-model ids this project's queries and slots refer to. */
437
+ content_models?: string[];
438
+ }
439
+ type CapabilityDisposition = 'migrated_static' | 'archived' | 'needs_runtime' | 'external_adapter' | 'kept_on_wordpress' | 'dropped';
440
+ interface HandoffCapability {
441
+ key: CapabilityKey;
442
+ disposition: CapabilityDisposition;
443
+ detail?: string;
444
+ /** e.g. comments: { migrated: 412, spam_skipped: 60 } */
445
+ counts?: Record<string, number>;
446
+ }
447
+ type OfferProvider = 'studio_managed' | 'adapter' | 'keep_wordpress';
448
+ interface CostEstimate {
449
+ currency: string;
450
+ monthly: number;
451
+ assumptions?: string[];
452
+ }
453
+ /**
454
+ * The comparison shown when a runtime capability is offered: what running the
455
+ * old server costs (maintenance, security, hosting) against the managed
456
+ * option — so declining is an informed choice, not a default.
457
+ */
458
+ interface CostComparison {
459
+ self_host?: CostEstimate;
460
+ managed?: CostEstimate;
461
+ }
462
+ interface HandoffOffer {
463
+ capability: CapabilityKey;
464
+ provider: OfferProvider;
465
+ /** Adapter identifier when `provider` is `adapter`. */
466
+ adapter?: string;
467
+ cost_comparison?: CostComparison;
468
+ /** e.g. "keeping comments on WordPress means the WordPress server stays live". */
469
+ warning?: string;
470
+ }
471
+ interface MigrationHandoff {
472
+ version: number;
473
+ site_url: string;
474
+ /** ISO 8601 UTC. */
475
+ generated_at: string;
476
+ repository?: {
477
+ provider: 'github' | 'gitlab';
478
+ owner: string;
479
+ name: string;
480
+ default_branch: string;
481
+ };
482
+ preview_url?: string;
483
+ content_summary?: {
484
+ models: number;
485
+ entries: number;
486
+ locales?: string[];
487
+ };
488
+ capabilities: HandoffCapability[];
489
+ offers?: HandoffOffer[];
490
+ notes?: string[];
491
+ }
492
+ //#endregion
1
493
  //#region src/provider.d.ts
2
494
  /**
3
495
  * A single file change within a plan.
@@ -38,6 +530,13 @@ interface ProviderCapabilities {
38
530
  pullRequestFallback: boolean;
39
531
  /** Provider can execute AST scanners against source files. Implies local disk access. */
40
532
  astScan: boolean;
533
+ /**
534
+ * Provider can write a two-parent merge commit (see
535
+ * `RepoProvider.createMergeCommit`). Absent = false. Optional so that
536
+ * existing external `implements` code keeps compiling: a required member
537
+ * here would be a breaking change for every custom provider.
538
+ */
539
+ mergeCommit?: boolean;
41
540
  }
42
541
  /** Capability set for the LocalProvider (simple-git + worktree). */
43
542
  declare const LOCAL_CAPABILITIES: ProviderCapabilities;
@@ -95,6 +594,15 @@ interface Commit {
95
594
  sync?: SyncResult;
96
595
  /** Non-fatal problem encountered while completing the write. */
97
596
  warning?: string;
597
+ /**
598
+ * Whether the base branch advanced to the contentrain tip after an
599
+ * auto-merge. Only providers that own the base advance (a local worktree)
600
+ * report it. `blocked_diverged` is a partial success: the content is on
601
+ * the contentrain branch; only the fast-forward is pending.
602
+ */
603
+ base_advance?: BaseAdvance;
604
+ /** Outcome of pushing the contentrain branch, when the provider pushes. */
605
+ remote_push?: RemotePush;
98
606
  }
99
607
  /**
100
608
  * Input to `applyPlan`. Represents a single atomic commit: all changes land
@@ -175,6 +683,86 @@ interface MergeResult {
175
683
  warning?: string;
176
684
  };
177
685
  }
686
+ /**
687
+ * What happened to the base branch after content landed on the
688
+ * content-tracking branch.
689
+ *
690
+ * `advanced` — the base branch fast-forwarded to the contentrain tip.
691
+ * `blocked_diverged` — the base branch holds commits that are not in
692
+ * contentrain, so fast-forwarding was impossible. The content itself is
693
+ * safe on the contentrain branch; only the advance is pending until the
694
+ * branches are reconciled.
695
+ *
696
+ * These two values are shared vocabulary across the ecosystem — Studio
697
+ * reports the same enum for its main-advance state. A pull request is an
698
+ * attachment (`pullRequestUrl`), never a third state: "PR opened" is
699
+ * `blocked_diverged` with a non-null URL.
700
+ */
701
+ type BaseAdvance = 'advanced' | 'blocked_diverged';
702
+ /**
703
+ * Outcome of pushing the content-tracking branch to the configured remote.
704
+ *
705
+ * `pushed` — the remote accepted the push (possibly after one
706
+ * fetch-merge-retry). `rejected` — the push failed even after the retry;
707
+ * local and remote have diverged and need reconciling. `no-remote` — the
708
+ * repository has no configured remote, so nothing was attempted.
709
+ */
710
+ type RemotePush = 'pushed' | 'rejected' | 'no-remote';
711
+ /**
712
+ * Machine-readable conflict kinds produced by the content-aware three-way
713
+ * reconcile planner. CLOSED SET: consumers (Studio) key their localized
714
+ * editor questions on these values, so a free string would drift. Adding a
715
+ * value here is a minor version bump and MUST carry a changelog entry;
716
+ * renaming or removing one is breaking.
717
+ */
718
+ type ConflictCode = 'field_value_conflict' | 'dictionary_value_conflict' | 'vocabulary_value_conflict' | 'model_key_conflict' | 'meta_status_conflict' | 'document_body_conflict' | 'frontmatter_value_conflict' | 'delete_edit_conflict' | 'file_conflict';
719
+ /**
720
+ * One surviving conflict from a three-way reconcile: base (merge-base),
721
+ * ours (content branch), theirs (base branch) disagree in a way the policy
722
+ * table cannot resolve mechanically. Everything mechanical has already been
723
+ * merged; only these items need a decision from an editor or agent.
724
+ *
725
+ * `id` is derived from the position AND the three values (see
726
+ * `conflictId`), so a resolution made against a stale dry-run no longer
727
+ * matches once any side changes — compare-and-set for free.
728
+ */
729
+ interface ConflictItem {
730
+ id: string;
731
+ /** Content-root-relative path of the file the conflict lives in. */
732
+ path: string;
733
+ model?: string;
734
+ kind: 'collection' | 'singleton' | 'document' | 'dictionary' | 'vocabulary' | 'model' | 'meta' | 'file';
735
+ /** Entry ID, dictionary key, vocabulary term, or model schema key. */
736
+ key?: string;
737
+ /** Field inside the entry / frontmatter key, when the conflict is field-level. */
738
+ field?: string;
739
+ locale?: string;
740
+ /** The three values in question — for display; omitted for `kind: 'file'`. */
741
+ base?: unknown;
742
+ ours?: unknown;
743
+ theirs?: unknown;
744
+ code: ConflictCode;
745
+ /** Ready-to-display English sentence naming the conflict. */
746
+ message: string;
747
+ /**
748
+ * Policy-table suggestion, never auto-applied. Present only where the
749
+ * approved policy names a preferred side (model schema keys → `theirs`).
750
+ */
751
+ suggested?: 'ours' | 'theirs';
752
+ }
753
+ /**
754
+ * A decision for one conflict, keyed by `ConflictItem.id`. `choose` picks a
755
+ * side (absence of the item on that side means deletion); `value` supplies
756
+ * a hand-authored replacement. A resolution whose `id` no longer matches a
757
+ * live conflict is dropped and the conflict re-reported with a fresh id.
758
+ */
759
+ type ConflictResolution = {
760
+ id: string;
761
+ choose: 'ours' | 'theirs';
762
+ } | {
763
+ id: string;
764
+ value: unknown;
765
+ };
178
766
  /**
179
767
  * One media asset as the provider's media stack reports it. `path` is the
180
768
  * repo-relative storage path content fields reference (`media/...`); `url`
@@ -310,6 +898,40 @@ interface RepoProvider extends RepoReader, RepoWriter {
310
898
  }): Promise<MergeResult>;
311
899
  isMerged(branch: string, into?: string): Promise<boolean>;
312
900
  getDefaultBranch(): Promise<string>;
901
+ /**
902
+ * The merge-base commit of two refs — the `base` input of a three-way
903
+ * reconcile.
904
+ *
905
+ * Reconciling a diverged content branch needs the last common ancestor,
906
+ * and every backend can name it (git `merge-base`, GitHub's compare API,
907
+ * GitLab's merge_base endpoint) — but not every existing implementation
908
+ * does yet, and demanding it would break external `implements` code.
909
+ *
910
+ * Optional: a provider that omits it cannot drive a reconcile; the caller
911
+ * falls back to a pull-request flow. Returns `null` when the refs share
912
+ * no history.
913
+ */
914
+ getMergeBase?(refA: string, refB: string): Promise<string | null>;
915
+ /**
916
+ * Write a set of resolved changes as a TWO-PARENT merge commit on
917
+ * `branch`, joining `ours` and `theirs` so that git history records the
918
+ * reconcile — after it, `theirs` is an ancestor of `branch` and a plain
919
+ * fast-forward advance works again. `changes` are applied on top of the
920
+ * `ours` tree; for content-owned paths the planner's output is
921
+ * authoritative, never a textual auto-merge.
922
+ *
923
+ * Optional, mirrored by `capabilities.mergeCommit`: a backend whose
924
+ * commits API cannot express two parents (GitLab) omits it and the
925
+ * caller falls back to a merge-request flow.
926
+ */
927
+ createMergeCommit?(input: {
928
+ /** Branch whose tip becomes the merge commit (the content branch). */branch: string; /** First parent — the current tip of `branch`. */
929
+ ours: string; /** Second parent — the ref being reconciled in. */
930
+ theirs: string;
931
+ changes: FileChange[];
932
+ message: string;
933
+ author: CommitAuthor;
934
+ }): Promise<Commit>;
313
935
  }
314
936
  //#endregion
315
937
  //#region src/index.d.ts
@@ -825,6 +1447,32 @@ declare function sortKeys(obj: unknown, fieldOrder?: readonly string[]): unknown
825
1447
  declare function canonicalStringify(data: unknown, fieldOrder?: readonly string[]): string;
826
1448
  /** Generate a 12-character hex entry ID */
827
1449
  declare function generateEntryId(): string;
1450
+ /**
1451
+ * FNV-1a 64-bit over the input's UTF-8 code points, returned as 16 hex
1452
+ * chars. Deliberately dependency-free (no `node:crypto` — this package has
1453
+ * zero runtime imports and runs anywhere). Not cryptographic; the use case
1454
+ * is change detection, not tamper resistance.
1455
+ */
1456
+ declare function stableHash(input: string): string;
1457
+ /**
1458
+ * Identity of a reconcile conflict: position AND values. Hashing the three
1459
+ * values into the id makes every resolution a compare-and-set — if any side
1460
+ * changes between the dry-run that produced the conflict and the apply that
1461
+ * carries the decision, the id no longer matches, the stale resolution is
1462
+ * dropped, and the conflict is re-reported with a fresh id. Values are
1463
+ * normalized through `sortKeys` first so semantically equal objects hash
1464
+ * equal regardless of key order; the positional array keeps absent and
1465
+ * null distinguishable per slot.
1466
+ */
1467
+ declare function conflictId(conflict: {
1468
+ path: string;
1469
+ key?: string;
1470
+ field?: string;
1471
+ locale?: string;
1472
+ base?: unknown;
1473
+ ours?: unknown;
1474
+ theirs?: unknown;
1475
+ }): string;
828
1476
  /** Parse markdown frontmatter — extracts YAML frontmatter and body from a markdown string */
829
1477
  declare function parseMarkdownFrontmatter(content: string): {
830
1478
  frontmatter: Record<string, unknown>;
@@ -833,5 +1481,5 @@ declare function parseMarkdownFrontmatter(content: string): {
833
1481
  /** Serialize data + body into markdown frontmatter format */
834
1482
  declare function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string;
835
1483
  //#endregion
836
- export { type ApplyPlanInput, AssetEntry, type Branch, CANONICAL_JSON, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, type Commit, type CommitAuthor, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, DICTIONARY_TITLE_FIELD, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EntryMeta, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GraphNode, LOCALE_PATTERN, LOCAL_CAPABILITIES, LocaleStrategy, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, PATH_PATTERNS, Platform, PolymorphicRelationRef, ProjectGraph, type ProviderCapabilities, type RepoProvider, type RepoReader, type RepoWriter, SECRET_PATTERNS, SLUG_PATTERN, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, SingletonContentFile, SingletonMeta, StackType, StringContext, SyncResult, TITLE_FIELD_TYPES, ValidationError, ValidationResult, Vocabulary, WorkflowMode, type WriteReadiness, canonicalStringify, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
1484
+ export { type ApplyPlanInput, AssetEntry, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DICTIONARY_TITLE_FIELD, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EntryMeta, ExcerptSource, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GraphNode, HandoffCapability, HandoffOffer, LEGACY_CSS_LAYER, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PATH_PATTERNS, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, type ProviderCapabilities, QueryBinding, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawIR, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawSite, RawTerm, RawTermRef, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, StackType, StringContext, SyncResult, TITLE_FIELD_TYPES, ValidationError, ValidationResult, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, canonicalStringify, conflictId, detectSecrets, generateEntryId, isMediaType, isTitleFieldType, orderedFieldNames, parseMarkdownFrontmatter, resolveFieldLabel, serializeMarkdownFrontmatter, sortKeys, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
837
1485
  //# sourceMappingURL=index.d.mts.map