@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.
- package/dist/actions/creative-context-a2a.js +15 -5
- package/dist/actions/creative-context-a2a.js.map +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/generation-artifact-access.d.ts +10 -0
- package/dist/server/generation-artifact-access.d.ts.map +1 -1
- package/dist/server/generation-artifact-access.js +75 -0
- package/dist/server/generation-artifact-access.js.map +1 -1
- package/dist/server/generation-context.d.ts +5 -0
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +36 -3
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/isolated-a2a.d.ts +4 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -1
- package/dist/server/isolated-a2a.js +2 -0
- package/dist/server/isolated-a2a.js.map +1 -1
- package/dist/store/generation.d.ts +15 -0
- package/dist/store/generation.d.ts.map +1 -1
- package/dist/store/generation.js +65 -17
- package/dist/store/generation.js.map +1 -1
- package/package.json +5 -5
- package/src/actions/creative-context-a2a.spec.ts +99 -0
- package/src/actions/creative-context-a2a.ts +22 -5
- package/src/connectors/figma-native.visual.spec.ts +3 -3
- package/src/server/context.ts +2 -0
- package/src/server/generation-artifact-access.spec.ts +45 -0
- package/src/server/generation-artifact-access.ts +117 -0
- package/src/server/generation-context-a2a.spec.ts +76 -8
- package/src/server/generation-context.ts +58 -1
- package/src/server/isolated-a2a.spec.ts +23 -0
- package/src/server/isolated-a2a.ts +4 -0
- package/src/store/access.integration.spec.ts +145 -0
- 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
|
-
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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({
|
package/src/store/generation.ts
CHANGED
|
@@ -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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
:
|
|
196
|
-
|
|
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(
|