@byline/db-postgres 2.6.1 → 3.0.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.
@@ -260,6 +260,25 @@ export declare const documents: import("drizzle-orm/pg-core").PgTableWithColumns
260
260
  }, {}, {
261
261
  pgColumnBuilderBrand: "PgCustomColumnBuilderBrand";
262
262
  }>;
263
+ source_locale: import("drizzle-orm/pg-core").PgColumn<{
264
+ name: "source_locale";
265
+ tableName: "byline_documents";
266
+ dataType: "string";
267
+ columnType: "PgVarchar";
268
+ data: string;
269
+ driverParam: string;
270
+ notNull: true;
271
+ hasDefault: false;
272
+ isPrimaryKey: false;
273
+ isAutoincrement: false;
274
+ hasRuntimeDefault: false;
275
+ enumValues: [string, ...string[]];
276
+ baseColumn: never;
277
+ identity: undefined;
278
+ generated: undefined;
279
+ }, {}, {
280
+ length: 10;
281
+ }>;
263
282
  };
264
283
  dialect: "pg";
265
284
  }>;
@@ -591,6 +610,143 @@ export declare const documentPaths: import("drizzle-orm/pg-core").PgTableWithCol
591
610
  };
592
611
  dialect: "pg";
593
612
  }>;
613
+ export declare const documentAvailableLocales: import("drizzle-orm/pg-core").PgTableWithColumns<{
614
+ name: "byline_document_available_locales";
615
+ schema: undefined;
616
+ columns: {
617
+ created_at: import("drizzle-orm/pg-core").PgColumn<{
618
+ name: string;
619
+ tableName: "byline_document_available_locales";
620
+ dataType: "date";
621
+ columnType: "PgTimestamp";
622
+ data: Date;
623
+ driverParam: string;
624
+ notNull: true;
625
+ hasDefault: true;
626
+ isPrimaryKey: false;
627
+ isAutoincrement: false;
628
+ hasRuntimeDefault: false;
629
+ enumValues: undefined;
630
+ baseColumn: never;
631
+ identity: undefined;
632
+ generated: undefined;
633
+ }, {}, {}>;
634
+ updated_at: import("drizzle-orm/pg-core").PgColumn<{
635
+ name: string;
636
+ tableName: "byline_document_available_locales";
637
+ dataType: "date";
638
+ columnType: "PgTimestamp";
639
+ data: Date;
640
+ driverParam: string;
641
+ notNull: true;
642
+ hasDefault: true;
643
+ isPrimaryKey: false;
644
+ isAutoincrement: false;
645
+ hasRuntimeDefault: false;
646
+ enumValues: undefined;
647
+ baseColumn: never;
648
+ identity: undefined;
649
+ generated: undefined;
650
+ }, {}, {}>;
651
+ document_id: import("drizzle-orm/pg-core").PgColumn<{
652
+ name: "document_id";
653
+ tableName: "byline_document_available_locales";
654
+ dataType: "string";
655
+ columnType: "PgUUID";
656
+ data: string;
657
+ driverParam: string;
658
+ notNull: true;
659
+ hasDefault: false;
660
+ isPrimaryKey: false;
661
+ isAutoincrement: false;
662
+ hasRuntimeDefault: false;
663
+ enumValues: undefined;
664
+ baseColumn: never;
665
+ identity: undefined;
666
+ generated: undefined;
667
+ }, {}, {}>;
668
+ locale: import("drizzle-orm/pg-core").PgColumn<{
669
+ name: "locale";
670
+ tableName: "byline_document_available_locales";
671
+ dataType: "string";
672
+ columnType: "PgVarchar";
673
+ data: string;
674
+ driverParam: string;
675
+ notNull: true;
676
+ hasDefault: false;
677
+ isPrimaryKey: false;
678
+ isAutoincrement: false;
679
+ hasRuntimeDefault: false;
680
+ enumValues: [string, ...string[]];
681
+ baseColumn: never;
682
+ identity: undefined;
683
+ generated: undefined;
684
+ }, {}, {
685
+ length: 10;
686
+ }>;
687
+ collection_id: import("drizzle-orm/pg-core").PgColumn<{
688
+ name: "collection_id";
689
+ tableName: "byline_document_available_locales";
690
+ dataType: "string";
691
+ columnType: "PgUUID";
692
+ data: string;
693
+ driverParam: string;
694
+ notNull: true;
695
+ hasDefault: false;
696
+ isPrimaryKey: false;
697
+ isAutoincrement: false;
698
+ hasRuntimeDefault: false;
699
+ enumValues: undefined;
700
+ baseColumn: never;
701
+ identity: undefined;
702
+ generated: undefined;
703
+ }, {}, {}>;
704
+ };
705
+ dialect: "pg";
706
+ }>;
707
+ export declare const documentVersionLocales: import("drizzle-orm/pg-core").PgTableWithColumns<{
708
+ name: "byline_document_version_locales";
709
+ schema: undefined;
710
+ columns: {
711
+ document_version_id: import("drizzle-orm/pg-core").PgColumn<{
712
+ name: "document_version_id";
713
+ tableName: "byline_document_version_locales";
714
+ dataType: "string";
715
+ columnType: "PgUUID";
716
+ data: string;
717
+ driverParam: string;
718
+ notNull: true;
719
+ hasDefault: false;
720
+ isPrimaryKey: false;
721
+ isAutoincrement: false;
722
+ hasRuntimeDefault: false;
723
+ enumValues: undefined;
724
+ baseColumn: never;
725
+ identity: undefined;
726
+ generated: undefined;
727
+ }, {}, {}>;
728
+ locale: import("drizzle-orm/pg-core").PgColumn<{
729
+ name: "locale";
730
+ tableName: "byline_document_version_locales";
731
+ dataType: "string";
732
+ columnType: "PgVarchar";
733
+ data: string;
734
+ driverParam: string;
735
+ notNull: true;
736
+ hasDefault: false;
737
+ isPrimaryKey: false;
738
+ isAutoincrement: false;
739
+ hasRuntimeDefault: false;
740
+ enumValues: [string, ...string[]];
741
+ baseColumn: never;
742
+ identity: undefined;
743
+ generated: undefined;
744
+ }, {}, {
745
+ length: 10;
746
+ }>;
747
+ };
748
+ dialect: "pg";
749
+ }>;
594
750
  export declare const documentRelationships: import("drizzle-orm/pg-core").PgTableWithColumns<{
595
751
  name: "byline_document_relationships";
596
752
  schema: undefined;
@@ -854,6 +1010,23 @@ export declare const currentDocumentsView: import("drizzle-orm/pg-core").PgViewW
854
1010
  identity: undefined;
855
1011
  generated: undefined;
856
1012
  }, {}, {}>;
1013
+ source_locale: import("drizzle-orm/pg-core").PgColumn<{
1014
+ name: "source_locale";
1015
+ tableName: "byline_current_documents";
1016
+ dataType: "string";
1017
+ columnType: "PgVarchar";
1018
+ data: string;
1019
+ driverParam: string;
1020
+ notNull: true;
1021
+ hasDefault: false;
1022
+ isPrimaryKey: false;
1023
+ isAutoincrement: false;
1024
+ hasRuntimeDefault: false;
1025
+ enumValues: [string, ...string[]];
1026
+ baseColumn: never;
1027
+ identity: undefined;
1028
+ generated: undefined;
1029
+ }, {}, {}>;
857
1030
  }>;
858
1031
  export declare const currentPublishedDocumentsView: import("drizzle-orm/pg-core").PgViewWithSelection<"byline_current_published_documents", false, {
859
1032
  id: import("drizzle-orm/pg-core").PgColumn<{
@@ -1060,6 +1233,23 @@ export declare const currentPublishedDocumentsView: import("drizzle-orm/pg-core"
1060
1233
  identity: undefined;
1061
1234
  generated: undefined;
1062
1235
  }, {}, {}>;
1236
+ source_locale: import("drizzle-orm/pg-core").PgColumn<{
1237
+ name: "source_locale";
1238
+ tableName: "byline_current_published_documents";
1239
+ dataType: "string";
1240
+ columnType: "PgVarchar";
1241
+ data: string;
1242
+ driverParam: string;
1243
+ notNull: true;
1244
+ hasDefault: false;
1245
+ isPrimaryKey: false;
1246
+ isAutoincrement: false;
1247
+ hasRuntimeDefault: false;
1248
+ enumValues: [string, ...string[]];
1249
+ baseColumn: never;
1250
+ identity: undefined;
1251
+ generated: undefined;
1252
+ }, {}, {}>;
1063
1253
  }>;
1064
1254
  export declare const textStore: import("drizzle-orm/pg-core").PgTableWithColumns<{
1065
1255
  name: "byline_store_text";
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import { eq, relations, sql } from 'drizzle-orm';
9
- import { bigint, boolean, customType, date, decimal, index, integer, jsonb, pgTable, pgView, real, text, time, timestamp, unique, uuid, varchar, } from 'drizzle-orm/pg-core';
9
+ import { bigint, boolean, customType, date, decimal, index, integer, jsonb, pgTable, pgView, primaryKey, real, text, time, timestamp, unique, uuid, varchar, } from 'drizzle-orm/pg-core';
10
10
  import { createdAt, timestamps } from './common.js';
11
11
  /**
12
12
  * `varchar(...)` with explicit byte-wise (C) collation.
@@ -63,6 +63,16 @@ export const documents = pgTable('byline_documents', {
63
63
  // comparison — the fractional-index algorithm requires this. See
64
64
  // `varcharByteSorted` above and docs/COLLECTIONS.md (Orderable collections).
65
65
  order_key: varcharByteSorted('order_key', { length: 128 }),
66
+ // The content locale this document was first authored in — its per-document
67
+ // data anchor. Set once at creation (= the global default content locale at
68
+ // that moment) and immutable in normal operation; changed only by the
69
+ // deliberate re-anchor operation. Re-bases the fallback floor, the path
70
+ // locale, and the completeness ledger off the mutable global config onto
71
+ // the document's own truth, so switching `i18n.content.defaultLocale` no
72
+ // longer silently re-interprets existing data. Backfilled by
73
+ // `backfillSourceLocales()` (boot-auto via initBylineCore).
74
+ //
75
+ source_locale: varchar('source_locale', { length: 10 }).notNull(),
66
76
  ...timestamps,
67
77
  }, (table) => [
68
78
  index('idx_documents_collection').on(table.collection_id),
@@ -129,6 +139,46 @@ export const documentPaths = pgTable('byline_document_paths', {
129
139
  // Reverse lookup by document.
130
140
  index('idx_document_paths_document_id').on(table.document_id),
131
141
  ]);
142
+ // Document → advertised content locales. One row per (logical document,
143
+ // advertised locale) — the editorial "advertise these locales" set an editor
144
+ // curates per document. The deliberate counterpart to the derived,
145
+ // version-grained `byline_document_version_locales` ledger: this is intent
146
+ // ("I want these advertised"), the ledger is fact ("this version is complete
147
+ // in these"). Document-grain and sticky across versions — editorial intent
148
+ // carries forward across edits and survives restore. Surfaced on reads as
149
+ // `availableLocales`; the public advertised set is the intersection with the
150
+ // ledger's `_availableVersionLocales`. Replaced wholesale on write (the lifecycle
151
+ // deletes then re-inserts the set), never appended. See docs/AVAILABLE-LOCALES.md.
152
+ export const documentAvailableLocales = pgTable('byline_document_available_locales', {
153
+ document_id: uuid('document_id')
154
+ .notNull()
155
+ .references(() => documents.id, { onDelete: 'cascade' }),
156
+ locale: varchar('locale', { length: 10 }).notNull(),
157
+ collection_id: uuid('collection_id')
158
+ .notNull()
159
+ .references(() => collections.id, { onDelete: 'cascade' }),
160
+ ...timestamps,
161
+ }, (table) => [
162
+ // One row per (logical document, advertised locale).
163
+ primaryKey({ columns: [table.document_id, table.locale] }),
164
+ // Reverse lookup by document for the read projection.
165
+ index('idx_document_available_locales_document_id').on(table.document_id),
166
+ ]);
167
+ // Document version → available content locales. One row per (version, locale)
168
+ // for every locale the version's content is *complete* in — path-coverage
169
+ // against the default content locale: a locale is recorded only when it covers
170
+ // every localized field path the default locale has. A version with no
171
+ // localized content at all gets a single `'all'` sentinel row (it renders
172
+ // identically in any locale). Computed status-blind at write time and frozen
173
+ // on the immutable version, so restore / point-in-time reads stay consistent.
174
+ // Drives `localeFallback: 'strict'` reads via an indexed EXISTS gate without
175
+ // scanning the store_* tables. See docs/CONTENT-LOCALE-RESOLUTION.md.
176
+ export const documentVersionLocales = pgTable('byline_document_version_locales', {
177
+ document_version_id: uuid('document_version_id')
178
+ .notNull()
179
+ .references(() => documentVersions.id, { onDelete: 'cascade' }),
180
+ locale: varchar('locale', { length: 10 }).notNull(),
181
+ }, (table) => [primaryKey({ columns: [table.document_version_id, table.locale] })]);
132
182
  // Document Relationships (Parent/Child) - Many-to-Many
133
183
  export const documentRelationships = pgTable('byline_document_relationships', {
134
184
  // Note: These reference the logical `document_id`, not the version `id`.
@@ -194,6 +244,12 @@ export const currentDocumentsView = pgView('byline_current_documents').as((qb) =
194
244
  created_by: sq.created_by,
195
245
  change_summary: sq.change_summary,
196
246
  order_key: documents.order_key,
247
+ // The document's content-locale anchor, projected here so locale-aware
248
+ // read paths (`buildLocaleChain` / `pathProjection` / field-fallback)
249
+ // re-base onto the per-document source rather than the mutable global
250
+ // default — a primary-key join, already present for `order_key`.
251
+ // See docs/DEFAULT-LOCALE-SWITCHING.md.
252
+ source_locale: documents.source_locale,
197
253
  })
198
254
  .from(sq)
199
255
  .innerJoin(documents, eq(documents.id, sq.document_id))
@@ -237,6 +293,9 @@ export const currentPublishedDocumentsView = pgView('byline_current_published_do
237
293
  created_by: sq.created_by,
238
294
  change_summary: sq.change_summary,
239
295
  order_key: documents.order_key,
296
+ // See `currentDocumentsView` — the per-document content-locale anchor,
297
+ // carried for locale-aware reads. PK join, already present.
298
+ source_locale: documents.source_locale,
240
299
  })
241
300
  .from(sq)
242
301
  .innerJoin(documents, eq(documents.id, sq.document_id))
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ import type { CollectionDefinition, IDbAdapter } from '@byline/core';
9
9
  import { type NodePgDatabase } from 'drizzle-orm/node-postgres';
10
10
  import pg from 'pg';
11
11
  import * as schema from './database/schema/index.js';
12
+ import { type ReAnchorReport, type ReAnchorResult } from './modules/storage/storage-commands.js';
13
+ export type { ReAnchorReport, ReAnchorResult, ReAnchorStatus, } from './modules/storage/storage-commands.js';
12
14
  /**
13
15
  * Public return type of `pgAdapter`. Extends `IDbAdapter` with concrete
14
16
  * Drizzle + pg handles so integrations that need the raw database (the
@@ -23,16 +25,65 @@ export interface PgAdapter extends IDbAdapter {
23
25
  drizzle: NodePgDatabase<typeof schema>;
24
26
  /** The pg connection pool — exposed for housekeeping and teardown. */
25
27
  pool: pg.Pool;
28
+ /**
29
+ * One-time maintenance: populate the version-locale availability ledger
30
+ * (`byline_document_version_locales`) for versions written before it
31
+ * existed, so `localeFallback: 'strict'` reads can see pre-existing
32
+ * documents. Idempotent; uses the configured default content locale. Kept
33
+ * off the core `IDbAdapter` contract (no service depends on it) — see
34
+ * docs/CONTENT-LOCALE-RESOLUTION.md.
35
+ */
36
+ backfillVersionLocales(): Promise<{
37
+ rowsInserted: number;
38
+ }>;
39
+ /**
40
+ * One-time maintenance: stamp `byline_documents.source_locale` for documents
41
+ * created before the column existed, setting NULL rows to the configured
42
+ * default content locale (the anchor they were implicitly authored against).
43
+ * Idempotent; run automatically at boot by `initBylineCore` (also exposed on
44
+ * the core `IDbAdapter` contract as an optional method) — see
45
+ * docs/DEFAULT-LOCALE-SWITCHING.md.
46
+ */
47
+ backfillSourceLocales(): Promise<{
48
+ rowsUpdated: number;
49
+ }>;
50
+ /**
51
+ * Re-anchor a single document's content source locale to `targetLocale`
52
+ * (its fallback floor, path locale, and completeness yardstick). Refuses
53
+ * unless the document is complete in the target. Writes a new immutable
54
+ * version. `dryRun` reports the would-be outcome without writing. Off the
55
+ * core `IDbAdapter` contract (maintenance/admin operation) — see
56
+ * docs/DEFAULT-LOCALE-SWITCHING.md.
57
+ */
58
+ reAnchorDocument(params: {
59
+ documentId: string;
60
+ targetLocale: string;
61
+ dryRun?: boolean;
62
+ }): Promise<ReAnchorResult>;
63
+ /**
64
+ * Bulk re-anchor every fully-translated document (optionally within one
65
+ * collection) onto `targetLocale`, skipping and reporting the rest. Each
66
+ * document is its own transaction — idempotent and resumable. This is the
67
+ * "switched the default content locale, move everything that's ready" command.
68
+ */
69
+ reAnchorDocuments(params: {
70
+ targetLocale: string;
71
+ collectionId?: string;
72
+ dryRun?: boolean;
73
+ }): Promise<ReAnchorReport>;
26
74
  }
27
75
  export declare const pgAdapter: ({ connectionString, collections, defaultContentLocale, max, idleTimeoutMillis, connectionTimeoutMillis, }: {
28
76
  connectionString: string;
29
77
  collections: CollectionDefinition[];
30
78
  /**
31
79
  * The installation's default content locale, sourced from
32
- * `ServerConfig.i18n.content.defaultLocale`. Used by the storage layer
33
- * for path resolution: read functions build a `[requested, default]`
34
- * fallback chain when looking up `byline_document_paths`, and write
35
- * functions tag default-locale path rows with this value.
80
+ * `ServerConfig.i18n.content.defaultLocale`. Used by the storage layer as
81
+ * the **fallback** anchor only: new documents are stamped with it as their
82
+ * `source_locale`, and it is the floor for row-less lookups (findByPath) and
83
+ * for documents whose `source_locale` is not yet backfilled. Per-document
84
+ * reads and writes otherwise re-base onto each document's own `source_locale`
85
+ * (carried on the current-documents views), so changing this value does not
86
+ * re-interpret existing data. See docs/DEFAULT-LOCALE-SWITCHING.md.
36
87
  */
37
88
  defaultContentLocale: string;
38
89
  /**
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { drizzle } from 'drizzle-orm/node-postgres';
9
9
  import pg from 'pg';
10
10
  import * as schema from './database/schema/index.js';
11
11
  import { createCounterCommands } from './modules/counters/counters-commands.js';
12
- import { createCommandBuilders } from './modules/storage/storage-commands.js';
12
+ import { createCommandBuilders, } from './modules/storage/storage-commands.js';
13
13
  import { createQueryBuilders } from './modules/storage/storage-queries.js';
14
14
  export const pgAdapter = ({ connectionString, collections, defaultContentLocale, max = 20, idleTimeoutMillis = 2000, connectionTimeoutMillis = 30000, }) => {
15
15
  const pool = new pg.Pool({
@@ -30,5 +30,9 @@ export const pgAdapter = ({ connectionString, collections, defaultContentLocale,
30
30
  queries: queryBuilders,
31
31
  drizzle: db,
32
32
  pool,
33
+ backfillVersionLocales: () => commandBuilders.documents.backfillVersionLocales(),
34
+ backfillSourceLocales: () => commandBuilders.documents.backfillSourceLocales(),
35
+ reAnchorDocument: (params) => commandBuilders.documents.reAnchorDocument(params),
36
+ reAnchorDocuments: (params) => commandBuilders.documents.reAnchorDocuments(params),
33
37
  };
34
38
  };
@@ -9,6 +9,29 @@ import type { CollectionDefinition, ICollectionCommands, IDocumentCommands } fro
9
9
  import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
10
10
  import type * as schema from '../../database/schema/index.js';
11
11
  type DatabaseConnection = NodePgDatabase<typeof schema>;
12
+ /** Outcome of re-anchoring a single document's content source locale. */
13
+ export type ReAnchorStatus = 'reanchored' | 'skipped-incomplete' | 'already-anchored' | 'not-found';
14
+ export interface ReAnchorResult {
15
+ documentId: string;
16
+ status: ReAnchorStatus;
17
+ /** The document's source locale before the operation (when known). */
18
+ fromLocale?: string;
19
+ /** The target source locale. */
20
+ toLocale: string;
21
+ /** The new version id written on a successful re-anchor. */
22
+ newVersionId?: string;
23
+ }
24
+ export interface ReAnchorReport {
25
+ targetLocale: string;
26
+ dryRun: boolean;
27
+ total: number;
28
+ reanchored: number;
29
+ skippedIncomplete: number;
30
+ alreadyAnchored: number;
31
+ notFound: number;
32
+ /** Per-document outcomes, for logging / inspection. */
33
+ results: ReAnchorResult[];
34
+ }
12
35
  /**
13
36
  * CollectionCommands
14
37
  */
@@ -70,11 +93,20 @@ export declare class DocumentCommands implements IDocumentCommands {
70
93
  documentData: any;
71
94
  /**
72
95
  * Optional. When provided, upserts a row into byline_document_paths
73
- * keyed by (document_id, this.defaultContentLocale). Omitted by the
74
- * lifecycle for non-default-locale (translation) saves so the
75
- * existing path row is left untouched.
96
+ * keyed by (document_id, <document source_locale>). Omitted by the
97
+ * lifecycle for non-source-locale (translation) saves so the existing
98
+ * path row is left untouched.
76
99
  */
77
100
  path?: string;
101
+ /**
102
+ * Optional. When provided, replaces the document's advertised-locale set
103
+ * in byline_document_available_locales wholesale (delete-then-insert).
104
+ * `undefined` leaves the existing set untouched (sticky across versions,
105
+ * like `path`); an empty array clears it (advertise nothing). The locale
106
+ * values are the advertised content locales themselves, not the default
107
+ * locale. See docs/AVAILABLE-LOCALES.md.
108
+ */
109
+ availableLocales?: string[];
78
110
  locale?: string;
79
111
  status?: string;
80
112
  createdBy?: string;
@@ -97,6 +129,112 @@ export declare class DocumentCommands implements IDocumentCommands {
97
129
  };
98
130
  fieldCount: number;
99
131
  }>;
132
+ /**
133
+ * writeVersionLocaleLedger
134
+ *
135
+ * Compute and insert a version's `byline_document_version_locales` rows: a
136
+ * locale is recorded when it covers every localized field path the version's
137
+ * `sourceLocale` has (path-coverage), and a version with no localized content
138
+ * records a single `'all'` sentinel. Reads the version's persisted store rows,
139
+ * so callers must have written them first. Shared by the create write path
140
+ * (step 6) and `reAnchorDocument` (which recomputes against the new source).
141
+ * Assumes the version has no ledger rows yet (a freshly-inserted version).
142
+ * See docs/CONTENT-LOCALE-RESOLUTION.md and docs/DEFAULT-LOCALE-SWITCHING.md.
143
+ */
144
+ private writeVersionLocaleLedger;
145
+ /**
146
+ * copyAllVersionStoreRows
147
+ *
148
+ * Copy every store row — all eight tables, all locales, including the `meta`
149
+ * identity rows (so block / array-item `_id`s are preserved) — from one
150
+ * document version to another, verbatim. New `id`s are minted; the target
151
+ * `document_version_id` is rebound; timestamps are refreshed. The target
152
+ * version is assumed fresh (no rows), so no conflict handling is needed.
153
+ * Used by `reAnchorDocument` to snapshot the current version into the new
154
+ * re-anchored one without re-flattening (lossless, identity-preserving).
155
+ */
156
+ private copyAllVersionStoreRows;
157
+ /**
158
+ * reAnchorDocument
159
+ *
160
+ * Change a single document's content source locale to `targetLocale` — its
161
+ * fallback floor, path locale, and completeness yardstick. Refuses unless the
162
+ * document is **complete** in the target (the current version's ledger covers
163
+ * it, or the document is locale-agnostic) — never manufactures a primary
164
+ * language with holes. In one transaction: flips `source_locale`, moves the
165
+ * path row onto the new locale (keeping the slug), writes a **new version**
166
+ * that is a verbatim copy of the current one (immutable version event,
167
+ * identities preserved), and computes that version's ledger against the new
168
+ * source. `dryRun` performs only the eligibility check and reports the
169
+ * outcome that *would* result, writing nothing. See
170
+ * docs/DEFAULT-LOCALE-SWITCHING.md.
171
+ */
172
+ reAnchorDocument(params: {
173
+ documentId: string;
174
+ targetLocale: string;
175
+ dryRun?: boolean;
176
+ }): Promise<ReAnchorResult>;
177
+ /**
178
+ * reAnchorDocuments
179
+ *
180
+ * Bulk re-anchor: walk every (non-deleted) logical document — optionally
181
+ * scoped to one collection — and re-anchor each that is complete in
182
+ * `targetLocale`, skipping (and reporting) the rest. Each document runs in its
183
+ * own transaction via `reAnchorDocument`, so one failure or skip never rolls
184
+ * back the others; the command is idempotent and resumable. This is the
185
+ * "client switched the default content locale, move every fully-translated
186
+ * document onto it" operation; the `skipped-incomplete` results double as the
187
+ * outstanding-translation backlog. `dryRun` reports what would happen without
188
+ * writing. See docs/DEFAULT-LOCALE-SWITCHING.md.
189
+ */
190
+ reAnchorDocuments(params: {
191
+ targetLocale: string;
192
+ collectionId?: string;
193
+ dryRun?: boolean;
194
+ }): Promise<ReAnchorReport>;
195
+ /**
196
+ * backfillVersionLocales
197
+ *
198
+ * One-time maintenance: populate `byline_document_version_locales` for
199
+ * versions written *before* the ledger existed (i.e. before the migration
200
+ * that added it). Going forward `createDocumentVersion` step 6 keeps the
201
+ * ledger current; this fills the historical gap so `localeFallback:
202
+ * 'strict'` reads can see pre-existing documents.
203
+ *
204
+ * Same path-coverage rule as the write path, applied set-wise across every
205
+ * version in one statement. The `canonical` anchor is each document's own
206
+ * `source_locale` (joined via `byline_document_versions` → `byline_documents`),
207
+ * falling back to the adapter's configured default content locale for rows
208
+ * not yet stamped by `backfillSourceLocales` — mirroring the per-document
209
+ * anchor the write path uses, rather than a single global locale. Idempotent
210
+ * — safe to re-run (PK + `ON CONFLICT DO NOTHING`); versions are immutable, so
211
+ * a version's computed locale set never changes. Returns the number of
212
+ * `(version, locale)` rows inserted.
213
+ *
214
+ * See docs/CONTENT-LOCALE-RESOLUTION.md.
215
+ */
216
+ backfillVersionLocales(): Promise<{
217
+ rowsInserted: number;
218
+ }>;
219
+ /**
220
+ * backfillSourceLocales
221
+ *
222
+ * One-time maintenance: stamp `byline_documents.source_locale` for documents
223
+ * created *before* the column existed. Sets every row whose `source_locale`
224
+ * is still NULL to the adapter's configured default content locale — the
225
+ * anchor those documents were implicitly authored against (a static SQL
226
+ * migration cannot know the configured default, mirroring
227
+ * `backfillVersionLocales`). Idempotent: only touches NULL rows, so re-runs
228
+ * and rows already stamped by the write path are left alone. Must run before
229
+ * the follow-up migration that sets the column NOT NULL.
230
+ *
231
+ * Returns the number of document rows stamped.
232
+ *
233
+ * See docs/DEFAULT-LOCALE-SWITCHING.md.
234
+ */
235
+ backfillSourceLocales(): Promise<{
236
+ rowsUpdated: number;
237
+ }>;
100
238
  /**
101
239
  * setDocumentStatus
102
240
  *