@agent-native/creative-context 0.8.36 → 0.8.37-nightly-20261009124314

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 (33) hide show
  1. package/dist/actions/creative-context-a2a.js +15 -5
  2. package/dist/actions/creative-context-a2a.js.map +1 -1
  3. package/dist/server/context.d.ts.map +1 -1
  4. package/dist/server/context.js.map +1 -1
  5. package/dist/server/generation-artifact-access.d.ts +10 -0
  6. package/dist/server/generation-artifact-access.d.ts.map +1 -1
  7. package/dist/server/generation-artifact-access.js +75 -0
  8. package/dist/server/generation-artifact-access.js.map +1 -1
  9. package/dist/server/generation-context.d.ts +5 -0
  10. package/dist/server/generation-context.d.ts.map +1 -1
  11. package/dist/server/generation-context.js +36 -3
  12. package/dist/server/generation-context.js.map +1 -1
  13. package/dist/server/isolated-a2a.d.ts +4 -0
  14. package/dist/server/isolated-a2a.d.ts.map +1 -1
  15. package/dist/server/isolated-a2a.js +2 -0
  16. package/dist/server/isolated-a2a.js.map +1 -1
  17. package/dist/store/generation.d.ts +15 -0
  18. package/dist/store/generation.d.ts.map +1 -1
  19. package/dist/store/generation.js +65 -17
  20. package/dist/store/generation.js.map +1 -1
  21. package/package.json +5 -5
  22. package/src/actions/creative-context-a2a.spec.ts +99 -0
  23. package/src/actions/creative-context-a2a.ts +22 -5
  24. package/src/connectors/figma-native.visual.spec.ts +3 -3
  25. package/src/server/context.ts +2 -0
  26. package/src/server/generation-artifact-access.spec.ts +45 -0
  27. package/src/server/generation-artifact-access.ts +117 -0
  28. package/src/server/generation-context-a2a.spec.ts +76 -8
  29. package/src/server/generation-context.ts +58 -1
  30. package/src/server/isolated-a2a.spec.ts +23 -0
  31. package/src/server/isolated-a2a.ts +4 -0
  32. package/src/store/access.integration.spec.ts +145 -0
  33. package/src/store/generation.ts +109 -16
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
17
17
  getRequestOrgId: vi.fn(),
18
18
  getRequestUserEmail: vi.fn(() => "user@example.test"),
19
19
  createArtifactCapability: vi.fn(),
20
+ createSnapshotCapability: vi.fn(),
20
21
  isCreativeContextLabAvailable: vi.fn(),
21
22
  }));
22
23
 
@@ -74,11 +75,14 @@ vi.mock("./context.js", () => ({
74
75
  vi.mock("./generation-artifact-access.js", () => ({
75
76
  assertGenerationArtifactAccess: vi.fn(),
76
77
  createGenerationArtifactAccessCapability: mocks.createArtifactCapability,
78
+ createGenerationCreativeContextSnapshotCapability:
79
+ mocks.createSnapshotCapability,
77
80
  }));
78
81
 
79
82
  import {
80
83
  getGenerationCreativeContext,
81
84
  recordGenerationCreativeContext,
85
+ recordGenerationCreativeContextFromSnapshot,
82
86
  resolveGenerationCreativeContext,
83
87
  validateGenerationCreativeContext,
84
88
  } from "./generation-context.js";
@@ -99,6 +103,7 @@ describe("generation context isolated A2A routing", () => {
99
103
  mocks.createArtifactCapability.mockImplementation(
100
104
  async (_identity, _target, operation) => `cap-${operation}`,
101
105
  );
106
+ mocks.createSnapshotCapability.mockResolvedValue("snapshot-cap");
102
107
  mocks.isCreativeContextLabAvailable.mockResolvedValue(true);
103
108
  mocks.callA2A.mockResolvedValue(emptyRemoteContext);
104
109
  mocks.listCreativeContexts.mockResolvedValue({ contexts: [] });
@@ -124,6 +129,7 @@ describe("generation context isolated A2A routing", () => {
124
129
  contextMode: "auto",
125
130
  contextPackId: null,
126
131
  reuseLabels: [],
132
+ onlyIfMissing: true,
127
133
  });
128
134
 
129
135
  expect(mocks.callA2A).toHaveBeenNthCalledWith(1, "resolve", {
@@ -147,18 +153,80 @@ describe("generation context isolated A2A routing", () => {
147
153
  },
148
154
  artifactAccessCapability: "cap-read",
149
155
  });
150
- expect(mocks.callA2A).toHaveBeenNthCalledWith(
151
- 4,
152
- "record",
153
- expect.objectContaining({
154
- artifactId: "deck-1",
155
- artifactAccessCapability: "cap-record",
156
- }),
157
- );
156
+ expect(mocks.callA2A).toHaveBeenNthCalledWith(4, "record", {
157
+ appId: "slides",
158
+ artifactType: "deck",
159
+ artifactId: "deck-1",
160
+ contextMode: "auto",
161
+ contextPackId: null,
162
+ reuseLabels: [],
163
+ onlyIfMissing: true,
164
+ artifactAccessCapability: "cap-record",
165
+ });
158
166
  expect(mocks.recordLocal).not.toHaveBeenCalled();
159
167
  expect(mocks.getLocal).not.toHaveBeenCalled();
160
168
  });
161
169
 
170
+ it("routes persisted provenance snapshots without reading mutable settings", async () => {
171
+ const input = {
172
+ appId: "content",
173
+ artifactType: "document",
174
+ artifactId: "document-1",
175
+ contextMode: "pinned" as const,
176
+ contextPackId: "revoked-pack",
177
+ reuseLabels: [
178
+ {
179
+ itemId: "item-1",
180
+ itemVersionId: "item-version-1",
181
+ kind: "brand-voice",
182
+ label: "Brand voice",
183
+ dataRole: "untrusted-reference" as const,
184
+ },
185
+ ],
186
+ elementProvenance: [
187
+ {
188
+ elementId: "document-1",
189
+ influence: "reference-conditioned" as const,
190
+ itemId: "item-1",
191
+ itemVersionId: "item-version-1",
192
+ label: "Brand voice",
193
+ },
194
+ ],
195
+ onlyIfMissing: true,
196
+ };
197
+
198
+ await recordGenerationCreativeContextFromSnapshot(input);
199
+
200
+ expect(mocks.readAppState).not.toHaveBeenCalled();
201
+ expect(mocks.callA2A).toHaveBeenCalledWith("record", {
202
+ ...input,
203
+ snapshotCapability: "snapshot-cap",
204
+ artifactAccessCapability: "cap-record",
205
+ });
206
+ expect(mocks.createSnapshotCapability).toHaveBeenCalledWith(input);
207
+ expect(mocks.recordLocal).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("keeps an explicit local read local when saved mode is auto", async () => {
211
+ const identity = {
212
+ appId: "content",
213
+ artifactType: "document",
214
+ artifactId: "document-1",
215
+ };
216
+ const localRecord = { contextMode: "off", contextPackId: null };
217
+ mocks.getLocal.mockResolvedValue(localRecord);
218
+
219
+ await expect(
220
+ getGenerationCreativeContext(identity, { localOnly: true }),
221
+ ).resolves.toEqual(localRecord);
222
+
223
+ expect(mocks.getLocal).toHaveBeenCalledWith(
224
+ identity,
225
+ expect.objectContaining({ db: undefined }),
226
+ );
227
+ expect(mocks.callA2A).not.toHaveBeenCalled();
228
+ });
229
+
162
230
  it.each([
163
231
  { path: "isolated A2A", hasA2A: true },
164
232
  { path: "local storage", hasA2A: false },
@@ -7,6 +7,7 @@ import {
7
7
  import {
8
8
  getGenerationCreativeContext as getGenerationCreativeContextLocal,
9
9
  recordGenerationCreativeContext as recordGenerationCreativeContextLocal,
10
+ recordGenerationCreativeContextFromSnapshot as recordGenerationCreativeContextFromSnapshotLocal,
10
11
  } from "../store/generation.js";
11
12
  import {
12
13
  createContextPack,
@@ -30,8 +31,10 @@ import { getCreativeContext } from "./context.js";
30
31
  import {
31
32
  assertGenerationArtifactAccess,
32
33
  createGenerationArtifactAccessCapability,
34
+ createGenerationCreativeContextSnapshotCapability,
33
35
  type GenerationArtifactAccessTarget,
34
36
  type GenerationArtifactIdentity,
37
+ type GenerationCreativeContextSnapshot,
35
38
  } from "./generation-artifact-access.js";
36
39
  import {
37
40
  callIsolatedCreativeContextA2A,
@@ -617,6 +620,58 @@ export async function recordGenerationCreativeContext(
617
620
  });
618
621
  }
619
622
 
623
+ export async function recordGenerationCreativeContextFromSnapshot(
624
+ input: Omit<
625
+ IsolatedRecordPayload,
626
+ "artifactAccessCapability" | "snapshotCapability"
627
+ >,
628
+ options: { db?: any; artifactAccess?: GenerationArtifactAccessTarget } = {},
629
+ ) {
630
+ if (!input.onlyIfMissing) {
631
+ throw new Error("Creative Context snapshots must be recorded idempotently");
632
+ }
633
+ if (!(await creativeContextLabEnabled())) return null;
634
+ const artifactAccessTarget = collaborativeArtifactTarget(
635
+ input,
636
+ options.artifactAccess,
637
+ );
638
+ if (
639
+ input.contextMode !== "off" &&
640
+ !options.db &&
641
+ hasIsolatedCreativeContextA2A()
642
+ ) {
643
+ const snapshot = {
644
+ ...input,
645
+ onlyIfMissing: true,
646
+ } satisfies GenerationCreativeContextSnapshot;
647
+ const snapshotCapability =
648
+ await createGenerationCreativeContextSnapshotCapability(snapshot);
649
+ const artifactAccessCapability = artifactAccessTarget
650
+ ? await createGenerationArtifactAccessCapability(
651
+ input,
652
+ artifactAccessTarget,
653
+ "record",
654
+ )
655
+ : undefined;
656
+ return callIsolatedCreativeContextA2A("record", {
657
+ ...snapshot,
658
+ snapshotCapability,
659
+ artifactAccessCapability,
660
+ });
661
+ }
662
+ const artifactAccess = artifactAccessTarget
663
+ ? await assertGenerationArtifactAccess(
664
+ input,
665
+ artifactAccessTarget,
666
+ "record",
667
+ )
668
+ : undefined;
669
+ return recordGenerationCreativeContextFromSnapshotLocal(input, {
670
+ db: options.db,
671
+ artifactAccess,
672
+ });
673
+ }
674
+
620
675
  export async function getGenerationCreativeContext(
621
676
  input: {
622
677
  appId: string;
@@ -626,6 +681,7 @@ export async function getGenerationCreativeContext(
626
681
  options: {
627
682
  artifactAccess?: GenerationArtifactAccessTarget;
628
683
  db?: any;
684
+ localOnly?: boolean;
629
685
  } = {},
630
686
  ) {
631
687
  if (!(await creativeContextLabEnabled())) return null;
@@ -633,7 +689,7 @@ export async function getGenerationCreativeContext(
633
689
  input,
634
690
  options.artifactAccess,
635
691
  );
636
- if (!options.db && hasIsolatedCreativeContextA2A()) {
692
+ if (!options.db && !options.localOnly && hasIsolatedCreativeContextA2A()) {
637
693
  const state = (await readAppState("creative-context")) as {
638
694
  contextMode?: "auto" | "off";
639
695
  } | null;
@@ -656,5 +712,6 @@ export async function getGenerationCreativeContext(
656
712
  : undefined;
657
713
  return getGenerationCreativeContextLocal(input, {
658
714
  artifactAccess,
715
+ db: options.db,
659
716
  });
660
717
  }
@@ -174,6 +174,29 @@ describe("isolated Creative Context A2A", () => {
174
174
  expect(() => decodeCreativeContextA2ARequest(requestToken)).toThrow();
175
175
  });
176
176
 
177
+ it("rejects the unsigned persisted-snapshot bypass flag", () => {
178
+ const requestToken = Buffer.from(
179
+ JSON.stringify({
180
+ protocol: "creative-context-a2a-v1",
181
+ requestId: "87f466ae-32f4-4d0f-9de7-96f955e69f7b",
182
+ operation: "record",
183
+ payload: {
184
+ appId: "slides",
185
+ artifactType: "deck",
186
+ artifactId: "guessed-deck",
187
+ contextMode: "pinned",
188
+ contextPackId: "private-pack",
189
+ reuseLabels: [],
190
+ onlyIfMissing: true,
191
+ persistedSnapshot: true,
192
+ },
193
+ }),
194
+ "utf8",
195
+ ).toString("base64url");
196
+
197
+ expect(() => decodeCreativeContextA2ARequest(requestToken)).toThrow();
198
+ });
199
+
177
200
  it("fails closed when the isolated URL has no signing secret", async () => {
178
201
  delete process.env.CREATIVE_CONTEXT_A2A_KEY;
179
202
  delete process.env.A2A_SECRET;
@@ -56,6 +56,8 @@ const generationRecordInputSchema = generationIdentitySchema
56
56
  contextPackId: boundedId.nullable(),
57
57
  reuseLabels: z.array(reuseLabelSchema).max(100),
58
58
  elementProvenance: z.array(elementProvenanceSchema).max(500).optional(),
59
+ onlyIfMissing: z.boolean().optional(),
60
+ snapshotCapability: z.string().min(1).max(8_192).optional(),
59
61
  })
60
62
  .strict();
61
63
  const artifactAccessCapabilitySchema = z.string().min(1).max(8_192);
@@ -412,5 +414,7 @@ export type IsolatedRecordPayload = {
412
414
  contextPackId: string | null;
413
415
  reuseLabels: CreativeContextReuseLabel[];
414
416
  elementProvenance?: CreativeContextElementProvenance[];
417
+ onlyIfMissing?: boolean;
418
+ snapshotCapability?: string;
415
419
  artifactAccessCapability?: string;
416
420
  };
@@ -607,6 +607,151 @@ describe("creative context access and revocation", () => {
607
607
  ).rejects.toThrow(/verified by the host application/i);
608
608
  });
609
609
 
610
+ it("records one generation context for concurrent create retries", async () => {
611
+ const { exec, runWithRequestContext, store } = await setup();
612
+ const input = {
613
+ appId: "content",
614
+ artifactType: "document",
615
+ artifactId: "concurrent-create-retry",
616
+ contextMode: "off" as const,
617
+ contextPackId: null,
618
+ reuseLabels: [],
619
+ elementProvenance: [
620
+ {
621
+ elementId: "concurrent-create-retry",
622
+ influence: "generated" as const,
623
+ },
624
+ ],
625
+ onlyIfMissing: true,
626
+ };
627
+ const record = () =>
628
+ runWithRequestContext({ userEmail: "alice@example.test" }, () =>
629
+ store.recordGenerationCreativeContext(input),
630
+ );
631
+
632
+ const records = await Promise.all([record(), record(), record()]);
633
+ expect(new Set(records.map((entry) => entry.id)).size).toBe(1);
634
+ const stored = await exec.execute({
635
+ sql: `SELECT id FROM creative_context_generation_records
636
+ WHERE app_id = ? AND artifact_type = ? AND artifact_id = ?
637
+ AND owner_email = ? AND org_id IS NULL`,
638
+ args: ["content", "document", input.artifactId, "alice@example.test"],
639
+ });
640
+ expect(stored.rows).toHaveLength(1);
641
+ expect(stored.rows[0]?.id).toBe(records[0]?.id);
642
+ });
643
+
644
+ it("records a validated provenance snapshot after its context pack is revoked", async () => {
645
+ const { runWithRequestContext, store } = await setup();
646
+ const input = {
647
+ appId: "content",
648
+ artifactType: "document",
649
+ artifactId: "snapshot-after-pack-revocation",
650
+ contextMode: "pinned" as const,
651
+ contextPackId: "pack-that-is-no-longer-accessible",
652
+ reuseLabels: [
653
+ {
654
+ itemId: "historical-item",
655
+ itemVersionId: "historical-item-version",
656
+ kind: "brand-voice",
657
+ label: "Saved voice",
658
+ dataRole: "untrusted-reference" as const,
659
+ influence: "reference-conditioned" as const,
660
+ },
661
+ ],
662
+ elementProvenance: [
663
+ {
664
+ elementId: "snapshot-after-pack-revocation",
665
+ influence: "reference-conditioned" as const,
666
+ itemId: "historical-item",
667
+ itemVersionId: "historical-item-version",
668
+ label: "Saved voice",
669
+ },
670
+ ],
671
+ onlyIfMissing: true,
672
+ };
673
+
674
+ await expect(
675
+ runWithRequestContext({ userEmail: "alice@example.test" }, () =>
676
+ store.recordGenerationCreativeContextFromSnapshot(input),
677
+ ),
678
+ ).resolves.toMatchObject({
679
+ artifactId: input.artifactId,
680
+ contextMode: "pinned",
681
+ contextPackId: input.contextPackId,
682
+ elementProvenance: input.elementProvenance,
683
+ });
684
+ });
685
+
686
+ it("repairs a missing generation projection when onlyIfMissing is retried", async () => {
687
+ const { runWithRequestContext, store } = await setup();
688
+ const server = await import("../server/index.js");
689
+ const projectionRecord = vi
690
+ .fn()
691
+ .mockRejectedValueOnce(new Error("projection unavailable"))
692
+ .mockResolvedValue(undefined);
693
+ server.configureCreativeContext({
694
+ projections: { generation: { record: projectionRecord } },
695
+ });
696
+
697
+ const input = {
698
+ appId: "content",
699
+ artifactType: "document",
700
+ artifactId: "projection-retry",
701
+ contextMode: "off" as const,
702
+ contextPackId: null,
703
+ reuseLabels: [],
704
+ elementProvenance: [
705
+ { elementId: "original-element", influence: "generated" as const },
706
+ ],
707
+ onlyIfMissing: true,
708
+ };
709
+ const record = (request: typeof input) =>
710
+ runWithRequestContext({ userEmail: "alice@example.test" }, () =>
711
+ store.recordGenerationCreativeContext(request),
712
+ );
713
+
714
+ await expect(record(input)).rejects.toThrow("projection unavailable");
715
+ expect(projectionRecord).toHaveBeenCalledTimes(1);
716
+
717
+ const persisted = await runWithRequestContext(
718
+ { userEmail: "alice@example.test" },
719
+ () =>
720
+ store.getGenerationCreativeContext({
721
+ appId: input.appId,
722
+ artifactType: input.artifactType,
723
+ artifactId: input.artifactId,
724
+ }),
725
+ );
726
+ expect(persisted).toMatchObject({
727
+ elementProvenance: input.elementProvenance,
728
+ });
729
+
730
+ const retry = {
731
+ ...input,
732
+ elementProvenance: [
733
+ { elementId: "retry-element", influence: "generated" as const },
734
+ ],
735
+ };
736
+ const retried = await record(retry);
737
+
738
+ expect(retried).toMatchObject({
739
+ id: persisted?.id,
740
+ elementProvenance: input.elementProvenance,
741
+ });
742
+ expect(projectionRecord).toHaveBeenCalledTimes(2);
743
+ expect(projectionRecord).toHaveBeenNthCalledWith(
744
+ 2,
745
+ expect.objectContaining({
746
+ appId: input.appId,
747
+ artifactType: input.artifactType,
748
+ artifactId: input.artifactId,
749
+ contextPackId: null,
750
+ elementProvenance: input.elementProvenance,
751
+ }),
752
+ );
753
+ });
754
+
610
755
  it("appends media enrichment while preserving version-pinned pack evidence", async () => {
611
756
  const { exec, runWithRequestContext, store } = await setup();
612
757
  await exec.execute({
@@ -1,5 +1,5 @@
1
1
  import { assertAccess } from "@agent-native/core/sharing";
2
- import { and, desc, eq } from "drizzle-orm";
2
+ import { and, desc, eq, isNull, sql } from "drizzle-orm";
3
3
 
4
4
  import { getCreativeContext } from "../server/context.js";
5
5
  import {
@@ -78,11 +78,54 @@ export async function recordGenerationCreativeContext(
78
78
  contextPackId: string | null;
79
79
  reuseLabels: CreativeContextReuseLabel[];
80
80
  elementProvenance?: CreativeContextElementProvenance[];
81
+ onlyIfMissing?: boolean;
81
82
  },
82
83
  options: {
83
84
  db?: any;
84
85
  artifactAccess?: GenerationArtifactAccessProof;
85
86
  } = {},
87
+ ): Promise<CreativeContextGenerationRecord> {
88
+ return recordGenerationCreativeContextInternal(input, options, false);
89
+ }
90
+
91
+ export async function recordGenerationCreativeContextFromSnapshot(
92
+ input: {
93
+ appId: string;
94
+ artifactType: string;
95
+ artifactId: string;
96
+ contextMode: "off" | "auto" | "pinned";
97
+ contextPackId: string | null;
98
+ reuseLabels: CreativeContextReuseLabel[];
99
+ elementProvenance?: CreativeContextElementProvenance[];
100
+ onlyIfMissing?: boolean;
101
+ },
102
+ options: {
103
+ db?: any;
104
+ artifactAccess?: GenerationArtifactAccessProof;
105
+ } = {},
106
+ ): Promise<CreativeContextGenerationRecord> {
107
+ if (!input.onlyIfMissing) {
108
+ throw new Error("Creative Context snapshots must be recorded idempotently");
109
+ }
110
+ return recordGenerationCreativeContextInternal(input, options, true);
111
+ }
112
+
113
+ async function recordGenerationCreativeContextInternal(
114
+ input: {
115
+ appId: string;
116
+ artifactType: string;
117
+ artifactId: string;
118
+ contextMode: "off" | "auto" | "pinned";
119
+ contextPackId: string | null;
120
+ reuseLabels: CreativeContextReuseLabel[];
121
+ elementProvenance?: CreativeContextElementProvenance[];
122
+ onlyIfMissing?: boolean;
123
+ },
124
+ options: {
125
+ db?: any;
126
+ artifactAccess?: GenerationArtifactAccessProof;
127
+ },
128
+ fromValidatedSnapshot: boolean,
86
129
  ): Promise<CreativeContextGenerationRecord> {
87
130
  const elementProvenance =
88
131
  input.elementProvenance ??
@@ -99,7 +142,7 @@ export async function recordGenerationCreativeContext(
99
142
  reuseLabels: input.reuseLabels,
100
143
  elementProvenance,
101
144
  });
102
- if (input.contextPackId) {
145
+ if (input.contextPackId && !fromValidatedSnapshot) {
103
146
  await assertAccess(
104
147
  "creative-context-pack",
105
148
  input.contextPackId,
@@ -108,7 +151,7 @@ export async function recordGenerationCreativeContext(
108
151
  { skipResourceBody: true },
109
152
  );
110
153
  }
111
- if (input.contextPackId) {
154
+ if (input.contextPackId && !fromValidatedSnapshot) {
112
155
  const { getDb, schema } = getCreativeContext();
113
156
  const db = options.db ?? getDb();
114
157
  const members = await db
@@ -156,18 +199,64 @@ export async function recordGenerationCreativeContext(
156
199
  ownerEmail: actor.ownerEmail,
157
200
  orgId: options.artifactAccess ? actor.orgId : null,
158
201
  };
159
- await db.insert(schema.generationRecords).values(row);
160
- await getCreativeContext().projections?.generation?.record({
161
- appId: input.appId,
162
- artifactType: input.artifactType,
163
- artifactId: input.artifactId,
164
- contextPackId: input.contextPackId,
165
- elementProvenance,
166
- });
167
- return {
168
- ...row,
169
- elementProvenance: parseJson(row.elementProvenance, []),
202
+ let recorded: {
203
+ record: CreativeContextGenerationRecord;
204
+ inserted: boolean;
170
205
  };
206
+ if (input.onlyIfMissing) {
207
+ // Matching retries can reach separate server processes, so coordinate in Postgres.
208
+ const scopeKey = options.artifactAccess
209
+ ? ["org", actor.orgId, input.appId, input.artifactType, input.artifactId]
210
+ : [
211
+ "owner",
212
+ actor.ownerEmail,
213
+ input.appId,
214
+ input.artifactType,
215
+ input.artifactId,
216
+ ];
217
+ recorded = await db.transaction(async (tx: any) => {
218
+ await tx.execute(
219
+ sql`SELECT pg_advisory_xact_lock(hashtextextended(${`creative-context-generation:${JSON.stringify(scopeKey)}`}, 0::bigint))`,
220
+ );
221
+ const existing = await getGenerationCreativeContext(
222
+ {
223
+ appId: input.appId,
224
+ artifactType: input.artifactType,
225
+ artifactId: input.artifactId,
226
+ },
227
+ { artifactAccess: options.artifactAccess, db: tx },
228
+ );
229
+ if (existing) return { record: existing, inserted: false };
230
+
231
+ await tx.insert(schema.generationRecords).values(row);
232
+ return {
233
+ record: {
234
+ ...row,
235
+ elementProvenance: parseJson(row.elementProvenance, []),
236
+ },
237
+ inserted: true,
238
+ };
239
+ });
240
+ } else {
241
+ await db.insert(schema.generationRecords).values(row);
242
+ recorded = {
243
+ record: {
244
+ ...row,
245
+ elementProvenance: parseJson(row.elementProvenance, []),
246
+ },
247
+ inserted: true,
248
+ };
249
+ }
250
+ if (recorded.inserted || input.onlyIfMissing) {
251
+ await getCreativeContext().projections?.generation?.record({
252
+ appId: recorded.record.appId,
253
+ artifactType: recorded.record.artifactType,
254
+ artifactId: recorded.record.artifactId,
255
+ contextPackId: recorded.record.contextPackId,
256
+ elementProvenance: recorded.record.elementProvenance,
257
+ });
258
+ }
259
+ return recorded.record;
171
260
  }
172
261
 
173
262
  export async function getGenerationCreativeContext(
@@ -178,6 +267,7 @@ export async function getGenerationCreativeContext(
178
267
  },
179
268
  options: {
180
269
  artifactAccess?: GenerationArtifactAccessProof;
270
+ db?: any;
181
271
  } = {},
182
272
  ): Promise<CreativeContextGenerationRecord | null> {
183
273
  const { getDb, schema } = getCreativeContext();
@@ -192,8 +282,11 @@ export async function getGenerationCreativeContext(
192
282
  }
193
283
  const actorScope = options.artifactAccess
194
284
  ? eq(schema.generationRecords.orgId, actor.orgId!)
195
- : eq(schema.generationRecords.ownerEmail, actor.ownerEmail);
196
- const rows = await getDb()
285
+ : and(
286
+ eq(schema.generationRecords.ownerEmail, actor.ownerEmail),
287
+ isNull(schema.generationRecords.orgId),
288
+ );
289
+ const rows = await (options.db ?? getDb())
197
290
  .select()
198
291
  .from(schema.generationRecords)
199
292
  .where(