@agent-native/core 0.101.6 → 0.101.10
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +255 -12
- package/corpus/core/src/agent/thread-data-builder.ts +84 -11
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
- package/corpus/core/src/integrations/adapters/discord.ts +3 -1
- package/corpus/core/src/integrations/adapters/email.ts +4 -1
- package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
- package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
- package/corpus/core/src/integrations/adapters/slack.ts +38 -9
- package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
- package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
- package/corpus/core/src/integrations/index.ts +1 -0
- package/corpus/core/src/integrations/types.ts +9 -2
- package/corpus/core/src/integrations/webhook-handler.ts +69 -45
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
- package/corpus/templates/content/AGENTS.md +32 -9
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
- package/corpus/templates/content/actions/_database-utils.ts +104 -13
- package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/_property-utils.ts +7 -1
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/configure-document-property.ts +11 -0
- package/corpus/templates/content/actions/create-content-database.ts +11 -0
- package/corpus/templates/content/actions/create-document.ts +9 -0
- package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/export-content-source.ts +1 -0
- package/corpus/templates/content/actions/get-content-database.ts +13 -1
- package/corpus/templates/content/actions/get-document.ts +12 -2
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/import-content-source.ts +6 -0
- package/corpus/templates/content/actions/list-documents.ts +3 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/pull-document.ts +2 -0
- package/corpus/templates/content/actions/search-documents.ts +5 -1
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-document.ts +16 -1
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/actions/view-screen.ts +15 -2
- package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
- package/corpus/templates/content/app/i18n-data.ts +339 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +51 -0
- package/corpus/templates/content/server/lib/document-context.ts +76 -0
- package/corpus/templates/content/server/lib/public-documents.ts +3 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +59 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
- package/corpus/templates/content/shared/api.ts +87 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/content-source.ts +7 -0
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/content/shared/properties.ts +2 -0
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/a2a/artifact-response.d.ts +15 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +204 -5
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +6 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +71 -8
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +72 -6
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/adapters/discord.d.ts.map +1 -1
- package/dist/integrations/adapters/discord.js +1 -0
- package/dist/integrations/adapters/discord.js.map +1 -1
- package/dist/integrations/adapters/email.d.ts.map +1 -1
- package/dist/integrations/adapters/email.js +2 -0
- package/dist/integrations/adapters/email.js.map +1 -1
- package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
- package/dist/integrations/adapters/google-docs.js +2 -0
- package/dist/integrations/adapters/google-docs.js.map +1 -1
- package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
- package/dist/integrations/adapters/microsoft-teams.js +1 -0
- package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +21 -6
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/adapters/telegram.d.ts.map +1 -1
- package/dist/integrations/adapters/telegram.js +11 -2
- package/dist/integrations/adapters/telegram.js.map +1 -1
- package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
- package/dist/integrations/adapters/whatsapp.js +3 -0
- package/dist/integrations/adapters/whatsapp.js.map +1 -1
- package/dist/integrations/index.d.ts +1 -1
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/types.d.ts +8 -2
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +40 -43
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +1 -0
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
2
3
|
import { getDialect, type Dialect } from "@agent-native/core/db";
|
|
3
4
|
import { and, asc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
4
5
|
|
|
@@ -38,6 +39,7 @@ import type {
|
|
|
38
39
|
import {
|
|
39
40
|
builderBlocksHash,
|
|
40
41
|
builderBlocksToReadableMarkdown,
|
|
42
|
+
builderEntryBlocks,
|
|
41
43
|
builderEntryToReadableMdxBundle,
|
|
42
44
|
builderEntryToMdxBundle,
|
|
43
45
|
builderReadableBodyToBuilderBlocks,
|
|
@@ -49,6 +51,7 @@ import {
|
|
|
49
51
|
parsePropertyOptions,
|
|
50
52
|
serializePropertyOptions,
|
|
51
53
|
serializePropertyValue,
|
|
54
|
+
type DocumentPropertyOption,
|
|
52
55
|
type DocumentPropertyOptionColor,
|
|
53
56
|
} from "../shared/properties.js";
|
|
54
57
|
import { sanitizeNormalizationFormula } from "../shared/properties.js";
|
|
@@ -63,6 +66,7 @@ import {
|
|
|
63
66
|
import {
|
|
64
67
|
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
65
68
|
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
69
|
+
BUILDER_CMS_BODY_LAST_UPDATED_KEY,
|
|
66
70
|
BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
|
|
67
71
|
BUILDER_CMS_BODY_READABLE_MAP_KEY,
|
|
68
72
|
BUILDER_CMS_BODY_SIDECARS_KEY,
|
|
@@ -72,6 +76,7 @@ import {
|
|
|
72
76
|
builderCmsSourceFieldKey,
|
|
73
77
|
builderCmsSourceMetadata,
|
|
74
78
|
builderCmsSourceRowIdentity,
|
|
79
|
+
builderCmsSourceRowIdentityState,
|
|
75
80
|
type BuilderCmsSourceEntry,
|
|
76
81
|
type ExistingBuilderSourceRowIdentity,
|
|
77
82
|
} from "./_builder-cms-source-adapter.js";
|
|
@@ -102,6 +107,16 @@ type ContentDatabaseSourceChangeReviewRowDb =
|
|
|
102
107
|
type ContentDatabaseSourceExecutionRowDb =
|
|
103
108
|
typeof schema.contentDatabaseSourceExecutions.$inferSelect;
|
|
104
109
|
|
|
110
|
+
function stableBuilderImportId(
|
|
111
|
+
prefix: "builder-doc" | "builder-item" | "builder-row",
|
|
112
|
+
parts: readonly string[],
|
|
113
|
+
) {
|
|
114
|
+
return `${prefix}_${createHash("sha256")
|
|
115
|
+
.update(parts.join("\0"))
|
|
116
|
+
.digest("base64url")
|
|
117
|
+
.slice(0, 24)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
const DEFAULT_SOURCE_CAPABILITIES: ContentDatabaseSourceCapabilities = {
|
|
106
121
|
canRefresh: true,
|
|
107
122
|
canCreateChangeSets: true,
|
|
@@ -318,7 +333,7 @@ export function serializeSourceField(
|
|
|
318
333
|
};
|
|
319
334
|
}
|
|
320
335
|
|
|
321
|
-
function serializeSourceRowRecord(
|
|
336
|
+
export function serializeSourceRowRecord(
|
|
322
337
|
row: ContentDatabaseSourceRecordRowDb,
|
|
323
338
|
options: { includeHeavyBuilderBodyValues?: boolean } = {},
|
|
324
339
|
): ContentDatabaseSourceRow {
|
|
@@ -392,6 +407,37 @@ function sourceSnapshotRowSelection(args: {
|
|
|
392
407
|
};
|
|
393
408
|
}
|
|
394
409
|
|
|
410
|
+
// Snapshot reads need document titles for change-set summaries. Body text is
|
|
411
|
+
// required only by write/review paths that explicitly request heavy Builder
|
|
412
|
+
// body values; routine list and source reads must not transfer it.
|
|
413
|
+
export function sourceSnapshotDocumentSelection(
|
|
414
|
+
includeHeavyBuilderBodyValues: true,
|
|
415
|
+
): {
|
|
416
|
+
id: typeof schema.documents.id;
|
|
417
|
+
title: typeof schema.documents.title;
|
|
418
|
+
content: typeof schema.documents.content;
|
|
419
|
+
};
|
|
420
|
+
export function sourceSnapshotDocumentSelection(
|
|
421
|
+
includeHeavyBuilderBodyValues: false,
|
|
422
|
+
): {
|
|
423
|
+
id: typeof schema.documents.id;
|
|
424
|
+
title: typeof schema.documents.title;
|
|
425
|
+
};
|
|
426
|
+
export function sourceSnapshotDocumentSelection(
|
|
427
|
+
includeHeavyBuilderBodyValues: boolean,
|
|
428
|
+
) {
|
|
429
|
+
return includeHeavyBuilderBodyValues
|
|
430
|
+
? {
|
|
431
|
+
id: schema.documents.id,
|
|
432
|
+
title: schema.documents.title,
|
|
433
|
+
content: schema.documents.content,
|
|
434
|
+
}
|
|
435
|
+
: {
|
|
436
|
+
id: schema.documents.id,
|
|
437
|
+
title: schema.documents.title,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
395
441
|
export function sourceValuesForSnapshot(
|
|
396
442
|
sourceValues: Record<string, DocumentPropertyValue>,
|
|
397
443
|
options: { includeHeavyBuilderBodyValues?: boolean } = {},
|
|
@@ -467,6 +513,47 @@ function serializeExecution(
|
|
|
467
513
|
};
|
|
468
514
|
}
|
|
469
515
|
|
|
516
|
+
/**
|
|
517
|
+
* A blocked execution with neither an attempt token nor a response proves it
|
|
518
|
+
* stopped before the Builder dispatch claim. This includes a blocked dry run
|
|
519
|
+
* and a validated plan stopped by the final live preflight. Any other state or
|
|
520
|
+
* remote evidence stays frozen until normal reconciliation resolves it.
|
|
521
|
+
*/
|
|
522
|
+
export function builderExecutionIsProvablyLocallyBlockedUnsent(execution: {
|
|
523
|
+
state: string;
|
|
524
|
+
payloadJson: string;
|
|
525
|
+
attemptToken?: string | null;
|
|
526
|
+
}) {
|
|
527
|
+
const payload = parseObject<Record<string, unknown>>(execution.payloadJson);
|
|
528
|
+
return (
|
|
529
|
+
execution.state === "blocked" &&
|
|
530
|
+
!execution.attemptToken &&
|
|
531
|
+
!(payload?.response && typeof payload.response === "object")
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function canRefreshLocallyBlockedBuilderReview(
|
|
536
|
+
executions: Array<{
|
|
537
|
+
state: string;
|
|
538
|
+
payloadJson: string;
|
|
539
|
+
attemptToken?: string | null;
|
|
540
|
+
}>,
|
|
541
|
+
) {
|
|
542
|
+
return (
|
|
543
|
+
executions.length === 0 ||
|
|
544
|
+
executions.every(builderExecutionIsProvablyLocallyBlockedUnsent)
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export const CANCELLED_BUILDER_EXECUTION_SUMMARY =
|
|
549
|
+
"Cancelled before Builder dispatch.";
|
|
550
|
+
export const CANCELLED_BUILDER_REVIEW_NOTE_PREFIX =
|
|
551
|
+
"Cancelled prepared Builder update before dispatch";
|
|
552
|
+
|
|
553
|
+
export function builderReferenceIdSourceValueKey(sourceFieldKey: string) {
|
|
554
|
+
return `__agent_native_builder_reference_id:${sourceFieldKey}`;
|
|
555
|
+
}
|
|
556
|
+
|
|
470
557
|
function riskRank(level: ContentDatabaseSourceRiskLevel) {
|
|
471
558
|
if (level === "high") return 3;
|
|
472
559
|
if (level === "medium") return 2;
|
|
@@ -608,7 +695,8 @@ const BUILDER_BODY_HYDRATION_OPEN_PRIORITY = 0;
|
|
|
608
695
|
const BUILDER_BODY_HYDRATION_BATCH_LIMIT = 25;
|
|
609
696
|
const BUILDER_BODY_HYDRATION_PROCESS_CONCURRENCY = 6;
|
|
610
697
|
const BUILDER_BODY_HYDRATION_MAX_ATTEMPTS = 5;
|
|
611
|
-
const BUILDER_BODY_HYDRATION_CODEC_VERSION =
|
|
698
|
+
const BUILDER_BODY_HYDRATION_CODEC_VERSION =
|
|
699
|
+
"readable-native-images-authoritative-raw-baseline-v9";
|
|
612
700
|
const BUILDER_CMS_REFRESH_INITIAL_PAGES = 1;
|
|
613
701
|
const BUILDER_BODY_NOT_AVAILABLE_ERROR = "body not yet available from Builder";
|
|
614
702
|
|
|
@@ -685,12 +773,27 @@ async function builderBodySnapshotForEntry(entry: BuilderCmsSourceEntry) {
|
|
|
685
773
|
return {
|
|
686
774
|
content: readableBundle.mdx.body,
|
|
687
775
|
losslessContent: losslessBundle.mdx.body,
|
|
688
|
-
blocksHash:
|
|
776
|
+
blocksHash: builderAuthoritativeRawBodyHash({
|
|
777
|
+
entry,
|
|
778
|
+
generatedBlocks: losslessBundle.blocks,
|
|
779
|
+
}),
|
|
689
780
|
readableMapJson: null,
|
|
690
781
|
sidecarsJson: JSON.stringify(sidecars),
|
|
691
782
|
};
|
|
692
783
|
}
|
|
693
784
|
|
|
785
|
+
export function builderAuthoritativeRawBodyHash(args: {
|
|
786
|
+
entry: BuilderCmsSourceEntry;
|
|
787
|
+
generatedBlocks: unknown[];
|
|
788
|
+
}) {
|
|
789
|
+
const rawBlocks = args.entry.rawEntry
|
|
790
|
+
? builderEntryBlocks(args.entry.rawEntry)
|
|
791
|
+
: [];
|
|
792
|
+
return builderBlocksHash(
|
|
793
|
+
rawBlocks.length > 0 ? rawBlocks : args.generatedBlocks,
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
694
797
|
export function builderBodyHydrationVersion(entry: BuilderCmsSourceEntry) {
|
|
695
798
|
const hash = stringSourceValue(
|
|
696
799
|
entry.sourceValues,
|
|
@@ -699,6 +802,15 @@ export function builderBodyHydrationVersion(entry: BuilderCmsSourceEntry) {
|
|
|
699
802
|
return `${hash ?? entry.updatedAt ?? entry.id}:${BUILDER_BODY_HYDRATION_CODEC_VERSION}`;
|
|
700
803
|
}
|
|
701
804
|
|
|
805
|
+
export function builderBodyUnavailableVersion(entry: BuilderCmsSourceEntry) {
|
|
806
|
+
const lastUpdated =
|
|
807
|
+
stringSourceValue(entry.sourceValues, BUILDER_CMS_BODY_LAST_UPDATED_KEY) ??
|
|
808
|
+
stringSourceValue(entry.sourceValues, "lastUpdated") ??
|
|
809
|
+
entry.updatedAt ??
|
|
810
|
+
entry.id;
|
|
811
|
+
return `${lastUpdated}:${BUILDER_BODY_HYDRATION_CODEC_VERSION}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
702
814
|
function normalizeHydrationLimit(limit: number | null | undefined) {
|
|
703
815
|
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
704
816
|
return BUILDER_BODY_HYDRATION_BATCH_LIMIT;
|
|
@@ -799,9 +911,14 @@ function builderStoredBodyIsStale(args: {
|
|
|
799
911
|
>;
|
|
800
912
|
entry: BuilderCmsSourceEntry;
|
|
801
913
|
}) {
|
|
914
|
+
const expectedVersion =
|
|
915
|
+
args.item.bodyHydrationStatus === "unavailable"
|
|
916
|
+
? builderBodyUnavailableVersion(args.entry)
|
|
917
|
+
: builderBodyHydrationVersion(args.entry);
|
|
802
918
|
return (
|
|
803
|
-
args.item.bodyHydrationStatus !== "hydrated"
|
|
804
|
-
|
|
919
|
+
(args.item.bodyHydrationStatus !== "hydrated" &&
|
|
920
|
+
args.item.bodyHydrationStatus !== "unavailable") ||
|
|
921
|
+
args.item.bodyHydrationVersion !== expectedVersion
|
|
805
922
|
);
|
|
806
923
|
}
|
|
807
924
|
|
|
@@ -812,7 +929,7 @@ function builderEntryHasBodyContent(entry: BuilderCmsSourceEntry | null) {
|
|
|
812
929
|
)?.trim();
|
|
813
930
|
}
|
|
814
931
|
|
|
815
|
-
async function
|
|
932
|
+
async function builderBodySnapshotFromStoredLossless(args: {
|
|
816
933
|
losslessContent: string | null;
|
|
817
934
|
sidecarsJson: string | null;
|
|
818
935
|
}) {
|
|
@@ -824,12 +941,60 @@ async function readableBuilderBodyFromStoredLossless(args: {
|
|
|
824
941
|
sidecars,
|
|
825
942
|
);
|
|
826
943
|
if (blocks.length === 0) return null;
|
|
827
|
-
return
|
|
944
|
+
return {
|
|
945
|
+
content: await builderBlocksToReadableMarkdown(blocks),
|
|
946
|
+
blocksHash: builderBlocksHash(blocks),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export function builderBodyHydrationNeedsLiveBaseline(args: {
|
|
951
|
+
bodyHydrationVersion: string | null | undefined;
|
|
952
|
+
storedBlocksHash: string | null | undefined;
|
|
953
|
+
rebuiltBlocksHash: string | null | undefined;
|
|
954
|
+
}) {
|
|
955
|
+
if (!args.storedBlocksHash) return false;
|
|
956
|
+
if (
|
|
957
|
+
args.bodyHydrationVersion &&
|
|
958
|
+
!args.bodyHydrationVersion.endsWith(
|
|
959
|
+
`:${BUILDER_BODY_HYDRATION_CODEC_VERSION}`,
|
|
960
|
+
)
|
|
961
|
+
) {
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
return Boolean(
|
|
965
|
+
!args.rebuiltBlocksHash || args.storedBlocksHash !== args.rebuiltBlocksHash,
|
|
966
|
+
);
|
|
828
967
|
}
|
|
829
968
|
|
|
830
|
-
|
|
969
|
+
export function builderBodyHydrationIsCodecMigration(
|
|
970
|
+
bodyHydrationVersion: string | null | undefined,
|
|
971
|
+
) {
|
|
972
|
+
return Boolean(
|
|
973
|
+
bodyHydrationVersion &&
|
|
974
|
+
!bodyHydrationVersion.endsWith(`:${BUILDER_BODY_HYDRATION_CODEC_VERSION}`),
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export function builderBodyHydrationCanAdoptSameVersionVariant(args: {
|
|
979
|
+
documentContent: string | null | undefined;
|
|
980
|
+
persistedContent: string | null | undefined;
|
|
981
|
+
}) {
|
|
982
|
+
return (
|
|
983
|
+
args.documentContent !== undefined &&
|
|
984
|
+
normalizeBuilderBodyBaselineContent(args.documentContent) ===
|
|
985
|
+
normalizeBuilderBodyBaselineContent(args.persistedContent)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export async function refreshBuilderBodySourceValuesFromStoredLossless(
|
|
831
990
|
entry: BuilderCmsSourceEntry,
|
|
832
991
|
) {
|
|
992
|
+
if (entry.rawEntry && builderEntryBlocks(entry.rawEntry).length > 0) {
|
|
993
|
+
// A fresh single-entry Builder read is the authoritative preflight
|
|
994
|
+
// representation. Never replace its raw block hash with a hash rebuilt
|
|
995
|
+
// from generated lossless MDX.
|
|
996
|
+
return entry;
|
|
997
|
+
}
|
|
833
998
|
const losslessContent = stringSourceValue(
|
|
834
999
|
entry.sourceValues,
|
|
835
1000
|
BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
|
|
@@ -838,20 +1003,51 @@ async function refreshBuilderBodySourceValuesFromStoredLossless(
|
|
|
838
1003
|
entry.sourceValues,
|
|
839
1004
|
BUILDER_CMS_BODY_SIDECARS_KEY,
|
|
840
1005
|
);
|
|
841
|
-
const
|
|
1006
|
+
const snapshot = await builderBodySnapshotFromStoredLossless({
|
|
842
1007
|
losslessContent,
|
|
843
1008
|
sidecarsJson,
|
|
844
1009
|
});
|
|
845
|
-
if (!
|
|
1010
|
+
if (!snapshot) return entry;
|
|
846
1011
|
return {
|
|
847
1012
|
...entry,
|
|
848
1013
|
sourceValues: {
|
|
849
1014
|
...entry.sourceValues,
|
|
850
|
-
[BUILDER_CMS_BODY_CONTENT_KEY]: content,
|
|
1015
|
+
[BUILDER_CMS_BODY_CONTENT_KEY]: snapshot.content,
|
|
1016
|
+
[BUILDER_CMS_BODY_BLOCKS_HASH_KEY]: snapshot.blocksHash,
|
|
851
1017
|
},
|
|
852
1018
|
};
|
|
853
1019
|
}
|
|
854
1020
|
|
|
1021
|
+
function builderTimestampEpoch(value: string | null | undefined) {
|
|
1022
|
+
if (!value?.trim()) return null;
|
|
1023
|
+
const numeric = Number(value);
|
|
1024
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
1025
|
+
const parsed = Date.parse(value);
|
|
1026
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function builderBodyBaselineHasSameVersionConflict(args: {
|
|
1030
|
+
persistedBlocksHash: string | null | undefined;
|
|
1031
|
+
incomingBlocksHash: string | null | undefined;
|
|
1032
|
+
persistedLastUpdated: string | null | undefined;
|
|
1033
|
+
incomingLastUpdated: string | null | undefined;
|
|
1034
|
+
}) {
|
|
1035
|
+
if (
|
|
1036
|
+
!args.persistedBlocksHash ||
|
|
1037
|
+
!args.incomingBlocksHash ||
|
|
1038
|
+
args.persistedBlocksHash === args.incomingBlocksHash
|
|
1039
|
+
) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
const persistedEpoch = builderTimestampEpoch(args.persistedLastUpdated);
|
|
1043
|
+
const incomingEpoch = builderTimestampEpoch(args.incomingLastUpdated);
|
|
1044
|
+
return (
|
|
1045
|
+
persistedEpoch !== null &&
|
|
1046
|
+
incomingEpoch !== null &&
|
|
1047
|
+
persistedEpoch === incomingEpoch
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
855
1051
|
function builderEntryFromSourceRow(args: {
|
|
856
1052
|
row: Pick<
|
|
857
1053
|
ContentDatabaseSourceRecordRowDb,
|
|
@@ -910,8 +1106,12 @@ async function readBuilderEntryWithLiveBodyFromSourceRow(args: {
|
|
|
910
1106
|
...liveEntry.sourceValues,
|
|
911
1107
|
},
|
|
912
1108
|
};
|
|
913
|
-
|
|
914
|
-
|
|
1109
|
+
// This entry came from a fresh Builder response. Preserve the block hash
|
|
1110
|
+
// computed from those authoritative raw blocks; rebuilding the generated
|
|
1111
|
+
// lossless MDX can normalize block details and produce a different hash than
|
|
1112
|
+
// the execute-time live preflight sees for the same response.
|
|
1113
|
+
const refreshedEntry = await withBuilderBodySourceValues(
|
|
1114
|
+
entryWithStoredValues,
|
|
915
1115
|
);
|
|
916
1116
|
return builderEntryHasBodyContent(refreshedEntry) ? refreshedEntry : null;
|
|
917
1117
|
}
|
|
@@ -1067,15 +1267,84 @@ export async function enqueueBuilderBodyHydrationForItems(args: {
|
|
|
1067
1267
|
now: string;
|
|
1068
1268
|
}) {
|
|
1069
1269
|
if (!args.builderEntriesByDocumentId?.size) return;
|
|
1270
|
+
const persistedStateByDocumentId = new Map<
|
|
1271
|
+
string,
|
|
1272
|
+
{
|
|
1273
|
+
entry: BuilderCmsSourceEntry;
|
|
1274
|
+
bodyHydrationStatus: string | null;
|
|
1275
|
+
bodyHydrationVersion: string | null;
|
|
1276
|
+
documentContent: string | null;
|
|
1277
|
+
}
|
|
1278
|
+
>();
|
|
1279
|
+
const documentIds = args.items.map((item) => item.document.id);
|
|
1280
|
+
for (const documentIdChunk of chunks(documentIds, idChunkSize())) {
|
|
1281
|
+
const rows = await getDb()
|
|
1282
|
+
.select({
|
|
1283
|
+
documentId: schema.contentDatabaseSourceRows.documentId,
|
|
1284
|
+
sourceRowId: schema.contentDatabaseSourceRows.sourceRowId,
|
|
1285
|
+
sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
1286
|
+
lastSourceUpdatedAt:
|
|
1287
|
+
schema.contentDatabaseSourceRows.lastSourceUpdatedAt,
|
|
1288
|
+
bodyHydrationStatus: schema.contentDatabaseItems.bodyHydrationStatus,
|
|
1289
|
+
bodyHydrationVersion: schema.contentDatabaseItems.bodyHydrationVersion,
|
|
1290
|
+
documentContent: schema.documents.content,
|
|
1291
|
+
})
|
|
1292
|
+
.from(schema.contentDatabaseSourceRows)
|
|
1293
|
+
.innerJoin(
|
|
1294
|
+
schema.contentDatabaseItems,
|
|
1295
|
+
eq(
|
|
1296
|
+
schema.contentDatabaseItems.id,
|
|
1297
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
1298
|
+
),
|
|
1299
|
+
)
|
|
1300
|
+
.innerJoin(
|
|
1301
|
+
schema.documents,
|
|
1302
|
+
eq(schema.documents.id, schema.contentDatabaseSourceRows.documentId),
|
|
1303
|
+
)
|
|
1304
|
+
.where(
|
|
1305
|
+
and(
|
|
1306
|
+
eq(schema.contentDatabaseSourceRows.sourceId, args.sourceId),
|
|
1307
|
+
inArray(schema.contentDatabaseSourceRows.documentId, documentIdChunk),
|
|
1308
|
+
),
|
|
1309
|
+
);
|
|
1310
|
+
for (const row of rows) {
|
|
1311
|
+
const entry = builderEntryFromSourceRow({
|
|
1312
|
+
row,
|
|
1313
|
+
sourceTable: args.sourceTable,
|
|
1314
|
+
fallbackTitle: row.sourceRowId ?? row.documentId,
|
|
1315
|
+
});
|
|
1316
|
+
if (entry) {
|
|
1317
|
+
persistedStateByDocumentId.set(row.documentId, {
|
|
1318
|
+
entry,
|
|
1319
|
+
bodyHydrationStatus: row.bodyHydrationStatus,
|
|
1320
|
+
bodyHydrationVersion: row.bodyHydrationVersion,
|
|
1321
|
+
documentContent: row.documentContent,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1070
1326
|
const requests: BuilderBodyHydrationEnqueueRequest[] = [];
|
|
1071
1327
|
for (const item of args.items) {
|
|
1072
1328
|
const entry = args.builderEntriesByDocumentId.get(item.document.id);
|
|
1073
1329
|
if (!entry) continue;
|
|
1330
|
+
const persistedState = persistedStateByDocumentId.get(item.document.id);
|
|
1331
|
+
const persistedEntry = persistedState?.entry ?? entry;
|
|
1332
|
+
const bodyHydrationStatus =
|
|
1333
|
+
persistedState?.bodyHydrationStatus ?? item.bodyHydration?.status;
|
|
1334
|
+
const bodyHydrationVersion =
|
|
1335
|
+
persistedState?.bodyHydrationVersion ?? item.bodyHydration?.version;
|
|
1336
|
+
const documentContent =
|
|
1337
|
+
persistedState?.documentContent ?? item.document.content;
|
|
1338
|
+
const expectedVersion =
|
|
1339
|
+
bodyHydrationStatus === "unavailable"
|
|
1340
|
+
? builderBodyUnavailableVersion(persistedEntry)
|
|
1341
|
+
: builderBodyHydrationVersion(persistedEntry);
|
|
1074
1342
|
if (
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1343
|
+
(bodyHydrationStatus === "unavailable" ||
|
|
1344
|
+
(bodyHydrationStatus === "hydrated" &&
|
|
1345
|
+
!isEffectivelyEmptyDocumentContent(documentContent) &&
|
|
1346
|
+
!builderBodyIsRawPlaceholderOnly(documentContent))) &&
|
|
1347
|
+
bodyHydrationVersion === expectedVersion
|
|
1079
1348
|
) {
|
|
1080
1349
|
continue;
|
|
1081
1350
|
}
|
|
@@ -1196,11 +1465,16 @@ async function processBuilderBodyHydrationJob(
|
|
|
1196
1465
|
preloaded?: {
|
|
1197
1466
|
sourceRow?: ContentDatabaseSourceRecordRowDb | null;
|
|
1198
1467
|
documentContent?: string | null;
|
|
1468
|
+
bodyHydrationVersion?: string | null;
|
|
1199
1469
|
},
|
|
1200
1470
|
) {
|
|
1201
1471
|
const db = getDb();
|
|
1202
1472
|
const entry = parseHydrationEntry(row);
|
|
1203
1473
|
if (!entry) throw new Error("Builder body hydration entry is missing.");
|
|
1474
|
+
const queuedBlocksHash = stringSourceValue(
|
|
1475
|
+
entry.sourceValues,
|
|
1476
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
1477
|
+
);
|
|
1204
1478
|
let activeSourceEntryJson = row.sourceEntryJson;
|
|
1205
1479
|
let entryWithBody = await refreshBuilderBodySourceValuesFromStoredLossless(
|
|
1206
1480
|
await withBuilderBodySourceValues(entry),
|
|
@@ -1226,6 +1500,73 @@ async function processBuilderBodyHydrationJob(
|
|
|
1226
1500
|
parseObject<Record<string, DocumentPropertyValue>>(
|
|
1227
1501
|
sourceRow?.sourceValuesJson ?? "{}",
|
|
1228
1502
|
) ?? {};
|
|
1503
|
+
const rebuiltBlocksHash = stringSourceValue(
|
|
1504
|
+
entryWithBody.sourceValues,
|
|
1505
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
1506
|
+
);
|
|
1507
|
+
const storedBlocksHash =
|
|
1508
|
+
queuedBlocksHash ??
|
|
1509
|
+
stringSourceValue(sourceValues, BUILDER_CMS_BODY_BLOCKS_HASH_KEY);
|
|
1510
|
+
const isCodecMigration = builderBodyHydrationIsCodecMigration(
|
|
1511
|
+
preloaded?.bodyHydrationVersion,
|
|
1512
|
+
);
|
|
1513
|
+
if (
|
|
1514
|
+
builderBodyHydrationNeedsLiveBaseline({
|
|
1515
|
+
bodyHydrationVersion: preloaded?.bodyHydrationVersion,
|
|
1516
|
+
storedBlocksHash,
|
|
1517
|
+
rebuiltBlocksHash,
|
|
1518
|
+
})
|
|
1519
|
+
) {
|
|
1520
|
+
if (!sourceRow) {
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
"Builder body baseline migration requires a linked source row.",
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const liveEntry = await readBuilderEntryWithLiveBodyFromSourceRow({
|
|
1526
|
+
row: sourceRow,
|
|
1527
|
+
sourceTable: row.sourceTable,
|
|
1528
|
+
fallbackTitle: entry.title,
|
|
1529
|
+
});
|
|
1530
|
+
if (!liveEntry) {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
"Builder body baseline migration could not read a fresh remote body; retry the refresh before reviewing or publishing.",
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
entryWithBody = liveEntry;
|
|
1536
|
+
}
|
|
1537
|
+
const incomingBlocksHash = stringSourceValue(
|
|
1538
|
+
entryWithBody.sourceValues,
|
|
1539
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
1540
|
+
);
|
|
1541
|
+
const persistedBaselineLastUpdated =
|
|
1542
|
+
stringSourceValue(sourceValues, BUILDER_CMS_BODY_LAST_UPDATED_KEY) ??
|
|
1543
|
+
stringSourceValue(sourceValues, "lastUpdated");
|
|
1544
|
+
const incomingLastUpdated =
|
|
1545
|
+
stringSourceValue(entryWithBody.sourceValues, "lastUpdated") ??
|
|
1546
|
+
entryWithBody.updatedAt;
|
|
1547
|
+
if (
|
|
1548
|
+
!isCodecMigration &&
|
|
1549
|
+
builderBodyBaselineHasSameVersionConflict({
|
|
1550
|
+
persistedBlocksHash: stringSourceValue(
|
|
1551
|
+
sourceValues,
|
|
1552
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
1553
|
+
),
|
|
1554
|
+
incomingBlocksHash,
|
|
1555
|
+
persistedLastUpdated: persistedBaselineLastUpdated,
|
|
1556
|
+
incomingLastUpdated,
|
|
1557
|
+
}) &&
|
|
1558
|
+
!builderBodyHydrationCanAdoptSameVersionVariant({
|
|
1559
|
+
documentContent: preloaded?.documentContent,
|
|
1560
|
+
persistedContent: stringSourceValue(
|
|
1561
|
+
sourceValues,
|
|
1562
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
1563
|
+
),
|
|
1564
|
+
})
|
|
1565
|
+
) {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
"Builder returned inconsistent body variants for the same lastUpdated value; the stored baseline and local document were preserved for retry.",
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1229
1570
|
let nextValues = {
|
|
1230
1571
|
...sourceValues,
|
|
1231
1572
|
...entryWithBody.sourceValues,
|
|
@@ -1354,9 +1695,11 @@ async function processBuilderBodyHydrationJob(
|
|
|
1354
1695
|
await tx
|
|
1355
1696
|
.update(schema.contentDatabaseItems)
|
|
1356
1697
|
.set({
|
|
1357
|
-
bodyHydrationStatus: "
|
|
1698
|
+
bodyHydrationStatus: "unavailable",
|
|
1358
1699
|
bodyHydrationAttemptedAt: now,
|
|
1359
|
-
bodyHydrationError:
|
|
1700
|
+
bodyHydrationError: null,
|
|
1701
|
+
bodyHydrationVersion:
|
|
1702
|
+
builderBodyUnavailableVersion(entryWithBody),
|
|
1360
1703
|
updatedAt: now,
|
|
1361
1704
|
})
|
|
1362
1705
|
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
@@ -1569,14 +1912,11 @@ async function processBuilderBodyHydrationJob(
|
|
|
1569
1912
|
})
|
|
1570
1913
|
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
1571
1914
|
});
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
// Non-fatal: the updated document row still lets open editors reconcile.
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1915
|
+
// Keep persisted and in-memory Yjs state intact. The SQL content + updatedAt
|
|
1916
|
+
// written above are authoritative; an open full-page editor reconciles that
|
|
1917
|
+
// snapshot into its Y.Doc. Deleting collab state here can race a connected
|
|
1918
|
+
// client, briefly bootstrap an empty fragment, and let that stale client
|
|
1919
|
+
// repersist the empty state over the newly hydrated Builder body.
|
|
1580
1920
|
}
|
|
1581
1921
|
|
|
1582
1922
|
async function enqueueStaleBuilderBodyHydrationForOpenDocument(args: {
|
|
@@ -1795,6 +2135,29 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
1795
2135
|
const sourceRowsByItemId = new Map(
|
|
1796
2136
|
sourceRows.map((row) => [row.databaseItemId, row]),
|
|
1797
2137
|
);
|
|
2138
|
+
const itemVersions =
|
|
2139
|
+
claimedJobs.length > 0
|
|
2140
|
+
? (
|
|
2141
|
+
await Promise.all(
|
|
2142
|
+
chunks(
|
|
2143
|
+
claimedJobs.map((job) => job.databaseItemId),
|
|
2144
|
+
idChunkSize(),
|
|
2145
|
+
).map((idChunk) =>
|
|
2146
|
+
db
|
|
2147
|
+
.select({
|
|
2148
|
+
id: schema.contentDatabaseItems.id,
|
|
2149
|
+
bodyHydrationVersion:
|
|
2150
|
+
schema.contentDatabaseItems.bodyHydrationVersion,
|
|
2151
|
+
})
|
|
2152
|
+
.from(schema.contentDatabaseItems)
|
|
2153
|
+
.where(inArray(schema.contentDatabaseItems.id, idChunk)),
|
|
2154
|
+
),
|
|
2155
|
+
)
|
|
2156
|
+
).flat()
|
|
2157
|
+
: [];
|
|
2158
|
+
const bodyHydrationVersionByItemId = new Map(
|
|
2159
|
+
itemVersions.map((item) => [item.id, item.bodyHydrationVersion]),
|
|
2160
|
+
);
|
|
1798
2161
|
const documents =
|
|
1799
2162
|
claimedJobs.length > 0
|
|
1800
2163
|
? (
|
|
@@ -1827,6 +2190,8 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
1827
2190
|
if (delayMs > 0) await sleep(delayMs);
|
|
1828
2191
|
await processBuilderBodyHydrationJob(job, attemptNow, {
|
|
1829
2192
|
sourceRow: sourceRowsByItemId.get(job.databaseItemId) ?? null,
|
|
2193
|
+
bodyHydrationVersion:
|
|
2194
|
+
bodyHydrationVersionByItemId.get(job.databaseItemId) ?? null,
|
|
1830
2195
|
documentContent: documentContentById.has(job.documentId)
|
|
1831
2196
|
? (documentContentById.get(job.documentId) ?? null)
|
|
1832
2197
|
: undefined,
|
|
@@ -1934,6 +2299,8 @@ export async function withBuilderBodySourceValues(
|
|
|
1934
2299
|
[BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY]: snapshot.losslessContent,
|
|
1935
2300
|
[BUILDER_CMS_BODY_READABLE_MAP_KEY]: snapshot.readableMapJson,
|
|
1936
2301
|
[BUILDER_CMS_BODY_BLOCKS_HASH_KEY]: snapshot.blocksHash,
|
|
2302
|
+
[BUILDER_CMS_BODY_LAST_UPDATED_KEY]:
|
|
2303
|
+
stringSourceValue(entry.sourceValues, "lastUpdated") ?? entry.updatedAt,
|
|
1937
2304
|
[BUILDER_CMS_BODY_SIDECARS_KEY]: snapshot.sidecarsJson,
|
|
1938
2305
|
},
|
|
1939
2306
|
};
|
|
@@ -1972,9 +2339,17 @@ export async function builderBodyChangeForLocalContent(args: {
|
|
|
1972
2339
|
const localContent = args.localContent ?? "";
|
|
1973
2340
|
if (!currentHash && !currentContent && !localContent.trim()) return null;
|
|
1974
2341
|
if (!currentContent?.trim() && !losslessContent?.trim()) return null;
|
|
2342
|
+
// Native media is converter-owned output. Re-run it even when the editable
|
|
2343
|
+
// Markdown text is byte-for-byte unchanged: a converter upgrade can turn a
|
|
2344
|
+
// legacy Text block containing a Markdown image into a real Builder Image
|
|
2345
|
+
// (or emit a native Video) without changing the document text at all.
|
|
2346
|
+
const usesCurrentMediaConverter =
|
|
2347
|
+
/!\[[^\]]*\]\(\s*https?:\/\//i.test(localContent) ||
|
|
2348
|
+
/<(?:img|video)\b/i.test(localContent);
|
|
1975
2349
|
const normalizedLocalContent =
|
|
1976
2350
|
normalizeBuilderBodyBaselineContent(localContent);
|
|
1977
2351
|
if (
|
|
2352
|
+
!usesCurrentMediaConverter &&
|
|
1978
2353
|
normalizedLocalContent &&
|
|
1979
2354
|
normalizedLocalContent ===
|
|
1980
2355
|
normalizeBuilderBodyBaselineContent(currentContent)
|
|
@@ -1982,6 +2357,7 @@ export async function builderBodyChangeForLocalContent(args: {
|
|
|
1982
2357
|
return null;
|
|
1983
2358
|
}
|
|
1984
2359
|
if (
|
|
2360
|
+
!usesCurrentMediaConverter &&
|
|
1985
2361
|
normalizedLocalContent &&
|
|
1986
2362
|
normalizedLocalContent ===
|
|
1987
2363
|
normalizeBuilderBodyBaselineContent(losslessContent)
|
|
@@ -2004,8 +2380,15 @@ export async function builderBodyChangeForLocalContent(args: {
|
|
|
2004
2380
|
}
|
|
2005
2381
|
|
|
2006
2382
|
try {
|
|
2007
|
-
const proposed =
|
|
2008
|
-
|
|
2383
|
+
const proposed = usesCurrentMediaConverter
|
|
2384
|
+
? {
|
|
2385
|
+
blocks: await builderMdxBodyToBuilderBlocks(
|
|
2386
|
+
normalizeUnsourcedBuilderCreateMdx(localContent),
|
|
2387
|
+
sidecars,
|
|
2388
|
+
),
|
|
2389
|
+
warnings: [] as string[],
|
|
2390
|
+
}
|
|
2391
|
+
: losslessContent && !localContent.includes("<Builder")
|
|
2009
2392
|
? await builderReadableBodyToBuilderBlocks({
|
|
2010
2393
|
localContent,
|
|
2011
2394
|
losslessContent,
|
|
@@ -2061,6 +2444,98 @@ export async function builderBodyChangeForLocalContent(args: {
|
|
|
2061
2444
|
}
|
|
2062
2445
|
}
|
|
2063
2446
|
|
|
2447
|
+
function htmlMediaAttribute(tag: string, name: string) {
|
|
2448
|
+
const match = tag.match(
|
|
2449
|
+
new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"),
|
|
2450
|
+
);
|
|
2451
|
+
return match?.[1] ?? match?.[2] ?? null;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function normalizeUnsourcedBuilderCreateMdx(content: string) {
|
|
2455
|
+
return content.replace(/<img\b[^>]*\/?\s*>/gi, (tag) => {
|
|
2456
|
+
const src = htmlMediaAttribute(tag, "src");
|
|
2457
|
+
if (!src) return tag;
|
|
2458
|
+
const alt = htmlMediaAttribute(tag, "alt") ?? "";
|
|
2459
|
+
return `![${alt.replace(/]/g, "\\]")}](${src.replace(/\)/g, "%29")})`;
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
export async function builderBodyChangeForUnsourcedLocalCreate(args: {
|
|
2464
|
+
localContent: string | null | undefined;
|
|
2465
|
+
}): Promise<ContentDatabaseSourceBodyChange | null> {
|
|
2466
|
+
const localContent = args.localContent ?? "";
|
|
2467
|
+
if (!localContent.trim()) return null;
|
|
2468
|
+
const sidecarsJson = "{}";
|
|
2469
|
+
|
|
2470
|
+
try {
|
|
2471
|
+
const blocks = await builderMdxBodyToBuilderBlocks(
|
|
2472
|
+
normalizeUnsourcedBuilderCreateMdx(localContent),
|
|
2473
|
+
{},
|
|
2474
|
+
);
|
|
2475
|
+
return {
|
|
2476
|
+
summary: "Builder draft body ready to create.",
|
|
2477
|
+
currentExcerpt: null,
|
|
2478
|
+
proposedExcerpt: bodyExcerpt(localContent),
|
|
2479
|
+
currentHash: null,
|
|
2480
|
+
proposedHash: builderBlocksHash(blocks),
|
|
2481
|
+
proposedContent: localContent,
|
|
2482
|
+
proposedBlocksJson: JSON.stringify(blocks),
|
|
2483
|
+
sidecarsJson,
|
|
2484
|
+
warnings: [],
|
|
2485
|
+
};
|
|
2486
|
+
} catch (error) {
|
|
2487
|
+
const message =
|
|
2488
|
+
error instanceof Error
|
|
2489
|
+
? error.message
|
|
2490
|
+
: "Builder body could not be converted.";
|
|
2491
|
+
return {
|
|
2492
|
+
summary: "Builder draft body needs attention before create.",
|
|
2493
|
+
currentExcerpt: null,
|
|
2494
|
+
proposedExcerpt: bodyExcerpt(localContent),
|
|
2495
|
+
currentHash: null,
|
|
2496
|
+
proposedHash: null,
|
|
2497
|
+
proposedContent: localContent,
|
|
2498
|
+
proposedBlocksJson: null,
|
|
2499
|
+
sidecarsJson,
|
|
2500
|
+
warnings: [message],
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
export async function builderBodyChangeForSourceSnapshotDocument(args: {
|
|
2506
|
+
row?: {
|
|
2507
|
+
documentId: string;
|
|
2508
|
+
sourceRowId: string;
|
|
2509
|
+
sourceQualifiedId: string;
|
|
2510
|
+
provenance: string | null;
|
|
2511
|
+
sourceValuesJson: string;
|
|
2512
|
+
};
|
|
2513
|
+
isHydrated: boolean;
|
|
2514
|
+
allowUnsourcedCreate: boolean;
|
|
2515
|
+
localContent: string | null | undefined;
|
|
2516
|
+
}): Promise<ContentDatabaseSourceBodyChange | null> {
|
|
2517
|
+
if (args.row) {
|
|
2518
|
+
const identity = builderCmsSourceRowIdentityState({ row: args.row });
|
|
2519
|
+
if (identity.isSyntheticFixture) {
|
|
2520
|
+
// Fixture rows are local placeholders, not imported Builder baselines.
|
|
2521
|
+
// Execution resolves their synthetic identity to create_draft, so their
|
|
2522
|
+
// local body must follow the same create path even before hydration.
|
|
2523
|
+
return builderBodyChangeForUnsourcedLocalCreate({
|
|
2524
|
+
localContent: args.localContent,
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
if (!args.isHydrated) return null;
|
|
2528
|
+
return builderBodyChangeForLocalContent({
|
|
2529
|
+
row: args.row,
|
|
2530
|
+
localContent: args.localContent,
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
if (!args.allowUnsourcedCreate) return null;
|
|
2534
|
+
return builderBodyChangeForUnsourcedLocalCreate({
|
|
2535
|
+
localContent: args.localContent,
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2064
2539
|
export function buildBuilderLocalOutboundChangeSets(args: {
|
|
2065
2540
|
source: ContentDatabaseSourceRowDb;
|
|
2066
2541
|
rowRows: ContentDatabaseSourceRecordRowDb[];
|
|
@@ -2077,6 +2552,8 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2077
2552
|
sourceFieldLabel: string;
|
|
2078
2553
|
propertyType?: DocumentProperty["definition"]["type"] | null;
|
|
2079
2554
|
propertyOptions?: DocumentPropertyOptions;
|
|
2555
|
+
sourceFieldType?: string;
|
|
2556
|
+
sourceFieldModel?: string;
|
|
2080
2557
|
}>;
|
|
2081
2558
|
// Row-union scoping (multi-source). Documents owned by ANOTHER source must
|
|
2082
2559
|
// never be create candidates for this one — each row belongs to exactly one
|
|
@@ -2092,6 +2569,12 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2092
2569
|
taggedSourceByDocumentId?: Map<string, string>;
|
|
2093
2570
|
bodyChangeByDocumentId?: Map<string, ContentDatabaseSourceBodyChange>;
|
|
2094
2571
|
sourceImportedDocumentIds?: Set<string>;
|
|
2572
|
+
/**
|
|
2573
|
+
* Rejected change-sets that are provably cancellations of a prepared,
|
|
2574
|
+
* pre-dispatch Builder gate. Their exact snapshot remains durable audit
|
|
2575
|
+
* history and suppresses only the byte-equivalent local-vs-source diff.
|
|
2576
|
+
*/
|
|
2577
|
+
cancelledRejectedChangeSetIds?: Set<string>;
|
|
2095
2578
|
}): ContentDatabaseSourceChangeSet[] {
|
|
2096
2579
|
if (normalizeSourceType(args.source.sourceType) !== "builder-cms") return [];
|
|
2097
2580
|
|
|
@@ -2102,6 +2585,106 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2102
2585
|
normalizeCapabilities(args.source.capabilitiesJson).liveWritesEnabled ===
|
|
2103
2586
|
true;
|
|
2104
2587
|
const pending: ContentDatabaseSourceChangeSet[] = [];
|
|
2588
|
+
|
|
2589
|
+
const sameCancelledSnapshot = (
|
|
2590
|
+
candidate: Pick<
|
|
2591
|
+
ContentDatabaseSourceChangeSet,
|
|
2592
|
+
| "databaseItemId"
|
|
2593
|
+
| "documentId"
|
|
2594
|
+
| "kind"
|
|
2595
|
+
| "direction"
|
|
2596
|
+
| "pushMode"
|
|
2597
|
+
| "fieldChanges"
|
|
2598
|
+
| "bodyChange"
|
|
2599
|
+
>,
|
|
2600
|
+
) =>
|
|
2601
|
+
args.storedChangeSets.some(
|
|
2602
|
+
(stored) =>
|
|
2603
|
+
stored.state === "rejected" &&
|
|
2604
|
+
args.cancelledRejectedChangeSetIds?.has(stored.id) === true &&
|
|
2605
|
+
stored.databaseItemId === candidate.databaseItemId &&
|
|
2606
|
+
stored.documentId === candidate.documentId &&
|
|
2607
|
+
stored.kind === candidate.kind &&
|
|
2608
|
+
stored.direction === candidate.direction &&
|
|
2609
|
+
stored.pushMode === candidate.pushMode &&
|
|
2610
|
+
stableValueString(stored.fieldChanges) ===
|
|
2611
|
+
stableValueString(candidate.fieldChanges) &&
|
|
2612
|
+
stableValueString(stored.bodyChange) ===
|
|
2613
|
+
stableValueString(candidate.bodyChange),
|
|
2614
|
+
);
|
|
2615
|
+
|
|
2616
|
+
const reviewableBuilderValue = (
|
|
2617
|
+
field: NonNullable<typeof args.writableFields>[number],
|
|
2618
|
+
localValue: unknown,
|
|
2619
|
+
): Pick<
|
|
2620
|
+
ContentDatabaseSourceFieldChange,
|
|
2621
|
+
"proposedValue" | "builderValueJson"
|
|
2622
|
+
> => {
|
|
2623
|
+
const type = field.propertyType;
|
|
2624
|
+
const options = field.propertyOptions?.options ?? [];
|
|
2625
|
+
const optionNameById = new Map(
|
|
2626
|
+
options.map((option) => [option.id, option.name]),
|
|
2627
|
+
);
|
|
2628
|
+
const sourceType = field.sourceFieldType?.trim().toLowerCase();
|
|
2629
|
+
if (sourceType === "reference") {
|
|
2630
|
+
const id = typeof localValue === "string" ? localValue.trim() : "";
|
|
2631
|
+
const label = optionNameById.get(id) ?? id;
|
|
2632
|
+
const builderValue =
|
|
2633
|
+
id && field.sourceFieldModel
|
|
2634
|
+
? {
|
|
2635
|
+
"@type": "@builder.io/core:Reference",
|
|
2636
|
+
id,
|
|
2637
|
+
model: field.sourceFieldModel,
|
|
2638
|
+
}
|
|
2639
|
+
: null;
|
|
2640
|
+
return {
|
|
2641
|
+
proposedValue: label || null,
|
|
2642
|
+
builderValueJson: JSON.stringify(builderValue),
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
if (sourceType === "file") {
|
|
2646
|
+
const values = Array.isArray(localValue)
|
|
2647
|
+
? localValue.filter(
|
|
2648
|
+
(value): value is string => typeof value === "string",
|
|
2649
|
+
)
|
|
2650
|
+
: typeof localValue === "string"
|
|
2651
|
+
? [localValue]
|
|
2652
|
+
: [];
|
|
2653
|
+
return {
|
|
2654
|
+
proposedValue: values,
|
|
2655
|
+
builderValueJson: JSON.stringify(
|
|
2656
|
+
values.length === 1 ? values[0] : null,
|
|
2657
|
+
),
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
if (type === "multi_select" && Array.isArray(localValue)) {
|
|
2661
|
+
const labels = localValue
|
|
2662
|
+
.filter((value): value is string => typeof value === "string")
|
|
2663
|
+
.map((value) => optionNameById.get(value) ?? value);
|
|
2664
|
+
return {
|
|
2665
|
+
proposedValue: labels,
|
|
2666
|
+
builderValueJson: JSON.stringify(labels),
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
if (
|
|
2670
|
+
(sourceType === "datetime" || sourceType === "date") &&
|
|
2671
|
+
localValue &&
|
|
2672
|
+
typeof localValue === "object" &&
|
|
2673
|
+
!Array.isArray(localValue) &&
|
|
2674
|
+
typeof (localValue as { start?: unknown }).start === "string"
|
|
2675
|
+
) {
|
|
2676
|
+
return {
|
|
2677
|
+
proposedValue: localValue as DocumentPropertyValue,
|
|
2678
|
+
builderValueJson: JSON.stringify(
|
|
2679
|
+
(localValue as { start: string }).start,
|
|
2680
|
+
),
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
proposedValue: (localValue ?? null) as DocumentPropertyValue,
|
|
2685
|
+
builderValueJson: JSON.stringify(localValue ?? null),
|
|
2686
|
+
};
|
|
2687
|
+
};
|
|
2105
2688
|
for (const row of args.rowRows) {
|
|
2106
2689
|
if (
|
|
2107
2690
|
skipFixtureRows &&
|
|
@@ -2110,7 +2693,15 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2110
2693
|
continue;
|
|
2111
2694
|
}
|
|
2112
2695
|
|
|
2113
|
-
const
|
|
2696
|
+
const rowSourceValues =
|
|
2697
|
+
parseObject<Record<string, DocumentPropertyValue>>(
|
|
2698
|
+
row.sourceValuesJson,
|
|
2699
|
+
) ?? {};
|
|
2700
|
+
const sourceTitleValue = rowSourceValues["data.title"];
|
|
2701
|
+
const sourceTitle =
|
|
2702
|
+
typeof sourceTitleValue === "string" && sourceTitleValue.trim()
|
|
2703
|
+
? sourceTitleValue.trim()
|
|
2704
|
+
: row.sourceDisplayKey.trim();
|
|
2114
2705
|
const localTitle = args.documentTitleById.get(row.documentId)?.trim() ?? "";
|
|
2115
2706
|
const fieldChanges: ContentDatabaseSourceFieldChange[] = [];
|
|
2116
2707
|
if (localTitle && localTitle !== sourceTitle) {
|
|
@@ -2128,18 +2719,20 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2128
2719
|
// local value means "not loaded", not "cleared" — skip it.
|
|
2129
2720
|
const rowLocalValues = args.localValuesByDocument?.get(row.documentId);
|
|
2130
2721
|
if (rowLocalValues) {
|
|
2131
|
-
const rowSourceValues =
|
|
2132
|
-
parseObject<Record<string, DocumentPropertyValue>>(
|
|
2133
|
-
row.sourceValuesJson,
|
|
2134
|
-
) ?? {};
|
|
2135
2722
|
for (const field of args.writableFields ?? []) {
|
|
2136
2723
|
if (!rowLocalValues.has(field.localFieldKey)) continue;
|
|
2137
2724
|
const localValue = rowLocalValues.get(field.localFieldKey);
|
|
2138
2725
|
const baseValue = rowSourceValues[field.sourceFieldKey];
|
|
2726
|
+
const comparisonBaseValue =
|
|
2727
|
+
field.sourceFieldType?.trim().toLowerCase() === "reference"
|
|
2728
|
+
? (rowSourceValues[
|
|
2729
|
+
builderReferenceIdSourceValueKey(field.sourceFieldKey)
|
|
2730
|
+
] ?? baseValue)
|
|
2731
|
+
: baseValue;
|
|
2139
2732
|
if (
|
|
2140
2733
|
sameMappedSourceFieldValue(
|
|
2141
2734
|
localValue,
|
|
2142
|
-
|
|
2735
|
+
comparisonBaseValue,
|
|
2143
2736
|
field.propertyType,
|
|
2144
2737
|
field.propertyOptions,
|
|
2145
2738
|
)
|
|
@@ -2158,7 +2751,7 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2158
2751
|
field.propertyOptions,
|
|
2159
2752
|
)
|
|
2160
2753
|
: (baseValue ?? null)) as DocumentPropertyValue,
|
|
2161
|
-
|
|
2754
|
+
...reviewableBuilderValue(field, localValue),
|
|
2162
2755
|
});
|
|
2163
2756
|
}
|
|
2164
2757
|
}
|
|
@@ -2205,8 +2798,12 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2205
2798
|
|
|
2206
2799
|
const now = new Date().toISOString();
|
|
2207
2800
|
const displayTitle = localTitle || sourceTitle;
|
|
2208
|
-
|
|
2209
|
-
|
|
2801
|
+
const candidate: ContentDatabaseSourceChangeSet = {
|
|
2802
|
+
// Keep the synthetic identity stable while body hydration catches up.
|
|
2803
|
+
// The lightweight UI snapshot may initially see only field changes while
|
|
2804
|
+
// the authoritative write snapshot also sees a body diff. The payload
|
|
2805
|
+
// fingerprint, not an ID suffix, distinguishes material revisions.
|
|
2806
|
+
id: `local-pending-${row.id}-change`,
|
|
2210
2807
|
databaseItemId: row.databaseItemId,
|
|
2211
2808
|
documentId: row.documentId,
|
|
2212
2809
|
kind:
|
|
@@ -2233,7 +2830,8 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2233
2830
|
executions: [],
|
|
2234
2831
|
createdAt: now,
|
|
2235
2832
|
updatedAt: now,
|
|
2236
|
-
}
|
|
2833
|
+
};
|
|
2834
|
+
if (!sameCancelledSnapshot(candidate)) pending.push(candidate);
|
|
2237
2835
|
}
|
|
2238
2836
|
|
|
2239
2837
|
// New-row creates: a local database item NOT linked to a Builder entry (no
|
|
@@ -2296,12 +2894,14 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2296
2894
|
localFieldKey: field.localFieldKey,
|
|
2297
2895
|
sourceFieldKey: field.sourceFieldKey,
|
|
2298
2896
|
currentValue: null,
|
|
2299
|
-
|
|
2300
|
-
|
|
2897
|
+
...reviewableBuilderValue(
|
|
2898
|
+
field,
|
|
2899
|
+
localValues.get(field.localFieldKey) ?? null,
|
|
2900
|
+
),
|
|
2301
2901
|
});
|
|
2302
2902
|
}
|
|
2303
2903
|
const now = new Date().toISOString();
|
|
2304
|
-
|
|
2904
|
+
const candidate: ContentDatabaseSourceChangeSet = {
|
|
2305
2905
|
id: `local-pending-create-${item.databaseItemId}`,
|
|
2306
2906
|
databaseItemId: item.databaseItemId,
|
|
2307
2907
|
documentId: item.documentId,
|
|
@@ -2322,7 +2922,8 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2322
2922
|
executions: [],
|
|
2323
2923
|
createdAt: now,
|
|
2324
2924
|
updatedAt: now,
|
|
2325
|
-
}
|
|
2925
|
+
};
|
|
2926
|
+
if (!sameCancelledSnapshot(candidate)) pending.push(candidate);
|
|
2326
2927
|
}
|
|
2327
2928
|
}
|
|
2328
2929
|
|
|
@@ -2520,21 +3121,31 @@ async function readSourceSnapshotRowsOnce(args: {
|
|
|
2520
3121
|
...databaseItemRows.map((item) => item.documentId),
|
|
2521
3122
|
]),
|
|
2522
3123
|
);
|
|
2523
|
-
const rowDocuments
|
|
3124
|
+
const rowDocuments: Array<{
|
|
3125
|
+
id: string;
|
|
3126
|
+
title: string;
|
|
3127
|
+
content?: string;
|
|
3128
|
+
}> =
|
|
2524
3129
|
allDocumentIds.length > 0
|
|
2525
|
-
?
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
)
|
|
2537
|
-
|
|
3130
|
+
? args.includeHeavyBuilderBodyValues
|
|
3131
|
+
? await db
|
|
3132
|
+
.select(sourceSnapshotDocumentSelection(true))
|
|
3133
|
+
.from(schema.documents)
|
|
3134
|
+
.where(
|
|
3135
|
+
and(
|
|
3136
|
+
inArray(schema.documents.id, allDocumentIds),
|
|
3137
|
+
eq(schema.documents.ownerEmail, args.source.ownerEmail),
|
|
3138
|
+
),
|
|
3139
|
+
)
|
|
3140
|
+
: await db
|
|
3141
|
+
.select(sourceSnapshotDocumentSelection(false))
|
|
3142
|
+
.from(schema.documents)
|
|
3143
|
+
.where(
|
|
3144
|
+
and(
|
|
3145
|
+
inArray(schema.documents.id, allDocumentIds),
|
|
3146
|
+
eq(schema.documents.ownerEmail, args.source.ownerEmail),
|
|
3147
|
+
),
|
|
3148
|
+
)
|
|
2538
3149
|
: [];
|
|
2539
3150
|
const propertyValueRows =
|
|
2540
3151
|
args.isBuilderSource && allDocumentIds.length > 0
|
|
@@ -2686,13 +3297,19 @@ async function loadSourceSnapshot(
|
|
|
2686
3297
|
const propertyOptionsById = new Map(
|
|
2687
3298
|
propertyDefs.map((row) => [row.id, parsePropertyOptions(row.optionsJson)]),
|
|
2688
3299
|
);
|
|
3300
|
+
const builderModelFieldBySourceKey = new Map(
|
|
3301
|
+
(
|
|
3302
|
+
parseObject<SourceMetadataRecord>(source.metadataJson)
|
|
3303
|
+
?.builderModelFields ?? []
|
|
3304
|
+
).map((field) => [`data.${field.name.trim()}`, field]),
|
|
3305
|
+
);
|
|
2689
3306
|
const fields = fieldRows.map((row) =>
|
|
2690
3307
|
serializeSourceField(
|
|
2691
3308
|
row,
|
|
2692
3309
|
row.propertyId ? (propertyNameById.get(row.propertyId) ?? null) : null,
|
|
2693
3310
|
),
|
|
2694
3311
|
);
|
|
2695
|
-
|
|
3312
|
+
let storedChangeSets = changeRows.map(serializeSourceChangeSet);
|
|
2696
3313
|
const reviewEventsByChangeSetId = new Map<
|
|
2697
3314
|
string,
|
|
2698
3315
|
ContentDatabaseSourceReviewEvent[]
|
|
@@ -2764,6 +3381,9 @@ async function loadSourceSnapshot(
|
|
|
2764
3381
|
propertyOptions: row.propertyId
|
|
2765
3382
|
? propertyOptionsById.get(row.propertyId)
|
|
2766
3383
|
: undefined,
|
|
3384
|
+
sourceFieldType: row.sourceFieldType,
|
|
3385
|
+
sourceFieldModel: builderModelFieldBySourceKey.get(row.sourceFieldKey)
|
|
3386
|
+
?.model,
|
|
2767
3387
|
}));
|
|
2768
3388
|
// Row-union ownership scoping (Builder only). Determine which documents belong
|
|
2769
3389
|
// to OTHER sources and whether this source is the primary (oldest), so the
|
|
@@ -2844,11 +3464,19 @@ async function loadSourceSnapshot(
|
|
|
2844
3464
|
);
|
|
2845
3465
|
await Promise.all(
|
|
2846
3466
|
allDocumentIds.map(async (documentId) => {
|
|
2847
|
-
if (!hydratedDocumentIds.has(documentId)) return;
|
|
2848
3467
|
const row = sourceRowByDocumentId.get(documentId);
|
|
2849
|
-
|
|
2850
|
-
|
|
3468
|
+
let bodyChange: ContentDatabaseSourceBodyChange | null = null;
|
|
3469
|
+
// Only the primary source may adopt a genuinely local row with no source
|
|
3470
|
+
// identity. Synthetic fixture rows already belong to this source, but
|
|
3471
|
+
// still represent create_draft targets rather than imported baselines.
|
|
3472
|
+
const allowUnsourcedCreate =
|
|
3473
|
+
isPrimarySource &&
|
|
3474
|
+
!otherSourceDocumentIds.has(documentId) &&
|
|
3475
|
+
!taggedSourceByDocumentId.has(documentId);
|
|
3476
|
+
bodyChange = await builderBodyChangeForSourceSnapshotDocument({
|
|
2851
3477
|
row,
|
|
3478
|
+
isHydrated: hydratedDocumentIds.has(documentId),
|
|
3479
|
+
allowUnsourcedCreate,
|
|
2852
3480
|
localContent: documentContentById.get(documentId),
|
|
2853
3481
|
});
|
|
2854
3482
|
if (bodyChange) bodyChangeByDocumentId.set(documentId, bodyChange);
|
|
@@ -2856,6 +3484,77 @@ async function loadSourceSnapshot(
|
|
|
2856
3484
|
);
|
|
2857
3485
|
}
|
|
2858
3486
|
|
|
3487
|
+
// A locally blocked dry run never crossed the Builder dispatch boundary, so
|
|
3488
|
+
// its approved body payload may be refreshed from the current document. This
|
|
3489
|
+
// is deliberately narrower than ordinary "retryable" state: running,
|
|
3490
|
+
// response-bearing, failed, reconciliatory, and otherwise ambiguous gates
|
|
3491
|
+
// retain the exact body that was originally approved.
|
|
3492
|
+
if (isBuilderSource && bodyChangeByDocumentId.size > 0) {
|
|
3493
|
+
const executionRowsByChangeSetId = new Map<
|
|
3494
|
+
string,
|
|
3495
|
+
ContentDatabaseSourceExecutionRowDb[]
|
|
3496
|
+
>();
|
|
3497
|
+
for (const execution of executionRows) {
|
|
3498
|
+
const rows = executionRowsByChangeSetId.get(execution.changeSetId) ?? [];
|
|
3499
|
+
rows.push(execution);
|
|
3500
|
+
executionRowsByChangeSetId.set(execution.changeSetId, rows);
|
|
3501
|
+
}
|
|
3502
|
+
storedChangeSets = storedChangeSets.map((changeSet) => {
|
|
3503
|
+
// Rejected snapshots are durable review/audit evidence. In particular,
|
|
3504
|
+
// a cancelled gate must retain the exact diff the user cancelled so a
|
|
3505
|
+
// later local or remote body change can be distinguished from it.
|
|
3506
|
+
if (changeSet.state !== "approved") return changeSet;
|
|
3507
|
+
const currentBody = changeSet.documentId
|
|
3508
|
+
? bodyChangeByDocumentId.get(changeSet.documentId)
|
|
3509
|
+
: null;
|
|
3510
|
+
if (!currentBody) return changeSet;
|
|
3511
|
+
const executions = executionRowsByChangeSetId.get(changeSet.id) ?? [];
|
|
3512
|
+
if (!canRefreshLocallyBlockedBuilderReview(executions)) return changeSet;
|
|
3513
|
+
return { ...changeSet, bodyChange: currentBody };
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
const cancellationReviewChangeSetIds = new Set(
|
|
3518
|
+
reviewRows
|
|
3519
|
+
.filter(
|
|
3520
|
+
(review) =>
|
|
3521
|
+
review.decision === "rejected" &&
|
|
3522
|
+
review.stateTo === "rejected" &&
|
|
3523
|
+
review.note?.startsWith(CANCELLED_BUILDER_REVIEW_NOTE_PREFIX),
|
|
3524
|
+
)
|
|
3525
|
+
.map((review) => review.changeSetId),
|
|
3526
|
+
);
|
|
3527
|
+
const executionRowsByChangeSetId = new Map<
|
|
3528
|
+
string,
|
|
3529
|
+
ContentDatabaseSourceExecutionRowDb[]
|
|
3530
|
+
>();
|
|
3531
|
+
for (const execution of executionRows) {
|
|
3532
|
+
const rows = executionRowsByChangeSetId.get(execution.changeSetId) ?? [];
|
|
3533
|
+
rows.push(execution);
|
|
3534
|
+
executionRowsByChangeSetId.set(execution.changeSetId, rows);
|
|
3535
|
+
}
|
|
3536
|
+
const cancelledRejectedChangeSetIds = new Set(
|
|
3537
|
+
storedChangeSets
|
|
3538
|
+
.filter((changeSet) => {
|
|
3539
|
+
if (
|
|
3540
|
+
changeSet.state !== "rejected" ||
|
|
3541
|
+
!cancellationReviewChangeSetIds.has(changeSet.id)
|
|
3542
|
+
) {
|
|
3543
|
+
return false;
|
|
3544
|
+
}
|
|
3545
|
+
const executions = executionRowsByChangeSetId.get(changeSet.id) ?? [];
|
|
3546
|
+
return (
|
|
3547
|
+
executions.length > 0 &&
|
|
3548
|
+
executions.every(
|
|
3549
|
+
(execution) =>
|
|
3550
|
+
execution.state === "blocked" &&
|
|
3551
|
+
execution.summary === CANCELLED_BUILDER_EXECUTION_SUMMARY,
|
|
3552
|
+
)
|
|
3553
|
+
);
|
|
3554
|
+
})
|
|
3555
|
+
.map((changeSet) => changeSet.id),
|
|
3556
|
+
);
|
|
3557
|
+
|
|
2859
3558
|
const localOutboundChangeSets = buildBuilderLocalOutboundChangeSets({
|
|
2860
3559
|
source,
|
|
2861
3560
|
rowRows,
|
|
@@ -2876,10 +3575,11 @@ async function loadSourceSnapshot(
|
|
|
2876
3575
|
.filter((row) => row.provenance === "Builder CMS read adapter")
|
|
2877
3576
|
.map((row) => row.documentId),
|
|
2878
3577
|
),
|
|
3578
|
+
cancelledRejectedChangeSetIds,
|
|
2879
3579
|
});
|
|
2880
3580
|
const rowByDocumentId = new Map(rowRows.map((row) => [row.documentId, row]));
|
|
2881
|
-
const changeSets = [
|
|
2882
|
-
(changeSet) =>
|
|
3581
|
+
const changeSets = [
|
|
3582
|
+
...storedChangeSets.map((changeSet) =>
|
|
2883
3583
|
reviewedChangeSet({
|
|
2884
3584
|
changeSet,
|
|
2885
3585
|
source,
|
|
@@ -2887,7 +3587,20 @@ async function loadSourceSnapshot(
|
|
|
2887
3587
|
reviewEvents: reviewEventsByChangeSetId.get(changeSet.id) ?? [],
|
|
2888
3588
|
executions: executionsByChangeSetId.get(changeSet.id) ?? [],
|
|
2889
3589
|
}),
|
|
2890
|
-
|
|
3590
|
+
),
|
|
3591
|
+
...localOutboundChangeSets.map((changeSet) =>
|
|
3592
|
+
reviewedChangeSet({
|
|
3593
|
+
changeSet,
|
|
3594
|
+
source,
|
|
3595
|
+
rowByDocumentId,
|
|
3596
|
+
// Synthetic current diffs deliberately reuse a stable identity. A
|
|
3597
|
+
// closed stored revision with that ID is historical evidence, not the
|
|
3598
|
+
// execution state of this new payload.
|
|
3599
|
+
reviewEvents: [],
|
|
3600
|
+
executions: [],
|
|
3601
|
+
}),
|
|
3602
|
+
),
|
|
3603
|
+
];
|
|
2891
3604
|
const metadata = parseObject<SourceMetadataRecord>(source.metadataJson) ?? {};
|
|
2892
3605
|
const normalizedWriteMode =
|
|
2893
3606
|
metadata.writeMode === "read_only" ||
|
|
@@ -2988,6 +3701,7 @@ async function loadSourceSnapshot(
|
|
|
2988
3701
|
.map((mode) => normalizePushMode(mode))
|
|
2989
3702
|
.filter((mode): mode is ContentDatabaseSourcePushMode => !!mode)
|
|
2990
3703
|
: undefined,
|
|
3704
|
+
builderModelFields: metadata.builderModelFields,
|
|
2991
3705
|
federation: normalizeSourceFederation(metadata.federation),
|
|
2992
3706
|
},
|
|
2993
3707
|
fields,
|
|
@@ -3034,6 +3748,7 @@ async function sourceBodyHydrationSummary(args: {
|
|
|
3034
3748
|
pending: 0,
|
|
3035
3749
|
hydrating: 0,
|
|
3036
3750
|
hydrated: 0,
|
|
3751
|
+
unavailable: 0,
|
|
3037
3752
|
error: 0,
|
|
3038
3753
|
total: rows.length,
|
|
3039
3754
|
};
|
|
@@ -3041,6 +3756,7 @@ async function sourceBodyHydrationSummary(args: {
|
|
|
3041
3756
|
if (row.status === "pending" || (!row.status && row.queueId)) {
|
|
3042
3757
|
summary.pending += 1;
|
|
3043
3758
|
} else if (row.status === "hydrating") summary.hydrating += 1;
|
|
3759
|
+
else if (row.status === "unavailable") summary.unavailable! += 1;
|
|
3044
3760
|
else if (row.status === "error") summary.error += 1;
|
|
3045
3761
|
else summary.hydrated += 1;
|
|
3046
3762
|
}
|
|
@@ -3157,6 +3873,7 @@ export function serializeBuilderCmsSourceReadMetadataRecord(args: {
|
|
|
3157
3873
|
);
|
|
3158
3874
|
return JSON.stringify({
|
|
3159
3875
|
...builderCmsSourceMetadata(args.sourceTable),
|
|
3876
|
+
...existingMetadata,
|
|
3160
3877
|
builderModelFields:
|
|
3161
3878
|
args.builderModelFields ?? existingMetadata?.builderModelFields,
|
|
3162
3879
|
readMode: args.readState === "live" ? "builder-api" : "fixture",
|
|
@@ -3258,6 +3975,41 @@ function normalizeBuilderCmsSourceFieldType(type: string) {
|
|
|
3258
3975
|
return normalized || "text";
|
|
3259
3976
|
}
|
|
3260
3977
|
|
|
3978
|
+
function isBuilderReferenceModelField(field: BuilderCmsModelFieldSummary) {
|
|
3979
|
+
return [field.type, field.inputType]
|
|
3980
|
+
.filter((value): value is string => typeof value === "string")
|
|
3981
|
+
.some((value) => /\b(reference|relation)\b/i.test(value));
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
/**
|
|
3985
|
+
* A raw Builder entry can teach required-field setup the reference model even
|
|
3986
|
+
* when Builder's model endpoint omits it. Refreshes must not throw that
|
|
3987
|
+
* provider-native enrichment away: the canonical local value is only an entry
|
|
3988
|
+
* id, and dispatch needs the learned model to reconstruct a Builder reference.
|
|
3989
|
+
*/
|
|
3990
|
+
export function mergeBuilderCmsModelFieldsPreservingReferenceModels(args: {
|
|
3991
|
+
existing?: BuilderCmsModelFieldSummary[];
|
|
3992
|
+
refreshed?: BuilderCmsModelFieldSummary[];
|
|
3993
|
+
}) {
|
|
3994
|
+
if (!args.refreshed || args.refreshed.length === 0) {
|
|
3995
|
+
return args.existing ?? args.refreshed;
|
|
3996
|
+
}
|
|
3997
|
+
const existingByName = new Map(
|
|
3998
|
+
(args.existing ?? []).map((field) => [field.name.trim(), field]),
|
|
3999
|
+
);
|
|
4000
|
+
return args.refreshed.map((field) => {
|
|
4001
|
+
const existing = existingByName.get(field.name.trim());
|
|
4002
|
+
if (
|
|
4003
|
+
field.model?.trim() ||
|
|
4004
|
+
!existing?.model?.trim() ||
|
|
4005
|
+
!isBuilderReferenceModelField(field)
|
|
4006
|
+
) {
|
|
4007
|
+
return field;
|
|
4008
|
+
}
|
|
4009
|
+
return { ...field, model: existing.model.trim() };
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
|
|
3261
4013
|
export async function seedMockSourceFields(args: {
|
|
3262
4014
|
sourceId: string;
|
|
3263
4015
|
ownerEmail: string;
|
|
@@ -3275,6 +4027,12 @@ export async function seedMockSourceFields(args: {
|
|
|
3275
4027
|
.filter((field) => isBuilder && field.propertyId)
|
|
3276
4028
|
.map((field) => [field.propertyId!, field]),
|
|
3277
4029
|
);
|
|
4030
|
+
const builderModelFieldBySourceKey = new Map(
|
|
4031
|
+
(args.builderModelFields ?? []).map((field) => [
|
|
4032
|
+
`data.${field.name.trim()}`,
|
|
4033
|
+
field,
|
|
4034
|
+
]),
|
|
4035
|
+
);
|
|
3278
4036
|
const rows = [
|
|
3279
4037
|
{
|
|
3280
4038
|
id: crypto.randomUUID(),
|
|
@@ -3385,7 +4143,18 @@ export async function seedMockSourceFields(args: {
|
|
|
3385
4143
|
))
|
|
3386
4144
|
: `fields.${slugifySourceField(property.definition.name)}`,
|
|
3387
4145
|
sourceFieldLabel: property.definition.name,
|
|
3388
|
-
sourceFieldType:
|
|
4146
|
+
sourceFieldType: isBuilder
|
|
4147
|
+
? normalizeBuilderCmsSourceFieldType(
|
|
4148
|
+
builderModelFieldBySourceKey.get(
|
|
4149
|
+
existingBuilderFieldByPropertyId.get(property.definition.id)
|
|
4150
|
+
?.sourceFieldKey ??
|
|
4151
|
+
builderCmsSourceFieldKey(
|
|
4152
|
+
property.definition.id,
|
|
4153
|
+
property.definition.name,
|
|
4154
|
+
),
|
|
4155
|
+
)?.type ?? property.definition.type,
|
|
4156
|
+
)
|
|
4157
|
+
: property.definition.type,
|
|
3389
4158
|
mappingType: "property",
|
|
3390
4159
|
writeOwner:
|
|
3391
4160
|
property.definition.type === "created_time" ||
|
|
@@ -3524,65 +4293,77 @@ export async function seedMockSourceRows(args: {
|
|
|
3524
4293
|
}) {
|
|
3525
4294
|
if (args.items.length === 0) return;
|
|
3526
4295
|
const db = getDb();
|
|
3527
|
-
await db
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
4296
|
+
await db
|
|
4297
|
+
.insert(schema.contentDatabaseSourceRows)
|
|
4298
|
+
.values(
|
|
4299
|
+
args.items.map((item, index) => {
|
|
4300
|
+
const builderEntry = args.builderEntriesByDocumentId?.get(
|
|
4301
|
+
item.document.id,
|
|
4302
|
+
);
|
|
4303
|
+
const existingBuilderRow = args.existingBuilderRows?.get(
|
|
4304
|
+
item.document.id,
|
|
4305
|
+
);
|
|
4306
|
+
const builderIdentity =
|
|
4307
|
+
args.sourceType === "builder-cms"
|
|
4308
|
+
? builderCmsSourceRowIdentity({
|
|
4309
|
+
item,
|
|
4310
|
+
sourceTable: args.sourceTable,
|
|
4311
|
+
now: args.now,
|
|
4312
|
+
existing: existingBuilderRow,
|
|
4313
|
+
entry: builderEntry,
|
|
4314
|
+
})
|
|
4315
|
+
: null;
|
|
4316
|
+
const sourceQualifiedId = builderIdentity
|
|
4317
|
+
? builderIdentity.sourceQualifiedId
|
|
4318
|
+
: `${args.sourceType}://${args.sourceTable}/${item.document.id}`;
|
|
4319
|
+
return {
|
|
4320
|
+
id: builderIdentity
|
|
4321
|
+
? stableBuilderImportId("builder-row", [
|
|
4322
|
+
args.sourceId,
|
|
4323
|
+
sourceQualifiedId,
|
|
4324
|
+
item.document.id,
|
|
4325
|
+
])
|
|
4326
|
+
: crypto.randomUUID(),
|
|
4327
|
+
ownerEmail: args.ownerEmail,
|
|
4328
|
+
sourceId: args.sourceId,
|
|
4329
|
+
databaseItemId: item.id,
|
|
4330
|
+
documentId: item.document.id,
|
|
4331
|
+
sourceRowId: builderIdentity
|
|
4332
|
+
? builderIdentity.sourceRowId
|
|
4333
|
+
: `${args.sourceType}-${item.document.id}`,
|
|
4334
|
+
sourceQualifiedId,
|
|
4335
|
+
sourceDisplayKey:
|
|
4336
|
+
builderIdentity?.sourceDisplayKey ??
|
|
4337
|
+
item.document.title?.trim() ??
|
|
4338
|
+
`${args.sourceType}-${index + 1}`,
|
|
4339
|
+
sourceValuesJson: JSON.stringify(
|
|
4340
|
+
sourceValuesForSeededSourceRow({
|
|
4341
|
+
sourceType: args.sourceType,
|
|
3538
4342
|
item,
|
|
3539
4343
|
sourceTable: args.sourceTable,
|
|
3540
4344
|
now: args.now,
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
:
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
:
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
sourceType: args.sourceType,
|
|
3564
|
-
item,
|
|
3565
|
-
sourceTable: args.sourceTable,
|
|
3566
|
-
now: args.now,
|
|
3567
|
-
builderEntry,
|
|
3568
|
-
existingSourceValuesJson: existingBuilderRow?.sourceValuesJson,
|
|
3569
|
-
}),
|
|
3570
|
-
),
|
|
3571
|
-
provenance:
|
|
3572
|
-
args.sourceType === "builder-cms"
|
|
3573
|
-
? args.builderEntriesByDocumentId?.has(item.document.id)
|
|
3574
|
-
? "Builder CMS read adapter"
|
|
3575
|
-
: BUILDER_CMS_FIXTURE_ROW_PROVENANCE
|
|
3576
|
-
: "mock source row",
|
|
3577
|
-
syncState: "linked",
|
|
3578
|
-
freshness: "fresh",
|
|
3579
|
-
lastSyncedAt: args.now,
|
|
3580
|
-
lastSourceUpdatedAt: builderIdentity?.lastSourceUpdatedAt ?? args.now,
|
|
3581
|
-
createdAt: args.now,
|
|
3582
|
-
updatedAt: args.now,
|
|
3583
|
-
};
|
|
3584
|
-
}),
|
|
3585
|
-
);
|
|
4345
|
+
builderEntry,
|
|
4346
|
+
existingSourceValuesJson: existingBuilderRow?.sourceValuesJson,
|
|
4347
|
+
existingLastSourceUpdatedAt:
|
|
4348
|
+
existingBuilderRow?.lastSourceUpdatedAt,
|
|
4349
|
+
}),
|
|
4350
|
+
),
|
|
4351
|
+
provenance:
|
|
4352
|
+
args.sourceType === "builder-cms"
|
|
4353
|
+
? args.builderEntriesByDocumentId?.has(item.document.id)
|
|
4354
|
+
? "Builder CMS read adapter"
|
|
4355
|
+
: BUILDER_CMS_FIXTURE_ROW_PROVENANCE
|
|
4356
|
+
: "mock source row",
|
|
4357
|
+
syncState: "linked",
|
|
4358
|
+
freshness: "fresh",
|
|
4359
|
+
lastSyncedAt: args.now,
|
|
4360
|
+
lastSourceUpdatedAt: builderIdentity?.lastSourceUpdatedAt ?? args.now,
|
|
4361
|
+
createdAt: args.now,
|
|
4362
|
+
updatedAt: args.now,
|
|
4363
|
+
};
|
|
4364
|
+
}),
|
|
4365
|
+
)
|
|
4366
|
+
.onConflictDoNothing();
|
|
3586
4367
|
}
|
|
3587
4368
|
|
|
3588
4369
|
export function sourceValuesForSeededSourceRow(args: {
|
|
@@ -3592,15 +4373,40 @@ export function sourceValuesForSeededSourceRow(args: {
|
|
|
3592
4373
|
now: string;
|
|
3593
4374
|
builderEntry?: BuilderCmsSourceEntry | null;
|
|
3594
4375
|
existingSourceValuesJson?: string | null;
|
|
4376
|
+
existingLastSourceUpdatedAt?: string | null;
|
|
3595
4377
|
}): Record<string, DocumentPropertyValue> {
|
|
3596
4378
|
const existingSourceValues = parseObject<
|
|
3597
4379
|
Record<string, DocumentPropertyValue>
|
|
3598
4380
|
>(args.existingSourceValuesJson);
|
|
3599
4381
|
if (args.builderEntry?.sourceValues) {
|
|
3600
|
-
|
|
4382
|
+
const values = builderSourceValuesWithPreservedBodyBaseline({
|
|
3601
4383
|
incoming: args.builderEntry.sourceValues,
|
|
3602
4384
|
existing: existingSourceValues,
|
|
4385
|
+
existingLastSourceUpdatedAt: args.existingLastSourceUpdatedAt,
|
|
3603
4386
|
});
|
|
4387
|
+
const rawData = args.builderEntry.rawEntry?.data;
|
|
4388
|
+
if (rawData && typeof rawData === "object" && !Array.isArray(rawData)) {
|
|
4389
|
+
for (const [name, rawValue] of Object.entries(rawData)) {
|
|
4390
|
+
if (
|
|
4391
|
+
!rawValue ||
|
|
4392
|
+
typeof rawValue !== "object" ||
|
|
4393
|
+
Array.isArray(rawValue)
|
|
4394
|
+
)
|
|
4395
|
+
continue;
|
|
4396
|
+
const reference = rawValue as Record<string, unknown>;
|
|
4397
|
+
if (
|
|
4398
|
+
reference["@type"] !== "@builder.io/core:Reference" ||
|
|
4399
|
+
typeof reference.id !== "string" ||
|
|
4400
|
+
!reference.id.trim()
|
|
4401
|
+
) {
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
const sourceFieldKey = `data.${name}`;
|
|
4405
|
+
values[builderReferenceIdSourceValueKey(sourceFieldKey)] =
|
|
4406
|
+
reference.id.trim();
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
return values;
|
|
3604
4410
|
}
|
|
3605
4411
|
if (existingSourceValues) return existingSourceValues;
|
|
3606
4412
|
if (args.sourceType !== "builder-cms") return {};
|
|
@@ -3614,6 +4420,7 @@ export function sourceValuesForSeededSourceRow(args: {
|
|
|
3614
4420
|
function builderSourceValuesWithPreservedBodyBaseline(args: {
|
|
3615
4421
|
incoming: Record<string, DocumentPropertyValue>;
|
|
3616
4422
|
existing?: Record<string, DocumentPropertyValue> | null;
|
|
4423
|
+
existingLastSourceUpdatedAt?: string | null;
|
|
3617
4424
|
}) {
|
|
3618
4425
|
const existing = args.existing;
|
|
3619
4426
|
if (!existing) return args.incoming;
|
|
@@ -3633,24 +4440,66 @@ function builderSourceValuesWithPreservedBodyBaseline(args: {
|
|
|
3633
4440
|
existing,
|
|
3634
4441
|
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
3635
4442
|
);
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
(
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
4443
|
+
const incomingLastUpdated =
|
|
4444
|
+
stringSourceValue(args.incoming, "lastUpdated") ?? null;
|
|
4445
|
+
const existingLastUpdated =
|
|
4446
|
+
stringSourceValue(existing, BUILDER_CMS_BODY_LAST_UPDATED_KEY) ??
|
|
4447
|
+
stringSourceValue(existing, "lastUpdated") ??
|
|
4448
|
+
args.existingLastSourceUpdatedAt ??
|
|
4449
|
+
null;
|
|
4450
|
+
const incomingEpoch = builderTimestampEpoch(incomingLastUpdated);
|
|
4451
|
+
const existingEpoch = builderTimestampEpoch(existingLastUpdated);
|
|
4452
|
+
const sameRemoteVersion =
|
|
4453
|
+
incomingEpoch !== null &&
|
|
4454
|
+
existingEpoch !== null &&
|
|
4455
|
+
incomingEpoch === existingEpoch;
|
|
3644
4456
|
const next = { ...args.incoming };
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4457
|
+
const preserveExistingBody =
|
|
4458
|
+
!incomingContent?.trim() &&
|
|
4459
|
+
!!existingContent?.trim() &&
|
|
4460
|
+
(sameRemoteVersion ||
|
|
4461
|
+
!(incomingHash && existingHash && incomingHash !== existingHash));
|
|
4462
|
+
if (preserveExistingBody) {
|
|
4463
|
+
for (const key of [
|
|
4464
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
4465
|
+
BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
|
|
4466
|
+
BUILDER_CMS_BODY_READABLE_MAP_KEY,
|
|
4467
|
+
BUILDER_CMS_BODY_SIDECARS_KEY,
|
|
4468
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
4469
|
+
BUILDER_CMS_BODY_LAST_UPDATED_KEY,
|
|
4470
|
+
]) {
|
|
4471
|
+
if (existing[key] !== undefined) {
|
|
4472
|
+
next[key] = existing[key];
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
if (
|
|
4476
|
+
next[BUILDER_CMS_BODY_LAST_UPDATED_KEY] === undefined &&
|
|
4477
|
+
args.existingLastSourceUpdatedAt
|
|
4478
|
+
) {
|
|
4479
|
+
next[BUILDER_CMS_BODY_LAST_UPDATED_KEY] =
|
|
4480
|
+
args.existingLastSourceUpdatedAt;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
// Required-reference materialization stores the canonical Builder entry id
|
|
4484
|
+
// beside the readable projection. Refresh receives only that projection, so
|
|
4485
|
+
// retain the id while the projected value is unchanged. If the remote label
|
|
4486
|
+
// changes, discard the old id instead of pairing stale identity with it.
|
|
4487
|
+
const referenceIdPrefix = "__agent_native_builder_reference_id:";
|
|
4488
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
4489
|
+
if (!key.startsWith(referenceIdPrefix)) continue;
|
|
4490
|
+
const sourceFieldKey = key.slice(referenceIdPrefix.length);
|
|
4491
|
+
if (
|
|
4492
|
+
stableValueString(existing[sourceFieldKey]) ===
|
|
4493
|
+
stableValueString(args.incoming[sourceFieldKey])
|
|
4494
|
+
) {
|
|
4495
|
+
next[key] = value;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
if (next[BUILDER_CMS_BODY_LAST_UPDATED_KEY] === undefined) {
|
|
4499
|
+
const existingBaselineLastUpdated =
|
|
4500
|
+
existing[BUILDER_CMS_BODY_LAST_UPDATED_KEY] ?? existing.lastUpdated;
|
|
4501
|
+
if (existingBaselineLastUpdated !== undefined) {
|
|
4502
|
+
next[BUILDER_CMS_BODY_LAST_UPDATED_KEY] = existingBaselineLastUpdated;
|
|
3654
4503
|
}
|
|
3655
4504
|
}
|
|
3656
4505
|
return next;
|
|
@@ -4113,13 +4962,19 @@ export function mapBuilderCmsEntriesToLocalItems(args: {
|
|
|
4113
4962
|
sourceTable: args.sourceTable,
|
|
4114
4963
|
now: args.now,
|
|
4115
4964
|
});
|
|
4965
|
+
const exactIdentityMatch = existing
|
|
4966
|
+
? (entriesById.get(existing.sourceRowId) ??
|
|
4967
|
+
entriesByQualifiedId.get(existing.sourceQualifiedId))
|
|
4968
|
+
: null;
|
|
4969
|
+
const allowNaturalKeyFallback =
|
|
4970
|
+
!existing ||
|
|
4971
|
+
builderCmsSourceRowIdentityState({ row: existing }).isSyntheticFixture;
|
|
4116
4972
|
const match =
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
entriesByTitle.get(item.document.title.trim().toLowerCase());
|
|
4973
|
+
exactIdentityMatch ??
|
|
4974
|
+
(allowNaturalKeyFallback
|
|
4975
|
+
? (entriesByUrlPath.get(fixtureEntry.urlPath.toLowerCase()) ??
|
|
4976
|
+
entriesByTitle.get(item.document.title.trim().toLowerCase()))
|
|
4977
|
+
: null);
|
|
4123
4978
|
if (match) entriesByDocumentId.set(item.document.id, match);
|
|
4124
4979
|
}
|
|
4125
4980
|
|
|
@@ -4206,8 +5061,18 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
4206
5061
|
eq(schema.documents.id, schema.contentDatabaseItems.documentId),
|
|
4207
5062
|
)
|
|
4208
5063
|
.where(eq(schema.contentDatabaseItems.databaseId, args.database.id));
|
|
4209
|
-
const
|
|
4210
|
-
|
|
5064
|
+
const representedDocumentIds = new Set(
|
|
5065
|
+
(args.existingSourceRows ?? [])
|
|
5066
|
+
.map((row) => row.documentId)
|
|
5067
|
+
.filter((documentId): documentId is string => Boolean(documentId)),
|
|
5068
|
+
);
|
|
5069
|
+
// Builder entry IDs, not titles, are the remote identity. Preserve every
|
|
5070
|
+
// same-title entry in a collection while retaining the legacy guard against
|
|
5071
|
+
// adopting an already-present local row during source replacement.
|
|
5072
|
+
const existingUnlinkedTitles = new Set(
|
|
5073
|
+
currentItems
|
|
5074
|
+
.filter((row) => !representedDocumentIds.has(row.document.id))
|
|
5075
|
+
.map((row) => row.document.title.trim().toLowerCase()),
|
|
4211
5076
|
);
|
|
4212
5077
|
|
|
4213
5078
|
// Reads MAX(position) for both `documents` and `content_database_items`
|
|
@@ -4256,11 +5121,24 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
4256
5121
|
|
|
4257
5122
|
const title = entry.title.trim() || entry.id;
|
|
4258
5123
|
const titleKey = title.toLowerCase();
|
|
4259
|
-
if (!args.skipTitleDedup &&
|
|
4260
|
-
|
|
5124
|
+
if (!args.skipTitleDedup && existingUnlinkedTitles.has(titleKey)) {
|
|
5125
|
+
continue;
|
|
5126
|
+
}
|
|
4261
5127
|
|
|
4262
|
-
const
|
|
4263
|
-
|
|
5128
|
+
const importIdentity = [
|
|
5129
|
+
args.database.ownerEmail,
|
|
5130
|
+
args.database.id,
|
|
5131
|
+
args.sourceTable,
|
|
5132
|
+
entry.id,
|
|
5133
|
+
];
|
|
5134
|
+
const documentId = stableBuilderImportId(
|
|
5135
|
+
"builder-doc",
|
|
5136
|
+
importIdentity,
|
|
5137
|
+
);
|
|
5138
|
+
const itemId = stableBuilderImportId(
|
|
5139
|
+
"builder-item",
|
|
5140
|
+
importIdentity,
|
|
5141
|
+
);
|
|
4264
5142
|
documentRows.push({
|
|
4265
5143
|
id: documentId,
|
|
4266
5144
|
ownerEmail: args.database.ownerEmail,
|
|
@@ -4295,13 +5173,19 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
4295
5173
|
documentRows,
|
|
4296
5174
|
bulkChunkSizeForColumnCount(13),
|
|
4297
5175
|
)) {
|
|
4298
|
-
await tx
|
|
5176
|
+
await tx
|
|
5177
|
+
.insert(schema.documents)
|
|
5178
|
+
.values(chunk)
|
|
5179
|
+
.onConflictDoNothing();
|
|
4299
5180
|
}
|
|
4300
5181
|
for (const chunk of chunks(
|
|
4301
5182
|
itemRows,
|
|
4302
5183
|
bulkChunkSizeForColumnCount(10),
|
|
4303
5184
|
)) {
|
|
4304
|
-
await tx
|
|
5185
|
+
await tx
|
|
5186
|
+
.insert(schema.contentDatabaseItems)
|
|
5187
|
+
.values(chunk)
|
|
5188
|
+
.onConflictDoNothing();
|
|
4305
5189
|
}
|
|
4306
5190
|
});
|
|
4307
5191
|
|
|
@@ -4345,6 +5229,10 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
4345
5229
|
builderModelFields = await readBuilderCmsModelFields({
|
|
4346
5230
|
model: args.source.sourceTable,
|
|
4347
5231
|
});
|
|
5232
|
+
builderModelFields = mergeBuilderCmsModelFieldsPreservingReferenceModels({
|
|
5233
|
+
existing: sourceMetadata.builderModelFields,
|
|
5234
|
+
refreshed: builderModelFields,
|
|
5235
|
+
});
|
|
4348
5236
|
} catch (error) {
|
|
4349
5237
|
builderModelFieldsReadFailed = true;
|
|
4350
5238
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -5193,6 +6081,34 @@ const SOURCE_OPTION_PALETTE: DocumentPropertyOptionColor[] = [
|
|
|
5193
6081
|
"red",
|
|
5194
6082
|
];
|
|
5195
6083
|
|
|
6084
|
+
export function sourcePropertyOptionsForSources(
|
|
6085
|
+
sources: Array<{ id: string; sourceName: string }>,
|
|
6086
|
+
priorOptions: DocumentPropertyOption[],
|
|
6087
|
+
) {
|
|
6088
|
+
const priorById = new Map(priorOptions.map((option) => [option.id, option]));
|
|
6089
|
+
const retainDescription = (id: string) => {
|
|
6090
|
+
const description = priorById.get(id)?.description;
|
|
6091
|
+
return description ? { description } : {};
|
|
6092
|
+
};
|
|
6093
|
+
return [
|
|
6094
|
+
...sources.map((source, index) => ({
|
|
6095
|
+
id: source.id,
|
|
6096
|
+
name: source.sourceName,
|
|
6097
|
+
color:
|
|
6098
|
+
priorById.get(source.id)?.color ??
|
|
6099
|
+
SOURCE_OPTION_PALETTE[index % SOURCE_OPTION_PALETTE.length],
|
|
6100
|
+
...retainDescription(source.id),
|
|
6101
|
+
})),
|
|
6102
|
+
{
|
|
6103
|
+
id: SOURCE_LOCAL_OPTION_ID,
|
|
6104
|
+
name: "Local",
|
|
6105
|
+
color: (priorById.get(SOURCE_LOCAL_OPTION_ID)?.color ??
|
|
6106
|
+
"gray") as DocumentPropertyOptionColor,
|
|
6107
|
+
...retainDescription(SOURCE_LOCAL_OPTION_ID),
|
|
6108
|
+
},
|
|
6109
|
+
];
|
|
6110
|
+
}
|
|
6111
|
+
|
|
5196
6112
|
/**
|
|
5197
6113
|
* Ensure a "Source" select property exists tagging each row with the collection
|
|
5198
6114
|
* it belongs to, and (re)set every item's value. Rows with no source binding are
|
|
@@ -5233,22 +6149,7 @@ export async function ensureDatabaseSourceProperty(args: {
|
|
|
5233
6149
|
// that can't collide with a UUID source id). Resolving a row's tag back to a
|
|
5234
6150
|
// source is then pure id matching — no source-name hop — so duplicate display
|
|
5235
6151
|
// names or a collection literally named "Local" can never misroute a row.
|
|
5236
|
-
const
|
|
5237
|
-
const options = [
|
|
5238
|
-
...sources.map((source, index) => ({
|
|
5239
|
-
id: source.id,
|
|
5240
|
-
name: source.sourceName,
|
|
5241
|
-
color:
|
|
5242
|
-
priorById.get(source.id)?.color ??
|
|
5243
|
-
SOURCE_OPTION_PALETTE[index % SOURCE_OPTION_PALETTE.length],
|
|
5244
|
-
})),
|
|
5245
|
-
{
|
|
5246
|
-
id: SOURCE_LOCAL_OPTION_ID,
|
|
5247
|
-
name: "Local",
|
|
5248
|
-
color: (priorById.get(SOURCE_LOCAL_OPTION_ID)?.color ??
|
|
5249
|
-
"gray") as DocumentPropertyOptionColor,
|
|
5250
|
-
},
|
|
5251
|
-
];
|
|
6152
|
+
const options = sourcePropertyOptionsForSources(sources, priorOptions);
|
|
5252
6153
|
const optionsJson = serializePropertyOptions({ options });
|
|
5253
6154
|
|
|
5254
6155
|
let propertyId: string;
|