@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.
- package/dist/admin.mjs +1 -1
- package/dist/chrome-provider.d.mts +1 -1
- package/dist/chrome-provider.mjs +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/context-DtHgyrgb.mjs +2 -0
- package/dist/{context-D7eEItCY.mjs.map → context-DtHgyrgb.mjs.map} +1 -1
- package/dist/document-site-B8tmEqLw.mjs +2 -0
- package/dist/document-site-B8tmEqLw.mjs.map +1 -0
- package/dist/entities/index.d.mts +2 -2
- package/dist/entities/index.mjs +1 -1
- package/dist/{index-C3VyZLjf.d.mts → index-Bni-PiIz.d.mts} +2 -2
- package/dist/{index-C3VyZLjf.d.mts.map → index-Bni-PiIz.d.mts.map} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/locale-registry-COsJrVZl.mjs +2 -0
- package/dist/locale-registry-COsJrVZl.mjs.map +1 -0
- package/dist/lock-service.d.mts +1 -1
- package/dist/plugin-CvPWz399.mjs +2 -0
- package/dist/plugin-CvPWz399.mjs.map +1 -0
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/resolve.mjs +1 -1
- package/dist/routes-D-NWP0hK.mjs +2 -0
- package/dist/routes-D-NWP0hK.mjs.map +1 -0
- package/dist/{routing-strategy-DV7KGarX.d.mts → routing-strategy-DVWB7qPp.d.mts} +2 -2
- package/dist/{routing-strategy-DV7KGarX.d.mts.map → routing-strategy-DVWB7qPp.d.mts.map} +1 -1
- package/dist/seed.d.mts.map +1 -1
- package/dist/seed.mjs +1 -1
- package/dist/seed.mjs.map +1 -1
- package/dist/site-chrome-BAtQaAqC.mjs +2 -0
- package/dist/{site-chrome-AffFxYrV.mjs.map → site-chrome-BAtQaAqC.mjs.map} +1 -1
- package/dist/{site-chrome-DJ8IJUi1.d.mts → site-chrome-BiV3zhw0.d.mts} +2 -2
- package/dist/{site-chrome-DJ8IJUi1.d.mts.map → site-chrome-BiV3zhw0.d.mts.map} +1 -1
- package/dist/translation-provenance--dHtcmfg.d.mts +253 -0
- package/dist/translation-provenance--dHtcmfg.d.mts.map +1 -0
- package/dist/translation-provenance-eqhsGAyi.mjs +2 -0
- package/dist/translation-provenance-eqhsGAyi.mjs.map +1 -0
- package/dist/translation.d.mts +2 -0
- package/dist/translation.mjs +1 -0
- package/dist/{types-B3K2p9t8.d.mts → types-wMTglM4j.d.mts} +13 -4
- package/dist/types-wMTglM4j.d.mts.map +1 -0
- package/dist/{versionable-ByZgfpp0.d.mts → versionable-D2JjselN.d.mts} +2 -2
- package/dist/{versionable-ByZgfpp0.d.mts.map → versionable-D2JjselN.d.mts.map} +1 -1
- package/dist/{versions-C5_IJpI8.mjs → versions-B0_9TtZ0.mjs} +2 -2
- package/dist/{versions-C5_IJpI8.mjs.map → versions-B0_9TtZ0.mjs.map} +1 -1
- package/package.json +14 -10
- package/dist/context-D7eEItCY.mjs +0 -2
- package/dist/locale-registry-Bn6e_nM7.mjs +0 -2
- package/dist/locale-registry-Bn6e_nM7.mjs.map +0 -1
- package/dist/plugin-DmyWYmuE.mjs +0 -2
- package/dist/plugin-DmyWYmuE.mjs.map +0 -1
- package/dist/registry-CaPbY5lm.mjs +0 -2
- package/dist/registry-CaPbY5lm.mjs.map +0 -1
- package/dist/routes-B6TJ5qiR.mjs +0 -2
- package/dist/routes-B6TJ5qiR.mjs.map +0 -1
- package/dist/site-chrome-AffFxYrV.mjs +0 -2
- package/dist/types-B3K2p9t8.d.mts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-registry-COsJrVZl.mjs","names":[],"sources":["../src/locale-registry.ts"],"sourcesContent":["/**\n * The content-locale **registry resolvers** (plan/config phase 05 §7, D004/D009/SD006).\n *\n * The effective content-locale set = **read-time-merge** of the `content()` shipped\n * defaults ⊕ the DB `locale` config-entity override rows. Resolved **once per request**\n * at context establishment (the admin API handler + `withAdminContext`) and threaded\n * onto {@link RequestContext.availableLocales}, so `validateLocale`'s synchronous\n * whitelist reflects runtime-added / disabled locales with **no rebuild and no\n * globalThis cache** — multi-process / multi-tenant correct.\n *\n * The registry read is a **system-context** query: managing the language set is\n * admin-only (`locale:*`), but every role must resolve valid locales to edit content,\n * exactly like the app-wide roles read. Reads degrade to the static `content()`\n * defaults on any error (fresh app pre-migration, transient DB failure) — deny-by-\n * default is preserved (a not-yet-known locale simply isn't whitelisted).\n */\n\nimport {\n createSystemReachResolver,\n getApp,\n getCurrentSite,\n mergeConfigOverrides,\n readSiteRecord,\n resolveImplicitSite,\n type SiteRecordView,\n type ToolkitApp,\n} from '@murumets-ee/core'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport { getContentConfig } from './content-config-store.js'\nimport { i18nSettings, ROUTING_STRATEGIES } from './i18n-settings.js'\nimport { Locale } from './locale-entity.js'\nimport type { I18nConfig, LocaleDirection, LocaleEntry, RoutingStrategy } from './types.js'\n\n/**\n * Elevated **reach** resolver for the app-wide registry read (the roles-read\n * analog). It grants `view` and nothing else — this module only ever READS the\n * `locale` rows, so an all-allow checker would have handed every caller of this\n * resolver a write grant it has no use for. The named constructor owns that\n * exception centrally rather than leaving it to be re-derived per call site;\n * a hand-written `checker: () => true` here is refused by `verify:boundaries`.\n *\n * The explicit id is the audit identity — two elevated readers sharing the\n * default `'system'` are indistinguishable in the log.\n */\nconst SYSTEM_CONTEXT = createSystemReachResolver('locale-registry')\n\n/** The `content()`-shipped locales projected to full {@link LocaleEntry}s (all enabled). */\nfunction shippedDefaults(): LocaleEntry[] {\n return getContentConfig().locales.map((l, i) => ({\n code: l.code,\n label: l.label,\n enabled: true,\n direction: 'ltr' as LocaleDirection,\n sortOrder: i,\n }))\n}\n\n/** Ascending by `sortOrder`, then `code` for a stable tie-break. */\nfunction bySortOrder(a: LocaleEntry, b: LocaleEntry): number {\n return a.sortOrder - b.sortOrder || a.code.localeCompare(b.code)\n}\n\n/**\n * Defensive upper bound on `locale` rows to read (a tenant realistically has <20). The\n * Languages-editor route MUST reconcile against the SAME bound so it can't miss/strand\n * rows — see admin-ui `locale-routes` `MAX_EXISTING_LOCALE_ROWS` (kept in sync).\n */\nconst MAX_LOCALE_ROWS = 1000\n\n/** Read + normalize the DB `locale` override rows (system context). */\nasync function readLocaleRows(app: ToolkitApp): Promise<LocaleEntry[]> {\n if (!app.entities?.has('locale')) return []\n const client = new QueryClient({\n entity: Locale,\n db: app.db.readOnly,\n logger: app.logger.child({ registry: 'locale' }),\n contextResolver: SYSTEM_CONTEXT,\n })\n const rows = await client.findMany({ limit: MAX_LOCALE_ROWS })\n return rows.map((r) => {\n const row = r as Record<string, unknown>\n return {\n code: String(row.code),\n label: typeof row.label === 'string' && row.label.length > 0 ? row.label : String(row.code),\n enabled: row.enabled !== false,\n direction: row.direction === 'rtl' ? 'rtl' : 'ltr',\n sortOrder: typeof row.sortOrder === 'number' ? row.sortOrder : 0,\n }\n })\n}\n\n/**\n * The effective content-locale set — shipped defaults ⊕ DB overrides, sorted by\n * `sortOrder`. Includes **disabled** locales (listable in the Languages editor);\n * callers that want only switchable/valid locales use {@link enabledLocaleCodes}.\n */\nexport async function resolveEffectiveLocales(app: ToolkitApp): Promise<LocaleEntry[]> {\n let rows: LocaleEntry[]\n try {\n rows = await readLocaleRows(app)\n } catch (err) {\n app.logger.warn({ err }, 'locale registry read failed — falling back to content() defaults')\n return [...shippedDefaults()].sort(bySortOrder)\n }\n const merged = mergeConfigOverrides<LocaleEntry, LocaleEntry>({\n defaults: shippedDefaults(),\n overrides: rows,\n idOfDefault: (l) => l.code,\n idOfOverride: (r) => r.code,\n // No `isDisabled`: a disabled locale stays in the merged list so the editor\n // can re-enable it — the switcher + validateLocale filter on `enabled`.\n })\n return (merged as LocaleEntry[]).sort(bySortOrder)\n}\n\n/** The **enabled** codes from an effective set — the switcher + validateLocale whitelist. */\nexport function enabledLocaleCodes(entries: readonly LocaleEntry[]): string[] {\n return entries.filter((e) => e.enabled).map((e) => e.code)\n}\n\n/**\n * The resolved global i18n singletons (`i18n` settings ⊕ `content()` defaults).\n * `defaultLocale` is guaranteed to be an **enabled** locale: the configured value\n * wins if enabled, else the `content()` default if enabled, else the first enabled\n * locale (last-resort, deterministic). Pass `entries` to reuse an already-resolved\n * effective set (avoids a second registry read within one request).\n */\nexport async function resolveI18nConfig(\n app: ToolkitApp,\n entries?: readonly LocaleEntry[],\n): Promise<I18nConfig> {\n const effective = entries ?? (await resolveEffectiveLocales(app))\n const enabled = enabledLocaleCodes(effective)\n const shippedDefault = getContentConfig().defaultLocale\n\n let values: Record<string, unknown> = {}\n try {\n // Dynamic import: `@murumets-ee/settings/runtime` evaluates the settings schema\n // (`defineTable`) at module load; importing it eagerly would drag that graph into\n // `@murumets-ee/content`'s main index (where only `resolveEffectiveLocales` — which\n // needs no settings — is on the hot import path). Loaded lazily here, only when the\n // i18n singletons are actually read. (Same pattern as rent/ticketing/consent.)\n const { createSettingsClient } = await import('@murumets-ee/settings/runtime')\n const client = createSettingsClient(i18nSettings, { app })\n values = (await client.getAll()) ?? {}\n } catch (err) {\n app.logger.warn({ err }, 'i18n settings read failed — falling back to content() defaults')\n }\n\n const configuredDefault =\n typeof values.defaultLocale === 'string' && values.defaultLocale.length > 0\n ? values.defaultLocale\n : shippedDefault\n\n const defaultLocale = enabled.includes(configuredDefault)\n ? configuredDefault\n : enabled.includes(shippedDefault)\n ? shippedDefault\n : (enabled[0] ?? shippedDefault)\n\n const rawStrategy = values.routingStrategy\n const routingStrategy: RoutingStrategy = (ROUTING_STRATEGIES as readonly string[]).includes(\n typeof rawStrategy === 'string' ? rawStrategy : '',\n )\n ? (rawStrategy as RoutingStrategy)\n : 'no-prefix-default'\n\n return { defaultLocale, routingStrategy }\n}\n\n/** What {@link resolveSiteI18n} returns. */\nexport interface SiteI18n {\n /** The site resolved, or `undefined` when none could be named (see below). */\n site: string | undefined\n /** The effective registry narrowed to the site (disabled locales kept). */\n entries: LocaleEntry[]\n /**\n * The site's PRESENTATION defaults: the locale a switcher, a sitemap or an\n * unlocalised request falls back to (an ENABLED locale of the site), and its\n * routing strategy.\n *\n * ⚠️ Not the STORAGE default. Which locale a row's base columns hold is an\n * install-wide fact (`content({ defaultLocale })`, `i18n.defaultLocale`) —\n * shared rows (media) are written from every site, so it cannot vary by\n * site. Read {@link install} for it.\n */\n i18n: I18nConfig\n /** The install-wide i18n singletons — the storage default. */\n install: I18nConfig\n}\n\n/** The site of `options.site`, the request, or the one site of a single-site install. */\nasync function nameSite(app: ToolkitApp, site: string | undefined): Promise<string | undefined> {\n return site ?? getCurrentSite() ?? (await resolveImplicitSite(app).catch(() => undefined))\n}\n\n/**\n * The site's registry row. A failed read PROPAGATES: answering the install-wide\n * set instead would silently lift the site's locale restriction (fail open).\n */\nfunction siteRecord(app: ToolkitApp, site: string): Promise<SiteRecordView | undefined> {\n return readSiteRecord(app, site)\n}\n\nfunction narrowTo(effective: LocaleEntry[], record: SiteRecordView | undefined): LocaleEntry[] {\n const served = record?.locales ?? []\n return served.length === 0 ? effective : effective.filter((e) => served.includes(e.code))\n}\n\n/**\n * The effective registry narrowed to ONE SITE's locales (plan `multisite`\n * phase 03) — the whitelist alone, without the i18n settings read, for the\n * per-request callers (the admin API handler) that need only the set.\n */\nexport async function resolveSiteLocales(\n app: ToolkitApp,\n options: { site?: string; entries?: readonly LocaleEntry[] } = {},\n): Promise<{ site: string | undefined; entries: LocaleEntry[] }> {\n const effective = [...(options.entries ?? (await resolveEffectiveLocales(app)))]\n const site = await nameSite(app, options.site)\n if (site === undefined) return { site, entries: effective }\n return { site, entries: narrowTo(effective, await siteRecord(app, site)) }\n}\n\n/**\n * The locale set and presentation defaults of ONE SITE (plan `multisite` D007,\n * phase 03): the effective registry narrowed to the site record's `locales`\n * (empty = every locale), with the site's own default and routing strategy.\n *\n * - `entries` keeps disabled locales (like {@link resolveEffectiveLocales});\n * the whitelist is `enabledLocaleCodes(entries)`.\n * - `i18n.defaultLocale` is an ENABLED locale of the site. On an app serving\n * several sites the record's `defaultLocale` / `routingStrategy` win; on a\n * single-site install the install's i18n settings stay authoritative (the\n * record only holds a one-time copy of them, and the Languages editor writes\n * the settings), so nothing about a one-site install changes.\n * - The site is `options.site`, else the request's, else the single site of a\n * one-site install. With none of those (a multi-site request that names no\n * site) the install-wide set is returned — what a SHARED entity (media) edits\n * against; a bound entity refuses on its own, without a site.\n */\nexport async function resolveSiteI18n(\n app: ToolkitApp,\n options: { site?: string; entries?: readonly LocaleEntry[] } = {},\n): Promise<SiteI18n> {\n const effective = [...(options.entries ?? (await resolveEffectiveLocales(app)))]\n const install = await resolveI18nConfig(app, effective)\n const site = await nameSite(app, options.site)\n if (site === undefined) return { site, entries: effective, i18n: install, install }\n\n const record = await siteRecord(app, site)\n const narrowed = narrowTo(effective, record)\n const enabled = enabledLocaleCodes(narrowed)\n const multiSite = app.config.sites?.multiple === true\n const wanted = multiSite ? record?.defaultLocale : undefined\n const defaultLocale =\n wanted !== undefined && enabled.includes(wanted)\n ? wanted\n : enabled.includes(install.defaultLocale)\n ? install.defaultLocale\n : (enabled[0] ?? install.defaultLocale)\n const strategy = multiSite ? record?.routingStrategy : undefined\n const routingStrategy: RoutingStrategy = (ROUTING_STRATEGIES as readonly string[]).includes(\n strategy ?? '',\n )\n ? (strategy as RoutingStrategy)\n : install.routingStrategy\n return { site, entries: narrowed, i18n: { defaultLocale, routingStrategy }, install }\n}\n\n/**\n * The install's base language for the `'sites.baseLocale'` contribution\n * (multisite F034) — the default of {@link resolveI18nConfig}, which is the\n * language every row's base columns hold. `undefined` when the app has no\n * language registry, so the site registry's guard then applies no rule: the\n * same answer the Sites API gives.\n */\nexport async function resolveInstallBaseLocale(): Promise<string | undefined> {\n const app = getApp()\n if (!app.entities.has('locale')) return undefined\n return (await resolveI18nConfig(app)).defaultLocale\n}\n"],"mappings":"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"}
|
package/dist/lock-service.d.mts
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./versionable-Br_G5VGR.mjs";import{r as t}from"./errors-NRNILhhm.mjs";import{r as n,t as r}from"./locale-entity-CoSq3C77.mjs";import{n as i,t as a}from"./content-config-store-8omQF5p5.mjs";import{r as o}from"./routing-strategy-D6B4ngT9.mjs";import{g as s,h as c,m as l,n as u,p as d}from"./translation-provenance-eqhsGAyi.mjs";import"./locale-utils-B0mFoV47.mjs";import{n as f}from"./document-site-B8tmEqLw.mjs";import{n as p}from"./chrome-entity-names-zYJzP-_U.mjs";import{r as m}from"./detail-template-names-CD1Y9n1U.mjs";import{c as h,i as g,n as _,o as v,r as y,t as b}from"./schema-BYrYzsmO.mjs";import{l as x,t as S,u as C}from"./schema-LlK6EkOJ.mjs";import{r as w,t as T}from"./routes-D-NWP0hK.mjs";import{n as E}from"./locks-5779ugSW.mjs";import{MAX_PATH_DEPTH as ee,buildPartitionCondition as D,hasPathParent as O,isSiteBound as k,resolveEffectiveSlugMode as A,resolvePattern as j}from"@murumets-ee/entity";import{definePlugin as M,getApp as N,getContext as P,getCurrentSite as F,requireRequestSite as I,resolveImplicitSite as L,runAsCli as R,runInEverySite as z,runInSite as B}from"@murumets-ee/core";import{column as V,defineTable as te,schemaRegistry as H}from"@murumets-ee/db";import{and as U,asc as W,eq as G,getTableColumns as ne,inArray as re,sql as ie}from"drizzle-orm";import{createHash as ae}from"node:crypto";import{z as K}from"zod";const oe=Symbol.for(`@murumets-ee/content:locale-status-writer`);async function q(e,t,n,r){let i=r===`published`?new Date:null;await e.upsert({entityId:t,locale:n,status:r,publishedAt:i},{target:[`entityId`,`locale`],set:{status:r,publishedAt:i}})}function se(e){globalThis[oe]=(n,r)=>{let{localeStatus:i}=c(n.name);if(i)return async(n,a,o,s)=>{let c=P()?.availableLocales??e;if(!c.includes(a))throw new t(a,[...c]);await q(i.makeClient(r,s),n,a,o)}}}function ce(){return{async writeDraft({documentType:e,documentId:t,locale:n,dataPatch:r,user:i,tx:a}){let{drafts:o}=c(e);if(!o)return!1;let s=o.makeClient(a,await f(e)),l=new Date,d=await s.upsert({entityId:t,locale:n,data:r,createdBy:i.id,createdByName:i.name??null,createdAt:l,updatedAt:l},{target:[`entityId`,`locale`],set:{updatedAt:l},mergeJsonb:[`data`]});return u({entityName:e,entityId:t,locale:n,userId:i.id,draftData:d.data??r}),!0},async readDraft({documentType:e,documentId:t,locale:n,db:r}){let{drafts:i}=c(e);if(!i)return null;let a=await i.makeClient(r,await f(e)).findOne({entityId:t,locale:n});return a?{data:a.data??{},createdBy:a.createdBy??null,createdByName:a.createdByName??null,updatedAt:a.updatedAt??null}:null},async readLocaleStatus({documentType:e,documentId:t,locale:n,db:r}){let{localeStatus:i}=c(e);if(!i)return null;let a=await i.makeClient(r,await f(e)).findOne({entityId:t,locale:n});return a?a.status===`published`?`published`:`draft`:null},async hasAnyDraft({documentType:e,documentId:t,db:n}){let{drafts:r}=c(e);return r?r.makeClient(n,await f(e)).exists({entityId:t}):!1},async hasAnyLocaleActivity({documentType:e,documentId:t,db:n}){let{localeStatus:r}=c(e),i=await f(e);if(r&&await r.makeClient(n,i).exists({entityId:t}))return!0;let a=H.get(`${e}_translations`);if(a){let r=a.entityId;if(r!==void 0){let o=i===void 0?G(r,t):U(G(r,t),D(a,{site:i},e));if((await n.select({one:ie`1`}).from(a).where(o).limit(1)).length>0)return!0}}return!1}}}function le(e){let{defaultLocale:t}=a();return e===`_`||e===t?{defaultLocale:t}:{locale:e,defaultLocale:t}}function ue(e){let{defaultLocale:t}=a();return e===`_`||e===t?`_`:e}async function de(e,t,n,r){let{drafts:i}=c(e);if(!i)return;let a=await i.makeClient(r,await f(e)).findOne({entityId:t,locale:n});return a?a.data??void 0:void 0}function fe(){return{async snapshotCurrentState({documentType:t,documentId:n,locale:r,user:i}){let a=N(),o=a.entities.get(t);if(!o)return!1;let s=e(o.behaviors);if(!s)return!1;let c=a.getClient(o),l=c.getDb(),u=ue(r),d=await c.findById(n,le(u));if(!d)return!1;let f=await de(t,n,u,l),p=f?{...d,...f}:{...d},[{createContentContext:m},{createVersionSnapshot:h}]=await Promise.all([import(`./context-DtHgyrgb.mjs`).then(e=>e.t),import(`./versions-B0_9TtZ0.mjs`).then(e=>e.c)]);return await h(m({admin:c,entity:o},s),n,p,d,u,{id:i.id,...i.name==null?{}:{name:i.name}}),!0}}}async function pe(e,t,n){let r=ne(b),i=r[y],a=r.entityType,o=r.entityId;if(!i||!a||!o)throw Error(`toolkit_paths is missing '${y}'/entity columns`);let s=await e.selectDistinct({site:i}).from(b).where(U(G(a,t),G(o,n))).limit(2);if(s.length>1)return null;let c=s[0]?.site;return typeof c==`string`?c:void 0}const me=Symbol.for(`@murumets-ee/content:path-writers-factory`),J=Symbol.for(`@murumets-ee/content:cascade-trigger-factory`);function he(e){globalThis[J]=e}function ge(){let e=globalThis[J];return typeof e==`function`?e:void 0}function _e(e,t){return async r=>{let i=await ve(e,t,r);if(i===null)return;let a=_.makeClient(e.db.readWrite,i),o=F();if(o!==void 0&&o!==i&&!(r.action===`delete`||r.action===`release`)){e.logger.warn({entity:r.entityName,id:r.id,site:i,requestSite:o},"toolkit_paths: a cascade reached a document on another site — its path is left for `lumi paths:rebuild` on that site rather than recomputed from this request");return}return o===void 0&&r.viaCascade===!0&&k(t)?B(i,()=>n(a,r)):n(a,r)};async function n(n,r){if(r.action===`delete`){await n.deleteMany({entityType:r.entityName,entityId:r.id});return}if(r.action===`release`){await n.deleteMany({entityType:r.entityName,entityId:r.id,isCanonical:!0});return}let i=a(),o=i.defaultLocale,s=i.reservedPathPrefixes??[],c=r.locale?[r.locale]:i.locales.map(e=>e.code),l=r.viaCascade===!0?e.getSystemClient(t):e.getClient(t),u=async n=>{if(!O(t))return;let i=ge();if(i)try{await i(e,t,r.id,n)}catch(t){e.logger.warn({err:t,entity:r.entityName,id:r.id,locale:n},`toolkit_paths: descendant-cascade trigger threw — the entity write already committed, so tolerating and continuing (paths are rebuildable, D005)`)}};for(let i of c){let a=i===r.locale?r.row??null:await l.findById(r.id,{locale:i,defaultLocale:o});if(!a){await v({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:null,skipReason:`row-unreadable`,reservedPathPrefixes:s,logger:e.logger});continue}let c=await x({entity:t,row:a,locale:i,defaultLocale:o,pathsClient:n}),d=await v({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},reservedPathPrefixes:s,logger:e.logger});(d.outcome===`renamed`||r.action===`restore`&&d.outcome===`created`)&&await u(i)}}}async function ve(e,t,n){if(n.viaCascade===!0&&k(t)){let t=await pe(e.db.readWrite,n.entityName,n.id);if(t===null)return e.logger.warn({entity:n.entityName,id:n.id},"toolkit_paths: a document has path rows on SEVERAL sites — refusing to guess which one this cascade maintains; `lumi paths:rebuild` per site repairs it"),null;if(t!==void 0)return t;let r=F();if(r!==void 0)return r;let i=await L(e).catch(()=>void 0);return i===void 0?(e.logger.warn({entity:n.entityName,id:n.id,action:n.action},"toolkit_paths: cascade child has no path rows and the request names no site — nothing to maintain here; `lumi paths:rebuild` recomputes it per site"),null):i}return I(`toolkit_paths`,e)}function ye(e,t){return async n=>{let r=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),i=a(),o=i.defaultLocale,s=i.reservedPathPrefixes??[],c=n.locale??o,l=null;n.id!==void 0&&(l=await e.getClient(t).findById(n.id,{locale:c,defaultLocale:o}));let u=A(n.data.slugMode,l?.slugMode);await C({entity:t,data:n.data,currentRow:l,id:n.id,locale:c,defaultLocale:o,slugMode:u,reservedPathPrefixes:s,pathsClient:r})}}function be(){globalThis[me]=(e,t)=>({pathIndexWriter:_e(e,t),pathConflictGuard:ye(e,t)})}const Y=K.object({entityType:K.string().min(1),parentId:K.string().uuid(),locale:K.string().min(1)}),X=K.object({entity:K.string().min(1).optional(),locale:K.string().min(1).optional(),dryRun:K.boolean()}),Z=K.object({dryRun:K.boolean()});async function xe(e,t,n={}){let{entityType:r,parentId:i,locale:o}=t,s=n.maxItems??2e3,c=n.concurrency??5,l={processed:0,regenerated:0,unchanged:0,skipped:0,failed:0,truncated:!1},u=e=>({phase:e,...l}),d=()=>({entityType:r,parentId:i,locale:o,cap:s,...l}),f=e.entities.get(r);if(!f)return e.logger.warn({entityType:r,parentId:i,locale:o},`paths-cascade: unknown entity type — nothing to regenerate`),n.onProgress?.(u(`done`)),d();if(!O(f))return e.logger.warn({entityType:r,parentId:i,locale:o},`paths-cascade: entity is not a pathParent() hierarchy — nothing to regenerate`),n.onProgress?.(u(`done`)),d();let p=a(),m=p.defaultLocale,g=p.reservedPathPrefixes??[],y=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),b=e.getClient(f),S=b.getTable().parent,C=async t=>{let n=t.id;try{let i=await x({entity:f,row:t,locale:o,defaultLocale:m,pathsClient:y});switch((await v({pathsClient:y,entityType:r,entityId:n,locale:o,path:i.path,...i.skipReason!==void 0&&{skipReason:i.skipReason},reservedPathPrefixes:g,logger:e.logger})).outcome){case`created`:case`renamed`:l.regenerated+=1;break;case`unchanged`:l.unchanged+=1;break;default:l.skipped+=1;break}}catch(t){l.failed+=1,e.logger.warn({err:t,entityType:r,id:n,locale:o},`paths-cascade: failed to regenerate one descendant path — continuing (per-item isolation)`)}},w=[i];for(;w.length>0;){let e=s-l.processed;if(e<=0){l.truncated=!0;break}let t=await b.findMany({where:re(S,w),locale:o,defaultLocale:m,limit:e+1});if(t.length===0)break;let r=t;if(t.length>e&&(r=t.slice(0,e),l.truncated=!0),await h(r,c,C),l.processed+=r.length,n.onProgress?.(u(`walking`)),l.truncated)break;w=r.map(e=>e.id)}return l.truncated&&e.logger.warn({entityType:r,parentId:i,locale:o,processed:l.processed,cap:s},`paths-cascade: descendant subtree exceeded the cap — deeper/remaining descendants were NOT regenerated. Run \`lumi paths:rebuild --entity ${r}\` for guaranteed full coverage.`),n.onProgress?.(u(`done`)),d()}async function Se(e,t,n={}){let r=n.pageSize??200,i=n.concurrency??5,o=n.sampleCap??25,s=t.dryRun,c=a(),l=c.defaultLocale,u=c.reservedPathPrefixes??[],d=c.locales.map(e=>e.code),f=t.locale?d.filter(e=>e===t.locale):d,p=[...e.entities.values()].filter(e=>!!e.frontend?.path&&(t.entity===void 0||e.name===t.entity)),m=t.entity!==void 0&&p.length===0,g=t.locale!==void 0&&f.length===0;m&&e.logger.warn({entity:t.entity,dryRun:s},`paths-rebuild: --entity matches no routable entity (one declaring frontend.path) — nothing to rebuild (likely a typo in the entity name)`),g&&e.logger.warn({locale:t.locale,configuredLocales:d,dryRun:s},`paths-rebuild: --locale matches no configured locale — nothing to rebuild (likely a typo in the locale code)`);let y=m||g?{entity:m,locale:g}:void 0,b=p.some(e=>O(e)),S=s&&b?`Preview reflects a SINGLE pass; a pathParent() hierarchy drifted at multiple levels may need the real (non-dry-run) rebuild's multi-pass convergence to fully resolve. The real run always converges.`:void 0,C=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),w={scanned:0,changed:0,applied:0,failed:0},T=new Map,E={missing:0,conflicts:0,unchanged:0,skipped:0},D=(e,t)=>{let n=T.get(e);n!==t&&(n!==void 0&&n!==`resolved`&&--E[n],T.set(e,t),t!==`resolved`&&(E[t]+=1))},k=[],A=!1,j=e=>{k.length<o?k.push(e):A=!0},M=0,N=e=>({phase:e,dryRun:s,scope:{entity:t.entity??null,locale:t.locale??null},scanned:w.scanned,changed:w.changed,applied:w.applied,failed:w.failed,missing:E.missing,conflicts:E.conflicts,unchanged:E.unchanged,skipped:E.skipped,passes:M,sample:k,sampleTruncated:A,...S===void 0?{}:{hierarchyNote:S},...y===void 0?{}:{unmatchedScope:y}}),P=async(t,n,r,i)=>{let a=r.id,o=!1;try{let c=await x({entity:t,row:r,locale:i,defaultLocale:l,pathsClient:C}),d=await v({pathsClient:C,entityType:n,entityId:a,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},suppressNoteworthy:[`parent-not-indexed`],reservedPathPrefixes:u,logger:e.logger,dryRun:s}),f=`${n}:${a}:${i}`;switch(d.outcome){case`unchanged`:D(f,`unchanged`);break;case`skipped-null`:case`skipped-reserved`:D(f,`skipped`);break;case`conflict`:D(f,`conflicts`),j({kind:`conflict`,entityType:n,entityId:a,locale:i,oldPath:d.oldPath,newPath:d.newPath,...d.conflictWith?{conflictWith:d.conflictWith.entityId}:{}});break;case`created`:s?D(f,`missing`):(w.applied+=1,o=!0,D(f,`resolved`)),j({kind:`missing`,entityType:n,entityId:a,locale:i,oldPath:null,newPath:d.newPath});break;case`renamed`:w.changed+=1,s||(w.applied+=1,o=!0),D(f,`resolved`),j({kind:`changed`,entityType:n,entityId:a,locale:i,oldPath:d.oldPath,newPath:d.newPath});break}}catch(t){w.failed+=1,e.logger.warn({err:t,entityType:n,id:a,locale:i},`paths-rebuild: failed to process one document — continuing (per-item isolation)`)}return o},F=async t=>{let a=0;for(let o of t){let t=e.getClient(o),s=o.name,c=t.getTable().id;for(let e of f){let u;for(;;){let d=await t.findMany({locale:e,defaultLocale:l,limit:r,orderBy:W(c),...u===void 0?{}:{cursor:{field:`id`,value:u,direction:`asc`}}});if(d.length===0)break;await h(d,i,async t=>{await P(o,s,t,e)&&(a+=1)}),w.scanned+=d.length;let f=d[d.length-1];if(f&&(u=f.id),n.onProgress?.(N(`scanning`)),d.length<r)break}}}return a},L=async t=>{let r=[];for(let[n,i]of T){if(i!==`conflicts`&&i!==`skipped`&&i!==`missing`)continue;let a=n.indexOf(`:`),o=n.lastIndexOf(`:`);if(a<=0||o<=a)continue;let s=n.slice(0,a);if(t.has(s))continue;let c=e.entities.get(s);c!==void 0&&r.push({entity:c,entityType:s,entityId:n.slice(a+1,o),locale:n.slice(o+1)})}if(r.length===0)return 0;let a=0;return await h(r,i,async t=>{let n=await e.getClient(t.entity).findById(t.entityId,{locale:t.locale,defaultLocale:l});n!==null&&(w.scanned+=1,await P(t.entity,t.entityType,n,t.locale)&&(a+=1))}),n.onProgress?.(N(`scanning`)),a},R=p.filter(e=>O(e)),z=new Set(R.map(e=>e.name)),B=s||!b?1:ee;for(let e=0;e<B;e++){M=e+1;let t=await F(e===0?p:R);if(e>0&&(t+=await L(z)),t===0)break}A&&e.logger.warn({scanned:w.scanned,changed:w.changed,missing:E.missing,conflicts:E.conflicts,sampleCap:o},`paths-rebuild: more diff entries than the progress sample can hold — see the aggregate counts; the stored sample is truncated`);let V=N(`done`);n.onProgress?.(V);let{phase:te,...H}=V;return H}async function Ce(e,t,n={}){let r=n.maxRows??1e4,i=n.batchSize??500,a={dryRun:t.dryRun,orphans:[],found:0,deleted:0,truncated:!1,refused:!1},o=new Set([...e.entities.values()].filter(e=>!!e.frontend?.path).map(e=>e.name));if(o.size===0)return e.logger.warn({dryRun:t.dryRun},`paths-prune: no entity declares frontend.path — refusing to treat every row as an orphan`),a.refused=!0,a;let s=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),c=(await s.distinct(`entityType`)).filter(e=>!o.has(e));for(let e of c){let t=await s.count({entityType:e});t>0&&a.orphans.push({entityType:e,rows:t}),a.found+=t}if(t.dryRun)return a;for(let{entityType:e}of a.orphans)for(;a.deleted<r;){let t=await s.findMany({where:{entityType:e},limit:Math.min(i,r-a.deleted)});if(t.length===0)break;let n=await s.deleteMany({id:{in:t.map(e=>e.id)}});a.deleted+=n.length}return a.truncated=a.deleted<a.found&&a.deleted>=r,a.truncated&&e.logger.warn({deleted:a.deleted,found:a.found,maxRows:r},`paths-prune: row cap reached — run again to continue`),a}async function we(e){let{defineJob:t,registerJob:n,QueueClient:r}=await import(`@murumets-ee/queue/client`),i=t({name:`content:paths-cascade`,description:`Regenerate toolkit_paths canonical rows for the descendants of a renamed routable-hierarchy ancestor (one locale). Walks the full bounded subtree in a single invocation.`,schema:Y}),a=t({name:`content:paths-rebuild`,description:`Recompute every routable document's canonical toolkit_paths row (optionally scoped to one entity/locale); dryRun reports the diff without writing. On-demand (lumi paths:rebuild).`,schema:X});n(t({name:`content:paths-prune`,description:`Delete toolkit_paths rows of entity types that are no longer routable (lost frontend.path or unregistered). dryRun reports them without deleting. On-demand (lumi paths:rebuild --orphans).`,schema:Z}),async t=>{let n={dryRun:Z.parse(t.payload).dryRun,orphans:[],found:0,deleted:0,truncated:!1,refused:!1};await R(()=>z(async()=>{let r=await Ce(e,Z.parse(t.payload));for(let e of r.orphans){let t=n.orphans.find(t=>t.entityType===e.entityType);t?t.rows+=e.rows:n.orphans.push({...e})}n.found+=r.found,n.deleted+=r.deleted,n.truncated||=r.truncated,n.refused||=r.refused,t.updateProgress(n),e.logger.info(r,`paths-prune: sweep complete`)},{app:e,label:`paths-prune`}))}),n(i,async t=>{await R(async()=>{let n=await xe(e,Y.parse(t.payload),{onProgress:e=>t.updateProgress(e)});e.logger.info(n,`paths-cascade: descendant regeneration complete`)})}),n(a,async t=>{await R(()=>z(async()=>{let n=await Se(e,X.parse(t.payload),{onProgress:e=>t.updateProgress({...e,site:F()??null})});e.logger.info({dryRun:n.dryRun,scanned:n.scanned,changed:n.changed,missing:n.missing,conflicts:n.conflicts,applied:n.applied,failed:n.failed,passes:n.passes,...n.hierarchyNote===void 0?{}:{hierarchyNote:n.hierarchyNote}},`paths-rebuild: sweep complete`)},{app:e,label:`paths-rebuild`}))}),he(async(e,t,n,a)=>{await new r({app:e,logger:e.logger}).enqueue(i,{entityType:t.name,parentId:n,locale:a})})}const Q=te({name:`toolkit_path_patterns`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityType:V.varchar({length:100,notNull:!0,pgName:`entity_type`}),locale:V.varchar({length:10,notNull:!0}),patternHash:V.varchar({length:64,notNull:!0,pgName:`pattern_hash`}),updatedAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityType`,`locale`]}]}),Te=Q.table;function Ee(e,t,n){let r=j(e,t,n);if(r===null)return null;let i=typeof r==`function`?r.toString():JSON.stringify(r);return ae(`sha256`).update(i).digest(`hex`)}async function De(e){let t=a(),n=t.defaultLocale,r=t.locales.map(e=>e.code),i=Q.makeClient(e.db.readWrite);for(let[t,a]of e.entities){let o=a.frontend?.path;if(o!==void 0)for(let a of r){let r=Ee(o,a,n);if(r===null)continue;let s=await i.findOne({entityType:t,locale:a});s!==null&&s.patternHash===r||(s!==null&&e.logger.warn({entity:t,locale:a,previousHash:s.patternHash,currentHash:r},`toolkit_paths: frontend.path pattern for '${t}' (locale '${a}') changed since last boot — stale toolkit_paths rows may exist; run \`lumi paths:rebuild --entity ${t}\``),await i.upsert({entityType:t,locale:a,patternHash:r},{target:[`entityType`,`locale`],set:{patternHash:r,updatedAt:new Date}}))}}}const Oe=async(e,t,n)=>{if(t.length===0)return new Map;let[{buildEnumerateDeps:r},{getCanonicalPaths:i}]=await Promise.all([import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),import(`./enumerate-CYHJKQz6.mjs`).then(e=>e.n)]),a=r(N()),o=await a.filterPublished(e,t,n);return o.size===0?new Map:i(e,[...o],n,a)},ke=async(e,t)=>{if(e.length===0)return new Set;let{buildEnumerateDeps:n}=await import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),r=n(N()),i=[...new Set(e)],a=i.slice(0,48);i.length>a.length&&N().logger.warn({paths:i.length,max:48},`content: publishedPathsAmong was asked for more paths than it reads; the rest count as unpublished`);let o=await r.pathsClient.findMany({where:{locale:t,isCanonical:!0,path:{in:a}},limit:a.length}),s=new Map;for(let e of o){let t=s.get(e.entityType);t===void 0&&(t=new Map,s.set(e.entityType,t)),t.set(e.entityId,e.path)}let c=new Set;for(let[e,n]of s){let i=await r.filterPublished(e,[...n.keys()],t);for(let[e,t]of n)i.has(e)&&c.add(t)}return c},Ae=async e=>{let[{buildBreadcrumbDeps:t},{resolveBreadcrumbTrail:n}]=await Promise.all([import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),import(`./breadcrumbs-BqSuMNac.mjs`).then(e=>e.t)]);return(await n(e,t(N())))?.steps};function $(e){return g(e,{name:`${e.name}_drafts`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityId:V.uuid({notNull:!0,pgName:`entity_id`}),locale:V.varchar({length:10,notNull:!0,default:`_`}),data:V.jsonb({notNull:!0}),createdBy:V.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:V.varchar({length:255,pgName:`created_by_name`}),createdAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function je(e){return g(e,{name:`${e.name}_locale_status`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityId:V.uuid({notNull:!0,pgName:`entity_id`}),locale:V.varchar({length:10,notNull:!0}),status:V.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:V.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function Me(e){return g(e,{name:`${e.name}_versions`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityId:V.uuid({notNull:!0,pgName:`entity_id`}),version:V.integer({notNull:!0}),locale:V.varchar({length:10,notNull:!0,default:`_`}),data:V.jsonb({notNull:!0}),delta:V.jsonb(),status:V.varchar({length:20}),createdBy:V.varchar({length:255,pgName:`created_by`}),createdByName:V.varchar({length:255,pgName:`created_by_name`}),createdAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:V.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}function Ne(e){if(!e.locales||e.locales.length===0)throw Error(`content() requires at least one locale`);if(!e.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!e.locales.some(t=>t.code===e.defaultLocale))throw Error(`defaultLocale "${e.defaultLocale}" not found in locales: [${e.locales.map(e=>e.code).join(`, `)}]`);if(e.reservedPathPrefixes){for(let t of e.reservedPathPrefixes)if(typeof t!=`string`||!t.startsWith(`/`))throw Error(`content(): reservedPathPrefixes entries must be absolute paths starting with '/', got ${JSON.stringify(t)}`)}let t=e.autoTrash??!0,a=t===!1?void 0:{publishable:!0,...typeof t==`object`&&t.exclude?{exclude:t.exclude}:{}};return M({name:`@murumets-ee/content`,contributes:{"blocks.documentDraftStore":ce(),"blocks.documentVersionStore":fe(),"blocks.publishedPathResolver":Oe,"entity.writeListeners":d,"blocks.publishedPathsAmong":ke,"blocks.breadcrumbTrail":Ae,"sites.baseLocale":async()=>(await import(`./locale-registry-COsJrVZl.mjs`).then(e=>e.n)).resolveInstallBaseLocale(),...a?{"trash.autoEnable":a}:{},...e.reservedPathPrefixes?{"routing.reservedPathPrefixes":e.reservedPathPrefixes}:{}},shared:{settings:[n],features:[{resource:`redirect`,actions:[`manage`],defaultRoles:[`admin`],description:`Create, edit, and delete manual URL redirects (toolkit_redirects).`},{resource:`path`,actions:[`resolve`],defaultRoles:[`admin`,`editor`],description:`Resolve a site URL path to the CMS document that serves it.`}]},server:{entities:[r,o],tables:{toolkitContentLocks:E,toolkitPaths:b,toolkitPathPatterns:Te,toolkitRedirects:S,toolkitTranslationProvenance:l},routes:[...T(),...w()],seeders:[{name:`content:site-chrome`,description:`Create the empty site_chrome row for each placement (header, footer), as a DRAFT, so they can be edited on the page canvas. Publishing one is what makes /v1/_chrome serve it instead of the delivery template's own fallback. Idempotent — an existing row for a placement is left alone.`,available:p,bootstrap:!0,run:async()=>{let{seedSiteChrome:e}=await import(`@murumets-ee/content/seed`);return await e()}},{name:`content:detail-templates`,description:`Create the DRAFT detail template for each entity drawn by one (block-library D009), so its page layout can be edited on the canvas. Until a template is published its rows keep the plain fallback page. Idempotent — an existing template is left alone.`,available:m,bootstrap:!0,run:async()=>{let{seedDetailTemplates:e}=await import(`@murumets-ee/content/seed`);return await e()}}],init:async t=>{i(e);let n=0,r=0,a=0;for(let[e,i]of t.entities){if(i.behaviors?.some(e=>e.name===`versionable`)){let t=`${e}_versions`;if(!H.has(t)){let n=Me(i);H.register(t,n.table),s(e,{versions:n})}n++}let t=i.behaviors?.some(e=>e.name===`publishable`),o=i.allFields,c=Object.values(o).some(e=>e.translatable);if(t&&c){let t=`${e}_locale_status`;if(!H.has(t)){let n=je(i);H.register(t,n.table),s(e,{localeStatus:n})}r++}if(t){let t=`${e}_drafts`;if(!H.has(t)){let n=$(i);H.register(t,n.table),s(e,{drafts:n})}a++}}se(e.locales.map(e=>e.code)),be();try{await we(t)}catch(e){t.logger.warn({err:e},"content: failed to register routing path jobs — descendant cascade + `lumi paths:rebuild` will be unavailable (path writes still succeed)")}t.logger.info({locales:e.locales.map(e=>e.code),defaultLocale:e.defaultLocale,versionableEntities:n,localeStatusEntities:r,draftEntities:a},`Content plugin initialized`)},onReady:async e=>{try{await De(e)}catch(t){e.logger.error({err:t},"content: boot-time frontend-path pattern-drift check failed — continuing app boot without it. Inspect the logged `err` for the cause: a transient DB error can be retried (and `lumi paths:rebuild` re-derives the index if a pattern actually changed), whereas a structural cause — e.g. a missing migration leaving `toolkit_path_patterns` absent — will recur on every boot and needs `lumi migrate`")}}}})}export{q as n,Ne as t};
|
|
2
|
+
//# sourceMappingURL=plugin-CvPWz399.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-CvPWz399.mjs","names":["cols","SITE_COLUMN"],"sources":["../src/locale-status-slot.ts","../src/document-draft-store.ts","../src/document-version-store.ts","../src/paths/document-site.ts","../src/paths/writers.ts","../src/paths/jobs.ts","../src/paths/pattern-hash-schema.ts","../src/paths/pattern-hash.ts","../src/paths/published-path-resolver.ts","../src/schema/drafts.ts","../src/schema/locale-status.ts","../src/schema/versions.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Process-global slot exposing the content plugin's per-locale publish\n * status writer to `AdminClient.updateForLocale` (SD021 — the\n * locale-write choke point).\n *\n * `@murumets-ee/core`'s `createAdminClient` reads the slot by symbol\n * identity when constructing a client, so EVERY framework-built\n * AdminClient routes non-default-locale `status` writes into\n * `<entity>_locale_status` automatically — future writers (import\n * pipelines, AI integration, workflow actions) cannot forget the\n * routing. Same `globalThis` + `Symbol.for` shape as queue's\n * `enqueue-on-commit-slot.ts`: survives HMR and avoids a dependency\n * edge from core onto content.\n *\n * NOTE: this module must stay free of `import 'server-only'` — it is in\n * the content plugin's `init` load path, which the `lumi` CLI loads in\n * plain Node (see memory: server-only in the CLI load path breaks every\n * command and only scaffold-e2e catches it).\n */\n\nimport { getContext } from '@murumets-ee/core'\nimport type { Entity } from '@murumets-ee/entity'\nimport type { LocaleStatusWriter } from '@murumets-ee/entity/admin'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { InvalidLocaleError } from './errors.js'\nimport type { LocaleStatusTable } from './schema/locale-status.js'\nimport { getContentTables } from './schema/registry.js'\n\nconst LOCALE_STATUS_WRITER_KEY = Symbol.for('@murumets-ee/content:locale-status-writer')\n\n/** The typed TableClient for a `{entity}_locale_status` table. */\ntype LocaleStatusClient = ReturnType<LocaleStatusTable['makeClient']>\n\n/**\n * Upsert one non-default locale's status row. The single implementation\n * shared by `setLocaleStatus` (content's own API) and the slot writer\n * below — keeping the `publishedAt` semantics in one place.\n *\n * `client` is the locale-status TableClient; callers resolve it from\n * `getContentTables(entity).localeStatus` (capability-gated).\n */\nexport async function upsertLocaleStatusRow(\n client: LocaleStatusClient,\n entityId: string,\n locale: string,\n status: 'draft' | 'published',\n): Promise<void> {\n const publishedAt = status === 'published' ? new Date() : null\n await client.upsert(\n { entityId, locale, status, publishedAt },\n { target: ['entityId', 'locale'], set: { status, publishedAt } },\n )\n}\n\n/**\n * Write the locale-status writer factory into the slot. Called from the\n * content plugin's `init` AFTER the per-entity `<entity>_locale_status`\n * tables are registered (the factory consults that registry per entity).\n *\n * `localeCodes` is the plugin's configured locale set (the shipped `content()`\n * defaults), passed in by init rather than read via `getContentConfig()` —\n * importing `locale-utils`/`plugin` here would close an import cycle (plugin →\n * slot → … → plugin; `verify:cycles` catches it). It is the **static fallback**\n * only; the write-time whitelist prefers the request-scoped effective set (below).\n *\n * The factory returns `undefined` for entities without a locale-status\n * table — `updateForLocale` then falls back to the base `status` column,\n * which is correct: for non-translatable publishables the default locale\n * is the only locale.\n */\nexport function setActiveLocaleStatusWriterFactory(localeCodes: readonly string[]): void {\n const factory = (entity: Entity, db: PostgresJsDatabase): LocaleStatusWriter | undefined => {\n const { localeStatus } = getContentTables(entity.name)\n if (!localeStatus) return undefined\n return async (entityId, locale, status, site) => {\n // Same runtime-aware whitelist as locale-utils' validateLocale (SD006), sans\n // the '_' sentinel (updateForLocale never sends it here): prefer the\n // request-scoped effective enabled codes so a locale added/enabled at runtime\n // can have translatable content published in it with no rebuild, falling back\n // to the init-time static set outside a request (CLI / build / tests).\n // Unvalidated locales must not mint locale_status rows no reader will ever merge.\n const allowed = getContext()?.availableLocales ?? localeCodes\n if (!allowed.includes(locale)) {\n throw new InvalidLocaleError(locale, [...allowed])\n }\n // The caller (core's createAdminClient) passes the SAME db handle the\n // AdminClient writes through — a client built against an explicit app\n // (tests, multi-app setups) must not write its locale-status rows\n // through the singleton app's connection.\n // `site` is the document's site when the entity is bound (AdminClient\n // resolved it); the partitioned client stamps it and confines the upsert.\n const client = localeStatus.makeClient(db, site)\n await upsertLocaleStatusRow(client, entityId, locale, status)\n }\n }\n ;(globalThis as Record<symbol, unknown>)[LOCALE_STATUS_WRITER_KEY] = factory\n}\n","/**\n * `DocumentDraftStore` implementation contributed to the block editor's\n * `blocks.documentDraftStore` slot (SD015).\n *\n * The block editor's op-commit route (in `@murumets-ee/blocks`) materializes a\n * `withOpLog()` document's working tree by calling this store inside its\n * commit transaction, and its bound-field route stages a bound column through\n * it (editor D019) — so canvas edits land in the per-locale `{entity}_drafts`\n * buffer, never the live/public row. A later \"Publish\" (`publishDraft`)\n * promotes the draft to the live row + snapshots a version.\n *\n * blocks owns the slot type (imported `type`-only below — that import also\n * pulls the `PluginContributionSlots` augmentation into this compilation);\n * content owns the draft tables. No runtime dependency between the packages.\n *\n * NB: no `import 'server-only'` here. This module is imported by\n * `content/plugin.ts` at construct time (for the `contributes` slot), and the\n * content plugin must stay loadable in a plain-Node context (the `lumi migrate`\n * CLI loads `lumi.config.ts` without the `react-server` resolution condition,\n * where `server-only` throws). The store is only ever *called* server-side (by\n * the op-commit committer) and is never reachable from a client bundle.\n */\n\nimport type { DocumentDraftStore } from '@murumets-ee/blocks/server'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { buildPartitionCondition } from '@murumets-ee/entity'\nimport { and, eq, sql } from 'drizzle-orm'\nimport type { PgColumn } from 'drizzle-orm/pg-core'\nimport { siteForDocumentType } from './internal/document-site.js'\nimport { getContentTables } from './schema/registry.js'\nimport { enqueueLocaleSave } from './translation-provenance.js'\n\n/**\n * Build the draft store. Stateless — resolves the per-entity drafts table from\n * the schema registry at call time, so it composes with any app's entity set.\n */\nexport function createDocumentDraftStore(): DocumentDraftStore {\n return {\n async writeDraft({ documentType, documentId, locale, dataPatch, user, tx }) {\n const { drafts } = getContentTables(documentType)\n // No drafts table → the document type isn't publishable; signal the\n // caller to fall back to writing the live row.\n if (!drafts) return false\n\n // Bound to the caller's handle — op-commit's SERIALIZABLE transaction, so\n // the upsert commits with the `editor_op` insert, or the bound-field\n // route's plain handle.\n // Confined to the document's site (multisite D003): the upsert stamps\n // `_site`, and the composite FK refuses a draft for another site's row.\n const client = drafts.makeClient(tx, await siteForDocumentType(documentType))\n const now = new Date()\n // MERGED in one statement (`stored || dataPatch`), not read-then-written.\n // Two writers stage into one draft since editor D019 — op-commit's tree\n // and a bound field's column — and a read-modify-write here let the later\n // one write back the draft as it was BEFORE the earlier one committed:\n // a block edit erased by a title typed a moment later. Same shallow,\n // top-level merge the spread used to do.\n const written = await client.upsert(\n {\n entityId: documentId,\n locale,\n data: dataPatch,\n createdBy: user.id,\n createdByName: user.name ?? null,\n createdAt: now,\n updatedAt: now,\n },\n {\n target: ['entityId', 'locale'],\n // On conflict, only touch the mutable fields — preserve createdBy/createdAt.\n set: { updatedAt: now },\n mergeJsonb: ['data'],\n },\n )\n // What this locale was saved from (agent-access D017). QUEUED, not\n // awaited: this runs inside op-commit's transaction, and the recording\n // reads on another connection — awaited, enough concurrent commits would\n // each hold one connection while waiting for a second. Handed the draft\n // the upsert just returned, since this transaction has not committed.\n enqueueLocaleSave({\n entityName: documentType,\n entityId: documentId,\n locale,\n userId: user.id,\n draftData: (written.data as Record<string, unknown> | undefined) ?? dataPatch,\n })\n return true\n },\n\n async readDraft({ documentType, documentId, locale, db }) {\n const { drafts } = getContentTables(documentType)\n // No drafts table → the document type isn't publishable; the caller\n // falls back to the live row, exactly as `writeDraft`'s `false` does.\n if (!drafts) return null\n const row = await drafts\n .makeClient(db, await siteForDocumentType(documentType))\n .findOne({ entityId: documentId, locale })\n if (!row) return null\n return {\n data: (row.data as Record<string, unknown> | undefined) ?? {},\n createdBy: row.createdBy ?? null,\n createdByName: row.createdByName ?? null,\n updatedAt: row.updatedAt ?? null,\n }\n },\n\n async readLocaleStatus({ documentType, documentId, locale, db }) {\n // No locale-status table → not publishable + translatable; the locale\n // inherits the base row's status, which the caller already holds.\n const { localeStatus } = getContentTables(documentType)\n if (!localeStatus) return null\n const row = await localeStatus\n .makeClient(db, await siteForDocumentType(documentType))\n .findOne({ entityId: documentId, locale })\n if (!row) return null\n return row.status === 'published' ? 'published' : 'draft'\n },\n\n async hasAnyDraft({ documentType, documentId, db }) {\n const { drafts } = getContentTables(documentType)\n if (!drafts) return false\n // Any-locale existence probe — the orphan-draft GC (blocks PR 25)\n // spares a row when ANY locale still carries draft work.\n const client = drafts.makeClient(db, await siteForDocumentType(documentType))\n return client.exists({ entityId: documentId })\n },\n\n async hasAnyLocaleActivity({ documentType, documentId, db }) {\n // Per-locale publish status (publishDraft on a non-default locale\n // writes HERE, never the base row's status — pr-review H2).\n const { localeStatus } = getContentTables(documentType)\n const site = await siteForDocumentType(documentType)\n if (localeStatus) {\n const client = localeStatus.makeClient(db, site)\n if (await client.exists({ entityId: documentId })) return true\n }\n // Translation rows (saveTranslation writes them directly, no hooks —\n // the base row's updatedAt never moves). Translation tables are\n // generated in @murumets-ee/entity; accessed via schemaRegistry +\n // direct Drizzle, same idiom as translation-status.ts.\n const translationTable = schemaRegistry.get(`${documentType}_translations`)\n if (translationTable) {\n const cols = translationTable as Record<string, PgColumn | undefined>\n const entityIdCol = cols.entityId\n if (entityIdCol !== undefined) {\n // A bound type's translation rows are confined to the site too — the\n // same predicate the partitioned clients above apply. The partition\n // builder THROWS if the column is missing, never widening.\n const where =\n site !== undefined\n ? and(\n eq(entityIdCol, documentId),\n buildPartitionCondition(translationTable, { site }, documentType),\n )\n : eq(entityIdCol, documentId)\n const rows = await db.select({ one: sql`1` }).from(translationTable).where(where).limit(1)\n if (rows.length > 0) return true\n }\n }\n return false\n },\n }\n}\n","/**\n * `DocumentVersionStore` implementation contributed to the block editor's\n * `blocks.documentVersionStore` slot (F053).\n *\n * The block editor's `snapshot-version` admin route (in `@murumets-ee/blocks`)\n * calls this BEFORE the detach flow forks a saved pattern and dispatches the\n * `block:detach` op, so a recovery point exists for the document's current\n * state even when it has never been published.\n *\n * ## Every value import here must be safe for a jiti-loaded plugin entry\n *\n * `plugin.ts` imports this module STATICALLY, at construct time, to build\n * its `contributes` object — `lumi migrate` then loads `plugin.ts` (via\n * `content/dist/plugin.mjs`) through jiti, which does NOT resolve the\n * `react-server` export condition. Any module reachable from here that\n * carries `import 'server-only'` at its top throws UNCONDITIONALLY in that\n * context (the package's bare export is the throwing variant outside\n * `react-server`) — `pnpm verify:plugin-loads` exists specifically to catch\n * this before a developer does. Three things fell out of getting this file\n * right, each recorded so the next module contributed from `plugin.ts`\n * doesn't rediscover them by trial and error:\n *\n * 1. **The draft is read via `getContentTables` directly** — the SAME\n * low-level access `document-draft-store.ts`'s own `readDraft` uses —\n * rather than through `drafts.ts`'s `getDraft(ctx, ...)`. `drafts.ts`\n * imports `versions.ts` (`server-only`) for `createVersionSnapshot`.\n *\n * 2. **`localeFindOptions`/`isDefaultLocale` are INLINED below**, not\n * imported from `internal/apply-locale-routed-update.ts`. That file's\n * OTHER export, `applyLocaleRoutedUpdate`, has a value import of\n * `getLocaleStatusClient` from `internal/context.ts` (`server-only`) —\n * and ES modules evaluate a file's full top-level graph on import\n * regardless of which single export you use. Importing the file at all,\n * for any export, drags `server-only` in. The two functions are small\n * enough that duplicating them beats a third module split.\n *\n * 3. **`createContentContext` (`internal/context.ts`) and\n * `createVersionSnapshot` (`versions.ts`) are DYNAMIC imports**, inside\n * `snapshotCurrentState` — never at module scope. Same precedent\n * `@murumets-ee/blocks/plugin.ts` already uses repeatedly for\n * `@murumets-ee/core/clients` (also `server-only`) inside its job\n * callbacks: dynamic import is NOT the banned \"circular-dependency\n * escape hatch\" pattern here — it is the sanctioned way to keep a\n * lazily-invoked function able to reach a genuinely server-only module\n * without tainting the eagerly-evaluated plugin entry that contributes it.\n *\n * `getApp`, `getContentTables`, and `getVersionableOptions` stay static\n * imports; none of the three carries the sentinel (verified, not assumed —\n * `grep -rl \"^import 'server-only'\" packages/content/src` names the eight\n * files that do, and these three aren't in it).\n *\n * blocks owns the slot type (imported `type`-only below — that import also\n * pulls the `PluginContributionSlots` augmentation into this compilation);\n * content owns the version tables + `createVersionSnapshot`. No runtime\n * dependency between the packages.\n */\n\nimport type { DocumentVersionStore } from '@murumets-ee/blocks/server'\nimport { getApp } from '@murumets-ee/core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { getVersionableOptions } from './behaviors/versionable.js'\nimport { getContentConfig } from './content-config-store.js'\nimport { siteForDocumentType } from './internal/document-site.js'\nimport { DEFAULT_LOCALE_SENTINEL } from './locale-utils.js'\nimport { getContentTables } from './schema/registry.js'\n\n/**\n * `findById` options that read the same locale view a write would target —\n * default locale reads the base row, non-default reads the\n * translation-merged row. DUPLICATED from\n * `internal/apply-locale-routed-update.ts`'s `localeFindOptions` /\n * `isDefaultLocale` — see the module docblock's point 2 for why importing\n * the original isn't safe here.\n */\nfunction localeFindOptions(\n locale: string,\n): { defaultLocale: string } | { locale: string; defaultLocale: string } {\n const { defaultLocale } = getContentConfig()\n const isDefault = locale === DEFAULT_LOCALE_SENTINEL || locale === defaultLocale\n return isDefault ? { defaultLocale } : { locale, defaultLocale }\n}\n\n/**\n * Map a caller-supplied locale to the STORAGE key drafts/versions actually\n * use. Both `'_'` and the literal default-locale code (e.g. `'en'`) name the\n * same document for a *read* (`localeFindOptions` already treats them as\n * equivalent) — but the draft buffer and version table key default-locale\n * rows under `'_'` ONLY, never the literal code (`WriteDraftArgs`'s own\n * docstring: \"`'_'` (default-locale sentinel) or a BCP-47 locale\" — the\n * sentinel is the one that is actually written). `createVersionSnapshot`\n * goes further: it updates the entity's `_version` column ONLY when its\n * `locale` argument is exactly `DEFAULT_LOCALE_SENTINEL` (`versions.ts`) —\n * passing the literal code silently skips that write.\n *\n * The one caller today (`use-patterns.ts` → `readDocumentMeta` →\n * `doc.session.locale`) already sends `'_'` for a default-locale document\n * (`entity-edit-page.tsx`'s `opLocale = isDefaultLocale ? '_' : contentLocale`\n * is what seeds the editor session), so this normalization is currently a\n * no-op on the only real path. It is not defensive filler, though: the\n * route's own `bodySchema` accepts either spelling, so anything else calling\n * `POST /api/admin/blocks/snapshot-version` with the literal default-locale\n * code would otherwise miss the pending draft and skip the `_version` bump —\n * silently produce a snapshot with less data than the author actually has,\n * which is the exact failure mode this whole feature exists to prevent.\n */\nfunction normalizeLocale(locale: string): string {\n const { defaultLocale } = getContentConfig()\n return locale === DEFAULT_LOCALE_SENTINEL || locale === defaultLocale\n ? DEFAULT_LOCALE_SENTINEL\n : locale\n}\n\n/**\n * Read one document's per-locale draft patch, or `undefined` when the\n * document type has no draft buffer or no draft row exists. Same low-level\n * access `document-draft-store.ts`'s `readDraft` uses — see the module\n * docblock's point 1 for why this doesn't go through `drafts.ts`'s\n * `getDraft`.\n */\nasync function readDraftPatch(\n documentType: string,\n documentId: string,\n locale: string,\n db: PostgresJsDatabase,\n): Promise<Record<string, unknown> | undefined> {\n const { drafts } = getContentTables(documentType)\n if (!drafts) return undefined\n const row = await drafts\n .makeClient(db, await siteForDocumentType(documentType))\n .findOne({ entityId: documentId, locale })\n return row ? ((row.data as Record<string, unknown> | undefined) ?? undefined) : undefined\n}\n\n/**\n * Build the version store. Stateless — resolves the entity + its AdminClient\n * from `app.entities` / `app.getClient` at call time, so it composes with\n * any app's entity set (same shape `createDocumentDraftStore` uses for the\n * content-tables registry).\n */\nexport function createDocumentVersionStore(): DocumentVersionStore {\n return {\n async snapshotCurrentState({ documentType, documentId, locale, user }) {\n const app = getApp()\n const entity = app.entities.get(documentType)\n if (!entity) return false\n\n const versionableOptions = getVersionableOptions(entity.behaviors)\n // Not versionable → nothing to snapshot. Not an error: the route\n // reports `snapshotted: false` and the client's flow proceeds\n // unchanged (PR07's original posture for this document type).\n if (!versionableOptions) return false\n\n const admin = app.getClient(entity)\n const db = admin.getDb()\n\n // Normalize ONCE — see `normalizeLocale`'s docblock — and use the\n // result everywhere below instead of the raw `locale` argument, so the\n // read, the draft lookup, and the snapshot write all key on the same\n // storage-locale value.\n const storageLocale = normalizeLocale(locale)\n\n // The locale-merged live row — the SAME read `publishDraft` takes as\n // its delta baseline, and the fallback state when no draft exists yet\n // (this document's very first edit is the detach itself).\n const live = await admin.findById(documentId, localeFindOptions(storageLocale))\n if (!live) return false\n\n // Overlay any pending draft patch on top of the live row, IN MEMORY\n // ONLY — this must never write the merge back (that would silently\n // publish the draft as a side effect of taking a safety snapshot).\n // Mirrors what `publishDraft` computes dynamically via\n // `applyLocaleRoutedUpdate`, minus the actual write.\n const draftPatch = await readDraftPatch(documentType, documentId, storageLocale, db)\n const merged: Record<string, unknown> = draftPatch ? { ...live, ...draftPatch } : { ...live }\n\n // 🔴 DO NOT \"TIDY\" THESE BACK TO STATIC IMPORTS. Both target modules\n // carry `import 'server-only'`; hoisting either one to the top of this\n // file reintroduces the exact throw the module docblock's point 3\n // describes — `pnpm verify:plugin-loads` (wired into CI,\n // .github/workflows/test.yml) will catch a regression, but it is\n // cheaper for the next reader to know this is load-bearing than to\n // rediscover it from a red PR.\n const [{ createContentContext }, { createVersionSnapshot }] = await Promise.all([\n import('./internal/context.js'),\n import('./versions.js'),\n ])\n const ctx = createContentContext({ admin, entity }, versionableOptions)\n\n await createVersionSnapshot(\n ctx,\n documentId,\n merged,\n live as Record<string, unknown>,\n storageLocale,\n // `exactOptionalPropertyTypes` — `createVersionSnapshot`'s `name?:\n // string` strictly excludes `null`, but the slot's `user` (matching\n // the wire-facing `ctx.user.name ?? null` shape) allows it.\n { id: user.id, ...(user.name != null ? { name: user.name } : {}) },\n )\n return true\n },\n }\n}\n","/**\n * The site a document's path rows record — the ONE unconfined read of the\n * site-partitioned `toolkit_paths` (plan `multisite` phase 03).\n *\n * Needed by exactly one caller: the path writer, for a cascade child. A cascade\n * from a SHARED parent crosses sites (plan `multisite` F006), so neither the\n * request nor the cascade knows the child's site — but its path rows do, and a\n * document id is a UUID that names one document on one site. The read returns\n * a site handle and nothing else, and its only consumer uses it to confine the\n * write that follows. Through the table's `.table` escape valve (CLAUDE.md,\n * infrastructure tables) because a partitioned client cannot, by design, be\n * built without a site.\n */\n\nimport { and, eq, getTableColumns } from 'drizzle-orm'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { SITE_PARTITION_COLUMN as SITE_COLUMN } from '../schema/site-partition.js'\nimport { toolkitPaths } from './schema.js'\n\n/**\n * `undefined` = the document has no path rows; `null` = its rows name SEVERAL\n * sites (no write path produces that — refused rather than guessed).\n */\nexport async function siteOfDocumentPaths(\n db: PostgresJsDatabase,\n entityType: string,\n entityId: string,\n): Promise<string | null | undefined> {\n // The `_site` column is hidden from the table's declared type (see\n // `defineSitePartitionedTable`); this is the one place it is read by name.\n const columns: Record<string, (typeof toolkitPaths)['_']['columns'][string] | undefined> =\n getTableColumns(toolkitPaths)\n const site = columns[SITE_COLUMN]\n const type = columns.entityType\n const id = columns.entityId\n if (!site || !type || !id)\n throw new Error(`toolkit_paths is missing '${SITE_COLUMN}'/entity columns`)\n // Two distinct values are enough to tell \"one site\" from \"several\".\n const rows = await db\n .selectDistinct({ site })\n .from(toolkitPaths)\n .where(and(eq(type, entityType), eq(id, entityId)))\n .limit(2)\n if (rows.length > 1) return null\n const value = rows[0]?.site\n return typeof value === 'string' ? value : undefined\n}\n","/**\n * The real bodies for the two `@murumets-ee/entity` path seams (plan/routing\n * PR03, Part 3 + Part 4), plus the process-global slot that exposes them to\n * `@murumets-ee/core`'s `createAdminClient` — the same `globalThis` +\n * `Symbol.for` capability-injection shape as `locale-status-slot.ts` (survives\n * HMR, avoids a core→content dependency edge).\n *\n * - `createPathIndexWriter` — POST-commit: recompute `toolkit_paths` for the\n * written locale(s). PR04 replaced the PR03 blind upsert with a\n * find-canonical → insert/demote so a slug change leaves the old path behind\n * as redirect history (`is_canonical = false`) instead of overwriting it in\n * place. Deletes ALL rows on `action:'delete'` (the document is gone) and\n * only the CANONICAL ones on `action:'release'` (the document was trashed,\n * so its URL is freed but its redirect history outlives it). Fires the\n * cascade-trigger\n * seam (below) when a `pathParent()` document's canonical path actually\n * changes, so a Task-2 queue job can regenerate descendant paths — and on\n * `action:'restore'`, where the release already deleted the canonical so a\n * move reports `created` rather than `renamed` (F062).\n * - `createPathConflictGuard` — PRE-commit: resolve the candidate path, reject\n * reserved-prefix hits + manual conflicts, auto-suffix `data.slug` otherwise.\n *\n * NOTE: this module is on the content plugin's `init` load path (via\n * `plugin.ts`), so it must stay free of `import 'server-only'` — every import\n * here (entity path helpers, the `defineTable`-backed schema, the leaf config\n * store) is server-only-free, and `ToolkitApp` is a TYPE-only import.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport {\n getCurrentSite,\n requireRequestSite,\n resolveImplicitSite,\n runInSite,\n} from '@murumets-ee/core'\nimport type { Entity } from '@murumets-ee/entity'\nimport { hasPathParent, isSiteBound, resolveEffectiveSlugMode } from '@murumets-ee/entity'\nimport type {\n PathConflictGuard,\n PathIndexWriteInput,\n PathIndexWriter,\n} from '@murumets-ee/entity/admin'\nimport { getContentConfig } from '../content-config-store.js'\nimport { applyCanonicalPath } from './apply-path.js'\nimport { siteOfDocumentPaths } from './document-site.js'\nimport { computeCanonicalPath, ensurePathAvailable } from './generate-path.js'\nimport { toolkitPathsTable } from './schema.js'\n\nconst PATH_WRITERS_KEY = Symbol.for('@murumets-ee/content:path-writers-factory')\n\nconst CASCADE_TRIGGER_KEY = Symbol.for('@murumets-ee/content:cascade-trigger-factory')\n\n/**\n * Cascade seam invoked when a `pathParent()` document's canonical path actually\n * changes in a locale — so descendants (whose `[parent.path]` expansions now\n * point at a stale ancestor URL) can be regenerated. The fan-out is too large\n * for the request path, so Task 2 registers a body here that enqueues a queue\n * job (§2/§4 change matrix). PR04 Task 1 ships only the SEAM: when nothing is\n * registered (queue is an optional peer dependency), the call is a NO-OP —\n * a deployment without a queue configured must never see path writes fail.\n *\n * @param app The resolved toolkit app (queue client, db, logger).\n * @param entity The routable entity whose ancestor row changed.\n * @param entityId The renamed ancestor document's id.\n * @param locale The locale whose canonical path changed.\n */\nexport type CascadeTrigger = (\n app: ToolkitApp,\n entity: Entity,\n entityId: string,\n locale: string,\n) => Promise<void>\n\n/**\n * Register the cascade-trigger body (called from content's plugin `init` in\n * Task 2). Same `globalThis` + `Symbol.for` capability-injection shape as\n * {@link setActivePathWritersFactory} / `locale-status-slot.ts` — survives HMR,\n * avoids a core→content dependency edge.\n */\nexport function setActiveCascadeTrigger(trigger: CascadeTrigger): void {\n ;(globalThis as Record<symbol, unknown>)[CASCADE_TRIGGER_KEY] = trigger\n}\n\n/** Read the registered cascade trigger, or `undefined` when none is wired. */\nfunction getActiveCascadeTrigger(): CascadeTrigger | undefined {\n const slot = (globalThis as Record<symbol, unknown>)[CASCADE_TRIGGER_KEY]\n return typeof slot === 'function' ? (slot as CascadeTrigger) : undefined\n}\n\n/** The pair of seams a routable entity's `AdminClient` gets wired with. */\nexport interface PathWriters {\n pathIndexWriter: PathIndexWriter\n pathConflictGuard: PathConflictGuard\n}\n\n/**\n * Factory read by core's `createAdminClient` (per client, with the resolved\n * `app` + the entity). Core gates on `entity.frontend?.path` before calling —\n * see `client-factories.ts`.\n */\nexport type PathWritersFactory = (app: ToolkitApp, entity: Entity) => PathWriters\n\n/**\n * POST-commit path-index maintenance for a routable entity. Recomputes every\n * relevant locale's canonical `toolkit_paths` row — or, on the two removal\n * actions, drops rows without recomputing anything: `'delete'` takes every row\n * (the document is physically gone, so its redirect history is meaningless),\n * `'release'` takes only the canonical ones (the document is in the trash, so\n * its URL must be free for another document while its history waits for a\n * restore).\n *\n * PR04 — instead of a blind upsert-in-place, each locale runs a\n * find-canonical → insert/demote (the shared {@link applyCanonicalPath}, reused\n * by the cascade + rebuild jobs so redirect history is minted identically):\n * - no canonical row yet (create) → insert a fresh canonical row;\n * - canonical row with the SAME path (unrelated field changed) → no-op;\n * - canonical row with a DIFFERENT path (a rename) → insert the new canonical\n * row, DEMOTE the old one to `is_canonical = false` (it stays as redirect\n * history — D005 / the change-matrix), prune history past\n * {@link MAX_REDIRECT_HISTORY}, and fire the cascade-trigger seam.\n *\n * The old canonical is DEMOTED before the new one is inserted — DB-safe because\n * of the `(entity_type, entity_id, locale) WHERE is_canonical` partial unique,\n * which caps a document at one canonical row per locale (an insert-first order\n * would transiently hold two and violate it). If the insert then fails on a\n * `(locale, path)` conflict (another document already owns the target URL — the\n * pre-commit guard only screens the write's OWN locale, F010) the old row is\n * re-promoted so the document is never left without a canonical (see\n * {@link applyCanonicalPath}).\n *\n * The ONE thing the writer adds over the shared helper: it fires the\n * cascade-trigger seam on an ANCESTOR's `renamed` outcome so a queue job can\n * regenerate descendants. The jobs themselves call `applyCanonicalPath`\n * directly (never re-firing the seam) — that's how a single cascade invocation\n * walks the whole subtree without tripping the queue's self-enqueue guard.\n *\n * `'restore'` is handled as an `'upsert'` that ALSO fires the cascade on a\n * `created` outcome. It has to be: the trash-side `'release'` deleted the\n * canonical row, so the restore has nothing to demote and reports `created`\n * even when it moved the document to a new URL. See the call site.\n */\nexport function createPathIndexWriter(app: ToolkitApp, entity: Entity): PathIndexWriter {\n return async (input) => {\n // The site whose index this write maintains (multisite phase 03). `null`\n // only for a cascade child whose site cannot be established — see\n // `siteForPathWrite`; nothing is written then.\n const site = await siteForPathWrite(app, entity, input)\n if (site === null) return\n // A partitioned client: every read below is confined to `site` and every\n // row it inserts is stamped with it. A constructor, no I/O.\n const pathsClient = toolkitPathsTable.makeClient(app.db.readWrite, site)\n\n // A cascade child of a SHARED parent can live on a site other than the\n // request's. Its row is re-read below through the entity clients, which\n // act on the request's site — so recompute it inside its own site when the\n // request names none, and leave it to `lumi paths:rebuild` when the request\n // is confined to a different one (a request never moves sideways).\n const ambient = getCurrentSite()\n if (ambient !== undefined && ambient !== site) {\n if (input.action === 'delete' || input.action === 'release') {\n // Removal re-reads nothing — it is safe from any frame.\n } else {\n app.logger.warn(\n { entity: input.entityName, id: input.id, site, requestSite: ambient },\n 'toolkit_paths: a cascade reached a document on another site — its path is left for ' +\n '`lumi paths:rebuild` on that site rather than recomputed from this request',\n )\n return\n }\n }\n if (ambient === undefined && input.viaCascade === true && isSiteBound(entity)) {\n return runInSite(site, () => maintain(pathsClient, input))\n }\n return maintain(pathsClient, input)\n }\n\n async function maintain(\n pathsClient: ReturnType<typeof toolkitPathsTable.makeClient>,\n input: PathIndexWriteInput,\n ): Promise<void> {\n if (input.action === 'delete') {\n await pathsClient.deleteMany({ entityType: input.entityName, entityId: input.id })\n return\n }\n\n // TRASH. Drop only the canonical rows: that releases the `(locale, path)`\n // claim so another document may take the URL, while leaving the demoted\n // history rows — the 301 sources a restore will need — in place.\n //\n // They are inert in the meantime rather than a leak: tier 4 of\n // `resolvePath` publish-checks a historical row's target before redirecting\n // to it, and a trashed document fails that check, so an old URL answers\n // `none` while the document is in the trash and starts redirecting again\n // the moment it is restored.\n //\n // `deleteMany` requires a non-empty where, which this is; the partial\n // unique that made the stale claim block a new document is predicated on\n // `is_canonical`, so dropping exactly these rows is what frees the URL.\n if (input.action === 'release') {\n await pathsClient.deleteMany({\n entityType: input.entityName,\n entityId: input.id,\n isCanonical: true,\n })\n return\n }\n\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const reservedPathPrefixes = config.reservedPathPrefixes ?? []\n // A locale-scoped write (`updateForLocale`) touches only that locale; a\n // base write recomputes every configured locale (the create + base-update\n // path, and how the fallback ladder gets each locale's row).\n const locales = input.locale ? [input.locale] : config.locales.map((l) => l.code)\n\n // Lazy (inside the body) so constructing this read client — which itself\n // re-wires these seams — cannot recurse at wiring time. `findById` never\n // fires the seams, so there is no runtime recursion either.\n //\n // F074 — `input.viaCascade` means `dispatchCascadeSeams` is the caller:\n // this event is for a CASCADE CHILD, not the entity the ambient caller\n // directly authorized a write on. `onDelete: 'cascade'` / `'set-null'` is\n // the data-model author's standing declaration that the child follows the\n // parent's lifecycle — the parent operation was already authorized, but\n // the ambient caller may hold no `view` grant at all on the child's\n // entity type. Reading it under the ambient context then throws or\n // silently returns null (see the `!row` branch below), and the child's\n // path stops being maintained even though the cascade itself was fine.\n // `app.getSystemClient` is `createSystemAdminClient`'s shape (identity +\n // scope preserved, checker elevated, no publish filter — same read\n // semantics as `app.getClient`, unlike a `createSystemReachResolver`\n // `QueryClient`, which would hide every DRAFT document from this same\n // read and silently stop indexing them). See `ToolkitApp.getSystemClient`.\n const readClient =\n input.viaCascade === true ? app.getSystemClient(entity) : app.getClient(entity)\n\n // Fire the cascade seam for an ANCESTOR whose canonical path changed. No-op\n // unless the entity is a `pathParent()` hierarchy AND a trigger is wired\n // (queue is an optional peer). Never let a cascade failure fail the already-\n // committed entity write (D005 drift tolerance — same as the uniqueness catch).\n const fireCascade = async (locale: string): Promise<void> => {\n if (!hasPathParent(entity)) return\n const trigger = getActiveCascadeTrigger()\n if (!trigger) return\n try {\n await trigger(app, entity, input.id, locale)\n } catch (err) {\n app.logger.warn(\n { err, entity: input.entityName, id: input.id, locale },\n 'toolkit_paths: descendant-cascade trigger threw — the entity write already ' +\n 'committed, so tolerating and continuing (paths are rebuildable, D005)',\n )\n }\n }\n\n for (const locale of locales) {\n const row =\n locale === input.locale\n ? (input.row ?? null)\n : ((await readClient.findById(input.id, { locale, defaultLocale })) as Record<\n string,\n unknown\n > | null)\n if (!row) {\n // F074/F075 — this used to be a bare `continue`: no row, nothing\n // computed, nothing recorded anywhere. Route it through the SAME\n // `skipped-null` reporting `applyCanonicalPath` already has (a\n // `path: null` call costs no DB round trip — it returns before any\n // query) so a defect here surfaces exactly like a broken-pattern\n // defect does. `row-unreadable` is always in NOTEWORTHY_SKIP_REASONS:\n // a row that legitimately has no path is never the one missing here —\n // there is no row read to be missing FOR.\n await applyCanonicalPath({\n pathsClient,\n entityType: input.entityName,\n entityId: input.id,\n locale,\n path: null,\n skipReason: 'row-unreadable',\n reservedPathPrefixes,\n logger: app.logger,\n })\n continue\n }\n\n const computed = await computeCanonicalPath({\n entity,\n row,\n locale,\n defaultLocale,\n pathsClient,\n })\n\n // Shared find-canonical → insert/demote (also honours the reserved-prefix\n // skip: `pathConflictGuard` only validates the WRITE's own locale (F010),\n // and a base-row write recomputes every locale, so a reserved path could\n // slip through undetected in a locale the guard never checked — F013).\n const result = await applyCanonicalPath({\n pathsClient,\n entityType: input.entityName,\n entityId: input.id,\n locale,\n path: computed.path,\n ...(computed.skipReason !== undefined && { skipReason: computed.skipReason }),\n reservedPathPrefixes,\n logger: app.logger,\n })\n\n // An ancestor rename → fan out to descendants via the queue-job seam.\n //\n // A RESTORE counts as a rename here even though the outcome says\n // `created`, and that asymmetry is the whole of F062. Trashing releases\n // the canonical row, so by the time a restore re-asserts one there is no\n // existing canonical to demote — `applyCanonicalPath` reports `created`\n // unconditionally, including when `opts.overrides.slug` moved the\n // document to a completely different URL. Gating on `renamed` alone left\n // every descendant deriving `[parent.path]` from a URL nothing owns,\n // while that URL was simultaneously free for another document to claim.\n //\n // Restricted to the restore action rather than firing on every `created`:\n // a genuine create has no descendants, so that form would enqueue a job\n // per create of a hierarchical entity to find nothing every time. A\n // restore is rare and human-initiated, and `fireCascade` no-ops unless\n // the entity is a `pathParent()` hierarchy AND a trigger is wired, so a\n // same-slug restore costs one job per locale that finds every descendant\n // `unchanged`. That is the price of not needing to know whether the slug\n // moved — the pre-trash canonical was deleted by the release, so nothing\n // here can answer that question.\n //\n // ⚠️ THE COUNT IS PER ROW, NOT PER RESTORE, and a cascade restore is\n // many rows. `AdminClient.restore` now runs this writer for every\n // reactivated cascade child and every same-entity batch sibling, so a\n // batch of N rows across L locales enqueues up to N×L jobs, each walking\n // its own subtree — and `setActiveCascadeTrigger`'s body is a bare\n // `enqueue` with no dedup key. What bounds it is on the caller's side:\n // `MAX_CASCADE_PATH_RECOMPUTES` refuses recompute-action path writes\n // past a per-operation budget, so N is capped before it reaches here.\n // Not reachable in-repo today either — `pathParent()` hardcodes\n // `onDelete: 'restrict'`, so a hierarchy never self-cascades — but the\n // bound is the caller's cap and not this line, and a reader of this line\n // alone would conclude otherwise.\n if (\n result.outcome === 'renamed' ||\n (input.action === 'restore' && result.outcome === 'created')\n )\n await fireCascade(locale)\n }\n }\n}\n\n/**\n * The site whose path index a write maintains.\n *\n * - A write the caller made directly: the request's site. For a site-bound\n * entity that is exactly the site `AdminClient` confined the write to (the\n * same rule — explicit site, else the single-site answer); for a shared\n * routable entity, which only a single-site app may have, the one site.\n * - A cascade child (`viaCascade`) of a site-bound entity: the site its path\n * rows already record. A cascade from a SHARED parent crosses sites, so the\n * request's site is not the child's. With no rows there is nothing to\n * remove and nothing a recompute could be confined by — `null`, logged.\n */\nasync function siteForPathWrite(\n app: ToolkitApp,\n entity: Entity,\n input: PathIndexWriteInput,\n): Promise<string | null> {\n if (input.viaCascade === true && isSiteBound(entity)) {\n const recorded = await siteOfDocumentPaths(app.db.readWrite, input.entityName, input.id)\n if (recorded === null) {\n app.logger.warn(\n { entity: input.entityName, id: input.id },\n 'toolkit_paths: a document has path rows on SEVERAL sites — refusing to guess which one ' +\n 'this cascade maintains; `lumi paths:rebuild` per site repairs it',\n )\n return null\n }\n if (recorded !== undefined) return recorded\n const ambient = getCurrentSite()\n if (ambient !== undefined) return ambient\n // A single-site install has only one candidate.\n const implicit = await resolveImplicitSite(app).catch(() => undefined)\n if (implicit !== undefined) return implicit\n app.logger.warn(\n { entity: input.entityName, id: input.id, action: input.action },\n 'toolkit_paths: cascade child has no path rows and the request names no site — ' +\n 'nothing to maintain here; `lumi paths:rebuild` recomputes it per site',\n )\n return null\n }\n return requireRequestSite('toolkit_paths', app)\n}\n\n/**\n * PRE-commit path-conflict guard for a routable entity. Resolves the candidate\n * path for the write's locale, throws on a reserved-prefix hit or a manual\n * conflict, and auto-suffixes `data.slug` in place otherwise (persisted by the\n * same write).\n */\nexport function createPathConflictGuard(app: ToolkitApp, entity: Entity): PathConflictGuard {\n return async (input) => {\n // Conflicts are per site (multisite D005, F004): the guard sees only the\n // writing site's paths, so a URL another site owns is no conflict — and a\n // 409 can never reveal that it exists there.\n const pathsClient = toolkitPathsTable.makeClient(\n app.db.readWrite,\n await requireRequestSite('toolkit_paths', app),\n )\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const reservedPathPrefixes = config.reservedPathPrefixes ?? []\n // Base/default writes check the default locale; `updateForLocale` checks\n // its own locale. Only one locale per write (F010 accepted simplification).\n const locale = input.locale ?? defaultLocale\n\n // For an update, load the pre-write locale-merged row so path tokens the\n // write didn't touch (slug on a title-only edit, parent, …) still resolve.\n // Lazy client construction — see writer note on recursion.\n let currentRow: Record<string, unknown> | null = null\n if (input.id !== undefined) {\n currentRow = (await app\n .getClient(entity)\n .findById(input.id, { locale, defaultLocale })) as Record<string, unknown> | null\n }\n\n // Effective slug mode for this locale — the same rule `sluggable()` itself\n // uses (explicit write value wins, else the current row's, else 'auto'),\n // shared via `resolveEffectiveSlugMode` so the two never drift apart.\n const slugMode = resolveEffectiveSlugMode(input.data.slugMode, currentRow?.slugMode)\n\n await ensurePathAvailable({\n entity,\n data: input.data,\n currentRow,\n id: input.id,\n locale,\n defaultLocale,\n slugMode,\n reservedPathPrefixes,\n pathsClient,\n })\n }\n}\n\n/**\n * Write the path-writers factory into the process-global slot. Called from the\n * content plugin's `init`. Core's `createAdminClient` reads it by `Symbol.for`\n * identity — keep {@link PATH_WRITERS_KEY} in sync with the reader in\n * `packages/core/src/client-factories.ts`.\n */\nexport function setActivePathWritersFactory(): void {\n const factory: PathWritersFactory = (app, entity) => ({\n pathIndexWriter: createPathIndexWriter(app, entity),\n pathConflictGuard: createPathConflictGuard(app, entity),\n })\n ;(globalThis as Record<symbol, unknown>)[PATH_WRITERS_KEY] = factory\n}\n","/**\n * Routing path-lifecycle queue jobs (plan/routing PR04 Task 2).\n *\n * Two on-demand (never scheduled) jobs maintain the `toolkit_paths` index\n * beyond a single first-party entity write:\n *\n * 1. `content:paths-cascade` — per-event. Fired from Task 1's cascade-trigger\n * seam when a `pathParent()` ancestor's canonical path changes in a locale.\n * A SINGLE invocation BFS-walks the ancestor's full (bounded) descendant\n * subtree and regenerates each descendant's canonical row — because every\n * descendant's `[parent.path]` expansion now points at a stale ancestor URL.\n *\n * 2. `content:paths-rebuild` — on-demand (the `lumi paths:rebuild` CLI, a later\n * task). Recomputes every routable document's canonical row, optionally\n * scoped to one entity/locale; `dryRun` reports the diff without writing.\n *\n * CRITICAL — no self-enqueue. `QueueClient.enqueue()` throws\n * `QueueSelfEnqueueError` if a handler enqueues its OWN job type (the recurrence\n * anti-pattern). So the cascade job must NOT re-enqueue itself per subtree\n * level. Instead each invocation walks the WHOLE subtree in-process, and applies\n * descendant path changes DIRECTLY via {@link applyCanonicalPath} — never\n * through `AdminClient.update`, which would re-fire the entity-hook writer's\n * cascade seam and try to enqueue `content:paths-cascade` from inside the\n * `content:paths-cascade` handler. Neither `runPathsCascade` nor\n * `runPathsRebuild` ever calls `enqueue`; the only enqueue is the trigger in\n * {@link registerPathJobs}, which runs OUTSIDE any job handler.\n *\n * Both jobs are bounded (CLAUDE.md's no-unbounded-fan-out rule): the cascade\n * caps total descendants processed and logs truncation; the rebuild pages\n * through documents and runs each page with bounded concurrency. Neither ever\n * `Promise.all`s over an unbounded set.\n *\n * NOTE: reachable only via `plugin.ts` (the jiti-safe plugin entry), never\n * content's `server-only` main entry. `@murumets-ee/queue/client` is\n * DYNAMIC-imported inside {@link registerPathJobs} so queue stays a peer off\n * this module's static graph (the blocks-jobs precedent).\n */\n\nimport {\n getCurrentSite,\n requireRequestSite,\n runAsCli,\n runInEverySite,\n type ToolkitApp,\n} from '@murumets-ee/core'\nimport { type Entity, hasPathParent, MAX_PATH_DEPTH } from '@murumets-ee/entity'\nimport { asc, inArray } from 'drizzle-orm'\nimport type { PgColumn } from 'drizzle-orm/pg-core'\nimport { z } from 'zod'\nimport { getContentConfig } from '../content-config-store.js'\nimport { applyCanonicalPath, mapBounded } from './apply-path.js'\nimport { computeCanonicalPath } from './generate-path.js'\nimport { toolkitPathsTable } from './schema.js'\nimport { setActiveCascadeTrigger } from './writers.js'\n\n// ── Job names (Task 3's CLI depends on these EXACT strings) ──────────────────\n\n/** Descendant-regeneration job. `pluginName:action` convention. */\nexport const PATHS_CASCADE_JOB_NAME = 'content:paths-cascade'\n/** Full-index rebuild job. `pluginName:action` convention. */\nexport const PATHS_REBUILD_JOB_NAME = 'content:paths-rebuild'\n\n/** Orphan-row prune job (F068). `lumi paths:rebuild --orphans` enqueues it. */\nexport const PATHS_PRUNE_JOB_NAME = 'content:paths-prune'\n\n// ── Bounds (see CLAUDE.md no-unbounded-fan-out) ──────────────────────────────\n\n/**\n * Cascade total-descendant ceiling. Depth is already capped at 10 by\n * `pathParent()`'s cycle/depth check, but BREADTH (siblings at any one level) is\n * uncapped — a single ancestor could have thousands of descendants. 2000 is a\n * generous ceiling for an interactive-triggered job that must not exhaust the\n * connection pool; beyond it we log truncation and point at `lumi paths:rebuild`\n * (Job 2) for guaranteed full coverage.\n */\nconst CASCADE_MAX_ITEMS = 2000\n\n/** Worker-pool size within one BFS level / rebuild page (the `runMeritBatch` shape). */\nconst DEFAULT_CONCURRENCY = 5\n\n/** Rebuild page size — documents fetched per `findMany` (never load a whole table). */\nconst REBUILD_PAGE_SIZE = 200\n\n/**\n * Max diff entries stored in the rebuild job's progress blob. `toolkit_jobs.progress`\n * is capped at ~7 KB (PROGRESS_MAX_BYTES) — an oversized payload is DROPPED, losing\n * even the counters. Each entry serializes to ~200 bytes, so 25 keeps the final\n * progress well under the cap while leaving room for the counters + scope. Overflow\n * beyond this is reported via `sampleTruncated` + the aggregate counts (never silent).\n */\nconst DIFF_SAMPLE_CAP = 25\n\n// ── Payloads (Task 3's CLI builds these) ─────────────────────────────────────\n\n/** `content:paths-cascade` payload. */\nexport const cascadePayloadSchema = z.object({\n /** The renamed ancestor's entity type (must be a `pathParent()` hierarchy). */\n entityType: z.string().min(1),\n /** The renamed ancestor document's id. */\n parentId: z.string().uuid(),\n /** The single locale whose canonical path changed. */\n locale: z.string().min(1),\n})\nexport type CascadePayload = z.infer<typeof cascadePayloadSchema>\n\n/** `content:paths-rebuild` payload. Omit `entity`/`locale` for all routable entities / all locales. */\nexport const rebuildPayloadSchema = z.object({\n entity: z.string().min(1).optional(),\n locale: z.string().min(1).optional(),\n dryRun: z.boolean(),\n})\nexport type RebuildPayload = z.infer<typeof rebuildPayloadSchema>\n\n/** `content:paths-prune` payload. `dryRun` reports the orphans without deleting. */\nexport const prunePayloadSchema = z.object({\n dryRun: z.boolean(),\n})\nexport type PrunePayload = z.infer<typeof prunePayloadSchema>\n\n/** Hard cap on rows one prune run deletes (bounded, off the request path). */\nconst PRUNE_MAX_ROWS = 10_000\n/** Rows read + deleted per statement. */\nconst PRUNE_BATCH = 500\n\n/** The `toolkit_jobs.progress` blob / result of {@link runPathsPrune}. */\nexport interface PruneResult {\n dryRun: boolean\n /** Entity types holding rows but no longer routable, with their row counts. */\n orphans: Array<{ entityType: string; rows: number }>\n /** Total orphan rows found (sum of `orphans[].rows`). */\n found: number\n /** Rows actually deleted (0 on a dry run). */\n deleted: number\n /** True when the {@link PRUNE_MAX_ROWS} cap stopped the run — re-run to continue. */\n truncated: boolean\n /** True when NO entity declares a frontend path: the run refused, deleting nothing. */\n refused: boolean\n}\n\n// ── Progress / result shapes (Task 3 reads these back via getJob(id).progress) ─\n\n/** Cascade progress snapshot (also the shape stored in `toolkit_jobs.progress`). */\nexport interface CascadeProgress {\n phase: 'walking' | 'done'\n /** Descendants examined so far. */\n processed: number\n /** Descendants whose canonical row was (re)generated (`created` or `renamed`). */\n regenerated: number\n /** Descendants already at the correct path (no-op). */\n unchanged: number\n /** Descendants skipped (broken link, reserved prefix, or cross-document conflict). */\n skipped: number\n /** Descendants that threw during processing (isolated — did not abort the batch). */\n failed: number\n /** True when the subtree exceeded {@link CASCADE_MAX_ITEMS} (deeper nodes NOT processed). */\n truncated: boolean\n}\n\n/** Cascade final result (returned by {@link runPathsCascade}). Counters only — no `phase`. */\nexport interface CascadeResult extends Omit<CascadeProgress, 'phase'> {\n entityType: string\n parentId: string\n locale: string\n /** The active cap for this run (echoes {@link CASCADE_MAX_ITEMS} unless overridden in tests). */\n cap: number\n}\n\n/** One rebuild diff entry (a document whose canonical row differs from the recompute). */\nexport interface RebuildDiffEntry {\n /** `changed` — path differs; `missing` — no current canonical; `conflict` — target owned by another doc. */\n kind: 'changed' | 'missing' | 'conflict'\n entityType: string\n entityId: string\n locale: string\n oldPath: string | null\n newPath: string | null\n /** For `kind:'conflict'`: entityId of the document already owning `newPath`. */\n conflictWith?: string\n}\n\n/** Rebuild progress snapshot (also the shape stored in `toolkit_jobs.progress`). */\nexport interface RebuildProgress {\n phase: 'scanning' | 'done'\n dryRun: boolean\n scope: { entity: string | null; locale: string | null }\n /**\n * Documents examined, SUMMED across every convergence pass — a document\n * re-scanned in a later pass is counted again. CUMULATIVE (like\n * `changed`/`applied`), NOT a terminal-state count. Kept cumulative so a reader\n * can see total scan work; the four \"outstanding state\" fields below are\n * terminal instead (see the note on `passes`).\n */\n scanned: number\n /**\n * Number of times a document's canonical path was corrected (or, in dry-run,\n * WOULD change), SUMMED across passes. CUMULATIVE work-log — a doc corrected\n * over N passes counts N times. (Fix 1 keeps this cumulative: \"how much\n * correcting happened\" is a meaningful metric.)\n */\n changed: number\n /**\n * FINAL (terminal) state, not a per-pass sum (Fix 1): documents STILL lacking a\n * canonical row after the last pass — a would-insert count in dry-run, and 0 in\n * a real run that inserted them all (a doc created in an early real pass is no\n * longer \"missing\" at the end).\n */\n missing: number\n /**\n * FINAL (terminal) state, not a per-pass sum (Fix 1): documents whose target\n * path STILL collides with a DIFFERENT document's canonical after the last\n * pass. A collision a later pass FREES (e.g. the colliding doc was itself\n * renamed away) is NOT counted — so a non-zero value is a real, outstanding URL\n * collision to resolve, never a transient scan-order artifact of an early pass.\n */\n conflicts: number\n /**\n * FINAL (terminal) state, not a per-pass sum (Fix 1): documents at the correct\n * path as of the last pass that observed them.\n */\n unchanged: number\n /**\n * FINAL (terminal) state, not a per-pass sum (Fix 1): documents skipped (broken\n * link or reserved prefix) as of the last pass that observed them.\n */\n skipped: number\n /** Rows actually written, SUMMED across every convergence pass (always 0 in dry-run). */\n applied: number\n /** Documents that threw during processing (isolated), SUMMED across passes. */\n failed: number\n /**\n * Number of full convergence passes the real-write path ran (see\n * {@link runPathsRebuild}'s outer loop). Always `1` for dry-run and for the\n * common case (no `pathParent()` hierarchy drift); higher only when a nested\n * hierarchy needed multiple passes to fully converge (bounded by\n * `MAX_PATH_DEPTH`). `scanned`/`changed`/`applied`/`failed` are CUMULATIVE\n * across these passes (a work-log of how much scanning/correcting happened);\n * `missing`/`conflicts`/`unchanged`/`skipped` instead reflect the FINAL\n * (terminal, last-observed) per-document state after the last pass (Fix 1), so\n * a problem an early pass saw but a later pass resolved is not double-reported.\n */\n passes: number\n /**\n * Present (and the relevant flag `true`) ONLY when a `--entity`/`--locale`\n * filter was set but matched nothing — the sweep then no-ops over an empty\n * scope, which is indistinguishable from \"nothing needed fixing\" by the counts\n * alone. Surfaced here (Fix 3) so the CLI — which polls this progress blob but\n * cannot pre-validate `--locale` (content's configured-locale list isn't loaded\n * under `initPlugins: 'none'`) — can print an explicit \"matched nothing\" note.\n * Omitted entirely when both filters matched (or weren't set).\n */\n unmatchedScope?: { entity: boolean; locale: boolean }\n /**\n * Present only in dry-run when the scope includes a `pathParent()` hierarchy:\n * the preview reflects a SINGLE pass, so a hierarchy drifted at multiple levels\n * may not be fully resolved in the diff — the real (non-dry-run) rebuild's\n * multi-pass convergence always resolves it. Omitted otherwise.\n */\n hierarchyNote?: string\n /** Bounded sample of `changed`/`missing`/`conflict` entries (see {@link DIFF_SAMPLE_CAP}). */\n sample: RebuildDiffEntry[]\n /** True when more diff entries existed than {@link DIFF_SAMPLE_CAP} could hold. */\n sampleTruncated: boolean\n}\n\n/** Rebuild final result (returned by {@link runPathsRebuild}). */\nexport type RebuildResult = Omit<RebuildProgress, 'phase'>\n\nexport interface CascadeRunOptions {\n /** Override the total-descendant cap (tests use a small value). */\n maxItems?: number\n /** Override the per-level worker-pool size. */\n concurrency?: number\n /** Progress callback (the handler bridges this to `JobContext.updateProgress`). */\n onProgress?: (progress: CascadeProgress) => void\n}\n\nexport interface RebuildRunOptions {\n pageSize?: number\n concurrency?: number\n sampleCap?: number\n onProgress?: (progress: RebuildProgress) => void\n}\n\n// ── Job 1 — cascade ──────────────────────────────────────────────────────────\n\n/**\n * Regenerate the canonical `toolkit_paths` rows for every descendant of a\n * renamed `pathParent()` ancestor, in one bounded, single-invocation BFS walk.\n *\n * Ordering: level by level (parents strictly before their own children) —\n * within a level, siblings are independent (their `[parent.path]` expands\n * against a PREVIOUS level's already-written canonical), so each level runs with\n * bounded concurrency; the next level is discovered + processed only AFTER the\n * current level's writes have flushed. Descendant path changes are applied\n * DIRECTLY (never via `AdminClient.update`) so the entity-hook cascade seam is\n * not re-fired — that is what keeps this off the queue self-enqueue guard.\n */\nexport async function runPathsCascade(\n app: ToolkitApp,\n payload: CascadePayload,\n opts: CascadeRunOptions = {},\n): Promise<CascadeResult> {\n const { entityType, parentId, locale } = payload\n const cap = opts.maxItems ?? CASCADE_MAX_ITEMS\n const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY\n\n const counters = {\n processed: 0,\n regenerated: 0,\n unchanged: 0,\n skipped: 0,\n failed: 0,\n truncated: false,\n }\n const snapshot = (phase: CascadeProgress['phase']): CascadeProgress => ({ phase, ...counters })\n const result = (): CascadeResult => ({ entityType, parentId, locale, cap, ...counters })\n\n const entity = app.entities.get(entityType)\n if (!entity) {\n app.logger.warn(\n { entityType, parentId, locale },\n 'paths-cascade: unknown entity type — nothing to regenerate',\n )\n opts.onProgress?.(snapshot('done'))\n return result()\n }\n if (!hasPathParent(entity)) {\n // Defensive: the trigger only fires for pathParent entities. A cascade for a\n // non-hierarchy entity has no `[parent.path]` descendants to regenerate.\n app.logger.warn(\n { entityType, parentId, locale },\n 'paths-cascade: entity is not a pathParent() hierarchy — nothing to regenerate',\n )\n opts.onProgress?.(snapshot('done'))\n return result()\n }\n\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const reservedPathPrefixes = config.reservedPathPrefixes ?? []\n // Per site (multisite phase 03): the job runs in the site it was enqueued\n // for (the queue captures it), or once per site for a sweep.\n const pathsClient = toolkitPathsTable.makeClient(\n app.db.readWrite,\n await requireRequestSite('toolkit_paths', app),\n )\n const client = app.getClient(entity)\n // Dynamic `parent` self-reference column (pathParent adds it). `getTable()` is\n // `PgTableWithColumns<any>`; the structural cast narrows to the one column we\n // filter on — the same idiom rent uses for dynamic column refs.\n const parentColumn = (client.getTable() as unknown as { parent: PgColumn }).parent\n\n const processOne = async (row: Record<string, unknown>): Promise<void> => {\n const id = row.id as string\n try {\n const computed = await computeCanonicalPath({\n entity,\n row,\n locale,\n defaultLocale,\n pathsClient,\n })\n const applied = await applyCanonicalPath({\n pathsClient,\n entityType,\n entityId: id,\n locale,\n path: computed.path,\n ...(computed.skipReason !== undefined && { skipReason: computed.skipReason }),\n reservedPathPrefixes,\n logger: app.logger,\n })\n switch (applied.outcome) {\n case 'created':\n case 'renamed':\n counters.regenerated += 1\n break\n case 'unchanged':\n counters.unchanged += 1\n break\n default:\n // skipped-null / skipped-reserved / conflict\n counters.skipped += 1\n break\n }\n } catch (err) {\n counters.failed += 1\n app.logger.warn(\n { err, entityType, id, locale },\n 'paths-cascade: failed to regenerate one descendant path — continuing (per-item isolation)',\n )\n }\n }\n\n // BFS: `expand` holds the ids whose children form the NEXT level to process.\n // Start by expanding the renamed ancestor itself (its own canonical was\n // already written by the entity-hook writer that fired this cascade).\n let expand: string[] = [parentId]\n while (expand.length > 0) {\n const remaining = cap - counters.processed\n if (remaining <= 0) {\n counters.truncated = true\n break\n }\n // One query per level (inArray over the whole level's parents), locale-merged\n // so each row carries the right per-locale slug. `remaining + 1` detects\n // overflow past the cap without another round trip.\n const level = (await client.findMany({\n where: inArray(parentColumn, expand),\n locale,\n defaultLocale,\n limit: remaining + 1,\n })) as Array<Record<string, unknown>>\n if (level.length === 0) break\n\n let batch = level\n if (level.length > remaining) {\n batch = level.slice(0, remaining)\n counters.truncated = true\n }\n\n await mapBounded(batch, concurrency, processOne)\n counters.processed += batch.length\n opts.onProgress?.(snapshot('walking'))\n\n if (counters.truncated) break\n // Next level = the ids we just wrote (their canonical paths are now current).\n expand = batch.map((r) => r.id as string)\n }\n\n if (counters.truncated) {\n app.logger.warn(\n { entityType, parentId, locale, processed: counters.processed, cap },\n 'paths-cascade: descendant subtree exceeded the cap — deeper/remaining descendants were ' +\n 'NOT regenerated. Run `lumi paths:rebuild --entity ' +\n `${entityType}\\` for guaranteed full coverage.`,\n )\n }\n\n opts.onProgress?.(snapshot('done'))\n return result()\n}\n\n// ── Job 2 — rebuild ──────────────────────────────────────────────────────────\n\n/**\n * Recompute every routable document's canonical `toolkit_paths` row (optionally\n * scoped to one entity and/or locale). `dryRun` reports the diff without\n * writing; otherwise each drifted row is corrected via the SAME\n * demote-then-insert-if-changed writer as the cascade + entity hook.\n *\n * Pages through documents with KEYSET (cursor) pagination on the immutable `id`\n * PK — NOT offset. Offset paging has undefined/shifting row order across page\n * boundaries under concurrent inserts/deletes during a large sweep (autovacuum,\n * an admin creating/deleting a document mid-rebuild), which silently skips or\n * double-processes documents and would undermine the \"full coverage\" guarantee\n * this job exists to provide. Keyset (`WHERE id > lastId ORDER BY id ASC`) visits\n * every document exactly once regardless of concurrent mutation. Each page runs\n * with bounded concurrency; the accumulated diff sample is bounded so the final\n * progress fits under the `toolkit_jobs.progress` cap.\n *\n * BOUNDED MULTI-PASS CONVERGENCE (real-write path). Documents are scanned in\n * ascending `id` (a random UUID — uncorrelated with hierarchy depth). A\n * `pathParent()` descendant scanned BEFORE its ancestor in the same sweep\n * expands `[parent.path]` against the ancestor's not-yet-corrected canonical\n * ({@link computeCanonicalPath} reads the parent's PERSISTED `toolkit_paths`\n * row), so one sweep can leave nested URLs stale. The fix: repeat the full sweep\n * until a pass writes nothing. Each pass corrects one more depth level — depth-0\n * roots are right after pass 1 (no parent dependency); a depth-`d` document is\n * right by the START of pass `d+1` because its parent is already right by then —\n * so `MAX_PATH_DEPTH` passes guarantee full convergence for any hierarchy this\n * codebase can build (deeper trees are rejected at write time by `pathParent()`'s\n * own cycle/depth check). We stop the instant a pass writes zero rows, so the\n * common case (no hierarchy drift, or a non-hierarchy entity) converges in\n * exactly ONE pass. Counters are CUMULATIVE across passes (see `passes` on\n * {@link RebuildProgress}). Dry-run writes nothing, so multi-pass can't converge\n * it — it runs a single pass and surfaces `hierarchyNote` when the scope includes\n * a hierarchy, rather than over-engineering an in-memory simulated convergence.\n */\nexport async function runPathsRebuild(\n app: ToolkitApp,\n payload: RebuildPayload,\n opts: RebuildRunOptions = {},\n): Promise<RebuildResult> {\n const pageSize = opts.pageSize ?? REBUILD_PAGE_SIZE\n const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY\n const sampleCap = opts.sampleCap ?? DIFF_SAMPLE_CAP\n const dryRun = payload.dryRun\n\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const reservedPathPrefixes = config.reservedPathPrefixes ?? []\n const allLocales = config.locales.map((l) => l.code)\n const targetLocales = payload.locale ? allLocales.filter((c) => c === payload.locale) : allLocales\n // Every entity declaring a frontend path, filtered to `payload.entity` if given.\n // Truthiness, NOT `!== undefined`. `client-factories.ts`'s\n // `readActivePathWriters` gate and `AdminClient`'s `routable` flag both test\n // `!!entity.frontend?.path`, so `frontend: { path: '' }` is non-routable to\n // the write path — and was routable here, which would have this job mint\n // canonical rows for a document no writer ever maintains. Nothing declares\n // an empty path today; the divergence is closed while it is still free.\n const targetEntities = [...app.entities.values()].filter(\n (e) => !!e.frontend?.path && (payload.entity === undefined || e.name === payload.entity),\n )\n\n // A typo'd / unknown `--entity` or `--locale` narrows the scope to nothing.\n // Left silent, the job would complete \"successfully\" with all-zero counts —\n // indistinguishable from \"nothing needed fixing\". Warn (mirroring the cascade\n // job's \"unknown entity type\" precedent above) so a likely typo is surfaced;\n // the sweep below then no-ops naturally over the empty scope (still a success,\n // not a hard error).\n const unmatchedEntity = payload.entity !== undefined && targetEntities.length === 0\n const unmatchedLocale = payload.locale !== undefined && targetLocales.length === 0\n if (unmatchedEntity) {\n app.logger.warn(\n { entity: payload.entity, dryRun },\n 'paths-rebuild: --entity matches no routable entity (one declaring frontend.path) — ' +\n 'nothing to rebuild (likely a typo in the entity name)',\n )\n }\n if (unmatchedLocale) {\n app.logger.warn(\n { locale: payload.locale, configuredLocales: allLocales, dryRun },\n 'paths-rebuild: --locale matches no configured locale — nothing to rebuild ' +\n '(likely a typo in the locale code)',\n )\n }\n // The worker's warn above only reaches the WORKER log, never the CLI. Surface\n // the same signal in the progress blob the CLI polls so it can print an\n // explicit \"matched nothing\" note (Fix 3) — the `--locale` half is otherwise\n // invisible to a CLI operator (it can't pre-validate the locale list). Present\n // only when a set filter matched nothing.\n const unmatchedScope: RebuildProgress['unmatchedScope'] =\n unmatchedEntity || unmatchedLocale\n ? { entity: unmatchedEntity, locale: unmatchedLocale }\n : undefined\n\n // Dry-run can't converge a multi-level `pathParent()` hierarchy the way the\n // real run does (the multi-pass loop relies on each pass reading the PERSISTED\n // writes of the previous one; dry-run writes nothing). Rather than simulate\n // convergence in memory (real scope creep), surface an honest caveat when the\n // scope includes a hierarchy entity: the preview is a best-effort SINGLE pass;\n // the real (non-dry-run) rebuild always fully converges.\n const scopeHasHierarchy = targetEntities.some((e) => hasPathParent(e))\n const hierarchyNote =\n dryRun && scopeHasHierarchy\n ? 'Preview reflects a SINGLE pass; a pathParent() hierarchy drifted at multiple levels may ' +\n \"need the real (non-dry-run) rebuild's multi-pass convergence to fully resolve. The real \" +\n 'run always converges.'\n : undefined\n\n // Per site (multisite phase 03): the job runs in the site it was enqueued\n // for (the queue captures it), or once per site for a sweep.\n const pathsClient = toolkitPathsTable.makeClient(\n app.db.readWrite,\n await requireRequestSite('toolkit_paths', app),\n )\n\n // CUMULATIVE work-log counters — SUMMED across every convergence pass.\n const counters = {\n scanned: 0,\n changed: 0,\n applied: 0,\n failed: 0,\n }\n // TERMINAL (last-observed) per-document outcome for the four \"outstanding\n // state\" categories (Fix 1). A document's outcome can legitimately change\n // between passes — a pass-1 `conflict` whose colliding URL a later pass frees\n // becomes a `renamed` — so these must report the FINAL state, not a per-pass\n // sum (which would phantom-report an already-self-resolved problem). Maintained\n // last-write-wins with O(1) tallies (keyed by `entityType:entityId:locale`) so\n // building a snapshot stays cheap even on a large, many-page sweep.\n type TerminalState = 'missing' | 'conflicts' | 'unchanged' | 'skipped' | 'resolved'\n const terminalOf = new Map<string, TerminalState>()\n // 'resolved' (a real write, or a dry-run would-change) is NOT an outstanding\n // state — it's captured by the cumulative `changed`/`applied` totals — so it is\n // tracked in the map (to overwrite an earlier problem entry) but never tallied.\n const terminal = { missing: 0, conflicts: 0, unchanged: 0, skipped: 0 }\n const setTerminal = (key: string, next: TerminalState): void => {\n const prev = terminalOf.get(key)\n if (prev === next) return\n if (prev !== undefined && prev !== 'resolved') terminal[prev] -= 1\n terminalOf.set(key, next)\n if (next !== 'resolved') terminal[next] += 1\n }\n const sample: RebuildDiffEntry[] = []\n let sampleTruncated = false\n const addSample = (entry: RebuildDiffEntry): void => {\n if (sample.length < sampleCap) sample.push(entry)\n else sampleTruncated = true\n }\n // Number of full convergence passes actually run (see the outer loop below).\n let passes = 0\n const snapshot = (phase: RebuildProgress['phase']): RebuildProgress => ({\n phase,\n dryRun,\n scope: { entity: payload.entity ?? null, locale: payload.locale ?? null },\n scanned: counters.scanned,\n changed: counters.changed,\n applied: counters.applied,\n failed: counters.failed,\n missing: terminal.missing,\n conflicts: terminal.conflicts,\n unchanged: terminal.unchanged,\n skipped: terminal.skipped,\n passes,\n sample,\n sampleTruncated,\n ...(hierarchyNote !== undefined ? { hierarchyNote } : {}),\n ...(unmatchedScope !== undefined ? { unmatchedScope } : {}),\n })\n\n // Compute + apply the canonical path for ONE (document, locale): updates the\n // cumulative counters, the terminal-state map, and the diff sample. Returns\n // `true` iff it made a REAL canonical write (created/renamed on the non-dry-run\n // path) so the caller can sum \"how many rows this pass wrote\" for the\n // convergence stop signal. Extracted (Fix 2) so BOTH the per-page keyset scan\n // (runOnePass) and the targeted problem-doc retry (retryProblemDocuments) go\n // through ONE outcome-handling path — no drift between the two callers.\n const processDocument = async (\n entity: Entity,\n entityType: string,\n row: Record<string, unknown>,\n locale: string,\n ): Promise<boolean> => {\n const id = row.id as string\n let wrote = false\n try {\n const computed = await computeCanonicalPath({\n entity,\n row,\n locale,\n defaultLocale,\n pathsClient,\n })\n const applied = await applyCanonicalPath({\n pathsClient,\n entityType,\n entityId: id,\n locale,\n path: computed.path,\n ...(computed.skipReason !== undefined && { skipReason: computed.skipReason }),\n // `parent-not-indexed` is EXPECTED here on a cold `toolkit_paths`\n // index: this sweep scans in ascending id (uncorrelated with\n // hierarchy depth — see the function docblock), so a descendant\n // legitimately outruns its not-yet-created ancestor on pass 1 and\n // self-heals by the ancestor's own pass. Warning on every such hit\n // across every pass of a first-time rebuild would be exactly the\n // \"trains operators to ignore the message\" failure F075 exists to\n // avoid — this job already reports its own skipped/regenerated\n // counts in its progress/summary.\n suppressNoteworthy: ['parent-not-indexed'],\n reservedPathPrefixes,\n logger: app.logger,\n dryRun,\n })\n // Terminal-outcome key: last-write-wins across passes (Fix 1).\n const termKey = `${entityType}:${id}:${locale}`\n switch (applied.outcome) {\n case 'unchanged':\n setTerminal(termKey, 'unchanged')\n break\n case 'skipped-null':\n case 'skipped-reserved':\n setTerminal(termKey, 'skipped')\n break\n case 'conflict':\n setTerminal(termKey, 'conflicts')\n addSample({\n kind: 'conflict',\n entityType,\n entityId: id,\n locale,\n oldPath: applied.oldPath,\n newPath: applied.newPath,\n ...(applied.conflictWith ? { conflictWith: applied.conflictWith.entityId } : {}),\n })\n break\n case 'created':\n if (!dryRun) {\n // Real insert — the doc is no longer \"missing\"; the write is\n // captured by cumulative `applied`, so its terminal state is\n // 'resolved' (not tallied), and a later pass may mark it\n // 'unchanged'. In dry-run nothing is written, so it stays a\n // would-insert → terminal 'missing'.\n counters.applied += 1\n wrote = true\n setTerminal(termKey, 'resolved')\n } else {\n setTerminal(termKey, 'missing')\n }\n addSample({\n kind: 'missing',\n entityType,\n entityId: id,\n locale,\n oldPath: null,\n newPath: applied.newPath,\n })\n break\n case 'renamed':\n // `changed` is the cumulative \"how many corrections happened\"\n // work-log; a rename (real OR dry-run would-change) is therefore\n // 'resolved' terminally, never an outstanding-state category.\n counters.changed += 1\n if (!dryRun) {\n counters.applied += 1\n wrote = true\n }\n setTerminal(termKey, 'resolved')\n addSample({\n kind: 'changed',\n entityType,\n entityId: id,\n locale,\n oldPath: applied.oldPath,\n newPath: applied.newPath,\n })\n break\n }\n } catch (err) {\n counters.failed += 1\n app.logger.warn(\n { err, entityType, id, locale },\n 'paths-rebuild: failed to process one document — continuing (per-item isolation)',\n )\n }\n return wrote\n }\n\n // One full sweep over every (entity, locale) in scope. Returns the number of\n // canonical rows this pass WROTE (created/renamed) — 0 means the index has\n // converged (or the only remaining diffs are conflicts / skips / failures that\n // more passes can't resolve), which is the outer loop's stop signal.\n const runOnePass = async (entities: readonly Entity[]): Promise<number> => {\n let passApplied = 0\n for (const entity of entities) {\n const client = app.getClient(entity)\n const entityType = entity.name\n // `id` column ref for the stable keyset ORDER BY — `findMany`'s `orderBy`\n // takes a Drizzle SQL/column, not the TableClient's `{ column, dir }` shape.\n // `getTable()` is `PgTableWithColumns<any>`; the structural cast narrows to\n // the one column we sort on (same idiom the cascade job uses for `parent`).\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n for (const locale of targetLocales) {\n // Keyset cursor on the immutable `id` PK (see the job docblock): visits\n // every document exactly once even under concurrent inserts/deletes, which\n // OFFSET paging cannot guarantee.\n let cursorId: string | undefined\n for (;;) {\n const page = (await client.findMany({\n locale,\n defaultLocale,\n limit: pageSize,\n orderBy: asc(idColumn),\n ...(cursorId !== undefined\n ? { cursor: { field: 'id', value: cursorId, direction: 'asc' } }\n : {}),\n })) as Array<Record<string, unknown>>\n if (page.length === 0) break\n\n await mapBounded(page, concurrency, async (row) => {\n const wrote = await processDocument(entity, entityType, row, locale)\n // `+= 1` between awaits is atomic (JS single-threaded) — no lock.\n if (wrote) passApplied += 1\n })\n\n counters.scanned += page.length\n // Advance the cursor to the last (max-`id`) row of this page. `page` is\n // non-empty here (length-0 broke above), so `lastRow` is always defined.\n const lastRow = page[page.length - 1]\n if (lastRow) cursorId = lastRow.id as string\n opts.onProgress?.(snapshot('scanning'))\n if (page.length < pageSize) break\n }\n }\n }\n return passApplied\n }\n\n // Fix 2 (correctness gap in the passes-2+ narrowing) — targeted retry of the\n // SPECIFIC non-hierarchy documents left in an outstanding \"problem\" state\n // (conflict / skipped / missing) by an earlier pass.\n //\n // Canonical-path uniqueness is GLOBAL per `(locale, path)`, NOT scoped to\n // entity type. So a NON-hierarchy document can `conflict` in pass 1 because a\n // HIERARCHY document currently owns its target path; when that hierarchy\n // document later moves away (its own ancestor corrected in a later pass), the\n // path frees up. The passes-2+ scan narrows to `hierarchyEntities`, so it\n // never re-scans that non-hierarchy document — leaving a stale `conflicts`\n // entry a second `paths:rebuild` invocation would have resolved.\n //\n // Rather than widen the pass-2+ scan back to every scoped entity (which would\n // undo the narrowing win), re-run JUST those problem documents as O(1) point\n // lookups keyed off the `terminalOf` map we already maintain. The set is\n // naturally small and shrinks as problems resolve, so it doesn't reintroduce\n // the rescan-everything cost. Hierarchy entities are EXCLUDED — runOnePass\n // already full-scans them every pass. Returns the number of REAL canonical\n // writes made (summed into the pass total so the loop keeps iterating while\n // retries make progress).\n const retryProblemDocuments = async (\n hierarchyEntityNames: ReadonlySet<string>,\n ): Promise<number> => {\n // Snapshot the problem set BEFORE processing — `processDocument` mutates\n // `terminalOf`, and mutating a Map's values while iterating it is fragile.\n const retryTargets: Array<{\n entity: Entity\n entityType: string\n entityId: string\n locale: string\n }> = []\n for (const [key, state] of terminalOf) {\n if (state !== 'conflicts' && state !== 'skipped' && state !== 'missing') continue\n // Parse `${entityType}:${entityId}:${locale}`. Entity names and BCP47\n // locale codes are colon-free, so anchoring on the FIRST and LAST colon\n // extracts all three parts even though the middle id is opaque.\n const firstColon = key.indexOf(':')\n const lastColon = key.lastIndexOf(':')\n if (firstColon <= 0 || lastColon <= firstColon) continue\n const entityType = key.slice(0, firstColon)\n // Hierarchy entities are already fully re-scanned every pass by runOnePass —\n // retrying them here would double-process for no gain.\n if (hierarchyEntityNames.has(entityType)) continue\n const entity = app.entities.get(entityType)\n if (entity === undefined) continue\n retryTargets.push({\n entity,\n entityType,\n entityId: key.slice(firstColon + 1, lastColon),\n locale: key.slice(lastColon + 1),\n })\n }\n if (retryTargets.length === 0) return 0\n\n let retryApplied = 0\n await mapBounded(retryTargets, concurrency, async (t) => {\n const client = app.getClient(t.entity)\n // Locale-merged single-row point lookup (same merge the page scan uses) —\n // NOT a table scan, so this keeps the narrowing's cost profile. A row\n // deleted mid-rebuild resolves to null → nothing to retry.\n const row = (await client.findById(t.entityId, {\n locale: t.locale,\n defaultLocale,\n })) as Record<string, unknown> | null\n if (row === null) return\n // Re-examined this pass — count it in the cumulative scan work-log.\n counters.scanned += 1\n const wrote = await processDocument(t.entity, t.entityType, row, t.locale)\n if (wrote) retryApplied += 1\n })\n opts.onProgress?.(snapshot('scanning'))\n return retryApplied\n }\n\n // Bounded multi-pass convergence — see the function docblock. Multi-pass is\n // only meaningful for a `pathParent()` hierarchy (a `[parent.path]` document\n // scanned before its ancestor). A scope with NO hierarchy entity has no\n // cross-document dependency, so a single pass provably corrects every drifted\n // row (order-independent) — no confirming pass needed. Dry-run writes nothing,\n // so re-running can't converge it either — it does one pass and relies on\n // `hierarchyNote` to flag a possibly-incomplete hierarchy preview. When we DO\n // multi-pass, the loop stops the instant a pass writes nothing.\n // `[parent.path]` is a SAME-entity self-reference only (generate-path.ts — no\n // cross-entity-type dependency), so any NON-`pathParent()` entity is provably\n // converged after ONE pass: it has no ancestor whose later correction could\n // shift its path. Re-scanning it on passes 2+ is pure read-amplification (up to\n // MAX_PATH_DEPTH× the whole keyset of every unrelated entity). So pass 1 scans\n // the full original scope; passes 2+ narrow to the hierarchy entities that can\n // still drift (Fix 2).\n const hierarchyEntities = targetEntities.filter((e) => hasPathParent(e))\n const hierarchyEntityNames: ReadonlySet<string> = new Set(hierarchyEntities.map((e) => e.name))\n const maxPasses = dryRun || !scopeHasHierarchy ? 1 : MAX_PATH_DEPTH\n for (let pass = 0; pass < maxPasses; pass++) {\n passes = pass + 1\n let passApplied = await runOnePass(pass === 0 ? targetEntities : hierarchyEntities)\n // Passes 2+ ALSO retry the specific non-hierarchy documents whose earlier\n // problem state a hierarchy move THIS pass may have just freed (Fix 2). Runs\n // AFTER the hierarchy full-scan so the freed path is already visible. Point\n // lookups, not a rescan — keeps pass 1's narrowing win.\n if (pass > 0) passApplied += await retryProblemDocuments(hierarchyEntityNames)\n if (passApplied === 0) break\n }\n\n if (sampleTruncated) {\n app.logger.warn(\n {\n scanned: counters.scanned,\n changed: counters.changed,\n missing: terminal.missing,\n conflicts: terminal.conflicts,\n sampleCap,\n },\n 'paths-rebuild: more diff entries than the progress sample can hold — see the aggregate counts; ' +\n 'the stored sample is truncated',\n )\n }\n\n const final = snapshot('done')\n opts.onProgress?.(final)\n const { phase: _phase, ...rest } = final\n return rest\n}\n\n// ── Job 3 — prune orphan rows (F068) ─────────────────────────────────────────\n\n/**\n * Delete the `toolkit_paths` rows (canonical AND redirect history) of every\n * entity type that is NOT routable any more — it lost `frontend.path`, or is\n * no longer registered. Nothing else maintains them: `paths:rebuild` only\n * iterates routable entities, and an entity write cannot reach a type it no\n * longer routes. Resolution already answers `none` for them\n * (`ResolvePathDeps.isRoutable`); this removes the rows themselves, which also\n * frees their paths for a new document.\n *\n * **Bound on what it may touch.** Only rows whose `entity_type` is outside the\n * set of entities declaring `frontend.path` are selected, and each batch is\n * deleted by id from that selection, so a routable entity's rows can never be\n * reached. If NO entity is routable the run refuses (a mis-loaded config would\n * otherwise read as \"everything is an orphan\"). Total deletions per run are\n * capped at {@link PRUNE_MAX_ROWS}; the cap is reported, never silent.\n */\nexport async function runPathsPrune(\n app: ToolkitApp,\n payload: PrunePayload,\n opts: { maxRows?: number; batchSize?: number } = {},\n): Promise<PruneResult> {\n const maxRows = opts.maxRows ?? PRUNE_MAX_ROWS\n const batchSize = opts.batchSize ?? PRUNE_BATCH\n const result: PruneResult = {\n dryRun: payload.dryRun,\n orphans: [],\n found: 0,\n deleted: 0,\n truncated: false,\n refused: false,\n }\n\n const routable = new Set(\n [...app.entities.values()].filter((e) => !!e.frontend?.path).map((e) => e.name),\n )\n if (routable.size === 0) {\n app.logger.warn(\n { dryRun: payload.dryRun },\n 'paths-prune: no entity declares frontend.path — refusing to treat every row as an orphan',\n )\n result.refused = true\n return result\n }\n\n const pathsClient = toolkitPathsTable.makeClient(\n app.db.readWrite,\n await requireRequestSite('toolkit_paths', app),\n )\n const orphanTypes = (await pathsClient.distinct('entityType')).filter((t) => !routable.has(t))\n\n for (const entityType of orphanTypes) {\n const rows = await pathsClient.count({ entityType })\n if (rows > 0) result.orphans.push({ entityType, rows })\n result.found += rows\n }\n if (payload.dryRun) return result\n\n for (const { entityType } of result.orphans) {\n while (result.deleted < maxRows) {\n const batch = await pathsClient.findMany({\n where: { entityType },\n limit: Math.min(batchSize, maxRows - result.deleted),\n })\n if (batch.length === 0) break\n const gone = await pathsClient.deleteMany({ id: { in: batch.map((r) => r.id) } })\n result.deleted += gone.length\n }\n }\n result.truncated = result.deleted < result.found && result.deleted >= maxRows\n if (result.truncated) {\n app.logger.warn(\n { deleted: result.deleted, found: result.found, maxRows },\n 'paths-prune: row cap reached — run again to continue',\n )\n }\n return result\n}\n\n// ── Registration (dynamic-imports queue — keeps it a peer off the static graph) ─\n\n/**\n * Register both path jobs and wire the Task 1 cascade-trigger seam to enqueue\n * the cascade job. Called from content's plugin `init` (dynamic-imported so\n * `@murumets-ee/queue` stays a peer — the blocks-jobs precedent).\n *\n * The cascade trigger runs OUTSIDE any job handler (post-commit on an entity\n * write), so enqueuing `content:paths-cascade` there is safe; the cascade\n * HANDLER never enqueues anything (it walks the subtree in-process), so the\n * queue self-enqueue guard is never engaged.\n */\nexport async function registerPathJobs(app: ToolkitApp): Promise<void> {\n const { defineJob, registerJob, QueueClient } = await import('@murumets-ee/queue/client')\n\n const cascadeJob = defineJob({\n name: PATHS_CASCADE_JOB_NAME,\n description:\n 'Regenerate toolkit_paths canonical rows for the descendants of a renamed routable-hierarchy ' +\n 'ancestor (one locale). Walks the full bounded subtree in a single invocation.',\n schema: cascadePayloadSchema,\n })\n\n const rebuildJob = defineJob({\n name: PATHS_REBUILD_JOB_NAME,\n description:\n \"Recompute every routable document's canonical toolkit_paths row (optionally scoped to one \" +\n 'entity/locale); dryRun reports the diff without writing. On-demand (lumi paths:rebuild).',\n schema: rebuildPayloadSchema,\n })\n\n const pruneJob = defineJob({\n name: PATHS_PRUNE_JOB_NAME,\n description:\n 'Delete toolkit_paths rows of entity types that are no longer routable (lost frontend.path or ' +\n 'unregistered). dryRun reports them without deleting. On-demand (lumi paths:rebuild --orphans).',\n schema: prunePayloadSchema,\n })\n\n registerJob(pruneJob, async (ctx) => {\n // One progress blob across every site, so an earlier site's orphans or\n // refusal are not overwritten by the last site's result.\n const dryRun = prunePayloadSchema.parse(ctx.payload).dryRun\n const sum: PruneResult = {\n dryRun,\n orphans: [],\n found: 0,\n deleted: 0,\n truncated: false,\n refused: false,\n }\n await runAsCli(() =>\n runInEverySite(\n async () => {\n const payload = prunePayloadSchema.parse(ctx.payload)\n const pruned = await runPathsPrune(app, payload)\n for (const o of pruned.orphans) {\n const seen = sum.orphans.find((x) => x.entityType === o.entityType)\n if (seen) seen.rows += o.rows\n else sum.orphans.push({ ...o })\n }\n sum.found += pruned.found\n sum.deleted += pruned.deleted\n sum.truncated ||= pruned.truncated\n sum.refused ||= pruned.refused\n ctx.updateProgress(sum)\n app.logger.info(pruned, 'paths-prune: sweep complete')\n },\n { app, label: 'paths-prune' },\n ),\n )\n })\n\n registerJob(cascadeJob, async (ctx) => {\n // The whole body runs elevated (AdminClient reads demand a SecurityContext,\n // and a background queue worker has no request-scoped one) — same\n // `runAsCli` wrapping every other entity-touching job handler in this\n // codebase uses (see e.g. `packages/rent/src/plugin.ts`'s\n // `close_batch_periods` job). `runPathsCascade` itself assumes it is\n // already elevated and never nests `runAsCli`.\n await runAsCli(async () => {\n const payload = cascadePayloadSchema.parse(ctx.payload)\n const cascadeResult = await runPathsCascade(app, payload, {\n onProgress: (p) => ctx.updateProgress(p),\n })\n app.logger.info(cascadeResult, 'paths-cascade: descendant regeneration complete')\n })\n })\n\n registerJob(rebuildJob, async (ctx) => {\n // Once per site on an app serving several (multisite D003) — a rebuild\n // reads every routable (site-bound) row and writes that site's path index.\n await runAsCli(() =>\n runInEverySite(\n async () => {\n const payload = rebuildPayloadSchema.parse(ctx.payload)\n const rebuildResult = await runPathsRebuild(app, payload, {\n onProgress: (p) => ctx.updateProgress({ ...p, site: getCurrentSite() ?? null }),\n })\n app.logger.info(\n {\n dryRun: rebuildResult.dryRun,\n scanned: rebuildResult.scanned,\n changed: rebuildResult.changed,\n missing: rebuildResult.missing,\n conflicts: rebuildResult.conflicts,\n applied: rebuildResult.applied,\n failed: rebuildResult.failed,\n // Multi-pass convergence visibility for worker-log-only operators\n // (matches what the CLI renderer already surfaces) — Fix 5.\n passes: rebuildResult.passes,\n ...(rebuildResult.hierarchyNote !== undefined\n ? { hierarchyNote: rebuildResult.hierarchyNote }\n : {}),\n },\n 'paths-rebuild: sweep complete',\n )\n },\n { app, label: 'paths-rebuild' },\n ),\n )\n })\n\n // Task 1 seam → enqueue the cascade job on an ancestor rename.\n setActiveCascadeTrigger(async (triggerApp, entity, entityId, locale) => {\n const qc = new QueueClient({ app: triggerApp, logger: triggerApp.logger })\n await qc.enqueue(cascadeJob, { entityType: entity.name, parentId: entityId, locale })\n })\n}\n","/**\n * `toolkit_path_patterns` — the per-(entity, locale) frontend-path PATTERN HASH\n * store (plan/routing PR04 §2 change-matrix row \"Pattern changed in code\").\n *\n * One row per (entity_type, locale) holding a sha256 of the RESOLVED\n * `frontend.path` pattern the last time the app booted. The content plugin's\n * `onReady` hook (Phase B — after every plugin's `init` has settled, so\n * `app.db` is available; a pure boot-lint at `createApp` step 4d can't reach\n * the DB) recomputes each pattern's hash at startup and compares it against the\n * stored row: a mismatch logs a loud drift warning naming the entity + locale,\n * because a code-side pattern change can leave stale rows in `toolkit_paths`.\n *\n * Distinct from `toolkit_paths` (the materialized per-DOCUMENT path index):\n * this table holds no document paths, only a fingerprint of the code-declared\n * PATTERN per (entity, locale) — a tiny, bounded table (rows = entities × locales).\n *\n * Uses `defineTable` (per plan/routing C3, and CLAUDE.md's \"no raw SQL\") so the\n * `onReady` check reads/writes through a typed `TableClient`. Registered\n * declaratively via the content plugin's `server.tables` slot (NOT an imperative\n * `schemaRegistry.register` from `init`) — it's statically known at\n * plugin-construct time, so it follows the same shape as `toolkit_paths`.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const toolkitPathPatternsTable = defineTable({\n name: 'toolkit_path_patterns',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityType: column.varchar({ length: 100, notNull: true, pgName: 'entity_type' }),\n locale: column.varchar({ length: 10, notNull: true }),\n /** sha256 hex digest (64 chars) of the resolved pattern — see `computePatternHash`. */\n patternHash: column.varchar({ length: 64, notNull: true, pgName: 'pattern_hash' }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [\n // Exactly one hash per (entity, locale) — the natural key the boot check\n // reads by and upserts on. A plain unique (no partial `where`): every row\n // is a live fingerprint, there is no history here (unlike `toolkit_paths`).\n { on: ['entityType', 'locale'] },\n ],\n})\n\n/** Backwards-compatible raw pgTable export (for the plugin `tables:` slot + migrations). */\nexport const toolkitPathPatterns = toolkitPathPatternsTable.table\n\n/** The full `DefinedTable` for `toolkit_path_patterns`. */\nexport type ToolkitPathPatternsTable = typeof toolkitPathPatternsTable\n\n/** The typed `TableClient` for `toolkit_path_patterns`. */\nexport type ToolkitPathPatternsClient = ReturnType<typeof toolkitPathPatternsTable.makeClient>\n","/**\n * Boot-time frontend-path PATTERN-DRIFT detection (plan/routing PR04 §2\n * change-matrix row: \"Pattern changed in code | Detected at boot: per-entity\n * pattern hash compared against the stored hash; mismatch logs a loud warning\n * naming the entity + locale\").\n *\n * `computePatternHash` fingerprints an entity's resolved `frontend.path` pattern\n * for a locale; `warnOnPatternDrift` (run from the content plugin's `onReady`)\n * compares every (entity, locale)'s current fingerprint against the last stored\n * one in `toolkit_path_patterns` and warns on a mismatch.\n *\n * Why `onReady`, not the PR02 boot-lint: PR02's `findFrontendPathCollisions`\n * (`@murumets-ee/core`) is a PURE function over `Entity[]`, wired into\n * `createApp` step 4d — which runs BEFORE the `app` object (and its `.db`)\n * exists. This check must READ the persisted last-known hash per (entity,\n * locale), so it needs `app.db` — only available in Phase B (`onReady`), after\n * every plugin's `init` has settled.\n *\n * Boundedness (CLAUDE.md \"no unbounded fan-out\"): this runs ONCE per process, at\n * boot — never on a request / per-keystroke path — and iterates a fixed,\n * app-configured set (entities × locales), so no additional concurrency bound is\n * needed.\n */\n\nimport { createHash } from 'node:crypto'\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { type EntityFrontendPath, resolvePattern } from '@murumets-ee/entity'\nimport { getContentConfig } from '../content-config-store.js'\nimport { toolkitPathPatternsTable } from './pattern-hash-schema.js'\n\n/**\n * Compute a stable sha256-hex fingerprint of an entity's `frontend.path`\n * RESOLVED for a single `locale`, or `null` when the entity has no pattern for\n * that locale (a per-locale map missing both the locale AND the `defaultLocale`\n * entry — the same typed-BROKEN contract `resolvePattern` returns).\n *\n * Two pattern forms after resolution (`resolvePattern` collapses the map form to\n * the locale's string):\n * - **string** → hash `JSON.stringify(resolved)`. Stable across processes: a\n * fixed string always JSON-encodes to the same bytes.\n * - **function** → hash the function's SOURCE TEXT (`fn.toString()`).\n *\n * KNOWN, ACCEPTED LIMITATION (documented per the F014 precedent of accepting a\n * narrow edge case rather than over-engineering a fix): the function form\n * fingerprints only the function's literal source text. A resolver function\n * whose OUTPUT changes via captured EXTERNAL state — a module-level constant it\n * closes over, an imported lookup table, an env var — while its own source text\n * stays byte-identical will produce the SAME hash, so that drift goes\n * undetected. Detecting it would require executing the resolver against\n * representative rows at boot (a fan-out with side-effect risk), which is\n * deliberately out of scope. Static string / per-locale-map patterns — the\n * overwhelmingly common case — are fully covered.\n *\n * The string form is JSON-encoded (`\"…\"`) and the function form is raw source\n * (`(args) => …` / `function …`), so the two forms can never collide on the\n * same digest.\n */\nexport function computePatternHash(\n path: EntityFrontendPath,\n locale: string,\n defaultLocale: string,\n): string | null {\n const resolved = resolvePattern(path, locale, defaultLocale)\n if (resolved === null) return null\n const material = typeof resolved === 'function' ? resolved.toString() : JSON.stringify(resolved)\n return createHash('sha256').update(material).digest('hex')\n}\n\n/**\n * Boot-time (`onReady`) drift check. For every entity in `app.entities` that\n * declares `frontend.path`, and every configured locale, compare the current\n * resolved-pattern hash against the stored `toolkit_path_patterns` row:\n *\n * - no stored row (first boot / a newly-added entity+locale) → record the\n * current hash, no warning (this is bootstrap, not drift);\n * - stored row, hash matches → no-op;\n * - stored row, hash differs → `app.logger.warn` naming entity + locale, THEN\n * re-baseline the stored hash to the current value. The warning answers \"did\n * the pattern change since the LAST BOOT\", NOT \"until someone rebuilds\" — so\n * it is deliberately decoupled from the `paths:rebuild` job: a subsequent boot\n * with the SAME (already-warned) pattern must not re-warn.\n *\n * The record/re-baseline write is a single `upsert` (NOT a plain insert) so two\n * app instances cold-starting against the same freshly-migrated (empty) table\n * can't race to a 23505: two instances both read `null`, both attempt to write\n * the first-boot baseline, and a plain INSERT would fail the loser on the\n * `UNIQUE(entity_type, locale)` constraint — an uncaught throw that, because this\n * runs in `onReady` (Phase B), ABORTS BOOT for that instance (a rolling /\n * multi-replica first deploy). `ON CONFLICT DO UPDATE` makes it idempotent\n * regardless of which instance's write lands first.\n */\nexport async function warnOnPatternDrift(app: ToolkitApp): Promise<void> {\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const locales = config.locales.map((l) => l.code)\n const client = toolkitPathPatternsTable.makeClient(app.db.readWrite)\n\n for (const [entityType, entity] of app.entities) {\n const declaredPath = entity.frontend?.path\n if (declaredPath === undefined) continue\n\n for (const locale of locales) {\n const currentHash = computePatternHash(declaredPath, locale, defaultLocale)\n // No resolvable pattern for this locale — nothing to fingerprint / track.\n if (currentHash === null) continue\n\n const stored = await client.findOne({ entityType, locale })\n\n // Unchanged — no-op (leaves the stored row's updatedAt untouched).\n if (stored !== null && stored.patternHash === currentHash) continue\n\n // Drift (stored row, different hash) → warn BEFORE re-baselining. First\n // boot (stored === null) is bootstrap, not drift, so it never warns.\n if (stored !== null) {\n app.logger.warn(\n {\n entity: entityType,\n locale,\n previousHash: stored.patternHash,\n currentHash,\n },\n `toolkit_paths: frontend.path pattern for '${entityType}' (locale '${locale}') changed ` +\n `since last boot — stale toolkit_paths rows may exist; ` +\n `run \\`lumi paths:rebuild --entity ${entityType}\\``,\n )\n }\n\n // Record the baseline (first boot) OR re-baseline (drift) via ONE\n // idempotent upsert — safe under concurrent cold-start, unlike a plain\n // insert (see the docblock; `onReady` throws abort boot).\n await client.upsert(\n { entityType, locale, patternHash: currentHash },\n {\n target: ['entityType', 'locale'],\n set: { patternHash: currentHash, updatedAt: new Date() },\n },\n )\n }\n }\n}\n","/**\n * The published-path resolver this plugin contributes to the block editor's\n * canvas (`blocks.publishedPathResolver`, editor PR59, F143), so a collection\n * card in Preview opens the entry it shows.\n *\n * The SAME lookup the public site links its cards through\n * (`createBlockResolver`'s `resolvePublishedPaths`): the batched publish filter,\n * then canonical paths — so a canvas card links exactly where the site's does,\n * and an unpublished or trashed entry yields no link.\n *\n * Lazy by necessity. This module is imported by `plugin.ts`, which the CLI loads\n * under plain Node through jiti; `./resolve-context.js` pulls the entity query\n * runtime and must not load there — the reason `paths/routes.ts` imports it the\n * same way.\n */\n\nimport type {\n BreadcrumbTrailResolver,\n PublishedPathResolver,\n PublishedPathsAmong,\n} from '@murumets-ee/blocks/server'\nimport { getApp } from '@murumets-ee/core'\n\nexport const resolvePublishedCanonicalPaths: PublishedPathResolver = async (\n entityType,\n ids,\n locale,\n) => {\n if (ids.length === 0) return new Map()\n const [{ buildEnumerateDeps }, { getCanonicalPaths }] = await Promise.all([\n import('./resolve-context.js'),\n import('./enumerate.js'),\n ])\n const deps = buildEnumerateDeps(getApp())\n const published = await deps.filterPublished(entityType, ids, locale)\n if (published.size === 0) return new Map()\n return getCanonicalPaths(entityType, [...published], locale, deps)\n}\n\n/**\n * How many paths one lookup reads — `@murumets-ee/blocks`' `COLLECTION_MAX_LIMIT`,\n * so no row a collection shows is left unasked. A local constant because this\n * module is reached from the plugin entry the CLI loads under plain Node; the\n * test pins the two equal.\n */\nexport const MAX_PATHS_AMONG = 48\n\n/**\n * Which of `paths` are a PUBLISHED document's canonical path in `locale` —\n * contributed as `blocks.publishedPathsAmong` (block-library PR21) for a\n * collection link with `onlyPublished`: a synced room's card is shown once the\n * room's PAGE is published.\n *\n * Bounded: ONE `toolkit_paths` read (`path IN (…)`, at most\n * {@link MAX_PATHS_AMONG}) and ONE publish check per entity type found — a\n * handful at most, never one per path. The publish check is the same\n * fail-closed filter the canonical-path resolver uses: an unpublished,\n * trashed or unregistered target is not published.\n */\nexport const resolvePublishedPathsAmong: PublishedPathsAmong = async (paths, locale) => {\n if (paths.length === 0) return new Set()\n const { buildEnumerateDeps } = await import('./resolve-context.js')\n const deps = buildEnumerateDeps(getApp())\n const unique = [...new Set(paths)]\n const asked = unique.slice(0, MAX_PATHS_AMONG)\n if (unique.length > asked.length) {\n // Logged, never silent: a path past the cap is reported as unpublished, so\n // its row is hidden — say so rather than look like \"checked everything\".\n getApp().logger.warn(\n { paths: unique.length, max: MAX_PATHS_AMONG },\n 'content: publishedPathsAmong was asked for more paths than it reads; the rest count as unpublished',\n )\n }\n const rows = await deps.pathsClient.findMany({\n where: { locale, isCanonical: true, path: { in: asked } },\n limit: asked.length,\n })\n const byType = new Map<string, Map<string, string>>()\n for (const row of rows) {\n let ids = byType.get(row.entityType)\n if (ids === undefined) {\n ids = new Map()\n byType.set(row.entityType, ids)\n }\n ids.set(row.entityId, row.path)\n }\n const published = new Set<string>()\n // Serial: one publish check per entity type, a config-time-fixed handful.\n for (const [entityType, ids] of byType) {\n const live = await deps.filterPublished(entityType, [...ids.keys()], locale)\n for (const [id, path] of ids) if (live.has(id)) published.add(path)\n }\n return published\n}\n\n/**\n * The breadcrumb trail of a document, contributed as `blocks.breadcrumbTrail`\n * (block-library D003) and called by the delivered page's block resolver and by\n * the canvas — one function, so the canvas cannot show a trail the site would\n * not. Lazy for the reason the resolvers above are.\n */\nexport const resolveDocumentBreadcrumbTrail: BreadcrumbTrailResolver = async (request) => {\n const [{ buildBreadcrumbDeps }, { resolveBreadcrumbTrail }] = await Promise.all([\n import('./resolve-context.js'),\n import('./breadcrumbs.js'),\n ])\n return (await resolveBreadcrumbTrail(request, buildBreadcrumbDeps(getApp())))?.steps\n}\n","/**\n * Draft overlay table schema generator\n *\n * Generates a {entity}_drafts table for entities with the publishable() behavior.\n * Stores a mutable work-in-progress snapshot that non-publishers edit instead of\n * writing directly to the live published row.\n *\n * One draft per entity+locale. Drafts are upserted, discarded, or promoted to live.\n *\n * Uses `defineTable` so ContentClient can operate on drafts through a typed\n * TableClient (see registry.ts). The migration pipeline uses the\n * entity-package generator (which emits FK references) — this one produces a\n * runtime-only table shape for read/write via TableClient.\n */\n\nimport { column } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\nimport { defineCompanionTable } from './site-partition.js'\n\n/**\n * Generate a `DefinedTable` for a publishable entity's drafts table.\n *\n * Schema:\n * {entity}_drafts (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_',\n * data jsonb NOT NULL, -- full entity snapshot\n * created_by varchar(255) NOT NULL, -- user ID (varchar, NOT uuid!)\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * updated_at timestamptz NOT NULL DEFAULT now(),\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateDraftsTable(entity: Entity) {\n return defineCompanionTable(entity, {\n name: `${entity.name}_drafts`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n createdBy: column.varchar({ length: 255, notNull: true, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type DraftsTable = ReturnType<typeof generateDraftsTable>\n","/**\n * Per-locale publish status table schema generator\n *\n * Generates a {entity}_locale_status table for entities that have both\n * publishable() behavior AND translatable fields. This allows different\n * locales to have independent publish statuses.\n *\n * Fallback rule: no row for a locale = inherit base entity status.\n *\n * Uses `defineTable` so ContentClient can operate on rows through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references); this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\nimport { defineCompanionTable } from './site-partition.js'\n\n/**\n * Generate a `DefinedTable` for a publishable + translatable entity's\n * locale status table.\n *\n * Schema:\n * {entity}_locale_status (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL,\n * status varchar(20) NOT NULL DEFAULT 'draft',\n * published_at timestamptz,\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateLocaleStatusTable(entity: Entity) {\n return defineCompanionTable(entity, {\n name: `${entity.name}_locale_status`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true }),\n status: column.varchar({ length: 20, notNull: true, default: 'draft' }),\n publishedAt: column.timestamp({ withTimezone: true, pgName: 'published_at' }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type LocaleStatusTable = ReturnType<typeof generateLocaleStatusTable>\n","/**\n * Version history table schema generator\n *\n * Generates a {entity}_versions table for entities with the versionable() behavior.\n * Each version stores a full snapshot + delta (changed fields only).\n *\n * Uses `defineTable` so ContentClient can operate on versions through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references) — this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\nimport { defineCompanionTable } from './site-partition.js'\n\n/**\n * Generate a `DefinedTable` for a versionable entity's versions table.\n *\n * Schema:\n * {entity}_versions (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * version integer NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_', -- locale this snapshot belongs to\n * data jsonb NOT NULL, -- full snapshot (locale-merged view)\n * delta jsonb, -- only changed fields\n * status varchar(20), -- 'draft' | 'published' at time of save\n * created_by varchar(255),\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * is_autosave boolean NOT NULL DEFAULT false,\n * UNIQUE(entity_id, version, locale)\n * )\n *\n * Each locale has its own version sequence. The default locale uses '_'\n * as the locale value (distinguishable from actual locale codes).\n */\nexport function generateVersionsTable(entity: Entity) {\n return defineCompanionTable(entity, {\n name: `${entity.name}_versions`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n version: column.integer({ notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n delta: column.jsonb<Record<string, unknown>>(),\n status: column.varchar({ length: 20 }),\n createdBy: column.varchar({ length: 255, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n isAutosave: column.boolean({ notNull: true, default: false, pgName: 'is_autosave' }),\n },\n unique: [{ on: ['entityId', 'version', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type VersionsTable = ReturnType<typeof generateVersionsTable>\n","/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport { definePlugin, type TrashAutoEnablePolicy } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { setContentConfig } from './content-config-store.js'\nimport { createDocumentDraftStore } from './document-draft-store.js'\nimport { createDocumentVersionStore } from './document-version-store.js'\nimport { i18nSettings } from './i18n-settings.js'\nimport { Locale } from './locale-entity.js'\nimport { Message } from './message-entity.js'\n\n// Re-export the config accessor from the leaf store so the `@murumets-ee/content/plugin`\n// subpath API is unchanged for existing consumers (admin-context, entity pages).\nexport { getContentConfig } from './content-config-store.js'\n// Re-export the i18n registry primitives from this jiti-safe subpath (all sources are\n// leaf: locale-entity → entity only, i18n-settings → settings/define, routing-strategy →\n// types only), so the admin-ui Languages editor route can import them WITHOUT dragging\n// content's `server-only` main entry through the config-load / bundle graph.\nexport { i18nSettings, ROUTING_STRATEGIES } from './i18n-settings.js'\nexport { Locale } from './locale-entity.js'\n// The `message` config entity is leaf-safe (entity-only), so the admin-ui\n// Translations editor route can import it from this jiti-safe subpath without\n// dragging content's `server-only` main entry through the config-load graph.\nexport { Message } from './message-entity.js'\nexport {\n localePrefixForStrategy,\n prefixDefaultLocaleForStrategy,\n} from './routing-strategy.js'\nexport type { I18nConfig, LocaleDirection, LocaleEntry, RoutingStrategy } from './types.js'\n\n// The `available` predicate, from the import-free leaf module. NOT from\n// `./seed.js` — that one imports `@murumets-ee/core/clients` (`server-only`),\n// and a static import of it here would put the sentinel on the module every\n// `lumi` command loads. See the `seeders` block below.\nimport { hasChromeEntities } from './chrome-entity-names.js'\nimport { hasDetailTemplateEntity } from './detail-template-names.js'\nimport { setActiveLocaleStatusWriterFactory } from './locale-status-slot.js'\nimport { registerPathJobs } from './paths/jobs.js'\nimport { warnOnPatternDrift } from './paths/pattern-hash.js'\nimport { toolkitPathPatterns } from './paths/pattern-hash-schema.js'\nimport {\n resolveDocumentBreadcrumbTrail,\n resolvePublishedCanonicalPaths,\n resolvePublishedPathsAmong,\n} from './paths/published-path-resolver.js'\nimport { pathRoutes } from './paths/routes.js'\nimport { toolkitPaths } from './paths/schema.js'\nimport { setActivePathWritersFactory } from './paths/writers.js'\nimport { redirectRoutes } from './redirects/routes.js'\nimport { toolkitRedirects } from './redirects/schema.js'\nimport { generateDraftsTable } from './schema/drafts.js'\nimport { generateLocaleStatusTable } from './schema/locale-status.js'\nimport { toolkitContentLocks } from './schema/locks.js'\nimport { registerContentTables } from './schema/registry.js'\nimport { toolkitTranslationProvenance } from './schema/translation-provenance.js'\nimport { generateVersionsTable } from './schema/versions.js'\nimport { translationProvenanceWriteListener } from './translation-provenance.js'\nimport type { ContentConfig } from './types.js'\n\n/**\n * Content plugin factory.\n */\nexport function content(config: ContentConfig) {\n // Validate config\n if (!config.locales || config.locales.length === 0) {\n throw new Error('content() requires at least one locale')\n }\n\n if (!config.defaultLocale) {\n throw new Error('content() requires a defaultLocale')\n }\n\n const defaultExists = config.locales.some((l) => l.code === config.defaultLocale)\n if (!defaultExists) {\n throw new Error(\n `defaultLocale \"${config.defaultLocale}\" not found in locales: [${config.locales.map((l) => l.code).join(', ')}]`,\n )\n }\n\n // Reserved path prefixes must be absolute (locale-relative) — a prefix that\n // doesn't start with '/' can never match a computed path (which always leads\n // with '/'), so it would be a silent no-op reservation. Fail loud (routing\n // PR03, Phase 02 §3).\n if (config.reservedPathPrefixes) {\n for (const prefix of config.reservedPathPrefixes) {\n if (typeof prefix !== 'string' || !prefix.startsWith('/')) {\n throw new Error(\n `content(): reservedPathPrefixes entries must be absolute paths starting with '/', ` +\n `got ${JSON.stringify(prefix)}`,\n )\n }\n }\n }\n\n // plan/trash SD002 — declare the auto-enable policy for the framework to\n // apply at boot (createApp injects `trashable()` into publishable entities\n // BEFORE schema-gen). Declarative, so it can't be forgotten and carries no\n // plugin-array-order hazard. `autoTrash` defaults ON.\n const autoTrash = config.autoTrash ?? true\n const trashPolicy: TrashAutoEnablePolicy | undefined =\n autoTrash === false\n ? undefined\n : {\n publishable: true,\n ...(typeof autoTrash === 'object' && autoTrash.exclude\n ? { exclude: autoTrash.exclude }\n : {}),\n }\n\n return definePlugin({\n name: '@murumets-ee/content',\n // SD015 — contribute the draft store the block editor's op-commit route\n // writes through. blocks owns the slot; content owns the {entity}_drafts\n // tables. Stateless; resolves the per-entity drafts table at call time.\n contributes: {\n 'blocks.documentDraftStore': createDocumentDraftStore(),\n // F053 — the version-snapshot seam the detach flow's snapshot-version\n // route calls before forking a saved pattern and dispatching the op.\n 'blocks.documentVersionStore': createDocumentVersionStore(),\n // editor PR59 (F143) — published canonical paths for the canvas's\n // collection cards, so Preview opens the recipe or tip a card shows.\n // content owns `toolkit_paths`; blocks cannot import it.\n 'blocks.publishedPathResolver': resolvePublishedCanonicalPaths,\n // agent-access D017 — a write that lands on the live row without a draft\n // (a generic PATCH, a restore, a seeder) still records what that locale\n // was saved from. The draft writers record for themselves.\n 'entity.writeListeners': translationProvenanceWriteListener,\n 'blocks.publishedPathsAmong': resolvePublishedPathsAmong,\n // block-library D003 — a document's breadcrumb trail, derived from the\n // same index, for the canvas (the delivered page asks the resolver itself).\n 'blocks.breadcrumbTrail': resolveDocumentBreadcrumbTrail,\n // multisite F034 — the base language the site registry's write guard\n // holds every site's language list to. core owns the registry and\n // cannot know the language registry; content does. Loaded on demand:\n // `locale-registry` reads through the query client, and a static import\n // would put that on the module every `lumi` command loads.\n 'sites.baseLocale': async () =>\n (await import('./locale-registry.js')).resolveInstallBaseLocale(),\n // SD002 — only contribute when auto-trash is enabled (omitted → no policy\n // → createApp's applyTrashAutoEnable is a no-op for content).\n ...(trashPolicy ? { 'trash.autoEnable': trashPolicy } : {}),\n // routing PR03 — surface reserved path prefixes to createApp's boot-time\n // lint (gathered BEFORE init, so available at the frontend-path lint step\n // even though config is stored in init). Omitted when none configured.\n ...(config.reservedPathPrefixes\n ? { 'routing.reservedPathPrefixes': config.reservedPathPrefixes }\n : {}),\n },\n // The `i18n` singletons (defaultLocale, routingStrategy) — a simple-config\n // namespace that auto-registers as a config type (D006/D009/SD006) and rides\n // export/import for free. Edited via the Languages editor (`hideFromMenu`).\n shared: {\n settings: [i18nSettings],\n // Manual-redirect CRUD permission (routing PR05, phase 04 §4). ONE\n // permission — `redirect:manage` — gates the whole `/api/admin/redirects/*`\n // surface (no per-verb split). `defineAdminRoute` already contributes it to\n // the catalog when `redirectRoutes()` is evaluated; declaring it here as a\n // feature is the stable, introspection-friendly source of the description\n // (CLAUDE.md `Plugin.shared.features` guidance). Repeat registration of the\n // same permission is a safe union-merge in `registerPermission`, not a throw.\n features: [\n {\n resource: 'redirect',\n actions: ['manage'],\n defaultRoles: ['admin'],\n description: 'Create, edit, and delete manual URL redirects (toolkit_redirects).',\n },\n // Reverse path lookup (#775) — `GET /api/admin/paths/resolve`. Editors\n // hold it because the only consumer is \"I clicked a link in the preview\n // canvas, open that page's editor\"; viewers have no editor to open. The\n // resolved document is ALSO checked against the caller's `view` grant on\n // that entity, so this permission alone discloses nothing.\n {\n resource: 'path',\n actions: ['resolve'],\n defaultRoles: ['admin', 'editor'],\n description: 'Resolve a site URL path to the CMS document that serves it.',\n },\n ],\n },\n server: {\n // `Locale` — the content-locale registry config entity (D009/SD006). `Message` —\n // the interface-message store config entity (D010). Declared statically so the\n // framework registers their tables at merge (before init) and auto-derives them\n // into the ConfigTypeRegistry.\n entities: [Locale, Message],\n // The shared locks + paths + path-pattern-hash tables are registered\n // declaratively via the framework's merge step (#469). The per-entity\n // tables below (versions / layout / drafts / locale_status) are generated\n // dynamically from `app.entities` and stay imperatively\n // registered — they're not known at plugin-construct time.\n tables: {\n toolkitContentLocks,\n toolkitPaths,\n toolkitPathPatterns,\n toolkitRedirects,\n // What each locale of a document was last saved from (agent-access D017).\n toolkitTranslationProvenance,\n },\n // Manual-redirect admin CRUD (routing PR05) + reverse path resolution\n // (#775). `defineAdminRoute` entries are built here at plugin-construct\n // time — no DB / server-only touch (handlers resolve the app lazily at\n // request time). See ./redirects/routes.ts + ./paths/routes.ts.\n routes: [...redirectRoutes(), ...pathRoutes()],\n // The site-chrome seeder, contributed rather than re-authored per app.\n //\n // `available` is the point of this entry, and the reason F009 deferred\n // it. The chrome entity is a FACTORY (`siteChromeEntity()`) an app may\n // or may not instantiate — `withOpLog()` has to come\n // from the app, because it ships from `@murumets-ee/blocks/server` and\n // this package must not depend on blocks. So there is no entity here to\n // put on `server.entities`, and contributing the seeder unconditionally\n // would give a project that registered no chrome a button whose entire\n // outcome is `created: 0` — which reads as \"broken\", not as \"this site\n // has no header\". The predicate makes it ABSENT instead, the same\n // contract `rent:cars-demo` has.\n //\n // The predicate and the seeder ask about the same NAME\n // (`CHROME_ENTITY_NAME`), imported rather than re-spelled: two spellings\n // would drift, and the drift is invisible — a button that appears and\n // creates nothing, or none on a site that has chrome.\n //\n // `hasChromeEntities` is imported STATICALLY, unlike `seedSiteChrome`.\n // It has to be: `available` is called at LIST time, on the seed page's\n // synchronous shell and inside `lumi seed --list`, so it cannot be\n // behind an `await import`. That is only safe because it lives in\n // `./chrome-entity-names.ts`, which imports NOTHING — importing it from\n // `./seed.js` would have hoisted that module's\n // `@murumets-ee/core/clients` into `dist/plugin.mjs` and thrown under\n // jiti on the next `lumi migrate`. The RUN path stays lazy per D004.\n seeders: [\n {\n name: 'content:site-chrome',\n description:\n \"Create the empty site_chrome row for each placement (header, footer), as a DRAFT, so they can be edited on the page canvas. Publishing one is what makes /v1/_chrome serve it instead of the delivery template's own fallback. Idempotent — an existing row for a placement is left alone.\",\n available: hasChromeEntities,\n // 🔴 THE ANSWER TO S5. `lumi migrate` runs every bootstrap seeder\n // after it applies migrations, and `pnpm db:migrate` is already step\n // 4 of the printed next-steps — so a scaffolded project's header is\n // editable on first boot with no seeder anyone invokes, no SQL and\n // no manual step.\n //\n // It is declarative for the usual reason: the alternative shapes all\n // put the trigger somewhere a plugin cannot reach. An install-time\n // data migration never ships (the scaffold skips `migrations/` and\n // regenerates from the runtime entities), a create-on-first-open\n // writes during a render, and hanging it off the app's own boot code\n // makes every app re-wire what the plugin already knows.\n //\n // Safe to run on every migrate ONLY because the run is idempotent —\n // see `seedSiteChrome`, which finds per placement before creating.\n bootstrap: true,\n run: async () => {\n const { seedSiteChrome } = await import('@murumets-ee/content/seed')\n return await seedSiteChrome()\n },\n },\n {\n name: 'content:detail-templates',\n description:\n 'Create the DRAFT detail template for each entity drawn by one (block-library D009), so its page layout can be edited on the canvas. Until a template is published its rows keep the plain fallback page. Idempotent — an existing template is left alone.',\n // Absent when the app registered no `detail_template` entity, for the\n // reason `content:site-chrome` gives. Statically imported from a\n // module that imports nothing — same constraint, same file shape.\n available: hasDetailTemplateEntity,\n // Bootstrapped on every `lumi migrate`, like chrome: the entity sets\n // `disableCreate`, so this is the only way a template row comes to\n // exist, and it must not depend on anyone pressing a button.\n bootstrap: true,\n run: async () => {\n const { seedDetailTemplates } = await import('@murumets-ee/content/seed')\n return await seedDetailTemplates()\n },\n },\n ],\n init: async (app) => {\n // Store config globally (leaf store — no schema imports)\n setContentConfig(config)\n\n let versionableCount = 0\n let localeStatusCount = 0\n let draftsCount = 0\n\n for (const [name, entity] of app.entities) {\n // Register version tables for versionable entities\n const hasVersionable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'versionable',\n )\n if (hasVersionable) {\n const versionsTableName = `${name}_versions`\n if (!schemaRegistry.has(versionsTableName)) {\n const versions = generateVersionsTable(entity)\n schemaRegistry.register(versionsTableName, versions.table)\n registerContentTables(name, { versions })\n }\n versionableCount++\n }\n\n // Register locale_status tables for publishable + translatable entities\n const isPublishable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'publishable',\n )\n const allFields = entity.allFields as Record<string, { translatable?: boolean }>\n const hasTranslatable = Object.values(allFields).some((f) => f.translatable)\n if (isPublishable && hasTranslatable) {\n const statusTableName = `${name}_locale_status`\n if (!schemaRegistry.has(statusTableName)) {\n const localeStatus = generateLocaleStatusTable(entity)\n schemaRegistry.register(statusTableName, localeStatus.table)\n registerContentTables(name, { localeStatus })\n }\n localeStatusCount++\n }\n\n // Register drafts table for publishable entities\n if (isPublishable) {\n const draftsTableName = `${name}_drafts`\n if (!schemaRegistry.has(draftsTableName)) {\n const drafts = generateDraftsTable(entity)\n schemaRegistry.register(draftsTableName, drafts.table)\n registerContentTables(name, { drafts })\n }\n draftsCount++\n }\n }\n\n // Expose the per-locale status writer to AdminClient.updateForLocale\n // (SD021). Must run AFTER the locale_status registration loop above —\n // the factory consults that registry per entity. This is capability\n // injection via a globalThis slot (queue's enqueue-on-commit\n // precedent), not contribution registration — no merge-step involved.\n setActiveLocaleStatusWriterFactory(config.locales.map((l) => l.code))\n\n // Expose the path-index writer + conflict guard to core's\n // createAdminClient for routable entities (routing PR03, F008/F010).\n // Same globalThis-slot capability injection; the shared `toolkit_paths`\n // table is registered declaratively (the `tables:` slot), so this needs\n // no per-entity registration and can run any time in init.\n setActivePathWritersFactory()\n\n // routing PR04 Task 2 — register the descendant-cascade + full-rebuild\n // queue jobs and wire the Task 1 cascade-trigger seam to enqueue the\n // cascade job. `@murumets-ee/queue/client` is DYNAMIC-imported inside\n // registerPathJobs (keeps queue a peer off content's static graph — the\n // blocks-jobs precedent). Wrapped so a queue-load failure degrades to\n // \"no cascade wired\" (Task 1's queueless no-op — the seam simply stays\n // unregistered) instead of failing content init or entity path writes.\n // Runs AFTER setActivePathWritersFactory so the writer seam is always\n // wired even if job registration fails.\n try {\n await registerPathJobs(app)\n } catch (err) {\n app.logger.warn(\n { err },\n 'content: failed to register routing path jobs — descendant cascade + ' +\n '`lumi paths:rebuild` will be unavailable (path writes still succeed)',\n )\n }\n\n app.logger.info(\n {\n locales: config.locales.map((l) => l.code),\n defaultLocale: config.defaultLocale,\n versionableEntities: versionableCount,\n localeStatusEntities: localeStatusCount,\n draftEntities: draftsCount,\n },\n 'Content plugin initialized',\n )\n },\n // Phase B (after every plugin's `init` has settled, so `app.db` is\n // available — a `createApp` step-4d pure boot-lint can't reach the DB).\n // Boot-time frontend-path pattern-drift check (routing PR04 §2): compares\n // each routable entity's resolved-pattern hash against the last stored\n // `toolkit_path_patterns` row and warns on drift. Runs ONCE per process at\n // startup — bounded by the app's own (entities × locales), never a\n // request path — so no fan-out concurrency bound is needed.\n //\n // Wrapped in try/catch: `warnOnPatternDrift` does a DB round-trip per\n // (entity × locale), and per core's plugin contract ANY throw from\n // `onReady` ABORTS app boot. This is a boot-time DIAGNOSTIC whose stated\n // intent is \"log a warning, never abort\" — so a transient DB hiccup during\n // it must degrade to a logged error, not crash the app (same posture as the\n // `registerPathJobs` wrap in `init` above).\n onReady: async (app) => {\n try {\n await warnOnPatternDrift(app)\n } catch (err) {\n app.logger.error(\n { err },\n 'content: boot-time frontend-path pattern-drift check failed — continuing app boot ' +\n 'without it. Inspect the logged `err` for the cause: a transient DB error can be ' +\n 'retried (and `lumi paths:rebuild` re-derives the index if a pattern actually ' +\n 'changed), whereas a structural cause — e.g. a missing migration leaving ' +\n '`toolkit_path_patterns` absent — will recur on every boot and needs `lumi migrate`',\n )\n }\n },\n },\n })\n}\n"],"mappings":"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"}
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as LocaleDirection, l as RoutingStrategy, o as LocaleEntry, r as I18nConfig, t as ContentConfig } from "./types-
|
|
2
|
-
import { a as ROUTING_STRATEGIES, i as Locale, n as prefixDefaultLocaleForStrategy, o as i18nSettings, r as Message, t as localePrefixForStrategy } from "./routing-strategy-
|
|
1
|
+
import { a as LocaleDirection, l as RoutingStrategy, o as LocaleEntry, r as I18nConfig, t as ContentConfig } from "./types-wMTglM4j.mjs";
|
|
2
|
+
import { a as ROUTING_STRATEGIES, i as Locale, n as prefixDefaultLocaleForStrategy, o as i18nSettings, r as Message, t as localePrefixForStrategy } from "./routing-strategy-DVWB7qPp.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/content-config-store.d.ts
|
|
5
5
|
/**
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/content-config-store.ts","../src/plugin.ts"],"mappings":";;;;;;;iBA0CgB,gBAAA,CAAA,GAAoB,aAAa;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/content-config-store.ts","../src/plugin.ts"],"mappings":";;;;;;;iBA0CgB,gBAAA,CAAA,GAAoB,aAAa;;;;;;iBCqCjC,OAAA,CAAQ,MAAA,EAAQ,aAAA,GAAa,IAAA,6BAAA,MAAA;EAAA"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,r as t,t as n}from"./locale-entity-CoSq3C77.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{n as i,r as a,t as o}from"./routing-strategy-D6B4ngT9.mjs";import{t as s}from"./plugin-
|
|
1
|
+
import{n as e,r as t,t as n}from"./locale-entity-CoSq3C77.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{n as i,r as a,t as o}from"./routing-strategy-D6B4ngT9.mjs";import{t as s}from"./plugin-CvPWz399.mjs";export{n as Locale,a as Message,e as ROUTING_STRATEGIES,s as content,r as getContentConfig,t as i18nSettings,o as localePrefixForStrategy,i as prefixDefaultLocaleForStrategy};
|
package/dist/resolve.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a as e,t}from"./locale-registry-COsJrVZl.mjs";import{c as n}from"./schema-BYrYzsmO.mjs";import{a as r,c as i,d as a,i as o,o as s,r as c,s as l}from"./schema-LlK6EkOJ.mjs";import{a as u,c as d,d as f,f as p,i as m,l as h,n as g,o as _,r as v,s as y}from"./resolve-context-DklN53BH.mjs";import{r as b,t as x}from"./enumerate-CYHJKQz6.mjs";import{readSiteRecord as S}from"@murumets-ee/core";const C=4e4,w=`sitemap.xml`;async function*T(e,t,n={}){let r=I(e.maxUrlsPerFile,C),i=I(n.batchSize,200),a=N(e.baseUrl),o=[];for(let s of e.locales){let c=[],l=null,u=0,d=()=>{if(c.length===0)return null;let e=F(s,u),t=(l??new Date).toISOString(),n={kind:`urlset`,filename:e,xml:O(c)};return o.push({filename:e,lastmod:t}),c=[],l=null,u+=1,n},f=x({locale:s},t.enumerate,n.enumerate);for await(let n of L(f,i)){let i=await D(n,s,t);if(i.length===0)continue;let o=await E(i,s,e,t);for(let t of i){let n=o.get(t.id)??[{locale:s,path:t.path}];if(c.push(k(t,s,n,e,a)),(l===null||t.updatedAt>l)&&(l=t.updatedAt),c.length>=r){let e=d();e&&(yield e)}}}let p=d();p&&(yield p)}o.length>1&&(yield{kind:`index`,filename:w,xml:A(o,a)})}async function E(e,t,n,r){let i=new Map;for(let n of e)i.set(n.id,[{locale:t,path:n.path}]);let a=n.locales.filter(e=>e!==t);if(a.length===0)return i;let o=new Map;for(let t of e){let e=o.get(t.entityType);e?e.push(t.id):o.set(t.entityType,[t.id])}for(let e of a)for(let[t,n]of o){let a=await b(t,n,e,r.canonicalBatch);if(a.size===0)continue;let o=[...a.keys()],s=await r.enumerate.filterPublished(t,o,e),c=r.filterNoIndex===void 0?void 0:await r.filterNoIndex(t,o,e);for(let[t,n]of a){if(!s.has(t)||c?.has(t))continue;let r=i.get(t);r&&r.push({locale:e,path:n})}}return i}async function D(e,t,r){let i=r.filterNoIndex;if(i===void 0)return[...e];let a=new Map;for(let t of e){let e=a.get(t.entityType);e?e.push(t.id):a.set(t.entityType,[t.id])}let o=new Map;return await n([...a.entries()],5,async([e,n])=>{o.set(e,await i(e,n,t))}),e.filter(e=>!o.get(e.entityType)?.has(e.id))}function O(e){return`<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${e.join(`
|
|
3
3
|
`)}\n</urlset>\n`}function k(e,t,n,r,i){let a=M(i,r.prefix,t,e.path),o=n.map(e=>` ${j(e.locale,M(i,r.prefix,e.locale,e.path))}`),s=n.find(e=>e.locale===r.defaultLocale);return s&&o.push(` ${j(`x-default`,M(i,r.prefix,s.locale,s.path))}`),` <url>
|
|
4
4
|
<loc>${P(a)}</loc>\n <lastmod>${e.updatedAt.toISOString()}</lastmod>\n${o.join(`
|