@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
@@ -266,36 +266,196 @@ describe("creative context access and revocation", () => {
266
266
  });
267
267
  });
268
268
 
269
- it("shares generation history only after an app asserts same-org artifact access", async () => {
269
+ it("rejects an oversized compiled layout before creating its projection", async () => {
270
270
  const { exec, runWithRequestContext, store } = await setup();
271
+ const server = await import("../server/index.js");
272
+ const promote = vi.fn(async () => {});
273
+ server.configureCreativeContext({
274
+ projections: {
275
+ layoutTemplate: { promote, demote: vi.fn(async () => {}) },
276
+ },
277
+ });
278
+ const html = `<div class="fmd-slide google-slides-native" data-slide-id="slide-1"><p>${"界".repeat(44_000)}</p></div>`;
271
279
  await exec.execute({
272
- sql: `INSERT INTO creative_context_generation_records
273
- (id, app_id, artifact_type, artifact_id, context_mode,
274
- context_pack_id, element_provenance, created_at, owner_email, org_id)
275
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
280
+ sql: `UPDATE creative_context_items
281
+ SET mime_type = ?, provenance = ? WHERE id = ?`,
282
+ args: [
283
+ "text/html",
284
+ JSON.stringify({
285
+ compiler: "@agent-native/creative-context:google-slides-native",
286
+ }),
287
+ "allowed",
288
+ ],
289
+ });
290
+ await exec.execute({
291
+ sql: `UPDATE creative_context_item_versions
292
+ SET content = ?, mime_type = ?, metadata = ? WHERE id = ?`,
276
293
  args: [
277
- "record-org-1",
278
- "slides",
279
- "deck",
280
- "shared-deck",
281
- "auto",
282
- null,
283
- '[{"elementId":"slide-1","influence":"generated"}]',
294
+ html,
295
+ "text/html",
296
+ JSON.stringify({
297
+ nativeArtifact: {
298
+ schemaVersion: 1,
299
+ app: "slides",
300
+ format: "slides-html",
301
+ rootExternalId: "presentation-1:slide-1",
302
+ fidelityReport: {
303
+ exact: { count: 1 },
304
+ approximated: { count: 0, reasons: [] },
305
+ imageFallback: { count: 0, reasons: [] },
306
+ },
307
+ },
308
+ }),
309
+ "allowed-v2",
310
+ ],
311
+ });
312
+ const suggestion = await runWithRequestContext(
313
+ { userEmail: "alice@example.test", orgId: "org-1" },
314
+ () =>
315
+ store.proposeCreativeContextSuggestion({
316
+ kind: "layout-template",
317
+ itemId: "allowed",
318
+ itemVersionId: "allowed-v2",
319
+ }),
320
+ );
321
+
322
+ await expect(
323
+ runWithRequestContext(
324
+ { userEmail: "alice@example.test", orgId: "org-1" },
325
+ () =>
326
+ store.applyLayoutTemplateSuggestion({
327
+ suggestionId: suggestion.id,
328
+ operation: "promote",
329
+ }),
330
+ ),
331
+ ).rejects.toThrow(/native content.*exceeds.*split the artifact/i);
332
+ expect(promote).not.toHaveBeenCalled();
333
+ const projectionCount = await exec.execute({
334
+ sql: "SELECT COUNT(*) AS count FROM creative_context_items WHERE kind = 'layout_template'",
335
+ args: [],
336
+ });
337
+ expect(Number(projectionCount.rows[0]?.count)).toBe(0);
338
+ });
339
+
340
+ it("never trusts a caller-supplied layout projection item id", async () => {
341
+ const { exec, runWithRequestContext, store } = await setup();
342
+ const server = await import("../server/index.js");
343
+ const demote = vi.fn(async () => {});
344
+ server.configureCreativeContext({
345
+ projections: {
346
+ layoutTemplate: { promote: vi.fn(async () => {}), demote },
347
+ },
348
+ });
349
+ const proposed = await runWithRequestContext(
350
+ { userEmail: "alice@example.test", orgId: "org-1" },
351
+ () =>
352
+ store.proposeCreativeContextSuggestion({
353
+ kind: "layout-template",
354
+ itemId: "allowed",
355
+ itemVersionId: "allowed-v2",
356
+ payload: { projectionItemId: "guessed-item", note: "keep" },
357
+ }),
358
+ );
359
+ expect(proposed.payload).toEqual({ note: "keep" });
360
+
361
+ await exec.execute({
362
+ sql: `INSERT INTO creative_context_sources
363
+ (id, name, kind, config, upstream_access, status, health_status,
364
+ item_count, restricted_item_count, owner_email, org_id, visibility,
365
+ created_at, updated_at)
366
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
367
+ args: [
368
+ "outside-source",
369
+ "Outside tenant",
370
+ "manual",
371
+ "{}",
372
+ "available",
373
+ "active",
374
+ "healthy",
375
+ 1,
376
+ 0,
377
+ "mallory@example.test",
378
+ "org-2",
379
+ "private",
380
+ "2026-07-16T00:00:00.000Z",
381
+ "2026-07-16T00:00:00.000Z",
382
+ ],
383
+ });
384
+ await exec.execute({
385
+ sql: `INSERT INTO creative_context_items
386
+ (id, source_id, external_id, kind, title, current_version_id,
387
+ current_content_hash, status, upstream_access, curation_status,
388
+ curation_rank, inventory_state, index_state, tags, colors, provenance,
389
+ metadata, created_at, updated_at, owner_email, org_id)
390
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
391
+ args: [
392
+ "outside-projection",
393
+ "outside-source",
394
+ "outside-projection",
395
+ "layout_template",
396
+ "Outside projection",
397
+ "outside-version",
398
+ "outside-hash",
399
+ "active",
400
+ "available",
401
+ "included",
402
+ "canonical",
403
+ "available",
404
+ "indexed",
405
+ "[]",
406
+ "[]",
407
+ JSON.stringify({
408
+ promotedFromSuggestionId: "malicious-layout-suggestion",
409
+ }),
410
+ "{}",
411
+ "2026-07-16T00:00:00.000Z",
284
412
  "2026-07-16T00:00:00.000Z",
285
- "alice@example.test",
286
- "org-1",
287
- "record-other-org",
288
- "slides",
289
- "deck",
290
- "shared-deck",
291
- "auto",
292
- null,
293
- '[{"elementId":"secret","influence":"generated"}]',
294
- "2026-07-16T01:00:00.000Z",
295
413
  "mallory@example.test",
296
414
  "org-2",
297
415
  ],
298
416
  });
417
+ await exec.execute({
418
+ sql: `INSERT INTO creative_context_suggestions
419
+ (id, kind, status, item_id, item_version_id, payload, created_at,
420
+ updated_at, owner_email, org_id)
421
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
422
+ args: [
423
+ "malicious-layout-suggestion",
424
+ "layout-template",
425
+ "promoted",
426
+ "allowed",
427
+ "allowed-v2",
428
+ JSON.stringify({ projectionItemId: "outside-projection" }),
429
+ "2026-07-16T00:00:00.000Z",
430
+ "2026-07-16T00:00:00.000Z",
431
+ "alice@example.test",
432
+ "org-1",
433
+ ],
434
+ });
435
+
436
+ await runWithRequestContext(
437
+ { userEmail: "alice@example.test", orgId: "org-1" },
438
+ () =>
439
+ store.applyLayoutTemplateSuggestion({
440
+ suggestionId: "malicious-layout-suggestion",
441
+ operation: "demote",
442
+ }),
443
+ );
444
+
445
+ const outside = await exec.execute(
446
+ "SELECT status FROM creative_context_items WHERE id = 'outside-projection'",
447
+ );
448
+ expect(outside.rows[0]?.status).toBe("active");
449
+ expect(demote).toHaveBeenCalledWith({
450
+ suggestionId: "malicious-layout-suggestion",
451
+ projectionItemId: null,
452
+ });
453
+ });
454
+
455
+ it("requires a host access proof before sharing generation history", async () => {
456
+ const { exec, runWithRequestContext, store } = await setup();
457
+ const { assertGenerationArtifactAccess } =
458
+ await import("../server/generation-artifact-access.js");
299
459
  const asBob = <T>(fn: () => Promise<T>) =>
300
460
  runWithRequestContext(
301
461
  { userEmail: "bob@example.test", orgId: "org-1" },
@@ -306,18 +466,122 @@ describe("creative context access and revocation", () => {
306
466
  artifactType: "deck",
307
467
  artifactId: "shared-deck",
308
468
  };
469
+ const aliceProof = await runWithRequestContext(
470
+ { userEmail: "alice@example.test", orgId: "org-1" },
471
+ () =>
472
+ assertGenerationArtifactAccess(
473
+ artifact,
474
+ {
475
+ resourceType: "creative-context-source",
476
+ resourceId: "source-1",
477
+ },
478
+ "editor",
479
+ ),
480
+ );
481
+ await runWithRequestContext(
482
+ { userEmail: "alice@example.test", orgId: "org-1" },
483
+ () =>
484
+ store.recordGenerationCreativeContext(
485
+ {
486
+ ...artifact,
487
+ contextMode: "auto",
488
+ contextPackId: null,
489
+ reuseLabels: [],
490
+ elementProvenance: [
491
+ { elementId: "slide-1", influence: "generated" },
492
+ ],
493
+ },
494
+ { artifactAccess: aliceProof },
495
+ ),
496
+ );
309
497
 
310
498
  await expect(
311
499
  asBob(() => store.getGenerationCreativeContext(artifact)),
312
500
  ).resolves.toBeNull();
501
+ await expect(
502
+ asBob(() =>
503
+ assertGenerationArtifactAccess(
504
+ artifact,
505
+ {
506
+ resourceType: "creative-context-source",
507
+ resourceId: "source-1",
508
+ },
509
+ "viewer",
510
+ ),
511
+ ),
512
+ ).rejects.toThrow(/no access/i);
513
+ await expect(
514
+ asBob(() =>
515
+ store.getGenerationCreativeContext(artifact, {
516
+ artifactAccess: {
517
+ identityKey: JSON.stringify([
518
+ artifact.appId,
519
+ artifact.artifactType,
520
+ artifact.artifactId,
521
+ ]),
522
+ minRole: "viewer",
523
+ } as never,
524
+ }),
525
+ ),
526
+ ).rejects.toThrow(/verified by the host application/i);
527
+
528
+ await asBob(() =>
529
+ store.recordGenerationCreativeContext({
530
+ ...artifact,
531
+ contextMode: "auto",
532
+ contextPackId: null,
533
+ reuseLabels: [],
534
+ elementProvenance: [{ elementId: "poison", influence: "generated" }],
535
+ }),
536
+ );
537
+ const poison = await exec.execute(
538
+ "SELECT org_id FROM creative_context_generation_records WHERE owner_email = 'bob@example.test'",
539
+ );
540
+ expect(poison.rows[0]?.org_id).toBeNull();
541
+ await expect(
542
+ runWithRequestContext(
543
+ { userEmail: "alice@example.test", orgId: "org-1" },
544
+ () =>
545
+ store.getGenerationCreativeContext(artifact, {
546
+ artifactAccess: aliceProof,
547
+ }),
548
+ ),
549
+ ).resolves.toMatchObject({
550
+ artifactId: "shared-deck",
551
+ elementProvenance: [{ elementId: "slide-1" }],
552
+ });
553
+
554
+ await exec.execute({
555
+ sql: `INSERT INTO creative_context_source_shares
556
+ (id, resource_id, principal_type, principal_id, role, created_by, created_at)
557
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
558
+ args: [
559
+ "source-share-bob",
560
+ "source-1",
561
+ "user",
562
+ "bob@example.test",
563
+ "viewer",
564
+ "alice@example.test",
565
+ "2026-07-16T00:00:00.000Z",
566
+ ],
567
+ });
568
+ const bobProof = await asBob(() =>
569
+ assertGenerationArtifactAccess(
570
+ artifact,
571
+ {
572
+ resourceType: "creative-context-source",
573
+ resourceId: "source-1",
574
+ },
575
+ "viewer",
576
+ ),
577
+ );
313
578
  await expect(
314
579
  asBob(() =>
315
580
  store.getGenerationCreativeContext(artifact, {
316
- accessScope: "artifact-access-asserted",
581
+ artifactAccess: bobProof,
317
582
  }),
318
583
  ),
319
584
  ).resolves.toMatchObject({
320
- id: "record-org-1",
321
585
  artifactId: "shared-deck",
322
586
  elementProvenance: [{ elementId: "slide-1" }],
323
587
  });
@@ -325,10 +589,138 @@ describe("creative context access and revocation", () => {
325
589
  asBob(() =>
326
590
  store.getGenerationCreativeContext(
327
591
  { ...artifact, artifactId: "arbitrary-unasserted-id" },
328
- { accessScope: "owner" },
592
+ { artifactAccess: bobProof },
329
593
  ),
330
594
  ),
331
- ).resolves.toBeNull();
595
+ ).rejects.toThrow(/verified by the host application/i);
596
+ });
597
+
598
+ it("appends media enrichment while preserving version-pinned pack evidence", async () => {
599
+ const { exec, runWithRequestContext, store } = await setup();
600
+ await exec.execute({
601
+ sql: `INSERT INTO creative_context_media
602
+ (id, item_id, item_version_id, kind, mime_type, access_mode,
603
+ storage_key, caption_status, palette, metadata, created_at,
604
+ owner_email, org_id)
605
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
606
+ args: [
607
+ "media-before-enrichment",
608
+ "allowed",
609
+ "allowed-v2",
610
+ "image",
611
+ "image/png",
612
+ "private",
613
+ "private-blob:fixture",
614
+ "pending",
615
+ "[]",
616
+ "{}",
617
+ "2026-07-16T00:00:00.000Z",
618
+ "alice@example.test",
619
+ "org-1",
620
+ ],
621
+ });
622
+ const asAlice = <T>(fn: () => Promise<T>) =>
623
+ runWithRequestContext(
624
+ { userEmail: "alice@example.test", orgId: "org-1" },
625
+ fn,
626
+ );
627
+ const pack = await asAlice(() =>
628
+ store.createContextPack({
629
+ name: "Pinned before enrichment",
630
+ members: [
631
+ {
632
+ itemId: "allowed",
633
+ itemVersionId: "allowed-v2",
634
+ reason: "Exact pre-enrichment evidence",
635
+ },
636
+ ],
637
+ }),
638
+ );
639
+
640
+ const enriched = await asAlice(() =>
641
+ store.appendMediaEnrichmentVersion({
642
+ mediaId: "media-before-enrichment",
643
+ palette: ["#663399", "#ffffff"],
644
+ contentHash: "enriched-image-hash",
645
+ caption: "Builder dashboard on a purple background",
646
+ captionStatus: "complete",
647
+ ocrText: "Ship faster",
648
+ }),
649
+ );
650
+
651
+ expect(enriched.appended).toBe(true);
652
+ expect(enriched.itemVersionId).not.toBe("allowed-v2");
653
+ expect(enriched.mediaId).not.toBe("media-before-enrichment");
654
+ const pinned = await asAlice(() =>
655
+ store.getCreativeContextItem("allowed", "allowed-v2"),
656
+ );
657
+ expect(pinned?.media).toEqual([
658
+ expect.objectContaining({
659
+ id: "media-before-enrichment",
660
+ caption: null,
661
+ captionStatus: "pending",
662
+ palette: [],
663
+ }),
664
+ ]);
665
+ const current = await asAlice(() =>
666
+ store.getCreativeContextItem("allowed"),
667
+ );
668
+ expect(current?.version.id).toBe(enriched.itemVersionId);
669
+ expect(current?.media).toEqual([
670
+ expect.objectContaining({
671
+ id: enriched.mediaId,
672
+ caption: "Builder dashboard on a purple background",
673
+ captionStatus: "complete",
674
+ ocrText: "Ship faster",
675
+ palette: ["#663399", "#ffffff"],
676
+ }),
677
+ ]);
678
+ const pinnedPack = await asAlice(() => store.getContextPack(pack.id));
679
+ expect(pinnedPack?.members).toEqual([
680
+ expect.objectContaining({
681
+ itemId: "allowed",
682
+ itemVersionId: "allowed-v2",
683
+ }),
684
+ ]);
685
+
686
+ const resync = await asAlice(() =>
687
+ store.ingestItems({
688
+ sourceId: "source-1",
689
+ items: [
690
+ {
691
+ externalId: "allowed",
692
+ kind: "slide",
693
+ title: "allowed title",
694
+ content: "allowed canonical version 2",
695
+ contentHash: "allowed-hash-v2",
696
+ upstreamAccess: "available",
697
+ },
698
+ ],
699
+ }),
700
+ );
701
+ expect(resync.unchanged).toBe(1);
702
+ const afterResync = await asAlice(() =>
703
+ store.getCreativeContextItem("allowed"),
704
+ );
705
+ expect(afterResync?.version.id).toBe(enriched.itemVersionId);
706
+ expect(afterResync?.media[0]?.captionStatus).toBe("complete");
707
+
708
+ const retried = await asAlice(() =>
709
+ store.appendMediaEnrichmentVersion({
710
+ mediaId: "media-before-enrichment",
711
+ palette: ["#663399", "#ffffff"],
712
+ contentHash: "enriched-image-hash",
713
+ caption: "Builder dashboard on a purple background",
714
+ captionStatus: "complete",
715
+ ocrText: "Ship faster",
716
+ }),
717
+ );
718
+ expect(retried).toEqual({
719
+ itemId: "allowed",
720
+ itemVersionId: enriched.itemVersionId,
721
+ mediaId: enriched.mediaId,
722
+ appended: false,
723
+ });
332
724
  });
333
725
 
334
726
  it("reloads a concurrently inserted item instead of failing its idempotent ingest", async () => {
@@ -371,6 +763,69 @@ describe("creative context access and revocation", () => {
371
763
  expect(versions.rows).toHaveLength(1);
372
764
  });
373
765
 
766
+ it("rejects direct ingest that bypasses normalized SQL text limits", async () => {
767
+ const { exec, runWithRequestContext, store } = await setup();
768
+ const asAlice = <T>(fn: () => Promise<T>) =>
769
+ runWithRequestContext(
770
+ { userEmail: "alice@example.test", orgId: "org-1" },
771
+ fn,
772
+ );
773
+ const ingest = (
774
+ item: Parameters<typeof store.ingestItems>[0]["items"][0],
775
+ ) =>
776
+ asAlice(() => store.ingestItems({ sourceId: "source-1", items: [item] }));
777
+ const base = {
778
+ kind: "document",
779
+ title: "Oversized direct ingest",
780
+ contentHash: "oversized-direct-hash",
781
+ };
782
+
783
+ await expect(
784
+ ingest({
785
+ ...base,
786
+ externalId: "oversized-searchable",
787
+ content: "界".repeat(22_000),
788
+ }),
789
+ ).rejects.toThrow(/searchable content.*exceeds.*normalize or chunk/i);
790
+ await expect(
791
+ ingest({
792
+ ...base,
793
+ externalId: "oversized-summary",
794
+ content: "bounded",
795
+ summary: "🙂".repeat(2_049),
796
+ }),
797
+ ).rejects.toThrow(/summary.*exceeds.*normalize the summary/i);
798
+ await expect(
799
+ ingest({
800
+ ...base,
801
+ externalId: "oversized-chunks",
802
+ content: "bounded",
803
+ chunks: [
804
+ { ordinal: 0, text: "a".repeat(40_000) },
805
+ { ordinal: 1, text: "b".repeat(30_000) },
806
+ ],
807
+ }),
808
+ ).rejects.toThrow(/chunks exceed.*normalize or split/i);
809
+ await expect(
810
+ ingest({
811
+ ...base,
812
+ externalId: "oversized-native",
813
+ kind: "google-slides-slide",
814
+ mimeType: "text/html",
815
+ content: `<div>${"🙂".repeat(32_768)}</div>`,
816
+ metadata: {
817
+ nativeArtifact: { app: "slides", format: "slides-html" },
818
+ },
819
+ }),
820
+ ).rejects.toThrow(/native content.*exceeds.*private blob storage/i);
821
+
822
+ const rows = await exec.execute({
823
+ sql: "SELECT id FROM creative_context_items WHERE external_id LIKE 'oversized-%'",
824
+ args: [],
825
+ });
826
+ expect(rows.rows).toHaveLength(0);
827
+ });
828
+
374
829
  it("keeps prior native code immutable across resync and resolves hierarchical children by source version", async () => {
375
830
  const { runWithRequestContext, store } = await setup();
376
831
  const asAlice = <T>(fn: () => Promise<T>) =>
@@ -1000,6 +1455,36 @@ describe("creative context access and revocation", () => {
1000
1455
  expect(rows.rows).toHaveLength(1);
1001
1456
  });
1002
1457
 
1458
+ it("deduplicates jobs inside one tenant without colliding across scopes", async () => {
1459
+ const { exec, runWithRequestContext, store } = await setup();
1460
+ const create = (userEmail: string, orgId: string | undefined) =>
1461
+ runWithRequestContext({ userEmail, orgId }, () =>
1462
+ store.createJob({
1463
+ kind: "metadata-refresh",
1464
+ dedupeKey: "same-logical-operation",
1465
+ }),
1466
+ );
1467
+
1468
+ const aliceOrgOne = await create("alice@example.test", "org-1");
1469
+ const aliceOrgOneAgain = await create("alice@example.test", "org-1");
1470
+ const bobOrgOne = await create("bob@example.test", "org-1");
1471
+ const aliceOrgTwo = await create("alice@example.test", "org-2");
1472
+ const alicePersonal = await create("alice@example.test", undefined);
1473
+
1474
+ expect(aliceOrgOneAgain.id).toBe(aliceOrgOne.id);
1475
+ expect(
1476
+ new Set([aliceOrgOne.id, bobOrgOne.id, aliceOrgTwo.id, alicePersonal.id]),
1477
+ ).toHaveLength(4);
1478
+ const rows = await exec.execute(
1479
+ `SELECT dedupe_key, dedupe_scope, scoped_dedupe_key
1480
+ FROM creative_context_jobs
1481
+ WHERE scoped_dedupe_key = 'same-logical-operation'`,
1482
+ );
1483
+ expect(rows.rows).toHaveLength(4);
1484
+ expect(new Set(rows.rows.map((row) => row.dedupe_scope))).toHaveLength(4);
1485
+ expect(rows.rows.every((row) => row.dedupe_key === null)).toBe(true);
1486
+ });
1487
+
1003
1488
  it("immediately tombstones sources when workspace connection access is removed", async () => {
1004
1489
  const { exec, runWithRequestContext, store } = await setup();
1005
1490
  await exec.execute(