@growth-labs/cms 0.8.33 → 0.8.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/engine/content-metadata.d.ts +1 -0
  2. package/dist/engine/content-metadata.d.ts.map +1 -1
  3. package/dist/engine/content-metadata.js +32 -0
  4. package/dist/engine/content-metadata.js.map +1 -1
  5. package/dist/engine/publisher.d.ts +4 -0
  6. package/dist/engine/publisher.d.ts.map +1 -1
  7. package/dist/engine/publisher.js +25 -9
  8. package/dist/engine/publisher.js.map +1 -1
  9. package/dist/foundry-emit.d.ts +11 -0
  10. package/dist/foundry-emit.d.ts.map +1 -1
  11. package/dist/foundry-emit.js +3 -0
  12. package/dist/foundry-emit.js.map +1 -1
  13. package/dist/routes/content-insights.d.ts.map +1 -1
  14. package/dist/routes/content-insights.js +76 -1
  15. package/dist/routes/content-insights.js.map +1 -1
  16. package/dist/routes/content.d.ts +2 -0
  17. package/dist/routes/content.d.ts.map +1 -1
  18. package/dist/routes/content.js +4 -0
  19. package/dist/routes/content.js.map +1 -1
  20. package/dist/routes/foundry-publish.d.ts.map +1 -1
  21. package/dist/routes/foundry-publish.js +100 -8
  22. package/dist/routes/foundry-publish.js.map +1 -1
  23. package/dist/ui/editor/ContentForm.d.ts.map +1 -1
  24. package/dist/ui/editor/ContentForm.js +7 -9
  25. package/dist/ui/editor/ContentForm.js.map +1 -1
  26. package/dist/ui/editor/content-payload.d.ts +5 -0
  27. package/dist/ui/editor/content-payload.d.ts.map +1 -1
  28. package/dist/ui/editor/content-payload.js +13 -0
  29. package/dist/ui/editor/content-payload.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/engine/content-metadata.ts +35 -0
  32. package/src/engine/publisher.ts +33 -7
  33. package/src/foundry-emit.ts +3 -0
  34. package/src/routes/content-insights.ts +79 -1
  35. package/src/routes/content.ts +6 -0
  36. package/src/routes/foundry-publish.ts +115 -7
  37. package/src/ui/editor/ContentForm.tsx +8 -13
  38. package/src/ui/editor/content-payload.ts +17 -0
@@ -49,6 +49,11 @@ export declare function canCreateContentDraft(contentType: ContentType, draft: C
49
49
  requirePrimaryTopic?: boolean;
50
50
  }): boolean;
51
51
  export declare function foundryDispatchSourceForDraft(contentType: ContentType, draft: ContentDraftFields): string | null;
52
+ /** Explain the same prerequisites that gate the first save, using the host's field label. */
53
+ export declare function contentDraftSaveError(contentType: ContentType, draft: ContentDraftFields, requirements?: {
54
+ requirePrimaryTopic?: boolean;
55
+ primaryTopicLabel?: string;
56
+ }): string | null;
52
57
  export declare function inferVideoSourceKind(sourceUrl: string): 'dropbox' | 'http_url';
53
58
  export declare function buildContentUpdatePayload(contentType: ContentType, draft: ContentDraftFields): Record<string, unknown>;
54
59
  export declare function buildContentCreatePayload(contentType: ContentType, draft: ContentDraftFields): Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"content-payload.d.ts","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"content-payload.d.ts","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
@@ -99,6 +99,19 @@ export function foundryDispatchSourceForDraft(contentType, draft) {
99
99
  }
100
100
  return null;
101
101
  }
102
+ /** Explain the same prerequisites that gate the first save, using the host's field label. */
103
+ export function contentDraftSaveError(contentType, draft, requirements = {}) {
104
+ if (canCreateContentDraft(contentType, draft, requirements))
105
+ return null;
106
+ if (requirements.requirePrimaryTopic && !draft.primaryTopic?.trim()) {
107
+ return `Select ${requirements.primaryTopicLabel || 'a primary topic'} before saving.`;
108
+ }
109
+ const mediaSource = foundryDispatchSourceForDraft(contentType, draft);
110
+ if (mediaSource && normalizeMediaDurationSeconds(draft.durationSeconds) === null) {
111
+ return 'Enter the authoritative media duration in whole seconds before saving.';
112
+ }
113
+ return 'Add the required title and content before saving.';
114
+ }
102
115
  export function inferVideoSourceKind(sourceUrl) {
103
116
  try {
104
117
  const { hostname } = new URL(sourceUrl);
@@ -1 +1 @@
1
- {"version":3,"file":"content-payload.js","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"content-payload.js","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/cms",
3
- "version": "0.8.33",
3
+ "version": "0.8.35",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "growth-labs-cms-migrations": "./dist/cli/migrations.js",
@@ -137,3 +137,38 @@ export const ContentMetadataSchema = z
137
137
  }
138
138
  })
139
139
  .transform((metadata) => metadata as ContentMetadata)
140
+
141
+ // A prepared improvement supplies partial credits. Keep recorded names and
142
+ // their provenance when preparation has no new credit, rather than replacing
143
+ // the entire source record with UNKNOWN fields.
144
+ export function mergePinnedArticleMetadata(
145
+ recorded: ContentMetadata,
146
+ incoming: ContentMetadata,
147
+ ): ContentMetadata {
148
+ const merged = { ...recorded, ...incoming }
149
+ const isObject = (value: ContentMetadataValue | undefined): value is ContentMetadata =>
150
+ value !== null && typeof value === 'object' && !Array.isArray(value)
151
+ if (!isObject(recorded.credits) || !isObject(incoming.credits)) return merged
152
+ const prior = recorded.credits
153
+ const updates = incoming.credits
154
+ const credits = { ...prior, ...updates }
155
+ for (const role of ['originalWriter', 'presenter', 'editor']) {
156
+ const statusKey = `${role}Status`
157
+ const name = updates[role]
158
+ const status = updates[statusKey]
159
+ const missingName = name === undefined || (typeof name === 'string' && name.trim() === '')
160
+ if (
161
+ prior[role] !== undefined &&
162
+ (status === 'UNKNOWN' || (missingName && status === undefined))
163
+ ) {
164
+ credits[role] = prior[role]
165
+ if (prior[statusKey] !== undefined) credits[statusKey] = prior[statusKey]
166
+ else delete credits[statusKey]
167
+ } else if (!missingName && name !== prior[role] && status === undefined) {
168
+ // A different name cannot inherit the previous person's verification.
169
+ credits[statusKey] = 'UNKNOWN'
170
+ }
171
+ }
172
+ merged.credits = credits
173
+ return merged
174
+ }
@@ -343,6 +343,8 @@ export interface UpdateContentInput extends Partial<BaseContentInput> {
343
343
  export interface UpdateContentOptions {
344
344
  /** Atomically apply the update only while the content row has this status. */
345
345
  requiredStatus?: ContentStatus
346
+ /** Pin a machine body update to the canonical version it inspected. Requires requiredStatus. */
347
+ expectedCanonicalVersion?: number
346
348
  /** Apply a resolved media source only while this exact source identity is still stored. */
347
349
  expectedMediaSource?: {
348
350
  sourceUrl: string | null
@@ -354,6 +356,8 @@ export interface UpdateContentResult {
354
356
  slug: string
355
357
  /** Present when requiredStatus lost a race before the mutation transaction. */
356
358
  statusConflict?: string
359
+ /** A newer canonical edit won before the atomic update. */
360
+ canonicalVersionConflict?: boolean
357
361
  /** Present when a newer media-source update won while this request was resolving. */
358
362
  mediaSourceConflict?: boolean
359
363
  }
@@ -811,6 +815,14 @@ export async function updateContentItem(
811
815
  input: UpdateContentInput,
812
816
  options: UpdateContentOptions = {},
813
817
  ): Promise<UpdateContentResult | null> {
818
+ if (
819
+ options.expectedCanonicalVersion !== undefined &&
820
+ (!options.requiredStatus ||
821
+ !Number.isSafeInteger(options.expectedCanonicalVersion) ||
822
+ options.expectedCanonicalVersion < 1)
823
+ ) {
824
+ throw new TypeError('Canonical-version updates require a valid version and status constraint')
825
+ }
814
826
  const tagPairs = input.tags === undefined ? null : validateDeclaredTagList(input.tags)
815
827
  const existing = await getContentItem(db, id)
816
828
  if (!existing) return null
@@ -1047,6 +1059,10 @@ export async function updateContentItem(
1047
1059
  // D1 batch is a transaction: the status predicate and every content-table
1048
1060
  // write linearize together, so a native publish/schedule cannot interleave
1049
1061
  // between the base row and its body row.
1062
+ const versionPredicate =
1063
+ options.expectedCanonicalVersion === undefined ? '' : ' AND canonical_version = ?'
1064
+ const versionBindings =
1065
+ options.expectedCanonicalVersion === undefined ? [] : [options.expectedCanonicalVersion]
1050
1066
  const statements = [
1051
1067
  db
1052
1068
  .prepare(
@@ -1056,7 +1072,7 @@ export async function updateContentItem(
1056
1072
  hero_image_id = ?, hero_image_alt = ?, hero_image_caption = ?, social_image_id = ?,
1057
1073
  canonical_url = ?, ai_locked_fields = ?, seo_focus_keyword = ?, metadata_json = ?,
1058
1074
  updated_at = unixepoch(), canonical_version = canonical_version + 1
1059
- WHERE id = ? AND status = ? AND deleted_at IS NULL
1075
+ WHERE id = ? AND status = ? AND deleted_at IS NULL${versionPredicate}
1060
1076
  RETURNING slug`,
1061
1077
  )
1062
1078
  .bind(
@@ -1081,6 +1097,7 @@ export async function updateContentItem(
1081
1097
  metadataJson,
1082
1098
  id,
1083
1099
  options.requiredStatus,
1100
+ ...versionBindings,
1084
1101
  ),
1085
1102
  ]
1086
1103
 
@@ -1137,7 +1154,7 @@ export async function updateContentItem(
1137
1154
  WHERE content_id = ?
1138
1155
  AND EXISTS (
1139
1156
  SELECT 1 FROM content_items
1140
- WHERE id = ? AND status = ? AND deleted_at IS NULL
1157
+ WHERE id = ? AND status = ? AND deleted_at IS NULL${versionPredicate}
1141
1158
  )
1142
1159
  RETURNING content_id`,
1143
1160
  )
@@ -1167,6 +1184,7 @@ export async function updateContentItem(
1167
1184
  id,
1168
1185
  id,
1169
1186
  options.requiredStatus,
1187
+ ...versionBindings,
1170
1188
  ),
1171
1189
  )
1172
1190
  }
@@ -1178,11 +1196,11 @@ export async function updateContentItem(
1178
1196
  // up front even when the guarded write below is refused.
1179
1197
  const tagRows = tagPairs.length > 0 ? await ensureTags(db, tagPairs) : []
1180
1198
  const statusGuard = `EXISTS (SELECT 1 FROM content_items
1181
- WHERE id = ? AND status = ? AND deleted_at IS NULL)`
1199
+ WHERE id = ? AND status = ? AND deleted_at IS NULL${versionPredicate})`
1182
1200
  statements.push(
1183
1201
  db
1184
1202
  .prepare(`DELETE FROM content_tag_links WHERE content_id = ? AND ${statusGuard}`)
1185
- .bind(id, id, options.requiredStatus),
1203
+ .bind(id, id, options.requiredStatus, ...versionBindings),
1186
1204
  )
1187
1205
  for (const tag of tagRows) {
1188
1206
  statements.push(
@@ -1191,11 +1209,16 @@ export async function updateContentItem(
1191
1209
  `INSERT INTO content_tag_links (content_id, tag_id, label)
1192
1210
  SELECT ?, ?, ? WHERE ${statusGuard}`,
1193
1211
  )
1194
- .bind(id, tag.id, tag.label, id, options.requiredStatus),
1212
+ .bind(id, tag.id, tag.label, id, options.requiredStatus, ...versionBindings),
1195
1213
  )
1196
1214
  }
1197
1215
  }
1198
1216
 
1217
+ // D1 batch is atomic. Keep the base row's version unchanged while every
1218
+ // dependent body/tag write checks it; advance it only as the final write.
1219
+ const baseStatement = statements.shift()!
1220
+ const baseResultIndex = statements.length
1221
+ statements.push(baseStatement)
1199
1222
  const statusResultIndex = statements.length
1200
1223
  statements.push(
1201
1224
  db
@@ -1204,10 +1227,13 @@ export async function updateContentItem(
1204
1227
  )
1205
1228
  const results = await db.batch(statements)
1206
1229
  const baseChanged =
1207
- (results[0]?.results?.length ?? 0) > 0 ||
1208
- (results[0]?.meta as { changes?: number } | undefined)?.changes === 1
1230
+ (results[baseResultIndex]?.results?.length ?? 0) > 0 ||
1231
+ (results[baseResultIndex]?.meta as { changes?: number } | undefined)?.changes === 1
1209
1232
  if (!baseChanged) {
1210
1233
  const status = results[statusResultIndex]?.results?.[0]?.status
1234
+ if (status === options.requiredStatus && options.expectedCanonicalVersion !== undefined) {
1235
+ return { slug: existing.slug, canonicalVersionConflict: true }
1236
+ }
1211
1237
  return typeof status === 'string' ? { slug: existing.slug, statusConflict: status } : null
1212
1238
  }
1213
1239
  return { slug }
@@ -35,6 +35,9 @@ const HERO_IMAGE_KEY = /^[A-Za-z0-9][A-Za-z0-9._\-/]*$/
35
35
  export const FoundryArticleV2Schema = z
36
36
  .object({
37
37
  schemaVersion: z.literal(FOUNDRY_ARTICLE_SCHEMA_VERSION),
38
+ createOnly: z.literal(true).optional(),
39
+ /** Actual CMS source revision pinned by a strategy-led improvement. */
40
+ expectedRevisionId: z.string().uuid().optional(),
38
41
  slug: z.string().refine(isCanonicalSlug, {
39
42
  message: 'slug must already be canonical: lowercase a-z, 0-9 and single hyphens, 3-512 chars',
40
43
  }),
@@ -11,7 +11,11 @@ import {
11
11
  undismissInsight,
12
12
  } from '../engine/content-insights.js'
13
13
  import { verifyHmac } from '../engine/foundry-dispatch.js'
14
- import { listPublishedContent, PublishedContentReadError } from '../engine/published-content.js'
14
+ import {
15
+ getPublishedContentBySlug,
16
+ listPublishedContent,
17
+ PublishedContentReadError,
18
+ } from '../engine/published-content.js'
15
19
  import { InsightsIngestPayloadSchema } from '../schema/insights-ingest.js'
16
20
  import { InsightDismissalReasonCodeSchema } from '../schema/types.js'
17
21
  import type { CmsRouteConfig } from './config.js'
@@ -56,6 +60,22 @@ const PublishedArticleStateSchema = z
56
60
  })
57
61
  .strict()
58
62
 
63
+ const PublishedArticleSourceSchema = z
64
+ .object({
65
+ ok: z.literal(true),
66
+ schema: z.literal('cms.published-article-source.v1'),
67
+ site: z.string().min(1),
68
+ observed_at: z.string().datetime(),
69
+ article: PublishedArticleStateSchema.extend({
70
+ hero_image_url: z.string().url().nullable(),
71
+ body_markdown: z.string().min(1).max(1_000_000),
72
+ description: z.string().nullable(),
73
+ byline: z.string().nullable(),
74
+ tags: z.array(z.string()),
75
+ }).strict(),
76
+ })
77
+ .strict()
78
+
59
79
  const PublishedInputsResponseSchema = z
60
80
  .object({
61
81
  ok: z.literal(true),
@@ -76,6 +96,64 @@ async function publishedInputs(ctx: RouteContext, url: URL, siteId: string): Pro
76
96
  return json({ error: 'invalid_published_mode' }, 400)
77
97
  if (siteId === 'default') return json({ error: 'publication_identity_not_configured' }, 503)
78
98
  if (url.searchParams.get('site') !== siteId) return json({ error: 'publication_mismatch' }, 403)
99
+ const articleSlug = url.searchParams.get('article')
100
+ if (articleSlug !== null) {
101
+ const slug = z
102
+ .string()
103
+ .min(1)
104
+ .max(512)
105
+ .regex(/^[a-z0-9][a-z0-9-]*$/)
106
+ .safeParse(articleSlug)
107
+ if (!slug.success || url.searchParams.has('cursor') || url.searchParams.has('limit'))
108
+ return json({ error: 'invalid_article_source_request' }, 400)
109
+ try {
110
+ const content = await getPublishedContentBySlug(ctx.db, slug.data)
111
+ if (!content || content.type !== 'article')
112
+ return json({ error: 'published_article_missing' }, 404)
113
+ const item = content.payload.item
114
+ let heroImageUrl: string | null = null
115
+ if (item.hero_image_id) {
116
+ const asset = await ctx.db
117
+ .prepare(
118
+ "SELECT public_url FROM media_assets WHERE id = ? AND asset_type = 'image' AND status = 'ready'",
119
+ )
120
+ .bind(item.hero_image_id)
121
+ .first<{ public_url: string | null }>()
122
+ const publicUrl = z.string().url().safeParse(asset?.public_url)
123
+ if (publicUrl.success) heroImageUrl = publicUrl.data
124
+ }
125
+ const result = PublishedArticleSourceSchema.safeParse({
126
+ ok: true,
127
+ schema: 'cms.published-article-source.v1',
128
+ site: siteId,
129
+ observed_at: new Date().toISOString(),
130
+ article: {
131
+ content_id: content.contentId,
132
+ revision_id: content.revisionId,
133
+ slug: content.slug,
134
+ title: item.title,
135
+ canonical_url: item.canonical_url ?? null,
136
+ published_at: content.publishedAt,
137
+ source_id: item.source_id ?? null,
138
+ youtube_video_id: content.metadata.videoId ?? null,
139
+ hero_image_id: item.hero_image_id ?? null,
140
+ hero_image_url: heroImageUrl,
141
+ hero_image_caption: item.hero_image_caption ?? null,
142
+ body_markdown: content.payload.content?.body_markdown,
143
+ description: item.description ?? null,
144
+ byline: item.byline ?? null,
145
+ tags: content.payload.tags,
146
+ },
147
+ })
148
+ if (!result.success) return json({ error: 'published_article_source_invalid' }, 409)
149
+ if (new TextEncoder().encode(JSON.stringify(result.data)).byteLength > 4 << 20)
150
+ return json({ error: 'published_article_source_too_large' }, 409)
151
+ return json(result.data)
152
+ } catch (error) {
153
+ if (error instanceof PublishedContentReadError) return json({ error: error.code }, 409)
154
+ throw error
155
+ }
156
+ }
79
157
  const limit = z.coerce
80
158
  .number()
81
159
  .int()
@@ -235,6 +235,8 @@ export interface ContentRouteConfig extends CmsRouteConfig {
235
235
  videoSourceProductionEnabled?: boolean
236
236
  /** Internal machine-route constraint: atomically update only in this status. */
237
237
  updateStatusConstraint?: ContentStatus
238
+ /** Internal machine-route pin; joins the existing status-constrained atomic batch. */
239
+ updateCanonicalVersionConstraint?: number
238
240
  /**
239
241
  * Internal (Foundry machine route, M3-18): replaces ONLY the publish action's
240
242
  * commit step (publishContent + createRevision) with the caller's — publishOne
@@ -1807,6 +1809,7 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
1807
1809
  try {
1808
1810
  updated = await updateContentItem(ctx.db, id, parsed.data, {
1809
1811
  requiredStatus: config.updateStatusConstraint,
1812
+ expectedCanonicalVersion: config.updateCanonicalVersionConstraint,
1810
1813
  ...(expectedMediaSource ? { expectedMediaSource } : {}),
1811
1814
  })
1812
1815
  } catch (error) {
@@ -1821,6 +1824,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
1821
1824
  if (updated.statusConflict) {
1822
1825
  return json({ error: 'update_status_conflict', state: updated.statusConflict }, 409)
1823
1826
  }
1827
+ if (updated.canonicalVersionConflict) {
1828
+ return json({ error: 'update_canonical_conflict' }, 409)
1829
+ }
1824
1830
  if (updated.mediaSourceConflict) {
1825
1831
  return json({ error: 'media_source_conflict' }, 409)
1826
1832
  }
@@ -1,5 +1,6 @@
1
1
  import { isCanonicalHost, sha256Hex, verifyFoundryRequest } from '@growth-labs/foundry-signing'
2
2
  import { z } from 'zod'
3
+ import { ContentMetadataSchema, mergePinnedArticleMetadata } from '../engine/content-metadata.js'
3
4
  import { type PublicationReconciler, publishOne } from '../engine/publication.js'
4
5
  import { PublicationCurrentSnapshotError } from '../engine/publication-current-guard.js'
5
6
  import { PublishContentValidationError } from '../engine/publisher.js'
@@ -263,6 +264,9 @@ async function mapUpdateConflict(response: Response, id: string): Promise<Respon
263
264
  if (response.status !== 409) return response
264
265
  try {
265
266
  const payload = (await response.clone().json()) as { error?: unknown; state?: unknown }
267
+ if (payload.error === 'update_canonical_conflict') {
268
+ return json({ error: 'human_edit_pending', external_id: id }, 409)
269
+ }
266
270
  if (payload.error === 'update_status_conflict' && typeof payload.state === 'string') {
267
271
  return approvalConflict(id, payload.state)
268
272
  }
@@ -470,6 +474,7 @@ async function bindAdoption(
470
474
  config: ContentRouteConfig,
471
475
  id: string,
472
476
  candidate: ContentCandidate,
477
+ expectedVersion?: number,
473
478
  ): Promise<boolean> {
474
479
  const sourceId = `${SOURCE_PREFIX}${id}`
475
480
  await ctx.db.batch([
@@ -477,9 +482,17 @@ async function bindAdoption(
477
482
  .prepare(
478
483
  `UPDATE content_items SET source_id = ?, updated_at = unixepoch()
479
484
  WHERE id = ? AND deleted_at IS NULL AND type = 'article' AND source_id = ?
480
- AND NOT EXISTS (SELECT 1 FROM cms_foundry_articles WHERE content_id = ?)`,
485
+ AND NOT EXISTS (SELECT 1 FROM cms_foundry_articles WHERE content_id = ?)
486
+ AND (? IS NULL OR canonical_version = ?)`,
481
487
  )
482
- .bind(sourceId, candidate.id, candidate.source_id, candidate.id),
488
+ .bind(
489
+ sourceId,
490
+ candidate.id,
491
+ candidate.source_id,
492
+ candidate.id,
493
+ expectedVersion ?? null,
494
+ expectedVersion ?? null,
495
+ ),
483
496
  ctx.db
484
497
  .prepare(
485
498
  `INSERT INTO cms_activity_log (id, workspace_id, user_id, action, target_id, target_title, type)
@@ -518,6 +531,27 @@ async function canonicalVersion(ctx: RouteContext, contentId: string): Promise<n
518
531
  return row?.canonical_version ?? null
519
532
  }
520
533
 
534
+ /** A signed improvement names the actual immutable CMS revision it read. */
535
+ async function sourceRevisionVersion(
536
+ ctx: RouteContext,
537
+ contentId: string,
538
+ revisionId: string,
539
+ ): Promise<number | null> {
540
+ const revision = await getRevision(ctx.db, contentId, revisionId)
541
+ if (!revision) return null
542
+ const item = revision.payload.item as Record<string, unknown> | undefined
543
+ const version = item?.canonical_version
544
+ if (
545
+ item?.id !== contentId ||
546
+ item.type !== 'article' ||
547
+ typeof version !== 'number' ||
548
+ !Number.isSafeInteger(version) ||
549
+ version < 1
550
+ )
551
+ return null
552
+ return (await canonicalVersion(ctx, contentId)) === version ? version : null
553
+ }
554
+
521
555
  /**
522
556
  * Record what this v2 PUT wrote as a History revision (the autosave/restore
523
557
  * mechanism; it never moves the published pointer), created_by `foundry` and
@@ -595,6 +629,12 @@ async function putV2(
595
629
  const body = parsed.data
596
630
 
597
631
  const prior = await findArticle(ctx, id)
632
+ if (body.createOnly && prior && (prior.request_sha256 !== bodyHash || prior.status !== 'draft')) {
633
+ return json(
634
+ { error: 'article_already_exists', external_id: id, content_id: prior.content_id },
635
+ 409,
636
+ )
637
+ }
598
638
  if (prior?.request_sha256 === bodyHash) {
599
639
  console.info('[cms] foundry article request deduplicated', {
600
640
  externalId: id,
@@ -610,8 +650,41 @@ async function putV2(
610
650
  // Scheduled, review and archived items stay human-owned.
611
651
  if (bound && !V2_WRITABLE.has(bound.status)) return approvalConflict(id, bound.status)
612
652
 
653
+ if (body.createOnly) {
654
+ if (body.expectedRevisionId) return json({ error: 'create_only_has_source_revision' }, 400)
655
+ if (body.adoptSourceId || bound)
656
+ return json({ error: 'article_already_exists', external_id: id }, 409)
657
+ const slugHolder = await findCandidate(ctx, 'slug', body.slug)
658
+ if (slugHolder) return slugConflict(id, body.slug, slugHolder.id)
659
+ const videoId = body.metadata?.videoId
660
+ if (typeof videoId !== 'string' || !/^[A-Za-z0-9_-]{11}$/.test(videoId))
661
+ return json({ error: 'create_only_requires_source_video' }, 400)
662
+ // Check the actual CMS working and immutable published projections, not
663
+ // just Foundry's own relationship ledger. A creation cannot adopt or edit.
664
+ const associated = await ctx.db
665
+ .prepare(`SELECT c.id FROM content_items c
666
+ LEFT JOIN content_revisions r ON r.id=c.published_revision_id AND r.content_id=c.id
667
+ WHERE c.type='article' AND c.deleted_at IS NULL AND (
668
+ CASE WHEN json_valid(c.metadata_json) THEN json_extract(c.metadata_json,'$.videoId') END = ?
669
+ OR CASE WHEN json_valid(json_extract(r.payload_json,'$.item.metadata_json')) THEN json_extract(json_extract(r.payload_json,'$.item.metadata_json'),'$.videoId') END = ?
670
+ ) LIMIT 1`)
671
+ .bind(videoId, videoId)
672
+ .first<{ id: string }>()
673
+ if (associated)
674
+ return json(
675
+ { error: 'article_video_already_exists', external_id: id, content_id: associated.id },
676
+ 409,
677
+ )
678
+ }
613
679
  let adoption: Adoption | null = null
680
+ let expectedSourceVersion: number | undefined
614
681
  if (bound) {
682
+ if (body.expectedRevisionId) {
683
+ const version = await sourceRevisionVersion(ctx, bound.id, body.expectedRevisionId)
684
+ if (version === null) return json({ error: 'human_edit_pending', external_id: id }, 409)
685
+ expectedSourceVersion = version
686
+ }
687
+
615
688
  const holder = await findCandidate(ctx, 'slug', body.slug)
616
689
  if (holder && holder.id !== bound.id) return slugConflict(id, body.slug, holder.id)
617
690
  } else {
@@ -619,7 +692,12 @@ async function putV2(
619
692
  if ('error' in resolved) return resolved.error
620
693
  if (resolved.candidate && resolved.matchedBy) {
621
694
  const candidate = resolved.candidate
622
- if (!(await bindAdoption(ctx, config, id, candidate))) {
695
+ if (body.expectedRevisionId) {
696
+ const version = await sourceRevisionVersion(ctx, candidate.id, body.expectedRevisionId)
697
+ if (version === null) return json({ error: 'human_edit_pending', external_id: id }, 409)
698
+ expectedSourceVersion = version
699
+ }
700
+ if (!(await bindAdoption(ctx, config, id, candidate, expectedSourceVersion))) {
623
701
  return json(
624
702
  {
625
703
  error: 'adoption_conflict',
@@ -656,11 +734,34 @@ async function putV2(
656
734
  }
657
735
  }
658
736
 
659
- // The canonical version Foundry's write must produce is one over this.
660
- const priorVersion = bound ? await canonicalVersion(ctx, bound.id) : null
737
+ // Preserve a newer human working copy before PUT, not merely at publication.
738
+ // The native update pins this same canonical version in its atomic batch.
739
+ if (body.expectedRevisionId && !bound)
740
+ return json({ error: 'source_revision_missing', external_id: id }, 409)
741
+ const priorVersion =
742
+ expectedSourceVersion ?? (bound ? await canonicalVersion(ctx, bound.id) : null)
661
743
  const heroImageId = body.hero_image_url
662
744
  ? await heroAssetId(body.hero_image_url)
663
745
  : (body.hero_image_key ?? undefined)
746
+ // A pinned improvement augments its exact source metadata. Existing dates,
747
+ // image provenance and other recorded fields survive preparation updates.
748
+ let metadata = body.metadata
749
+ if (body.expectedRevisionId && body.metadata !== undefined && bound) {
750
+ const source = await getRevision(ctx.db, bound.id, body.expectedRevisionId)
751
+ let original: unknown
752
+ try {
753
+ original = JSON.parse(String(source?.payload.item?.metadata_json ?? '{}'))
754
+ } catch {
755
+ return json({ error: 'source_metadata_invalid', external_id: id }, 409)
756
+ }
757
+ const recorded = ContentMetadataSchema.safeParse(original)
758
+ if (!recorded.success) return json({ error: 'source_metadata_invalid', external_id: id }, 409)
759
+ const merged = ContentMetadataSchema.safeParse(
760
+ mergePinnedArticleMetadata(recorded.data, body.metadata),
761
+ )
762
+ if (!merged.success) return json({ error: 'source_metadata_invalid', external_id: id }, 409)
763
+ metadata = merged.data
764
+ }
664
765
  const nativeBody = {
665
766
  title: body.title,
666
767
  slug: body.slug,
@@ -672,7 +773,7 @@ async function putV2(
672
773
  ...(body.desk !== undefined ? { primaryCategory: body.desk } : {}),
673
774
  ...(body.tags !== undefined ? { tags: body.tags } : {}),
674
775
  ...(body.visibility !== undefined ? { visibility: body.visibility } : {}),
675
- ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
776
+ ...(metadata !== undefined ? { metadata } : {}),
676
777
  content: { bodyMarkdown: body.body_html, bodyHtml: body.body_html },
677
778
  }
678
779
 
@@ -681,7 +782,12 @@ async function putV2(
681
782
  // Each write is a CAS on the status it was read in: a draft stays a
682
783
  // draft write, and a published item is edited in place only while it is
683
784
  // still published (its live revision changes only on POST /publish).
684
- const writer = bound.status === 'published' ? handlers.published : handlers.draft
785
+ if (priorVersion === null) return json({ error: 'human_edit_pending', external_id: id }, 409)
786
+ const writer = createContentRoutes({
787
+ ...machineConfig(config),
788
+ updateStatusConstraint: bound.status,
789
+ updateCanonicalVersionConstraint: priorVersion,
790
+ })
685
791
  nativeResponse = await mapUpdateConflict(
686
792
  await writer.update(nativeContext(ctx, 'PATCH', nativeBody, { id: bound.id })),
687
793
  id,
@@ -698,6 +804,8 @@ async function putV2(
698
804
  )
699
805
  } catch (error) {
700
806
  const raced = await findContentBySource(ctx, id)
807
+ if (body.createOnly && raced)
808
+ return json({ error: 'article_already_exists', external_id: id, content_id: raced.id }, 409)
701
809
  if (raced?.type !== 'article') throw error
702
810
  nativeResponse = await mapUpdateConflict(
703
811
  await handlers.draft.update(nativeContext(ctx, 'PATCH', nativeBody, { id: raced.id })),
@@ -33,7 +33,7 @@ import {
33
33
  applyMediaSourceReadiness,
34
34
  buildContentCreatePayload,
35
35
  buildContentUpdatePayload,
36
- canCreateContentDraft,
36
+ contentDraftSaveError,
37
37
  foundryDispatchSourceForDraft,
38
38
  initialContentMetadata,
39
39
  type MediaSourceReadinessPayload,
@@ -406,19 +406,14 @@ export function ContentForm({
406
406
  const draft = currentForm
407
407
 
408
408
  if (id === null) {
409
- if (
410
- !canCreateContentDraft(contentType, draft, {
411
- requirePrimaryTopic: primaryTopicEnabled,
412
- })
413
- ) {
414
- const mediaSource = foundryDispatchSourceForDraft(contentType, draft)
415
- const missingMediaDuration =
416
- mediaSource && normalizeMediaDurationSeconds(draft.durationSeconds) === null
409
+ const saveError = contentDraftSaveError(contentType, draft, {
410
+ requirePrimaryTopic: primaryTopicEnabled,
411
+ primaryTopicLabel: primaryTopic?.label,
412
+ })
413
+ if (saveError) {
417
414
  setForm((prev) => ({
418
415
  ...prev,
419
- saveError: missingMediaDuration
420
- ? 'Enter the authoritative media duration in whole seconds before saving.'
421
- : 'Add the required title and content before saving.',
416
+ saveError,
422
417
  }))
423
418
  return
424
419
  }
@@ -543,7 +538,7 @@ export function ContentForm({
543
538
  }
544
539
  }
545
540
  },
546
- [contentType, onDocCreated, primaryTopicEnabled, queueFoundryProcessing],
541
+ [contentType, onDocCreated, primaryTopicEnabled, primaryTopic?.label, queueFoundryProcessing],
547
542
  )
548
543
 
549
544
  useEffect(() => {