@agent-native/creative-context 0.3.0 → 0.5.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.
- package/README.md +51 -9
- package/agent-native.package.json +4 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-context-memberships.d.ts +11 -0
- package/dist/actions/list-context-memberships.d.ts.map +1 -0
- package/dist/actions/list-context-memberships.js +47 -0
- package/dist/actions/list-context-memberships.js.map +1 -0
- package/dist/actions/list-creative-contexts.d.ts +12 -0
- package/dist/actions/list-creative-contexts.d.ts.map +1 -0
- package/dist/actions/list-creative-contexts.js +28 -0
- package/dist/actions/list-creative-contexts.js.map +1 -0
- package/dist/actions/manage-context-membership.d.ts +64 -0
- package/dist/actions/manage-context-membership.d.ts.map +1 -0
- package/dist/actions/manage-context-membership.js +70 -0
- package/dist/actions/manage-context-membership.js.map +1 -0
- package/dist/actions/manage-context-pack.d.ts +3 -0
- package/dist/actions/manage-context-pack.js +6 -0
- package/dist/actions/manage-context-pack.js.map +1 -1
- package/dist/actions/manage-context-source.js +4 -11
- package/dist/actions/manage-context-source.js.map +1 -1
- package/dist/actions/manage-creative-context.d.ts +28 -0
- package/dist/actions/manage-creative-context.d.ts.map +1 -0
- package/dist/actions/manage-creative-context.js +65 -0
- package/dist/actions/manage-creative-context.js.map +1 -0
- package/dist/actions/process-context-purge.d.ts +16 -1
- package/dist/actions/process-context-purge.js +2 -1
- package/dist/actions/process-context-purge.js.map +1 -1
- package/dist/actions/search-creative-context.d.ts +1 -0
- package/dist/actions/search-creative-context.js +2 -1
- package/dist/actions/search-creative-context.js.map +1 -1
- package/dist/client/CreativeContextChip.d.ts +5 -1
- package/dist/client/CreativeContextChip.d.ts.map +1 -1
- package/dist/client/CreativeContextChip.js +36 -7
- package/dist/client/CreativeContextChip.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +406 -89
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/CreativeContextShareTab.d.ts +56 -0
- package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
- package/dist/client/CreativeContextShareTab.js +213 -0
- package/dist/client/CreativeContextShareTab.js.map +1 -0
- package/dist/client/actions.d.ts +210 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +318 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/application-state.d.ts +2 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +25 -2
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +77 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/types.d.ts +2 -2
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1592 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +95 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +80 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/server/generation-artifact-access.js +2 -0
- package/dist/server/generation-artifact-access.js.map +1 -1
- package/dist/server/generation-context.d.ts +12 -4
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +129 -7
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +16 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/isolated-a2a.d.ts +3 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -1
- package/dist/server/isolated-a2a.js +3 -1
- package/dist/server/isolated-a2a.js.map +1 -1
- package/dist/server/media.d.ts +1 -1
- package/dist/server/media.d.ts.map +1 -1
- package/dist/server/media.js +24 -9
- package/dist/server/media.js.map +1 -1
- package/dist/server/native-resource-capture.d.ts +78 -0
- package/dist/server/native-resource-capture.d.ts.map +1 -0
- package/dist/server/native-resource-capture.js +94 -0
- package/dist/server/native-resource-capture.js.map +1 -0
- package/dist/server/public-serialization.js +1 -1
- package/dist/server/public-serialization.js.map +1 -1
- package/dist/server/retrieval.d.ts +1 -0
- package/dist/server/retrieval.d.ts.map +1 -1
- package/dist/server/retrieval.js +2 -0
- package/dist/server/retrieval.js.map +1 -1
- package/dist/server/safe-native-preview.d.ts +15 -0
- package/dist/server/safe-native-preview.d.ts.map +1 -0
- package/dist/server/safe-native-preview.js +149 -0
- package/dist/server/safe-native-preview.js.map +1 -0
- package/dist/store/brand.d.ts.map +1 -1
- package/dist/store/brand.js +2 -16
- package/dist/store/brand.js.map +1 -1
- package/dist/store/content.d.ts +1 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +84 -20
- package/dist/store/content.js.map +1 -1
- package/dist/store/contexts.d.ts +216 -0
- package/dist/store/contexts.d.ts.map +1 -0
- package/dist/store/contexts.js +1341 -0
- package/dist/store/contexts.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/packs.d.ts +3 -0
- package/dist/store/packs.d.ts.map +1 -1
- package/dist/store/packs.js +126 -9
- package/dist/store/packs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +103 -6
- package/dist/store/purge.js.map +1 -1
- package/dist/store/sources.d.ts.map +1 -1
- package/dist/store/sources.js +2 -15
- package/dist/store/sources.js.map +1 -1
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +3 -1
- package/dist/store/suggestions.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +13 -5
- package/package.json +3 -3
- package/src/actions/index.ts +8 -0
- package/src/actions/job-public-serialization.spec.ts +21 -0
- package/src/actions/list-context-memberships.ts +56 -0
- package/src/actions/list-creative-contexts.spec.ts +38 -0
- package/src/actions/list-creative-contexts.ts +32 -0
- package/src/actions/manage-context-membership.ts +80 -0
- package/src/actions/manage-context-pack.ts +6 -0
- package/src/actions/manage-context-source.ts +8 -11
- package/src/actions/manage-creative-context.ts +73 -0
- package/src/actions/process-context-purge.ts +4 -1
- package/src/actions/search-creative-context.ts +2 -1
- package/src/client/CreativeContextChip.spec.ts +32 -0
- package/src/client/CreativeContextChip.tsx +83 -14
- package/src/client/CreativeContextPanel.tsx +1947 -885
- package/src/client/CreativeContextShareTab.spec.ts +70 -0
- package/src/client/CreativeContextShareTab.tsx +657 -0
- package/src/client/actions.spec.ts +177 -0
- package/src/client/actions.ts +566 -0
- package/src/client/application-state.spec.ts +5 -0
- package/src/client/application-state.ts +30 -2
- package/src/client/index.ts +33 -0
- package/src/client/messages.ts +95 -0
- package/src/connectors/types.ts +6 -8
- package/src/eval/library-flow.acceptance.spec.tsx +1 -0
- package/src/schema/index.ts +116 -0
- package/src/schema/migrations.ts +80 -0
- package/src/server/generation-artifact-access.ts +1 -0
- package/src/server/generation-context-a2a.spec.ts +133 -0
- package/src/server/generation-context.ts +153 -7
- package/src/server/index.spec.ts +15 -0
- package/src/server/index.ts +16 -0
- package/src/server/isolated-a2a.ts +4 -1
- package/src/server/media.spec.ts +30 -2
- package/src/server/media.ts +31 -12
- package/src/server/native-resource-capture.spec.ts +121 -0
- package/src/server/native-resource-capture.ts +198 -0
- package/src/server/public-serialization.spec.ts +1 -0
- package/src/server/public-serialization.ts +1 -1
- package/src/server/retrieval.ts +3 -0
- package/src/server/safe-native-preview.spec.ts +29 -0
- package/src/server/safe-native-preview.ts +200 -0
- package/src/store/access.integration.spec.ts +1116 -0
- package/src/store/brand.ts +2 -21
- package/src/store/content.ts +217 -17
- package/src/store/contexts.ts +1826 -0
- package/src/store/index.ts +1 -0
- package/src/store/packs.ts +279 -24
- package/src/store/purge.ts +139 -6
- package/src/store/sources.ts +2 -18
- package/src/store/suggestions.ts +3 -1
- package/src/types.ts +95 -0
|
@@ -1266,6 +1266,762 @@ describe("creative context access and revocation", () => {
|
|
|
1266
1266
|
).rejects.toThrow("unavailable at the pinned source version");
|
|
1267
1267
|
});
|
|
1268
1268
|
|
|
1269
|
+
it("publishes trusted Slides and Figma hierarchies without losing compiler metadata or child pins", async () => {
|
|
1270
|
+
const { runWithRequestContext, store } = await setup();
|
|
1271
|
+
const { reassembleNativeCreativeArtifact } =
|
|
1272
|
+
await import("../native-artifact-reassembly.js");
|
|
1273
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1274
|
+
runWithRequestContext(
|
|
1275
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
1276
|
+
fn,
|
|
1277
|
+
);
|
|
1278
|
+
const asBob = <T>(fn: () => Promise<T>) =>
|
|
1279
|
+
runWithRequestContext(
|
|
1280
|
+
{ userEmail: "bob@example.test", orgId: "org-1" },
|
|
1281
|
+
fn,
|
|
1282
|
+
);
|
|
1283
|
+
const fidelityReport = {
|
|
1284
|
+
exact: { count: 1 },
|
|
1285
|
+
approximated: { count: 0, reasons: [] },
|
|
1286
|
+
imageFallback: { count: 0, reasons: [] },
|
|
1287
|
+
};
|
|
1288
|
+
for (const fixture of [
|
|
1289
|
+
{
|
|
1290
|
+
app: "slides" as const,
|
|
1291
|
+
format: "slides-html" as const,
|
|
1292
|
+
compiler: "@agent-native/creative-context:google-slides-native",
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
app: "design" as const,
|
|
1296
|
+
format: "design-html" as const,
|
|
1297
|
+
compiler: "@agent-native/core/ingestion:figma-node-to-html",
|
|
1298
|
+
},
|
|
1299
|
+
]) {
|
|
1300
|
+
const rootExternalId = `${fixture.app}:published-root`;
|
|
1301
|
+
const childExternalId = `${fixture.app}:published-child`;
|
|
1302
|
+
const rootHtml = `<!doctype html><html><head></head><body><div data-creative-context-child="${childExternalId}"></div></body></html>`;
|
|
1303
|
+
const childHtml = `<!doctype html><html><head></head><body><div>${fixture.app} child</div></body></html>`;
|
|
1304
|
+
const ingested = await asAlice(() =>
|
|
1305
|
+
store.ingestItems({
|
|
1306
|
+
sourceId: "source-1",
|
|
1307
|
+
items: [
|
|
1308
|
+
{
|
|
1309
|
+
externalId: rootExternalId,
|
|
1310
|
+
kind: "native-manifest",
|
|
1311
|
+
title: `${fixture.app} root`,
|
|
1312
|
+
mimeType: "text/html",
|
|
1313
|
+
content: rootHtml,
|
|
1314
|
+
contentHash: `${fixture.app}-root-v1`,
|
|
1315
|
+
provenance: { compiler: fixture.compiler },
|
|
1316
|
+
metadata: {
|
|
1317
|
+
nativeArtifact: {
|
|
1318
|
+
schemaVersion: 1,
|
|
1319
|
+
app: fixture.app,
|
|
1320
|
+
format: fixture.format,
|
|
1321
|
+
rootExternalId,
|
|
1322
|
+
childExternalIds: [childExternalId],
|
|
1323
|
+
manifest: {
|
|
1324
|
+
kind: "hierarchical-artboard",
|
|
1325
|
+
children: [
|
|
1326
|
+
{
|
|
1327
|
+
externalId: childExternalId,
|
|
1328
|
+
sourceNodeId: "child-1",
|
|
1329
|
+
bounds: { x: 0, y: 0, width: 10, height: 10 },
|
|
1330
|
+
zOrder: 0,
|
|
1331
|
+
},
|
|
1332
|
+
],
|
|
1333
|
+
},
|
|
1334
|
+
fidelityReport,
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
edges: [
|
|
1338
|
+
{
|
|
1339
|
+
relation: "contains-native-child",
|
|
1340
|
+
toExternalId: childExternalId,
|
|
1341
|
+
},
|
|
1342
|
+
],
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
externalId: childExternalId,
|
|
1346
|
+
kind: "native-child",
|
|
1347
|
+
title: `${fixture.app} child`,
|
|
1348
|
+
mimeType: "text/html",
|
|
1349
|
+
content: childHtml,
|
|
1350
|
+
contentHash: `${fixture.app}-child-v1`,
|
|
1351
|
+
provenance: { compiler: fixture.compiler },
|
|
1352
|
+
metadata: {
|
|
1353
|
+
nativeArtifact: {
|
|
1354
|
+
schemaVersion: 1,
|
|
1355
|
+
app: fixture.app,
|
|
1356
|
+
format: fixture.format,
|
|
1357
|
+
rootExternalId: childExternalId,
|
|
1358
|
+
fidelityReport,
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
],
|
|
1363
|
+
}),
|
|
1364
|
+
);
|
|
1365
|
+
const context = await asAlice(() =>
|
|
1366
|
+
store.createCreativeContext({
|
|
1367
|
+
name: `${fixture.app} fidelity`,
|
|
1368
|
+
kind: "specialty",
|
|
1369
|
+
}),
|
|
1370
|
+
);
|
|
1371
|
+
await asAlice(() =>
|
|
1372
|
+
store.manageContextMembership({
|
|
1373
|
+
operation: "submit",
|
|
1374
|
+
contextId: context!.id,
|
|
1375
|
+
itemId: ingested.itemIds[0],
|
|
1376
|
+
confirmBroaderPublication: true,
|
|
1377
|
+
}),
|
|
1378
|
+
);
|
|
1379
|
+
const membership = (
|
|
1380
|
+
await asAlice(() =>
|
|
1381
|
+
store.listContextMemberships({ contextId: context!.id, limit: 20 }),
|
|
1382
|
+
)
|
|
1383
|
+
).memberships.find(
|
|
1384
|
+
(entry) => entry.artifactKey === `source-1:${rootExternalId}`,
|
|
1385
|
+
)!;
|
|
1386
|
+
const published = await asBob(() =>
|
|
1387
|
+
store.getCreativeContextItem(
|
|
1388
|
+
membership.publishedItemId!,
|
|
1389
|
+
membership.publishedItemVersionId!,
|
|
1390
|
+
),
|
|
1391
|
+
);
|
|
1392
|
+
expect(published?.item.provenance.compiler).toBe(fixture.compiler);
|
|
1393
|
+
expect(published?.version.metadata).toMatchObject({
|
|
1394
|
+
nativeArtifact: { app: fixture.app, format: fixture.format },
|
|
1395
|
+
});
|
|
1396
|
+
const reassembled = await asBob(() =>
|
|
1397
|
+
reassembleNativeCreativeArtifact({
|
|
1398
|
+
root: published!,
|
|
1399
|
+
app: fixture.app,
|
|
1400
|
+
format: fixture.format,
|
|
1401
|
+
resolveChild: store.getCreativeContextItemByExternalId,
|
|
1402
|
+
}),
|
|
1403
|
+
);
|
|
1404
|
+
expect(reassembled.html).toContain(`${fixture.app} child`);
|
|
1405
|
+
expect(reassembled.evidence).toHaveLength(2);
|
|
1406
|
+
|
|
1407
|
+
const childEdge = published!.edges.find(
|
|
1408
|
+
(edge) => edge.relation === "contains-native-child",
|
|
1409
|
+
);
|
|
1410
|
+
expect(childEdge).toMatchObject({
|
|
1411
|
+
toItemId: expect.any(String),
|
|
1412
|
+
toItemVersionId: expect.any(String),
|
|
1413
|
+
});
|
|
1414
|
+
const pack = await asAlice(() =>
|
|
1415
|
+
store.createContextPack({
|
|
1416
|
+
name: `${fixture.app} hierarchical replay`,
|
|
1417
|
+
baseContextId: context!.id,
|
|
1418
|
+
members: [
|
|
1419
|
+
{
|
|
1420
|
+
itemId: published!.item.id,
|
|
1421
|
+
itemVersionId: published!.version.id,
|
|
1422
|
+
reason: "Explicit root",
|
|
1423
|
+
score: 0.91,
|
|
1424
|
+
scoreMetadata: { source: "test" },
|
|
1425
|
+
},
|
|
1426
|
+
],
|
|
1427
|
+
}),
|
|
1428
|
+
);
|
|
1429
|
+
expect(pack.members).toEqual([
|
|
1430
|
+
expect.objectContaining({
|
|
1431
|
+
itemId: published!.item.id,
|
|
1432
|
+
itemVersionId: published!.version.id,
|
|
1433
|
+
ordinal: 0,
|
|
1434
|
+
reason: "Explicit root",
|
|
1435
|
+
score: 0.91,
|
|
1436
|
+
scoreMetadata: { source: "test" },
|
|
1437
|
+
}),
|
|
1438
|
+
expect.objectContaining({
|
|
1439
|
+
itemId: childEdge!.toItemId,
|
|
1440
|
+
itemVersionId: childEdge!.toItemVersionId,
|
|
1441
|
+
ordinal: 1,
|
|
1442
|
+
reason: "Native artifact dependency",
|
|
1443
|
+
score: null,
|
|
1444
|
+
}),
|
|
1445
|
+
]);
|
|
1446
|
+
await asAlice(() =>
|
|
1447
|
+
store.manageContextMembership({
|
|
1448
|
+
operation: "remove",
|
|
1449
|
+
contextId: context!.id,
|
|
1450
|
+
membershipId: membership.id,
|
|
1451
|
+
}),
|
|
1452
|
+
);
|
|
1453
|
+
const removedMemberships = await asAlice(() =>
|
|
1454
|
+
store.listContextMemberships({ contextId: context!.id, limit: 20 }),
|
|
1455
|
+
);
|
|
1456
|
+
expect(
|
|
1457
|
+
removedMemberships.memberships.filter(
|
|
1458
|
+
(entry) =>
|
|
1459
|
+
entry.publishedItemId === published!.item.id ||
|
|
1460
|
+
entry.publishedItemId === childEdge!.toItemId,
|
|
1461
|
+
),
|
|
1462
|
+
).toEqual(
|
|
1463
|
+
expect.arrayContaining([
|
|
1464
|
+
expect.objectContaining({
|
|
1465
|
+
publishedItemId: published!.item.id,
|
|
1466
|
+
status: "removed",
|
|
1467
|
+
}),
|
|
1468
|
+
expect.objectContaining({
|
|
1469
|
+
publishedItemId: childEdge!.toItemId,
|
|
1470
|
+
status: "removed",
|
|
1471
|
+
}),
|
|
1472
|
+
]),
|
|
1473
|
+
);
|
|
1474
|
+
const replayPack = await asBob(() => store.getContextPack(pack.id));
|
|
1475
|
+
expect(replayPack?.members).toHaveLength(2);
|
|
1476
|
+
const replayRoot = await asBob(() =>
|
|
1477
|
+
store.getCreativeContextItem(published!.item.id, published!.version.id),
|
|
1478
|
+
);
|
|
1479
|
+
const replayChild = await asBob(() =>
|
|
1480
|
+
store.getCreativeContextItem(
|
|
1481
|
+
childEdge!.toItemId!,
|
|
1482
|
+
childEdge!.toItemVersionId!,
|
|
1483
|
+
),
|
|
1484
|
+
);
|
|
1485
|
+
expect(replayRoot?.version.id).toBe(published!.version.id);
|
|
1486
|
+
expect(replayChild?.version.id).toBe(childEdge!.toItemVersionId);
|
|
1487
|
+
const replayed = await asBob(() =>
|
|
1488
|
+
reassembleNativeCreativeArtifact({
|
|
1489
|
+
root: replayRoot!,
|
|
1490
|
+
app: fixture.app,
|
|
1491
|
+
format: fixture.format,
|
|
1492
|
+
resolveChild: store.getCreativeContextItemByExternalId,
|
|
1493
|
+
}),
|
|
1494
|
+
);
|
|
1495
|
+
expect(replayed.html).toContain(`${fixture.app} child`);
|
|
1496
|
+
expect(replayed.evidence).toEqual(
|
|
1497
|
+
expect.arrayContaining([
|
|
1498
|
+
{
|
|
1499
|
+
itemId: published!.item.id,
|
|
1500
|
+
itemVersionId: published!.version.id,
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
itemId: childEdge!.toItemId,
|
|
1504
|
+
itemVersionId: childEdge!.toItemVersionId,
|
|
1505
|
+
},
|
|
1506
|
+
]),
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
it("does not turn a forged compiler id into a trusted published artifact", async () => {
|
|
1512
|
+
const { runWithRequestContext, store } = await setup();
|
|
1513
|
+
const { reassembleNativeCreativeArtifact } =
|
|
1514
|
+
await import("../native-artifact-reassembly.js");
|
|
1515
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1516
|
+
runWithRequestContext(
|
|
1517
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
1518
|
+
fn,
|
|
1519
|
+
);
|
|
1520
|
+
const ingested = await asAlice(() =>
|
|
1521
|
+
store.ingestItems({
|
|
1522
|
+
sourceId: "source-1",
|
|
1523
|
+
items: [
|
|
1524
|
+
{
|
|
1525
|
+
externalId: "forged-native",
|
|
1526
|
+
kind: "native",
|
|
1527
|
+
title: "Forged",
|
|
1528
|
+
mimeType: "text/html",
|
|
1529
|
+
content:
|
|
1530
|
+
"<!doctype html><html><head></head><body><div>forged</div></body></html>",
|
|
1531
|
+
contentHash: "forged-v1",
|
|
1532
|
+
provenance: { compiler: "attacker/compiler" },
|
|
1533
|
+
metadata: {
|
|
1534
|
+
nativeArtifact: {
|
|
1535
|
+
schemaVersion: 1,
|
|
1536
|
+
app: "design",
|
|
1537
|
+
format: "design-html",
|
|
1538
|
+
rootExternalId: "forged-native",
|
|
1539
|
+
fidelityReport: {
|
|
1540
|
+
exact: { count: 1 },
|
|
1541
|
+
approximated: { count: 0, reasons: [] },
|
|
1542
|
+
imageFallback: { count: 0, reasons: [] },
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
},
|
|
1547
|
+
],
|
|
1548
|
+
}),
|
|
1549
|
+
);
|
|
1550
|
+
const context = await asAlice(() =>
|
|
1551
|
+
store.createCreativeContext({ name: "forged", kind: "specialty" }),
|
|
1552
|
+
);
|
|
1553
|
+
await asAlice(() =>
|
|
1554
|
+
store.manageContextMembership({
|
|
1555
|
+
operation: "submit",
|
|
1556
|
+
contextId: context!.id,
|
|
1557
|
+
itemId: ingested.itemIds[0],
|
|
1558
|
+
confirmBroaderPublication: true,
|
|
1559
|
+
}),
|
|
1560
|
+
);
|
|
1561
|
+
const membership = (
|
|
1562
|
+
await asAlice(() =>
|
|
1563
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1564
|
+
)
|
|
1565
|
+
).memberships[0]!;
|
|
1566
|
+
const published = await asAlice(() =>
|
|
1567
|
+
store.getCreativeContextItem(
|
|
1568
|
+
membership.publishedItemId!,
|
|
1569
|
+
membership.publishedItemVersionId!,
|
|
1570
|
+
),
|
|
1571
|
+
);
|
|
1572
|
+
await expect(
|
|
1573
|
+
asAlice(() =>
|
|
1574
|
+
reassembleNativeCreativeArtifact({
|
|
1575
|
+
root: published!,
|
|
1576
|
+
app: "design",
|
|
1577
|
+
format: "design-html",
|
|
1578
|
+
resolveChild: store.getCreativeContextItemByExternalId,
|
|
1579
|
+
}),
|
|
1580
|
+
),
|
|
1581
|
+
).rejects.toThrow(/trusted native artifact compiler/i);
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
it("keeps pending submissions private to their submitter and context reviewers", async () => {
|
|
1585
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
1586
|
+
const asUser = <T>(userEmail: string, fn: () => Promise<T>) =>
|
|
1587
|
+
runWithRequestContext({ userEmail, orgId: "org-1" }, fn);
|
|
1588
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1589
|
+
asUser("alice@example.test", fn);
|
|
1590
|
+
const asBob = <T>(fn: () => Promise<T>) => asUser("bob@example.test", fn);
|
|
1591
|
+
const asCarol = <T>(fn: () => Promise<T>) =>
|
|
1592
|
+
asUser("carol@example.test", fn);
|
|
1593
|
+
const asDrew = <T>(fn: () => Promise<T>) => asUser("drew@example.test", fn);
|
|
1594
|
+
const context = await asAlice(() =>
|
|
1595
|
+
store.createCreativeContext({
|
|
1596
|
+
name: "Review-only governance",
|
|
1597
|
+
kind: "specialty",
|
|
1598
|
+
approvalPolicy: "review",
|
|
1599
|
+
}),
|
|
1600
|
+
);
|
|
1601
|
+
const timestamp = "2026-07-17T00:00:00.000Z";
|
|
1602
|
+
await exec.execute({
|
|
1603
|
+
sql: `INSERT INTO creative_context_source_shares
|
|
1604
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
1605
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1606
|
+
args: [
|
|
1607
|
+
"source-share-bob",
|
|
1608
|
+
"source-1",
|
|
1609
|
+
"user",
|
|
1610
|
+
"bob@example.test",
|
|
1611
|
+
"admin",
|
|
1612
|
+
"alice@example.test",
|
|
1613
|
+
timestamp,
|
|
1614
|
+
],
|
|
1615
|
+
});
|
|
1616
|
+
for (const [id, email] of [
|
|
1617
|
+
["context-share-bob", "bob@example.test"],
|
|
1618
|
+
["context-share-drew", "drew@example.test"],
|
|
1619
|
+
]) {
|
|
1620
|
+
await exec.execute({
|
|
1621
|
+
sql: `INSERT INTO creative_context_shares
|
|
1622
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
1623
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1624
|
+
args: [
|
|
1625
|
+
id,
|
|
1626
|
+
context!.id,
|
|
1627
|
+
"user",
|
|
1628
|
+
email,
|
|
1629
|
+
"editor",
|
|
1630
|
+
"alice@example.test",
|
|
1631
|
+
timestamp,
|
|
1632
|
+
],
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
await exec.execute({
|
|
1636
|
+
sql: "UPDATE creative_context_items SET thumbnail_blob_ref = ? WHERE id = ?",
|
|
1637
|
+
args: ["creative-context-blob:v1:pending-preview-opaque", "allowed"],
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
await expect(
|
|
1641
|
+
asCarol(() =>
|
|
1642
|
+
store.manageContextMembership({
|
|
1643
|
+
operation: "submit",
|
|
1644
|
+
contextId: context!.id,
|
|
1645
|
+
itemId: "allowed",
|
|
1646
|
+
}),
|
|
1647
|
+
),
|
|
1648
|
+
).rejects.toThrow();
|
|
1649
|
+
expect(
|
|
1650
|
+
(await asBob(() => store.getCreativeContextById(context!.id)))?.access,
|
|
1651
|
+
).toMatchObject({ canSubmit: true, canReview: false });
|
|
1652
|
+
|
|
1653
|
+
const submitted = await asBob(() =>
|
|
1654
|
+
store.manageContextMembership({
|
|
1655
|
+
operation: "submit",
|
|
1656
|
+
contextId: context!.id,
|
|
1657
|
+
itemId: "allowed",
|
|
1658
|
+
confirmBroaderPublication: true,
|
|
1659
|
+
}),
|
|
1660
|
+
);
|
|
1661
|
+
const membershipId = submitted.membershipId;
|
|
1662
|
+
await expect(
|
|
1663
|
+
asBob(() =>
|
|
1664
|
+
store.manageContextMembership({
|
|
1665
|
+
operation: "approve",
|
|
1666
|
+
contextId: context!.id,
|
|
1667
|
+
membershipId,
|
|
1668
|
+
}),
|
|
1669
|
+
),
|
|
1670
|
+
).rejects.toThrow();
|
|
1671
|
+
await expect(
|
|
1672
|
+
asDrew(() =>
|
|
1673
|
+
store.manageContextMembership({
|
|
1674
|
+
operation: "remove",
|
|
1675
|
+
contextId: context!.id,
|
|
1676
|
+
membershipId,
|
|
1677
|
+
}),
|
|
1678
|
+
),
|
|
1679
|
+
).rejects.toThrow();
|
|
1680
|
+
await expect(
|
|
1681
|
+
asBob(() =>
|
|
1682
|
+
store.updateCreativeContext(context!.id, { name: "Unapproved edit" }),
|
|
1683
|
+
),
|
|
1684
|
+
).rejects.toThrow();
|
|
1685
|
+
|
|
1686
|
+
const ownPending = await asBob(() =>
|
|
1687
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1688
|
+
);
|
|
1689
|
+
expect(ownPending.memberships).toEqual([
|
|
1690
|
+
expect.objectContaining({
|
|
1691
|
+
id: membershipId,
|
|
1692
|
+
artifactKey: "source-1:allowed",
|
|
1693
|
+
pendingSubmission: expect.objectContaining({
|
|
1694
|
+
submittedBy: "bob@example.test",
|
|
1695
|
+
proposedItem: expect.objectContaining({
|
|
1696
|
+
title: "allowed title",
|
|
1697
|
+
itemVersionId: expect.any(String),
|
|
1698
|
+
}),
|
|
1699
|
+
}),
|
|
1700
|
+
}),
|
|
1701
|
+
]);
|
|
1702
|
+
const proposedItem =
|
|
1703
|
+
ownPending.memberships[0]?.pendingSubmission?.proposedItem;
|
|
1704
|
+
expect(proposedItem).toMatchObject({
|
|
1705
|
+
id: expect.any(String),
|
|
1706
|
+
itemVersionId: expect.any(String),
|
|
1707
|
+
});
|
|
1708
|
+
expect(JSON.stringify(ownPending)).not.toContain("pending-preview-opaque");
|
|
1709
|
+
await expect(
|
|
1710
|
+
asBob(() =>
|
|
1711
|
+
store.readPendingCreativeContextMedia({
|
|
1712
|
+
itemId: proposedItem!.id,
|
|
1713
|
+
itemVersionId: proposedItem!.itemVersionId,
|
|
1714
|
+
}),
|
|
1715
|
+
),
|
|
1716
|
+
).resolves.toMatchObject({
|
|
1717
|
+
storageKey: "creative-context-blob:v1:pending-preview-opaque",
|
|
1718
|
+
});
|
|
1719
|
+
await expect(
|
|
1720
|
+
asAlice(() =>
|
|
1721
|
+
store.readPendingCreativeContextMedia({
|
|
1722
|
+
itemId: proposedItem!.id,
|
|
1723
|
+
itemVersionId: proposedItem!.itemVersionId,
|
|
1724
|
+
}),
|
|
1725
|
+
),
|
|
1726
|
+
).resolves.toMatchObject({
|
|
1727
|
+
storageKey: "creative-context-blob:v1:pending-preview-opaque",
|
|
1728
|
+
});
|
|
1729
|
+
await expect(
|
|
1730
|
+
asDrew(() =>
|
|
1731
|
+
store.readPendingCreativeContextMedia({
|
|
1732
|
+
itemId: proposedItem!.id,
|
|
1733
|
+
itemVersionId: proposedItem!.itemVersionId,
|
|
1734
|
+
}),
|
|
1735
|
+
),
|
|
1736
|
+
).resolves.toBeNull();
|
|
1737
|
+
await expect(
|
|
1738
|
+
asCarol(() =>
|
|
1739
|
+
store.readPendingCreativeContextMedia({
|
|
1740
|
+
itemId: proposedItem!.id,
|
|
1741
|
+
itemVersionId: proposedItem!.itemVersionId,
|
|
1742
|
+
}),
|
|
1743
|
+
),
|
|
1744
|
+
).resolves.toBeNull();
|
|
1745
|
+
await expect(
|
|
1746
|
+
asDrew(() =>
|
|
1747
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1748
|
+
),
|
|
1749
|
+
).resolves.toMatchObject({ memberships: [] });
|
|
1750
|
+
await expect(
|
|
1751
|
+
asCarol(() =>
|
|
1752
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1753
|
+
),
|
|
1754
|
+
).resolves.toMatchObject({ memberships: [] });
|
|
1755
|
+
|
|
1756
|
+
const reviewerView = await asAlice(() =>
|
|
1757
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1758
|
+
);
|
|
1759
|
+
expect(reviewerView.memberships[0]).toMatchObject({
|
|
1760
|
+
id: membershipId,
|
|
1761
|
+
pendingSubmission: expect.objectContaining({
|
|
1762
|
+
submittedBy: "bob@example.test",
|
|
1763
|
+
proposedItem: expect.objectContaining({
|
|
1764
|
+
title: "allowed title",
|
|
1765
|
+
}),
|
|
1766
|
+
}),
|
|
1767
|
+
});
|
|
1768
|
+
expect(
|
|
1769
|
+
(await asAlice(() => store.getCreativeContextById(context!.id)))?.access,
|
|
1770
|
+
).toMatchObject({ canReview: true, canAdmin: true });
|
|
1771
|
+
|
|
1772
|
+
await asAlice(() =>
|
|
1773
|
+
store.manageContextMembership({
|
|
1774
|
+
operation: "approve",
|
|
1775
|
+
contextId: context!.id,
|
|
1776
|
+
membershipId,
|
|
1777
|
+
}),
|
|
1778
|
+
);
|
|
1779
|
+
const approvedForViewer = await asCarol(() =>
|
|
1780
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1781
|
+
);
|
|
1782
|
+
expect(approvedForViewer.memberships).toEqual([
|
|
1783
|
+
expect.objectContaining({
|
|
1784
|
+
id: membershipId,
|
|
1785
|
+
artifactKey: "source-1:allowed",
|
|
1786
|
+
pendingSubmissionId: null,
|
|
1787
|
+
pendingSubmission: null,
|
|
1788
|
+
publishedItem: expect.objectContaining({ id: expect.any(String) }),
|
|
1789
|
+
}),
|
|
1790
|
+
]);
|
|
1791
|
+
|
|
1792
|
+
const approvedVersionId =
|
|
1793
|
+
approvedForViewer.memberships[0]!.publishedItemVersionId;
|
|
1794
|
+
await asAlice(() =>
|
|
1795
|
+
store.ingestItems({
|
|
1796
|
+
sourceId: "source-1",
|
|
1797
|
+
items: [
|
|
1798
|
+
{
|
|
1799
|
+
externalId: "allowed",
|
|
1800
|
+
kind: "slide",
|
|
1801
|
+
title: "allowed title update",
|
|
1802
|
+
content: "new source version pending review",
|
|
1803
|
+
contentHash: "allowed-hash-v3",
|
|
1804
|
+
upstreamAccess: "available",
|
|
1805
|
+
},
|
|
1806
|
+
],
|
|
1807
|
+
}),
|
|
1808
|
+
);
|
|
1809
|
+
await asBob(() =>
|
|
1810
|
+
store.manageContextMembership({
|
|
1811
|
+
operation: "submit",
|
|
1812
|
+
contextId: context!.id,
|
|
1813
|
+
itemId: "allowed",
|
|
1814
|
+
confirmBroaderPublication: true,
|
|
1815
|
+
}),
|
|
1816
|
+
);
|
|
1817
|
+
const viewerDuringUpdate = await asCarol(() =>
|
|
1818
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1819
|
+
);
|
|
1820
|
+
expect(viewerDuringUpdate.memberships[0]).toMatchObject({
|
|
1821
|
+
publishedItemVersionId: approvedVersionId,
|
|
1822
|
+
pendingSubmissionId: null,
|
|
1823
|
+
pendingSubmission: null,
|
|
1824
|
+
});
|
|
1825
|
+
const reviewerDuringUpdate = await asAlice(() =>
|
|
1826
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1827
|
+
);
|
|
1828
|
+
expect(reviewerDuringUpdate.memberships[0]).toMatchObject({
|
|
1829
|
+
publishedItemVersionId: approvedVersionId,
|
|
1830
|
+
pendingSubmission: expect.objectContaining({ status: "pending" }),
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
it("applies open and admins-only publication policies to editor submissions", async () => {
|
|
1835
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
1836
|
+
const asUser = <T>(userEmail: string, fn: () => Promise<T>) =>
|
|
1837
|
+
runWithRequestContext({ userEmail, orgId: "org-1" }, fn);
|
|
1838
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1839
|
+
asUser("alice@example.test", fn);
|
|
1840
|
+
const asBob = <T>(fn: () => Promise<T>) => asUser("bob@example.test", fn);
|
|
1841
|
+
await exec.execute({
|
|
1842
|
+
sql: "UPDATE creative_context_sources SET visibility = 'org' WHERE id = 'source-1'",
|
|
1843
|
+
args: [],
|
|
1844
|
+
});
|
|
1845
|
+
const [open, adminsOnly] = await Promise.all([
|
|
1846
|
+
asAlice(() =>
|
|
1847
|
+
store.createCreativeContext({
|
|
1848
|
+
name: "Open",
|
|
1849
|
+
kind: "specialty",
|
|
1850
|
+
approvalPolicy: "open",
|
|
1851
|
+
}),
|
|
1852
|
+
),
|
|
1853
|
+
asAlice(() =>
|
|
1854
|
+
store.createCreativeContext({
|
|
1855
|
+
name: "Admin publishing",
|
|
1856
|
+
kind: "specialty",
|
|
1857
|
+
approvalPolicy: "admins-only",
|
|
1858
|
+
}),
|
|
1859
|
+
),
|
|
1860
|
+
]);
|
|
1861
|
+
for (const [id, contextId] of [
|
|
1862
|
+
["open-editor", open!.id],
|
|
1863
|
+
["admins-editor", adminsOnly!.id],
|
|
1864
|
+
]) {
|
|
1865
|
+
await exec.execute({
|
|
1866
|
+
sql: `INSERT INTO creative_context_shares
|
|
1867
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
1868
|
+
VALUES (?, ?, 'user', 'bob@example.test', 'editor', 'alice@example.test', ?)`,
|
|
1869
|
+
args: [id, contextId, "2026-07-17T00:00:00.000Z"],
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
const openSubmission = await asBob(() =>
|
|
1873
|
+
store.manageContextMembership({
|
|
1874
|
+
operation: "submit",
|
|
1875
|
+
contextId: open!.id,
|
|
1876
|
+
itemId: "allowed",
|
|
1877
|
+
}),
|
|
1878
|
+
);
|
|
1879
|
+
expect(openSubmission.submission).toMatchObject({ status: "approved" });
|
|
1880
|
+
|
|
1881
|
+
const governedSubmission = await asBob(() =>
|
|
1882
|
+
store.manageContextMembership({
|
|
1883
|
+
operation: "submit",
|
|
1884
|
+
contextId: adminsOnly!.id,
|
|
1885
|
+
itemId: "allowed",
|
|
1886
|
+
}),
|
|
1887
|
+
);
|
|
1888
|
+
expect(governedSubmission.submission).toMatchObject({ status: "pending" });
|
|
1889
|
+
await expect(
|
|
1890
|
+
asBob(() =>
|
|
1891
|
+
store.manageContextMembership({
|
|
1892
|
+
operation: "approve",
|
|
1893
|
+
contextId: adminsOnly!.id,
|
|
1894
|
+
membershipId: governedSubmission.membershipId,
|
|
1895
|
+
}),
|
|
1896
|
+
),
|
|
1897
|
+
).rejects.toThrow();
|
|
1898
|
+
await expect(
|
|
1899
|
+
asAlice(() =>
|
|
1900
|
+
store.manageContextMembership({
|
|
1901
|
+
operation: "approve",
|
|
1902
|
+
contextId: adminsOnly!.id,
|
|
1903
|
+
membershipId: governedSubmission.membershipId,
|
|
1904
|
+
}),
|
|
1905
|
+
),
|
|
1906
|
+
).resolves.toEqual({ approved: true });
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
it("serializes concurrent Default creation to one scope-owned context", async () => {
|
|
1910
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
1911
|
+
const createDefault = () =>
|
|
1912
|
+
runWithRequestContext(
|
|
1913
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
1914
|
+
() => store.createCreativeContext({ name: "Default", kind: "default" }),
|
|
1915
|
+
);
|
|
1916
|
+
const [first, second] = await Promise.all([
|
|
1917
|
+
createDefault(),
|
|
1918
|
+
createDefault(),
|
|
1919
|
+
]);
|
|
1920
|
+
expect(first?.id).toBe(second?.id);
|
|
1921
|
+
const rows = await exec.execute({
|
|
1922
|
+
sql: `SELECT id FROM creative_contexts
|
|
1923
|
+
WHERE default_scope_key = ?`,
|
|
1924
|
+
args: ["org:org-1"],
|
|
1925
|
+
});
|
|
1926
|
+
expect(rows.rows).toHaveLength(1);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
it("keeps Default non-archivable and does not broaden private corpus during org backfill", async () => {
|
|
1930
|
+
const { runWithRequestContext, store } = await setup();
|
|
1931
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1932
|
+
runWithRequestContext(
|
|
1933
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
1934
|
+
fn,
|
|
1935
|
+
);
|
|
1936
|
+
const context = await asAlice(() => store.ensureDefaultCreativeContext());
|
|
1937
|
+
expect(context).toMatchObject({ kind: "default", visibility: "org" });
|
|
1938
|
+
await expect(
|
|
1939
|
+
asAlice(() => store.archiveCreativeContext(context!.id)),
|
|
1940
|
+
).rejects.toThrow(/Default Creative Context cannot be archived/);
|
|
1941
|
+
const memberships = await asAlice(() =>
|
|
1942
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
1943
|
+
);
|
|
1944
|
+
expect(memberships.memberships).toEqual([]);
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
it("applies each Creative Context's rank to the same item at retrieval time", async () => {
|
|
1948
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
1949
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1950
|
+
runWithRequestContext(
|
|
1951
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
1952
|
+
fn,
|
|
1953
|
+
);
|
|
1954
|
+
const [canonical, normal] = await Promise.all([
|
|
1955
|
+
asAlice(() =>
|
|
1956
|
+
store.createCreativeContext({
|
|
1957
|
+
name: "Canonical lane",
|
|
1958
|
+
kind: "specialty",
|
|
1959
|
+
}),
|
|
1960
|
+
),
|
|
1961
|
+
asAlice(() =>
|
|
1962
|
+
store.createCreativeContext({ name: "Normal lane", kind: "specialty" }),
|
|
1963
|
+
),
|
|
1964
|
+
]);
|
|
1965
|
+
for (const [id, contextId, rank] of [
|
|
1966
|
+
["canonical-membership", canonical!.id, "canonical"],
|
|
1967
|
+
["normal-membership", normal!.id, "normal"],
|
|
1968
|
+
]) {
|
|
1969
|
+
await exec.execute({
|
|
1970
|
+
sql: `INSERT INTO creative_context_memberships
|
|
1971
|
+
(id, context_id, artifact_key, published_item_id, published_item_version_id,
|
|
1972
|
+
rank, status, created_at, updated_at, owner_email, org_id)
|
|
1973
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1974
|
+
args: [
|
|
1975
|
+
id,
|
|
1976
|
+
contextId,
|
|
1977
|
+
"source-1:allowed",
|
|
1978
|
+
"allowed",
|
|
1979
|
+
"allowed-v2",
|
|
1980
|
+
rank,
|
|
1981
|
+
"active",
|
|
1982
|
+
"2026-07-17T00:00:00.000Z",
|
|
1983
|
+
"2026-07-17T00:00:00.000Z",
|
|
1984
|
+
"alice@example.test",
|
|
1985
|
+
"org-1",
|
|
1986
|
+
],
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
const [canonicalResults, normalResults, canonicalDocuments] =
|
|
1990
|
+
await Promise.all([
|
|
1991
|
+
asAlice(() =>
|
|
1992
|
+
store.listAccessibleLexicalCandidates({
|
|
1993
|
+
query: "canonical version 2",
|
|
1994
|
+
contextId: canonical!.id,
|
|
1995
|
+
limit: 10,
|
|
1996
|
+
}),
|
|
1997
|
+
),
|
|
1998
|
+
asAlice(() =>
|
|
1999
|
+
store.listAccessibleLexicalCandidates({
|
|
2000
|
+
query: "canonical version 2",
|
|
2001
|
+
contextId: normal!.id,
|
|
2002
|
+
limit: 10,
|
|
2003
|
+
}),
|
|
2004
|
+
),
|
|
2005
|
+
asAlice(() =>
|
|
2006
|
+
store.listAccessibleSearchDocuments({
|
|
2007
|
+
contextId: canonical!.id,
|
|
2008
|
+
limit: 10,
|
|
2009
|
+
}),
|
|
2010
|
+
),
|
|
2011
|
+
]);
|
|
2012
|
+
expect(canonicalResults.results[0]).toMatchObject({
|
|
2013
|
+
itemId: "allowed",
|
|
2014
|
+
itemVersionId: "allowed-v2",
|
|
2015
|
+
});
|
|
2016
|
+
expect(canonicalResults.results[0]!.score).toBeGreaterThan(
|
|
2017
|
+
normalResults.results[0]!.score,
|
|
2018
|
+
);
|
|
2019
|
+
expect(canonicalDocuments[0]).toMatchObject({
|
|
2020
|
+
itemId: "allowed",
|
|
2021
|
+
curationRank: "canonical",
|
|
2022
|
+
});
|
|
2023
|
+
});
|
|
2024
|
+
|
|
1269
2025
|
it("finds native artifacts through bounded code-token chunks", async () => {
|
|
1270
2026
|
const { runWithRequestContext, store } = await setup();
|
|
1271
2027
|
const { performCreativeContextSearch } =
|
|
@@ -1578,6 +2334,366 @@ describe("creative context access and revocation", () => {
|
|
|
1578
2334
|
]);
|
|
1579
2335
|
});
|
|
1580
2336
|
|
|
2337
|
+
it("limits pack replay bypasses to context-owned published snapshots", async () => {
|
|
2338
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2339
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2340
|
+
runWithRequestContext(
|
|
2341
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2342
|
+
fn,
|
|
2343
|
+
);
|
|
2344
|
+
const asBob = <T>(fn: () => Promise<T>) =>
|
|
2345
|
+
runWithRequestContext(
|
|
2346
|
+
{ userEmail: "bob@example.test", orgId: "org-1" },
|
|
2347
|
+
fn,
|
|
2348
|
+
);
|
|
2349
|
+
|
|
2350
|
+
await exec.execute({
|
|
2351
|
+
sql: `INSERT INTO creative_context_packs
|
|
2352
|
+
(id, name, context_mode, request, created_at, owner_email, org_id, visibility)
|
|
2353
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2354
|
+
args: [
|
|
2355
|
+
"unsafe-pack",
|
|
2356
|
+
"Unsafe library replay",
|
|
2357
|
+
"manual",
|
|
2358
|
+
"{}",
|
|
2359
|
+
"2026-07-16T00:00:00.000Z",
|
|
2360
|
+
"alice@example.test",
|
|
2361
|
+
"org-1",
|
|
2362
|
+
"private",
|
|
2363
|
+
],
|
|
2364
|
+
});
|
|
2365
|
+
await exec.execute({
|
|
2366
|
+
sql: `INSERT INTO creative_context_pack_members
|
|
2367
|
+
(id, pack_id, item_id, item_version_id, ordinal, score_metadata,
|
|
2368
|
+
created_at, owner_email, org_id)
|
|
2369
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2370
|
+
args: [
|
|
2371
|
+
"unsafe-member",
|
|
2372
|
+
"unsafe-pack",
|
|
2373
|
+
"allowed",
|
|
2374
|
+
"allowed-v1",
|
|
2375
|
+
0,
|
|
2376
|
+
"{}",
|
|
2377
|
+
"2026-07-16T00:00:00.000Z",
|
|
2378
|
+
"alice@example.test",
|
|
2379
|
+
"org-1",
|
|
2380
|
+
],
|
|
2381
|
+
});
|
|
2382
|
+
await exec.execute({
|
|
2383
|
+
sql: `INSERT INTO creative_context_pack_shares
|
|
2384
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
2385
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2386
|
+
args: [
|
|
2387
|
+
"unsafe-pack-share",
|
|
2388
|
+
"unsafe-pack",
|
|
2389
|
+
"user",
|
|
2390
|
+
"bob@example.test",
|
|
2391
|
+
"viewer",
|
|
2392
|
+
"alice@example.test",
|
|
2393
|
+
"2026-07-16T00:00:00.000Z",
|
|
2394
|
+
],
|
|
2395
|
+
});
|
|
2396
|
+
await exec.execute(
|
|
2397
|
+
"UPDATE creative_context_sources SET upstream_access = 'restricted' WHERE id = 'source-1'",
|
|
2398
|
+
);
|
|
2399
|
+
|
|
2400
|
+
await expect(
|
|
2401
|
+
asBob(() => store.getCreativeContextItem("allowed", "allowed-v1")),
|
|
2402
|
+
).resolves.toBeNull();
|
|
2403
|
+
await expect(
|
|
2404
|
+
asBob(() =>
|
|
2405
|
+
store.listAccessibleLexicalCandidates({
|
|
2406
|
+
query: "version 1",
|
|
2407
|
+
packId: "unsafe-pack",
|
|
2408
|
+
limit: 10,
|
|
2409
|
+
}),
|
|
2410
|
+
),
|
|
2411
|
+
).resolves.toEqual({ results: [], nextCursor: undefined });
|
|
2412
|
+
|
|
2413
|
+
for (const source of [
|
|
2414
|
+
["context-staging-source", "Context staging"],
|
|
2415
|
+
["context-published-source", "Context published"],
|
|
2416
|
+
]) {
|
|
2417
|
+
await exec.execute({
|
|
2418
|
+
sql: `INSERT INTO creative_context_sources
|
|
2419
|
+
(id, name, kind, config, upstream_access, status, health_status,
|
|
2420
|
+
item_count, restricted_item_count, owner_email, org_id, visibility,
|
|
2421
|
+
created_at, updated_at)
|
|
2422
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2423
|
+
args: [
|
|
2424
|
+
source[0],
|
|
2425
|
+
source[1],
|
|
2426
|
+
"native-app",
|
|
2427
|
+
"{}",
|
|
2428
|
+
"available",
|
|
2429
|
+
"active",
|
|
2430
|
+
"healthy",
|
|
2431
|
+
1,
|
|
2432
|
+
0,
|
|
2433
|
+
"alice@example.test",
|
|
2434
|
+
"org-1",
|
|
2435
|
+
"private",
|
|
2436
|
+
"2026-07-16T00:00:00.000Z",
|
|
2437
|
+
"2026-07-16T00:00:00.000Z",
|
|
2438
|
+
],
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
await exec.execute({
|
|
2442
|
+
sql: `INSERT INTO creative_contexts
|
|
2443
|
+
(id, name, kind, staging_source_id, published_source_id, approval_policy,
|
|
2444
|
+
created_at, updated_at, owner_email, org_id, visibility)
|
|
2445
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2446
|
+
args: [
|
|
2447
|
+
"context-owned",
|
|
2448
|
+
"Governed context",
|
|
2449
|
+
"specialty",
|
|
2450
|
+
"context-staging-source",
|
|
2451
|
+
"context-published-source",
|
|
2452
|
+
"review",
|
|
2453
|
+
"2026-07-16T00:00:00.000Z",
|
|
2454
|
+
"2026-07-16T00:00:00.000Z",
|
|
2455
|
+
"alice@example.test",
|
|
2456
|
+
"org-1",
|
|
2457
|
+
"private",
|
|
2458
|
+
],
|
|
2459
|
+
});
|
|
2460
|
+
await exec.execute({
|
|
2461
|
+
sql: `INSERT INTO creative_context_items
|
|
2462
|
+
(id, source_id, external_id, kind, title, current_version_id,
|
|
2463
|
+
current_content_hash, status, upstream_access, curation_status,
|
|
2464
|
+
curation_rank, inventory_state, index_state, tags, colors, provenance,
|
|
2465
|
+
metadata, created_at, updated_at, owner_email, org_id)
|
|
2466
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2467
|
+
args: [
|
|
2468
|
+
"published-item",
|
|
2469
|
+
"context-published-source",
|
|
2470
|
+
"published-artifact",
|
|
2471
|
+
"slide",
|
|
2472
|
+
"Published evidence",
|
|
2473
|
+
"published-version",
|
|
2474
|
+
"published-hash",
|
|
2475
|
+
"active",
|
|
2476
|
+
"available",
|
|
2477
|
+
"included",
|
|
2478
|
+
"normal",
|
|
2479
|
+
"available",
|
|
2480
|
+
"pending",
|
|
2481
|
+
"[]",
|
|
2482
|
+
"[]",
|
|
2483
|
+
"{}",
|
|
2484
|
+
"{}",
|
|
2485
|
+
"2026-07-16T00:00:00.000Z",
|
|
2486
|
+
"2026-07-16T00:00:00.000Z",
|
|
2487
|
+
"alice@example.test",
|
|
2488
|
+
"org-1",
|
|
2489
|
+
],
|
|
2490
|
+
});
|
|
2491
|
+
await exec.execute({
|
|
2492
|
+
sql: `INSERT INTO creative_context_item_versions
|
|
2493
|
+
(id, item_id, version_number, content_hash, title, content, parse_status,
|
|
2494
|
+
metadata, created_at, owner_email, org_id)
|
|
2495
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2496
|
+
args: [
|
|
2497
|
+
"published-version",
|
|
2498
|
+
"published-item",
|
|
2499
|
+
1,
|
|
2500
|
+
"published-hash",
|
|
2501
|
+
"Published evidence",
|
|
2502
|
+
"historical governed context evidence",
|
|
2503
|
+
"parsed",
|
|
2504
|
+
"{}",
|
|
2505
|
+
"2026-07-16T00:00:00.000Z",
|
|
2506
|
+
"alice@example.test",
|
|
2507
|
+
"org-1",
|
|
2508
|
+
],
|
|
2509
|
+
});
|
|
2510
|
+
await exec.execute({
|
|
2511
|
+
sql: `INSERT INTO creative_context_chunks
|
|
2512
|
+
(id, item_id, item_version_id, ordinal, kind, text, metadata,
|
|
2513
|
+
created_at, owner_email, org_id)
|
|
2514
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2515
|
+
args: [
|
|
2516
|
+
"published-chunk",
|
|
2517
|
+
"published-item",
|
|
2518
|
+
"published-version",
|
|
2519
|
+
0,
|
|
2520
|
+
"text",
|
|
2521
|
+
"historical governed context evidence",
|
|
2522
|
+
"{}",
|
|
2523
|
+
"2026-07-16T00:00:00.000Z",
|
|
2524
|
+
"alice@example.test",
|
|
2525
|
+
"org-1",
|
|
2526
|
+
],
|
|
2527
|
+
});
|
|
2528
|
+
await exec.execute({
|
|
2529
|
+
sql: `INSERT INTO creative_context_memberships
|
|
2530
|
+
(id, context_id, artifact_key, published_item_id, published_item_version_id,
|
|
2531
|
+
rank, status, created_at, updated_at, owner_email, org_id)
|
|
2532
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2533
|
+
args: [
|
|
2534
|
+
"removed-membership",
|
|
2535
|
+
"context-owned",
|
|
2536
|
+
"published-artifact",
|
|
2537
|
+
"published-item",
|
|
2538
|
+
"published-version",
|
|
2539
|
+
"normal",
|
|
2540
|
+
"removed",
|
|
2541
|
+
"2026-07-16T00:00:00.000Z",
|
|
2542
|
+
"2026-07-16T00:00:00.000Z",
|
|
2543
|
+
"alice@example.test",
|
|
2544
|
+
"org-1",
|
|
2545
|
+
],
|
|
2546
|
+
});
|
|
2547
|
+
await exec.execute({
|
|
2548
|
+
sql: `INSERT INTO creative_context_published_snapshots
|
|
2549
|
+
(id, context_id, source_id, membership_id, item_id, item_version_id,
|
|
2550
|
+
submission_id, created_at, owner_email, org_id)
|
|
2551
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2552
|
+
args: [
|
|
2553
|
+
"published-snapshot",
|
|
2554
|
+
"context-owned",
|
|
2555
|
+
"context-published-source",
|
|
2556
|
+
"removed-membership",
|
|
2557
|
+
"published-item",
|
|
2558
|
+
"published-version",
|
|
2559
|
+
"approved-submission",
|
|
2560
|
+
"2026-07-16T00:00:00.000Z",
|
|
2561
|
+
"alice@example.test",
|
|
2562
|
+
"org-1",
|
|
2563
|
+
],
|
|
2564
|
+
});
|
|
2565
|
+
const historicalPack = await asAlice(() =>
|
|
2566
|
+
store.createContextPack({
|
|
2567
|
+
name: "Historical governed replay",
|
|
2568
|
+
baseContextId: "context-owned",
|
|
2569
|
+
members: [
|
|
2570
|
+
{ itemId: "published-item", itemVersionId: "published-version" },
|
|
2571
|
+
],
|
|
2572
|
+
}),
|
|
2573
|
+
);
|
|
2574
|
+
await exec.execute({
|
|
2575
|
+
sql: `INSERT INTO creative_context_pack_shares
|
|
2576
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
2577
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2578
|
+
args: [
|
|
2579
|
+
"historical-pack-share",
|
|
2580
|
+
historicalPack.id,
|
|
2581
|
+
"user",
|
|
2582
|
+
"bob@example.test",
|
|
2583
|
+
"viewer",
|
|
2584
|
+
"alice@example.test",
|
|
2585
|
+
"2026-07-16T00:00:00.000Z",
|
|
2586
|
+
],
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
await expect(
|
|
2590
|
+
asBob(() => store.getContextPack(historicalPack.id)),
|
|
2591
|
+
).resolves.toMatchObject({
|
|
2592
|
+
members: [
|
|
2593
|
+
expect.objectContaining({
|
|
2594
|
+
itemId: "published-item",
|
|
2595
|
+
itemVersionId: "published-version",
|
|
2596
|
+
}),
|
|
2597
|
+
],
|
|
2598
|
+
});
|
|
2599
|
+
await expect(
|
|
2600
|
+
asBob(() =>
|
|
2601
|
+
store.getCreativeContextItem("published-item", "published-version"),
|
|
2602
|
+
),
|
|
2603
|
+
).resolves.toMatchObject({
|
|
2604
|
+
version: { id: "published-version" },
|
|
2605
|
+
});
|
|
2606
|
+
await expect(
|
|
2607
|
+
asBob(() =>
|
|
2608
|
+
store.listAccessibleLexicalCandidates({
|
|
2609
|
+
query: "historical governed",
|
|
2610
|
+
packId: historicalPack.id,
|
|
2611
|
+
limit: 10,
|
|
2612
|
+
}),
|
|
2613
|
+
),
|
|
2614
|
+
).resolves.toMatchObject({
|
|
2615
|
+
results: [
|
|
2616
|
+
expect.objectContaining({ itemVersionId: "published-version" }),
|
|
2617
|
+
],
|
|
2618
|
+
});
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
it("legal purge follows provenance into staged and context-owned copies", async () => {
|
|
2622
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2623
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2624
|
+
runWithRequestContext(
|
|
2625
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2626
|
+
fn,
|
|
2627
|
+
);
|
|
2628
|
+
const context = await asAlice(() =>
|
|
2629
|
+
store.createCreativeContext({
|
|
2630
|
+
name: "Purge provenance",
|
|
2631
|
+
kind: "specialty",
|
|
2632
|
+
approvalPolicy: "open",
|
|
2633
|
+
}),
|
|
2634
|
+
);
|
|
2635
|
+
const submitted = await asAlice(() =>
|
|
2636
|
+
store.manageContextMembership({
|
|
2637
|
+
operation: "submit",
|
|
2638
|
+
contextId: context!.id,
|
|
2639
|
+
itemId: "allowed",
|
|
2640
|
+
confirmBroaderPublication: true,
|
|
2641
|
+
}),
|
|
2642
|
+
);
|
|
2643
|
+
const membership = (
|
|
2644
|
+
await asAlice(() =>
|
|
2645
|
+
store.listContextMemberships({ contextId: context!.id, limit: 10 }),
|
|
2646
|
+
)
|
|
2647
|
+
).memberships[0]!;
|
|
2648
|
+
await asAlice(() =>
|
|
2649
|
+
store.createContextPack({
|
|
2650
|
+
name: "Published snapshot receipt",
|
|
2651
|
+
members: [
|
|
2652
|
+
{
|
|
2653
|
+
itemId: membership.publishedItemId!,
|
|
2654
|
+
itemVersionId: membership.publishedItemVersionId!,
|
|
2655
|
+
},
|
|
2656
|
+
],
|
|
2657
|
+
}),
|
|
2658
|
+
);
|
|
2659
|
+
|
|
2660
|
+
await expect(
|
|
2661
|
+
asAlice(() => store.purgeContextSourceArtifacts("source-1")),
|
|
2662
|
+
).resolves.toMatchObject({ sourceId: "source-1" });
|
|
2663
|
+
|
|
2664
|
+
const membershipRows = await exec.execute({
|
|
2665
|
+
sql: `SELECT status, published_item_id, published_item_version_id,
|
|
2666
|
+
pending_submission_id FROM creative_context_memberships WHERE id = ?`,
|
|
2667
|
+
args: [submitted.membershipId],
|
|
2668
|
+
});
|
|
2669
|
+
expect(membershipRows.rows[0]).toMatchObject({
|
|
2670
|
+
status: "removed",
|
|
2671
|
+
published_item_id: null,
|
|
2672
|
+
published_item_version_id: null,
|
|
2673
|
+
pending_submission_id: null,
|
|
2674
|
+
});
|
|
2675
|
+
for (const table of [
|
|
2676
|
+
"creative_context_submissions",
|
|
2677
|
+
"creative_context_published_snapshots",
|
|
2678
|
+
]) {
|
|
2679
|
+
const rows = await exec.execute({
|
|
2680
|
+
sql: `SELECT id FROM ${table} WHERE context_id = ?`,
|
|
2681
|
+
args: [context!.id],
|
|
2682
|
+
});
|
|
2683
|
+
expect(rows.rows).toEqual([]);
|
|
2684
|
+
}
|
|
2685
|
+
const copiedItems = await exec.execute({
|
|
2686
|
+
sql: `SELECT id FROM creative_context_items
|
|
2687
|
+
WHERE id = ? OR source_id IN (
|
|
2688
|
+
SELECT staging_source_id FROM creative_contexts WHERE id = ?
|
|
2689
|
+
UNION
|
|
2690
|
+
SELECT published_source_id FROM creative_contexts WHERE id = ?
|
|
2691
|
+
)`,
|
|
2692
|
+
args: [membership.publishedItemId, context!.id, context!.id],
|
|
2693
|
+
});
|
|
2694
|
+
expect(copiedItems.rows).toEqual([]);
|
|
2695
|
+
});
|
|
2696
|
+
|
|
1581
2697
|
it("allows explicit deprecated search without making removed items retrievable", async () => {
|
|
1582
2698
|
const { exec, runWithRequestContext, store } = await setup();
|
|
1583
2699
|
await exec.execute(
|