@agent-native/creative-context 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/agent-native.package.json +11 -0
  2. package/dist/actions/continue-context-import.d.ts +16 -1
  3. package/dist/actions/continue-context-import.js +4 -1
  4. package/dist/actions/continue-context-import.js.map +1 -1
  5. package/dist/actions/get-creative-context.d.ts +38 -0
  6. package/dist/actions/get-creative-context.d.ts.map +1 -1
  7. package/dist/actions/get-creative-context.js +59 -23
  8. package/dist/actions/get-creative-context.js.map +1 -1
  9. package/dist/actions/get-google-picker-session.d.ts +10 -0
  10. package/dist/actions/get-google-picker-session.d.ts.map +1 -0
  11. package/dist/actions/get-google-picker-session.js +38 -0
  12. package/dist/actions/get-google-picker-session.js.map +1 -0
  13. package/dist/actions/index.d.ts.map +1 -1
  14. package/dist/actions/index.js +2 -0
  15. package/dist/actions/index.js.map +1 -1
  16. package/dist/actions/start-context-enrichment.d.ts +16 -1
  17. package/dist/actions/start-context-enrichment.js +6 -4
  18. package/dist/actions/start-context-enrichment.js.map +1 -1
  19. package/dist/actions/start-context-import.d.ts +16 -1
  20. package/dist/actions/start-context-import.js +2 -1
  21. package/dist/actions/start-context-import.js.map +1 -1
  22. package/dist/client/CreativeContextPanel.d.ts +1 -59
  23. package/dist/client/CreativeContextPanel.d.ts.map +1 -1
  24. package/dist/client/CreativeContextPanel.js +111 -36
  25. package/dist/client/CreativeContextPanel.js.map +1 -1
  26. package/dist/client/actions.d.ts +8 -0
  27. package/dist/client/actions.d.ts.map +1 -1
  28. package/dist/client/actions.js +4 -0
  29. package/dist/client/actions.js.map +1 -1
  30. package/dist/client/google-slides-picker.d.ts +19 -0
  31. package/dist/client/google-slides-picker.d.ts.map +1 -0
  32. package/dist/client/google-slides-picker.js +106 -0
  33. package/dist/client/google-slides-picker.js.map +1 -0
  34. package/dist/client/index.d.ts +1 -1
  35. package/dist/client/index.d.ts.map +1 -1
  36. package/dist/client/index.js +1 -1
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/client/messages.d.ts +1 -1
  39. package/dist/client/messages.d.ts.map +1 -1
  40. package/dist/client/messages.js +22 -0
  41. package/dist/client/messages.js.map +1 -1
  42. package/dist/connectors/google-slides.d.ts +1 -1
  43. package/dist/connectors/google-slides.d.ts.map +1 -1
  44. package/dist/connectors/google-slides.js +1 -2
  45. package/dist/connectors/google-slides.js.map +1 -1
  46. package/dist/connectors/normalize.d.ts +7 -0
  47. package/dist/connectors/normalize.d.ts.map +1 -1
  48. package/dist/connectors/normalize.js +122 -3
  49. package/dist/connectors/normalize.js.map +1 -1
  50. package/dist/schema/index.d.ts +30 -30
  51. package/dist/server/enrichment.d.ts +1 -0
  52. package/dist/server/enrichment.d.ts.map +1 -1
  53. package/dist/server/enrichment.js +26 -24
  54. package/dist/server/enrichment.js.map +1 -1
  55. package/dist/server/generation-context.d.ts +1 -1
  56. package/dist/server/generation-context.d.ts.map +1 -1
  57. package/dist/server/generation-context.js +1 -1
  58. package/dist/server/generation-context.js.map +1 -1
  59. package/dist/store/content.d.ts +13 -0
  60. package/dist/store/content.d.ts.map +1 -1
  61. package/dist/store/content.js +172 -1
  62. package/dist/store/content.js.map +1 -1
  63. package/dist/store/jobs.d.ts.map +1 -1
  64. package/dist/store/jobs.js +2 -2
  65. package/dist/store/jobs.js.map +1 -1
  66. package/dist/store/purge.d.ts.map +1 -1
  67. package/dist/store/purge.js +9 -8
  68. package/dist/store/purge.js.map +1 -1
  69. package/dist/store/suggestions.d.ts +1 -0
  70. package/dist/store/suggestions.d.ts.map +1 -1
  71. package/dist/store/suggestions.js +56 -38
  72. package/dist/store/suggestions.js.map +1 -1
  73. package/docs/skills/creative-context/SKILL.md +8 -0
  74. package/package.json +3 -3
  75. package/src/actions/continue-context-import.ts +4 -1
  76. package/src/actions/creative-context-a2a.spec.ts +98 -0
  77. package/src/actions/get-creative-context.spec.ts +121 -1
  78. package/src/actions/get-creative-context.ts +78 -29
  79. package/src/actions/get-google-picker-session.spec.ts +62 -0
  80. package/src/actions/get-google-picker-session.ts +45 -0
  81. package/src/actions/index.ts +2 -0
  82. package/src/actions/job-public-serialization.spec.ts +122 -0
  83. package/src/actions/start-context-enrichment.ts +6 -4
  84. package/src/actions/start-context-import.ts +2 -1
  85. package/src/client/CreativeContextPanel.tsx +163 -64
  86. package/src/client/actions.ts +18 -0
  87. package/src/client/google-slides-picker.spec.ts +36 -0
  88. package/src/client/google-slides-picker.ts +146 -0
  89. package/src/client/index.ts +2 -0
  90. package/src/client/messages.ts +35 -0
  91. package/src/client/source-config.spec.ts +24 -10
  92. package/src/connectors/google-slides.ts +1 -2
  93. package/src/connectors/normalize.spec.ts +144 -0
  94. package/src/connectors/normalize.ts +201 -3
  95. package/src/server/enrichment.spec.ts +94 -0
  96. package/src/server/enrichment.ts +27 -24
  97. package/src/server/generation-artifact-access.spec.ts +104 -0
  98. package/src/server/generation-context-a2a.spec.ts +20 -2
  99. package/src/server/generation-context.ts +6 -6
  100. package/src/server/isolated-a2a.spec.ts +21 -1
  101. package/src/store/access.integration.spec.ts +511 -26
  102. package/src/store/content.ts +213 -1
  103. package/src/store/jobs.ts +2 -0
  104. package/src/store/purge.ts +18 -9
  105. package/src/store/suggestions.ts +67 -41
@@ -22,6 +22,7 @@ import {
22
22
  sql,
23
23
  } from "drizzle-orm";
24
24
 
25
+ import { assertContextItemSqlTextLimits } from "../connectors/normalize.js";
25
26
  import { matchesCreativeSearchMode } from "../search/mode.js";
26
27
  import { getCreativeContext } from "../server/context.js";
27
28
  import type {
@@ -153,6 +154,19 @@ function mapEdge(row: any): ContextEdge {
153
154
  };
154
155
  }
155
156
 
157
+ function mediaEnrichmentSourceContentHash(
158
+ metadata: Record<string, unknown>,
159
+ fallback: string,
160
+ ): string {
161
+ const derivation = metadata.__creativeContextDerivation;
162
+ if (!derivation || typeof derivation !== "object") return fallback;
163
+ const record = derivation as Record<string, unknown>;
164
+ return record.kind === "media-enrichment" &&
165
+ typeof record.sourceContentHash === "string"
166
+ ? record.sourceContentHash
167
+ : fallback;
168
+ }
169
+
156
170
  export function assertImmutableContextVersion(operation: string): never {
157
171
  throw new Error(
158
172
  `Creative context item versions are immutable; ${operation} must create a new version`,
@@ -195,6 +209,9 @@ async function ingestItemsAttempt(
195
209
  undefined,
196
210
  { skipResourceBody: true },
197
211
  );
212
+ for (const item of batch.items) {
213
+ assertContextItemSqlTextLimits(item);
214
+ }
198
215
  const { getDb, schema } = getCreativeContext();
199
216
  const actor = requireActor();
200
217
  const timestamp = batch.completedAt ?? nowIso();
@@ -229,6 +246,8 @@ async function ingestItemsAttempt(
229
246
  .select({
230
247
  id: schema.contextItemVersions.id,
231
248
  versionNumber: schema.contextItemVersions.versionNumber,
249
+ contentHash: schema.contextItemVersions.contentHash,
250
+ metadata: schema.contextItemVersions.metadata,
232
251
  })
233
252
  .from(schema.contextItemVersions)
234
253
  .where(inArray(schema.contextItemVersions.id, currentVersionIds))
@@ -236,6 +255,15 @@ async function ingestItemsAttempt(
236
255
  const versionNumberById = new Map<string, number>(
237
256
  currentVersionRows.map((row: any) => [row.id, row.versionNumber as number]),
238
257
  );
258
+ const sourceContentHashByVersionId = new Map<string, string>(
259
+ currentVersionRows.map((row: any) => {
260
+ const metadata = parseJson<Record<string, unknown>>(row.metadata, {});
261
+ return [
262
+ row.id,
263
+ mediaEnrichmentSourceContentHash(metadata, row.contentHash),
264
+ ];
265
+ }),
266
+ );
239
267
 
240
268
  const newItems: any[] = [];
241
269
  const itemUpdates: Array<{ id: string; values: Record<string, unknown> }> =
@@ -255,7 +283,12 @@ async function ingestItemsAttempt(
255
283
 
256
284
  for (const input of batch.items) {
257
285
  const existing = existingByExternalId.get(input.externalId) as any;
258
- if (existing?.currentContentHash === input.contentHash) {
286
+ if (
287
+ existing &&
288
+ (existing.currentContentHash === input.contentHash ||
289
+ sourceContentHashByVersionId.get(existing.currentVersionId) ===
290
+ input.contentHash)
291
+ ) {
259
292
  batchVersionTargets.set(input.externalId, {
260
293
  itemId: existing.id,
261
294
  itemVersionId: existing.currentVersionId,
@@ -1378,6 +1411,185 @@ export async function getCreativeContextItem(
1378
1411
  };
1379
1412
  }
1380
1413
 
1414
+ export async function appendMediaEnrichmentVersion(input: {
1415
+ mediaId: string;
1416
+ palette: string[];
1417
+ contentHash: string;
1418
+ caption: string | null;
1419
+ captionStatus: "pending" | "complete" | "failed" | "not-needed";
1420
+ ocrText: string | null;
1421
+ }): Promise<{
1422
+ itemId: string;
1423
+ itemVersionId: string;
1424
+ mediaId: string;
1425
+ appended: boolean;
1426
+ }> {
1427
+ const { getDb, schema } = getCreativeContext();
1428
+ const rows = await getDb()
1429
+ .select({ itemId: schema.contextMedia.itemId })
1430
+ .from(schema.contextMedia)
1431
+ .where(eq(schema.contextMedia.id, input.mediaId))
1432
+ .limit(1);
1433
+ const itemId = rows[0]?.itemId;
1434
+ if (!itemId) throw new Error("Creative context media was not found");
1435
+ const detail = await getCreativeContextItem(itemId);
1436
+ if (!detail) throw new Error("Creative context media is not accessible");
1437
+ const target = detail.media.find(
1438
+ (media) =>
1439
+ media.id === input.mediaId ||
1440
+ media.metadata.__creativeContextEnrichmentSourceMediaId ===
1441
+ input.mediaId ||
1442
+ media.metadata.__creativeContextPreviousMediaId === input.mediaId,
1443
+ );
1444
+ if (!target) {
1445
+ throw new Error(
1446
+ "Creative context media enrichment can only append from the current item version",
1447
+ );
1448
+ }
1449
+ if (
1450
+ target.caption === input.caption &&
1451
+ target.captionStatus === input.captionStatus &&
1452
+ target.ocrText === input.ocrText &&
1453
+ target.contentHash === input.contentHash &&
1454
+ stringifyJson(target.palette) === stringifyJson(input.palette)
1455
+ ) {
1456
+ return {
1457
+ itemId,
1458
+ itemVersionId: detail.version.id,
1459
+ mediaId: target.id,
1460
+ appended: false,
1461
+ };
1462
+ }
1463
+
1464
+ const sourceContentHash = mediaEnrichmentSourceContentHash(
1465
+ detail.version.metadata,
1466
+ detail.version.contentHash,
1467
+ );
1468
+ const media = detail.media.map((entry) => {
1469
+ const enriched = entry.id === target.id;
1470
+ return {
1471
+ kind: entry.kind,
1472
+ ...(entry.mimeType ? { mimeType: entry.mimeType } : {}),
1473
+ accessMode: entry.accessMode,
1474
+ ...(entry.url ? { url: entry.url } : {}),
1475
+ ...(entry.storageKey ? { storageKey: entry.storageKey } : {}),
1476
+ ...(entry.provenanceUrl ? { provenanceUrl: entry.provenanceUrl } : {}),
1477
+ ...(entry.altText ? { altText: entry.altText } : {}),
1478
+ ...(enriched
1479
+ ? { caption: input.caption ?? undefined }
1480
+ : { caption: entry.caption ?? undefined }),
1481
+ captionStatus: enriched ? input.captionStatus : entry.captionStatus,
1482
+ ...(enriched
1483
+ ? { ocrText: input.ocrText ?? undefined }
1484
+ : { ocrText: entry.ocrText ?? undefined }),
1485
+ palette: enriched ? input.palette : entry.palette,
1486
+ ...(enriched
1487
+ ? { contentHash: input.contentHash }
1488
+ : { contentHash: entry.contentHash ?? undefined }),
1489
+ ...(entry.width === null ? {} : { width: entry.width }),
1490
+ ...(entry.height === null ? {} : { height: entry.height }),
1491
+ ...(entry.durationMs === null ? {} : { durationMs: entry.durationMs }),
1492
+ metadata: {
1493
+ ...entry.metadata,
1494
+ __creativeContextEnrichmentSourceMediaId:
1495
+ entry.metadata.__creativeContextEnrichmentSourceMediaId ?? entry.id,
1496
+ __creativeContextPreviousMediaId: entry.id,
1497
+ ...(enriched ? { enrichmentDerived: true } : {}),
1498
+ },
1499
+ };
1500
+ });
1501
+ const canonicalMedia = media.map((entry) => {
1502
+ const {
1503
+ __creativeContextEnrichmentSourceMediaId: _sourceMediaId,
1504
+ __creativeContextPreviousMediaId: _previousMediaId,
1505
+ ...metadata
1506
+ } = entry.metadata;
1507
+ return { ...entry, metadata };
1508
+ });
1509
+ const derivedContentHash = await sha256(
1510
+ stringifyJson({
1511
+ sourceContentHash,
1512
+ media: canonicalMedia,
1513
+ }),
1514
+ );
1515
+ await ingestItems({
1516
+ sourceId: detail.item.sourceId,
1517
+ items: [
1518
+ {
1519
+ externalId: detail.item.externalId,
1520
+ kind: detail.item.kind,
1521
+ title: detail.item.title,
1522
+ canonicalUrl: detail.item.canonicalUrl ?? undefined,
1523
+ mimeType: detail.item.mimeType ?? undefined,
1524
+ content: detail.version.content,
1525
+ summary: detail.version.summary ?? undefined,
1526
+ contentHash: derivedContentHash,
1527
+ sourceModifiedAt: detail.version.sourceModifiedAt ?? undefined,
1528
+ sourceVersion: detail.version.sourceVersion ?? undefined,
1529
+ rawSnapshotBlobRef: detail.version.rawSnapshotBlobRef ?? undefined,
1530
+ parseStatus: detail.version.parseStatus,
1531
+ parseError: detail.version.parseError ?? undefined,
1532
+ upstreamAccess: detail.item.upstreamAccess,
1533
+ curationStatus: detail.item.curationStatus,
1534
+ curationRank: detail.item.curationRank,
1535
+ starred: detail.item.starred,
1536
+ inventoryState: detail.item.inventoryState,
1537
+ indexState: detail.item.indexState,
1538
+ tags: detail.item.tags,
1539
+ colors: detail.item.colors,
1540
+ sortOrder: detail.item.sortOrder,
1541
+ parentItemId: detail.item.parentItemId ?? undefined,
1542
+ provenance: detail.item.provenance,
1543
+ thumbnailBlobRef: detail.item.thumbnailBlobRef ?? undefined,
1544
+ metadata: {
1545
+ ...detail.version.metadata,
1546
+ __creativeContextDerivation: {
1547
+ kind: "media-enrichment",
1548
+ sourceContentHash,
1549
+ derivedFromVersionId: detail.version.id,
1550
+ },
1551
+ },
1552
+ chunks: detail.chunks.map((chunk) => ({
1553
+ ordinal: chunk.ordinal,
1554
+ kind: chunk.kind,
1555
+ text: chunk.text,
1556
+ startOffset: chunk.startOffset ?? undefined,
1557
+ endOffset: chunk.endOffset ?? undefined,
1558
+ tokenCount: chunk.tokenCount ?? undefined,
1559
+ metadata: chunk.metadata,
1560
+ })),
1561
+ media,
1562
+ edges: detail.edges
1563
+ .filter(
1564
+ (edge) =>
1565
+ edge.relation !== "revision-of" ||
1566
+ edge.metadata.automatic !== true,
1567
+ )
1568
+ .map((edge) => ({
1569
+ relation: edge.relation,
1570
+ toItemId: edge.toItemId ?? undefined,
1571
+ toItemVersionId: edge.toItemVersionId ?? undefined,
1572
+ toExternalId: edge.toExternalId ?? undefined,
1573
+ metadata: edge.metadata,
1574
+ })),
1575
+ },
1576
+ ],
1577
+ });
1578
+ const appended = await getCreativeContextItem(itemId);
1579
+ const enrichedMedia = appended?.media.find(
1580
+ (entry) => entry.metadata.__creativeContextPreviousMediaId === target.id,
1581
+ );
1582
+ if (!appended || !enrichedMedia) {
1583
+ throw new Error("Failed to append creative context media enrichment");
1584
+ }
1585
+ return {
1586
+ itemId,
1587
+ itemVersionId: appended.version.id,
1588
+ mediaId: enrichedMedia.id,
1589
+ appended: appended.version.id !== detail.version.id,
1590
+ };
1591
+ }
1592
+
1381
1593
  export async function getCreativeContextItemByExternalId(input: {
1382
1594
  sourceId: string;
1383
1595
  externalId: string;
package/src/store/jobs.ts CHANGED
@@ -260,6 +260,7 @@ export async function enqueueContextRebuildJob(input: {
260
260
  .where(
261
261
  and(
262
262
  eq(schema.contextJobs.ownerEmail, actor.ownerEmail),
263
+ actorOrgScope(schema, actor),
263
264
  eq(schema.contextJobs.sourceId, input.sourceId),
264
265
  eq(schema.contextJobs.kind, "embed"),
265
266
  inArray(schema.contextJobs.status, ["queued", "paused"]),
@@ -315,6 +316,7 @@ export async function getJob(jobId: string): Promise<ContextJob | null> {
315
316
  and(
316
317
  eq(schema.contextJobs.id, jobId),
317
318
  eq(schema.contextJobs.ownerEmail, actor.ownerEmail),
319
+ actorOrgScope(schema, actor),
318
320
  ),
319
321
  )
320
322
  .limit(1);
@@ -12,6 +12,7 @@ import { deletePostgresFtsDocuments } from "../search/postgres-fts.js";
12
12
  import { getCreativeContext } from "../server/context.js";
13
13
  import { nowIso, parseJson, stringifyJson } from "./helpers.js";
14
14
  import { createJob } from "./jobs.js";
15
+ import { resolveLayoutProjectionItemId } from "./suggestions.js";
15
16
 
16
17
  export async function purgeContextSourceArtifacts(sourceId: string) {
17
18
  await assertAccess("creative-context-source", sourceId, "editor", undefined, {
@@ -119,15 +120,23 @@ export async function purgeContextSourceArtifacts(sourceId: string) {
119
120
  )
120
121
  .where(inArray(schema.brandDnaVersions.id, dnaVersionIds))
121
122
  : [];
122
- const promotedLayouts = (suggestions as any[]).flatMap((suggestion) => {
123
- if (suggestion.status !== "promoted") return [];
124
- const payload = parseJson<Record<string, unknown>>(suggestion.payload, {});
125
- const projectionItemId =
126
- typeof payload.projectionItemId === "string"
127
- ? payload.projectionItemId
128
- : null;
129
- return [{ suggestionId: suggestion.id as string, projectionItemId }];
130
- });
123
+ const promotedLayouts = await Promise.all(
124
+ (suggestions as any[])
125
+ .filter((suggestion) => suggestion.status === "promoted")
126
+ .map(async (suggestion) => {
127
+ const payload = parseJson<Record<string, unknown>>(
128
+ suggestion.payload,
129
+ {},
130
+ );
131
+ return {
132
+ suggestionId: suggestion.id as string,
133
+ projectionItemId: await resolveLayoutProjectionItemId(
134
+ suggestion.id,
135
+ payload.projectionItemId,
136
+ ),
137
+ };
138
+ }),
139
+ );
131
140
  for (const layout of promotedLayouts) {
132
141
  await projections?.layoutTemplate?.demote(layout);
133
142
  }
@@ -1,6 +1,7 @@
1
1
  import { assertAccess } from "@agent-native/core/sharing";
2
2
  import { and, desc, eq } from "drizzle-orm";
3
3
 
4
+ import { assertContextItemSqlTextLimits } from "../connectors/normalize.js";
4
5
  import { reassembleNativeCreativeArtifact } from "../native-artifact-reassembly.js";
5
6
  import { nativeCreativeArtifactFromMetadata } from "../native-artifact.js";
6
7
  import { getCreativeContext } from "../server/context.js";
@@ -134,6 +135,8 @@ export async function proposeCreativeContextSuggestion(input: {
134
135
  return mapSuggestion(existing[0]);
135
136
  }
136
137
  const timestamp = nowIso();
138
+ const payload = { ...(input.payload ?? {}) };
139
+ if (input.kind === "layout-template") delete payload.projectionItemId;
137
140
  const row = {
138
141
  id: newId("ccsg"),
139
142
  kind: input.kind,
@@ -142,7 +145,7 @@ export async function proposeCreativeContextSuggestion(input: {
142
145
  itemId: input.itemId,
143
146
  itemVersionId: detail.version.id,
144
147
  reason: input.reason ?? null,
145
- payload: stringifyJson(input.payload),
148
+ payload: stringifyJson(payload),
146
149
  createdAt: timestamp,
147
150
  updatedAt: timestamp,
148
151
  ownerEmail: actor.ownerEmail,
@@ -195,6 +198,19 @@ export async function decideCanonicalLogoSuggestion(input: {
195
198
 
196
199
  const PROMOTED_LAYOUT_SOURCE = "Promoted layout templates";
197
200
 
201
+ export async function resolveLayoutProjectionItemId(
202
+ suggestionId: string,
203
+ candidateId: unknown,
204
+ ): Promise<string | null> {
205
+ if (typeof candidateId !== "string" || !candidateId) return null;
206
+ const detail = await getCreativeContextItem(candidateId);
207
+ if (!detail || detail.item.kind !== "layout_template") return null;
208
+ const provenance = detail.item.provenance as Record<string, unknown>;
209
+ return provenance.promotedFromSuggestionId === suggestionId
210
+ ? detail.item.id
211
+ : null;
212
+ }
213
+
198
214
  async function loadCompiledLayoutSource(itemId: string, itemVersionId: string) {
199
215
  const detail = await getCreativeContextItem(itemId, itemVersionId);
200
216
  if (!detail) throw new Error("Layout source version is no longer accessible");
@@ -236,14 +252,52 @@ export async function applyLayoutTemplateSuggestion(input: {
236
252
  "Layout-template projection is not configured for this app; keep the proposal pending",
237
253
  );
238
254
  }
239
- let projectionItemId =
240
- typeof payload.projectionItemId === "string"
241
- ? payload.projectionItemId
242
- : null;
255
+ let projectionItemId = await resolveLayoutProjectionItemId(
256
+ row.id,
257
+ payload.projectionItemId,
258
+ );
243
259
  const source =
244
260
  input.operation === "promote"
245
261
  ? await loadCompiledLayoutSource(row.itemId, row.itemVersionId)
246
262
  : null;
263
+ const projectionItem = source
264
+ ? {
265
+ externalId: `layout-template:${row.id}`,
266
+ kind: "layout_template",
267
+ title: source!.detail.item.title,
268
+ content: source!.compiled.html,
269
+ summary: source!.detail.version.summary ?? undefined,
270
+ mimeType: "text/html",
271
+ contentHash: await sha256(
272
+ `${row.id}:${source!.detail.version.id}:${source!.compiled.html}`,
273
+ ),
274
+ upstreamAccess: "available" as const,
275
+ curationStatus: "included" as const,
276
+ curationRank: "canonical" as const,
277
+ provenance: {
278
+ ...source!.detail.item.provenance,
279
+ promotedFromSuggestionId: row.id,
280
+ promotedFromItemId: source!.detail.item.id,
281
+ promotedFromItemVersionId: source!.detail.version.id,
282
+ },
283
+ metadata: {
284
+ promotedFromSuggestionId: row.id,
285
+ nativeArtifact: {
286
+ ...source!.compiled.artifact,
287
+ childExternalIds: undefined,
288
+ manifest: undefined,
289
+ },
290
+ },
291
+ edges: source!.compiled.evidence.map((evidence) => ({
292
+ relation: "derived-from",
293
+ toItemId: evidence.itemId,
294
+ toItemVersionId: evidence.itemVersionId,
295
+ })),
296
+ }
297
+ : null;
298
+ if (projectionItem) {
299
+ assertContextItemSqlTextLimits(projectionItem);
300
+ }
247
301
  if (input.operation === "promote" && !projectionItemId) {
248
302
  const sources = await listContextSources({ kind: "manual", limit: 100 });
249
303
  let projectionSource = sources.sources.find(
@@ -259,41 +313,7 @@ export async function applyLayoutTemplateSuggestion(input: {
259
313
  }
260
314
  const ingested = await ingestItems({
261
315
  sourceId: projectionSource.id,
262
- items: [
263
- {
264
- externalId: `layout-template:${row.id}`,
265
- kind: "layout_template",
266
- title: source!.detail.item.title,
267
- content: source!.compiled.html,
268
- summary: source!.detail.version.summary ?? undefined,
269
- mimeType: "text/html",
270
- contentHash: await sha256(
271
- `${row.id}:${source!.detail.version.id}:${source!.compiled.html}`,
272
- ),
273
- upstreamAccess: "available",
274
- curationStatus: "included",
275
- curationRank: "canonical",
276
- provenance: {
277
- ...source!.detail.item.provenance,
278
- promotedFromSuggestionId: row.id,
279
- promotedFromItemId: source!.detail.item.id,
280
- promotedFromItemVersionId: source!.detail.version.id,
281
- },
282
- metadata: {
283
- promotedFromSuggestionId: row.id,
284
- nativeArtifact: {
285
- ...source!.compiled.artifact,
286
- childExternalIds: undefined,
287
- manifest: undefined,
288
- },
289
- },
290
- edges: source!.compiled.evidence.map((evidence) => ({
291
- relation: "derived-from",
292
- toItemId: evidence.itemId,
293
- toItemVersionId: evidence.itemVersionId,
294
- })),
295
- },
296
- ],
316
+ items: [projectionItem!],
297
317
  });
298
318
  projectionItemId = ingested.itemIds[0] ?? null;
299
319
  if (!projectionItemId) throw new Error("Failed to promote layout template");
@@ -308,11 +328,17 @@ export async function applyLayoutTemplateSuggestion(input: {
308
328
  });
309
329
  } else {
310
330
  const { getDb, schema } = getCreativeContext();
331
+ const actor = requireActor();
311
332
  if (projectionItemId) {
312
333
  await getDb()
313
334
  .update(schema.contextItems)
314
335
  .set({ status: "deprecated", updatedAt: nowIso() })
315
- .where(eq(schema.contextItems.id, projectionItemId));
336
+ .where(
337
+ and(
338
+ eq(schema.contextItems.id, projectionItemId),
339
+ eq(schema.contextItems.ownerEmail, actor.ownerEmail),
340
+ ),
341
+ );
316
342
  }
317
343
  await adapter.demote({ suggestionId: row.id, projectionItemId });
318
344
  }