@contentrain/types 1.16.0 → 1.18.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
@@ -13,9 +13,10 @@ declare function canonicalStringify(data: unknown, fieldOrder?: readonly string[
13
13
  declare const MIGRATION_CONTRACT_VERSION = 1;
14
14
  /**
15
15
  * How the WordPress site's data was reached, ordered from least to most
16
- * complete. Measured field coverage rises with each rung (roughly 34% for
17
- * public REST, 57% with an Application Password, 82% from a WXR export, 100%
18
- * via the Bridge plugin) — which is why provenance is recorded on every RawIR:
16
+ * complete. Field coverage rises with each rung — measured in August 2026 at
17
+ * roughly 34% for public REST, 57% with an Application Password and 82% from a
18
+ * WXR export; the Bridge plugin reads WordPress from inside and is meant to
19
+ * reach every field — which is why provenance is recorded on every RawIR:
19
20
  * two documents for the same site are only comparable if their rungs are.
20
21
  */
21
22
  type SourceAccessKind = 'rest_public' | 'rest_auth' | 'wxr' | 'bridge';
@@ -215,13 +216,338 @@ interface RawComment {
215
216
  user_id?: number | null;
216
217
  meta?: Record<string, unknown>;
217
218
  }
218
- /** A redirect rule (e.g. from the Redirection plugin — visible from the authenticated rung up). */
219
+ /**
220
+ * How a redirect's `from` is compared with a request path. Only `url` without
221
+ * `regex` is a plain one-to-one mapping; the others are patterns, and turning
222
+ * one into a literal `from` produces the wrong redirect.
223
+ */
224
+ type RawRedirectMatch = 'url' | 'regex' | 'start' | 'contains' | 'end';
225
+ /** A redirect rule the live site serves (e.g. from the Redirection plugin — visible from the authenticated rung up). */
219
226
  interface RawRedirect {
220
227
  from: string;
221
228
  to: string;
222
229
  status?: number;
223
230
  /** Which plugin/table produced the rule. */
224
231
  source?: string;
232
+ /** Stable id within the export, `<source>:<source-specific id>`. */
233
+ id?: string;
234
+ /** How `from` matches; absent means `url`. */
235
+ match?: RawRedirectMatch;
236
+ /** `from` is a regular expression in the source's own dialect. */
237
+ regex?: boolean;
238
+ /** The source's own module/group serving it, when it has one (e.g. Redirection's "Apache"). */
239
+ served_by?: string;
240
+ /** An adjustment the extractor made, stated rather than hidden (e.g. a non-redirect status read as 301). */
241
+ status_note?: string;
242
+ }
243
+ /**
244
+ * A rule a source holds that the site does not serve as a plain redirect, with
245
+ * the reason. Every rule lands in `RawIR.redirects` or here, so the two
246
+ * together account for the source's whole table.
247
+ */
248
+ interface RawRedirectExcluded extends Omit<Partial<RawRedirect>, 'match'> {
249
+ id: string;
250
+ source: string;
251
+ /** The source's own match type — may be a condition (`login`, `referrer`, …) rather than a {@link RawRedirectMatch}. */
252
+ match?: RawRedirectMatch | (string & {});
253
+ reason: 'disabled' | 'source-inactive' | 'slug-reused' | 'no-slug-address' | `not-a-redirect:${string}` | `conditional-match:${string}`;
254
+ /** The condition a conditional rule depends on, verbatim. */
255
+ condition?: unknown;
256
+ }
257
+ declare const SEO_PROVIDERS: readonly ["yoast", "rank_math", "aioseo"];
258
+ type SeoProvider = (typeof SEO_PROVIDERS)[number];
259
+ /** One page's SEO as one provider holds it. */
260
+ interface RawSeoEntry {
261
+ /** true: the running plugin rendered these values; false: stored values and templates only. */
262
+ resolved?: boolean;
263
+ title?: string;
264
+ description?: string;
265
+ canonical?: string;
266
+ robots?: {
267
+ index?: 'index' | 'noindex';
268
+ follow?: 'follow' | 'nofollow';
269
+ advanced?: string[];
270
+ };
271
+ /** The directives the page actually carries, after WordPress core and the plugin reconcile them. */
272
+ robots_served?: string[];
273
+ open_graph?: {
274
+ title?: string;
275
+ description?: string;
276
+ type?: string;
277
+ url?: string;
278
+ image?: string;
279
+ site_name?: string;
280
+ };
281
+ twitter?: {
282
+ card?: string;
283
+ title?: string;
284
+ description?: string;
285
+ image?: string;
286
+ };
287
+ focus_keyword?: string;
288
+ /** Schema.org types, and the JSON-LD graph when the plugin rendered it. */
289
+ schema?: {
290
+ types: string[];
291
+ graph?: unknown;
292
+ };
293
+ /** What the plugin stored for this page (templates such as `%%title%% %%sep%%`), secrets removed. */
294
+ stored?: Record<string, unknown>;
295
+ }
296
+ /** A provider's site-wide settings. */
297
+ interface RawSeoSettings {
298
+ separator?: string | null;
299
+ /** Keyed by context: `post`, `page`, `home`, `author`, `archive`, `tax:<taxonomy>`, `ptarchive-<type>`, … */
300
+ title_templates: Record<string, string>;
301
+ description_templates: Record<string, string>;
302
+ noindex?: Record<string, boolean>;
303
+ social?: Record<string, unknown>;
304
+ /** Site verification codes — public meta values. */
305
+ verification?: Record<string, string>;
306
+ /** Option name → cleaned value, verbatim otherwise. */
307
+ raw: Record<string, unknown>;
308
+ }
309
+ /**
310
+ * The site's SEO layer: which plugin serves the head, its settings, and each
311
+ * page's values. `status: 'none'` is an answer — no SEO plugin, and WordPress
312
+ * core renders only the title — not a missing export.
313
+ */
314
+ interface RawSeo {
315
+ /** Producer's format identifier, e.g. `contentrain-bridge-seo@1`. */
316
+ format?: string;
317
+ status: 'present' | 'none';
318
+ /** Whose output the live site serves. */
319
+ serving: SeoProvider | 'wordpress-core';
320
+ providers: Record<SeoProvider, {
321
+ status: 'active' | 'inactive-with-data' | 'absent';
322
+ version?: string;
323
+ }>;
324
+ settings: Partial<Record<SeoProvider, RawSeoSettings>>;
325
+ /** `post:<id>` or `term:<taxonomy>:<id>` → one block per provider that has data for it. */
326
+ entries: Record<string, Partial<Record<SeoProvider, RawSeoEntry>>>;
327
+ /** Values deliberately not exported (e.g. a sensitive key), by address. */
328
+ excluded?: {
329
+ source: string;
330
+ reason: string;
331
+ }[];
332
+ }
333
+ /** A page WordPress routes to itself (front page, posts page). */
334
+ interface RawRoutedPage {
335
+ id: number;
336
+ slug: string;
337
+ /** Site-root-relative address. */
338
+ path: string;
339
+ }
340
+ /**
341
+ * The site's URL rules — what `RawPost.link` holds as facts, stated as the
342
+ * structure that produced them. Permastructs are site-root-relative with
343
+ * `front` already applied where `with_front` asks for it.
344
+ */
345
+ interface RawRouting {
346
+ /** Producer's format identifier, e.g. `contentrain-bridge-routing@1`. */
347
+ format?: string;
348
+ /** The site's home URL. */
349
+ home?: string;
350
+ permalink_structure: string;
351
+ /** Plain `?p=` permalinks — no structure at all. */
352
+ plain: boolean;
353
+ trailing_slash: boolean;
354
+ front: string;
355
+ category_base: string;
356
+ tag_base: string;
357
+ pagination_base: string;
358
+ author_base: string;
359
+ search_base: string;
360
+ feed_base?: string;
361
+ comments_pagination_base?: string;
362
+ author_structure?: string | null;
363
+ date_structure?: string | null;
364
+ page_structure?: string | null;
365
+ /** `posts` or `page`. */
366
+ show_on_front: string;
367
+ page_on_front?: RawRoutedPage | null;
368
+ page_for_posts?: RawRoutedPage | null;
369
+ posts_per_page: number;
370
+ post_types: {
371
+ name: string;
372
+ hierarchical: boolean;
373
+ rewrite: {
374
+ slug?: string;
375
+ with_front?: boolean;
376
+ feeds?: boolean;
377
+ pages?: boolean;
378
+ } | false;
379
+ permastruct: string | null;
380
+ has_archive: boolean;
381
+ archive_path: string | null;
382
+ query_var: string | false;
383
+ }[];
384
+ taxonomies: {
385
+ name: string;
386
+ object_types: string[];
387
+ hierarchical: boolean;
388
+ rewrite: {
389
+ slug?: string;
390
+ with_front?: boolean;
391
+ hierarchical?: boolean;
392
+ } | false;
393
+ permastruct: string | null;
394
+ }[];
395
+ }
396
+ /** Where a candidate was found. */
397
+ declare const TEXT_CANDIDATE_KINDS: readonly ["php-gettext", "php-html", "php-echo", "html", "js", "widget", "menu", "option", "customizer", "render"];
398
+ type RawTextCandidateKind = (typeof TEXT_CANDIDATE_KINDS)[number];
399
+ /** Why a candidate is not transferred. `RawTextCandidate.reason` joins one or more with `,`. */
400
+ declare const TEXT_EXCLUDE_REASONS: readonly ["empty", "too-long", "secret", "code", "url", "number", "no-letters", "placeholder-only", "dynamic", "not-text", "content", "rendered-from-source"];
401
+ type RawTextExcludeReason = (typeof TEXT_EXCLUDE_REASONS)[number];
402
+ /** Where a transferred candidate lands in the store. */
403
+ type RawTextTarget = 'dictionary:ui-strings' | `theme-settings.${string}` | 'site.title' | 'site.description' | 'content:wp-menu-items';
404
+ /** One place a candidate's text was found. */
405
+ interface RawTextOccurrence {
406
+ kind: RawTextCandidateKind;
407
+ /** A file path relative to `wp-content`, `render:<state>`, `nav_menu_item#<id>`, … */
408
+ source: string;
409
+ /** 1-based line in a file; `0` for a source without lines (render, menu, option). */
410
+ line: number;
411
+ }
412
+ /**
413
+ * One piece of interface text in one place. Candidates merge only when text,
414
+ * locale and context are all equal: "Read more" in an `a` and in a `button`
415
+ * are two candidates, and so are `_x('Post', 'noun')` and `_x('Post', 'verb')`.
416
+ */
417
+ interface RawTextCandidate {
418
+ /** `sha256(text \0 locale \0 context)`, first 20 hex characters. */
419
+ id: string;
420
+ /** The text; `[redacted]` when `reason` includes `secret`. */
421
+ value: string;
422
+ locale: string;
423
+ /**
424
+ * Where the text sits: an HTML tag (`a`, `input@placeholder`),
425
+ * `gettext[:<ctx>][:plural]`, `js`, `widget:<tag>` / `widget:title`, `menu`,
426
+ * `option:<name>`, `customizer:<mod>`, or for rendered text
427
+ * `<landmark>><tag>` (`footer>p`, `nav>nav@aria-label`).
428
+ */
429
+ context: string;
430
+ kind: RawTextCandidateKind;
431
+ /** The first occurrence's `source` and `line`. */
432
+ source: string;
433
+ line: number;
434
+ /** Every place the text was found, in the producer's listing order. */
435
+ occurrences: RawTextOccurrence[];
436
+ outcome: 'transfer' | 'exclude';
437
+ /** Set exactly when `outcome` is `transfer`. */
438
+ target?: RawTextTarget;
439
+ /** Set exactly when `outcome` is `exclude`: one or more `RawTextExcludeReason`s joined with `,`. */
440
+ reason?: string;
441
+ /**
442
+ * With `rendered-from-source`: ids of the source candidates this page text
443
+ * came from. Page text found in source is excluded here and transferred
444
+ * there, so the same words never land twice and contexts are not merged.
445
+ */
446
+ related?: string[];
447
+ /**
448
+ * The store key: `<group>.<context>.<words>-<sha256(text \0 context)[0:6]>`,
449
+ * or `theme-settings.<mod>` for a Customizer value. A function of text and
450
+ * context only — moving a string to another file changes no key. A
451
+ * collision fails the export rather than merging two texts.
452
+ */
453
+ key: string;
454
+ decision: 'review' | 'include' | 'exclude';
455
+ }
456
+ /**
457
+ * Every piece of interface text a site shows, each with one outcome. Totals
458
+ * close: `by_outcome.transfer + by_outcome.exclude = candidates`,
459
+ * `by_outcome.error = errors.length`, and `occurrences` counts every
460
+ * occurrence the scanners found — listed ones plus those past the per-file
461
+ * listing cap, which are counted by reason instead.
462
+ */
463
+ interface RawHardcodedText {
464
+ /** Producer's format identifier, e.g. `contentrain-bridge-hardcoded-text@1`. */
465
+ format: string;
466
+ candidates: RawTextCandidate[];
467
+ /**
468
+ * Sources that could not be read — an outcome, not a silent gap. Reasons:
469
+ * `source-over-2MiB`, `source-unreadable`, `render-fetch-failed: <code>`,
470
+ * `render-http-<status>`, `render-no-body`.
471
+ */
472
+ errors: {
473
+ source: string;
474
+ reason: string;
475
+ }[];
476
+ totals: {
477
+ occurrences: number;
478
+ occurrences_listed: number; /** Occurrences past the listing cap: excluded and counted, not listed. */
479
+ unlisted_excluded: number;
480
+ unlisted_by_reason: Record<string, number>;
481
+ candidates: number;
482
+ by_outcome: {
483
+ transfer: number;
484
+ exclude: number;
485
+ error: number;
486
+ };
487
+ sources: {
488
+ files: number;
489
+ settings: number;
490
+ render_states: number;
491
+ };
492
+ };
493
+ }
494
+ declare const INTEGRATION_CATEGORIES: readonly ["analytics", "crm", "newsletter", "ads", "comments", "captcha", "cdn", "other"];
495
+ type IntegrationCategory = (typeof INTEGRATION_CATEGORIES)[number];
496
+ declare const INTEGRATION_EVIDENCE_KINDS: readonly ["plugin", "option-key", "script-domain", "form-config", "embed-domain"];
497
+ type IntegrationEvidenceKind = (typeof INTEGRATION_EVIDENCE_KINDS)[number];
498
+ /** One outside service the site uses, and why we believe it does. */
499
+ interface RawIntegration {
500
+ /** Catalog id: `mailchimp`, `hubspot`, `google-tag-manager`, `jetpack-stats`, `embed-youtube`, … */
501
+ service: string;
502
+ /** Display name. */
503
+ name: string;
504
+ category: IntegrationCategory;
505
+ /**
506
+ * Never a value. `detail` names a plugin and its version, a setting by name
507
+ * (`mc4wp (api_key)`), a script host and where it was seen
508
+ * (`static.hotjar.com @ render:home`), a form's wiring, or an embed host.
509
+ */
510
+ evidence: {
511
+ kind: IntegrationEvidenceKind;
512
+ detail: string;
513
+ }[];
514
+ /** An account must be connected again on the new site; `false` for an embed, which has no account. */
515
+ reconnect_required: boolean;
516
+ /** A credential is configured on WordPress. Only this boolean is exported, never the value. */
517
+ secret_present: boolean;
518
+ /** What to do, in words for the person receiving the site. */
519
+ notes: string;
520
+ }
521
+ /**
522
+ * The document a bridge writes (`bridge/integrations.json`). `RawIR.integrations`
523
+ * carries its `services`. An empty `services` with `scanned` beside it is an
524
+ * answer — the site uses no outside service we recognise — not a missing scan.
525
+ */
526
+ interface RawIntegrationScan {
527
+ /** Producer's format identifier, e.g. `contentrain-bridge-integrations@1`. */
528
+ format: string;
529
+ /** How much was looked at: option and post counts, active plugins, script sources, and whether the home page was rendered. */
530
+ scanned: {
531
+ options: number;
532
+ plugins: number;
533
+ posts: number;
534
+ rendered_home: boolean;
535
+ script_sources: number;
536
+ };
537
+ services: RawIntegration[];
538
+ totals: {
539
+ services: number;
540
+ reconnect_required: number;
541
+ };
542
+ }
543
+ /**
544
+ * The intake / handoff issue for services that must be connected again. Raised
545
+ * once, listing every `RawIntegration` with `reconnect_required: true`; not
546
+ * raised when there are none.
547
+ */
548
+ interface IntegrationReconnectRequiredIssue {
549
+ code: 'integration_reconnect_required';
550
+ services: Pick<RawIntegration, 'service' | 'name' | 'category' | 'secret_present'>[];
225
551
  }
226
552
  /**
227
553
  * Translation grouping for multilingual sites. The REST rungs read it from
@@ -250,6 +576,14 @@ interface RawIR {
250
576
  menus?: RawMenu[];
251
577
  comments?: RawComment[];
252
578
  redirects?: RawRedirect[];
579
+ /** Rules the sources hold that the site does not serve as plain redirects. */
580
+ redirects_excluded?: RawRedirectExcluded[];
581
+ seo?: RawSeo;
582
+ routing?: RawRouting;
583
+ /** Interface text outside the content tables, each piece with one outcome. */
584
+ hardcoded_text?: RawHardcodedText;
585
+ /** Outside services the site is connected to (`RawIntegrationScan.services`). */
586
+ integrations?: RawIntegration[];
253
587
  language_pairs?: RawLanguagePair[];
254
588
  /** Site options (bridge rung), verbatim. */
255
589
  options?: Record<string, unknown>;
@@ -760,6 +1094,22 @@ interface HandoffOffer {
760
1094
  /** e.g. "keeping comments on WordPress means the WordPress server stays live". */
761
1095
  warning?: string;
762
1096
  }
1097
+ /**
1098
+ * What a standalone page's body is, as far as the migration can carry it.
1099
+ *
1100
+ * - `html` — markup written in the editor. It travels as content and stays
1101
+ * editable.
1102
+ * - `builder_html` — markup a page builder rendered (the builder's own data
1103
+ * is not HTML). It travels as it was rendered: the page looks right, but its
1104
+ * layout is no longer edited in the builder that made it.
1105
+ * - `none` — no body; the theme renders the page from other data. Nothing to
1106
+ * carry as content.
1107
+ *
1108
+ * Counted because they promise different things to the person receiving the
1109
+ * site, and a single "pages migrated" number hides which promise was made.
1110
+ */
1111
+ declare const PAGE_BODY_KINDS: readonly ["html", "builder_html", "none"];
1112
+ type PageBodyKind = (typeof PAGE_BODY_KINDS)[number];
763
1113
  interface MigrationHandoff {
764
1114
  version: number;
765
1115
  site_url: string;
@@ -776,6 +1126,11 @@ interface MigrationHandoff {
776
1126
  models: number;
777
1127
  entries: number;
778
1128
  locales?: string[];
1129
+ /**
1130
+ * Standalone pages by body kind — see `PageBodyKind`. Every kind is present
1131
+ * when the field is, so a zero is a count, not an omission.
1132
+ */
1133
+ body_kinds?: Record<PageBodyKind, number>;
779
1134
  };
780
1135
  capabilities: HandoffCapability[];
781
1136
  /** Present whenever the source had comments — see `HandoffComments`. */
@@ -1224,11 +1579,51 @@ interface SourceDeltaEntry {
1224
1579
  /** Origin content fingerprint at cursor time and now. */
1225
1580
  fingerprint_before?: string;
1226
1581
  fingerprint_after?: string;
1227
- /** For `moved`: the slugs, in that order. Redirects are generated from these. */
1582
+ /** For `moved`: the slugs, in that order. */
1228
1583
  slug_before?: string;
1229
1584
  slug_after?: string;
1585
+ /**
1586
+ * For `moved`: the site-root-relative addresses, in that order. A slug
1587
+ * cannot say that a page changed parent, a term base changed or a dated
1588
+ * permalink moved; the path can, and redirects are generated from it.
1589
+ */
1590
+ path_before?: string;
1591
+ path_after?: string;
1592
+ /**
1593
+ * For `deleted`: `trashed` is still at the origin and can come back;
1594
+ * `purged` is gone from it. A draft or private record is `updated`, never
1595
+ * `deleted` — unpublishing is the planner's decision.
1596
+ */
1597
+ deleted_kind?: 'trashed' | 'purged';
1230
1598
  /** True when the repository also changed this record since the cursor. */
1231
1599
  conflict?: boolean;
1600
+ /**
1601
+ * The repository-side write behind `conflict`: the entry's meta when it was
1602
+ * last written by something other than the importer (Studio, an agent, a
1603
+ * person). `updated_at` is absent when the store never recorded it — an
1604
+ * unknown time is still a conflict.
1605
+ */
1606
+ repo_edit?: {
1607
+ updated_by: string;
1608
+ updated_at?: string;
1609
+ source: string;
1610
+ };
1611
+ /**
1612
+ * Why the planner could not place this record in the store:
1613
+ * `not-in-source-map` (a post-type record the source map does not list),
1614
+ * `no-model-for-type` (no store model holds this origin type),
1615
+ * `entry-not-found` (the model exists, the entry does not).
1616
+ */
1617
+ unmapped?: 'not-in-source-map' | 'no-model-for-type' | 'entry-not-found';
1618
+ /**
1619
+ * For `updated` and `moved`: the fields whose stored value differs from the
1620
+ * incoming export's, by field name (`body` for a document's body). Computed
1621
+ * against the repository as it is now, so a repository-side edit shows here
1622
+ * too — which is what `conflict` is for.
1623
+ */
1624
+ fields_changed?: string[];
1625
+ /** For `moved`: the entry id at the new address, when the store derives ids from slugs and the id changes. */
1626
+ entry_id_after?: string;
1232
1627
  /** Why this entry is here, for the person reading the plan. */
1233
1628
  detail?: string;
1234
1629
  }
@@ -1253,6 +1648,12 @@ interface SourceDeltaPlan {
1253
1648
  * "nothing was deleted".
1254
1649
  */
1255
1650
  deletions_detectable: boolean;
1651
+ /**
1652
+ * Origin types whose deletions could not be determined even though the plan
1653
+ * as a whole could — a type that left the inventory's scope (its plugin was
1654
+ * deactivated) is not a deletion of every record in it.
1655
+ */
1656
+ deletions_undetectable_types?: string[];
1256
1657
  /** Redirects implied by the `moved` entries. */
1257
1658
  redirects?: {
1258
1659
  from: string;
@@ -1286,6 +1687,67 @@ declare function planHashPayload(plan: ExecutionPlan): string;
1286
1687
  * value, so a collision is an approval bypass, not a cache miss.
1287
1688
  */
1288
1689
  declare function computePlanHash(plan: ExecutionPlan): Promise<string>;
1690
+ /** One origin record as the inventory saw it. */
1691
+ interface SourceInventoryRecord {
1692
+ /** Origin object type ("post", "page", "attachment", or a taxonomy name for a term). */
1693
+ wp_type: string;
1694
+ /** Origin id — unique only together with `wp_type`: post 5 and category 5 are different records. */
1695
+ wp_id: number;
1696
+ /** Origin status (`publish`, `draft`, `trash`, …); a term has none. */
1697
+ status?: string;
1698
+ /**
1699
+ * Password-protected at the origin. The record is inventoried like any other
1700
+ * — so removing the password is an `updated`, not a `created` — but has no
1701
+ * public address.
1702
+ */
1703
+ protected?: boolean;
1704
+ slug?: string;
1705
+ /** Site-root-relative public address. A change here is a `moved`. */
1706
+ path?: string;
1707
+ /** Origin id of the parent record, when it has one. */
1708
+ parent?: number;
1709
+ /** ISO 8601 UTC, from the origin — a hint only: a meta-only edit does not change it. */
1710
+ modified_at?: string;
1711
+ /**
1712
+ * Hash of the record as mapped, volatile fields excluded. `updated` is
1713
+ * decided on this, never on `modified_at`.
1714
+ */
1715
+ fingerprint: string;
1716
+ /** Slugs the origin remembers for this record (WordPress `_wp_old_slug`). Corroborates a move; never the authority. */
1717
+ old_slugs?: string[];
1718
+ /** Locale of the record, when the origin is multilingual. */
1719
+ locale?: string;
1720
+ }
1721
+ /**
1722
+ * Every record in scope at one moment — what a `bridge_inventory` cursor's
1723
+ * `inventory_hash` identifies.
1724
+ *
1725
+ * Deletions and moves are proven by comparing two inventories, not by
1726
+ * reading a feed; a `modified_after` query only narrows which bodies to fetch
1727
+ * again. A type in the earlier inventory's `scope` but not in this one's left
1728
+ * the scope — it was not deleted.
1729
+ */
1730
+ interface SourceInventory {
1731
+ /** Inventory format identifier, e.g. `contentrain-bridge-inventory@2`. */
1732
+ format: string;
1733
+ /** ISO 8601 UTC time the inventory was taken. */
1734
+ taken_at: string;
1735
+ /** What was enumerated. */
1736
+ scope: {
1737
+ post_types: string[];
1738
+ taxonomies?: string[];
1739
+ };
1740
+ records: SourceInventoryRecord[];
1741
+ /**
1742
+ * The value a cursor's `inventory_hash` carries: lowercase hex SHA-256 of the
1743
+ * records sorted by `wp_type` (byte order) then `wp_id` (numeric), each
1744
+ * serialized as the JSON array `[wp_type, wp_id, fingerprint, path ?? null,
1745
+ * status ?? null]` — no spaces, slashes and non-ASCII unescaped (what
1746
+ * `JSON.stringify` prints) — joined by `\n`, no trailing newline. Other fields
1747
+ * do not enter it; a change they make shows through `fingerprint`.
1748
+ */
1749
+ inventory_hash: string;
1750
+ }
1289
1751
  //#endregion
1290
1752
  //#region src/approval.d.ts
1291
1753
  /**
@@ -1881,6 +2343,23 @@ interface ModelDefinition {
1881
2343
  kind: ModelKind;
1882
2344
  domain: string;
1883
2345
  i18n: boolean;
2346
+ /**
2347
+ * The project locales this model's content actually covers — a subset of
2348
+ * `config.locales.supported`.
2349
+ *
2350
+ * Absent means every project locale, which is the behaviour every model had
2351
+ * before this field existed: parity is checked against the whole supported
2352
+ * list. Present, it narrows that check to the locales named here, so a
2353
+ * partially-translated site can declare the truth instead of failing.
2354
+ *
2355
+ * A locale not in `config.locales.supported` is a validation error — see
2356
+ * {@link validateModelLocales}. Meaningless on a non-i18n model, which has
2357
+ * only the default locale.
2358
+ *
2359
+ * It narrows validation; it never grants a locale. Writing content in a
2360
+ * locale outside this list is still governed by `config.locales.supported`.
2361
+ */
2362
+ locales?: string[];
1884
2363
  /**
1885
2364
  * Name of the field whose value is shown as an entry's title — in a Studio row,
1886
2365
  * a picker, a relation reference. Declared, never guessed: a consumer that infers
@@ -2130,7 +2609,7 @@ declare const CANONICAL_JSON: {
2130
2609
  * the `keyof` assertion in index.test.ts catches a `ModelDefinition` key missing
2131
2610
  * from this array. Between them, drift fails `tsc`, which runs on every commit.
2132
2611
  */
2133
- declare const MODEL_FIELD_ORDER: readonly ["id", "name", "kind", "domain", "i18n", "title_field", "description", "content_path", "locale_strategy", "fields", "form", "comments"];
2612
+ declare const MODEL_FIELD_ORDER: readonly ["id", "name", "kind", "domain", "i18n", "locales", "title_field", "description", "content_path", "locale_strategy", "fields", "form", "comments"];
2134
2613
  interface ScaffoldTemplate {
2135
2614
  id: string;
2136
2615
  models: ModelDefinition[];
@@ -2316,6 +2795,51 @@ declare function validateSlug(slug: string): string | null;
2316
2795
  declare function validateEntryId(id: string): string | null;
2317
2796
  /** Validate a locale code against pattern and config — returns error message or null if valid */
2318
2797
  declare function validateLocale(locale: string, config: ContentrainConfig): string | null;
2798
+ /**
2799
+ * The locales one model's content is checked against, and where that list came
2800
+ * from — `'model'` when the model narrowed it with `locales`, `'project'` when
2801
+ * it is the project-wide list (or, for a non-i18n model, the default locale).
2802
+ *
2803
+ * The provenance travels with the list because every message derived from it
2804
+ * has to say which list it evaluated: "missing tr" means something different
2805
+ * when tr is one of four project locales than when the model declares it.
2806
+ */
2807
+ interface ModelLocaleScope {
2808
+ locales: string[];
2809
+ source: 'model' | 'project';
2810
+ }
2811
+ /**
2812
+ * The locales a model's content is expected to cover.
2813
+ *
2814
+ * A non-i18n model stores content locale-agnostically, so its scope is the
2815
+ * default locale alone — unchanged, and `locales` never applies to it. An i18n
2816
+ * model narrows to `model.locales` when it declares one, otherwise it covers
2817
+ * every supported locale, which is what every model did before the field
2818
+ * existed.
2819
+ *
2820
+ * Locales are returned in `config.locales.supported` order, not the order the
2821
+ * model happened to list them: a message that names the list should read the
2822
+ * same for two models that declare the same locales differently. Anything the
2823
+ * model names that the project does not support is dropped here and reported
2824
+ * by {@link validateModelLocales} — resolution never invents coverage.
2825
+ */
2826
+ declare function resolveModelLocales(model: Pick<ModelDefinition, 'i18n' | 'locales'>, config: ContentrainConfig): ModelLocaleScope;
2827
+ /** Name a locale scope the way a validation message should quote it. */
2828
+ declare function describeModelLocaleScope(scope: ModelLocaleScope): string;
2829
+ /**
2830
+ * Check a model's `locales` declaration against the project config.
2831
+ *
2832
+ * Absent is valid and means every supported locale, so this returns nothing for
2833
+ * the models that predate the field. What it rejects is a declaration that
2834
+ * cannot be honoured: a locale the project does not support (the coverage would
2835
+ * be unreachable), an empty list (a model that covers nothing is a deleted
2836
+ * model, not a narrowed one), a duplicate, or a non-string entry.
2837
+ *
2838
+ * Declaring `locales` on a non-i18n model is a warning, not an error: the field
2839
+ * has no effect there, but the model is still readable and its content still
2840
+ * validates, so failing the project over it would be out of proportion.
2841
+ */
2842
+ declare function validateModelLocales(model: Pick<ModelDefinition, 'i18n' | 'locales'>, config: ContentrainConfig): ValidationError[];
2319
2843
  /**
2320
2844
  * Does a value assigned to an API-key setting look like a credential rather
2321
2845
  * than prose about one?
@@ -2462,5 +2986,5 @@ declare function parseMarkdownFrontmatter(content: string): {
2462
2986
  /** Serialize data + body into markdown frontmatter format */
2463
2987
  declare function serializeMarkdownFrontmatter(data: Record<string, unknown>, body: string): string;
2464
2988
  //#endregion
2465
- export { APPROVAL_GATES, APPROVAL_MODES, ActorRef, type ApplyPlanInput, ApprovalDecision, ApprovalEvaluation, ApprovalGate, ApprovalGrant, ApprovalMode, ApprovalPolicyFile, ApprovalRequirement, ApprovalRule, ApprovalStatus, AssetEntry, AutomationDefinition, AutomationSchedule, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, Checkpoint, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, CommentsExport, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, DeploymentTarget, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, EntryMeta, EntrySourceMap, EntrySourceRef, ExcerptSource, ExecutionCost, ExecutionEstimate, ExecutionPlan, ExecutionReceipt, ExecutionScope, ExecutionStep, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GrantRejection, GraphNode, HandoffCapability, HandoffComments, HandoffOffer, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, MisfirePolicy, ModelDefinition, ModelKind, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PATH_PATTERNS, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, ProjectRef, type ProviderCapabilities, QueryBinding, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawIR, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawSite, RawTerm, RawTermRef, RejectedGrant, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RiskClass, RollbackPlan, RootAttrs, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, RunStatus, RuntimeBinding, SCHEDULE_KINDS, SECRET_PATTERNS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, ScheduleKind, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, SourceCursor, SourceDeltaEntry, SourceDeltaOp, SourceDeltaPlan, StackType, StringContext, SyncResult, TERMINAL_RUN_STATUSES, TITLE_FIELD_TYPES, ValidationError, ValidationResult, VerificationCheck, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, approversFor, canonicalStringify, componentSlot, computePlanHash, conflictId, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isReservedPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateSemanticType, validateSlug };
2989
+ export { APPROVAL_GATES, APPROVAL_MODES, ActorRef, type ApplyPlanInput, ApprovalDecision, ApprovalEvaluation, ApprovalGate, ApprovalGrant, ApprovalMode, ApprovalPolicyFile, ApprovalRequirement, ApprovalRule, ApprovalStatus, AssetEntry, AutomationDefinition, AutomationSchedule, type BaseAdvance, type Branch, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, CapabilityDetection, CapabilityDisposition, CapabilityKey, CapabilityManifest, Checkpoint, ChromeChunk, CollectionContentFile, CollectionContentOutput, CollectionEntry, CollectionMeta, CollectionRuntimeFormat, CommentsExport, type Commit, type CommitAuthor, ComponentDef, ComponentPlacement, ComponentSource, ComponentType, ComponentVariantDef, type ConflictCode, type ConflictItem, type ConflictResolution, ContentSource, ContentStatus, ContentrainConfig, ContentrainError, ContextJson, ContextSource, CostComparison, CostEstimate, CssStrategy, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, DeploymentTarget, DesignTokens, DictionaryContentFile, DictionaryMeta, DocumentContentOutput, DocumentEntry, DocumentMeta, DuplicateGroup, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, EntryMeta, EntrySourceMap, EntrySourceRef, ExcerptSource, ExecutionCost, ExecutionEstimate, ExecutionPlan, ExecutionReceipt, ExecutionScope, ExecutionStep, FamilyVariant, FieldDef, FieldType, FileCategory, type FileChange, type FileDiff, FileFramework, GrantRejection, GraphNode, HandoffCapability, HandoffComments, HandoffOffer, INTEGRATION_CATEGORIES, INTEGRATION_EVIDENCE_KINDS, IntegrationCategory, IntegrationEvidenceKind, IntegrationReconnectRequiredIssue, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, LayoutFamily, LocaleStrategy, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, type MediaAsset, type MediaIngestInput, type MediaListOptions, type MediaListResult, type MediaProvider, type MediaUpdateInput, type MergeResult, MigrationHandoff, MisfirePolicy, ModelDefinition, ModelKind, ModelLocaleScope, ModelSummary, NormalizePlan, NormalizePlanExtraction, NormalizePlanModel, NormalizePlanPatch, OfferProvider, PAGE_BODY_KINDS, PATH_PATTERNS, PageBodyKind, PaginationKind, Platform, PolymorphicRelationRef, ProjectGraph, ProjectIR, ProjectRef, type ProviderCapabilities, QueryBinding, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, RawAcfValue, RawAttachment, RawAuthor, RawComment, RawHardcodedText, RawIR, RawIntegration, RawIntegrationScan, RawLanguagePair, RawMenu, RawMenuItem, RawMenuTarget, RawPost, RawProvenance, RawRedirect, RawRedirectExcluded, RawRedirectMatch, RawRoutedPage, RawRouting, RawSeo, RawSeoEntry, RawSeoSettings, RawSite, RawTerm, RawTermRef, RawTextCandidate, RawTextCandidateKind, RawTextExcludeReason, RawTextOccurrence, RawTextTarget, RejectedGrant, type RemotePush, type RepoProvider, type RepoReader, type RepoWriter, RiskClass, RollbackPlan, RootAttrs, RouteKind, RouteModel, RouteParamDef, RouteVariantRule, RunStatus, RuntimeBinding, SCHEDULE_KINDS, SECRET_PATTERNS, SEO_PROVIDERS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, ScaffoldTemplate, ScanCandidate, ScanCandidatesResult, ScanSummaryResult, ScheduleKind, SeoProvider, SingletonContentFile, SingletonMeta, SlotBinding, SlotKind, SourceAccessKind, SourceCursor, SourceDeltaEntry, SourceDeltaOp, SourceDeltaPlan, SourceInventory, SourceInventoryRecord, StackType, StringContext, SyncResult, TERMINAL_RUN_STATUSES, TEXT_CANDIDATE_KINDS, TEXT_EXCLUDE_REASONS, TITLE_FIELD_TYPES, ValidationError, ValidationResult, VerificationCheck, ViewportStrategy, Vocabulary, WorkflowMode, type WriteReadiness, approversFor, canonicalStringify, componentSlot, computePlanHash, conflictId, describeModelLocaleScope, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isReservedPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, resolveModelLocales, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, validateAccept, validateEntryId, validateFieldValue, validateLocale, validateModelLocales, validateSemanticType, validateSlug };
2466
2990
  //# sourceMappingURL=index.d.mts.map