@murumets-ee/content 0.99.0 → 0.102.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.
Files changed (60) hide show
  1. package/dist/admin.mjs +1 -1
  2. package/dist/chrome-provider.d.mts +1 -1
  3. package/dist/chrome-provider.mjs +1 -1
  4. package/dist/client.d.mts +2 -2
  5. package/dist/client.mjs +1 -1
  6. package/dist/client.mjs.map +1 -1
  7. package/dist/context-DtHgyrgb.mjs +2 -0
  8. package/dist/{context-D7eEItCY.mjs.map → context-DtHgyrgb.mjs.map} +1 -1
  9. package/dist/document-site-B8tmEqLw.mjs +2 -0
  10. package/dist/document-site-B8tmEqLw.mjs.map +1 -0
  11. package/dist/entities/index.d.mts +2 -2
  12. package/dist/entities/index.mjs +1 -1
  13. package/dist/{index-C3VyZLjf.d.mts → index-Bni-PiIz.d.mts} +2 -2
  14. package/dist/{index-C3VyZLjf.d.mts.map → index-Bni-PiIz.d.mts.map} +1 -1
  15. package/dist/index.d.mts +4 -4
  16. package/dist/index.mjs +1 -1
  17. package/dist/locale-registry-COsJrVZl.mjs +2 -0
  18. package/dist/locale-registry-COsJrVZl.mjs.map +1 -0
  19. package/dist/lock-service.d.mts +1 -1
  20. package/dist/plugin-CvPWz399.mjs +2 -0
  21. package/dist/plugin-CvPWz399.mjs.map +1 -0
  22. package/dist/plugin.d.mts +2 -2
  23. package/dist/plugin.d.mts.map +1 -1
  24. package/dist/plugin.mjs +1 -1
  25. package/dist/resolve.mjs +1 -1
  26. package/dist/routes-D-NWP0hK.mjs +2 -0
  27. package/dist/routes-D-NWP0hK.mjs.map +1 -0
  28. package/dist/{routing-strategy-DV7KGarX.d.mts → routing-strategy-DVWB7qPp.d.mts} +2 -2
  29. package/dist/{routing-strategy-DV7KGarX.d.mts.map → routing-strategy-DVWB7qPp.d.mts.map} +1 -1
  30. package/dist/seed.d.mts.map +1 -1
  31. package/dist/seed.mjs +1 -1
  32. package/dist/seed.mjs.map +1 -1
  33. package/dist/site-chrome-BAtQaAqC.mjs +2 -0
  34. package/dist/{site-chrome-AffFxYrV.mjs.map → site-chrome-BAtQaAqC.mjs.map} +1 -1
  35. package/dist/{site-chrome-DJ8IJUi1.d.mts → site-chrome-BiV3zhw0.d.mts} +2 -2
  36. package/dist/{site-chrome-DJ8IJUi1.d.mts.map → site-chrome-BiV3zhw0.d.mts.map} +1 -1
  37. package/dist/translation-provenance--dHtcmfg.d.mts +253 -0
  38. package/dist/translation-provenance--dHtcmfg.d.mts.map +1 -0
  39. package/dist/translation-provenance-eqhsGAyi.mjs +2 -0
  40. package/dist/translation-provenance-eqhsGAyi.mjs.map +1 -0
  41. package/dist/translation.d.mts +2 -0
  42. package/dist/translation.mjs +1 -0
  43. package/dist/{types-B3K2p9t8.d.mts → types-wMTglM4j.d.mts} +13 -4
  44. package/dist/types-wMTglM4j.d.mts.map +1 -0
  45. package/dist/{versionable-ByZgfpp0.d.mts → versionable-D2JjselN.d.mts} +2 -2
  46. package/dist/{versionable-ByZgfpp0.d.mts.map → versionable-D2JjselN.d.mts.map} +1 -1
  47. package/dist/{versions-C5_IJpI8.mjs → versions-B0_9TtZ0.mjs} +2 -2
  48. package/dist/{versions-C5_IJpI8.mjs.map → versions-B0_9TtZ0.mjs.map} +1 -1
  49. package/package.json +14 -10
  50. package/dist/context-D7eEItCY.mjs +0 -2
  51. package/dist/locale-registry-Bn6e_nM7.mjs +0 -2
  52. package/dist/locale-registry-Bn6e_nM7.mjs.map +0 -1
  53. package/dist/plugin-DmyWYmuE.mjs +0 -2
  54. package/dist/plugin-DmyWYmuE.mjs.map +0 -1
  55. package/dist/registry-CaPbY5lm.mjs +0 -2
  56. package/dist/registry-CaPbY5lm.mjs.map +0 -1
  57. package/dist/routes-B6TJ5qiR.mjs +0 -2
  58. package/dist/routes-B6TJ5qiR.mjs.map +0 -1
  59. package/dist/site-chrome-AffFxYrV.mjs +0 -2
  60. package/dist/types-B3K2p9t8.d.mts.map +0 -1
@@ -0,0 +1,253 @@
1
+ import { Entity } from "@murumets-ee/entity";
2
+ //#region src/schema/translation-provenance.d.ts
3
+ /**
4
+ * Translation provenance — what each locale of a document was last saved FROM
5
+ * (agent-access D017).
6
+ *
7
+ * One row per (document, locale), the source locale included:
8
+ *
9
+ * - `contentHash` is a hash of that locale's own reader text as of its last
10
+ * save. For the SOURCE locale this is the value every translation is
11
+ * compared against, and `contentChangedAt` is when it last moved — the date
12
+ * in "Outdated since …".
13
+ * - `sourceLocale` / `sourceHash` record, for a translation, which locale it
14
+ * was made from and that locale's `contentHash` at the time. A translation
15
+ * is outdated when the source's `contentHash` no longer equals it.
16
+ * - `method` is how the text got there (`ai` | `human` | `copy`); machine
17
+ * output is "not reviewed" until `reviewedAt` is set.
18
+ *
19
+ * Storing the source's hash on its OWN row is what keeps "outdated" a
20
+ * comparison of two stored values: a list or a report never has to re-read and
21
+ * re-hash a document to answer it.
22
+ *
23
+ * Shared, not per-entity (like `toolkit_content_locks`). A row is addressed by
24
+ * a document's uuid and is only ever read for a document the caller has
25
+ * already been handed by its own, site-confined client.
26
+ */
27
+ declare const translationProvenanceTable: {
28
+ table: import("drizzle-orm/pg-core").PgTableWithColumns<{
29
+ name: string;
30
+ schema: undefined;
31
+ columns: {
32
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
33
+ name: string;
34
+ tableName: string;
35
+ dataType: import("drizzle-orm").ColumnDataType;
36
+ columnType: string;
37
+ data: unknown;
38
+ driverParam: unknown;
39
+ notNull: false;
40
+ hasDefault: false;
41
+ isPrimaryKey: false;
42
+ isAutoincrement: false;
43
+ hasRuntimeDefault: false;
44
+ enumValues: string[] | undefined;
45
+ baseColumn: never;
46
+ identity: undefined;
47
+ generated: undefined;
48
+ }, {}, {}>;
49
+ };
50
+ dialect: "pg";
51
+ }>;
52
+ schema: import("@murumets-ee/db").TableDefinition<{
53
+ readonly id: import("@murumets-ee/db").ColumnFactory<string, "uuid", true, true>;
54
+ readonly entityType: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
55
+ readonly entityId: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>; /** A real locale code — the default locale by its code, never `'_'`. */
56
+ readonly locale: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
57
+ readonly contentHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
58
+ readonly contentChangedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
59
+ readonly sourceLocale: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
60
+ readonly sourceHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
61
+ readonly method: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
62
+ readonly reviewedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", false, false>;
63
+ readonly reviewedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
64
+ readonly updatedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
65
+ readonly updatedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
66
+ }>;
67
+ columnKinds: Readonly<Record<string, import("@murumets-ee/db").ColumnKind>>;
68
+ primaryKeyColumns: readonly string[];
69
+ makeClient: (database: import("drizzle-orm/postgres-js").PostgresJsDatabase, partitionValue?: string) => import("@murumets-ee/db").TableClient<{
70
+ readonly id: import("@murumets-ee/db").ColumnFactory<string, "uuid", true, true>;
71
+ readonly entityType: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
72
+ readonly entityId: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>; /** A real locale code — the default locale by its code, never `'_'`. */
73
+ readonly locale: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
74
+ readonly contentHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
75
+ readonly contentChangedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
76
+ readonly sourceLocale: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
77
+ readonly sourceHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
78
+ readonly method: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
79
+ readonly reviewedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", false, false>;
80
+ readonly reviewedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
81
+ readonly updatedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
82
+ readonly updatedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
83
+ }, import("drizzle-orm/pg-core").PgTableWithColumns<{
84
+ name: string;
85
+ schema: undefined;
86
+ columns: {
87
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
88
+ name: string;
89
+ tableName: string;
90
+ dataType: import("drizzle-orm").ColumnDataType;
91
+ columnType: string;
92
+ data: unknown;
93
+ driverParam: unknown;
94
+ notNull: false;
95
+ hasDefault: false;
96
+ isPrimaryKey: false;
97
+ isAutoincrement: false;
98
+ hasRuntimeDefault: false;
99
+ enumValues: string[] | undefined;
100
+ baseColumn: never;
101
+ identity: undefined;
102
+ generated: undefined;
103
+ }, {}, {}>;
104
+ };
105
+ dialect: "pg";
106
+ }>>;
107
+ partitionColumn: "id" | "updatedAt" | "updatedBy" | "locale" | "entityType" | "entityId" | "contentHash" | "contentChangedAt" | "sourceLocale" | "sourceHash" | "method" | "reviewedAt" | "reviewedBy" | undefined;
108
+ };
109
+ //#endregion
110
+ //#region src/translation-provenance.d.ts
111
+ /** How a locale's text got there. */
112
+ type TranslationMethod = 'ai' | 'human' | 'copy';
113
+ /** One locale's stored provenance. */
114
+ interface TranslationProvenance {
115
+ locale: string;
116
+ contentHash: string;
117
+ contentChangedAt: Date;
118
+ sourceLocale: string | null;
119
+ sourceHash: string | null;
120
+ method: TranslationMethod | null;
121
+ reviewedAt: Date | null;
122
+ reviewedBy: string | null;
123
+ }
124
+ type Row = Record<string, unknown>;
125
+ /**
126
+ * The entity's translatable fields whose value is prose: `text` and `richtext`.
127
+ * A slug is derived from its source field, and a select, a reference or a
128
+ * media id is not text. The translation engine translates exactly these.
129
+ */
130
+ declare function translatableTextFields(entity: Pick<Entity, 'allFields'>): string[];
131
+ /** The entity's translatable block-tree fields. */
132
+ declare function translatableTreeFields(entity: Pick<Entity, 'allFields'>): string[];
133
+ /** Whether a document of this entity has any text a translation could go stale against. */
134
+ /**
135
+ * The prose a document's translatable text fields hold right now: `[field,
136
+ * text]` for every field whose value is a sentence — not an address (a
137
+ * translatable `canonicalUrl` is a `text` field too) and not empty.
138
+ */
139
+ declare function translatableFieldTexts(entity: Pick<Entity, 'allFields'>, data: Readonly<Record<string, unknown>>): [string, string][];
140
+ declare function hasTranslatableText(entity: Pick<Entity, 'allFields'>): boolean;
141
+ /**
142
+ * Hash the reader text of one locale's data (a row, or a row with a draft laid
143
+ * over it). Order-independent across blocks; whitespace-insensitive.
144
+ */
145
+ declare function documentTextHash(entity: Pick<Entity, 'allFields'>, data: Row): string;
146
+ type Db = Parameters<typeof translationProvenanceTable.makeClient>[0];
147
+ /** What {@link recordLocaleSave} is told about the save. */
148
+ interface LocaleSaveInput {
149
+ /** The entity type. One the app does not know is ignored. */
150
+ entityName: string;
151
+ entityId: string;
152
+ /** The saved locale — a real code, or `'_'` for the default locale. */
153
+ locale: string;
154
+ /** Who saved. */
155
+ userId?: string | undefined;
156
+ /**
157
+ * The locale's whole draft AS JUST WRITTEN, when the caller holds it. A draft
158
+ * writer inside a transaction must pass it: this module reads on its own
159
+ * connection and would otherwise see the draft as it was before the save.
160
+ */
161
+ draftData?: Readonly<Record<string, unknown>> | undefined;
162
+ /**
163
+ * `'ai'` when the caller KNOWS the text is machine output (the translation
164
+ * engine). Left out, a save through a token-derived door is `'ai'` too — an
165
+ * agent typing a translation is a machine translating — and a person's save
166
+ * is `'human'`, or `'copy'` when the text is identical to the source's.
167
+ */
168
+ method?: 'ai' | undefined;
169
+ /**
170
+ * This save REMOVED text rather than wrote it (a draft was discarded). It is
171
+ * recorded — the working text changed back to the row's — but it is never
172
+ * machine output, whoever did it: nobody wrote a word.
173
+ */
174
+ reverted?: boolean | undefined;
175
+ /**
176
+ * Refresh this locale's TEXT HASH only — never its method, its source or its
177
+ * review. For a locale that was READ as a source, not written: without it a
178
+ * non-default source would be re-judged as a translation, under the caller's
179
+ * door, and a hand-written Russian page an agent translated FROM would be
180
+ * recorded as machine output.
181
+ */
182
+ contentOnly?: boolean | undefined;
183
+ /** The locale the text was translated from. Defaults to the default locale. */
184
+ sourceLocale?: string | undefined;
185
+ /**
186
+ * The source's text hash AS TRANSLATED, when the caller translated something
187
+ * other than the source's current working text (its published row).
188
+ */
189
+ sourceHash?: string | undefined;
190
+ }
191
+ /**
192
+ * Record one locale save. Reads that locale's working text, stores its hash,
193
+ * and — for a translation — the source hash it now corresponds to.
194
+ *
195
+ * Machine output stays `ai` and unreviewed through later human edits: fixing a
196
+ * typo is not a review. Only "Mark reviewed" ({@link markTranslationReviewed})
197
+ * clears it, and a fresh machine translation sets it again.
198
+ */
199
+ declare function recordLocaleSave(input: LocaleSaveInput): Promise<void>;
200
+ /**
201
+ * Mark a translation reviewed. The caller checks permission.
202
+ *
203
+ * - `'none'` — nothing is recorded for that locale; there is nothing to review.
204
+ * - `'self'` — `refuseSelf` was set and the reviewer is the one who last wrote
205
+ * the text. A review is someone ELSE vouching for it: an agent that could
206
+ * clear "not reviewed" on its own output would make the badge say nothing
207
+ * (the approval queue refuses the asking principal for the same reason).
208
+ */
209
+ declare function markTranslationReviewed(input: {
210
+ entityType: string;
211
+ entityId: string;
212
+ locale: string;
213
+ userId: string;
214
+ refuseSelf?: boolean | undefined;
215
+ }): Promise<'reviewed' | 'none' | 'self'>;
216
+ /** Every stored provenance row for a set of documents of one entity type — ONE query. */
217
+ declare function getTranslationProvenanceBatch(entityType: string, entityIds: readonly string[], db?: Db): Promise<Map<string, Map<string, TranslationProvenance>>>;
218
+ /** What a translation's provenance says about it, against its source's. */
219
+ interface TranslationFreshness {
220
+ method: TranslationMethod | null;
221
+ sourceLocale: string | null;
222
+ /** The source text has changed since this translation was saved. */
223
+ outdated: boolean;
224
+ /** When the source text changed — ISO, only when {@link outdated}. */
225
+ outdatedSince: string | null;
226
+ /** Machine output nobody has marked reviewed. */
227
+ machineUnreviewed: boolean;
228
+ reviewedAt: string | null;
229
+ }
230
+ /** Compare one locale's provenance with its source's. `null` when nothing is recorded. */
231
+ declare function translationFreshness(byLocale: ReadonlyMap<string, TranslationProvenance> | undefined, locale: string): TranslationFreshness | null;
232
+ /**
233
+ * Record a locale save WITHOUT being awaited by it: bounded, coalesced per
234
+ * document and locale, and run under the caller's own request context.
235
+ *
236
+ * For a writer that holds a transaction (the block editor's draft write inside
237
+ * op-commit's): awaiting a recording there would take a SECOND pool connection
238
+ * while holding the first, and enough concurrent commits would each wait for a
239
+ * connection none of them can release.
240
+ */
241
+ declare function enqueueLocaleSave(input: LocaleSaveInput): void;
242
+ /**
243
+ * Resolves once the queued recordings for ONE document and locale have run —
244
+ * for a caller about to write that row itself, which must come last. Bounded:
245
+ * it waits for that key only, never the whole queue, and gives up after
246
+ * `maxMs` rather than hold a request on a recording that will not settle.
247
+ */
248
+ declare function localeSaveSettled(entityName: string, entityId: string, locale: string, maxMs?: number): Promise<void>;
249
+ /** Resolves once every queued recording has run. For tests; a request uses {@link localeSaveSettled}. */
250
+ declare function provenanceQueueIdle(): Promise<void>;
251
+ //#endregion
252
+ export { documentTextHash as a, hasTranslatableText as c, provenanceQueueIdle as d, recordLocaleSave as f, translationFreshness as g, translatableTreeFields as h, TranslationProvenance as i, localeSaveSettled as l, translatableTextFields as m, TranslationFreshness as n, enqueueLocaleSave as o, translatableFieldTexts as p, TranslationMethod as r, getTranslationProvenanceBatch as s, LocaleSaveInput as t, markTranslationReviewed as u };
253
+ //# sourceMappingURL=translation-provenance--dHtcmfg.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-provenance--dHtcmfg.d.mts","names":[],"sources":["../src/schema/translation-provenance.ts","../src/translation-provenance.ts"],"mappings":";;;;;;AA2BA;;;;;;;;;;;;;;;;;;;;cAAa,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KCsBD,iBAAA;;UAGK,qBAAA;EACf,MAAA;EACA,WAAA;EACA,gBAAA,EAAkB,IAAA;EAClB,YAAA;EACA,UAAA;EACA,MAAA,EAAQ,iBAAA;EACR,UAAA,EAAY,IAAA;EACZ,UAAA;AAAA;AAAA,KAGG,GAAA,GAAM,MAAM;;;;;;iBAYD,sBAAA,CAAuB,MAAA,EAAQ,IAAI,CAAC,MAAA;;iBAUpC,sBAAA,CAAuB,MAAA,EAAQ,IAAI,CAAC,MAAA;;;;;;;iBAapC,sBAAA,CACd,MAAA,EAAQ,IAAA,CAAK,MAAA,gBACb,IAAA,EAAM,QAAA,CAAS,MAAA;AAAA,iBAUD,mBAAA,CAAoB,MAAA,EAAQ,IAAI,CAAC,MAAA;;;;;iBAwBjC,gBAAA,CAAiB,MAAA,EAAQ,IAAA,CAAK,MAAA,gBAAsB,IAAA,EAAM,GAAA;AAAA,KA4BrE,EAAA,GAAK,UAAU,QAAQ,0BAAA,CAA2B,UAAA;;UA8DtC,eAAA;;EAEf,UAAA;EACA,QAAA;;EAEA,MAAA;;EAEA,MAAA;;;;;;EAMA,SAAA,GAAY,QAAQ,CAAC,MAAA;;;;;;;EAOrB,MAAA;;;;;;EAMA,QAAA;;;;;;;;EAQA,WAAA;;EAEA,YAAA;;;;;EAKA,UAAA;AAAA;;;;;;;;;iBAWoB,gBAAA,CAAiB,KAAA,EAAO,eAAA,GAAkB,OAAO;;;;;;;;;;iBA2KjD,uBAAA,CAAwB,KAAA;EAC5C,UAAA;EACA,QAAA;EACA,MAAA;EACA,MAAA;EACA,UAAA;AAAA,IACE,OAAO;AApZkB;AAAA,iBAiaP,6BAAA,CACpB,UAAA,UACA,SAAA,qBACA,EAAA,GAAK,EAAA,GACJ,OAAA,CAAQ,GAAA,SAAY,GAAA,SAAY,qBAAA;;UAoBlB,oBAAA;EACf,MAAA,EAAQ,iBAAiB;EACzB,YAAA;EAjbY;EAmbZ,QAAA;EAnbgB;EAqbhB,aAAA;EA1bA;EA4bA,iBAAA;EACA,UAAA;AAAA;;iBAIc,oBAAA,CACd,QAAA,EAAU,WAAA,SAAoB,qBAAA,eAC9B,MAAA,WACC,oBAAA;;;;;;AA9bS;AACX;;;iBAkgBe,iBAAA,CAAkB,KAAsB,EAAf,eAAe;;;;;;;iBAsDlC,iBAAA,CACpB,UAAA,UACA,QAAA,UACA,MAAA,UACA,KAAA,YACC,OAAO;AAriB8D;AAAA,iBAojBlD,mBAAA,CAAA,GAAuB,OAAO"}
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./content-config-store-8omQF5p5.mjs";import"./locale-utils-B0mFoV47.mjs";import{blockTreeFieldsOf as t}from"@murumets-ee/entity";import{getApp as n,getContext as r,isTokenDerivedDoor as i}from"@murumets-ee/core";import{column as a,defineTable as o}from"@murumets-ee/db";import{AsyncResource as s}from"node:async_hooks";import{createHash as c}from"node:crypto";import{isProseText as l,treeTextStrings as u}from"@murumets-ee/blocks/translation-text";const d=Symbol.for(`@murumets-ee/content:content-tables-registry`);function f(){let e=globalThis,t=e[d];return t||(t=new Map,e[d]=t),t}function p(e,t){let n=f(),r=n.get(e)??{};n.set(e,{...r,...t})}function m(e){return f().get(e)??{}}const h=o({name:`toolkit_translation_provenance`,columns:{id:a.uuid({primaryKey:!0,defaultRandom:!0}),entityType:a.varchar({length:100,notNull:!0,pgName:`entity_type`}),entityId:a.varchar({length:255,notNull:!0,pgName:`entity_id`}),locale:a.varchar({length:10,notNull:!0}),contentHash:a.varchar({length:64,notNull:!0,pgName:`content_hash`}),contentChangedAt:a.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`content_changed_at`}),sourceLocale:a.varchar({length:10,pgName:`source_locale`}),sourceHash:a.varchar({length:64,pgName:`source_hash`}),method:a.varchar({length:10}),reviewedAt:a.timestamp({withTimezone:!0,pgName:`reviewed_at`}),reviewedBy:a.varchar({length:255,pgName:`reviewed_by`}),updatedAt:a.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`}),updatedBy:a.varchar({length:255,pgName:`updated_by`})},unique:[{on:[`entityType`,`entityId`,`locale`]}]}),g=h.table;function _(e){return e.allFields}function v(e){let t=[];for(let[n,r]of Object.entries(_(e)))r.translatable===!0&&(r.type===`text`||r.type===`richtext`)&&t.push(n);return t}function y(e){let n=_(e);return t(e).map(e=>e.name).filter(e=>n[e]?.translatable===!0)}function b(e,t){let n=[];for(let r of v(e)){let e=t[r];typeof e==`string`&&l(e)&&n.push([r,e])}return n}function x(e){return v(e).length>0||y(e).length>0}const S=/<\/?(?:a|abbr|b|code|em|i|mark|s|small|span|strong|sub|sup|u)(?:\s[^<>]*)?\/?>/gi;function C(e){return e.replace(S,``).replace(/<[^<>]*>/g,` `)}function w(e){return e.replace(/\s+/g,` `).trim()}function T(e,t){let n=[],r=_(e);for(let i of v(e)){let e=t[i];if(typeof e!=`string`||!l(e))continue;let a=r[i]?.type===`richtext`?C(e):e;n.push(`${i}=${w(a)}`)}for(let r of y(e)){let e=t[r];if(Array.isArray(e))for(let t of u(e)){let e=w(t);e!==``&&n.push(e)}}return n.sort(),c(`sha256`).update(JSON.stringify(n)).digest(`hex`)}function E(e,t){return e===`_`?t:e}function D(e=n().db.readWrite){return h.makeClient(e)}function O(){try{return n()}catch{return}}function k(e){let t=e.method;return{locale:String(e.locale),contentHash:String(e.contentHash),contentChangedAt:e.contentChangedAt,sourceLocale:typeof e.sourceLocale==`string`?e.sourceLocale:null,sourceHash:typeof e.sourceHash==`string`?e.sourceHash:null,method:t===`ai`||t===`human`||t===`copy`?t:null,reviewedAt:e.reviewedAt??null,reviewedBy:typeof e.reviewedBy==`string`?e.reviewedBy:null}}async function A(e,t,r,i,a){let o=n(),s=r===i,c=await o.getClient(e).findById(t,{...s?{}:{locale:r,defaultLocale:i}});if(!c)return null;if(a!==void 0)return{row:c,working:{...c,...a},drafted:!0};let{drafts:l}=m(e.name);if(!l)return{row:c,working:c,drafted:!1};let{siteForDocumentType:u}=await import(`./document-site-B8tmEqLw.mjs`).then(e=>e.t),d=(await l.makeClient(o.db.readWrite,await u(e.name)).findOne({entityId:t,locale:s?`_`:r}))?.data;return d&&typeof d==`object`?{row:c,working:{...c,...d},drafted:!0}:{row:c,working:c,drafted:!1}}async function j(t){let{entityName:n,entityId:a}=t,o=O();if(o)try{let s=o.entities.get(n);if(!s||!x(s))return;let c=e().defaultLocale,l=E(t.locale,c),u=await A(s,a,l,c,t.draftData===void 0?void 0:{...t.draftData});if(!u)return;let d=T(s,u.working),f=D(),p=new Date,m={entityType:s.name,entityId:a};if(t.contentOnly===!0||l===c&&t.sourceLocale===void 0){let e=t.contentOnly===!0||t.reverted===!0?void 0:t.userId;await M(f,{...m,locale:l},d,p,e);return}let h=E(t.sourceLocale??c,c);if(h===l)return;let g=t.sourceHash;if(g===void 0){let e=await f.findOne({...m,locale:h});if(e)g=String(e.contentHash);else{let e=await A(s,a,h,c);if(!e)return;g=T(s,e.working),await M(f,{...m,locale:h},g,p,void 0)}}let _=await f.findOne({...m,locale:l}),v=_===null?u.drafted?T(s,u.row)!==d:d!==g:_.contentHash!==d,y=t.method===`ai`||v&&t.reverted!==!0&&i(r()?.door),b=t.userId!==void 0&&t.reverted!==!0&&(_===null||_.contentHash!==d),S=_?.method===`ai`,C=y||S?`ai`:d===g?`copy`:`human`;await f.upsert({...m,locale:l,contentHash:d,contentChangedAt:p,sourceLocale:h,sourceHash:g,method:C,reviewedAt:null,reviewedBy:null,updatedAt:p,updatedBy:t.userId??null},{target:[`entityType`,`entityId`,`locale`],set:{contentHash:d,...(_===null||_.contentHash!==d)&&{contentChangedAt:p},sourceLocale:h,sourceHash:g,method:C,...y&&{reviewedAt:null,reviewedBy:null},updatedAt:p,...b&&{updatedBy:t.userId}}})}catch(e){o.logger.warn({err:e,entity:n,entityId:a,locale:t.locale},`translation provenance: could not record a locale save`)}}async function M(e,t,n,r,i){let a=await e.findOne(t);a&&a.contentHash===n||await e.upsert({...t,contentHash:n,contentChangedAt:r,updatedAt:r,updatedBy:i??null},{target:[`entityType`,`entityId`,`locale`],set:{contentHash:n,contentChangedAt:r,updatedAt:r,...i!==void 0&&{updatedBy:i}}})}async function N(e){let t=D(),n={entityType:e.entityType,entityId:e.entityId,locale:e.locale},r=await t.findOne(n);if(!r)return`none`;if(e.refuseSelf===!0&&r.updatedBy===e.userId)return`self`;let i=new Date;return await t.update(n,{reviewedAt:i,reviewedBy:e.userId,updatedAt:i}),`reviewed`}async function P(e,t,n){let r=new Map;if(t.length===0)return r;let i=await D(n).findMany({where:{entityType:{eq:e},entityId:{in:[...t]}}});for(let e of i){let t=String(e.entityId),n=r.get(t);n||(n=new Map,r.set(t,n));let i=k(e);n.set(i.locale,i)}return r}function F(e,t){let n=e?.get(t);if(!n)return null;let r=n.sourceLocale===null?void 0:e?.get(n.sourceLocale),i=r!==void 0&&n.sourceHash!==null&&r.contentHash!==n.sourceHash;return{method:n.method,sourceLocale:n.sourceLocale,outdated:i,outdatedSince:i&&r?new Date(r.contentChangedAt).toISOString():null,machineUnreviewed:n.method===`ai`&&n.reviewedAt===null,reviewedAt:n.reviewedAt?new Date(n.reviewedAt).toISOString():null}}const I=new Map,L=new Map;function R(){for(let[e,t]of I){if(L.size>=2)return;if(L.has(e))continue;I.delete(e);let n=t.run().finally(()=>{L.delete(e),R()});L.set(e,n)}}function z(e){let t=O();if(!t)return;let n=V(e.entityName,e.entityId,e.locale);if(!I.has(n)&&I.size>=200){t.logger.warn({entity:e.entityName,entityId:e.entityId,locale:e.locale,waiting:I.size},`translation provenance: too many recordings waiting — this save was not recorded`);return}I.set(n,{run:s.bind(()=>j(e))}),R()}const B=e=>{if(e.action===`delete`)return;let t=O();if(!t)return;let n=t.entities.get(e.entityName);!n||!x(n)||z({entityName:e.entityName,entityId:e.id,locale:e.locale??`_`,userId:r()?.user?.id})};function V(e,t,n){return`${e}:${t}:${n}`}async function H(e,t,n,r=3e3){let i=V(e,t,n),a=Date.now()+r;for(;Date.now()<a;){let e=L.get(i);if(e!==void 0){await Promise.race([e,new Promise(e=>setTimeout(e,50))]);continue}if(!I.has(i))return;await new Promise(e=>setTimeout(e,5))}}async function U(){for(;L.size>0||I.size>0;)await new Promise(e=>setTimeout(e,5))}export{H as a,j as c,y as d,F as f,p as g,m as h,x as i,b as l,g as m,z as n,N as o,B as p,P as r,U as s,T as t,v as u};
2
+ //# sourceMappingURL=translation-provenance-eqhsGAyi.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-provenance-eqhsGAyi.mjs","names":[],"sources":["../src/schema/registry.ts","../src/schema/translation-provenance.ts","../src/translation-provenance.ts"],"sourcesContent":["/**\n * Per-entity content table registry.\n *\n * Maps an entity name to its content infrastructure tables — drafts,\n * versions, locale_status. Each entry stores the `DefinedTable` (from\n * `defineTable`), which exposes a typed `.makeClient(db)` for CRUD via\n * `TableClient` and a raw `.table` for legacy callers.\n *\n * The content plugin populates this registry at `init` time; ContentClient\n * reads from it lazily on first use. Tables that don't apply to an entity\n * (non-publishable entities have no drafts, etc.) are simply absent.\n */\n\nimport type { DraftsTable } from './drafts.js'\nimport type { LocaleStatusTable } from './locale-status.js'\nimport type { VersionsTable } from './versions.js'\n\n/**\n * The full set of optional content infra tables attached to an entity.\n * Every field is optional — entities without the relevant behavior don't\n * get the corresponding table.\n */\nexport interface ContentTables {\n /** `{entity}_drafts` — present when the entity has `publishable()`. */\n drafts?: DraftsTable\n /** `{entity}_versions` — present when the entity has `versionable()`. */\n versions?: VersionsTable\n /** `{entity}_locale_status` — present when the entity is publishable + has translatable fields. */\n localeStatus?: LocaleStatusTable\n}\n\n/**\n * The registry map lives on `globalThis` keyed by `Symbol.for(...)`, NOT as a\n * module-level binding — matching core's `PluginContributionsRegistry`\n * pattern. A module-level `Map` here is the exact Turbopack-dev footgun\n * CLAUDE.md documents (the createdBy/updatedBy bug class): a lazily-compiled\n * route chunk can resolve this module to a DIFFERENT instance than the boot\n * path that called `registerContentTables`, so `getContentTables()` returns\n * `{}` and callers silently take their no-table fallbacks. Observed in the\n * wild 2026-06-11 (F031 #1): the blocks op-commit route saw an empty registry,\n * `writeDraft` fell back to live-row writes, and Publish 500'd with \"No draft\n * found\" — flaky per-recompile, dev-only. `Symbol.for` is process-global, so\n * every duplicate module instance reads the same map.\n */\nconst REGISTRY_KEY = Symbol.for('@murumets-ee/content:content-tables-registry')\n\ninterface GlobalWithRegistry {\n [REGISTRY_KEY]?: Map<string, ContentTables>\n}\n\nfunction registryMap(): Map<string, ContentTables> {\n const g = globalThis as GlobalWithRegistry\n let map = g[REGISTRY_KEY]\n if (!map) {\n map = new Map<string, ContentTables>()\n g[REGISTRY_KEY] = map\n }\n return map\n}\n\n/**\n * Register content tables for an entity. Called from `content().init()`.\n *\n * Subsequent calls for the same entity name MERGE the provided patch into\n * the existing entry. This lets the plugin add tables one at a time (drafts,\n * then versions, then locale_status) without the caller needing to build the\n * whole object up front.\n */\nexport function registerContentTables(entityName: string, patch: ContentTables): void {\n const tables = registryMap()\n const existing = tables.get(entityName) ?? {}\n tables.set(entityName, { ...existing, ...patch })\n}\n\n/**\n * Get the content tables for an entity. Returns an empty object (rather\n * than undefined) when the entity has no registered tables — that\n * simplifies the common ContentClient lookup idiom:\n *\n * const { drafts } = getContentTables(entity.name)\n * if (!drafts) return null\n */\nexport function getContentTables(entityName: string): ContentTables {\n return registryMap().get(entityName) ?? {}\n}\n\n/**\n * Clear the registry. **Test-only.** Throws in production.\n */\nexport function clearContentTables(): void {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'clearContentTables() is not allowed in production — this is a test-only utility.',\n )\n }\n registryMap().clear()\n}\n","/**\n * Translation provenance — what each locale of a document was last saved FROM\n * (agent-access D017).\n *\n * One row per (document, locale), the source locale included:\n *\n * - `contentHash` is a hash of that locale's own reader text as of its last\n * save. For the SOURCE locale this is the value every translation is\n * compared against, and `contentChangedAt` is when it last moved — the date\n * in \"Outdated since …\".\n * - `sourceLocale` / `sourceHash` record, for a translation, which locale it\n * was made from and that locale's `contentHash` at the time. A translation\n * is outdated when the source's `contentHash` no longer equals it.\n * - `method` is how the text got there (`ai` | `human` | `copy`); machine\n * output is \"not reviewed\" until `reviewedAt` is set.\n *\n * Storing the source's hash on its OWN row is what keeps \"outdated\" a\n * comparison of two stored values: a list or a report never has to re-read and\n * re-hash a document to answer it.\n *\n * Shared, not per-entity (like `toolkit_content_locks`). A row is addressed by\n * a document's uuid and is only ever read for a document the caller has\n * already been handed by its own, site-confined client.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const translationProvenanceTable = defineTable({\n name: 'toolkit_translation_provenance',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityType: column.varchar({ length: 100, notNull: true, pgName: 'entity_type' }),\n entityId: column.varchar({ length: 255, notNull: true, pgName: 'entity_id' }),\n /** A real locale code — the default locale by its code, never `'_'`. */\n locale: column.varchar({ length: 10, notNull: true }),\n contentHash: column.varchar({ length: 64, notNull: true, pgName: 'content_hash' }),\n contentChangedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'content_changed_at',\n }),\n sourceLocale: column.varchar({ length: 10, pgName: 'source_locale' }),\n sourceHash: column.varchar({ length: 64, pgName: 'source_hash' }),\n method: column.varchar({ length: 10 }),\n reviewedAt: column.timestamp({ withTimezone: true, pgName: 'reviewed_at' }),\n // A user id — better-auth ids are random strings, never uuids.\n reviewedBy: column.varchar({ length: 255, pgName: 'reviewed_by' }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n updatedBy: column.varchar({ length: 255, pgName: 'updated_by' }),\n },\n unique: [{ on: ['entityType', 'entityId', 'locale'] }],\n})\n\nexport const toolkitTranslationProvenance = translationProvenanceTable.table\n","/**\n * Translation provenance — recording what each locale was saved from, and\n * reading \"outdated\" / \"machine translated, not reviewed\" back\n * (agent-access D017).\n *\n * WHAT IS HASHED. A locale's reader TEXT, as the editor would show it: the\n * stored row with that locale's draft laid over it. Text means the entity's\n * translatable `text` / `richtext` fields and the text of every block tree\n * (`treeTextStrings`, the same walk machine translation uses). The strings are\n * hashed as a SORTED list, so moving a block changes nothing; an image, a link\n * or a design field is not text, so swapping one changes nothing either.\n * Slugs are derived, not written, and are left out.\n *\n * WHERE IT IS RECORDED. No single hook sees every locale save (R016), so there\n * are two:\n *\n * - the DRAFT writers record for themselves — this is every edit a person\n * makes to publishable content. `mergeDraft` / `saveDraft` / `deleteDraft`\n * await {@link recordLocaleSave}; the block editor's\n * `DocumentDraftStore.writeDraft` runs inside op-commit's transaction, so it\n * QUEUES the recording ({@link enqueueLocaleSave}) instead of awaiting it;\n * - a write that lands on the live row without a draft (a generic PATCH, a\n * version restore, a seeder, a non-publishable entity) reaches\n * {@link translationProvenanceWriteListener} through\n * `'entity.writeListeners'`.\n *\n * Publish needs no recording of its own: it moves the draft's text onto the\n * row, and the working text — draft over row — is the same before and after.\n *\n * NEVER FAILS A SAVE, AND NEVER HOLDS ONE UP. Recording runs on its own\n * connection, outside the caller's transaction — which also means a save that\n * rolls back can leave its hash recorded until the next save corrects it — and\n * every failure is logged and dropped: a missing\n * provenance row reads as \"unknown\", which is what every document saved before\n * this table existed already is.\n */\n\nimport { AsyncResource } from 'node:async_hooks'\nimport { createHash } from 'node:crypto'\nimport { isProseText, treeTextStrings } from '@murumets-ee/blocks/translation-text'\nimport { getApp, getContext, isTokenDerivedDoor } from '@murumets-ee/core'\nimport { blockTreeFieldsOf, type Entity, type FieldConfig } from '@murumets-ee/entity'\nimport type { EntityWriteObserver } from '@murumets-ee/entity/admin'\nimport { getContentConfig } from './content-config-store.js'\nimport { DEFAULT_LOCALE_SENTINEL } from './locale-utils.js'\nimport { getContentTables } from './schema/registry.js'\nimport { translationProvenanceTable } from './schema/translation-provenance.js'\n\n/** How a locale's text got there. */\nexport type TranslationMethod = 'ai' | 'human' | 'copy'\n\n/** One locale's stored provenance. */\nexport interface TranslationProvenance {\n locale: string\n contentHash: string\n contentChangedAt: Date\n sourceLocale: string | null\n sourceHash: string | null\n method: TranslationMethod | null\n reviewedAt: Date | null\n reviewedBy: string | null\n}\n\ntype Row = Record<string, unknown>\ntype Fields = Readonly<Record<string, FieldConfig>>\n\nfunction fieldsOf(entity: Pick<Entity, 'allFields'>): Fields {\n return entity.allFields as Fields\n}\n\n/**\n * The entity's translatable fields whose value is prose: `text` and `richtext`.\n * A slug is derived from its source field, and a select, a reference or a\n * media id is not text. The translation engine translates exactly these.\n */\nexport function translatableTextFields(entity: Pick<Entity, 'allFields'>): string[] {\n const out: string[] = []\n for (const [name, config] of Object.entries(fieldsOf(entity))) {\n if (config.translatable !== true) continue\n if (config.type === 'text' || config.type === 'richtext') out.push(name)\n }\n return out\n}\n\n/** The entity's translatable block-tree fields. */\nexport function translatableTreeFields(entity: Pick<Entity, 'allFields'>): string[] {\n const fields = fieldsOf(entity)\n return blockTreeFieldsOf(entity as Entity)\n .map((ref) => ref.name)\n .filter((name) => fields[name]?.translatable === true)\n}\n\n/** Whether a document of this entity has any text a translation could go stale against. */\n/**\n * The prose a document's translatable text fields hold right now: `[field,\n * text]` for every field whose value is a sentence — not an address (a\n * translatable `canonicalUrl` is a `text` field too) and not empty.\n */\nexport function translatableFieldTexts(\n entity: Pick<Entity, 'allFields'>,\n data: Readonly<Record<string, unknown>>,\n): [string, string][] {\n const out: [string, string][] = []\n for (const name of translatableTextFields(entity)) {\n const value = data[name]\n if (typeof value === 'string' && isProseText(value)) out.push([name, value])\n }\n return out\n}\n\nexport function hasTranslatableText(entity: Pick<Entity, 'allFields'>): boolean {\n return translatableTextFields(entity).length > 0 || translatableTreeFields(entity).length > 0\n}\n\nconst INLINE_TAG =\n /<\\/?(?:a|abbr|b|code|em|i|mark|s|small|span|strong|sub|sup|u)(?:\\s[^<>]*)?\\/?>/gi\n\n/**\n * The words of an HTML string. An INLINE tag disappears (bold in the middle of\n * a word, or before a comma, is the same words); any other tag is a break\n * between words.\n */\nfunction htmlText(html: string): string {\n return html.replace(INLINE_TAG, '').replace(/<[^<>]*>/g, ' ')\n}\n\nfunction squash(text: string): string {\n return text.replace(/\\s+/g, ' ').trim()\n}\n\n/**\n * Hash the reader text of one locale's data (a row, or a row with a draft laid\n * over it). Order-independent across blocks; whitespace-insensitive.\n */\nexport function documentTextHash(entity: Pick<Entity, 'allFields'>, data: Row): string {\n const parts: string[] = []\n const fields = fieldsOf(entity)\n for (const name of translatableTextFields(entity)) {\n const value = data[name]\n if (typeof value !== 'string' || !isProseText(value)) continue\n // An entity `richtext` column is an HTML string: its tags are not text, so\n // changing a link's target or making a word bold is not a text change.\n const text = fields[name]?.type === 'richtext' ? htmlText(value) : value\n parts.push(`${name}=${squash(text)}`)\n }\n for (const name of translatableTreeFields(entity)) {\n const value = data[name]\n if (!Array.isArray(value)) continue\n for (const text of treeTextStrings(value as never)) {\n const squashed = squash(text)\n if (squashed !== '') parts.push(squashed)\n }\n }\n parts.sort()\n return createHash('sha256').update(JSON.stringify(parts)).digest('hex')\n}\n\n/** `'_'` → the configured default locale's code; anything else unchanged. */\nfunction realLocale(locale: string, defaultLocale: string): string {\n return locale === DEFAULT_LOCALE_SENTINEL ? defaultLocale : locale\n}\n\ntype Db = Parameters<typeof translationProvenanceTable.makeClient>[0]\n\nfunction provenanceClient(db: Db = getApp().db.readWrite) {\n return translationProvenanceTable.makeClient(db)\n}\n\n/** The app, or `undefined` in a realm that has none (a unit test of a neighbouring module). */\nfunction appOrNone(): ReturnType<typeof getApp> | undefined {\n try {\n return getApp()\n } catch {\n // No app in this realm — there is no database to record against either.\n return undefined\n }\n}\n\nfunction toProvenance(row: Row): TranslationProvenance {\n const method = row.method\n return {\n locale: String(row.locale),\n contentHash: String(row.contentHash),\n contentChangedAt: row.contentChangedAt as Date,\n sourceLocale: typeof row.sourceLocale === 'string' ? row.sourceLocale : null,\n sourceHash: typeof row.sourceHash === 'string' ? row.sourceHash : null,\n method: method === 'ai' || method === 'human' || method === 'copy' ? method : null,\n reviewedAt: (row.reviewedAt as Date | null | undefined) ?? null,\n reviewedBy: typeof row.reviewedBy === 'string' ? row.reviewedBy : null,\n }\n}\n\n/**\n * One locale's stored row (locale-merged) and its working data — the row with\n * that locale's draft, if any, laid over it. `null` when the caller cannot read\n * the row.\n */\nasync function readWorkingData(\n entity: Entity,\n entityId: string,\n locale: string,\n defaultLocale: string,\n draftData?: Row,\n): Promise<{ row: Row; working: Row; drafted: boolean } | null> {\n const app = getApp()\n const isDefault = locale === defaultLocale\n const row = (await app.getClient(entity).findById(entityId, {\n ...(isDefault ? {} : { locale, defaultLocale }),\n })) as Row | null\n if (!row) return null\n if (draftData !== undefined) return { row, working: { ...row, ...draftData }, drafted: true }\n const { drafts } = getContentTables(entity.name)\n if (!drafts) return { row, working: row, drafted: false }\n const { siteForDocumentType } = await import('./internal/document-site.js')\n const draft = await drafts\n .makeClient(app.db.readWrite, await siteForDocumentType(entity.name))\n .findOne({ entityId, locale: isDefault ? DEFAULT_LOCALE_SENTINEL : locale })\n const data = draft?.data\n return data && typeof data === 'object'\n ? { row, working: { ...row, ...(data as Row) }, drafted: true }\n : { row, working: row, drafted: false }\n}\n\n/** What {@link recordLocaleSave} is told about the save. */\nexport interface LocaleSaveInput {\n /** The entity type. One the app does not know is ignored. */\n entityName: string\n entityId: string\n /** The saved locale — a real code, or `'_'` for the default locale. */\n locale: string\n /** Who saved. */\n userId?: string | undefined\n /**\n * The locale's whole draft AS JUST WRITTEN, when the caller holds it. A draft\n * writer inside a transaction must pass it: this module reads on its own\n * connection and would otherwise see the draft as it was before the save.\n */\n draftData?: Readonly<Record<string, unknown>> | undefined\n /**\n * `'ai'` when the caller KNOWS the text is machine output (the translation\n * engine). Left out, a save through a token-derived door is `'ai'` too — an\n * agent typing a translation is a machine translating — and a person's save\n * is `'human'`, or `'copy'` when the text is identical to the source's.\n */\n method?: 'ai' | undefined\n /**\n * This save REMOVED text rather than wrote it (a draft was discarded). It is\n * recorded — the working text changed back to the row's — but it is never\n * machine output, whoever did it: nobody wrote a word.\n */\n reverted?: boolean | undefined\n /**\n * Refresh this locale's TEXT HASH only — never its method, its source or its\n * review. For a locale that was READ as a source, not written: without it a\n * non-default source would be re-judged as a translation, under the caller's\n * door, and a hand-written Russian page an agent translated FROM would be\n * recorded as machine output.\n */\n contentOnly?: boolean | undefined\n /** The locale the text was translated from. Defaults to the default locale. */\n sourceLocale?: string | undefined\n /**\n * The source's text hash AS TRANSLATED, when the caller translated something\n * other than the source's current working text (its published row).\n */\n sourceHash?: string | undefined\n}\n\n/**\n * Record one locale save. Reads that locale's working text, stores its hash,\n * and — for a translation — the source hash it now corresponds to.\n *\n * Machine output stays `ai` and unreviewed through later human edits: fixing a\n * typo is not a review. Only \"Mark reviewed\" ({@link markTranslationReviewed})\n * clears it, and a fresh machine translation sets it again.\n */\nexport async function recordLocaleSave(input: LocaleSaveInput): Promise<void> {\n const { entityName, entityId } = input\n const app = appOrNone()\n if (!app) return\n try {\n const entity = app.entities.get(entityName)\n if (!entity || !hasTranslatableText(entity)) return\n const defaultLocale = getContentConfig().defaultLocale\n const locale = realLocale(input.locale, defaultLocale)\n const read = await readWorkingData(\n entity,\n entityId,\n locale,\n defaultLocale,\n input.draftData === undefined ? undefined : { ...input.draftData },\n )\n if (!read) return\n const contentHash = documentTextHash(entity, read.working)\n const client = provenanceClient()\n const now = new Date()\n const key = { entityType: entity.name, entityId }\n\n if (\n input.contentOnly === true ||\n (locale === defaultLocale && input.sourceLocale === undefined)\n ) {\n // A hash-only refresh and a discard author nothing — no user is recorded.\n const author =\n input.contentOnly === true || input.reverted === true ? undefined : input.userId\n await upsertContent(client, { ...key, locale }, contentHash, now, author)\n return\n }\n\n const sourceLocale = realLocale(input.sourceLocale ?? defaultLocale, defaultLocale)\n if (sourceLocale === locale) return\n let sourceHash = input.sourceHash\n if (sourceHash === undefined) {\n // The source's own row is the comparison target, so make sure it exists:\n // a document last saved before this table did has none.\n const source = await client.findOne({ ...key, locale: sourceLocale })\n if (source) {\n sourceHash = String(source.contentHash)\n } else {\n const sourceData = await readWorkingData(entity, entityId, sourceLocale, defaultLocale)\n if (!sourceData) return\n sourceHash = documentTextHash(entity, sourceData.working)\n await upsertContent(client, { ...key, locale: sourceLocale }, sourceHash, now, undefined)\n }\n }\n\n const existing = await client.findOne({ ...key, locale })\n // Did this save change the locale's TEXT? Against the recorded hash when\n // there is one. A locale with nothing recorded (every document saved before\n // this table existed, and every brand-new translation) has no hash to\n // compare with, so:\n // - with a DRAFT over the row, the row is the \"before\": a draft whose text\n // equals the row's changed no words — an agent swapping an image in a\n // hand-translated page has not machine-translated it;\n // - with NO draft, the write landed on the row itself and there is no\n // \"before\" left to read. Then the question is whether the locale holds a\n // translation at all (its text differs from the source's). That errs\n // toward `ai` for an agent's first write to an old hand translation —\n // a badge a person clears with \"Mark reviewed\" — and never toward\n // recording machine text as human, which is the failure that matters.\n const changed =\n existing !== null\n ? existing.contentHash !== contentHash\n : read.drafted\n ? documentTextHash(entity, read.row) !== contentHash\n : contentHash !== sourceHash\n // Machine output is: what the engine says it wrote, or TEXT that arrived\n // through a token-derived door. The second needs the text to have changed —\n // an agent publishing, or re-saving the same words, has written nothing,\n // and must not turn a reviewed translation back into an unreviewed one. A\n // discard writes nothing either.\n const machine =\n input.method === 'ai' ||\n (changed && input.reverted !== true && isTokenDerivedDoor(getContext()?.door))\n const authored =\n input.userId !== undefined &&\n input.reverted !== true &&\n (existing === null || existing.contentHash !== contentHash)\n const wasMachine = existing?.method === 'ai'\n const method: TranslationMethod =\n machine || wasMachine ? 'ai' : contentHash === sourceHash ? 'copy' : 'human'\n await client.upsert(\n {\n ...key,\n locale,\n contentHash,\n contentChangedAt: now,\n sourceLocale,\n sourceHash,\n method,\n reviewedAt: null,\n reviewedBy: null,\n updatedAt: now,\n updatedBy: input.userId ?? null,\n },\n {\n target: ['entityType', 'entityId', 'locale'],\n set: {\n contentHash,\n ...((existing === null || existing.contentHash !== contentHash) && {\n contentChangedAt: now,\n }),\n sourceLocale,\n sourceHash,\n method,\n // Fresh machine output is unreviewed again; anything else keeps the\n // review it had.\n ...(machine && { reviewedAt: null, reviewedBy: null }),\n updatedAt: now,\n // WHO LAST WROTE THE TEXT — what \"an agent may not review its own\n // translation\" is checked against. So it moves only when this save\n // changed the words AND says who did: a discard, a publish or a\n // recording with no known user wrote nothing, and letting one of\n // them blank it would let an agent launder its own authorship\n // (write, stage a draft, discard it, then review).\n ...(authored && { updatedBy: input.userId }),\n },\n },\n )\n } catch (error) {\n // Deliberately swallowed — see the module docblock (\"never fails a save\").\n app.logger.warn(\n { err: error, entity: entityName, entityId, locale: input.locale },\n 'translation provenance: could not record a locale save',\n )\n }\n}\n\nasync function upsertContent(\n client: ReturnType<typeof provenanceClient>,\n key: { entityType: string; entityId: string; locale: string },\n contentHash: string,\n now: Date,\n userId: string | undefined,\n): Promise<void> {\n const existing = await client.findOne(key)\n if (existing && existing.contentHash === contentHash) return\n await client.upsert(\n {\n ...key,\n contentHash,\n contentChangedAt: now,\n updatedAt: now,\n updatedBy: userId ?? null,\n },\n {\n target: ['entityType', 'entityId', 'locale'],\n // The author moves only when one is known — see `authored` above.\n set: {\n contentHash,\n contentChangedAt: now,\n updatedAt: now,\n ...(userId !== undefined && { updatedBy: userId }),\n },\n },\n )\n}\n\n/**\n * Mark a translation reviewed. The caller checks permission.\n *\n * - `'none'` — nothing is recorded for that locale; there is nothing to review.\n * - `'self'` — `refuseSelf` was set and the reviewer is the one who last wrote\n * the text. A review is someone ELSE vouching for it: an agent that could\n * clear \"not reviewed\" on its own output would make the badge say nothing\n * (the approval queue refuses the asking principal for the same reason).\n */\nexport async function markTranslationReviewed(input: {\n entityType: string\n entityId: string\n locale: string\n userId: string\n refuseSelf?: boolean | undefined\n}): Promise<'reviewed' | 'none' | 'self'> {\n const client = provenanceClient()\n const key = { entityType: input.entityType, entityId: input.entityId, locale: input.locale }\n const existing = await client.findOne(key)\n if (!existing) return 'none'\n if (input.refuseSelf === true && existing.updatedBy === input.userId) return 'self'\n const now = new Date()\n // `updatedBy` is left alone: it is who last WROTE the text.\n await client.update(key, { reviewedAt: now, reviewedBy: input.userId, updatedAt: now })\n return 'reviewed'\n}\n\n/** Every stored provenance row for a set of documents of one entity type — ONE query. */\nexport async function getTranslationProvenanceBatch(\n entityType: string,\n entityIds: readonly string[],\n db?: Db,\n): Promise<Map<string, Map<string, TranslationProvenance>>> {\n const out = new Map<string, Map<string, TranslationProvenance>>()\n if (entityIds.length === 0) return out\n const rows = await provenanceClient(db).findMany({\n where: { entityType: { eq: entityType }, entityId: { in: [...entityIds] } },\n })\n for (const row of rows) {\n const id = String(row.entityId)\n let byLocale = out.get(id)\n if (!byLocale) {\n byLocale = new Map()\n out.set(id, byLocale)\n }\n const provenance = toProvenance(row as Row)\n byLocale.set(provenance.locale, provenance)\n }\n return out\n}\n\n/** What a translation's provenance says about it, against its source's. */\nexport interface TranslationFreshness {\n method: TranslationMethod | null\n sourceLocale: string | null\n /** The source text has changed since this translation was saved. */\n outdated: boolean\n /** When the source text changed — ISO, only when {@link outdated}. */\n outdatedSince: string | null\n /** Machine output nobody has marked reviewed. */\n machineUnreviewed: boolean\n reviewedAt: string | null\n}\n\n/** Compare one locale's provenance with its source's. `null` when nothing is recorded. */\nexport function translationFreshness(\n byLocale: ReadonlyMap<string, TranslationProvenance> | undefined,\n locale: string,\n): TranslationFreshness | null {\n const own = byLocale?.get(locale)\n if (!own) return null\n const source = own.sourceLocale !== null ? byLocale?.get(own.sourceLocale) : undefined\n const outdated =\n source !== undefined && own.sourceHash !== null && source.contentHash !== own.sourceHash\n return {\n method: own.method,\n sourceLocale: own.sourceLocale,\n outdated,\n outdatedSince: outdated && source ? new Date(source.contentChangedAt).toISOString() : null,\n machineUnreviewed: own.method === 'ai' && own.reviewedAt === null,\n reviewedAt: own.reviewedAt ? new Date(own.reviewedAt).toISOString() : null,\n }\n}\n\n// ---------------------------------------------------------------------------\n// The queue — recordings that must not be awaited by the write that caused them\n// ---------------------------------------------------------------------------\n\n/**\n * A recording waiting to run. `run` is bound to the async context it was\n * QUEUED in (`AsyncResource.bind`): the queue is drained from the completion of\n * whichever recording finished last, and without the binding a queued recording\n * would run under THAT request's context — another caller's door, user and\n * site (review, PR10).\n */\ninterface Pending {\n run: () => Promise<void>\n}\n\n/** Keyed so a burst of writes to one document and locale collapses to the last. */\nconst pending = new Map<string, Pending>()\n/** Recordings running now, by key — one document and locale is never recorded twice at once. */\nconst inFlight = new Map<string, Promise<void>>()\n\n/** Recordings in flight at once — far below any connection pool. */\nconst MAX_CONCURRENT = 2\n/**\n * Beyond this many waiting, new ones are dropped (and logged): a bulk import\n * must not queue without end, and each entry may hold a draft's data.\n */\nconst MAX_PENDING = 200\n\nfunction drain(): void {\n for (const [key, entry] of pending) {\n if (inFlight.size >= MAX_CONCURRENT) return\n // The same document and locale is already being recorded: this one waits\n // for it. Started together, the OLDER one could finish last and leave its\n // hash behind — two quick edits of one page are ordinary.\n if (inFlight.has(key)) continue\n pending.delete(key)\n const done = entry.run().finally(() => {\n inFlight.delete(key)\n drain()\n })\n inFlight.set(key, done)\n }\n}\n\n/**\n * Record a locale save WITHOUT being awaited by it: bounded, coalesced per\n * document and locale, and run under the caller's own request context.\n *\n * For a writer that holds a transaction (the block editor's draft write inside\n * op-commit's): awaiting a recording there would take a SECOND pool connection\n * while holding the first, and enough concurrent commits would each wait for a\n * connection none of them can release.\n */\nexport function enqueueLocaleSave(input: LocaleSaveInput): void {\n const app = appOrNone()\n if (!app) return\n const key = queueKey(input.entityName, input.entityId, input.locale)\n if (!pending.has(key) && pending.size >= MAX_PENDING) {\n app.logger.warn(\n {\n entity: input.entityName,\n entityId: input.entityId,\n locale: input.locale,\n waiting: pending.size,\n },\n 'translation provenance: too many recordings waiting — this save was not recorded',\n )\n return\n }\n pending.set(key, { run: AsyncResource.bind(() => recordLocaleSave(input)) })\n drain()\n}\n\n/**\n * A write that landed on the live row — the `'entity.writeListeners'`\n * contribution. Synchronous and `void`, per the slot's contract.\n *\n * A `delete` is NOT recorded and removes nothing: the event does not say\n * whether the row was trashed or purged, and a trashed document that is\n * restored must come back with what it was translated from and whether that\n * was reviewed. A purged document's rows are a few hundred bytes addressed by\n * a uuid that is never reused.\n */\nexport const translationProvenanceWriteListener: EntityWriteObserver = (event) => {\n if (event.action === 'delete') return\n const app = appOrNone()\n if (!app) return\n const entity = app.entities.get(event.entityName)\n if (!entity || !hasTranslatableText(entity)) return\n enqueueLocaleSave({\n entityName: event.entityName,\n entityId: event.id,\n locale: event.locale ?? DEFAULT_LOCALE_SENTINEL,\n userId: getContext()?.user?.id,\n })\n}\n\nfunction queueKey(entityName: string, entityId: string, locale: string): string {\n return `${entityName}:${entityId}:${locale}`\n}\n\n/**\n * Resolves once the queued recordings for ONE document and locale have run —\n * for a caller about to write that row itself, which must come last. Bounded:\n * it waits for that key only, never the whole queue, and gives up after\n * `maxMs` rather than hold a request on a recording that will not settle.\n */\nexport async function localeSaveSettled(\n entityName: string,\n entityId: string,\n locale: string,\n maxMs = 3000,\n): Promise<void> {\n const key = queueKey(entityName, entityId, locale)\n const deadline = Date.now() + maxMs\n while (Date.now() < deadline) {\n const running = inFlight.get(key)\n if (running !== undefined) {\n await Promise.race([running, new Promise((resolve) => setTimeout(resolve, 50))])\n continue\n }\n if (!pending.has(key)) return\n await new Promise((resolve) => setTimeout(resolve, 5))\n }\n}\n\n/** Resolves once every queued recording has run. For tests; a request uses {@link localeSaveSettled}. */\nexport async function provenanceQueueIdle(): Promise<void> {\n while (inFlight.size > 0 || pending.size > 0)\n await new Promise((resolve) => setTimeout(resolve, 5))\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { a as documentTextHash, c as hasTranslatableText, d as provenanceQueueIdle, f as recordLocaleSave, g as translationFreshness, h as translatableTreeFields, i as TranslationProvenance, l as localeSaveSettled, m as translatableTextFields, n as TranslationFreshness, o as enqueueLocaleSave, p as translatableFieldTexts, r as TranslationMethod, s as getTranslationProvenanceBatch, t as LocaleSaveInput, u as markTranslationReviewed } from "./translation-provenance--dHtcmfg.mjs";
2
+ export { type LocaleSaveInput, type TranslationFreshness, type TranslationMethod, type TranslationProvenance, documentTextHash, enqueueLocaleSave, getTranslationProvenanceBatch, hasTranslatableText, localeSaveSettled, markTranslationReviewed, provenanceQueueIdle, recordLocaleSave, translatableFieldTexts, translatableTextFields, translatableTreeFields, translationFreshness };
@@ -0,0 +1 @@
1
+ import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./translation-provenance-eqhsGAyi.mjs";export{u as documentTextHash,o as enqueueLocaleSave,c as getTranslationProvenanceBatch,i as hasTranslatableText,e as localeSaveSettled,s as markTranslationReviewed,l as provenanceQueueIdle,t as recordLocaleSave,a as translatableFieldTexts,d as translatableTextFields,n as translatableTreeFields,r as translationFreshness};
@@ -1,7 +1,6 @@
1
+ import { n as TranslationFreshness } from "./translation-provenance--dHtcmfg.mjs";
2
+
1
3
  //#region src/types.d.ts
2
- /**
3
- * Content package types
4
- */
5
4
  interface LocaleConfig {
6
5
  code: string;
7
6
  label: string;
@@ -105,6 +104,12 @@ interface TranslationFieldStatus {
105
104
  total: number;
106
105
  translated: number;
107
106
  missing: string[];
107
+ /**
108
+ * What the locale was last saved from (agent-access D017) — absent when
109
+ * nothing is recorded, which is every document last saved before provenance
110
+ * existed. Absent never means "up to date"; it means "unknown".
111
+ */
112
+ freshness?: TranslationFreshness;
108
113
  }
109
114
  interface TranslationStatus {
110
115
  [locale: string]: TranslationFieldStatus;
@@ -114,6 +119,10 @@ interface TranslationReport {
114
119
  complete: number;
115
120
  partial: number;
116
121
  missing: number;
122
+ /** Documents whose source text changed after this locale was last saved. */
123
+ outdated: number;
124
+ /** Documents whose text in this locale is machine output nobody has marked reviewed. */
125
+ machineUnreviewed: number;
117
126
  }
118
127
  interface LocalePublishStatus {
119
128
  locale: string;
@@ -138,4 +147,4 @@ interface LockInfo {
138
147
  }
139
148
  //#endregion
140
149
  export { LocaleDirection as a, LockInfo as c, TranslationReport as d, TranslationStatus as f, LocaleConfig as i, RoutingStrategy as l, VersionableOptions as m, DraftEntry as n, LocaleEntry as o, VersionEntry as p, I18nConfig as r, LocalePublishStatus as s, ContentConfig as t, TranslationFieldStatus as u };
141
- //# sourceMappingURL=types-B3K2p9t8.d.mts.map
150
+ //# sourceMappingURL=types-wMTglM4j.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-wMTglM4j.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAUiB,YAAA;EACf,IAAA;EACA,KAAK;AAAA;AAIP;AAAA,KAAY,eAAA;;;AAAe;AAQ3B;;;KAAY,eAAA;AAAe;AAU3B;;;;;;;AAV2B,UAUV,WAAA;EACf,IAAA;EACA,KAAA;EACA,OAAA;EACA,SAAA,EAAW,eAAe;EAC1B,SAAA;AAAA;;;;;;;UASe,UAAA;EACf,aAAA;EACA,eAAA,EAAiB,eAAe;AAAA;AAAA,UAGjB,aAAA;EACf,OAAA,EAAS,YAAY;EACrB,aAAA;EAAA;;;;;AAmC+B;AAOjC;;;;AAIU;AAOV;;;;;;;;;EAhCE,oBAAA;EA0Ce;;;;;;;;;;;;;EA5Bf,SAAA;IAAwB,OAAA;EAAA;AAAA;AAAA,UAOT,kBAAA;EAsBf;EApBA,SAAA;EAoBU;EAlBV,QAAQ;AAAA;AAAA,UAOO,YAAA,eAA2B,MAAA,oBAA0B,MAAA;EACpE,EAAA;EACA,QAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,KAAA,EAAO,OAAA,CAAQ,KAAA;EACf,MAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA;AAAA;AAAA,UAae,sBAAA;EACf,QAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;EAmBA;;;AAEiB;AAOnB;EAtBE,SAAA,GAAY,oBAAoB;AAAA;AAAA,UAGjB,iBAAA;EAAA,CACd,MAAA,WAAiB,sBAAsB;AAAA;AAAA,UAGzB,iBAAA;EACf,KAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EAqByB;EAnBzB,QAAA;EAmBwC;EAjBxC,iBAAA;AAAA;AAAA,UAOe,mBAAA;EACf,MAAA;EACA,MAAA;EACA,WAAA,EAAa,IAAI;AAAA;AAAA,UAOF,UAAA,eAAyB,MAAA,oBAA0B,MAAA;EAClE,EAAA;EACA,QAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;AAAA,UAOI,QAAA;EACf,QAAA;EACA,YAAA;EACA,QAAA,EAAU,IAAA;EACV,SAAA,EAAW,IAAI;AAAA"}
@@ -1,4 +1,4 @@
1
- import { m as VersionableOptions } from "./types-B3K2p9t8.mjs";
1
+ import { m as VersionableOptions } from "./types-wMTglM4j.mjs";
2
2
  import { Behavior, DateField, NumberField } from "@murumets-ee/entity";
3
3
 
4
4
  //#region src/behaviors/versionable.d.ts
@@ -15,4 +15,4 @@ declare function versionable(options?: VersionableOptions): Behavior<Versionable
15
15
  declare function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null;
16
16
  //#endregion
17
17
  export { getVersionableOptions as n, versionable as r, VersionableFields as t };
18
- //# sourceMappingURL=versionable-ByZgfpp0.d.mts.map
18
+ //# sourceMappingURL=versionable-D2JjselN.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"versionable-ByZgfpp0.d.mts","names":[],"sources":["../src/behaviors/versionable.ts"],"mappings":";;;;KAmBY,iBAAA;EACV,QAAA,EAAU,WAAA;IAAgB,QAAA;EAAA;EAC1B,YAAA,EAAc,SAAS;AAAA;AAAA,iBAQT,WAAA,CAAY,OAAA,GAAU,kBAAA,GAAqB,QAAA,CAAS,iBAAA;;;;iBAqDpD,qBAAA,CAAsB,SAAA,GAAY,QAAA,KAAa,QAAA,CAAS,kBAAA"}
1
+ {"version":3,"file":"versionable-D2JjselN.d.mts","names":[],"sources":["../src/behaviors/versionable.ts"],"mappings":";;;;KAmBY,iBAAA;EACV,QAAA,EAAU,WAAA;IAAgB,QAAA;EAAA;EAC1B,YAAA,EAAc,SAAS;AAAA;AAAA,iBAQT,WAAA,CAAY,OAAA,GAAU,kBAAA,GAAqB,QAAA,CAAS,iBAAA;;;;iBAqDpD,qBAAA,CAAsB,SAAA,GAAY,QAAA,KAAa,QAAA,CAAS,kBAAA"}
@@ -1,2 +1,2 @@
1
- import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{i as t,n}from"./errors-NRNILhhm.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{n as i}from"./plugin-DmyWYmuE.mjs";import{t as a}from"./locale-utils-B0mFoV47.mjs";import{a as o,i as s}from"./context-D7eEItCY.mjs";import{schemaRegistry as c}from"@murumets-ee/db";import{eq as l}from"drizzle-orm";import"server-only";function u(e){return e.match(/\S+|\s+/g)??[]}function d(e,t){if(e===t)return e?[{type:`equal`,value:e}]:[];if(!e)return[{type:`add`,value:t}];if(!t)return[{type:`remove`,value:e}];let n=u(e),r=u(t),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=1;e<=i;e++){let t=o[e],i=o[e-1];if(!(!t||!i))for(let o=1;o<=a;o++)n[e-1]===r[o-1]?t[o]=(i[o-1]??0)+1:t[o]=Math.max(i[o]??0,t[o-1]??0)}let s=[],c=i,l=a,d=[];for(;c>0||l>0;){let e=c>0?n[c-1]:void 0,t=l>0?r[l-1]:void 0;if(c>0&&l>0&&e!==void 0&&e===t)d.push({type:`equal`,value:e}),c--,l--;else if(l>0&&t!==void 0&&(c===0||(o[c]?.[l-1]??0)>=(o[c-1]?.[l]??0)))d.push({type:`add`,value:t}),l--;else if(e!==void 0)d.push({type:`remove`,value:e}),c--;else break}d.reverse();for(let e of d){let t=s[s.length-1];t&&t.type===e.type?t.value+=e.value:s.push({...e})}return s}function f(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function p(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function m(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??g(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:d(p(t),p(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:d(f(p(t)),f(p(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blockTree`:return{field:e,type:`blockTree`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function h(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=m(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function g(e,t){let n=e??t;if(typeof n==`number`)return`number`;if(typeof n==`boolean`)return`boolean`;if(n instanceof Date)return`date`;if(typeof n==`string`)return/^\d{4}-\d{2}-\d{2}/.test(n)?`date`:`text`;if(Array.isArray(n)){let e=n[0];return n.length>0&&typeof e==`object`&&e&&`type`in e&&`id`in e?`blockTree`:`json`}return typeof n==`object`?`json`:`unknown`}function _(e,t){if(typeof e==`string`||typeof t==`string`)return d(p(e),p(t))}function v(e){if(typeof e!=`object`||!e)return null;let t=e;if(typeof t.id!=`string`||typeof t.type!=`string`)return null;let n={...t.props??{}};return t.traits!==void 0&&(n.traits=t.traits),t.children!==void 0&&(n.children=t.children),{id:t.id,type:t.type,props:n}}function y(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:_(n,a)})}return r}function b(e){let t=new Map,n=0;for(let r of e){let e=v(r);e&&(t.set(e.id,{block:e,index:n}),n++)}return t}function x(e,t){let n=b(e),r=b(t),i=[],a=new Set;for(let[e,{block:t,index:o}]of r){a.add(e);let r=n.get(e);if(!r){i.push({blockId:e,blockType:t.type,status:`added`,toIndex:o});continue}let s=r.block,c=r.index,l=y(s.props,t.props),u=c!==o;l.length===0&&!u?i.push({blockId:e,blockType:t.type,status:`unchanged`,fromIndex:c,toIndex:o}):l.length>0?i.push({blockId:e,blockType:t.type,status:`changed`,fromIndex:c,toIndex:o,propDiffs:l}):i.push({blockId:e,blockType:t.type,status:`moved`,fromIndex:c,toIndex:o})}for(let[e,{block:t,index:r}]of n)a.has(e)||i.push({blockId:e,blockType:t.type,status:`removed`,fromIndex:r});return i}function S(e){return e===`_`||e===r().defaultLocale}function C(e){let{defaultLocale:t}=r();return S(e)?{defaultLocale:t}:{locale:e,defaultLocale:t}}async function w(e,t,n,o,c){if(c!==void 0&&e.db!==c)throw Error("applyLocaleRoutedUpdate: `tx` must be the context's own db handle");let l=await s(e);return e.admin.updateForLocale(t,n,o,{defaultLocale:r().defaultLocale,...c!==void 0&&{tx:c},...l!==null&&{localeStatusWriter:(e,t,n)=>(a(t),i(l,e,t,n))}})}const T=new Set([`id`,`createdAt`,`createdBy`,`updatedAt`,`updatedBy`]);function E(e,t){let n=e.allFields,r={};for(let e of Object.keys(t)){if(T.has(e)||e.startsWith(`_`)||!Object.hasOwn(n,e))continue;let i=n[e];i&&i.internal===!0||(r[e]=t[e])}return r}function D(e,t){return JSON.stringify(e)===JSON.stringify(t)}function O(e,t){let n={};for(let r of Object.keys(t))r===`id`||r.startsWith(`_`)||D(e[r],t[r])||(n[r]=t[r]);return n}var k=e({compareVersions:()=>M,createVersionSnapshot:()=>I,getVersion:()=>j,getVersions:()=>A,pruneVersions:()=>P,restoreVersion:()=>N,saveVersionSnapshot:()=>F,writeVersionColumn:()=>L});async function A(e,t,n){n?.locale!==void 0&&a(n.locale);let r=await o(e);return r?await r.findMany({where:{entityId:t,locale:n?.locale??`_`},orderBy:[{column:`version`,dir:`desc`}],...n?.limit===void 0?{}:{limit:n.limit},...n?.offset===void 0?{}:{offset:n.offset}}):[]}async function j(e,t,n,r=`_`){a(r);let i=await o(e);return i?await i.findOne({entityId:t,version:n,locale:r})??null:null}async function M(e,t,n,r,i=`_`){a(i);let[o,s]=await Promise.all([j(e,t,n,i),j(e,t,r,i)]);if(!o||!s)return null;let c=e.entity.allFields,l=h(o.data,s.data,c);for(let e of l)e.type===`blockTree`&&(e.blockDiffs=x(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:l,fromVersion:o,toVersion:s}}async function N(e,r,i,s=`_`){if(a(s),!await o(e))throw new n(e.entity.name,`versions`);let c=await j(e,r,i,s);if(!c)throw new t(r,i,s);let l=c.data;return w(e,r,E(e.entity,l),s)}async function P(e,t,n=`_`){if(a(n),!e.versionableOptions)return;let r=await o(e);if(!r)return;let i=e.versionableOptions.maxPerDoc,s=await r.findMany({where:{entityId:t,locale:n},orderBy:[{column:`version`,dir:`desc`}],limit:i});if(s.length<i)return;let c=s.map(e=>e.id);await r.deleteMany({entityId:t,locale:n,id:{notIn:c}})}async function F(e,t,n,r,i,s=`_`,c){a(s);let l=await o(e);if(!l)return;let[u]=await l.aggregate({select:{maxVersion:{fn:`max`,column:`version`}},where:{entityId:t,locale:s}}),d=Number(u?.maxVersion??0)+1,f=n.status??null;if(i){let e=await l.findOne({entityId:t,isAutosave:!0,version:d,locale:s});if(e)return await l.update({id:e.id},{data:n,delta:r,status:f,createdAt:new Date}),d}return await l.insert({entityId:t,version:d,locale:s,data:n,delta:r,status:f,createdBy:c?.id??null,createdByName:c?.name??null,isAutosave:i}),d}async function I(e,t,n,r,i,o){if(i!==void 0&&a(i),!e.versionableOptions)return;let s=i??`_`,c=await F(e,t,n,r?O(r,n):null,!1,s,o);c!==void 0&&s===`_`&&await L(e,t,c),await P(e,t,s)}async function L(e,t,n){let r=c.get(e.entity.name);r&&await e.db.update(r).set({_version:n}).where(l(r.id,t))}export{P as a,k as c,E as d,w as f,h,A as i,L as l,x as m,I as n,N as o,C as p,j as r,F as s,M as t,O as u};
2
- //# sourceMappingURL=versions-C5_IJpI8.mjs.map
1
+ import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{i as t,n}from"./errors-NRNILhhm.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{n as i}from"./plugin-CvPWz399.mjs";import{t as a}from"./locale-utils-B0mFoV47.mjs";import{a as o,i as s}from"./context-DtHgyrgb.mjs";import{schemaRegistry as c}from"@murumets-ee/db";import{eq as l}from"drizzle-orm";import"server-only";function u(e){return e.match(/\S+|\s+/g)??[]}function d(e,t){if(e===t)return e?[{type:`equal`,value:e}]:[];if(!e)return[{type:`add`,value:t}];if(!t)return[{type:`remove`,value:e}];let n=u(e),r=u(t),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=1;e<=i;e++){let t=o[e],i=o[e-1];if(!(!t||!i))for(let o=1;o<=a;o++)n[e-1]===r[o-1]?t[o]=(i[o-1]??0)+1:t[o]=Math.max(i[o]??0,t[o-1]??0)}let s=[],c=i,l=a,d=[];for(;c>0||l>0;){let e=c>0?n[c-1]:void 0,t=l>0?r[l-1]:void 0;if(c>0&&l>0&&e!==void 0&&e===t)d.push({type:`equal`,value:e}),c--,l--;else if(l>0&&t!==void 0&&(c===0||(o[c]?.[l-1]??0)>=(o[c-1]?.[l]??0)))d.push({type:`add`,value:t}),l--;else if(e!==void 0)d.push({type:`remove`,value:e}),c--;else break}d.reverse();for(let e of d){let t=s[s.length-1];t&&t.type===e.type?t.value+=e.value:s.push({...e})}return s}function f(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function p(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function m(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??g(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:d(p(t),p(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:d(f(p(t)),f(p(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blockTree`:return{field:e,type:`blockTree`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function h(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=m(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function g(e,t){let n=e??t;if(typeof n==`number`)return`number`;if(typeof n==`boolean`)return`boolean`;if(n instanceof Date)return`date`;if(typeof n==`string`)return/^\d{4}-\d{2}-\d{2}/.test(n)?`date`:`text`;if(Array.isArray(n)){let e=n[0];return n.length>0&&typeof e==`object`&&e&&`type`in e&&`id`in e?`blockTree`:`json`}return typeof n==`object`?`json`:`unknown`}function _(e,t){if(typeof e==`string`||typeof t==`string`)return d(p(e),p(t))}function v(e){if(typeof e!=`object`||!e)return null;let t=e;if(typeof t.id!=`string`||typeof t.type!=`string`)return null;let n={...t.props??{}};return t.traits!==void 0&&(n.traits=t.traits),t.children!==void 0&&(n.children=t.children),{id:t.id,type:t.type,props:n}}function y(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:_(n,a)})}return r}function b(e){let t=new Map,n=0;for(let r of e){let e=v(r);e&&(t.set(e.id,{block:e,index:n}),n++)}return t}function x(e,t){let n=b(e),r=b(t),i=[],a=new Set;for(let[e,{block:t,index:o}]of r){a.add(e);let r=n.get(e);if(!r){i.push({blockId:e,blockType:t.type,status:`added`,toIndex:o});continue}let s=r.block,c=r.index,l=y(s.props,t.props),u=c!==o;l.length===0&&!u?i.push({blockId:e,blockType:t.type,status:`unchanged`,fromIndex:c,toIndex:o}):l.length>0?i.push({blockId:e,blockType:t.type,status:`changed`,fromIndex:c,toIndex:o,propDiffs:l}):i.push({blockId:e,blockType:t.type,status:`moved`,fromIndex:c,toIndex:o})}for(let[e,{block:t,index:r}]of n)a.has(e)||i.push({blockId:e,blockType:t.type,status:`removed`,fromIndex:r});return i}function S(e){return e===`_`||e===r().defaultLocale}function C(e){let{defaultLocale:t}=r();return S(e)?{defaultLocale:t}:{locale:e,defaultLocale:t}}async function w(e,t,n,o,c){if(c!==void 0&&e.db!==c)throw Error("applyLocaleRoutedUpdate: `tx` must be the context's own db handle");let l=await s(e);return e.admin.updateForLocale(t,n,o,{defaultLocale:r().defaultLocale,...c!==void 0&&{tx:c},...l!==null&&{localeStatusWriter:(e,t,n)=>(a(t),i(l,e,t,n))}})}const T=new Set([`id`,`createdAt`,`createdBy`,`updatedAt`,`updatedBy`]);function E(e,t){let n=e.allFields,r={};for(let e of Object.keys(t)){if(T.has(e)||e.startsWith(`_`)||!Object.hasOwn(n,e))continue;let i=n[e];i&&i.internal===!0||(r[e]=t[e])}return r}function D(e,t){return JSON.stringify(e)===JSON.stringify(t)}function O(e,t){let n={};for(let r of Object.keys(t))r===`id`||r.startsWith(`_`)||D(e[r],t[r])||(n[r]=t[r]);return n}var k=e({compareVersions:()=>M,createVersionSnapshot:()=>I,getVersion:()=>j,getVersions:()=>A,pruneVersions:()=>P,restoreVersion:()=>N,saveVersionSnapshot:()=>F,writeVersionColumn:()=>L});async function A(e,t,n){n?.locale!==void 0&&a(n.locale);let r=await o(e);return r?await r.findMany({where:{entityId:t,locale:n?.locale??`_`},orderBy:[{column:`version`,dir:`desc`}],...n?.limit===void 0?{}:{limit:n.limit},...n?.offset===void 0?{}:{offset:n.offset}}):[]}async function j(e,t,n,r=`_`){a(r);let i=await o(e);return i?await i.findOne({entityId:t,version:n,locale:r})??null:null}async function M(e,t,n,r,i=`_`){a(i);let[o,s]=await Promise.all([j(e,t,n,i),j(e,t,r,i)]);if(!o||!s)return null;let c=e.entity.allFields,l=h(o.data,s.data,c);for(let e of l)e.type===`blockTree`&&(e.blockDiffs=x(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:l,fromVersion:o,toVersion:s}}async function N(e,r,i,s=`_`){if(a(s),!await o(e))throw new n(e.entity.name,`versions`);let c=await j(e,r,i,s);if(!c)throw new t(r,i,s);let l=c.data;return w(e,r,E(e.entity,l),s)}async function P(e,t,n=`_`){if(a(n),!e.versionableOptions)return;let r=await o(e);if(!r)return;let i=e.versionableOptions.maxPerDoc,s=await r.findMany({where:{entityId:t,locale:n},orderBy:[{column:`version`,dir:`desc`}],limit:i});if(s.length<i)return;let c=s.map(e=>e.id);await r.deleteMany({entityId:t,locale:n,id:{notIn:c}})}async function F(e,t,n,r,i,s=`_`,c){a(s);let l=await o(e);if(!l)return;let[u]=await l.aggregate({select:{maxVersion:{fn:`max`,column:`version`}},where:{entityId:t,locale:s}}),d=Number(u?.maxVersion??0)+1,f=n.status??null;if(i){let e=await l.findOne({entityId:t,isAutosave:!0,version:d,locale:s});if(e)return await l.update({id:e.id},{data:n,delta:r,status:f,createdAt:new Date}),d}return await l.insert({entityId:t,version:d,locale:s,data:n,delta:r,status:f,createdBy:c?.id??null,createdByName:c?.name??null,isAutosave:i}),d}async function I(e,t,n,r,i,o){if(i!==void 0&&a(i),!e.versionableOptions)return;let s=i??`_`,c=await F(e,t,n,r?O(r,n):null,!1,s,o);c!==void 0&&s===`_`&&await L(e,t,c),await P(e,t,s)}async function L(e,t,n){let r=c.get(e.entity.name);r&&await e.db.update(r).set({_version:n}).where(l(r.id,t))}export{P as a,k as c,E as d,w as f,h,A as i,L as l,x as m,I as n,N as o,C as p,j as r,F as s,M as t,O as u};
2
+ //# sourceMappingURL=versions-B0_9TtZ0.mjs.map