@agent-native/creative-context 0.0.0-beta-20260820202238
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 +154 -0
- package/agent-native.eject.json +41 -0
- package/agent-native.package.json +119 -0
- package/dist/actions/confirm-canonical-logo.d.ts +6 -0
- package/dist/actions/confirm-canonical-logo.d.ts.map +1 -0
- package/dist/actions/confirm-canonical-logo.js +19 -0
- package/dist/actions/confirm-canonical-logo.js.map +1 -0
- package/dist/actions/continue-context-import.d.ts +22 -0
- package/dist/actions/continue-context-import.d.ts.map +1 -0
- package/dist/actions/continue-context-import.js +13 -0
- package/dist/actions/continue-context-import.js.map +1 -0
- package/dist/actions/creative-context-a2a.d.ts +10 -0
- package/dist/actions/creative-context-a2a.d.ts.map +1 -0
- package/dist/actions/creative-context-a2a.js +66 -0
- package/dist/actions/creative-context-a2a.js.map +1 -0
- package/dist/actions/get-brand-profile.d.ts +33 -0
- package/dist/actions/get-brand-profile.d.ts.map +1 -0
- package/dist/actions/get-brand-profile.js +13 -0
- package/dist/actions/get-brand-profile.js.map +1 -0
- package/dist/actions/get-context-import-status.d.ts +22 -0
- package/dist/actions/get-context-import-status.d.ts.map +1 -0
- package/dist/actions/get-context-import-status.js +12 -0
- package/dist/actions/get-context-import-status.js.map +1 -0
- package/dist/actions/get-context-item.d.ts +2 -0
- package/dist/actions/get-context-item.d.ts.map +1 -0
- package/dist/actions/get-context-item.js +2 -0
- package/dist/actions/get-context-item.js.map +1 -0
- package/dist/actions/get-context-pack.d.ts +7 -0
- package/dist/actions/get-context-pack.d.ts.map +1 -0
- package/dist/actions/get-context-pack.js +12 -0
- package/dist/actions/get-context-pack.js.map +1 -0
- package/dist/actions/get-creative-context.d.ts +157 -0
- package/dist/actions/get-creative-context.d.ts.map +1 -0
- package/dist/actions/get-creative-context.js +148 -0
- package/dist/actions/get-creative-context.js.map +1 -0
- package/dist/actions/get-google-picker-session.d.ts +10 -0
- package/dist/actions/get-google-picker-session.d.ts.map +1 -0
- package/dist/actions/get-google-picker-session.js +38 -0
- package/dist/actions/get-google-picker-session.js.map +1 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +73 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/infer-brand-dna.d.ts +52 -0
- package/dist/actions/infer-brand-dna.d.ts.map +1 -0
- package/dist/actions/infer-brand-dna.js +13 -0
- package/dist/actions/infer-brand-dna.js.map +1 -0
- package/dist/actions/list-canonical-logo-candidates.d.ts +9 -0
- package/dist/actions/list-canonical-logo-candidates.d.ts.map +1 -0
- package/dist/actions/list-canonical-logo-candidates.js +82 -0
- package/dist/actions/list-canonical-logo-candidates.js.map +1 -0
- package/dist/actions/list-context-connections.d.ts +22 -0
- package/dist/actions/list-context-connections.d.ts.map +1 -0
- package/dist/actions/list-context-connections.js +43 -0
- package/dist/actions/list-context-connections.js.map +1 -0
- 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-context-packs.d.ts +10 -0
- package/dist/actions/list-context-packs.d.ts.map +1 -0
- package/dist/actions/list-context-packs.js +16 -0
- package/dist/actions/list-context-packs.js.map +1 -0
- package/dist/actions/list-context-sources.d.ts +12 -0
- package/dist/actions/list-context-sources.d.ts.map +1 -0
- package/dist/actions/list-context-sources.js +20 -0
- package/dist/actions/list-context-sources.js.map +1 -0
- package/dist/actions/list-context-suggestions.d.ts +13 -0
- package/dist/actions/list-context-suggestions.d.ts.map +1 -0
- package/dist/actions/list-context-suggestions.js +28 -0
- package/dist/actions/list-context-suggestions.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-brand-profile.d.ts +28 -0
- package/dist/actions/manage-brand-profile.d.ts.map +1 -0
- package/dist/actions/manage-brand-profile.js +47 -0
- package/dist/actions/manage-brand-profile.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 +36 -0
- package/dist/actions/manage-context-pack.d.ts.map +1 -0
- package/dist/actions/manage-context-pack.js +71 -0
- package/dist/actions/manage-context-pack.js.map +1 -0
- package/dist/actions/manage-context-source.d.ts +51 -0
- package/dist/actions/manage-context-source.d.ts.map +1 -0
- package/dist/actions/manage-context-source.js +130 -0
- package/dist/actions/manage-context-source.js.map +1 -0
- 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/manage-layout-template.d.ts +13 -0
- package/dist/actions/manage-layout-template.d.ts.map +1 -0
- package/dist/actions/manage-layout-template.js +43 -0
- package/dist/actions/manage-layout-template.js.map +1 -0
- package/dist/actions/preview-context-import.d.ts +23 -0
- package/dist/actions/preview-context-import.d.ts.map +1 -0
- package/dist/actions/preview-context-import.js +57 -0
- package/dist/actions/preview-context-import.js.map +1 -0
- package/dist/actions/process-context-purge.d.ts +37 -0
- package/dist/actions/process-context-purge.d.ts.map +1 -0
- package/dist/actions/process-context-purge.js +20 -0
- package/dist/actions/process-context-purge.js.map +1 -0
- package/dist/actions/propose-brand-dna.d.ts +15 -0
- package/dist/actions/propose-brand-dna.d.ts.map +1 -0
- package/dist/actions/propose-brand-dna.js +19 -0
- package/dist/actions/propose-brand-dna.js.map +1 -0
- package/dist/actions/propose-canonical-logo.d.ts +9 -0
- package/dist/actions/propose-canonical-logo.d.ts.map +1 -0
- package/dist/actions/propose-canonical-logo.js +16 -0
- package/dist/actions/propose-canonical-logo.js.map +1 -0
- package/dist/actions/publish-brand-dna.d.ts +13 -0
- package/dist/actions/publish-brand-dna.d.ts.map +1 -0
- package/dist/actions/publish-brand-dna.js +23 -0
- package/dist/actions/publish-brand-dna.js.map +1 -0
- package/dist/actions/recommend-context-roots.d.ts +14 -0
- package/dist/actions/recommend-context-roots.d.ts.map +1 -0
- package/dist/actions/recommend-context-roots.js +22 -0
- package/dist/actions/recommend-context-roots.js.map +1 -0
- package/dist/actions/record-context-feedback.d.ts +10 -0
- package/dist/actions/record-context-feedback.d.ts.map +1 -0
- package/dist/actions/record-context-feedback.js +14 -0
- package/dist/actions/record-context-feedback.js.map +1 -0
- package/dist/actions/review-context-items.d.ts +35 -0
- package/dist/actions/review-context-items.d.ts.map +1 -0
- package/dist/actions/review-context-items.js +65 -0
- package/dist/actions/review-context-items.js.map +1 -0
- package/dist/actions/run-embedding-bakeoff.d.ts +15 -0
- package/dist/actions/run-embedding-bakeoff.d.ts.map +1 -0
- package/dist/actions/run-embedding-bakeoff.js +72 -0
- package/dist/actions/run-embedding-bakeoff.js.map +1 -0
- package/dist/actions/search-creative-context.d.ts +80 -0
- package/dist/actions/search-creative-context.d.ts.map +1 -0
- package/dist/actions/search-creative-context.js +42 -0
- package/dist/actions/search-creative-context.js.map +1 -0
- package/dist/actions/set-brand-dna.d.ts +15 -0
- package/dist/actions/set-brand-dna.d.ts.map +1 -0
- package/dist/actions/set-brand-dna.js +19 -0
- package/dist/actions/set-brand-dna.js.map +1 -0
- package/dist/actions/start-context-enrichment.d.ts +25 -0
- package/dist/actions/start-context-enrichment.d.ts.map +1 -0
- package/dist/actions/start-context-enrichment.js +48 -0
- package/dist/actions/start-context-enrichment.js.map +1 -0
- package/dist/actions/start-context-import.d.ts +24 -0
- package/dist/actions/start-context-import.d.ts.map +1 -0
- package/dist/actions/start-context-import.js +37 -0
- package/dist/actions/start-context-import.js.map +1 -0
- package/dist/client/CreativeContextChip.d.ts +18 -0
- package/dist/client/CreativeContextChip.d.ts.map +1 -0
- package/dist/client/CreativeContextChip.js +80 -0
- package/dist/client/CreativeContextChip.js.map +1 -0
- package/dist/client/CreativeContextPanel.d.ts +73 -0
- package/dist/client/CreativeContextPanel.d.ts.map +1 -0
- package/dist/client/CreativeContextPanel.js +1171 -0
- package/dist/client/CreativeContextPanel.js.map +1 -0
- package/dist/client/CreativeContextSettingsLink.d.ts +4 -0
- package/dist/client/CreativeContextSettingsLink.d.ts.map +1 -0
- package/dist/client/CreativeContextSettingsLink.js +8 -0
- package/dist/client/CreativeContextSettingsLink.js.map +1 -0
- 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 +474 -0
- package/dist/client/actions.d.ts.map +1 -0
- package/dist/client/actions.js +411 -0
- package/dist/client/actions.js.map +1 -0
- package/dist/client/agent-tab.d.ts +10 -0
- package/dist/client/agent-tab.d.ts.map +1 -0
- package/dist/client/agent-tab.js +32 -0
- package/dist/client/agent-tab.js.map +1 -0
- package/dist/client/application-state.d.ts +24 -0
- package/dist/client/application-state.d.ts.map +1 -0
- package/dist/client/application-state.js +111 -0
- package/dist/client/application-state.js.map +1 -0
- package/dist/client/google-slides-picker.d.ts +19 -0
- package/dist/client/google-slides-picker.d.ts.map +1 -0
- package/dist/client/google-slides-picker.js +106 -0
- package/dist/client/google-slides-picker.js.map +1 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +10 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/messages.d.ts +5 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +1158 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/connectors/context.d.ts +9 -0
- package/dist/connectors/context.d.ts.map +1 -0
- package/dist/connectors/context.js +59 -0
- package/dist/connectors/context.js.map +1 -0
- package/dist/connectors/document-parser.d.ts +2 -0
- package/dist/connectors/document-parser.d.ts.map +1 -0
- package/dist/connectors/document-parser.js +2 -0
- package/dist/connectors/document-parser.js.map +1 -0
- package/dist/connectors/figma-native.d.ts +18 -0
- package/dist/connectors/figma-native.d.ts.map +1 -0
- package/dist/connectors/figma-native.js +883 -0
- package/dist/connectors/figma-native.js.map +1 -0
- package/dist/connectors/figma.d.ts +10 -0
- package/dist/connectors/figma.d.ts.map +1 -0
- package/dist/connectors/figma.js +166 -0
- package/dist/connectors/figma.js.map +1 -0
- package/dist/connectors/google-slides-native.d.ts +58 -0
- package/dist/connectors/google-slides-native.d.ts.map +1 -0
- package/dist/connectors/google-slides-native.js +1118 -0
- package/dist/connectors/google-slides-native.js.map +1 -0
- package/dist/connectors/google-slides.d.ts +15 -0
- package/dist/connectors/google-slides.d.ts.map +1 -0
- package/dist/connectors/google-slides.js +895 -0
- package/dist/connectors/google-slides.js.map +1 -0
- package/dist/connectors/index.d.ts +21 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +45 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/manual.d.ts +9 -0
- package/dist/connectors/manual.d.ts.map +1 -0
- package/dist/connectors/manual.js +91 -0
- package/dist/connectors/manual.js.map +1 -0
- package/dist/connectors/normalize.d.ts +42 -0
- package/dist/connectors/normalize.d.ts.map +1 -0
- package/dist/connectors/normalize.js +335 -0
- package/dist/connectors/normalize.js.map +1 -0
- package/dist/connectors/notion.d.ts +10 -0
- package/dist/connectors/notion.d.ts.map +1 -0
- package/dist/connectors/notion.js +393 -0
- package/dist/connectors/notion.js.map +1 -0
- package/dist/connectors/private-artifacts.d.ts +39 -0
- package/dist/connectors/private-artifacts.d.ts.map +1 -0
- package/dist/connectors/private-artifacts.js +175 -0
- package/dist/connectors/private-artifacts.js.map +1 -0
- package/dist/connectors/provider-response.d.ts +20 -0
- package/dist/connectors/provider-response.d.ts.map +1 -0
- package/dist/connectors/provider-response.js +98 -0
- package/dist/connectors/provider-response.js.map +1 -0
- package/dist/connectors/recommendations.d.ts +25 -0
- package/dist/connectors/recommendations.d.ts.map +1 -0
- package/dist/connectors/recommendations.js +172 -0
- package/dist/connectors/recommendations.js.map +1 -0
- package/dist/connectors/registry.d.ts +10 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +34 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/rendered-design.d.ts +68 -0
- package/dist/connectors/rendered-design.d.ts.map +1 -0
- package/dist/connectors/rendered-design.js +362 -0
- package/dist/connectors/rendered-design.js.map +1 -0
- package/dist/connectors/rendered-page.d.ts +115 -0
- package/dist/connectors/rendered-page.d.ts.map +1 -0
- package/dist/connectors/rendered-page.js +1036 -0
- package/dist/connectors/rendered-page.js.map +1 -0
- package/dist/connectors/smart-defaults.d.ts +9 -0
- package/dist/connectors/smart-defaults.d.ts.map +1 -0
- package/dist/connectors/smart-defaults.js +72 -0
- package/dist/connectors/smart-defaults.js.map +1 -0
- package/dist/connectors/types.d.ts +79 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +2 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/connectors/upload.d.ts +9 -0
- package/dist/connectors/upload.d.ts.map +1 -0
- package/dist/connectors/upload.js +440 -0
- package/dist/connectors/upload.js.map +1 -0
- package/dist/connectors/website.d.ts +20 -0
- package/dist/connectors/website.d.ts.map +1 -0
- package/dist/connectors/website.js +616 -0
- package/dist/connectors/website.js.map +1 -0
- package/dist/embeddings/index.d.ts +3 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/providers.d.ts +2 -0
- package/dist/embeddings/providers.d.ts.map +1 -0
- package/dist/embeddings/providers.js +2 -0
- package/dist/embeddings/providers.js.map +1 -0
- package/dist/embeddings/types.d.ts +2 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +2 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/eval/acceptance-fixtures.d.ts +3 -0
- package/dist/eval/acceptance-fixtures.d.ts.map +1 -0
- package/dist/eval/acceptance-fixtures.js +84 -0
- package/dist/eval/acceptance-fixtures.js.map +1 -0
- package/dist/eval/acceptance.d.ts +114 -0
- package/dist/eval/acceptance.d.ts.map +1 -0
- package/dist/eval/acceptance.js +260 -0
- package/dist/eval/acceptance.js.map +1 -0
- package/dist/eval/fixtures.d.ts +16 -0
- package/dist/eval/fixtures.d.ts.map +1 -0
- package/dist/eval/fixtures.js +119 -0
- package/dist/eval/fixtures.js.map +1 -0
- package/dist/eval/index.d.ts +63 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +174 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/background-worker.d.ts +31 -0
- package/dist/jobs/background-worker.d.ts.map +1 -0
- package/dist/jobs/background-worker.js +315 -0
- package/dist/jobs/background-worker.js.map +1 -0
- package/dist/jobs/import-runner.d.ts +3 -0
- package/dist/jobs/import-runner.d.ts.map +1 -0
- package/dist/jobs/import-runner.js +613 -0
- package/dist/jobs/import-runner.js.map +1 -0
- package/dist/jobs/index.d.ts +9 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +8 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/progress.d.ts +3 -0
- package/dist/jobs/progress.d.ts.map +1 -0
- package/dist/jobs/progress.js +30 -0
- package/dist/jobs/progress.js.map +1 -0
- package/dist/jobs/rebuild.d.ts +13 -0
- package/dist/jobs/rebuild.d.ts.map +1 -0
- package/dist/jobs/rebuild.js +178 -0
- package/dist/jobs/rebuild.js.map +1 -0
- package/dist/jobs/server-worker.d.ts +16 -0
- package/dist/jobs/server-worker.d.ts.map +1 -0
- package/dist/jobs/server-worker.js +262 -0
- package/dist/jobs/server-worker.js.map +1 -0
- package/dist/jobs/store-adapter.d.ts +3 -0
- package/dist/jobs/store-adapter.d.ts.map +1 -0
- package/dist/jobs/store-adapter.js +37 -0
- package/dist/jobs/store-adapter.js.map +1 -0
- package/dist/jobs/types.d.ts +138 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +2 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/jobs/worker.d.ts +27 -0
- package/dist/jobs/worker.d.ts.map +1 -0
- package/dist/jobs/worker.js +90 -0
- package/dist/jobs/worker.js.map +1 -0
- package/dist/media-url.d.ts +7 -0
- package/dist/media-url.d.ts.map +1 -0
- package/dist/media-url.js +15 -0
- package/dist/media-url.js.map +1 -0
- package/dist/native-artifact-reassembly.d.ts +25 -0
- package/dist/native-artifact-reassembly.d.ts.map +1 -0
- package/dist/native-artifact-reassembly.js +262 -0
- package/dist/native-artifact-reassembly.js.map +1 -0
- package/dist/native-artifact.d.ts +88 -0
- package/dist/native-artifact.d.ts.map +1 -0
- package/dist/native-artifact.js +86 -0
- package/dist/native-artifact.js.map +1 -0
- package/dist/schema/index.d.ts +7111 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +469 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/migrations.d.ts +10 -0
- package/dist/schema/migrations.d.ts.map +1 -0
- package/dist/schema/migrations.js +333 -0
- package/dist/schema/migrations.js.map +1 -0
- package/dist/search/fusion.d.ts +2 -0
- package/dist/search/fusion.d.ts.map +1 -0
- package/dist/search/fusion.js +2 -0
- package/dist/search/fusion.js.map +1 -0
- package/dist/search/mode.d.ts +4 -0
- package/dist/search/mode.d.ts.map +1 -0
- package/dist/search/mode.js +13 -0
- package/dist/search/mode.js.map +1 -0
- package/dist/search/postgres-fts.d.ts +2 -0
- package/dist/search/postgres-fts.d.ts.map +1 -0
- package/dist/search/postgres-fts.js +2 -0
- package/dist/search/postgres-fts.js.map +1 -0
- package/dist/server/brand-context.d.ts +15 -0
- package/dist/server/brand-context.d.ts.map +1 -0
- package/dist/server/brand-context.js +149 -0
- package/dist/server/brand-context.js.map +1 -0
- package/dist/server/context.d.ts +77 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +63 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/enrichment.d.ts +24 -0
- package/dist/server/enrichment.d.ts.map +1 -0
- package/dist/server/enrichment.js +138 -0
- package/dist/server/enrichment.js.map +1 -0
- package/dist/server/generation-artifact-access.d.ts +21 -0
- package/dist/server/generation-artifact-access.d.ts.map +1 -0
- package/dist/server/generation-artifact-access.js +113 -0
- package/dist/server/generation-artifact-access.js.map +1 -0
- package/dist/server/generation-context.d.ts +220 -0
- package/dist/server/generation-context.d.ts.map +1 -0
- package/dist/server/generation-context.js +432 -0
- package/dist/server/generation-context.js.map +1 -0
- package/dist/server/index.d.ts +27 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +115 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/isolated-a2a.d.ts +175 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -0
- package/dist/server/isolated-a2a.js +312 -0
- package/dist/server/isolated-a2a.js.map +1 -0
- package/dist/server/media.d.ts +15 -0
- package/dist/server/media.d.ts.map +1 -0
- package/dist/server/media.js +108 -0
- package/dist/server/media.js.map +1 -0
- 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/prompt-provider.d.ts +5 -0
- package/dist/server/prompt-provider.d.ts.map +1 -0
- package/dist/server/prompt-provider.js +59 -0
- package/dist/server/prompt-provider.js.map +1 -0
- package/dist/server/public-serialization.d.ts +160 -0
- package/dist/server/public-serialization.d.ts.map +1 -0
- package/dist/server/public-serialization.js +108 -0
- package/dist/server/public-serialization.js.map +1 -0
- package/dist/server/resource-paths.d.ts +3 -0
- package/dist/server/resource-paths.d.ts.map +1 -0
- package/dist/server/resource-paths.js +5 -0
- package/dist/server/resource-paths.js.map +1 -0
- package/dist/server/retrieval.d.ts +83 -0
- package/dist/server/retrieval.d.ts.map +1 -0
- package/dist/server/retrieval.js +445 -0
- package/dist/server/retrieval.js.map +1 -0
- 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 +148 -0
- package/dist/server/safe-native-preview.js.map +1 -0
- package/dist/store/brand.d.ts +47 -0
- package/dist/store/brand.d.ts.map +1 -0
- package/dist/store/brand.js +346 -0
- package/dist/store/brand.js.map +1 -0
- package/dist/store/content.d.ts +134 -0
- package/dist/store/content.d.ts.map +1 -0
- package/dist/store/content.js +1677 -0
- package/dist/store/content.js.map +1 -0
- package/dist/store/contexts.d.ts +216 -0
- package/dist/store/contexts.d.ts.map +1 -0
- package/dist/store/contexts.js +1346 -0
- package/dist/store/contexts.js.map +1 -0
- package/dist/store/generation.d.ts +28 -0
- package/dist/store/generation.d.ts.map +1 -0
- package/dist/store/generation.js +140 -0
- package/dist/store/generation.js.map +1 -0
- package/dist/store/helpers.d.ts +12 -0
- package/dist/store/helpers.d.ts.map +1 -0
- package/dist/store/helpers.js +41 -0
- package/dist/store/helpers.js.map +1 -0
- package/dist/store/index.d.ts +11 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +11 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/inference.d.ts +57 -0
- package/dist/store/inference.d.ts.map +1 -0
- package/dist/store/inference.js +279 -0
- package/dist/store/inference.js.map +1 -0
- package/dist/store/jobs.d.ts +80 -0
- package/dist/store/jobs.d.ts.map +1 -0
- package/dist/store/jobs.js +428 -0
- package/dist/store/jobs.js.map +1 -0
- package/dist/store/native-artifact-summary.d.ts +7 -0
- package/dist/store/native-artifact-summary.d.ts.map +1 -0
- package/dist/store/native-artifact-summary.js +10 -0
- package/dist/store/native-artifact-summary.js.map +1 -0
- package/dist/store/packs.d.ts +36 -0
- package/dist/store/packs.d.ts.map +1 -0
- package/dist/store/packs.js +438 -0
- package/dist/store/packs.js.map +1 -0
- package/dist/store/purge.d.ts +16 -0
- package/dist/store/purge.d.ts.map +1 -0
- package/dist/store/purge.js +354 -0
- package/dist/store/purge.js.map +1 -0
- package/dist/store/sources.d.ts +76 -0
- package/dist/store/sources.d.ts.map +1 -0
- package/dist/store/sources.js +525 -0
- package/dist/store/sources.js.map +1 -0
- package/dist/store/suggestions.d.ts +29 -0
- package/dist/store/suggestions.d.ts.map +1 -0
- package/dist/store/suggestions.js +270 -0
- package/dist/store/suggestions.js.map +1 -0
- package/dist/types.d.ts +578 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/untrusted-reference.d.ts +8 -0
- package/dist/untrusted-reference.d.ts.map +1 -0
- package/dist/untrusted-reference.js +19 -0
- package/dist/untrusted-reference.js.map +1 -0
- package/dist/vector/index.d.ts +2 -0
- package/dist/vector/index.d.ts.map +1 -0
- package/dist/vector/index.js +2 -0
- package/dist/vector/index.js.map +1 -0
- package/dist/vector/pgvector.d.ts +2 -0
- package/dist/vector/pgvector.d.ts.map +1 -0
- package/dist/vector/pgvector.js +2 -0
- package/dist/vector/pgvector.js.map +1 -0
- package/docs/evaluation.md +92 -0
- package/docs/llms-full.txt +168 -0
- package/docs/llms.txt +76 -0
- package/docs/skills/creative-context/SKILL.md +90 -0
- package/package.json +100 -0
- package/src/actions/agent-input-schemas.spec.ts +46 -0
- package/src/actions/confirm-canonical-logo.ts +20 -0
- package/src/actions/continue-context-import.ts +15 -0
- package/src/actions/creative-context-a2a.spec.ts +162 -0
- package/src/actions/creative-context-a2a.ts +88 -0
- package/src/actions/get-brand-profile.ts +16 -0
- package/src/actions/get-context-import-status.ts +13 -0
- package/src/actions/get-context-item.ts +1 -0
- package/src/actions/get-context-pack.ts +14 -0
- package/src/actions/get-creative-context.spec.ts +435 -0
- package/src/actions/get-creative-context.ts +197 -0
- package/src/actions/get-google-picker-session.spec.ts +62 -0
- package/src/actions/get-google-picker-session.ts +45 -0
- package/src/actions/index.ts +75 -0
- package/src/actions/infer-brand-dna.ts +15 -0
- package/src/actions/job-public-serialization.spec.ts +143 -0
- package/src/actions/list-canonical-logo-candidates.ts +121 -0
- package/src/actions/list-context-connections.spec.ts +23 -0
- package/src/actions/list-context-connections.ts +51 -0
- package/src/actions/list-context-memberships.ts +56 -0
- package/src/actions/list-context-packs.ts +17 -0
- package/src/actions/list-context-sources.ts +21 -0
- package/src/actions/list-context-suggestions.ts +30 -0
- package/src/actions/list-creative-contexts.spec.ts +38 -0
- package/src/actions/list-creative-contexts.ts +32 -0
- package/src/actions/manage-brand-profile.ts +58 -0
- package/src/actions/manage-context-membership.ts +80 -0
- package/src/actions/manage-context-pack.ts +81 -0
- package/src/actions/manage-context-source.ts +151 -0
- package/src/actions/manage-creative-context.ts +73 -0
- package/src/actions/manage-layout-template.ts +51 -0
- package/src/actions/preview-context-import.spec.ts +64 -0
- package/src/actions/preview-context-import.ts +61 -0
- package/src/actions/process-context-purge.ts +28 -0
- package/src/actions/propose-brand-dna.ts +22 -0
- package/src/actions/propose-canonical-logo.ts +19 -0
- package/src/actions/publish-brand-dna.ts +25 -0
- package/src/actions/recommend-context-roots.ts +24 -0
- package/src/actions/record-context-feedback.ts +16 -0
- package/src/actions/review-context-items.ts +69 -0
- package/src/actions/run-embedding-bakeoff.ts +90 -0
- package/src/actions/search-creative-context.ts +48 -0
- package/src/actions/set-brand-dna.ts +22 -0
- package/src/actions/start-context-enrichment.ts +52 -0
- package/src/actions/start-context-import.ts +38 -0
- package/src/client/CreativeContextChip.spec.ts +59 -0
- package/src/client/CreativeContextChip.tsx +243 -0
- package/src/client/CreativeContextPanel.tsx +3600 -0
- package/src/client/CreativeContextSettingsLink.tsx +30 -0
- package/src/client/CreativeContextShareTab.spec.ts +70 -0
- package/src/client/CreativeContextShareTab.tsx +666 -0
- package/src/client/actions.spec.ts +177 -0
- package/src/client/actions.ts +1024 -0
- package/src/client/agent-tab.tsx +56 -0
- package/src/client/application-state.spec.ts +46 -0
- package/src/client/application-state.ts +158 -0
- package/src/client/google-slides-picker.spec.ts +36 -0
- package/src/client/google-slides-picker.ts +146 -0
- package/src/client/index.ts +102 -0
- package/src/client/messages.ts +1370 -0
- package/src/client/source-config.spec.ts +166 -0
- package/src/connectors/connectors.spec.ts +778 -0
- package/src/connectors/context.ts +83 -0
- package/src/connectors/document-parser.ts +5 -0
- package/src/connectors/figma-native.ts +1192 -0
- package/src/connectors/figma-native.visual.spec.ts +221 -0
- package/src/connectors/figma.spec.ts +812 -0
- package/src/connectors/figma.ts +257 -0
- package/src/connectors/fixtures/google-slides-native.json +353 -0
- package/src/connectors/google-slides-native.spec.ts +804 -0
- package/src/connectors/google-slides-native.ts +1656 -0
- package/src/connectors/google-slides-native.visual.spec.ts +119 -0
- package/src/connectors/google-slides.ts +1171 -0
- package/src/connectors/index.ts +112 -0
- package/src/connectors/manual.ts +132 -0
- package/src/connectors/normalize.spec.ts +144 -0
- package/src/connectors/normalize.ts +475 -0
- package/src/connectors/notion.ts +529 -0
- package/src/connectors/private-artifacts.spec.ts +95 -0
- package/src/connectors/private-artifacts.ts +248 -0
- package/src/connectors/provider-response.ts +144 -0
- package/src/connectors/recommendations.ts +237 -0
- package/src/connectors/registry.ts +48 -0
- package/src/connectors/rendered-design.spec.ts +171 -0
- package/src/connectors/rendered-design.ts +495 -0
- package/src/connectors/rendered-page.spec.ts +388 -0
- package/src/connectors/rendered-page.ts +1387 -0
- package/src/connectors/smart-defaults.spec.ts +135 -0
- package/src/connectors/smart-defaults.ts +87 -0
- package/src/connectors/types.ts +113 -0
- package/src/connectors/upload.spec.ts +175 -0
- package/src/connectors/upload.ts +548 -0
- package/src/connectors/website.spec.ts +465 -0
- package/src/connectors/website.ts +777 -0
- package/src/embeddings/index.ts +2 -0
- package/src/embeddings/providers.spec.ts +73 -0
- package/src/embeddings/providers.ts +1 -0
- package/src/embeddings/types.ts +6 -0
- package/src/eval/acceptance-fixtures.ts +97 -0
- package/src/eval/acceptance.spec.ts +92 -0
- package/src/eval/acceptance.ts +371 -0
- package/src/eval/connectors-golden.spec.ts +421 -0
- package/src/eval/eval.spec.ts +166 -0
- package/src/eval/fixtures/README.md +17 -0
- package/src/eval/fixtures/figma-brand-system-v1.json +140 -0
- package/src/eval/fixtures/figma-brand-system-v2.json +127 -0
- package/src/eval/fixtures/launch-hero.png +0 -0
- package/src/eval/fixtures/launch-system-v1.pptx +0 -0
- package/src/eval/fixtures/launch-system-v2.pptx +0 -0
- package/src/eval/fixtures/notion-brand-root.json +218 -0
- package/src/eval/fixtures/pricing-hero.png +0 -0
- package/src/eval/fixtures/rendered-brand-page.html +76 -0
- package/src/eval/fixtures.spec.ts +43 -0
- package/src/eval/fixtures.ts +136 -0
- package/src/eval/index.ts +291 -0
- package/src/eval/library-flow.acceptance.spec.tsx +145 -0
- package/src/index.ts +4 -0
- package/src/jobs/background-worker.ts +423 -0
- package/src/jobs/import-runner.spec.ts +582 -0
- package/src/jobs/import-runner.ts +791 -0
- package/src/jobs/index.ts +40 -0
- package/src/jobs/progress.ts +37 -0
- package/src/jobs/rebuild.ts +245 -0
- package/src/jobs/server-worker.spec.ts +181 -0
- package/src/jobs/server-worker.ts +354 -0
- package/src/jobs/store-adapter.ts +52 -0
- package/src/jobs/types.ts +147 -0
- package/src/jobs/worker.ts +152 -0
- package/src/manifest.spec.ts +26 -0
- package/src/media-url.ts +17 -0
- package/src/native-artifact-reassembly.spec.ts +386 -0
- package/src/native-artifact-reassembly.ts +406 -0
- package/src/native-artifact.ts +102 -0
- package/src/schema/index.ts +527 -0
- package/src/schema/migrations.spec.ts +41 -0
- package/src/schema/migrations.ts +338 -0
- package/src/search/fusion.spec.ts +21 -0
- package/src/search/fusion.ts +1 -0
- package/src/search/mode.spec.ts +28 -0
- package/src/search/mode.ts +21 -0
- package/src/search/postgres-fts.spec.ts +63 -0
- package/src/search/postgres-fts.ts +1 -0
- package/src/server/brand-context.spec.ts +43 -0
- package/src/server/brand-context.ts +160 -0
- package/src/server/context.ts +162 -0
- package/src/server/enrichment.spec.ts +94 -0
- package/src/server/enrichment.ts +166 -0
- package/src/server/generation-artifact-access.spec.ts +104 -0
- package/src/server/generation-artifact-access.ts +217 -0
- package/src/server/generation-context-a2a.spec.ts +282 -0
- package/src/server/generation-context-off.spec.ts +213 -0
- package/src/server/generation-context.spec.ts +124 -0
- package/src/server/generation-context.ts +639 -0
- package/src/server/index.spec.ts +30 -0
- package/src/server/index.ts +163 -0
- package/src/server/isolated-a2a.spec.ts +184 -0
- package/src/server/isolated-a2a.ts +416 -0
- package/src/server/media.spec.ts +120 -0
- package/src/server/media.ts +136 -0
- package/src/server/native-resource-capture.spec.ts +121 -0
- package/src/server/native-resource-capture.ts +198 -0
- package/src/server/prompt-provider.spec.ts +78 -0
- package/src/server/prompt-provider.ts +73 -0
- package/src/server/public-serialization.spec.ts +150 -0
- package/src/server/public-serialization.ts +130 -0
- package/src/server/resource-paths.spec.ts +9 -0
- package/src/server/resource-paths.ts +5 -0
- package/src/server/retrieval-vector.spec.ts +180 -0
- package/src/server/retrieval.ts +590 -0
- package/src/server/safe-native-preview.spec.ts +29 -0
- package/src/server/safe-native-preview.ts +199 -0
- package/src/store/access.integration.spec.ts +3523 -0
- package/src/store/brand.ts +458 -0
- package/src/store/content.ts +2317 -0
- package/src/store/contexts.ts +1840 -0
- package/src/store/generation.ts +226 -0
- package/src/store/helpers.ts +53 -0
- package/src/store/index.ts +10 -0
- package/src/store/inference.spec.ts +84 -0
- package/src/store/inference.ts +339 -0
- package/src/store/invariants.spec.ts +119 -0
- package/src/store/jobs.ts +674 -0
- package/src/store/native-artifact-summary.ts +13 -0
- package/src/store/packs.ts +721 -0
- package/src/store/purge.ts +463 -0
- package/src/store/sources.spec.ts +102 -0
- package/src/store/sources.ts +692 -0
- package/src/store/suggestions.ts +361 -0
- package/src/types.ts +671 -0
- package/src/untrusted-reference.spec.ts +22 -0
- package/src/untrusted-reference.ts +24 -0
- package/src/vector/index.ts +1 -0
- package/src/vector/pgvector.spec.ts +115 -0
- package/src/vector/pgvector.ts +1 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,3523 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { normalizeContextItem } from "../connectors/normalize.js";
|
|
8
|
+
|
|
9
|
+
vi.setConfig({ hookTimeout: 30_000, testTimeout: 30_000 });
|
|
10
|
+
|
|
11
|
+
const originalDatabaseUrl = process.env.DATABASE_URL;
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "creative-context-access-"));
|
|
16
|
+
process.env.DATABASE_URL = `file:${path.join(tempDir, "app.db")}`;
|
|
17
|
+
delete process.env.DATABASE_AUTH_TOKEN;
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
const { closeDbExec } = await import("@agent-native/core/db");
|
|
23
|
+
await closeDbExec();
|
|
24
|
+
if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
|
|
25
|
+
else process.env.DATABASE_URL = originalDatabaseUrl;
|
|
26
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function setup() {
|
|
30
|
+
const [{ getDbExec, runMigrations }, { runWithRequestContext }] =
|
|
31
|
+
await Promise.all([
|
|
32
|
+
import("@agent-native/core/db"),
|
|
33
|
+
import("@agent-native/core/server"),
|
|
34
|
+
]);
|
|
35
|
+
const [{ creativeContextMigrations }, server, store] = await Promise.all([
|
|
36
|
+
import("../schema/migrations.js"),
|
|
37
|
+
import("../server/index.js"),
|
|
38
|
+
import("./index.js"),
|
|
39
|
+
]);
|
|
40
|
+
server.configureCreativeContext();
|
|
41
|
+
await runMigrations(creativeContextMigrations, {
|
|
42
|
+
table: "creative_context_test_migrations",
|
|
43
|
+
})({});
|
|
44
|
+
const exec = getDbExec();
|
|
45
|
+
await exec.execute(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS org_members (
|
|
47
|
+
id TEXT PRIMARY KEY, org_id TEXT NOT NULL, email TEXT NOT NULL,
|
|
48
|
+
role TEXT NOT NULL, joined_at INTEGER NOT NULL
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
await exec.execute({
|
|
52
|
+
sql: "INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
53
|
+
args: [
|
|
54
|
+
"member-alice",
|
|
55
|
+
"org-1",
|
|
56
|
+
"alice@example.test",
|
|
57
|
+
"owner",
|
|
58
|
+
0,
|
|
59
|
+
"member-bob",
|
|
60
|
+
"org-1",
|
|
61
|
+
"bob@example.test",
|
|
62
|
+
"member",
|
|
63
|
+
0,
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
await exec.execute({
|
|
67
|
+
sql: `INSERT INTO creative_context_sources
|
|
68
|
+
(id, name, kind, config, upstream_access, status, health_status,
|
|
69
|
+
item_count, restricted_item_count, owner_email, org_id, visibility,
|
|
70
|
+
created_at, updated_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
72
|
+
args: [
|
|
73
|
+
"source-1",
|
|
74
|
+
"Brand corpus",
|
|
75
|
+
"website",
|
|
76
|
+
"{}",
|
|
77
|
+
"available",
|
|
78
|
+
"active",
|
|
79
|
+
"healthy",
|
|
80
|
+
2,
|
|
81
|
+
1,
|
|
82
|
+
"alice@example.test",
|
|
83
|
+
"org-1",
|
|
84
|
+
"private",
|
|
85
|
+
"2026-07-16T00:00:00.000Z",
|
|
86
|
+
"2026-07-16T00:00:00.000Z",
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
for (const item of [
|
|
90
|
+
{ id: "allowed", access: "available", curation: "included" },
|
|
91
|
+
{ id: "restricted", access: "restricted", curation: "review" },
|
|
92
|
+
]) {
|
|
93
|
+
await exec.execute({
|
|
94
|
+
sql: `INSERT INTO creative_context_items
|
|
95
|
+
(id, source_id, external_id, kind, title, current_version_id,
|
|
96
|
+
current_content_hash, status, upstream_access, curation_status,
|
|
97
|
+
curation_rank, inventory_state, index_state, tags, colors, provenance,
|
|
98
|
+
metadata, created_at, updated_at, owner_email, org_id)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
100
|
+
args: [
|
|
101
|
+
item.id,
|
|
102
|
+
"source-1",
|
|
103
|
+
item.id,
|
|
104
|
+
"slide",
|
|
105
|
+
`${item.id} title`,
|
|
106
|
+
`${item.id}-v2`,
|
|
107
|
+
`${item.id}-hash-v2`,
|
|
108
|
+
"active",
|
|
109
|
+
item.access,
|
|
110
|
+
item.curation,
|
|
111
|
+
"normal",
|
|
112
|
+
"available",
|
|
113
|
+
"pending",
|
|
114
|
+
"[]",
|
|
115
|
+
"[]",
|
|
116
|
+
"{}",
|
|
117
|
+
"{}",
|
|
118
|
+
"2026-07-16T00:00:00.000Z",
|
|
119
|
+
"2026-07-16T00:00:00.000Z",
|
|
120
|
+
"alice@example.test",
|
|
121
|
+
"org-1",
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
for (const version of [1, 2]) {
|
|
125
|
+
await exec.execute({
|
|
126
|
+
sql: `INSERT INTO creative_context_item_versions
|
|
127
|
+
(id, item_id, version_number, content_hash, title, content,
|
|
128
|
+
parse_status, metadata, created_at, owner_email, org_id)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
130
|
+
args: [
|
|
131
|
+
`${item.id}-v${version}`,
|
|
132
|
+
item.id,
|
|
133
|
+
version,
|
|
134
|
+
`${item.id}-hash-v${version}`,
|
|
135
|
+
`${item.id} title`,
|
|
136
|
+
`${item.id} canonical version ${version}`,
|
|
137
|
+
"parsed",
|
|
138
|
+
"{}",
|
|
139
|
+
`2026-07-1${version}T00:00:00.000Z`,
|
|
140
|
+
"alice@example.test",
|
|
141
|
+
"org-1",
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
await exec.execute({
|
|
145
|
+
sql: `INSERT INTO creative_context_chunks
|
|
146
|
+
(id, item_id, item_version_id, ordinal, kind, text, metadata,
|
|
147
|
+
created_at, owner_email, org_id)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
149
|
+
args: [
|
|
150
|
+
`${item.id}-chunk-v${version}`,
|
|
151
|
+
item.id,
|
|
152
|
+
`${item.id}-v${version}`,
|
|
153
|
+
0,
|
|
154
|
+
"text",
|
|
155
|
+
`${item.id} canonical version ${version}`,
|
|
156
|
+
"{}",
|
|
157
|
+
`2026-07-1${version}T00:00:00.000Z`,
|
|
158
|
+
"alice@example.test",
|
|
159
|
+
"org-1",
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { exec, runWithRequestContext, store };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
describe("creative context access and revocation", () => {
|
|
168
|
+
it("promotes the exact compiled Slides layout instead of a supplied snapshot", async () => {
|
|
169
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
170
|
+
const server = await import("../server/index.js");
|
|
171
|
+
const promote = vi.fn(async () => {});
|
|
172
|
+
server.configureCreativeContext({
|
|
173
|
+
projections: {
|
|
174
|
+
layoutTemplate: {
|
|
175
|
+
promote,
|
|
176
|
+
demote: vi.fn(async () => {}),
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const html =
|
|
181
|
+
'<div class="fmd-slide google-slides-native" data-slide-id="slide-1"><p>Exact layout</p></div>';
|
|
182
|
+
const nativeArtifact = {
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
app: "slides",
|
|
185
|
+
format: "slides-html",
|
|
186
|
+
rootExternalId: "presentation-1:slide-1",
|
|
187
|
+
fidelityReport: {
|
|
188
|
+
exact: { count: 1 },
|
|
189
|
+
approximated: { count: 0, reasons: [] },
|
|
190
|
+
imageFallback: { count: 0, reasons: [] },
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
await exec.execute({
|
|
194
|
+
sql: `UPDATE creative_context_items
|
|
195
|
+
SET mime_type = ?, provenance = ? WHERE id = ?`,
|
|
196
|
+
args: [
|
|
197
|
+
"text/html",
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
compiler: "@agent-native/creative-context:google-slides-native",
|
|
200
|
+
}),
|
|
201
|
+
"allowed",
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
await exec.execute({
|
|
205
|
+
sql: `UPDATE creative_context_item_versions
|
|
206
|
+
SET content = ?, mime_type = ?, metadata = ? WHERE id = ?`,
|
|
207
|
+
args: [
|
|
208
|
+
html,
|
|
209
|
+
"text/html",
|
|
210
|
+
JSON.stringify({ nativeArtifact }),
|
|
211
|
+
"allowed-v2",
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const suggestion = await runWithRequestContext(
|
|
216
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
217
|
+
() =>
|
|
218
|
+
store.proposeCreativeContextSuggestion({
|
|
219
|
+
kind: "layout-template",
|
|
220
|
+
itemId: "allowed",
|
|
221
|
+
itemVersionId: "allowed-v2",
|
|
222
|
+
payload: { htmlSnapshot: "<p>Untrusted supplied snapshot</p>" },
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
await runWithRequestContext(
|
|
226
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
227
|
+
() =>
|
|
228
|
+
store.applyLayoutTemplateSuggestion({
|
|
229
|
+
suggestionId: suggestion.id,
|
|
230
|
+
operation: "promote",
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(promote).toHaveBeenCalledWith(
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
itemId: "allowed",
|
|
237
|
+
itemVersionId: "allowed-v2",
|
|
238
|
+
htmlSnapshot: html,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
const projection = await exec.execute({
|
|
242
|
+
sql: `SELECT i.mime_type, i.provenance, v.content, v.metadata
|
|
243
|
+
FROM creative_context_items i
|
|
244
|
+
JOIN creative_context_item_versions v ON v.id = i.current_version_id
|
|
245
|
+
WHERE i.kind = 'layout_template'`,
|
|
246
|
+
args: [],
|
|
247
|
+
});
|
|
248
|
+
expect(projection.rows).toHaveLength(1);
|
|
249
|
+
expect(projection.rows[0]).toMatchObject({
|
|
250
|
+
mime_type: "text/html",
|
|
251
|
+
content: html,
|
|
252
|
+
});
|
|
253
|
+
expect(String(projection.rows[0]?.content)).not.toContain(
|
|
254
|
+
"Untrusted supplied snapshot",
|
|
255
|
+
);
|
|
256
|
+
expect(JSON.parse(String(projection.rows[0]?.provenance))).toMatchObject({
|
|
257
|
+
compiler: "@agent-native/creative-context:google-slides-native",
|
|
258
|
+
promotedFromItemId: "allowed",
|
|
259
|
+
promotedFromItemVersionId: "allowed-v2",
|
|
260
|
+
});
|
|
261
|
+
expect(JSON.parse(String(projection.rows[0]?.metadata))).toMatchObject({
|
|
262
|
+
nativeArtifact: {
|
|
263
|
+
app: "slides",
|
|
264
|
+
format: "slides-html",
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("rejects an oversized compiled layout before creating its projection", async () => {
|
|
270
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
271
|
+
const server = await import("../server/index.js");
|
|
272
|
+
const promote = vi.fn(async () => {});
|
|
273
|
+
server.configureCreativeContext({
|
|
274
|
+
projections: {
|
|
275
|
+
layoutTemplate: { promote, demote: vi.fn(async () => {}) },
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
const html = `<div class="fmd-slide google-slides-native" data-slide-id="slide-1"><p>${"界".repeat(44_000)}</p></div>`;
|
|
279
|
+
await exec.execute({
|
|
280
|
+
sql: `UPDATE creative_context_items
|
|
281
|
+
SET mime_type = ?, provenance = ? WHERE id = ?`,
|
|
282
|
+
args: [
|
|
283
|
+
"text/html",
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
compiler: "@agent-native/creative-context:google-slides-native",
|
|
286
|
+
}),
|
|
287
|
+
"allowed",
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
await exec.execute({
|
|
291
|
+
sql: `UPDATE creative_context_item_versions
|
|
292
|
+
SET content = ?, mime_type = ?, metadata = ? WHERE id = ?`,
|
|
293
|
+
args: [
|
|
294
|
+
html,
|
|
295
|
+
"text/html",
|
|
296
|
+
JSON.stringify({
|
|
297
|
+
nativeArtifact: {
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
app: "slides",
|
|
300
|
+
format: "slides-html",
|
|
301
|
+
rootExternalId: "presentation-1:slide-1",
|
|
302
|
+
fidelityReport: {
|
|
303
|
+
exact: { count: 1 },
|
|
304
|
+
approximated: { count: 0, reasons: [] },
|
|
305
|
+
imageFallback: { count: 0, reasons: [] },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
"allowed-v2",
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
const suggestion = await runWithRequestContext(
|
|
313
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
314
|
+
() =>
|
|
315
|
+
store.proposeCreativeContextSuggestion({
|
|
316
|
+
kind: "layout-template",
|
|
317
|
+
itemId: "allowed",
|
|
318
|
+
itemVersionId: "allowed-v2",
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
await expect(
|
|
323
|
+
runWithRequestContext(
|
|
324
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
325
|
+
() =>
|
|
326
|
+
store.applyLayoutTemplateSuggestion({
|
|
327
|
+
suggestionId: suggestion.id,
|
|
328
|
+
operation: "promote",
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
331
|
+
).rejects.toThrow(/native content.*exceeds.*split the artifact/i);
|
|
332
|
+
expect(promote).not.toHaveBeenCalled();
|
|
333
|
+
const projectionCount = await exec.execute({
|
|
334
|
+
sql: "SELECT COUNT(*) AS count FROM creative_context_items WHERE kind = 'layout_template'",
|
|
335
|
+
args: [],
|
|
336
|
+
});
|
|
337
|
+
expect(Number(projectionCount.rows[0]?.count)).toBe(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("never trusts a caller-supplied layout projection item id", async () => {
|
|
341
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
342
|
+
const server = await import("../server/index.js");
|
|
343
|
+
const demote = vi.fn(async () => {});
|
|
344
|
+
server.configureCreativeContext({
|
|
345
|
+
projections: {
|
|
346
|
+
layoutTemplate: { promote: vi.fn(async () => {}), demote },
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
const proposed = await runWithRequestContext(
|
|
350
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
351
|
+
() =>
|
|
352
|
+
store.proposeCreativeContextSuggestion({
|
|
353
|
+
kind: "layout-template",
|
|
354
|
+
itemId: "allowed",
|
|
355
|
+
itemVersionId: "allowed-v2",
|
|
356
|
+
payload: { projectionItemId: "guessed-item", note: "keep" },
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
expect(proposed.payload).toEqual({ note: "keep" });
|
|
360
|
+
|
|
361
|
+
await exec.execute({
|
|
362
|
+
sql: `INSERT INTO creative_context_sources
|
|
363
|
+
(id, name, kind, config, upstream_access, status, health_status,
|
|
364
|
+
item_count, restricted_item_count, owner_email, org_id, visibility,
|
|
365
|
+
created_at, updated_at)
|
|
366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
367
|
+
args: [
|
|
368
|
+
"outside-source",
|
|
369
|
+
"Outside tenant",
|
|
370
|
+
"manual",
|
|
371
|
+
"{}",
|
|
372
|
+
"available",
|
|
373
|
+
"active",
|
|
374
|
+
"healthy",
|
|
375
|
+
1,
|
|
376
|
+
0,
|
|
377
|
+
"mallory@example.test",
|
|
378
|
+
"org-2",
|
|
379
|
+
"private",
|
|
380
|
+
"2026-07-16T00:00:00.000Z",
|
|
381
|
+
"2026-07-16T00:00:00.000Z",
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
await exec.execute({
|
|
385
|
+
sql: `INSERT INTO creative_context_items
|
|
386
|
+
(id, source_id, external_id, kind, title, current_version_id,
|
|
387
|
+
current_content_hash, status, upstream_access, curation_status,
|
|
388
|
+
curation_rank, inventory_state, index_state, tags, colors, provenance,
|
|
389
|
+
metadata, created_at, updated_at, owner_email, org_id)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
391
|
+
args: [
|
|
392
|
+
"outside-projection",
|
|
393
|
+
"outside-source",
|
|
394
|
+
"outside-projection",
|
|
395
|
+
"layout_template",
|
|
396
|
+
"Outside projection",
|
|
397
|
+
"outside-version",
|
|
398
|
+
"outside-hash",
|
|
399
|
+
"active",
|
|
400
|
+
"available",
|
|
401
|
+
"included",
|
|
402
|
+
"canonical",
|
|
403
|
+
"available",
|
|
404
|
+
"indexed",
|
|
405
|
+
"[]",
|
|
406
|
+
"[]",
|
|
407
|
+
JSON.stringify({
|
|
408
|
+
promotedFromSuggestionId: "malicious-layout-suggestion",
|
|
409
|
+
}),
|
|
410
|
+
"{}",
|
|
411
|
+
"2026-07-16T00:00:00.000Z",
|
|
412
|
+
"2026-07-16T00:00:00.000Z",
|
|
413
|
+
"mallory@example.test",
|
|
414
|
+
"org-2",
|
|
415
|
+
],
|
|
416
|
+
});
|
|
417
|
+
await exec.execute({
|
|
418
|
+
sql: `INSERT INTO creative_context_suggestions
|
|
419
|
+
(id, kind, status, item_id, item_version_id, payload, created_at,
|
|
420
|
+
updated_at, owner_email, org_id)
|
|
421
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
422
|
+
args: [
|
|
423
|
+
"malicious-layout-suggestion",
|
|
424
|
+
"layout-template",
|
|
425
|
+
"promoted",
|
|
426
|
+
"allowed",
|
|
427
|
+
"allowed-v2",
|
|
428
|
+
JSON.stringify({ projectionItemId: "outside-projection" }),
|
|
429
|
+
"2026-07-16T00:00:00.000Z",
|
|
430
|
+
"2026-07-16T00:00:00.000Z",
|
|
431
|
+
"alice@example.test",
|
|
432
|
+
"org-1",
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
await runWithRequestContext(
|
|
437
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
438
|
+
() =>
|
|
439
|
+
store.applyLayoutTemplateSuggestion({
|
|
440
|
+
suggestionId: "malicious-layout-suggestion",
|
|
441
|
+
operation: "demote",
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const outside = await exec.execute(
|
|
446
|
+
"SELECT status FROM creative_context_items WHERE id = 'outside-projection'",
|
|
447
|
+
);
|
|
448
|
+
expect(outside.rows[0]?.status).toBe("active");
|
|
449
|
+
expect(demote).toHaveBeenCalledWith({
|
|
450
|
+
suggestionId: "malicious-layout-suggestion",
|
|
451
|
+
projectionItemId: null,
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("requires a host access proof before sharing generation history", async () => {
|
|
456
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
457
|
+
const { assertGenerationArtifactAccess } =
|
|
458
|
+
await import("../server/generation-artifact-access.js");
|
|
459
|
+
const asBob = <T>(fn: () => Promise<T>) =>
|
|
460
|
+
runWithRequestContext(
|
|
461
|
+
{ userEmail: "bob@example.test", orgId: "org-1" },
|
|
462
|
+
fn,
|
|
463
|
+
);
|
|
464
|
+
const artifact = {
|
|
465
|
+
appId: "slides",
|
|
466
|
+
artifactType: "deck",
|
|
467
|
+
artifactId: "shared-deck",
|
|
468
|
+
};
|
|
469
|
+
const aliceProof = await runWithRequestContext(
|
|
470
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
471
|
+
() =>
|
|
472
|
+
assertGenerationArtifactAccess(
|
|
473
|
+
artifact,
|
|
474
|
+
{
|
|
475
|
+
resourceType: "creative-context-source",
|
|
476
|
+
resourceId: "source-1",
|
|
477
|
+
},
|
|
478
|
+
"editor",
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
await runWithRequestContext(
|
|
482
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
483
|
+
() =>
|
|
484
|
+
store.recordGenerationCreativeContext(
|
|
485
|
+
{
|
|
486
|
+
...artifact,
|
|
487
|
+
contextMode: "auto",
|
|
488
|
+
contextPackId: null,
|
|
489
|
+
reuseLabels: [],
|
|
490
|
+
elementProvenance: [
|
|
491
|
+
{ elementId: "slide-1", influence: "generated" },
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
{ artifactAccess: aliceProof },
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await expect(
|
|
499
|
+
asBob(() => store.getGenerationCreativeContext(artifact)),
|
|
500
|
+
).resolves.toBeNull();
|
|
501
|
+
await expect(
|
|
502
|
+
asBob(() =>
|
|
503
|
+
assertGenerationArtifactAccess(
|
|
504
|
+
artifact,
|
|
505
|
+
{
|
|
506
|
+
resourceType: "creative-context-source",
|
|
507
|
+
resourceId: "source-1",
|
|
508
|
+
},
|
|
509
|
+
"viewer",
|
|
510
|
+
),
|
|
511
|
+
),
|
|
512
|
+
).rejects.toThrow(/no access/i);
|
|
513
|
+
await expect(
|
|
514
|
+
asBob(() =>
|
|
515
|
+
store.getGenerationCreativeContext(artifact, {
|
|
516
|
+
artifactAccess: {
|
|
517
|
+
identityKey: JSON.stringify([
|
|
518
|
+
artifact.appId,
|
|
519
|
+
artifact.artifactType,
|
|
520
|
+
artifact.artifactId,
|
|
521
|
+
]),
|
|
522
|
+
minRole: "viewer",
|
|
523
|
+
} as never,
|
|
524
|
+
}),
|
|
525
|
+
),
|
|
526
|
+
).rejects.toThrow(/verified by the host application/i);
|
|
527
|
+
|
|
528
|
+
await asBob(() =>
|
|
529
|
+
store.recordGenerationCreativeContext({
|
|
530
|
+
...artifact,
|
|
531
|
+
contextMode: "auto",
|
|
532
|
+
contextPackId: null,
|
|
533
|
+
reuseLabels: [],
|
|
534
|
+
elementProvenance: [{ elementId: "poison", influence: "generated" }],
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
const poison = await exec.execute(
|
|
538
|
+
"SELECT org_id FROM creative_context_generation_records WHERE owner_email = 'bob@example.test'",
|
|
539
|
+
);
|
|
540
|
+
expect(poison.rows[0]?.org_id).toBeNull();
|
|
541
|
+
await expect(
|
|
542
|
+
runWithRequestContext(
|
|
543
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
544
|
+
() =>
|
|
545
|
+
store.getGenerationCreativeContext(artifact, {
|
|
546
|
+
artifactAccess: aliceProof,
|
|
547
|
+
}),
|
|
548
|
+
),
|
|
549
|
+
).resolves.toMatchObject({
|
|
550
|
+
artifactId: "shared-deck",
|
|
551
|
+
elementProvenance: [{ elementId: "slide-1" }],
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
await exec.execute({
|
|
555
|
+
sql: `INSERT INTO creative_context_source_shares
|
|
556
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
557
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
558
|
+
args: [
|
|
559
|
+
"source-share-bob",
|
|
560
|
+
"source-1",
|
|
561
|
+
"user",
|
|
562
|
+
"bob@example.test",
|
|
563
|
+
"viewer",
|
|
564
|
+
"alice@example.test",
|
|
565
|
+
"2026-07-16T00:00:00.000Z",
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
const bobProof = await asBob(() =>
|
|
569
|
+
assertGenerationArtifactAccess(
|
|
570
|
+
artifact,
|
|
571
|
+
{
|
|
572
|
+
resourceType: "creative-context-source",
|
|
573
|
+
resourceId: "source-1",
|
|
574
|
+
},
|
|
575
|
+
"viewer",
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
await expect(
|
|
579
|
+
asBob(() =>
|
|
580
|
+
store.getGenerationCreativeContext(artifact, {
|
|
581
|
+
artifactAccess: bobProof,
|
|
582
|
+
}),
|
|
583
|
+
),
|
|
584
|
+
).resolves.toMatchObject({
|
|
585
|
+
artifactId: "shared-deck",
|
|
586
|
+
elementProvenance: [{ elementId: "slide-1" }],
|
|
587
|
+
});
|
|
588
|
+
await expect(
|
|
589
|
+
asBob(() =>
|
|
590
|
+
store.getGenerationCreativeContext(
|
|
591
|
+
{ ...artifact, artifactId: "arbitrary-unasserted-id" },
|
|
592
|
+
{ artifactAccess: bobProof },
|
|
593
|
+
),
|
|
594
|
+
),
|
|
595
|
+
).rejects.toThrow(/verified by the host application/i);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("appends media enrichment while preserving version-pinned pack evidence", async () => {
|
|
599
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
600
|
+
await exec.execute({
|
|
601
|
+
sql: `INSERT INTO creative_context_media
|
|
602
|
+
(id, item_id, item_version_id, kind, mime_type, access_mode,
|
|
603
|
+
storage_key, caption_status, palette, metadata, created_at,
|
|
604
|
+
owner_email, org_id)
|
|
605
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
606
|
+
args: [
|
|
607
|
+
"media-before-enrichment",
|
|
608
|
+
"allowed",
|
|
609
|
+
"allowed-v2",
|
|
610
|
+
"image",
|
|
611
|
+
"image/png",
|
|
612
|
+
"private",
|
|
613
|
+
"private-blob:fixture",
|
|
614
|
+
"pending",
|
|
615
|
+
"[]",
|
|
616
|
+
"{}",
|
|
617
|
+
"2026-07-16T00:00:00.000Z",
|
|
618
|
+
"alice@example.test",
|
|
619
|
+
"org-1",
|
|
620
|
+
],
|
|
621
|
+
});
|
|
622
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
623
|
+
runWithRequestContext(
|
|
624
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
625
|
+
fn,
|
|
626
|
+
);
|
|
627
|
+
const pack = await asAlice(() =>
|
|
628
|
+
store.createContextPack({
|
|
629
|
+
name: "Pinned before enrichment",
|
|
630
|
+
members: [
|
|
631
|
+
{
|
|
632
|
+
itemId: "allowed",
|
|
633
|
+
itemVersionId: "allowed-v2",
|
|
634
|
+
reason: "Exact pre-enrichment evidence",
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const enriched = await asAlice(() =>
|
|
641
|
+
store.appendMediaEnrichmentVersion({
|
|
642
|
+
mediaId: "media-before-enrichment",
|
|
643
|
+
palette: ["#663399", "#ffffff"],
|
|
644
|
+
contentHash: "enriched-image-hash",
|
|
645
|
+
caption: "Builder dashboard on a purple background",
|
|
646
|
+
captionStatus: "complete",
|
|
647
|
+
ocrText: "Ship faster",
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(enriched.appended).toBe(true);
|
|
652
|
+
expect(enriched.itemVersionId).not.toBe("allowed-v2");
|
|
653
|
+
expect(enriched.mediaId).not.toBe("media-before-enrichment");
|
|
654
|
+
const pinned = await asAlice(() =>
|
|
655
|
+
store.getCreativeContextItem("allowed", "allowed-v2"),
|
|
656
|
+
);
|
|
657
|
+
expect(pinned?.media).toEqual([
|
|
658
|
+
expect.objectContaining({
|
|
659
|
+
id: "media-before-enrichment",
|
|
660
|
+
caption: null,
|
|
661
|
+
captionStatus: "pending",
|
|
662
|
+
palette: [],
|
|
663
|
+
}),
|
|
664
|
+
]);
|
|
665
|
+
const current = await asAlice(() =>
|
|
666
|
+
store.getCreativeContextItem("allowed"),
|
|
667
|
+
);
|
|
668
|
+
expect(current?.version.id).toBe(enriched.itemVersionId);
|
|
669
|
+
expect(current?.media).toEqual([
|
|
670
|
+
expect.objectContaining({
|
|
671
|
+
id: enriched.mediaId,
|
|
672
|
+
caption: "Builder dashboard on a purple background",
|
|
673
|
+
captionStatus: "complete",
|
|
674
|
+
ocrText: "Ship faster",
|
|
675
|
+
palette: ["#663399", "#ffffff"],
|
|
676
|
+
}),
|
|
677
|
+
]);
|
|
678
|
+
const pinnedPack = await asAlice(() => store.getContextPack(pack.id));
|
|
679
|
+
expect(pinnedPack?.members).toEqual([
|
|
680
|
+
expect.objectContaining({
|
|
681
|
+
itemId: "allowed",
|
|
682
|
+
itemVersionId: "allowed-v2",
|
|
683
|
+
}),
|
|
684
|
+
]);
|
|
685
|
+
|
|
686
|
+
const resync = await asAlice(() =>
|
|
687
|
+
store.ingestItems({
|
|
688
|
+
sourceId: "source-1",
|
|
689
|
+
items: [
|
|
690
|
+
{
|
|
691
|
+
externalId: "allowed",
|
|
692
|
+
kind: "slide",
|
|
693
|
+
title: "allowed title",
|
|
694
|
+
content: "allowed canonical version 2",
|
|
695
|
+
contentHash: "allowed-hash-v2",
|
|
696
|
+
upstreamAccess: "available",
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
}),
|
|
700
|
+
);
|
|
701
|
+
expect(resync.unchanged).toBe(1);
|
|
702
|
+
const afterResync = await asAlice(() =>
|
|
703
|
+
store.getCreativeContextItem("allowed"),
|
|
704
|
+
);
|
|
705
|
+
expect(afterResync?.version.id).toBe(enriched.itemVersionId);
|
|
706
|
+
expect(afterResync?.media[0]?.captionStatus).toBe("complete");
|
|
707
|
+
|
|
708
|
+
const retried = await asAlice(() =>
|
|
709
|
+
store.appendMediaEnrichmentVersion({
|
|
710
|
+
mediaId: "media-before-enrichment",
|
|
711
|
+
palette: ["#663399", "#ffffff"],
|
|
712
|
+
contentHash: "enriched-image-hash",
|
|
713
|
+
caption: "Builder dashboard on a purple background",
|
|
714
|
+
captionStatus: "complete",
|
|
715
|
+
ocrText: "Ship faster",
|
|
716
|
+
}),
|
|
717
|
+
);
|
|
718
|
+
expect(retried).toEqual({
|
|
719
|
+
itemId: "allowed",
|
|
720
|
+
itemVersionId: enriched.itemVersionId,
|
|
721
|
+
mediaId: enriched.mediaId,
|
|
722
|
+
appended: false,
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("reloads a concurrently inserted item instead of failing its idempotent ingest", async () => {
|
|
727
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
728
|
+
const ingest = () =>
|
|
729
|
+
runWithRequestContext(
|
|
730
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
731
|
+
() =>
|
|
732
|
+
store.ingestItems({
|
|
733
|
+
sourceId: "source-1",
|
|
734
|
+
items: [
|
|
735
|
+
{
|
|
736
|
+
externalId: "concurrent-item",
|
|
737
|
+
kind: "slide",
|
|
738
|
+
title: "Concurrent item",
|
|
739
|
+
content: "Same immutable content",
|
|
740
|
+
contentHash: "concurrent-hash",
|
|
741
|
+
provenance: { fixture: true },
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const results = await Promise.all([ingest(), ingest()]);
|
|
748
|
+
expect(results.reduce((total, result) => total + result.created, 0)).toBe(
|
|
749
|
+
1,
|
|
750
|
+
);
|
|
751
|
+
expect(results.reduce((total, result) => total + result.unchanged, 0)).toBe(
|
|
752
|
+
1,
|
|
753
|
+
);
|
|
754
|
+
const items = await exec.execute({
|
|
755
|
+
sql: "SELECT id FROM creative_context_items WHERE source_id = ? AND external_id = ?",
|
|
756
|
+
args: ["source-1", "concurrent-item"],
|
|
757
|
+
});
|
|
758
|
+
expect(items.rows).toHaveLength(1);
|
|
759
|
+
const versions = await exec.execute({
|
|
760
|
+
sql: "SELECT id FROM creative_context_item_versions WHERE item_id = ?",
|
|
761
|
+
args: [String(items.rows[0]!.id)],
|
|
762
|
+
});
|
|
763
|
+
expect(versions.rows).toHaveLength(1);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("rejects direct ingest that bypasses normalized SQL text limits", async () => {
|
|
767
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
768
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
769
|
+
runWithRequestContext(
|
|
770
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
771
|
+
fn,
|
|
772
|
+
);
|
|
773
|
+
const ingest = (
|
|
774
|
+
item: Parameters<typeof store.ingestItems>[0]["items"][0],
|
|
775
|
+
) =>
|
|
776
|
+
asAlice(() => store.ingestItems({ sourceId: "source-1", items: [item] }));
|
|
777
|
+
const base = {
|
|
778
|
+
kind: "document",
|
|
779
|
+
title: "Oversized direct ingest",
|
|
780
|
+
contentHash: "oversized-direct-hash",
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
await expect(
|
|
784
|
+
ingest({
|
|
785
|
+
...base,
|
|
786
|
+
externalId: "oversized-searchable",
|
|
787
|
+
content: "界".repeat(22_000),
|
|
788
|
+
}),
|
|
789
|
+
).rejects.toThrow(/searchable content.*exceeds.*normalize or chunk/i);
|
|
790
|
+
await expect(
|
|
791
|
+
ingest({
|
|
792
|
+
...base,
|
|
793
|
+
externalId: "oversized-summary",
|
|
794
|
+
content: "bounded",
|
|
795
|
+
summary: "🙂".repeat(2_049),
|
|
796
|
+
}),
|
|
797
|
+
).rejects.toThrow(/summary.*exceeds.*normalize the summary/i);
|
|
798
|
+
await expect(
|
|
799
|
+
ingest({
|
|
800
|
+
...base,
|
|
801
|
+
externalId: "oversized-chunks",
|
|
802
|
+
content: "bounded",
|
|
803
|
+
chunks: [
|
|
804
|
+
{ ordinal: 0, text: "a".repeat(40_000) },
|
|
805
|
+
{ ordinal: 1, text: "b".repeat(30_000) },
|
|
806
|
+
],
|
|
807
|
+
}),
|
|
808
|
+
).rejects.toThrow(/chunks exceed.*normalize or split/i);
|
|
809
|
+
await expect(
|
|
810
|
+
ingest({
|
|
811
|
+
...base,
|
|
812
|
+
externalId: "oversized-native",
|
|
813
|
+
kind: "google-slides-slide",
|
|
814
|
+
mimeType: "text/html",
|
|
815
|
+
content: `<div>${"🙂".repeat(32_768)}</div>`,
|
|
816
|
+
metadata: {
|
|
817
|
+
nativeArtifact: { app: "slides", format: "slides-html" },
|
|
818
|
+
},
|
|
819
|
+
}),
|
|
820
|
+
).rejects.toThrow(/native content.*exceeds.*private blob storage/i);
|
|
821
|
+
|
|
822
|
+
const rows = await exec.execute({
|
|
823
|
+
sql: "SELECT id FROM creative_context_items WHERE external_id LIKE 'oversized-%'",
|
|
824
|
+
args: [],
|
|
825
|
+
});
|
|
826
|
+
expect(rows.rows).toHaveLength(0);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it("keeps prior native code immutable across resync and resolves hierarchical children by source version", async () => {
|
|
830
|
+
const { runWithRequestContext, store } = await setup();
|
|
831
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
832
|
+
runWithRequestContext(
|
|
833
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
834
|
+
fn,
|
|
835
|
+
);
|
|
836
|
+
const firstHtml =
|
|
837
|
+
"<!doctype html><html><head></head><body><div>Version one</div></body></html>";
|
|
838
|
+
const secondHtml =
|
|
839
|
+
"<!doctype html><html><head></head><body><div>Version two</div></body></html>";
|
|
840
|
+
const first = await asAlice(() =>
|
|
841
|
+
store.ingestItems({
|
|
842
|
+
sourceId: "source-1",
|
|
843
|
+
items: [
|
|
844
|
+
{
|
|
845
|
+
externalId: "figma-file:frame-1",
|
|
846
|
+
kind: "figma-frame",
|
|
847
|
+
title: "Native frame",
|
|
848
|
+
mimeType: "text/html",
|
|
849
|
+
content: firstHtml,
|
|
850
|
+
contentHash: "native-code-v1",
|
|
851
|
+
sourceVersion: "figma-v1",
|
|
852
|
+
provenance: {
|
|
853
|
+
compiler: "@agent-native/core/ingestion:figma-node-to-html",
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
],
|
|
857
|
+
}),
|
|
858
|
+
);
|
|
859
|
+
const itemId = first.itemIds[0]!;
|
|
860
|
+
const firstDetail = await asAlice(() =>
|
|
861
|
+
store.getCreativeContextItem(itemId),
|
|
862
|
+
);
|
|
863
|
+
const firstVersionId = firstDetail!.version.id;
|
|
864
|
+
|
|
865
|
+
const second = await asAlice(() =>
|
|
866
|
+
store.ingestItems({
|
|
867
|
+
sourceId: "source-1",
|
|
868
|
+
items: [
|
|
869
|
+
{
|
|
870
|
+
externalId: "figma-file:frame-1",
|
|
871
|
+
kind: "figma-frame",
|
|
872
|
+
title: "Native frame",
|
|
873
|
+
mimeType: "text/html",
|
|
874
|
+
content: secondHtml,
|
|
875
|
+
contentHash: "native-code-v2",
|
|
876
|
+
sourceVersion: "figma-v2",
|
|
877
|
+
provenance: {
|
|
878
|
+
compiler: "@agent-native/core/ingestion:figma-node-to-html",
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
}),
|
|
883
|
+
);
|
|
884
|
+
expect(second.versioned).toBe(1);
|
|
885
|
+
await expect(
|
|
886
|
+
asAlice(() => store.getCreativeContextItem(itemId, firstVersionId)),
|
|
887
|
+
).resolves.toMatchObject({ version: { content: firstHtml } });
|
|
888
|
+
await expect(
|
|
889
|
+
asAlice(() => store.getCreativeContextItem(itemId)),
|
|
890
|
+
).resolves.toMatchObject({ version: { content: secondHtml } });
|
|
891
|
+
await expect(
|
|
892
|
+
asAlice(() =>
|
|
893
|
+
store.getCreativeContextItemByExternalId({
|
|
894
|
+
sourceId: "source-1",
|
|
895
|
+
externalId: "figma-file:frame-1",
|
|
896
|
+
sourceVersion: "figma-v1",
|
|
897
|
+
}),
|
|
898
|
+
),
|
|
899
|
+
).resolves.toMatchObject({
|
|
900
|
+
version: { id: firstVersionId, content: firstHtml },
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("versions notes-only changes while packs retain the prior immutable evidence", async () => {
|
|
905
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
906
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
907
|
+
runWithRequestContext(
|
|
908
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
909
|
+
fn,
|
|
910
|
+
);
|
|
911
|
+
const slide = (notes: string) =>
|
|
912
|
+
normalizeContextItem({
|
|
913
|
+
externalId: "notes-deck:slide-1",
|
|
914
|
+
kind: "google-slides-slide",
|
|
915
|
+
title: "Launch plan",
|
|
916
|
+
mimeType: "text/html",
|
|
917
|
+
content: '<div class="fmd-slide">Unchanged slide</div>',
|
|
918
|
+
summary: `Speaker notes: ${notes}`,
|
|
919
|
+
sourceVersion: "same-provider-revision",
|
|
920
|
+
metadata: {
|
|
921
|
+
speakerNotes: notes,
|
|
922
|
+
nativeArtifact: {
|
|
923
|
+
schemaVersion: 1,
|
|
924
|
+
app: "slides",
|
|
925
|
+
format: "slides-html",
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
chunks: [
|
|
929
|
+
{
|
|
930
|
+
ordinal: 0,
|
|
931
|
+
kind: "slides-native-lexical",
|
|
932
|
+
text: `Unchanged slide Speaker notes ${notes}`,
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
const first = await asAlice(() =>
|
|
938
|
+
store.ingestItems({ sourceId: "source-1", items: [slide("Draft")] }),
|
|
939
|
+
);
|
|
940
|
+
const itemId = first.itemIds[0]!;
|
|
941
|
+
const firstDetail = await asAlice(() =>
|
|
942
|
+
store.getCreativeContextItem(itemId),
|
|
943
|
+
);
|
|
944
|
+
const firstVersionId = firstDetail!.version.id;
|
|
945
|
+
const pack = await asAlice(() =>
|
|
946
|
+
store.createContextPack({
|
|
947
|
+
name: "Notes evidence",
|
|
948
|
+
members: [{ itemId }],
|
|
949
|
+
}),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const second = await asAlice(() =>
|
|
953
|
+
store.ingestItems({ sourceId: "source-1", items: [slide("Approved")] }),
|
|
954
|
+
);
|
|
955
|
+
expect(second).toMatchObject({ versioned: 1, unchanged: 0 });
|
|
956
|
+
await expect(
|
|
957
|
+
asAlice(() => store.getCreativeContextItem(itemId, firstVersionId)),
|
|
958
|
+
).resolves.toMatchObject({
|
|
959
|
+
version: { versionNumber: 1, summary: "Speaker notes: Draft" },
|
|
960
|
+
chunks: [
|
|
961
|
+
expect.objectContaining({ text: expect.stringContaining("Draft") }),
|
|
962
|
+
],
|
|
963
|
+
});
|
|
964
|
+
await expect(
|
|
965
|
+
asAlice(() => store.getCreativeContextItem(itemId)),
|
|
966
|
+
).resolves.toMatchObject({
|
|
967
|
+
version: { versionNumber: 2, summary: "Speaker notes: Approved" },
|
|
968
|
+
chunks: [
|
|
969
|
+
expect.objectContaining({ text: expect.stringContaining("Approved") }),
|
|
970
|
+
],
|
|
971
|
+
});
|
|
972
|
+
await expect(
|
|
973
|
+
asAlice(() => store.getContextPack(pack.id)),
|
|
974
|
+
).resolves.toMatchObject({
|
|
975
|
+
members: [expect.objectContaining({ itemVersionId: firstVersionId })],
|
|
976
|
+
});
|
|
977
|
+
const versions = await exec.execute({
|
|
978
|
+
sql: "SELECT version_number FROM creative_context_item_versions WHERE item_id = ? ORDER BY version_number",
|
|
979
|
+
args: [itemId],
|
|
980
|
+
});
|
|
981
|
+
expect(versions.rows).toEqual([
|
|
982
|
+
expect.objectContaining({ version_number: 1 }),
|
|
983
|
+
expect.objectContaining({ version_number: 2 }),
|
|
984
|
+
]);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it("dedupes reordered evidence but versions metadata and media-only changes", async () => {
|
|
988
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
989
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
990
|
+
runWithRequestContext(
|
|
991
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
992
|
+
fn,
|
|
993
|
+
);
|
|
994
|
+
const item = (input: { reversed: boolean; assetRole: string }) => {
|
|
995
|
+
const chunks = [
|
|
996
|
+
{ ordinal: 0, kind: "text", text: "Stable body" },
|
|
997
|
+
{ ordinal: 1, kind: "code", text: "font-family: Inter" },
|
|
998
|
+
];
|
|
999
|
+
const media = [
|
|
1000
|
+
{
|
|
1001
|
+
kind: "image" as const,
|
|
1002
|
+
accessMode: "public" as const,
|
|
1003
|
+
url: "https://cdn.example.test/a.png",
|
|
1004
|
+
contentHash: "asset-a",
|
|
1005
|
+
metadata: { role: input.assetRole },
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
kind: "image" as const,
|
|
1009
|
+
accessMode: "public" as const,
|
|
1010
|
+
url: "https://cdn.example.test/b.png",
|
|
1011
|
+
contentHash: "asset-b",
|
|
1012
|
+
metadata: { role: "supporting" },
|
|
1013
|
+
},
|
|
1014
|
+
];
|
|
1015
|
+
const edges = [
|
|
1016
|
+
{ relation: "uses-asset", toExternalId: "asset-a" },
|
|
1017
|
+
{ relation: "uses-asset", toExternalId: "asset-b" },
|
|
1018
|
+
];
|
|
1019
|
+
return normalizeContextItem({
|
|
1020
|
+
externalId: "media-evidence",
|
|
1021
|
+
kind: "design-artifact",
|
|
1022
|
+
title: "Media evidence",
|
|
1023
|
+
content: "Stable body",
|
|
1024
|
+
sourceVersion: "stable-source-version",
|
|
1025
|
+
metadata: { fidelity: { exact: 2 }, provider: "fixture" },
|
|
1026
|
+
chunks: input.reversed ? [...chunks].reverse() : chunks,
|
|
1027
|
+
media: input.reversed ? [...media].reverse() : media,
|
|
1028
|
+
edges: input.reversed ? [...edges].reverse() : edges,
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const original = item({ reversed: false, assetRole: "hero" });
|
|
1033
|
+
const reordered = item({ reversed: true, assetRole: "hero" });
|
|
1034
|
+
expect(reordered.contentHash).toBe(original.contentHash);
|
|
1035
|
+
const first = await asAlice(() =>
|
|
1036
|
+
store.ingestItems({ sourceId: "source-1", items: [original] }),
|
|
1037
|
+
);
|
|
1038
|
+
const itemId = first.itemIds[0]!;
|
|
1039
|
+
const firstVersionId = (await asAlice(() =>
|
|
1040
|
+
store.getCreativeContextItem(itemId),
|
|
1041
|
+
))!.version.id;
|
|
1042
|
+
const pack = await asAlice(() =>
|
|
1043
|
+
store.createContextPack({
|
|
1044
|
+
name: "Media evidence",
|
|
1045
|
+
members: [{ itemId }],
|
|
1046
|
+
}),
|
|
1047
|
+
);
|
|
1048
|
+
const unchanged = await asAlice(() =>
|
|
1049
|
+
store.ingestItems({ sourceId: "source-1", items: [reordered] }),
|
|
1050
|
+
);
|
|
1051
|
+
expect(unchanged).toMatchObject({ versioned: 0, unchanged: 1 });
|
|
1052
|
+
|
|
1053
|
+
const changed = await asAlice(() =>
|
|
1054
|
+
store.ingestItems({
|
|
1055
|
+
sourceId: "source-1",
|
|
1056
|
+
items: [item({ reversed: true, assetRole: "canonical-logo" })],
|
|
1057
|
+
}),
|
|
1058
|
+
);
|
|
1059
|
+
expect(changed).toMatchObject({ versioned: 1, unchanged: 0 });
|
|
1060
|
+
await expect(
|
|
1061
|
+
asAlice(() => store.getCreativeContextItem(itemId, firstVersionId)),
|
|
1062
|
+
).resolves.toMatchObject({
|
|
1063
|
+
version: { versionNumber: 1 },
|
|
1064
|
+
media: [
|
|
1065
|
+
expect.objectContaining({ metadata: { role: "hero" } }),
|
|
1066
|
+
expect.objectContaining({ metadata: { role: "supporting" } }),
|
|
1067
|
+
],
|
|
1068
|
+
});
|
|
1069
|
+
await expect(
|
|
1070
|
+
asAlice(() => store.getCreativeContextItem(itemId)),
|
|
1071
|
+
).resolves.toMatchObject({
|
|
1072
|
+
version: { versionNumber: 2 },
|
|
1073
|
+
media: expect.arrayContaining([
|
|
1074
|
+
expect.objectContaining({ metadata: { role: "canonical-logo" } }),
|
|
1075
|
+
]),
|
|
1076
|
+
});
|
|
1077
|
+
await expect(
|
|
1078
|
+
asAlice(() => store.getContextPack(pack.id)),
|
|
1079
|
+
).resolves.toMatchObject({
|
|
1080
|
+
members: [expect.objectContaining({ itemVersionId: firstVersionId })],
|
|
1081
|
+
});
|
|
1082
|
+
const versions = await exec.execute({
|
|
1083
|
+
sql: "SELECT version_number FROM creative_context_item_versions WHERE item_id = ? ORDER BY version_number",
|
|
1084
|
+
args: [itemId],
|
|
1085
|
+
});
|
|
1086
|
+
expect(versions.rows).toHaveLength(2);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("pins hierarchical parent versions to exact child versions across later resyncs and access loss", async () => {
|
|
1090
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
1091
|
+
const { reassembleNativeCreativeArtifact } =
|
|
1092
|
+
await import("../native-artifact-reassembly.js");
|
|
1093
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
1094
|
+
runWithRequestContext(
|
|
1095
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
1096
|
+
fn,
|
|
1097
|
+
);
|
|
1098
|
+
const fidelityReport = {
|
|
1099
|
+
exact: { count: 1 },
|
|
1100
|
+
approximated: { count: 0, reasons: [] },
|
|
1101
|
+
imageFallback: { count: 0, reasons: [] },
|
|
1102
|
+
};
|
|
1103
|
+
const parentExternalId = "figma-file:parent";
|
|
1104
|
+
const childExternalId = "figma-file:child";
|
|
1105
|
+
const parentHtml = (label: string) =>
|
|
1106
|
+
`<!doctype html><html><head></head><body><div data-label="${label}"><div data-creative-context-child="${childExternalId}"></div></div></body></html>`;
|
|
1107
|
+
const childHtml = (label: string) =>
|
|
1108
|
+
`<!doctype html><html><head></head><body><div>${label}</div></body></html>`;
|
|
1109
|
+
const nativeMetadata = (externalId: string, child = false) => ({
|
|
1110
|
+
nativeArtifact: {
|
|
1111
|
+
schemaVersion: 1,
|
|
1112
|
+
app: "design",
|
|
1113
|
+
format: "design-html",
|
|
1114
|
+
rootExternalId: externalId,
|
|
1115
|
+
...(child
|
|
1116
|
+
? {}
|
|
1117
|
+
: {
|
|
1118
|
+
childExternalIds: [childExternalId],
|
|
1119
|
+
manifest: {
|
|
1120
|
+
kind: "hierarchical-artboard",
|
|
1121
|
+
children: [
|
|
1122
|
+
{
|
|
1123
|
+
externalId: childExternalId,
|
|
1124
|
+
sourceNodeId: "2:1",
|
|
1125
|
+
bounds: { x: 10, y: 20, width: 200, height: 100 },
|
|
1126
|
+
zOrder: 0,
|
|
1127
|
+
},
|
|
1128
|
+
],
|
|
1129
|
+
},
|
|
1130
|
+
}),
|
|
1131
|
+
fidelityReport,
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
const provenance = {
|
|
1135
|
+
compiler: "@agent-native/core/ingestion:figma-node-to-html",
|
|
1136
|
+
};
|
|
1137
|
+
const parentEdges = [
|
|
1138
|
+
{
|
|
1139
|
+
relation: "contains-native-child",
|
|
1140
|
+
toExternalId: childExternalId,
|
|
1141
|
+
},
|
|
1142
|
+
];
|
|
1143
|
+
|
|
1144
|
+
const first = await asAlice(() =>
|
|
1145
|
+
store.ingestItems({
|
|
1146
|
+
sourceId: "source-1",
|
|
1147
|
+
items: [
|
|
1148
|
+
{
|
|
1149
|
+
externalId: parentExternalId,
|
|
1150
|
+
kind: "figma-artboard-manifest",
|
|
1151
|
+
title: "Parent",
|
|
1152
|
+
mimeType: "text/html",
|
|
1153
|
+
content: parentHtml("parent-v1"),
|
|
1154
|
+
contentHash: "parent-v1",
|
|
1155
|
+
sourceVersion: "figma-v1",
|
|
1156
|
+
provenance,
|
|
1157
|
+
metadata: nativeMetadata(parentExternalId),
|
|
1158
|
+
edges: parentEdges,
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
externalId: childExternalId,
|
|
1162
|
+
kind: "figma-frame",
|
|
1163
|
+
title: "Child",
|
|
1164
|
+
mimeType: "text/html",
|
|
1165
|
+
content: childHtml("Child v1"),
|
|
1166
|
+
contentHash: "child-v1",
|
|
1167
|
+
sourceVersion: "figma-v1",
|
|
1168
|
+
provenance,
|
|
1169
|
+
metadata: nativeMetadata(childExternalId, true),
|
|
1170
|
+
},
|
|
1171
|
+
],
|
|
1172
|
+
}),
|
|
1173
|
+
);
|
|
1174
|
+
const parentId = first.itemIds[0]!;
|
|
1175
|
+
const childId = first.itemIds[1]!;
|
|
1176
|
+
const childV1 = await asAlice(() => store.getCreativeContextItem(childId));
|
|
1177
|
+
|
|
1178
|
+
await asAlice(() =>
|
|
1179
|
+
store.ingestItems({
|
|
1180
|
+
sourceId: "source-1",
|
|
1181
|
+
items: [
|
|
1182
|
+
{
|
|
1183
|
+
externalId: parentExternalId,
|
|
1184
|
+
kind: "figma-artboard-manifest",
|
|
1185
|
+
title: "Parent",
|
|
1186
|
+
mimeType: "text/html",
|
|
1187
|
+
content: parentHtml("parent-v2"),
|
|
1188
|
+
contentHash: "parent-v2",
|
|
1189
|
+
sourceVersion: "figma-v2",
|
|
1190
|
+
provenance,
|
|
1191
|
+
metadata: nativeMetadata(parentExternalId),
|
|
1192
|
+
edges: parentEdges,
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
externalId: childExternalId,
|
|
1196
|
+
kind: "figma-frame",
|
|
1197
|
+
title: "Child",
|
|
1198
|
+
mimeType: "text/html",
|
|
1199
|
+
content: childHtml("Child v1"),
|
|
1200
|
+
contentHash: "child-v1",
|
|
1201
|
+
sourceVersion: "figma-v2",
|
|
1202
|
+
provenance,
|
|
1203
|
+
metadata: nativeMetadata(childExternalId, true),
|
|
1204
|
+
},
|
|
1205
|
+
],
|
|
1206
|
+
}),
|
|
1207
|
+
);
|
|
1208
|
+
const parentV2 = await asAlice(() =>
|
|
1209
|
+
store.getCreativeContextItem(parentId),
|
|
1210
|
+
);
|
|
1211
|
+
expect(parentV2?.edges).toEqual(
|
|
1212
|
+
expect.arrayContaining([
|
|
1213
|
+
expect.objectContaining({
|
|
1214
|
+
relation: "contains-native-child",
|
|
1215
|
+
toItemId: childId,
|
|
1216
|
+
toItemVersionId: childV1!.version.id,
|
|
1217
|
+
}),
|
|
1218
|
+
]),
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
await asAlice(() =>
|
|
1222
|
+
store.ingestItems({
|
|
1223
|
+
sourceId: "source-1",
|
|
1224
|
+
items: [
|
|
1225
|
+
{
|
|
1226
|
+
externalId: childExternalId,
|
|
1227
|
+
kind: "figma-frame",
|
|
1228
|
+
title: "Child",
|
|
1229
|
+
mimeType: "text/html",
|
|
1230
|
+
content: childHtml("Child v2"),
|
|
1231
|
+
contentHash: "child-v2",
|
|
1232
|
+
sourceVersion: "figma-v3",
|
|
1233
|
+
provenance,
|
|
1234
|
+
metadata: nativeMetadata(childExternalId, true),
|
|
1235
|
+
},
|
|
1236
|
+
],
|
|
1237
|
+
}),
|
|
1238
|
+
);
|
|
1239
|
+
const pinnedParent = await asAlice(() =>
|
|
1240
|
+
store.getCreativeContextItem(parentId, parentV2!.version.id),
|
|
1241
|
+
);
|
|
1242
|
+
const reassembled = await asAlice(() =>
|
|
1243
|
+
reassembleNativeCreativeArtifact({
|
|
1244
|
+
root: pinnedParent!,
|
|
1245
|
+
app: "design",
|
|
1246
|
+
format: "design-html",
|
|
1247
|
+
resolveChild: store.getCreativeContextItemByExternalId,
|
|
1248
|
+
}),
|
|
1249
|
+
);
|
|
1250
|
+
expect(reassembled.html).toContain("Child v1");
|
|
1251
|
+
expect(reassembled.html).not.toContain("Child v2");
|
|
1252
|
+
|
|
1253
|
+
await exec.execute({
|
|
1254
|
+
sql: "UPDATE creative_context_items SET status = 'unavailable' WHERE id = ?",
|
|
1255
|
+
args: [childId],
|
|
1256
|
+
});
|
|
1257
|
+
await expect(
|
|
1258
|
+
asAlice(() =>
|
|
1259
|
+
reassembleNativeCreativeArtifact({
|
|
1260
|
+
root: pinnedParent!,
|
|
1261
|
+
app: "design",
|
|
1262
|
+
format: "design-html",
|
|
1263
|
+
resolveChild: store.getCreativeContextItemByExternalId,
|
|
1264
|
+
}),
|
|
1265
|
+
),
|
|
1266
|
+
).rejects.toThrow("unavailable at the pinned source version");
|
|
1267
|
+
});
|
|
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
|
+
|
|
2025
|
+
it("finds native artifacts through bounded code-token chunks", async () => {
|
|
2026
|
+
const { runWithRequestContext, store } = await setup();
|
|
2027
|
+
const { performCreativeContextSearch } =
|
|
2028
|
+
await import("../server/retrieval.js");
|
|
2029
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2030
|
+
runWithRequestContext(
|
|
2031
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2032
|
+
fn,
|
|
2033
|
+
);
|
|
2034
|
+
const ingested = await asAlice(() =>
|
|
2035
|
+
store.ingestItems({
|
|
2036
|
+
sourceId: "source-1",
|
|
2037
|
+
items: [
|
|
2038
|
+
{
|
|
2039
|
+
externalId: "figma-file:metric-card",
|
|
2040
|
+
kind: "figma-frame",
|
|
2041
|
+
title: "Metric card",
|
|
2042
|
+
mimeType: "text/html",
|
|
2043
|
+
content:
|
|
2044
|
+
'<!doctype html><html><head></head><body><div class="metric-card"></div></body></html>',
|
|
2045
|
+
contentHash: "metric-card-native-v1",
|
|
2046
|
+
chunks: [
|
|
2047
|
+
{
|
|
2048
|
+
ordinal: 0,
|
|
2049
|
+
kind: "code",
|
|
2050
|
+
text: "metric-card display-grid align-items-center",
|
|
2051
|
+
metadata: {
|
|
2052
|
+
role: "code-tokens",
|
|
2053
|
+
format: "design-html",
|
|
2054
|
+
},
|
|
2055
|
+
},
|
|
2056
|
+
],
|
|
2057
|
+
},
|
|
2058
|
+
],
|
|
2059
|
+
}),
|
|
2060
|
+
);
|
|
2061
|
+
|
|
2062
|
+
const result = await asAlice(() =>
|
|
2063
|
+
performCreativeContextSearch({
|
|
2064
|
+
query: "metric-card align-items-center",
|
|
2065
|
+
limit: 5,
|
|
2066
|
+
matchMode: "allTerms",
|
|
2067
|
+
snapshot: false,
|
|
2068
|
+
}),
|
|
2069
|
+
);
|
|
2070
|
+
expect(result.results).toEqual(
|
|
2071
|
+
expect.arrayContaining([
|
|
2072
|
+
expect.objectContaining({
|
|
2073
|
+
itemId: ingested.itemIds[0],
|
|
2074
|
+
kind: "figma-frame",
|
|
2075
|
+
}),
|
|
2076
|
+
]),
|
|
2077
|
+
);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
it("reloads a concurrently discovered inventory item instead of duplicating it", async () => {
|
|
2081
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2082
|
+
const upsert = () =>
|
|
2083
|
+
runWithRequestContext(
|
|
2084
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2085
|
+
() =>
|
|
2086
|
+
store.upsertSourceInventory({
|
|
2087
|
+
sourceId: "source-1",
|
|
2088
|
+
items: [
|
|
2089
|
+
{
|
|
2090
|
+
externalId: "concurrent-inventory",
|
|
2091
|
+
kind: "document",
|
|
2092
|
+
title: "Concurrent inventory",
|
|
2093
|
+
},
|
|
2094
|
+
],
|
|
2095
|
+
}),
|
|
2096
|
+
);
|
|
2097
|
+
|
|
2098
|
+
const results = await Promise.all([upsert(), upsert()]);
|
|
2099
|
+
expect(results.reduce((total, result) => total + result.created, 0)).toBe(
|
|
2100
|
+
1,
|
|
2101
|
+
);
|
|
2102
|
+
expect(results.reduce((total, result) => total + result.updated, 0)).toBe(
|
|
2103
|
+
1,
|
|
2104
|
+
);
|
|
2105
|
+
const items = await exec.execute({
|
|
2106
|
+
sql: "SELECT id FROM creative_context_items WHERE source_id = ? AND external_id = ?",
|
|
2107
|
+
args: ["source-1", "concurrent-inventory"],
|
|
2108
|
+
});
|
|
2109
|
+
expect(items.rows).toHaveLength(1);
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
it("reconciles removed and restored inventory without weakening source access", async () => {
|
|
2113
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2114
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2115
|
+
runWithRequestContext(
|
|
2116
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2117
|
+
fn,
|
|
2118
|
+
);
|
|
2119
|
+
const asBob = <T>(fn: () => Promise<T>) =>
|
|
2120
|
+
runWithRequestContext(
|
|
2121
|
+
{ userEmail: "bob@example.test", orgId: "org-1" },
|
|
2122
|
+
fn,
|
|
2123
|
+
);
|
|
2124
|
+
|
|
2125
|
+
await expect(
|
|
2126
|
+
asBob(() =>
|
|
2127
|
+
store.reconcileSourceInventory({
|
|
2128
|
+
sourceId: "source-1",
|
|
2129
|
+
presentExternalIds: ["allowed"],
|
|
2130
|
+
}),
|
|
2131
|
+
),
|
|
2132
|
+
).rejects.toThrow();
|
|
2133
|
+
|
|
2134
|
+
await expect(
|
|
2135
|
+
asAlice(() =>
|
|
2136
|
+
store.reconcileSourceInventory({
|
|
2137
|
+
sourceId: "source-1",
|
|
2138
|
+
presentExternalIds: ["allowed"],
|
|
2139
|
+
completedAt: "2026-07-16T01:00:00.000Z",
|
|
2140
|
+
}),
|
|
2141
|
+
),
|
|
2142
|
+
).resolves.toEqual({ removed: 1, restored: 0 });
|
|
2143
|
+
let rows = await exec.execute(
|
|
2144
|
+
"SELECT id, status, inventory_state FROM creative_context_items ORDER BY id",
|
|
2145
|
+
);
|
|
2146
|
+
expect(rows.rows).toEqual([
|
|
2147
|
+
expect.objectContaining({
|
|
2148
|
+
id: "allowed",
|
|
2149
|
+
status: "active",
|
|
2150
|
+
inventory_state: "available",
|
|
2151
|
+
}),
|
|
2152
|
+
expect.objectContaining({
|
|
2153
|
+
id: "restricted",
|
|
2154
|
+
status: "unavailable",
|
|
2155
|
+
inventory_state: "removed",
|
|
2156
|
+
}),
|
|
2157
|
+
]);
|
|
2158
|
+
let source = await exec.execute(
|
|
2159
|
+
"SELECT item_count, restricted_item_count FROM creative_context_sources WHERE id = 'source-1'",
|
|
2160
|
+
);
|
|
2161
|
+
expect(source.rows[0]).toMatchObject({
|
|
2162
|
+
item_count: 1,
|
|
2163
|
+
restricted_item_count: 0,
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
await expect(
|
|
2167
|
+
asAlice(() =>
|
|
2168
|
+
store.reconcileSourceInventory({
|
|
2169
|
+
sourceId: "source-1",
|
|
2170
|
+
presentExternalIds: ["allowed", "restricted"],
|
|
2171
|
+
completedAt: "2026-07-16T02:00:00.000Z",
|
|
2172
|
+
}),
|
|
2173
|
+
),
|
|
2174
|
+
).resolves.toEqual({ removed: 0, restored: 1 });
|
|
2175
|
+
rows = await exec.execute(
|
|
2176
|
+
"SELECT status, inventory_state, upstream_access, curation_status FROM creative_context_items WHERE id = 'restricted'",
|
|
2177
|
+
);
|
|
2178
|
+
expect(rows.rows[0]).toMatchObject({
|
|
2179
|
+
status: "active",
|
|
2180
|
+
inventory_state: "available",
|
|
2181
|
+
upstream_access: "restricted",
|
|
2182
|
+
curation_status: "review",
|
|
2183
|
+
});
|
|
2184
|
+
source = await exec.execute(
|
|
2185
|
+
"SELECT item_count, restricted_item_count FROM creative_context_sources WHERE id = 'source-1'",
|
|
2186
|
+
);
|
|
2187
|
+
expect(source.rows[0]).toMatchObject({
|
|
2188
|
+
item_count: 2,
|
|
2189
|
+
restricted_item_count: 1,
|
|
2190
|
+
});
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
it("creates one daily maintenance job across concurrent app workers", async () => {
|
|
2194
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2195
|
+
const create = () =>
|
|
2196
|
+
runWithRequestContext(
|
|
2197
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2198
|
+
() =>
|
|
2199
|
+
store.createDailyMaintenanceJob({
|
|
2200
|
+
sourceId: "source-1",
|
|
2201
|
+
scheduledAt: "2026-07-16T17:00:00.000Z",
|
|
2202
|
+
}),
|
|
2203
|
+
);
|
|
2204
|
+
const results = await Promise.all([create(), create(), create(), create()]);
|
|
2205
|
+
expect(results.filter((result) => result.created)).toHaveLength(1);
|
|
2206
|
+
expect(new Set(results.map((result) => result.job.id))).toHaveLength(1);
|
|
2207
|
+
const rows = await exec.execute({
|
|
2208
|
+
sql: "SELECT id FROM creative_context_jobs WHERE scoped_dedupe_key = ?",
|
|
2209
|
+
args: ["daily-maintenance:source-1:2026-07-16"],
|
|
2210
|
+
});
|
|
2211
|
+
expect(rows.rows).toHaveLength(1);
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
it("deduplicates jobs inside one tenant without colliding across scopes", async () => {
|
|
2215
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2216
|
+
const create = (userEmail: string, orgId: string | undefined) =>
|
|
2217
|
+
runWithRequestContext({ userEmail, orgId }, () =>
|
|
2218
|
+
store.createJob({
|
|
2219
|
+
kind: "metadata-refresh",
|
|
2220
|
+
dedupeKey: "same-logical-operation",
|
|
2221
|
+
}),
|
|
2222
|
+
);
|
|
2223
|
+
|
|
2224
|
+
const aliceOrgOne = await create("alice@example.test", "org-1");
|
|
2225
|
+
const aliceOrgOneAgain = await create("alice@example.test", "org-1");
|
|
2226
|
+
const bobOrgOne = await create("bob@example.test", "org-1");
|
|
2227
|
+
const aliceOrgTwo = await create("alice@example.test", "org-2");
|
|
2228
|
+
const alicePersonal = await create("alice@example.test", undefined);
|
|
2229
|
+
|
|
2230
|
+
expect(aliceOrgOneAgain.id).toBe(aliceOrgOne.id);
|
|
2231
|
+
expect(
|
|
2232
|
+
new Set([aliceOrgOne.id, bobOrgOne.id, aliceOrgTwo.id, alicePersonal.id]),
|
|
2233
|
+
).toHaveLength(4);
|
|
2234
|
+
const rows = await exec.execute(
|
|
2235
|
+
`SELECT dedupe_key, dedupe_scope, scoped_dedupe_key
|
|
2236
|
+
FROM creative_context_jobs
|
|
2237
|
+
WHERE scoped_dedupe_key = 'same-logical-operation'`,
|
|
2238
|
+
);
|
|
2239
|
+
expect(rows.rows).toHaveLength(4);
|
|
2240
|
+
expect(new Set(rows.rows.map((row) => row.dedupe_scope))).toHaveLength(4);
|
|
2241
|
+
expect(rows.rows.every((row) => row.dedupe_key === null)).toBe(true);
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
it("immediately tombstones sources when workspace connection access is removed", async () => {
|
|
2245
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2246
|
+
await exec.execute(
|
|
2247
|
+
"UPDATE creative_context_sources SET connection_id = 'connection-1' WHERE id = 'source-1'",
|
|
2248
|
+
);
|
|
2249
|
+
const result = await runWithRequestContext(
|
|
2250
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2251
|
+
() =>
|
|
2252
|
+
store.handleWorkspaceConnectionLifecycle({
|
|
2253
|
+
type: "grant-revoked",
|
|
2254
|
+
connectionId: "connection-1",
|
|
2255
|
+
appId: "slides",
|
|
2256
|
+
ownerEmail: "alice@example.test",
|
|
2257
|
+
orgId: "org-1",
|
|
2258
|
+
}),
|
|
2259
|
+
);
|
|
2260
|
+
expect(result).toEqual({ sources: 1, jobs: 1 });
|
|
2261
|
+
const source = await exec.execute(
|
|
2262
|
+
"SELECT status, health_status FROM creative_context_sources WHERE id = 'source-1'",
|
|
2263
|
+
);
|
|
2264
|
+
expect(source.rows[0]).toMatchObject({
|
|
2265
|
+
status: "paused",
|
|
2266
|
+
health_status: "needs_setup",
|
|
2267
|
+
});
|
|
2268
|
+
const items = await exec.execute(
|
|
2269
|
+
"SELECT DISTINCT status, inventory_state FROM creative_context_items WHERE source_id = 'source-1'",
|
|
2270
|
+
);
|
|
2271
|
+
expect(items.rows).toEqual([
|
|
2272
|
+
expect.objectContaining({
|
|
2273
|
+
status: "unavailable",
|
|
2274
|
+
inventory_state: "removed",
|
|
2275
|
+
}),
|
|
2276
|
+
]);
|
|
2277
|
+
const jobs = await exec.execute(
|
|
2278
|
+
"SELECT kind, source_id FROM creative_context_jobs",
|
|
2279
|
+
);
|
|
2280
|
+
expect(jobs.rows).toEqual([
|
|
2281
|
+
expect.objectContaining({ kind: "purge", source_id: "source-1" }),
|
|
2282
|
+
]);
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
it("searches an immutable pack at its pinned historical item version", async () => {
|
|
2286
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2287
|
+
await exec.execute({
|
|
2288
|
+
sql: `INSERT INTO creative_context_packs
|
|
2289
|
+
(id, name, context_mode, request, created_at, owner_email, org_id, visibility)
|
|
2290
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2291
|
+
args: [
|
|
2292
|
+
"pack-v1",
|
|
2293
|
+
"Historical launch pack",
|
|
2294
|
+
"manual",
|
|
2295
|
+
"{}",
|
|
2296
|
+
"2026-07-16T00:00:00.000Z",
|
|
2297
|
+
"alice@example.test",
|
|
2298
|
+
"org-1",
|
|
2299
|
+
"private",
|
|
2300
|
+
],
|
|
2301
|
+
});
|
|
2302
|
+
await exec.execute({
|
|
2303
|
+
sql: `INSERT INTO creative_context_pack_members
|
|
2304
|
+
(id, pack_id, item_id, item_version_id, ordinal, score_metadata,
|
|
2305
|
+
created_at, owner_email, org_id)
|
|
2306
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2307
|
+
args: [
|
|
2308
|
+
"member-v1",
|
|
2309
|
+
"pack-v1",
|
|
2310
|
+
"allowed",
|
|
2311
|
+
"allowed-v1",
|
|
2312
|
+
0,
|
|
2313
|
+
"{}",
|
|
2314
|
+
"2026-07-16T00:00:00.000Z",
|
|
2315
|
+
"alice@example.test",
|
|
2316
|
+
"org-1",
|
|
2317
|
+
],
|
|
2318
|
+
});
|
|
2319
|
+
const result = await runWithRequestContext(
|
|
2320
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2321
|
+
() =>
|
|
2322
|
+
store.listAccessibleLexicalCandidates({
|
|
2323
|
+
query: "version 1",
|
|
2324
|
+
packId: "pack-v1",
|
|
2325
|
+
limit: 10,
|
|
2326
|
+
}),
|
|
2327
|
+
);
|
|
2328
|
+
expect(result.results).toEqual([
|
|
2329
|
+
expect.objectContaining({
|
|
2330
|
+
itemId: "allowed",
|
|
2331
|
+
itemVersionId: "allowed-v1",
|
|
2332
|
+
chunkId: "allowed-chunk-v1",
|
|
2333
|
+
}),
|
|
2334
|
+
]);
|
|
2335
|
+
});
|
|
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
|
+
|
|
2697
|
+
it("allows explicit deprecated search without making removed items retrievable", async () => {
|
|
2698
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2699
|
+
await exec.execute(
|
|
2700
|
+
"UPDATE creative_context_items SET status = 'deprecated' WHERE id = 'allowed'",
|
|
2701
|
+
);
|
|
2702
|
+
const search = (statuses: any[]) =>
|
|
2703
|
+
runWithRequestContext(
|
|
2704
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2705
|
+
() =>
|
|
2706
|
+
store.listAccessibleLexicalCandidates({
|
|
2707
|
+
query: "canonical version 2",
|
|
2708
|
+
statuses,
|
|
2709
|
+
limit: 10,
|
|
2710
|
+
}),
|
|
2711
|
+
);
|
|
2712
|
+
await expect(search(["deprecated"])).resolves.toMatchObject({
|
|
2713
|
+
results: [expect.objectContaining({ itemId: "allowed" })],
|
|
2714
|
+
});
|
|
2715
|
+
await expect(search(["deleted"])).rejects.toThrow(
|
|
2716
|
+
"cannot include removed items",
|
|
2717
|
+
);
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
it("stops injecting published DNA immediately when its source is purged", async () => {
|
|
2721
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2722
|
+
await exec.execute({
|
|
2723
|
+
sql: `INSERT INTO creative_context_brand_profiles
|
|
2724
|
+
(id, name, current_dna_version_id, created_at, updated_at,
|
|
2725
|
+
owner_email, org_id, visibility)
|
|
2726
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2727
|
+
args: [
|
|
2728
|
+
"profile-1",
|
|
2729
|
+
"Brand",
|
|
2730
|
+
"dna-1",
|
|
2731
|
+
"2026-07-16T00:00:00.000Z",
|
|
2732
|
+
"2026-07-16T00:00:00.000Z",
|
|
2733
|
+
"alice@example.test",
|
|
2734
|
+
"org-1",
|
|
2735
|
+
"private",
|
|
2736
|
+
],
|
|
2737
|
+
});
|
|
2738
|
+
await exec.execute({
|
|
2739
|
+
sql: `INSERT INTO creative_context_brand_dna_versions
|
|
2740
|
+
(id, profile_id, version_number, payload, content_hash, status,
|
|
2741
|
+
created_at, owner_email, org_id)
|
|
2742
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2743
|
+
args: [
|
|
2744
|
+
"dna-1",
|
|
2745
|
+
"profile-1",
|
|
2746
|
+
1,
|
|
2747
|
+
'{"summary":"Imported brand","colors":["#112233"]}',
|
|
2748
|
+
"dna-hash",
|
|
2749
|
+
"published",
|
|
2750
|
+
"2026-07-16T00:00:00.000Z",
|
|
2751
|
+
"alice@example.test",
|
|
2752
|
+
"org-1",
|
|
2753
|
+
],
|
|
2754
|
+
});
|
|
2755
|
+
await exec.execute({
|
|
2756
|
+
sql: `INSERT INTO creative_context_brand_dna_evidence
|
|
2757
|
+
(id, dna_version_id, item_id, item_version_id, created_at,
|
|
2758
|
+
owner_email, org_id)
|
|
2759
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2760
|
+
args: [
|
|
2761
|
+
"evidence-1",
|
|
2762
|
+
"dna-1",
|
|
2763
|
+
"allowed",
|
|
2764
|
+
"allowed-v1",
|
|
2765
|
+
"2026-07-16T00:00:00.000Z",
|
|
2766
|
+
"alice@example.test",
|
|
2767
|
+
"org-1",
|
|
2768
|
+
],
|
|
2769
|
+
});
|
|
2770
|
+
|
|
2771
|
+
const result = await runWithRequestContext(
|
|
2772
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2773
|
+
() => store.purgeContextSourceArtifacts("source-1"),
|
|
2774
|
+
);
|
|
2775
|
+
|
|
2776
|
+
expect(result).toMatchObject({
|
|
2777
|
+
invalidatedBrandProfiles: 1,
|
|
2778
|
+
dnaRecomputeJobs: 0,
|
|
2779
|
+
});
|
|
2780
|
+
const profile = await exec.execute(
|
|
2781
|
+
"SELECT current_dna_version_id FROM creative_context_brand_profiles WHERE id = 'profile-1'",
|
|
2782
|
+
);
|
|
2783
|
+
expect(profile.rows[0]?.current_dna_version_id).toBeNull();
|
|
2784
|
+
const jobs = await exec.execute(
|
|
2785
|
+
"SELECT id FROM creative_context_jobs WHERE kind = 'brand-dna'",
|
|
2786
|
+
);
|
|
2787
|
+
expect(jobs.rows).toEqual([]);
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
it("publishes only the exact human-reviewed DNA proposal", async () => {
|
|
2791
|
+
const { runWithRequestContext, store } = await setup();
|
|
2792
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2793
|
+
runWithRequestContext(
|
|
2794
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2795
|
+
fn,
|
|
2796
|
+
);
|
|
2797
|
+
const proposed = await asAlice(() =>
|
|
2798
|
+
store.saveBrandDnaCandidate({
|
|
2799
|
+
name: "Reviewed brand",
|
|
2800
|
+
status: "proposed",
|
|
2801
|
+
dna: { summary: "Warm and direct" },
|
|
2802
|
+
evidenceItemIds: ["allowed"],
|
|
2803
|
+
}),
|
|
2804
|
+
);
|
|
2805
|
+
await expect(
|
|
2806
|
+
asAlice(() =>
|
|
2807
|
+
store.publishBrandDna({
|
|
2808
|
+
profileId: proposed.profile.id,
|
|
2809
|
+
proposalVersionId: proposed.dna.id,
|
|
2810
|
+
confirmation: {
|
|
2811
|
+
proposalVersionId: proposed.dna.id,
|
|
2812
|
+
contentHash: "0".repeat(64),
|
|
2813
|
+
},
|
|
2814
|
+
}),
|
|
2815
|
+
),
|
|
2816
|
+
).rejects.toThrow("changed after review");
|
|
2817
|
+
await expect(
|
|
2818
|
+
asAlice(() =>
|
|
2819
|
+
store.publishBrandDna({
|
|
2820
|
+
profileId: proposed.profile.id,
|
|
2821
|
+
proposalVersionId: proposed.dna.id,
|
|
2822
|
+
confirmation: {
|
|
2823
|
+
proposalVersionId: proposed.dna.id,
|
|
2824
|
+
contentHash: proposed.dna.contentHash,
|
|
2825
|
+
},
|
|
2826
|
+
}),
|
|
2827
|
+
),
|
|
2828
|
+
).resolves.toMatchObject({
|
|
2829
|
+
profile: { currentDnaVersionId: proposed.dna.id },
|
|
2830
|
+
dna: { id: proposed.dna.id, status: "published" },
|
|
2831
|
+
});
|
|
2832
|
+
});
|
|
2833
|
+
|
|
2834
|
+
it("executes every advertised enrichment worker idempotently", async () => {
|
|
2835
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2836
|
+
const jobs = await import("../jobs/index.js");
|
|
2837
|
+
const dispatch = vi.fn(async () => {});
|
|
2838
|
+
const unregister =
|
|
2839
|
+
jobs.registerCreativeContextImportContinuationDispatcher(dispatch);
|
|
2840
|
+
await exec.execute(
|
|
2841
|
+
"UPDATE creative_context_items SET title = 'Primary brand logo', curation_rank = 'canonical' WHERE id = 'allowed'",
|
|
2842
|
+
);
|
|
2843
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2844
|
+
runWithRequestContext(
|
|
2845
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2846
|
+
fn,
|
|
2847
|
+
);
|
|
2848
|
+
const run = async (kind: any) => {
|
|
2849
|
+
const job = await asAlice(() =>
|
|
2850
|
+
store.createJob({
|
|
2851
|
+
sourceId: "source-1",
|
|
2852
|
+
kind,
|
|
2853
|
+
request: {},
|
|
2854
|
+
}),
|
|
2855
|
+
);
|
|
2856
|
+
return jobs.processCreativeContextBackgroundJob({
|
|
2857
|
+
jobId: job.id,
|
|
2858
|
+
ownerEmail: "alice@example.test",
|
|
2859
|
+
orgId: "org-1",
|
|
2860
|
+
appId: "slides",
|
|
2861
|
+
workerId: `worker:${kind}`,
|
|
2862
|
+
});
|
|
2863
|
+
};
|
|
2864
|
+
|
|
2865
|
+
await run("canonical-logo");
|
|
2866
|
+
await run("canonical-logo");
|
|
2867
|
+
await run("layout-suggestion");
|
|
2868
|
+
const refresh = await run("metadata-refresh");
|
|
2869
|
+
|
|
2870
|
+
const suggestions = await exec.execute(
|
|
2871
|
+
"SELECT kind, item_id, item_version_id FROM creative_context_suggestions ORDER BY kind",
|
|
2872
|
+
);
|
|
2873
|
+
expect(suggestions.rows).toEqual([
|
|
2874
|
+
expect.objectContaining({
|
|
2875
|
+
kind: "canonical-logo",
|
|
2876
|
+
item_id: "allowed",
|
|
2877
|
+
item_version_id: "allowed-v2",
|
|
2878
|
+
}),
|
|
2879
|
+
expect.objectContaining({
|
|
2880
|
+
kind: "layout-template",
|
|
2881
|
+
item_id: "allowed",
|
|
2882
|
+
item_version_id: "allowed-v2",
|
|
2883
|
+
}),
|
|
2884
|
+
]);
|
|
2885
|
+
expect(refresh).toMatchObject({
|
|
2886
|
+
status: "completed",
|
|
2887
|
+
result: { importJobId: expect.any(String) },
|
|
2888
|
+
});
|
|
2889
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
2890
|
+
const importJobs = await exec.execute(
|
|
2891
|
+
"SELECT id FROM creative_context_jobs WHERE kind = 'import' AND scoped_dedupe_key IS NOT NULL",
|
|
2892
|
+
);
|
|
2893
|
+
expect(importJobs.rows).toHaveLength(1);
|
|
2894
|
+
unregister();
|
|
2895
|
+
});
|
|
2896
|
+
|
|
2897
|
+
it("enforces review, source access, revocation, exact current versions, and org vectors", async () => {
|
|
2898
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
2899
|
+
const asBob = <T>(fn: () => Promise<T>) =>
|
|
2900
|
+
runWithRequestContext(
|
|
2901
|
+
{ userEmail: "bob@example.test", orgId: "org-1" },
|
|
2902
|
+
fn,
|
|
2903
|
+
);
|
|
2904
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
2905
|
+
runWithRequestContext(
|
|
2906
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
2907
|
+
fn,
|
|
2908
|
+
);
|
|
2909
|
+
|
|
2910
|
+
await expect(
|
|
2911
|
+
asBob(() => store.listAccessibleSearchDocuments({ limit: 20 })),
|
|
2912
|
+
).resolves.toEqual([]);
|
|
2913
|
+
await exec.execute({
|
|
2914
|
+
sql: `INSERT INTO creative_context_source_shares
|
|
2915
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
2916
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2917
|
+
args: [
|
|
2918
|
+
"share-1",
|
|
2919
|
+
"source-1",
|
|
2920
|
+
"user",
|
|
2921
|
+
"bob@example.test",
|
|
2922
|
+
"viewer",
|
|
2923
|
+
"alice@example.test",
|
|
2924
|
+
"2026-07-16T00:00:00.000Z",
|
|
2925
|
+
],
|
|
2926
|
+
});
|
|
2927
|
+
const initial = await asBob(() =>
|
|
2928
|
+
store.listAccessibleSearchDocuments({ limit: 20 }),
|
|
2929
|
+
);
|
|
2930
|
+
expect(initial.map((document) => document.itemId)).toEqual(["allowed"]);
|
|
2931
|
+
expect(initial[0]?.itemVersionId).toBe("allowed-v2");
|
|
2932
|
+
expect(initial[0]?.body).toContain("version 2");
|
|
2933
|
+
|
|
2934
|
+
const privatePack = await asAlice(() =>
|
|
2935
|
+
store.createContextPack({
|
|
2936
|
+
name: "Private generation intent",
|
|
2937
|
+
members: [{ itemId: "allowed", itemVersionId: "allowed-v2" }],
|
|
2938
|
+
}),
|
|
2939
|
+
);
|
|
2940
|
+
await expect(
|
|
2941
|
+
asBob(() => store.getContextPack(privatePack.id)),
|
|
2942
|
+
).resolves.toBeNull();
|
|
2943
|
+
await exec.execute({
|
|
2944
|
+
sql: `INSERT INTO creative_context_pack_shares
|
|
2945
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
2946
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2947
|
+
args: [
|
|
2948
|
+
"pack-share-1",
|
|
2949
|
+
privatePack.id,
|
|
2950
|
+
"user",
|
|
2951
|
+
"bob@example.test",
|
|
2952
|
+
"viewer",
|
|
2953
|
+
"alice@example.test",
|
|
2954
|
+
"2026-07-16T00:00:00.000Z",
|
|
2955
|
+
],
|
|
2956
|
+
});
|
|
2957
|
+
await expect(
|
|
2958
|
+
asBob(() => store.getContextPack(privatePack.id)),
|
|
2959
|
+
).resolves.toMatchObject({
|
|
2960
|
+
id: privatePack.id,
|
|
2961
|
+
members: [
|
|
2962
|
+
expect.objectContaining({
|
|
2963
|
+
itemId: "allowed",
|
|
2964
|
+
itemVersionId: "allowed-v2",
|
|
2965
|
+
}),
|
|
2966
|
+
],
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
await expect(
|
|
2970
|
+
asAlice(() =>
|
|
2971
|
+
store.createContextPack({
|
|
2972
|
+
name: "Bypass attempt",
|
|
2973
|
+
members: [{ itemId: "restricted" }],
|
|
2974
|
+
}),
|
|
2975
|
+
),
|
|
2976
|
+
).rejects.toThrow(/must be accessible/);
|
|
2977
|
+
|
|
2978
|
+
await exec.execute(
|
|
2979
|
+
"UPDATE creative_context_items SET curation_status = 'included' WHERE id = 'restricted'",
|
|
2980
|
+
);
|
|
2981
|
+
const approved = await asBob(() =>
|
|
2982
|
+
store.listAccessibleSearchDocuments({ limit: 20 }),
|
|
2983
|
+
);
|
|
2984
|
+
expect(new Set(approved.map((document) => document.itemId))).toEqual(
|
|
2985
|
+
new Set(["allowed", "restricted"]),
|
|
2986
|
+
);
|
|
2987
|
+
|
|
2988
|
+
await exec.execute(
|
|
2989
|
+
"UPDATE creative_context_sources SET upstream_access = 'restricted' WHERE id = 'source-1'",
|
|
2990
|
+
);
|
|
2991
|
+
await expect(
|
|
2992
|
+
asBob(() => store.listAccessibleSearchDocuments({ limit: 20 })),
|
|
2993
|
+
).resolves.toEqual([]);
|
|
2994
|
+
await expect(
|
|
2995
|
+
asBob(() => store.getCreativeContextItem("allowed")),
|
|
2996
|
+
).resolves.toBeNull();
|
|
2997
|
+
|
|
2998
|
+
await exec.execute(
|
|
2999
|
+
"UPDATE creative_context_sources SET upstream_access = 'available' WHERE id = 'source-1'",
|
|
3000
|
+
);
|
|
3001
|
+
await exec.execute("DELETE FROM creative_context_source_shares");
|
|
3002
|
+
await expect(
|
|
3003
|
+
asBob(() => store.listAccessibleSearchDocuments({ limit: 20 })),
|
|
3004
|
+
).resolves.toEqual([]);
|
|
3005
|
+
|
|
3006
|
+
await exec.execute(
|
|
3007
|
+
"UPDATE creative_context_sources SET visibility = 'org' WHERE id = 'source-1'",
|
|
3008
|
+
);
|
|
3009
|
+
const set = await asAlice(() =>
|
|
3010
|
+
store.createEmbeddingSet({
|
|
3011
|
+
name: "Approved winner",
|
|
3012
|
+
provider: "test",
|
|
3013
|
+
family: "test:family",
|
|
3014
|
+
model: "test-model",
|
|
3015
|
+
version: "1",
|
|
3016
|
+
dimensions: 3,
|
|
3017
|
+
}),
|
|
3018
|
+
);
|
|
3019
|
+
await asAlice(() =>
|
|
3020
|
+
store.recordEmbeddingMetadata({
|
|
3021
|
+
embeddingSetId: set.id,
|
|
3022
|
+
itemId: "allowed",
|
|
3023
|
+
itemVersionId: "allowed-v2",
|
|
3024
|
+
chunkId: "allowed-chunk-v2",
|
|
3025
|
+
targetType: "chunk",
|
|
3026
|
+
targetId: "allowed-chunk-v2",
|
|
3027
|
+
vectorKey: "vector-allowed",
|
|
3028
|
+
dimensions: 3,
|
|
3029
|
+
}),
|
|
3030
|
+
);
|
|
3031
|
+
await expect(
|
|
3032
|
+
asBob(() => store.getActiveEmbeddingSet()),
|
|
3033
|
+
).resolves.toMatchObject({ id: set.id });
|
|
3034
|
+
await expect(
|
|
3035
|
+
asBob(() =>
|
|
3036
|
+
store.listEmbeddingMetadata({
|
|
3037
|
+
embeddingSetId: set.id,
|
|
3038
|
+
itemVersionIds: ["allowed-v2"],
|
|
3039
|
+
}),
|
|
3040
|
+
),
|
|
3041
|
+
).resolves.toMatchObject([{ vectorKey: "vector-allowed" }]);
|
|
3042
|
+
await expect(
|
|
3043
|
+
asBob(() =>
|
|
3044
|
+
store.createEmbeddingSet({
|
|
3045
|
+
name: "Unauthorized rotation",
|
|
3046
|
+
provider: "test",
|
|
3047
|
+
family: "other",
|
|
3048
|
+
model: "other",
|
|
3049
|
+
version: "1",
|
|
3050
|
+
dimensions: 2,
|
|
3051
|
+
}),
|
|
3052
|
+
),
|
|
3053
|
+
).rejects.toThrow(/owners or admins/);
|
|
3054
|
+
await exec.execute(
|
|
3055
|
+
"UPDATE creative_context_sources SET upstream_access = 'restricted' WHERE id = 'source-1'",
|
|
3056
|
+
);
|
|
3057
|
+
await expect(
|
|
3058
|
+
asBob(() =>
|
|
3059
|
+
store.listEmbeddingMetadata({
|
|
3060
|
+
embeddingSetId: set.id,
|
|
3061
|
+
itemVersionIds: ["allowed-v2"],
|
|
3062
|
+
}),
|
|
3063
|
+
),
|
|
3064
|
+
).resolves.toEqual([]);
|
|
3065
|
+
});
|
|
3066
|
+
|
|
3067
|
+
it("revokes newly restricted unchanged items but preserves explicit approvals across resync", async () => {
|
|
3068
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
3069
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
3070
|
+
runWithRequestContext(
|
|
3071
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
3072
|
+
fn,
|
|
3073
|
+
);
|
|
3074
|
+
await exec.execute(
|
|
3075
|
+
"UPDATE creative_context_items SET curation_status = 'included' WHERE id = 'restricted'",
|
|
3076
|
+
);
|
|
3077
|
+
const base = {
|
|
3078
|
+
externalId: "restricted",
|
|
3079
|
+
kind: "slide",
|
|
3080
|
+
title: "restricted title",
|
|
3081
|
+
content: "restricted canonical version 2",
|
|
3082
|
+
contentHash: "restricted-hash-v2",
|
|
3083
|
+
upstreamAccess: "restricted" as const,
|
|
3084
|
+
curationStatus: "review" as const,
|
|
3085
|
+
};
|
|
3086
|
+
await asAlice(() =>
|
|
3087
|
+
store.ingestItems({ sourceId: "source-1", items: [base] }),
|
|
3088
|
+
);
|
|
3089
|
+
let row = await exec.execute(
|
|
3090
|
+
"SELECT curation_status FROM creative_context_items WHERE id = 'restricted'",
|
|
3091
|
+
);
|
|
3092
|
+
expect(row.rows[0]?.curation_status).toBe("included");
|
|
3093
|
+
|
|
3094
|
+
await asAlice(() =>
|
|
3095
|
+
store.ingestItems({
|
|
3096
|
+
sourceId: "source-1",
|
|
3097
|
+
items: [
|
|
3098
|
+
{
|
|
3099
|
+
...base,
|
|
3100
|
+
content: "restricted canonical version 3",
|
|
3101
|
+
contentHash: "restricted-hash-v3",
|
|
3102
|
+
},
|
|
3103
|
+
],
|
|
3104
|
+
}),
|
|
3105
|
+
);
|
|
3106
|
+
row = await exec.execute(
|
|
3107
|
+
"SELECT curation_status FROM creative_context_items WHERE id = 'restricted'",
|
|
3108
|
+
);
|
|
3109
|
+
expect(row.rows[0]?.curation_status).toBe("included");
|
|
3110
|
+
|
|
3111
|
+
await exec.execute(
|
|
3112
|
+
"UPDATE creative_context_items SET upstream_access = 'available', curation_status = 'included' WHERE id = 'allowed'",
|
|
3113
|
+
);
|
|
3114
|
+
await asAlice(() =>
|
|
3115
|
+
store.ingestItems({
|
|
3116
|
+
sourceId: "source-1",
|
|
3117
|
+
items: [
|
|
3118
|
+
{
|
|
3119
|
+
externalId: "allowed",
|
|
3120
|
+
kind: "slide",
|
|
3121
|
+
title: "allowed title",
|
|
3122
|
+
content: "allowed canonical version 2",
|
|
3123
|
+
contentHash: "allowed-hash-v2",
|
|
3124
|
+
upstreamAccess: "restricted",
|
|
3125
|
+
curationStatus: "review",
|
|
3126
|
+
},
|
|
3127
|
+
],
|
|
3128
|
+
}),
|
|
3129
|
+
);
|
|
3130
|
+
row = await exec.execute(
|
|
3131
|
+
"SELECT upstream_access, curation_status FROM creative_context_items WHERE id = 'allowed'",
|
|
3132
|
+
);
|
|
3133
|
+
expect(row.rows[0]).toMatchObject({
|
|
3134
|
+
upstream_access: "restricted",
|
|
3135
|
+
curation_status: "review",
|
|
3136
|
+
});
|
|
3137
|
+
});
|
|
3138
|
+
|
|
3139
|
+
it("purges dependent references and invalidates affected published DNA", async () => {
|
|
3140
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
3141
|
+
const statements: Array<{ sql: string; args: unknown[] }> = [
|
|
3142
|
+
{
|
|
3143
|
+
sql: `INSERT INTO creative_context_packs
|
|
3144
|
+
(id, name, context_mode, request, created_at, owner_email, org_id, visibility)
|
|
3145
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3146
|
+
args: [
|
|
3147
|
+
"pack-purge",
|
|
3148
|
+
"Purge pack",
|
|
3149
|
+
"manual",
|
|
3150
|
+
"{}",
|
|
3151
|
+
"2026-07-16T00:00:00.000Z",
|
|
3152
|
+
"alice@example.test",
|
|
3153
|
+
"org-1",
|
|
3154
|
+
"private",
|
|
3155
|
+
],
|
|
3156
|
+
},
|
|
3157
|
+
{
|
|
3158
|
+
sql: `INSERT INTO creative_context_pack_members
|
|
3159
|
+
(id, pack_id, item_id, item_version_id, ordinal, score_metadata,
|
|
3160
|
+
created_at, owner_email, org_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3161
|
+
args: [
|
|
3162
|
+
"member-purge",
|
|
3163
|
+
"pack-purge",
|
|
3164
|
+
"allowed",
|
|
3165
|
+
"allowed-v2",
|
|
3166
|
+
0,
|
|
3167
|
+
"{}",
|
|
3168
|
+
"2026-07-16T00:00:00.000Z",
|
|
3169
|
+
"alice@example.test",
|
|
3170
|
+
"org-1",
|
|
3171
|
+
],
|
|
3172
|
+
},
|
|
3173
|
+
{
|
|
3174
|
+
sql: `INSERT INTO creative_context_brand_profiles
|
|
3175
|
+
(id, name, current_dna_version_id, created_at, updated_at,
|
|
3176
|
+
owner_email, org_id, visibility) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3177
|
+
args: [
|
|
3178
|
+
"profile-purge",
|
|
3179
|
+
"Purge profile",
|
|
3180
|
+
"dna-purge",
|
|
3181
|
+
"2026-07-16T00:00:00.000Z",
|
|
3182
|
+
"2026-07-16T00:00:00.000Z",
|
|
3183
|
+
"alice@example.test",
|
|
3184
|
+
"org-1",
|
|
3185
|
+
"private",
|
|
3186
|
+
],
|
|
3187
|
+
},
|
|
3188
|
+
{
|
|
3189
|
+
sql: `INSERT INTO creative_context_brand_dna_versions
|
|
3190
|
+
(id, profile_id, version_number, payload, content_hash, status,
|
|
3191
|
+
created_at, owner_email, org_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3192
|
+
args: [
|
|
3193
|
+
"dna-purge",
|
|
3194
|
+
"profile-purge",
|
|
3195
|
+
1,
|
|
3196
|
+
"{}",
|
|
3197
|
+
"dna-hash",
|
|
3198
|
+
"published",
|
|
3199
|
+
"2026-07-16T00:00:00.000Z",
|
|
3200
|
+
"alice@example.test",
|
|
3201
|
+
"org-1",
|
|
3202
|
+
],
|
|
3203
|
+
},
|
|
3204
|
+
{
|
|
3205
|
+
sql: `INSERT INTO creative_context_brand_dna_evidence
|
|
3206
|
+
(id, dna_version_id, item_id, item_version_id, created_at,
|
|
3207
|
+
owner_email, org_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
3208
|
+
args: [
|
|
3209
|
+
"evidence-purge",
|
|
3210
|
+
"dna-purge",
|
|
3211
|
+
"allowed",
|
|
3212
|
+
"allowed-v2",
|
|
3213
|
+
"2026-07-16T00:00:00.000Z",
|
|
3214
|
+
"alice@example.test",
|
|
3215
|
+
"org-1",
|
|
3216
|
+
],
|
|
3217
|
+
},
|
|
3218
|
+
{
|
|
3219
|
+
sql: `INSERT INTO creative_context_edges
|
|
3220
|
+
(id, from_item_id, from_item_version_id, to_item_id,
|
|
3221
|
+
to_item_version_id, relation, metadata, created_at, owner_email, org_id)
|
|
3222
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3223
|
+
args: [
|
|
3224
|
+
"edge-to-purge",
|
|
3225
|
+
"outside-item",
|
|
3226
|
+
"outside-version",
|
|
3227
|
+
"allowed",
|
|
3228
|
+
"allowed-v2",
|
|
3229
|
+
"derived-from",
|
|
3230
|
+
"{}",
|
|
3231
|
+
"2026-07-16T00:00:00.000Z",
|
|
3232
|
+
"alice@example.test",
|
|
3233
|
+
"org-1",
|
|
3234
|
+
],
|
|
3235
|
+
},
|
|
3236
|
+
{
|
|
3237
|
+
sql: `INSERT INTO creative_context_suggestions
|
|
3238
|
+
(id, kind, status, item_id, item_version_id, payload, created_at,
|
|
3239
|
+
updated_at, owner_email, org_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3240
|
+
args: [
|
|
3241
|
+
"suggestion-purge",
|
|
3242
|
+
"layout-template",
|
|
3243
|
+
"promoted",
|
|
3244
|
+
"allowed",
|
|
3245
|
+
"allowed-v2",
|
|
3246
|
+
'{"projectionItemId":"outside-item"}',
|
|
3247
|
+
"2026-07-16T00:00:00.000Z",
|
|
3248
|
+
"2026-07-16T00:00:00.000Z",
|
|
3249
|
+
"alice@example.test",
|
|
3250
|
+
"org-1",
|
|
3251
|
+
],
|
|
3252
|
+
},
|
|
3253
|
+
{
|
|
3254
|
+
sql: `INSERT INTO creative_context_generation_records
|
|
3255
|
+
(id, app_id, artifact_type, artifact_id, context_mode,
|
|
3256
|
+
context_pack_id, element_provenance, created_at, owner_email, org_id)
|
|
3257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3258
|
+
args: [
|
|
3259
|
+
"generation-purge",
|
|
3260
|
+
"slides",
|
|
3261
|
+
"deck",
|
|
3262
|
+
"deck-1",
|
|
3263
|
+
"pinned",
|
|
3264
|
+
"pack-purge",
|
|
3265
|
+
'[{"elementId":"title","itemId":"allowed","itemVersionId":"allowed-v2","influence":"adapted","label":"Brand title"}]',
|
|
3266
|
+
"2026-07-16T00:00:00.000Z",
|
|
3267
|
+
"alice@example.test",
|
|
3268
|
+
"org-1",
|
|
3269
|
+
],
|
|
3270
|
+
},
|
|
3271
|
+
];
|
|
3272
|
+
for (const statement of statements) await exec.execute(statement);
|
|
3273
|
+
|
|
3274
|
+
const result = await runWithRequestContext(
|
|
3275
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
3276
|
+
() => store.purgeContextSourceArtifacts("source-1"),
|
|
3277
|
+
);
|
|
3278
|
+
expect(result).toMatchObject({
|
|
3279
|
+
purgedItems: 2,
|
|
3280
|
+
demotedLayouts: 1,
|
|
3281
|
+
invalidatedBrandProfiles: 1,
|
|
3282
|
+
dnaRecomputeJobs: 0,
|
|
3283
|
+
});
|
|
3284
|
+
for (const table of [
|
|
3285
|
+
"creative_context_pack_members",
|
|
3286
|
+
"creative_context_brand_dna_evidence",
|
|
3287
|
+
"creative_context_edges",
|
|
3288
|
+
"creative_context_suggestions",
|
|
3289
|
+
"creative_context_items",
|
|
3290
|
+
]) {
|
|
3291
|
+
const rows = await exec.execute(`SELECT id FROM ${table}`);
|
|
3292
|
+
expect(rows.rows, table).toHaveLength(0);
|
|
3293
|
+
}
|
|
3294
|
+
const generation = await exec.execute(
|
|
3295
|
+
"SELECT element_provenance FROM creative_context_generation_records WHERE id = 'generation-purge'",
|
|
3296
|
+
);
|
|
3297
|
+
expect(JSON.parse(String(generation.rows[0]?.element_provenance))).toEqual([
|
|
3298
|
+
expect.objectContaining({
|
|
3299
|
+
elementId: "title",
|
|
3300
|
+
referenceUnavailable: true,
|
|
3301
|
+
}),
|
|
3302
|
+
]);
|
|
3303
|
+
expect(String(generation.rows[0]?.element_provenance)).not.toContain(
|
|
3304
|
+
"allowed-v2",
|
|
3305
|
+
);
|
|
3306
|
+
const pack = await exec.execute(
|
|
3307
|
+
"SELECT archived_at FROM creative_context_packs WHERE id = 'pack-purge'",
|
|
3308
|
+
);
|
|
3309
|
+
expect(pack.rows[0]?.archived_at).toEqual(expect.any(String));
|
|
3310
|
+
const jobs = await exec.execute(
|
|
3311
|
+
"SELECT kind FROM creative_context_jobs WHERE kind = 'brand-dna'",
|
|
3312
|
+
);
|
|
3313
|
+
expect(jobs.rows).toEqual([]);
|
|
3314
|
+
const profile = await exec.execute(
|
|
3315
|
+
"SELECT current_dna_version_id FROM creative_context_brand_profiles WHERE id = 'profile-purge'",
|
|
3316
|
+
);
|
|
3317
|
+
expect(profile.rows[0]?.current_dna_version_id).toBeNull();
|
|
3318
|
+
});
|
|
3319
|
+
|
|
3320
|
+
it("scans beyond early rows and preserves exact lexical mode semantics", async () => {
|
|
3321
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
3322
|
+
await exec.execute({
|
|
3323
|
+
sql: `INSERT INTO creative_context_items
|
|
3324
|
+
(id, source_id, external_id, kind, title, current_version_id,
|
|
3325
|
+
current_content_hash, status, upstream_access, curation_status,
|
|
3326
|
+
curation_rank, inventory_state, index_state, tags, colors, provenance,
|
|
3327
|
+
metadata, created_at, updated_at, owner_email, org_id)
|
|
3328
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3329
|
+
args: [
|
|
3330
|
+
"tagged-late",
|
|
3331
|
+
"source-1",
|
|
3332
|
+
"tagged-late",
|
|
3333
|
+
"slide",
|
|
3334
|
+
"Tagged late",
|
|
3335
|
+
"tagged-late-v1",
|
|
3336
|
+
"tagged-late-hash",
|
|
3337
|
+
"active",
|
|
3338
|
+
"available",
|
|
3339
|
+
"included",
|
|
3340
|
+
"normal",
|
|
3341
|
+
"available",
|
|
3342
|
+
"indexed",
|
|
3343
|
+
'["late-tag"]',
|
|
3344
|
+
'["#123456"]',
|
|
3345
|
+
"{}",
|
|
3346
|
+
"{}",
|
|
3347
|
+
"2026-07-16T00:00:00.000Z",
|
|
3348
|
+
"2026-07-16T00:00:00.000Z",
|
|
3349
|
+
"alice@example.test",
|
|
3350
|
+
"org-1",
|
|
3351
|
+
],
|
|
3352
|
+
});
|
|
3353
|
+
await exec.execute({
|
|
3354
|
+
sql: `INSERT INTO creative_context_item_versions
|
|
3355
|
+
(id, item_id, version_number, content_hash, title, content,
|
|
3356
|
+
parse_status, metadata, created_at, owner_email, org_id)
|
|
3357
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3358
|
+
args: [
|
|
3359
|
+
"tagged-late-v1",
|
|
3360
|
+
"tagged-late",
|
|
3361
|
+
1,
|
|
3362
|
+
"tagged-late-hash",
|
|
3363
|
+
"Tagged late",
|
|
3364
|
+
"late filtered content",
|
|
3365
|
+
"parsed",
|
|
3366
|
+
"{}",
|
|
3367
|
+
"2026-07-16T00:00:00.000Z",
|
|
3368
|
+
"alice@example.test",
|
|
3369
|
+
"org-1",
|
|
3370
|
+
],
|
|
3371
|
+
});
|
|
3372
|
+
const values: string[] = [];
|
|
3373
|
+
const args: unknown[] = [];
|
|
3374
|
+
const addChunk = (id: string, ordinal: number, text: string) => {
|
|
3375
|
+
values.push("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
3376
|
+
args.push(
|
|
3377
|
+
id,
|
|
3378
|
+
"allowed",
|
|
3379
|
+
"allowed-v2",
|
|
3380
|
+
ordinal,
|
|
3381
|
+
"text",
|
|
3382
|
+
text,
|
|
3383
|
+
"{}",
|
|
3384
|
+
"2026-07-16T00:00:00.000Z",
|
|
3385
|
+
"alice@example.test",
|
|
3386
|
+
"org-1",
|
|
3387
|
+
);
|
|
3388
|
+
};
|
|
3389
|
+
for (let index = 1; index <= 1_005; index += 1) {
|
|
3390
|
+
addChunk(
|
|
3391
|
+
`a-irrelevant-${String(index).padStart(4, "0")}`,
|
|
3392
|
+
index,
|
|
3393
|
+
"irrelevant corpus row",
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
addChunk("x-separated", 1_006, "needle middle alpha");
|
|
3397
|
+
addChunk("y-decoy", 1_007, "alpha only");
|
|
3398
|
+
addChunk("z-later-match", 1_008, "needle alpha literal 100%_match");
|
|
3399
|
+
values.push("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
3400
|
+
args.push(
|
|
3401
|
+
"zz-tagged-late",
|
|
3402
|
+
"tagged-late",
|
|
3403
|
+
"tagged-late-v1",
|
|
3404
|
+
0,
|
|
3405
|
+
"text",
|
|
3406
|
+
"late filtered content",
|
|
3407
|
+
"{}",
|
|
3408
|
+
"2026-07-16T00:00:00.000Z",
|
|
3409
|
+
"alice@example.test",
|
|
3410
|
+
"org-1",
|
|
3411
|
+
);
|
|
3412
|
+
await exec.execute({
|
|
3413
|
+
sql: `INSERT INTO creative_context_chunks
|
|
3414
|
+
(id, item_id, item_version_id, ordinal, kind, text, metadata,
|
|
3415
|
+
created_at, owner_email, org_id) VALUES ${values.join(",")}`,
|
|
3416
|
+
args,
|
|
3417
|
+
});
|
|
3418
|
+
const search = (query: string, matchMode: any) =>
|
|
3419
|
+
runWithRequestContext(
|
|
3420
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
3421
|
+
() =>
|
|
3422
|
+
store.listAccessibleLexicalCandidates({
|
|
3423
|
+
query,
|
|
3424
|
+
matchMode,
|
|
3425
|
+
limit: 20,
|
|
3426
|
+
}),
|
|
3427
|
+
);
|
|
3428
|
+
const allTerms = await search("needle alpha", "allTerms");
|
|
3429
|
+
expect(allTerms.results.map((result) => result.chunkId)).toEqual(
|
|
3430
|
+
expect.arrayContaining(["x-separated", "z-later-match"]),
|
|
3431
|
+
);
|
|
3432
|
+
expect(allTerms.results.map((result) => result.chunkId)).not.toContain(
|
|
3433
|
+
"y-decoy",
|
|
3434
|
+
);
|
|
3435
|
+
const anyTerm = await search("needle alpha", "anyTerm");
|
|
3436
|
+
expect(anyTerm.results.map((result) => result.chunkId)).toContain(
|
|
3437
|
+
"y-decoy",
|
|
3438
|
+
);
|
|
3439
|
+
const phrase = await search("needle alpha", "phrase");
|
|
3440
|
+
expect(phrase.results.map((result) => result.chunkId)).toContain(
|
|
3441
|
+
"z-later-match",
|
|
3442
|
+
);
|
|
3443
|
+
expect(phrase.results.map((result) => result.chunkId)).not.toContain(
|
|
3444
|
+
"x-separated",
|
|
3445
|
+
);
|
|
3446
|
+
const regex = await search("needle\\s+alpha", "regex");
|
|
3447
|
+
expect(regex.results.map((result) => result.chunkId)).toContain(
|
|
3448
|
+
"z-later-match",
|
|
3449
|
+
);
|
|
3450
|
+
const escaped = await search("100%_match", "phrase");
|
|
3451
|
+
expect(escaped.results.map((result) => result.chunkId)).toEqual([
|
|
3452
|
+
"z-later-match",
|
|
3453
|
+
]);
|
|
3454
|
+
const filtered = await runWithRequestContext(
|
|
3455
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
3456
|
+
() =>
|
|
3457
|
+
store.listAccessibleSearchDocuments({
|
|
3458
|
+
tags: ["late-tag"],
|
|
3459
|
+
colors: ["#123456"],
|
|
3460
|
+
limit: 1,
|
|
3461
|
+
}),
|
|
3462
|
+
);
|
|
3463
|
+
expect(filtered.map((document) => document.itemId)).toEqual([
|
|
3464
|
+
"tagged-late",
|
|
3465
|
+
]);
|
|
3466
|
+
|
|
3467
|
+
await exec.execute({
|
|
3468
|
+
sql: `INSERT INTO creative_context_embedding_sets
|
|
3469
|
+
(id, name, provider, family, model, version, dimensions, metric,
|
|
3470
|
+
status, metadata, created_at, owner_email, org_id)
|
|
3471
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3472
|
+
args: [
|
|
3473
|
+
"set-late",
|
|
3474
|
+
"Late vector set",
|
|
3475
|
+
"test",
|
|
3476
|
+
"test-family",
|
|
3477
|
+
"test-model",
|
|
3478
|
+
"v1",
|
|
3479
|
+
2,
|
|
3480
|
+
"cosine",
|
|
3481
|
+
"active",
|
|
3482
|
+
"{}",
|
|
3483
|
+
"2026-07-16T00:00:00.000Z",
|
|
3484
|
+
"alice@example.test",
|
|
3485
|
+
"org-1",
|
|
3486
|
+
],
|
|
3487
|
+
});
|
|
3488
|
+
await exec.execute({
|
|
3489
|
+
sql: `INSERT INTO creative_context_embeddings
|
|
3490
|
+
(id, embedding_set_id, family, model, version, item_id,
|
|
3491
|
+
item_version_id, target_type, target_id, vector_key, dimensions,
|
|
3492
|
+
created_at, owner_email, org_id)
|
|
3493
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3494
|
+
args: [
|
|
3495
|
+
"embedding-late",
|
|
3496
|
+
"set-late",
|
|
3497
|
+
"test-family",
|
|
3498
|
+
"test-model",
|
|
3499
|
+
"v1",
|
|
3500
|
+
"tagged-late",
|
|
3501
|
+
"tagged-late-v1",
|
|
3502
|
+
"item",
|
|
3503
|
+
"tagged-late-v1",
|
|
3504
|
+
"vector-late",
|
|
3505
|
+
2,
|
|
3506
|
+
"2026-07-16T00:00:00.000Z",
|
|
3507
|
+
"alice@example.test",
|
|
3508
|
+
"org-1",
|
|
3509
|
+
],
|
|
3510
|
+
});
|
|
3511
|
+
const vectorCandidates = await runWithRequestContext(
|
|
3512
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
3513
|
+
() =>
|
|
3514
|
+
store.listEmbeddingMetadata({
|
|
3515
|
+
embeddingSetId: "set-late",
|
|
3516
|
+
tags: ["late-tag"],
|
|
3517
|
+
}),
|
|
3518
|
+
);
|
|
3519
|
+
expect(vectorCandidates.map((candidate) => candidate.vectorKey)).toEqual([
|
|
3520
|
+
"vector-late",
|
|
3521
|
+
]);
|
|
3522
|
+
});
|
|
3523
|
+
});
|