spine-framework 0.3.144 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +76 -0
- package/.framework/.spine-manifest.json +19 -0
- package/.framework/cli/commands/agents.ts +61 -33
- package/.framework/cli/commands/auth.ts +60 -28
- package/.framework/cli/commands/create-app.ts +212 -80
- package/.framework/cli/commands/dev.ts +184 -68
- package/.framework/cli/commands/doctor.ts +324 -152
- package/.framework/cli/commands/generate.ts +119 -68
- package/.framework/cli/commands/init.ts +141 -242
- package/.framework/cli/commands/install-app.ts +1336 -314
- package/.framework/cli/commands/items.ts +31 -29
- package/.framework/cli/commands/migrate.ts +299 -156
- package/.framework/cli/commands/migrations.ts +62 -30
- package/.framework/cli/commands/pipelines.ts +31 -29
- package/.framework/cli/commands/status.ts +105 -45
- package/.framework/cli/commands/system.ts +47 -42
- package/.framework/cli/commands/test.ts +161 -102
- package/.framework/cli/commands/upgrade.ts +260 -0
- package/.framework/cli/commands/validate-app.ts +517 -161
- package/.framework/cli/context.ts +117 -85
- package/.framework/cli/env-loader.ts +74 -43
- package/.framework/cli/index.ts +70 -46
- package/.framework/cli/lib/project-files.ts +404 -0
- package/.framework/docs/guides/testing-guide.md +1 -1
- package/.framework/docs/runtime-types.md +72 -72
- package/.framework/functions/_shared/access.ts +84 -0
- package/.framework/functions/_shared/agent-runner.ts +5051 -1026
- package/.framework/functions/_shared/app-manifest.ts +214 -94
- package/.framework/functions/_shared/ast.ts +214 -0
- package/.framework/functions/_shared/audit.ts +91 -55
- package/.framework/functions/_shared/cil.ts +368 -127
- package/.framework/functions/_shared/db.ts +337 -68
- package/.framework/functions/_shared/funnel.ts +228 -0
- package/.framework/functions/_shared/geo.ts +105 -0
- package/.framework/functions/_shared/hash-api-key.ts +94 -17
- package/.framework/functions/_shared/index.ts +22 -224
- package/.framework/functions/_shared/kb.ts +832 -58
- package/.framework/functions/_shared/llm.ts +118 -18
- package/.framework/functions/_shared/logging.ts +317 -19
- package/.framework/functions/_shared/middleware.ts +701 -131
- package/.framework/functions/_shared/notify.ts +70 -0
- package/.framework/functions/_shared/outcomes.ts +34 -19
- package/.framework/functions/_shared/permissions.ts +501 -315
- package/.framework/functions/_shared/pipeline-runner.ts +704 -155
- package/.framework/functions/_shared/principal.ts +436 -263
- package/.framework/functions/_shared/rate-limit.ts +233 -0
- package/.framework/functions/_shared/resolve-credentials.ts +86 -19
- package/.framework/functions/_shared/resolve-ids.ts +203 -139
- package/.framework/functions/_shared/retry-deliveries.ts +51 -16
- package/.framework/functions/_shared/schema-utils.ts +602 -521
- package/.framework/functions/_shared/security.ts +345 -0
- package/.framework/functions/_shared/testing.ts +197 -172
- package/.framework/functions/_shared/tracing.ts +351 -21
- package/.framework/functions/_shared/trigger-engine.ts +425 -136
- package/.framework/functions/_shared/webhook-registration.ts +71 -70
- package/.framework/functions/_shared/webhook-registry.ts +126 -83
- package/.framework/functions/account-nodes.ts +30 -30
- package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
- package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
- package/.framework/functions/adapters/openai-outbound.ts +15 -16
- package/.framework/functions/admin-data.ts +596 -173
- package/.framework/functions/admin-funnel.ts +64 -60
- package/.framework/functions/admin-install-funnel.ts +84 -66
- package/.framework/functions/admin-signals.ts +69 -51
- package/.framework/functions/ai-agents.ts +431 -106
- package/.framework/functions/ai-interactions.ts +186 -112
- package/.framework/functions/api-keys.ts +107 -107
- package/.framework/functions/apps.ts +154 -136
- package/.framework/functions/auth.ts +83 -65
- package/.framework/functions/cil-agent-ingest.ts +189 -232
- package/.framework/functions/cil-agent.ts +433 -177
- package/.framework/functions/cil-callmap.ts +642 -430
- package/.framework/functions/cil-config.ts +139 -135
- package/.framework/functions/cil-context.ts +214 -127
- package/.framework/functions/cil-doc.ts +587 -458
- package/.framework/functions/cil-embeddings.ts +453 -364
- package/.framework/functions/cil-graph.ts +427 -142
- package/.framework/functions/cil-ingest.ts +1418 -1154
- package/.framework/functions/cil-pipeline.ts +276 -21
- package/.framework/functions/cil-project.ts +659 -132
- package/.framework/functions/cil-validate.ts +198 -164
- package/.framework/functions/debug-auth.ts +57 -55
- package/.framework/functions/deliveries.ts +80 -21
- package/.framework/functions/embeddings.ts +1023 -318
- package/.framework/functions/eval-harness.ts +146 -108
- package/.framework/functions/integration-outbound.ts +205 -75
- package/.framework/functions/integration-routes.ts +128 -23
- package/.framework/functions/integrations.ts +125 -100
- package/.framework/functions/invites.ts +1095 -460
- package/.framework/functions/item-progress.ts +158 -102
- package/.framework/functions/kb.ts +34 -10
- package/.framework/functions/logs.ts +133 -133
- package/.framework/functions/observability.ts +152 -122
- package/.framework/functions/pipeline-executions.ts +147 -132
- package/.framework/functions/pipelines.ts +134 -119
- package/.framework/functions/prompt-configs.ts +96 -80
- package/.framework/functions/roles.ts +112 -82
- package/.framework/functions/security.ts +450 -0
- package/.framework/functions/system-cron.ts +117 -80
- package/.framework/functions/system.ts +158 -98
- package/.framework/functions/tests.ts +29 -29
- package/.framework/functions/timers.ts +94 -95
- package/.framework/functions/trigger-scheduler.ts +10 -0
- package/.framework/functions/triggers.ts +179 -149
- package/.framework/functions/types.ts +137 -125
- package/.framework/mcp/index.ts +133 -0
- package/.framework/mcp/load-env.ts +60 -0
- package/.framework/mcp/shared/app-manifest.ts +275 -0
- package/.framework/mcp/shared/db.ts +477 -0
- package/.framework/mcp/shared/kb.ts +118 -0
- package/.framework/mcp/tools/architect.ts +431 -0
- package/.framework/mcp/tools/cil/context.ts +345 -0
- package/.framework/mcp/tools/cil/graph.ts +242 -0
- package/.framework/mcp/tools/cil/ingest.ts +1217 -0
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
- package/.framework/mcp/tools/cil/pipeline.ts +767 -0
- package/.framework/mcp/tools/cil/project.ts +791 -0
- package/.framework/mcp/tools/cil/retrieval.ts +336 -0
- package/.framework/mcp/tools/cil/search-core.ts +454 -0
- package/.framework/mcp/tools/cil/search.ts +97 -0
- package/.framework/mcp/tools/cil/suggest.ts +186 -0
- package/.framework/mcp/tools/cil/validate.ts +1369 -0
- package/.framework/mcp/tools/kb/delete.ts +139 -0
- package/.framework/mcp/tools/kb/index.ts +52 -0
- package/.framework/mcp/tools/kb/ingest.ts +84 -0
- package/.framework/mcp/tools/kb/list-types.ts +102 -0
- package/.framework/mcp/tools/kb/publish.ts +100 -0
- package/.framework/mcp/tools/kb/reindex.ts +86 -0
- package/.framework/mcp/tools/kb/revoke.ts +85 -0
- package/.framework/mcp/tools/kb/search.ts +101 -0
- package/.framework/mcp/tools/kb/upsert.ts +125 -0
- package/.framework/mcp/tools/observability.ts +510 -0
- package/.framework/migrations/001_core_schema.sql +15 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
- package/.framework/migrations/003_core_system_data.sql +16 -1
- package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
- package/.framework/migrations/005_core_observability.sql +17 -1
- package/.framework/migrations/006_security_hardening_rls.sql +15 -0
- package/.framework/migrations/007_kb_article_list.sql +17 -1
- package/.framework/migrations/008_items_target.sql +15 -0
- package/.framework/migrations/009_roles_slug_unique.sql +15 -0
- package/.framework/migrations/010_auth_user_trigger.sql +15 -0
- package/.framework/migrations/011_spine_system_account.sql +16 -0
- package/.framework/migrations/012_service_role_grants.sql +16 -0
- package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
- package/.framework/migrations/014_apps_required_roles.sql +16 -0
- package/.framework/migrations/015_types_kind_slug_unique.sql +20 -0
- package/.framework/migrations/016_cil_types.sql +281 -0
- package/.framework/migrations/017_cil_stable_ids.sql +37 -0
- package/.framework/migrations/018_runtime_tables.sql +129 -0
- package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
- package/.framework/migrations/020_schema_reconciliation.sql +67 -0
- package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
- package/.framework/migrations/032_cil_status_migration.sql +73 -0
- package/.framework/migrations/033_cil_versioning.sql +73 -0
- package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
- package/.framework/migrations/035_rate_limits.sql +55 -0
- package/.framework/migrations/036_demo_record_flags.sql +72 -0
- package/.framework/migrations/037_kb_types_canonical.sql +244 -0
- package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
- package/.framework/migrations/042_registration_hierarchy.sql +309 -0
- package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
- package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
- package/.framework/migrations/045_security_critical_rls.sql +139 -0
- package/.framework/migrations/046_security_missing_rls.sql +79 -0
- package/.framework/migrations/047_people_updated_by.sql +27 -0
- package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
- package/.framework/migrations/049_security_blocklist.sql +148 -0
- package/.framework/migrations/050_notifications.sql +49 -0
- package/.framework/migrations/051_message_agent_fields.sql +74 -0
- package/.framework/migrations/052_registration_security.sql +421 -0
- package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
- package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
- package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
- package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
- package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
- package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
- package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
- package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
- package/.framework/migrations/061_fk_dedup.sql +32 -0
- package/.framework/public/spine-logo.svg +10 -3
- package/.framework/scripts/app-install-cli.ts +190 -93
- package/.framework/scripts/assemble-frontend.sh +127 -44
- package/.framework/scripts/assemble-functions.sh +128 -30
- package/.framework/scripts/boundary-check.sh +6 -2
- package/.framework/scripts/build-manifest.sh +24 -68
- package/.framework/scripts/check-core-integrity.sh +15 -76
- package/.framework/scripts/checksum-manifest.mts +343 -0
- package/.framework/scripts/cil-migrate-markers.ts +403 -0
- package/.framework/scripts/fix-cil-markers.mjs +176 -0
- package/.framework/scripts/verify-integrity.sh +21 -57
- package/.framework/scripts/verify-manifest.mts +305 -0
- package/.framework/seeds/types/funnel_signal_use.json +74 -21
- package/.framework/seeds/types/spine_instance.json +73 -21
- package/.framework/src/App.tsx +118 -72
- package/.framework/src/apps/admin/index.tsx +855 -58
- package/.framework/src/components/AppWrapper.tsx +43 -45
- package/.framework/src/components/CustomAppLoader.tsx +160 -116
- package/.framework/src/components/admin/AdminListPage.tsx +46 -44
- package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
- package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
- package/.framework/src/components/admin/PageInstructions.tsx +98 -2
- package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
- package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
- package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
- package/.framework/src/components/app-sidebar.tsx +42 -45
- package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
- package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
- package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
- package/.framework/src/components/kb/KBArticleList.tsx +251 -10
- package/.framework/src/components/layout/AppShell.tsx +51 -45
- package/.framework/src/components/layout/Header.tsx +42 -43
- package/.framework/src/components/layout/Layout.tsx +46 -46
- package/.framework/src/components/layout/Sidebar.tsx +196 -96
- package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
- package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
- package/.framework/src/components/runtime/DataFilters.tsx +49 -74
- package/.framework/src/components/runtime/DataHeader.tsx +53 -32
- package/.framework/src/components/runtime/DataListPage.tsx +51 -155
- package/.framework/src/components/runtime/DataStats.tsx +66 -44
- package/.framework/src/components/runtime/DataTable.tsx +49 -149
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
- package/.framework/src/components/runtime/index.ts +119 -16
- package/.framework/src/components/search-form.tsx +46 -29
- package/.framework/src/components/shared/AgentView.tsx +65 -48
- package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
- package/.framework/src/components/shared/InviteManager.tsx +207 -97
- package/.framework/src/components/shared/MemberList.tsx +97 -0
- package/.framework/src/components/shared/SchemaFields.tsx +107 -86
- package/.framework/src/components/ui/DataTable.tsx +80 -68
- package/.framework/src/components/ui/Form.tsx +157 -146
- package/.framework/src/components/ui/ItemCard.tsx +74 -67
- package/.framework/src/components/ui/ItemListView.tsx +73 -68
- package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
- package/.framework/src/components/ui/Modal.tsx +78 -68
- package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
- package/.framework/src/components/ui/accordion.tsx +85 -86
- package/.framework/src/components/ui/alert-dialog.tsx +197 -184
- package/.framework/src/components/ui/alert.tsx +105 -94
- package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
- package/.framework/src/components/ui/avatar.tsx +122 -107
- package/.framework/src/components/ui/badge.tsx +60 -55
- package/.framework/src/components/ui/breadcrumb.tsx +127 -128
- package/.framework/src/components/ui/button-group.tsx +86 -72
- package/.framework/src/components/ui/button.tsx +71 -50
- package/.framework/src/components/ui/calendar.tsx +72 -57
- package/.framework/src/components/ui/card.tsx +136 -129
- package/.framework/src/components/ui/carousel.tsx +226 -107
- package/.framework/src/components/ui/chart.tsx +252 -147
- package/.framework/src/components/ui/checkbox.tsx +43 -44
- package/.framework/src/components/ui/collapsible.tsx +86 -71
- package/.framework/src/components/ui/combobox.tsx +282 -249
- package/.framework/src/components/ui/command.tsx +158 -146
- package/.framework/src/components/ui/context-menu.tsx +239 -240
- package/.framework/src/components/ui/dialog.tsx +174 -157
- package/.framework/src/components/ui/direction.tsx +72 -56
- package/.framework/src/components/ui/drawer.tsx +168 -169
- package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
- package/.framework/src/components/ui/empty.tsx +131 -120
- package/.framework/src/components/ui/field.tsx +205 -173
- package/.framework/src/components/ui/hover-card.tsx +71 -72
- package/.framework/src/components/ui/input-group.tsx +148 -133
- package/.framework/src/components/ui/input-otp.tsx +99 -86
- package/.framework/src/components/ui/input.tsx +43 -41
- package/.framework/src/components/ui/item.tsx +207 -185
- package/.framework/src/components/ui/kbd.tsx +57 -56
- package/.framework/src/components/ui/label.tsx +45 -41
- package/.framework/src/components/ui/menubar.tsx +268 -236
- package/.framework/src/components/ui/native-select.tsx +85 -86
- package/.framework/src/components/ui/navigation-menu.tsx +166 -156
- package/.framework/src/components/ui/pagination.tsx +131 -141
- package/.framework/src/components/ui/popover.tsx +125 -128
- package/.framework/src/components/ui/progress.tsx +47 -44
- package/.framework/src/components/ui/radio-group.tsx +60 -55
- package/.framework/src/components/ui/resizable.tsx +85 -72
- package/.framework/src/components/ui/scroll-area.tsx +57 -58
- package/.framework/src/components/ui/select.tsx +182 -169
- package/.framework/src/components/ui/separator.tsx +42 -43
- package/.framework/src/components/ui/sheet.tsx +170 -159
- package/.framework/src/components/ui/sidebar.tsx +395 -398
- package/.framework/src/components/ui/skeleton.tsx +42 -43
- package/.framework/src/components/ui/slider.tsx +61 -44
- package/.framework/src/components/ui/sonner.tsx +44 -44
- package/.framework/src/components/ui/spinner.tsx +47 -42
- package/.framework/src/components/ui/switch.tsx +56 -42
- package/.framework/src/components/ui/table-primitives.tsx +272 -132
- package/.framework/src/components/ui/table.tsx +140 -142
- package/.framework/src/components/ui/tabs.tsx +110 -93
- package/.framework/src/components/ui/textarea.tsx +43 -44
- package/.framework/src/components/ui/toggle-group.tsx +87 -71
- package/.framework/src/components/ui/toggle.tsx +56 -56
- package/.framework/src/components/ui/tooltip.tsx +99 -85
- package/.framework/src/contexts/AppContext.tsx +166 -114
- package/.framework/src/contexts/AuthContext.tsx +232 -137
- package/.framework/src/hooks/use-mobile.ts +43 -44
- package/.framework/src/hooks/useApi.ts +185 -172
- package/.framework/src/hooks/useAppPath.ts +31 -29
- package/.framework/src/hooks/useApps.ts +77 -42
- package/.framework/src/hooks/useEntityList.ts +66 -29
- package/.framework/src/hooks/useEntityRecord.ts +76 -32
- package/.framework/src/hooks/useForm.ts +62 -30
- package/.framework/src/hooks/useListSchema.ts +60 -43
- package/.framework/src/hooks/useSchemaRecord.ts +115 -95
- package/.framework/src/lib/api.ts +258 -134
- package/.framework/src/lib/registration.ts +181 -0
- package/.framework/src/lib/supabase.ts +118 -81
- package/.framework/src/lib/utils.ts +266 -266
- package/.framework/src/main.tsx +43 -44
- package/.framework/src/pages/DashboardPage.tsx +44 -43
- package/.framework/src/pages/NoAppPage.tsx +50 -0
- package/.framework/src/pages/NotFoundPage.tsx +32 -29
- package/.framework/src/pages/SetupPage.tsx +34 -33
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
- package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
- package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
- package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
- package/.framework/src/pages/admin/AppsPage.tsx +45 -42
- package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
- package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
- package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
- package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
- package/.framework/src/pages/admin/LogsPage.tsx +87 -137
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
- package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
- package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
- package/.framework/src/pages/admin/RolesPage.tsx +47 -164
- package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
- package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
- package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
- package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
- package/.framework/src/pages/admin/TimersPage.tsx +44 -42
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
- package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
- package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
- package/.framework/src/pages/admin/TypesPage.tsx +45 -42
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
- package/.framework/src/pages/auth/LoginPage.tsx +38 -32
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
- package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
- package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
- package/.framework/src/types/auth.ts +101 -93
- package/.framework/src/types/types.ts +283 -262
- package/.framework/templates/project.json +87 -0
- package/.framework/templates/spine.config.neutral.json +7 -0
- package/.framework/tests/api/api-surface.test.ts +381 -0
- package/.framework/tests/fixtures/seed.ts +427 -0
- package/.framework/tests/fixtures/teardown.ts +228 -0
- package/.framework/tests/reporter.ts +351 -0
- package/.framework/tests/setup.ts +135 -0
- package/.framework/tests/ui/ui-sweep.ts +278 -0
- package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
- package/.framework/tests/unit/agent-runner.test.ts +2389 -0
- package/.framework/tests/unit/ai-interactions.test.ts +81 -0
- package/.framework/tests/unit/app-manifest.test.ts +359 -0
- package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
- package/.framework/tests/unit/cil-determinism.test.ts +85 -0
- package/.framework/tests/unit/core-isolation.test.ts +153 -0
- package/.framework/tests/unit/funnel-signals.test.ts +135 -0
- package/.framework/tests/unit/install-app.test.ts +1759 -0
- package/.framework/tests/unit/invites-security.test.ts +572 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
- package/.framework/tests/unit/kb-search.test.ts +119 -0
- package/.framework/tests/unit/migrate.test.ts +509 -0
- package/.framework/tests/unit/notify.test.ts +136 -0
- package/.framework/tests/unit/permissions.test.ts +269 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
- package/.framework/tests/unit/principal-cache.test.ts +138 -0
- package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
- package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
- package/.framework/tests/unit/schema-utils.test.ts +225 -0
- package/.framework/tests/unit/trigger-engine.test.ts +513 -0
- package/.framework/tests/unit/validate-app.test.ts +527 -0
- package/README.md +34 -3
- package/bin/spine-framework.cjs +1 -0
- package/config/tailwind.config.cjs +7 -0
- package/config/tsconfig.functions.json +42 -0
- package/config/tsconfig.json +37 -9
- package/config/vite.config.ts +31 -2
- package/netlify.toml +31 -3
- package/package.json +46 -29
- package/scripts/assemble-frontend.sh +127 -44
- package/scripts/assemble-functions.sh +127 -29
- package/scripts/boundary-check.sh +6 -2
- package/scripts/build-manifest.sh +24 -68
- package/scripts/checksum-manifest.mts +345 -0
- package/scripts/dev-clean.sh +86 -0
- package/scripts/dev-netlify.sh +49 -0
- package/scripts/netlify-dev-wrapper.sh +2 -0
- package/scripts/verify-install.ts +358 -0
- package/scripts/verify-integrity.sh +21 -57
- package/scripts/verify-manifest.mts +305 -0
- package/templates/project/gitignore +45 -0
- package/{config → templates/project}/package-lock.json +22335 -9366
- package/vitest.config.ts +46 -0
- package/.framework/public/spine.config.json +0 -91
- package/config/deno.lock +0 -108
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/vite.config.d.ts +0 -2
- package/custom/.devin/AGENT.md +0 -357
- package/custom/.devin/skills/install-app/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
- package/custom/.devin/workflows/app-install.yaml +0 -83
- package/custom/README.md +0 -79
- package/docs/README.md +0 -15
- package/scripts/app-install-cli.ts +0 -286
- package/scripts/check-core-integrity.sh +0 -82
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/kb-ingest-docs.ts +0 -203
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/test-login.js +0 -33
- package/spine.config.json +0 -91
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// CIL[BEGIN stable_id="~/.framework/tests/unit/principal-cache.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
|
|
2
|
+
// CIL[DOC-MACRO purpose="Unit tests for the human principal cache placement guard"]
|
|
3
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Verify principals missing both account_id and role_id (mid-registration) are never cached, while placed principals and account-scoped contacts cache normally"]
|
|
4
|
+
// CIL[DOC-CONTEXT module="tests/unit/principal-cache" layer="test-unit" related_files=".framework/functions/_shared/principal.ts,.framework/migrations/052_registration_security.sql"]
|
|
5
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="vitest assertions" errors="none" side_effects="Mocks @supabase/supabase-js createClient and db module"]
|
|
6
|
+
// CIL[DOC-MICRO logic_summary="Drives resolvePrincipal with Bearer tokens while toggling the mocked people row between unplaced and placed states; asserts re-resolution instead of stale cache replay" notable_decisions="Regression coverage for the ~60s 'Account context required' stall documented in app install docs"]
|
|
7
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
8
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
9
|
+
// CIL[DOC-AUDIENCE audience="test-engineers"]
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
11
|
+
|
|
12
|
+
const { mockGetUser, mockPeopleSingle, mockPersonByAuthUid } = vi.hoisted(() => ({
|
|
13
|
+
mockGetUser: vi.fn(),
|
|
14
|
+
mockPeopleSingle: vi.fn(),
|
|
15
|
+
mockPersonByAuthUid: vi.fn(),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock('@supabase/supabase-js', () => ({
|
|
19
|
+
createClient: vi.fn(() => ({
|
|
20
|
+
auth: { getUser: mockGetUser },
|
|
21
|
+
})),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('../../functions/_shared/db.ts', () => ({
|
|
25
|
+
adminDb: {
|
|
26
|
+
from: vi.fn(() => ({
|
|
27
|
+
select: vi.fn(() => ({
|
|
28
|
+
eq: vi.fn(() => ({ maybeSingle: mockPersonByAuthUid })),
|
|
29
|
+
})),
|
|
30
|
+
})),
|
|
31
|
+
rpc: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
getUserDb: vi.fn(() => ({
|
|
34
|
+
from: vi.fn(() => ({
|
|
35
|
+
select: vi.fn(() => ({
|
|
36
|
+
eq: vi.fn(() => ({ single: mockPeopleSingle })),
|
|
37
|
+
})),
|
|
38
|
+
})),
|
|
39
|
+
})),
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
import { resolvePrincipal } from '../../functions/_shared/principal.ts'
|
|
43
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Unit tests for the human principal cache placement guard'; No verified external callers; imports: .framework/functions/_shared/principal.ts" status="ready"]
|
|
44
|
+
|
|
45
|
+
const event = (token: string) => ({
|
|
46
|
+
headers: { authorization: `Bearer ${token}`, 'user-agent': 'vitest' },
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const authUser = { id: 'auth-1', email: 'new-user@example.com' }
|
|
50
|
+
|
|
51
|
+
function setPerson(person: any) {
|
|
52
|
+
mockPeopleSingle.mockResolvedValue({ data: person, error: null })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.clearAllMocks()
|
|
57
|
+
mockGetUser.mockResolvedValue({ data: { user: authUser }, error: null })
|
|
58
|
+
mockPersonByAuthUid.mockResolvedValue({ data: { id: 'person-1' } })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('resolvePrincipal human cache — mid-registration guard', () => {
|
|
62
|
+
it('does not cache a principal with both account_id and role_id null', async () => {
|
|
63
|
+
// First call lands mid-registration (placement RPC has not run yet).
|
|
64
|
+
setPerson({
|
|
65
|
+
id: 'person-1',
|
|
66
|
+
email: authUser.email,
|
|
67
|
+
account_id: null,
|
|
68
|
+
role_id: null,
|
|
69
|
+
role: null,
|
|
70
|
+
})
|
|
71
|
+
const first = await resolvePrincipal(event('tok-unplaced'))
|
|
72
|
+
expect(first.type).toBe('human')
|
|
73
|
+
expect(first.accountId).toBeNull()
|
|
74
|
+
|
|
75
|
+
// Placement commits — same token must re-resolve and see the account.
|
|
76
|
+
setPerson({
|
|
77
|
+
id: 'person-1',
|
|
78
|
+
email: authUser.email,
|
|
79
|
+
account_id: 'acct-1',
|
|
80
|
+
role_id: 'role-1',
|
|
81
|
+
role: { id: 'role-1', slug: 'member_admin', name: 'Admin', is_system: false, permissions: [] },
|
|
82
|
+
})
|
|
83
|
+
const second = await resolvePrincipal(event('tok-unplaced'))
|
|
84
|
+
expect(second.accountId).toBe('acct-1')
|
|
85
|
+
expect(second.roles).toEqual(['member_admin'])
|
|
86
|
+
expect(mockPeopleSingle).toHaveBeenCalledTimes(2)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('caches a fully placed principal on subsequent requests', async () => {
|
|
90
|
+
setPerson({
|
|
91
|
+
id: 'person-1',
|
|
92
|
+
email: authUser.email,
|
|
93
|
+
account_id: 'acct-1',
|
|
94
|
+
role_id: 'role-1',
|
|
95
|
+
role: { id: 'role-1', slug: 'member', name: 'Member', is_system: false, permissions: [] },
|
|
96
|
+
})
|
|
97
|
+
const first = await resolvePrincipal(event('tok-placed'))
|
|
98
|
+
expect(first.accountId).toBe('acct-1')
|
|
99
|
+
|
|
100
|
+
// Second call must hit the cache — the people lookup is not repeated.
|
|
101
|
+
setPerson({
|
|
102
|
+
id: 'person-1',
|
|
103
|
+
email: authUser.email,
|
|
104
|
+
account_id: 'acct-changed',
|
|
105
|
+
role_id: 'role-1',
|
|
106
|
+
role: { id: 'role-1', slug: 'member', name: 'Member', is_system: false, permissions: [] },
|
|
107
|
+
})
|
|
108
|
+
const second = await resolvePrincipal(event('tok-placed'))
|
|
109
|
+
expect(second.accountId).toBe('acct-1')
|
|
110
|
+
expect(mockPeopleSingle).toHaveBeenCalledTimes(1)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('caches an account-scoped contact that has no role', async () => {
|
|
114
|
+
// Directory contacts legitimately have account_id without role_id —
|
|
115
|
+
// the both-null guard must not skip caching for this steady state.
|
|
116
|
+
setPerson({
|
|
117
|
+
id: 'person-1',
|
|
118
|
+
email: authUser.email,
|
|
119
|
+
account_id: 'acct-1',
|
|
120
|
+
role_id: null,
|
|
121
|
+
role: null,
|
|
122
|
+
})
|
|
123
|
+
const first = await resolvePrincipal(event('tok-contact'))
|
|
124
|
+
expect(first.accountId).toBe('acct-1')
|
|
125
|
+
|
|
126
|
+
setPerson({
|
|
127
|
+
id: 'person-1',
|
|
128
|
+
email: authUser.email,
|
|
129
|
+
account_id: 'acct-other',
|
|
130
|
+
role_id: null,
|
|
131
|
+
role: null,
|
|
132
|
+
})
|
|
133
|
+
const second = await resolvePrincipal(event('tok-contact'))
|
|
134
|
+
expect(second.accountId).toBe('acct-1')
|
|
135
|
+
expect(mockPeopleSingle).toHaveBeenCalledTimes(1)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
// CIL[END stable_id="~/.framework/tests/unit/principal-cache.test.ts:module_header:module_header"]
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// CIL[BEGIN stable_id="~/.framework/tests/unit/retrieval-golden.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
|
|
2
|
+
// CIL[DOC-MACRO purpose="Golden retrieval contract — pins retrieveKnowledge's observable result set (order, ids, similarity, lexical flags, scope filtering) so latency optimizations can never silently alter ranking or eligibility"]
|
|
3
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Snapshot the exact doc lists, mean similarity computation, RRF fusion ordering, security/item-type/source filtering, and denied paths for a fixed mock corpus"]
|
|
4
|
+
// CIL[DOC-CONTEXT module="tests/unit/retrieval-golden" layer="test-unit" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/_shared/kb.ts"]
|
|
5
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="assertions on retrieveKnowledge return shape" errors="none" side_effects="none"]
|
|
6
|
+
// CIL[DOC-MICRO logic_summary="Synchronous mock harness (no deferreds) — apps/envelope, embeddings lexical rows, match_embeddings per vector type, items hydration; asserts exact document_id ordering and similarity/meanSimilarity values so perf refactors must produce byte-equivalent result sets"]
|
|
7
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
|
|
8
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
9
|
+
// CIL[DOC-AUDIENCE audience="framework maintainers"]
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
11
|
+
import type { CoreContext } from '../../functions/_shared/middleware.ts'
|
|
12
|
+
|
|
13
|
+
vi.mock('../../functions/_shared/db.ts', () => ({
|
|
14
|
+
adminDb: { from: vi.fn(), rpc: vi.fn() }
|
|
15
|
+
}))
|
|
16
|
+
vi.mock('../../functions/_shared/audit.ts', () => ({
|
|
17
|
+
emitAudit: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
emitLog: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
emitSpan: vi.fn().mockResolvedValue(undefined)
|
|
20
|
+
}))
|
|
21
|
+
vi.mock('../../functions/_shared/outcomes.ts', () => ({
|
|
22
|
+
emitOutcome: vi.fn().mockResolvedValue(undefined)
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('../../functions/_shared/llm.ts', () => ({
|
|
25
|
+
callOpenAIChat: vi.fn(),
|
|
26
|
+
callOpenAIEmbeddings: vi.fn().mockResolvedValue({ data: [{ embedding: [0.1, 0.2, 0.3] }] })
|
|
27
|
+
}))
|
|
28
|
+
vi.mock('../../functions/admin-data.ts', () => ({ create: vi.fn() }))
|
|
29
|
+
vi.mock('../../functions/integration-outbound.ts', () => ({ execute: vi.fn() }))
|
|
30
|
+
vi.mock('../../functions/_shared/pipeline-runner.ts', () => ({ runPipeline: vi.fn() }))
|
|
31
|
+
vi.mock('../../functions/_shared/funnel.ts', () => ({
|
|
32
|
+
emitSignal: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
accountDataFlag: vi.fn().mockResolvedValue(false)
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
import { adminDb } from '../../functions/_shared/db.ts'
|
|
37
|
+
import { retrieveKnowledge, executeToolAction, computeEvidenceState, hasInRangeCitationMarker, ensureInlineCitations } from '../../functions/_shared/agent-runner.ts'
|
|
38
|
+
import { callOpenAIEmbeddings } from '../../functions/_shared/llm.ts'
|
|
39
|
+
|
|
40
|
+
function makeCtx(overrides: Partial<CoreContext> = {}): CoreContext {
|
|
41
|
+
return {
|
|
42
|
+
principal: { id: 'person-1', type: 'human', accountId: 'acct-1', roles: ['member'] },
|
|
43
|
+
accountId: 'acct-1',
|
|
44
|
+
requestId: 'req-test',
|
|
45
|
+
...overrides
|
|
46
|
+
} as CoreContext
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Fixture corpus ──────────────────────────────────────────────────────────
|
|
50
|
+
// IDs are real-shaped UUIDs so hydration goes through the uuid branch.
|
|
51
|
+
const DOC = {
|
|
52
|
+
strong: '11111111-1111-4111-8111-111111111111',
|
|
53
|
+
mid: '22222222-2222-4222-8222-222222222222',
|
|
54
|
+
weak: '33333333-3333-4333-8333-333333333333',
|
|
55
|
+
restricted: '44444444-4444-4444-8444-444444444444',
|
|
56
|
+
otherAcct: '55555555-5555-4555-8555-555555555555',
|
|
57
|
+
lexicalOnly: '66666666-6666-4666-8666-666666666666',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ITEM_ROWS = [
|
|
61
|
+
{ id: DOC.strong, title: 'Strong Article', data: { content: 'Full body of the strong article' } },
|
|
62
|
+
{ id: DOC.mid, title: 'Mid Article', data: {} },
|
|
63
|
+
{ id: DOC.weak, title: 'Weak Article', data: {} },
|
|
64
|
+
{ id: DOC.restricted, title: 'Restricted Doc', data: {} },
|
|
65
|
+
{ id: DOC.otherAcct, title: 'Other Acct Doc', data: {} },
|
|
66
|
+
{ id: DOC.lexicalOnly, title: 'Lexical Only', data: {} },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
function vectorRow(id: string, similarity: number, opts: {
|
|
70
|
+
vectorType?: string, itemType?: string, security?: string, accountId?: string, content?: string
|
|
71
|
+
} = {}) {
|
|
72
|
+
return {
|
|
73
|
+
document_id: id,
|
|
74
|
+
content: opts.content ?? `content for ${id}`,
|
|
75
|
+
metadata: {
|
|
76
|
+
item_type: opts.itemType ?? 'kb_article',
|
|
77
|
+
vector_type: opts.vectorType ?? 'semantic',
|
|
78
|
+
...(opts.security ? { security_level: opts.security } : {}),
|
|
79
|
+
},
|
|
80
|
+
similarity,
|
|
81
|
+
account_id: opts.accountId ?? 'acct-1',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Mock wiring: apps returns the envelope; embeddings returns lexical rows via
|
|
86
|
+
// textSearch; match_embeddings returns rows keyed on filter_vector_type;
|
|
87
|
+
// items hydrates document ids.
|
|
88
|
+
function setupRetrieval(opts: {
|
|
89
|
+
envelope?: any
|
|
90
|
+
appsError?: any
|
|
91
|
+
vectorRows?: Record<string, any[]> // keyed by vector_type
|
|
92
|
+
lexicalRows?: any[]
|
|
93
|
+
lexicalError?: any
|
|
94
|
+
items?: any[]
|
|
95
|
+
} = {}) {
|
|
96
|
+
// envelope: undefined → default {account:true}; null → config.knowledge absent; {} → empty envelope
|
|
97
|
+
const envelope = opts.envelope === undefined ? { account: true } : opts.envelope
|
|
98
|
+
vi.mocked(adminDb.from).mockImplementation((table: string) => {
|
|
99
|
+
if (table === 'apps') {
|
|
100
|
+
const q: any = {
|
|
101
|
+
select: vi.fn(() => q), eq: vi.fn(() => q),
|
|
102
|
+
maybeSingle: vi.fn(() => Promise.resolve(
|
|
103
|
+
opts.appsError
|
|
104
|
+
? { data: null, error: opts.appsError }
|
|
105
|
+
: { data: { id: 'app-1', slug: 'test-app', required_roles: [], config: envelope === null ? {} : { knowledge: envelope } }, error: null }
|
|
106
|
+
)),
|
|
107
|
+
}
|
|
108
|
+
return q
|
|
109
|
+
}
|
|
110
|
+
if (table === 'accounts') {
|
|
111
|
+
const q: any = {
|
|
112
|
+
select: vi.fn(() => q), eq: vi.fn(() => q),
|
|
113
|
+
maybeSingle: vi.fn(() => Promise.resolve({ data: { id: 'sys-acct' }, error: null })),
|
|
114
|
+
}
|
|
115
|
+
return q
|
|
116
|
+
}
|
|
117
|
+
if (table === 'embeddings') {
|
|
118
|
+
const q: any = {
|
|
119
|
+
select: vi.fn(() => q), in: vi.fn(() => q), is: vi.fn(() => q),
|
|
120
|
+
not: vi.fn(() => q), or: vi.fn(() => q), textSearch: vi.fn(() => q),
|
|
121
|
+
limit: vi.fn(() => q),
|
|
122
|
+
then: (onF: any, onR: any) => Promise.resolve(
|
|
123
|
+
opts.lexicalError ? { data: null, error: opts.lexicalError } : { data: opts.lexicalRows ?? [], error: null }
|
|
124
|
+
).then(onF, onR),
|
|
125
|
+
}
|
|
126
|
+
return q
|
|
127
|
+
}
|
|
128
|
+
if (table === 'items') {
|
|
129
|
+
const q: any = {
|
|
130
|
+
select: vi.fn(() => q),
|
|
131
|
+
in: vi.fn((_field: string, ids: any[]) =>
|
|
132
|
+
Promise.resolve({ data: (opts.items ?? ITEM_ROWS).filter((i) => ids.includes(i.id)), error: null })),
|
|
133
|
+
}
|
|
134
|
+
return q
|
|
135
|
+
}
|
|
136
|
+
return {} as any
|
|
137
|
+
})
|
|
138
|
+
vi.mocked(adminDb.rpc).mockImplementation((name: string, args: any) => {
|
|
139
|
+
if (name !== 'match_embeddings') return Promise.resolve({ data: null, error: null }) as any
|
|
140
|
+
const rows = opts.vectorRows?.[args?.filter_vector_type] ?? []
|
|
141
|
+
return Promise.resolve({ data: rows, error: null }) as any
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function lexicalRow(id: string, opts: { itemType?: string; accountId?: string } = {}) {
|
|
146
|
+
return {
|
|
147
|
+
document_id: id,
|
|
148
|
+
content: `lexical content for ${id}`,
|
|
149
|
+
metadata: { item_type: opts.itemType ?? 'kb_article' },
|
|
150
|
+
account_id: opts.accountId ?? 'acct-1',
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const POLICY = { vector_types: ['semantic'] }
|
|
155
|
+
const APP = 'test-app'
|
|
156
|
+
|
|
157
|
+
describe('retrieveKnowledge — golden result sets', () => {
|
|
158
|
+
beforeEach(() => vi.clearAllMocks())
|
|
159
|
+
|
|
160
|
+
it('orders by RRF-fused rank: vector+lexical hit beats single-source hits', async () => {
|
|
161
|
+
setupRetrieval({
|
|
162
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.6)] },
|
|
163
|
+
lexicalRows: [lexicalRow(DOC.strong), lexicalRow(DOC.lexicalOnly)],
|
|
164
|
+
})
|
|
165
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
166
|
+
const ids = docs.map((d: any) => d.document_id)
|
|
167
|
+
// strong: vector#0 + lexical#0 → 2/(61). mid: vector#1 → 1/62.
|
|
168
|
+
// lexicalOnly: lexical#1 → 1/62. mid vs lexicalOnly tie on score — order
|
|
169
|
+
// by insertion (mid inserted first via vector list).
|
|
170
|
+
expect(ids).toEqual([DOC.strong, DOC.mid, DOC.lexicalOnly])
|
|
171
|
+
expect(docs.find((d: any) => d.document_id === DOC.strong).matched_lexically).toBe(true)
|
|
172
|
+
expect(docs.find((d: any) => d.document_id === DOC.mid).matched_lexically).toBe(false)
|
|
173
|
+
expect(docs.find((d: any) => d.document_id === DOC.lexicalOnly).matched_lexically).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('computes meanSimilarity over top-3 RRF docs that have similarity > 0 (lexical-only excluded)', async () => {
|
|
177
|
+
setupRetrieval({
|
|
178
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.6)] },
|
|
179
|
+
lexicalRows: [lexicalRow(DOC.strong), lexicalRow(DOC.lexicalOnly)],
|
|
180
|
+
})
|
|
181
|
+
const { meanSimilarity } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
182
|
+
expect(meanSimilarity).toBeCloseTo((0.9 + 0.6) / 2, 6)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('drops rows below the 0.15 similarity floor and restricted rows from other accounts', async () => {
|
|
186
|
+
setupRetrieval({
|
|
187
|
+
vectorRows: {
|
|
188
|
+
semantic: [
|
|
189
|
+
vectorRow(DOC.strong, 0.9),
|
|
190
|
+
vectorRow(DOC.weak, 0.10), // below floor
|
|
191
|
+
vectorRow(DOC.restricted, 0.95, { security: 'restricted', accountId: 'acct-other' }),
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
lexicalRows: [],
|
|
195
|
+
})
|
|
196
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
197
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('honors max_security_level — absent security_level defaults to internal, so external-only excludes it', async () => {
|
|
201
|
+
setupRetrieval({
|
|
202
|
+
vectorRows: {
|
|
203
|
+
semantic: [
|
|
204
|
+
vectorRow(DOC.strong, 0.9), // internal — excluded at 'external'
|
|
205
|
+
vectorRow(DOC.lexicalOnly, 0.8, { security: 'external' }), // allowed
|
|
206
|
+
vectorRow(DOC.mid, 0.95, { security: 'restricted', accountId: 'acct-1' }), // own restricted — still excluded
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
lexicalRows: [],
|
|
210
|
+
})
|
|
211
|
+
const { docs } = await retrieveKnowledge(
|
|
212
|
+
'query', [], 'acct-1', { ...POLICY, max_security_level: 'external' }, makeCtx(), APP)
|
|
213
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.lexicalOnly])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('max_security_level internal admits internal + external but not restricted', async () => {
|
|
217
|
+
setupRetrieval({
|
|
218
|
+
vectorRows: {
|
|
219
|
+
semantic: [
|
|
220
|
+
vectorRow(DOC.strong, 0.9), // default internal — allowed
|
|
221
|
+
vectorRow(DOC.mid, 0.95, { security: 'restricted', accountId: 'acct-1' }), // own restricted — excluded
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
lexicalRows: [],
|
|
225
|
+
})
|
|
226
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
227
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('scopes to knowledge_sources pick when provided', async () => {
|
|
231
|
+
setupRetrieval({
|
|
232
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.8)] },
|
|
233
|
+
lexicalRows: [],
|
|
234
|
+
})
|
|
235
|
+
const { docs } = await retrieveKnowledge('query', [DOC.strong], 'acct-1', POLICY, makeCtx(), APP)
|
|
236
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('returns empty + zero mean when nothing matches (no-result query)', async () => {
|
|
240
|
+
setupRetrieval({ vectorRows: { semantic: [] }, lexicalRows: [] })
|
|
241
|
+
const res = await retrieveKnowledge('zzzz no matches', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
242
|
+
expect(res.docs).toEqual([])
|
|
243
|
+
expect(res.meanSimilarity).toBe(0)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('survives lexical failure — vector results still return', async () => {
|
|
247
|
+
setupRetrieval({
|
|
248
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
|
|
249
|
+
lexicalError: { message: 'fts error' },
|
|
250
|
+
})
|
|
251
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
252
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('survives vector/embedding failure — lexical results still return', async () => {
|
|
256
|
+
const { callOpenAIEmbeddings } = await import('../../functions/_shared/llm.ts')
|
|
257
|
+
vi.mocked(callOpenAIEmbeddings).mockRejectedValueOnce(new Error('embed fail'))
|
|
258
|
+
setupRetrieval({ lexicalRows: [lexicalRow(DOC.lexicalOnly)] })
|
|
259
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
260
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.lexicalOnly])
|
|
261
|
+
expect(docs[0].matched_lexically).toBe(true)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('denies when the app has a knowledge key permitting no corpora (empty envelope)', async () => {
|
|
265
|
+
setupRetrieval({ envelope: {} })
|
|
266
|
+
const res = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
267
|
+
expect(res.docs).toEqual([])
|
|
268
|
+
expect(res.denied).toBe(true)
|
|
269
|
+
expect(res.deniedReason).toContain('empty_scope')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('denies when the app declares no knowledge envelope at all', async () => {
|
|
273
|
+
// config.knowledge absent entirely — stronger deny reason
|
|
274
|
+
setupRetrieval({ envelope: null })
|
|
275
|
+
const res = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
276
|
+
expect(res.docs).toEqual([])
|
|
277
|
+
expect(res.denied).toBe(true)
|
|
278
|
+
expect(res.deniedReason).toContain('no_knowledge_envelope')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('multi-vector-type: merges channels, dedupes by document_id keeping max similarity', async () => {
|
|
282
|
+
setupRetrieval({
|
|
283
|
+
vectorRows: {
|
|
284
|
+
semantic: [
|
|
285
|
+
vectorRow(DOC.strong, 0.7, { vectorType: 'semantic', content: 'SEMANTIC body for strong' }),
|
|
286
|
+
vectorRow(DOC.mid, 0.6, { vectorType: 'semantic' }),
|
|
287
|
+
],
|
|
288
|
+
title: [
|
|
289
|
+
vectorRow(DOC.strong, 0.95, { vectorType: 'title', content: 'TITLE string only' }),
|
|
290
|
+
vectorRow(DOC.weak, 0.5, { vectorType: 'title' }),
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
lexicalRows: [],
|
|
294
|
+
// Items without data.content so the title→non-title content swap is
|
|
295
|
+
// observable (full-document expansion would otherwise overwrite it).
|
|
296
|
+
items: [
|
|
297
|
+
{ id: DOC.strong, title: 'Strong Article', data: {} },
|
|
298
|
+
{ id: DOC.mid, title: 'Mid Article', data: {} },
|
|
299
|
+
{ id: DOC.weak, title: 'Weak Article', data: {} },
|
|
300
|
+
],
|
|
301
|
+
})
|
|
302
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', { vector_types: ['semantic', 'title'] }, makeCtx(), APP)
|
|
303
|
+
const strong = docs.find((d: any) => d.document_id === DOC.strong)
|
|
304
|
+
expect(strong.similarity).toBe(0.95) // max across channels
|
|
305
|
+
// Title match swaps in the semantic channel's body content
|
|
306
|
+
expect(strong.content).toContain('SEMANTIC body for strong')
|
|
307
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong, DOC.mid, DOC.weak])
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('envelope item_types restrict results even when the RPC returns them', async () => {
|
|
311
|
+
setupRetrieval({
|
|
312
|
+
envelope: { account: { item_types: ['kb_article'] } },
|
|
313
|
+
vectorRows: {
|
|
314
|
+
semantic: [
|
|
315
|
+
vectorRow(DOC.strong, 0.9, { itemType: 'kb_article' }),
|
|
316
|
+
vectorRow(DOC.mid, 0.95, { itemType: 'secret_doc' }), // outside envelope types
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
lexicalRows: [],
|
|
320
|
+
})
|
|
321
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
322
|
+
expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('hydrates titles and expands full document body for small articles', async () => {
|
|
326
|
+
setupRetrieval({
|
|
327
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
|
|
328
|
+
lexicalRows: [],
|
|
329
|
+
})
|
|
330
|
+
const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
|
|
331
|
+
expect(docs[0].title).toBe('Strong Article')
|
|
332
|
+
expect(docs[0].content).toBe('Full body of the strong article')
|
|
333
|
+
expect(docs[0].full_document).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
// ─── Episode-scoped retrieval cache (P1) ─────────────────────────────────────
|
|
339
|
+
// search_knowledge tool calls keyed by normalized query + policy + scope reuse
|
|
340
|
+
// the hydrated docs already fetched for this episode — identical identity hits,
|
|
341
|
+
// reformulated queries miss by design.
|
|
342
|
+
describe('episode retrieval cache — search_knowledge', () => {
|
|
343
|
+
beforeEach(() => vi.clearAllMocks())
|
|
344
|
+
|
|
345
|
+
const toolAction = { handler: 'search_knowledge' }
|
|
346
|
+
|
|
347
|
+
it('second identical query returns cached docs without a new embed/search', async () => {
|
|
348
|
+
setupRetrieval({
|
|
349
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.7)] },
|
|
350
|
+
lexicalRows: [],
|
|
351
|
+
})
|
|
352
|
+
const cache: Record<string, any> = {}
|
|
353
|
+
const ctx = makeCtx({ appId: APP } as any)
|
|
354
|
+
const runCtx = { retrievalCache: cache }
|
|
355
|
+
|
|
356
|
+
const r1 = await executeToolAction(toolAction, { query: 'what is cil', limit: 5 }, ctx, runCtx)
|
|
357
|
+
const embedsAfterFirst = vi.mocked(callOpenAIEmbeddings).mock.calls.length
|
|
358
|
+
const rpcAfterFirst = vi.mocked(adminDb.rpc).mock.calls.length
|
|
359
|
+
expect(r1.results.map((d: any) => d.document_id)).toEqual([DOC.strong, DOC.mid])
|
|
360
|
+
expect(r1.cached).toBeUndefined()
|
|
361
|
+
|
|
362
|
+
const r2 = await executeToolAction(toolAction, { query: 'what is cil', limit: 5 }, ctx, runCtx)
|
|
363
|
+
expect(r2.results.map((d: any) => d.document_id)).toEqual([DOC.strong, DOC.mid])
|
|
364
|
+
expect(r2.cached).toBe(true)
|
|
365
|
+
expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBe(embedsAfterFirst)
|
|
366
|
+
expect(vi.mocked(adminDb.rpc).mock.calls.length).toBe(rpcAfterFirst)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('normalization: case/whitespace variants of the same query hit', async () => {
|
|
370
|
+
setupRetrieval({
|
|
371
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
|
|
372
|
+
lexicalRows: [],
|
|
373
|
+
})
|
|
374
|
+
const cache: Record<string, any> = {}
|
|
375
|
+
const ctx = makeCtx({ appId: APP } as any)
|
|
376
|
+
const runCtx = { retrievalCache: cache }
|
|
377
|
+
|
|
378
|
+
await executeToolAction(toolAction, { query: 'what is cil' }, ctx, runCtx)
|
|
379
|
+
const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
|
|
380
|
+
|
|
381
|
+
const r = await executeToolAction(toolAction, { query: ' What Is CIL ' }, ctx, runCtx)
|
|
382
|
+
expect(r.cached).toBe(true)
|
|
383
|
+
expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBe(embeds)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('a reformulated query is a deliberate miss — real retrieval runs', async () => {
|
|
387
|
+
setupRetrieval({
|
|
388
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
|
|
389
|
+
lexicalRows: [],
|
|
390
|
+
})
|
|
391
|
+
const cache: Record<string, any> = {}
|
|
392
|
+
const ctx = makeCtx({ appId: APP } as any)
|
|
393
|
+
const runCtx = { retrievalCache: cache }
|
|
394
|
+
|
|
395
|
+
await executeToolAction(toolAction, { query: 'what is cil' }, ctx, runCtx)
|
|
396
|
+
const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
|
|
397
|
+
|
|
398
|
+
const r = await executeToolAction(toolAction, { query: 'code intelligence layer markers' }, ctx, runCtx)
|
|
399
|
+
expect(r.cached).toBeUndefined()
|
|
400
|
+
expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBeGreaterThan(embeds)
|
|
401
|
+
// …and that second query is now cached too
|
|
402
|
+
const r2 = await executeToolAction(toolAction, { query: 'code intelligence layer markers' }, ctx, runCtx)
|
|
403
|
+
expect(r2.cached).toBe(true)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('policy difference (vector_types) produces a different key → miss', async () => {
|
|
407
|
+
setupRetrieval({
|
|
408
|
+
vectorRows: {
|
|
409
|
+
semantic: [vectorRow(DOC.strong, 0.9)],
|
|
410
|
+
title: [vectorRow(DOC.mid, 0.8, { vectorType: 'title' })],
|
|
411
|
+
},
|
|
412
|
+
lexicalRows: [],
|
|
413
|
+
})
|
|
414
|
+
const cache: Record<string, any> = {}
|
|
415
|
+
const ctx = makeCtx({ appId: APP } as any)
|
|
416
|
+
const semanticCtx = {
|
|
417
|
+
retrievalCache: cache,
|
|
418
|
+
config: { promptConfig: { metadata: { retrieval_policy: { vector_types: ['semantic'] } } }, agent: {} },
|
|
419
|
+
}
|
|
420
|
+
const titleCtx = {
|
|
421
|
+
retrievalCache: cache,
|
|
422
|
+
config: { promptConfig: { metadata: { retrieval_policy: { vector_types: ['title'] } } }, agent: {} },
|
|
423
|
+
}
|
|
424
|
+
await executeToolAction(toolAction, { query: 'q' }, ctx, semanticCtx as any)
|
|
425
|
+
const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
|
|
426
|
+
const r = await executeToolAction(toolAction, { query: 'q' }, ctx, titleCtx as any)
|
|
427
|
+
expect(r.cached).toBeUndefined()
|
|
428
|
+
expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBeGreaterThan(embeds)
|
|
429
|
+
expect(r.results.map((d: any) => d.document_id)).toEqual([DOC.mid])
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('denied results are cached and replay the denial, not an empty success', async () => {
|
|
433
|
+
setupRetrieval({ envelope: null }) // no knowledge envelope → denied
|
|
434
|
+
const cache: Record<string, any> = {}
|
|
435
|
+
const ctx = makeCtx({ appId: APP } as any)
|
|
436
|
+
const runCtx = { retrievalCache: cache }
|
|
437
|
+
|
|
438
|
+
const r1 = await executeToolAction(toolAction, { query: 'q' }, ctx, runCtx)
|
|
439
|
+
expect(r1.error).toContain('knowledge_denied')
|
|
440
|
+
const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
|
|
441
|
+
|
|
442
|
+
const r2 = await executeToolAction(toolAction, { query: 'q' }, ctx, runCtx)
|
|
443
|
+
expect(r2.error).toContain('knowledge_denied')
|
|
444
|
+
expect(r2.cached).toBe(true)
|
|
445
|
+
expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBe(embeds)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('limit slices post-cache — different limits share one entry', async () => {
|
|
449
|
+
setupRetrieval({
|
|
450
|
+
vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.7)] },
|
|
451
|
+
lexicalRows: [],
|
|
452
|
+
})
|
|
453
|
+
const cache: Record<string, any> = {}
|
|
454
|
+
const ctx = makeCtx({ appId: APP } as any)
|
|
455
|
+
const runCtx = { retrievalCache: cache }
|
|
456
|
+
|
|
457
|
+
const r1 = await executeToolAction(toolAction, { query: 'q', limit: 1 }, ctx, runCtx)
|
|
458
|
+
expect(r1.results).toHaveLength(1)
|
|
459
|
+
const r2 = await executeToolAction(toolAction, { query: 'q', limit: 5 }, ctx, runCtx)
|
|
460
|
+
expect(r2.cached).toBe(true)
|
|
461
|
+
expect(r2.results).toHaveLength(2)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
// ─── Evidence-state recalibration + citation proof (P6) ──────────────────────
|
|
467
|
+
// evidence_found requires aggregate mean >= 0.35 OR a decisive top hit >= 0.5 —
|
|
468
|
+
// a paraphrased query that puts the right article at rank 1 with a noisy tail
|
|
469
|
+
// must not read 'incomplete'. And any evidence_found answer must carry a
|
|
470
|
+
// resolvable [n] marker; citation repair runs before the gate.
|
|
471
|
+
describe('computeEvidenceState — top-hit path', () => {
|
|
472
|
+
const doc = (similarity: number) => ({ document_id: 'x', similarity })
|
|
473
|
+
|
|
474
|
+
it('strong top hit + weak tail → evidence_found (the paraphrase case)', () => {
|
|
475
|
+
const docs = [doc(0.62), doc(0.22), doc(0.18), doc(0.15)]
|
|
476
|
+
const mean = 0.29 // (0.62+0.22+0.18+0.15)/4 — below 0.35
|
|
477
|
+
expect(computeEvidenceState(docs, mean)).toBe('evidence_found')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('strong mean still passes without a top hit', () => {
|
|
481
|
+
const docs = [doc(0.4), doc(0.38), doc(0.3)]
|
|
482
|
+
expect(computeEvidenceState(docs, 0.36)).toBe('evidence_found')
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('all-weak results stay incomplete', () => {
|
|
486
|
+
const docs = [doc(0.3), doc(0.25), doc(0.2)]
|
|
487
|
+
expect(computeEvidenceState(docs, 0.25)).toBe('incomplete')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('a lone top hit below the decisive bar is still incomplete', () => {
|
|
491
|
+
const docs = [doc(0.45), doc(0.2), doc(0.18)]
|
|
492
|
+
expect(computeEvidenceState(docs, 0.27)).toBe('incomplete')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('no docs → not_established', () => {
|
|
496
|
+
expect(computeEvidenceState([], 0)).toBe('not_established')
|
|
497
|
+
expect(computeEvidenceState(undefined, 0)).toBe('not_established')
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
describe('citation proof — hasInRangeCitationMarker + ensureInlineCitations', () => {
|
|
502
|
+
const sources = [{ document_id: 'a' }, { document_id: 'b' }]
|
|
503
|
+
|
|
504
|
+
it('an in-range marker is proof', () => {
|
|
505
|
+
expect(hasInRangeCitationMarker('The answer is X [1].', 2)).toBe(true)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('out-of-range markers are not proof', () => {
|
|
509
|
+
expect(hasInRangeCitationMarker('See [9] for details.', 2)).toBe(false)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('no markers is not proof (unlike extractCitedSources fallback)', () => {
|
|
513
|
+
expect(hasInRangeCitationMarker('A plain answer with no citations.', 2)).toBe(false)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('ensureInlineCitations repair produces a provable marker', () => {
|
|
517
|
+
const repaired = ensureInlineCitations('A grounded answer with no markers.', sources)
|
|
518
|
+
expect(hasInRangeCitationMarker(repaired, sources.length)).toBe(true)
|
|
519
|
+
})
|
|
520
|
+
})
|