.github/9
ISSUE_TEMPLATE/3bug_report.md config.yml feature_request.md
workflows/3ci.yml clean-clone.yml release.yml
CODEOWNERS dependabot.yml pull_request_template.md
apps/630
api/250
src/243
common/30
__tests__/3authorize.test.ts bigint-serialization.interceptor.test.ts files-limits.test.ts
filters/1all-exceptions.filter.ts
guards/3auth.guard.ts internal-service.guard.ts rate-limit.guard.ts
interceptors/2actor-context.interceptor.ts bigint-serialization.interceptor.ts
middleware/2idempotency.test.ts idempotency.ts
pipes/1zod-validation.pipe.ts
actor-context.ts audit.service.ts cache.test.ts cache.ts cookies.ts core.module.ts deprecation.ts email.service.ts errors.ts event-bus.ts log-scrubber.test.ts log-scrubber.ts logger.ts permission-cache-bridge.ts prisma.service.ts redis.service.ts trace-metadata.ts types.ts
config/1env.ts
lib/11
__tests__/5crypto.test.ts filter-builder.test.ts jwt.test.ts pagination.test.ts positions.test.ts
authorize.ts crypto.ts filter-builder.ts jwt.ts pagination.ts positions.ts
modules/197
ai/28
__tests__/4ai-writing.test.ts database-ai.test.ts generate-executor.test.ts prompts.test.ts
generator/4executor.ts generate.controller.ts generate.dto.ts generate.service.ts
ai-chat.service.ts ai-rate-limit.ts ai-writing.controller.ts ai-writing.dto.ts ai-writing.service.ts ai.controller.ts ai.module.ts ai.service.ts budget-guard.service.ts database-ai.controller.ts database-ai.dto.ts database-ai.service.ts db-prompts.ts dto.ts gateway-provider.ts ledger.service.ts prompts.ts rag.controller.ts rag.dto.ts rag.service.ts
api-keys/3api-keys.controller.ts api-keys.module.ts api-keys.service.ts
audit/2audit-log.controller.ts audit.module.ts
auth/4auth.controller.ts auth.module.ts auth.service.ts dto.ts
automations/5automations.controller.ts automations.module.ts automations.service.ts dto.ts webhook-incoming.controller.ts
blocks/5
__tests__/1batch-version-cas.test.ts
block.service.ts blocks.controller.ts blocks.module.ts dto.ts
comments/4comments.controller.ts comments.module.ts comments.service.ts dto.ts
databases/9
__tests__/3normalize-select-options.test.ts option-id-canonicalization.test.ts record-open-query-shape.test.ts
database.service.ts databases.controller.ts databases.module.ts dto.ts property-cache-schema.ts query.service.ts
docs/2docs.controller.ts docs.module.ts
files/10
__tests__/1quota.test.ts
dto.ts files.controller.ts files.module.ts files.service.ts quota.ts record.ts s3.ts uploads.controller.ts uploads.service.ts
forms/18debug.controller.ts form.dto.ts forms.controller.ts forms.module.ts forms.service.ts markdown.test.ts markdown.ts optional-auth.guard.ts public-forms.controller.ts public-forms.service.ts question.dto.ts questions.service.ts rate-limit.ts recaptcha.ts slug.ts submissions.controller.ts submissions.service.ts submit-schema.ts
health/3health.controller.ts health.module.ts health.service.ts
integrations/3integrations.controller.ts integrations.module.ts integrations.service.ts
internal/12
__tests__/2flush-query-shape.test.ts internal.endpoints.test.ts
ai-test-seam.controller.ts ai-test-seam.dto.ts ai-test-seam.service.ts internal.controller.ts internal.dto.ts internal.module.ts internal.service.ts test-seam.controller.ts test-seam.dto.ts test-seam.service.ts
notifications/3notifications.controller.ts notifications.module.ts notifications.service.ts
oauth/6
__tests__/1oauth.scopes.test.ts
oauth.controller.ts oauth.module.ts oauth.service.ts scopes.ts token-keys.ts
org/15audit.service.ts embed-preview.controller.ts guards.ts members.service.ts org-admin.controller.ts org-policy.service.ts org.module.ts overview.service.ts policy-effective.controller.ts policy.schema.ts policy.test.ts retention.service.ts totp.test.ts totp.ts two-factor.service.ts
orgs/5dto.ts orgs.controller.ts orgs.module.ts orgs.service.ts util.ts
pages/4dto.ts page.service.ts pages.controller.ts pages.module.ts
scim/15
__tests__/3email-mapping.test.ts etag.test.ts filters.test.ts
etag.ts filters.ts groups.service.ts schemas.ts scim-admin.controller.ts scim-admin.service.ts scim-auth.guard.ts scim-auth.service.ts scim-errors.ts scim.controller.ts scim.module.ts users.service.ts
search/4
__tests__/1search.service.test.ts
search.controller.ts search.module.ts search.service.ts
sessions/3sessions.controller.ts sessions.module.ts sessions.service.ts
share/3share.controller.ts share.module.ts share.service.ts
sso/16
__tests__/4enforcement.test.ts oidc.verify.test.ts provisioning.test.ts saml.validate.test.ts
enforcement.ts oidc.controller.ts oidc.service.ts oidc.ts provisioning.service.ts saml.controller.ts saml.ts schemas.ts sso-admin.controller.ts sso-admin.service.ts sso-secrets.ts sso.module.ts
teams/4dto.ts teams.controller.ts teams.module.ts teams.service.ts
users/4dto.ts users.controller.ts users.module.ts users.service.ts
webhooks/3webhooks.controller.ts webhooks.module.ts webhooks.service.ts
workspaces/4dto.ts workspaces.controller.ts workspaces.module.ts workspaces.service.ts
types/1xmldom.d.ts
app.module.ts constants.ts main.ts
_probe_rawbody.cjs .env.example Dockerfile package.json README.md tsconfig.json vitest.config.ts
realtime/33
src/17auth.ts comments.ts config.ts event-source.ts http-store.ts index.ts metrics.ts notifications.ts page-updated-source.ts presence.ts prisma-store.ts protocol.ts rate-limit.ts room-key.ts rooms.ts server.ts update-log.ts
test/11auth.test.ts config.test.ts event-source.test.ts http-store.test.ts metrics-merge.test.ts page-updated-source.test.ts presence.test.ts protocol.test.ts rate-limit.test.ts rooms.test.ts task-0094-builders.test.ts
.env.example Dockerfile package.json README.md tsconfig.json
web/255
src/244
app/45
__tests__/1theme-tokens.test.ts
(app)/26
w/25
[workspace]/25
ask/1page.tsx
databases/1
[id]/1page.tsx
favorites/1page.tsx
files/1page.tsx
notifications/1page.tsx
page/4
[id]/4
[block]/1page.tsx
share/1page.tsx
layout.tsx page.tsx
search/1page.tsx
settings/10
appearance/1page.tsx
automations/3
[id]/1page.tsx
new/1page.tsx
page.tsx
billing/1page.tsx
developers/1page.tsx
members/1page.tsx
permissions/1page.tsx
teams/1page.tsx
page.tsx
shared/1page.tsx
templates/1page.tsx
trash/1page.tsx
layout.tsx page.tsx
layout.tsx
(auth)/4
login/1page.tsx
register/1page.tsx
reset-password/1page.tsx
layout.tsx
api/2
metrics/1route.ts
v1/1
health/1route.ts
f/1
[slug]/1page.tsx
health/1
ready/1route.ts
org/2
[orgId]/2
admin/2
integrations/1page.tsx
page.tsx
share/1
[id]/1page.tsx
error.tsx global-error.tsx globals.css layout.tsx not-found.tsx page.tsx providers.tsx
components/118
admin/10
org-admin/8ApiKeysTab.tsx AuditTab.tsx MembersTab.tsx OverviewTab.tsx PoliciesTab.tsx RetentionTab.tsx WebhooksTab.tsx WorkspacePicker.tsx
ScimCard.tsx SsoTabs.tsx
ai/6ai-panel.tsx ask-ai-submenu.tsx db-ai-dialog.tsx generate-page-dialog.tsx writing-actions-menu.tsx writing-diff.tsx
automations/5action-editor.tsx condition-editor.tsx lib.ts run-history.tsx trigger-editor.tsx
collaboration/6CursorLayer.tsx OfflineBanner.tsx PresenceBar.tsx README.md ReviewFailedDialog.tsx StatusChip.tsx
comments/3comment-panel.tsx comment-thread.tsx mention-input.tsx
database/27
__tests__/2cell-editor.test.tsx cell-renderer.test.tsx
AiExtractDialog.tsx AiFormulaDialog.tsx AiGenerateDialog.tsx board-view.tsx buttons-panel.tsx calendar-view.tsx cell-editor.tsx cell-renderer.tsx chart-view.tsx database-page-view.tsx database-toolbar.tsx database-view-renderer.tsx db-embedded.tsx filter-builder.tsx form-builder-view.tsx formula-editor.tsx gallery-view.tsx group-builder.tsx list-view.tsx property-editor.tsx record-header.tsx record-panel.tsx sort-builder.tsx table-view.tsx timeline-view.tsx
dialogs/7
__tests__/1move-to-dialog.test.tsx
create-dialog.tsx dialog.tsx file-permissions-dialog.tsx invite-dialog.tsx move-to-dialog.tsx share-dialog.tsx
editor/19
__tests__/3code-block-text.test.ts code-block.test.tsx expandable.test.tsx
block-card.tsx block-renderer.tsx block-save-queue.ts block-toolbar.tsx context-menu.tsx expandable.tsx history-popover.tsx inline-editor.tsx insert-dialog.tsx page-canvas.tsx page-spinner.tsx page-view.tsx selection-toolbar.tsx slash-menu.tsx template-dialog.tsx type-menu.tsx
files/4FileBlock.tsx FileBrowser.tsx FilePicker.tsx PreviewModal.tsx
form/4form-field.tsx form-renderer.tsx form-responses-view.tsx form-view.tsx
layout/8
__tests__/1page-tree-icon.test.tsx
command-palette.tsx factory-logo.tsx page-tree-node.tsx sidebar.tsx theme-provider.tsx topbar.tsx workspace-shell.tsx
mentions/3
__tests__/1mention-model.test.ts
mention-model.ts MentionPopup.tsx
search/3ask-ai-search.tsx results-list.tsx search-dialog.tsx
settings/10
api-keys/4ApiKeyCreateDialog.tsx ApiKeyRevealDialog.tsx ApiKeyTable.tsx ScopesBadge.tsx
webhooks/5CurlExamples.tsx DeliveryInspector.tsx EventExplorer.tsx OverviewTab.tsx WebhooksTab.tsx
settings-shell.tsx
ui/3confirm-dialog.tsx form.tsx toaster.tsx
hooks/21
__tests__/4realtime-frame-handler.test.ts use-blocks.test.ts use-database-options.test.ts use-workspace.test.tsx
realtime-frame-handler.ts use-ai-stream.ts use-auth.ts use-blocks.ts use-database.ts use-db-ai-stream.ts use-forms.ts use-members.ts use-mention-sources.ts use-notifications.ts use-offline-status.ts use-pages.ts use-presence-announcer.ts use-presence-toasts.ts use-presence.ts use-realtime.ts use-workspace.ts
lib/50
__tests__/21ai-database.test.ts ai-generate.test.ts ai-sse.test.ts ai-writing.test.ts api-keys.test.ts api-members.test.ts auth-refresh.test.ts auth-store.test.ts batch-queue-drop.test.ts block-content.test.ts clipboard-import.test.ts code-highlight.test.ts offline-status.test.ts presence-announcer.test.ts presence-color.test.ts presence-cursor.test.ts presence-emitter.test.ts realtime-url.test.ts search.test.ts utils.test.ts writing-diff.test.ts
files/7
__tests__/2picker.test.ts upload.test.ts
api.ts picker.ts upload-store.ts upload.ts use-file-uploads.ts
ai-database.ts ai.ts api-keys.ts api.ts auth-store.ts batch-queue.ts block-content.ts clipboard-import.ts code-highlight.ts constants.ts offline-status.ts org-admin.ts presence-announcer.ts presence-color.ts presence-cursor.ts presence-editor-views.ts presence-emitter.ts query-client.ts realtime.ts search.ts types.ts utils.ts
stores/8
__tests__/1block-patch-store.test.ts
block-patch-store.ts comment-pill-store.ts database-store.ts editor-store.ts offlineQueue.ts presence-store.ts ui-store.ts
instrumentation.ts middleware.ts
.env.example Dockerfile next-env.d.ts next.config.mjs package.json postcss.config.mjs README.md tailwind.config.ts tsconfig.json tsconfig.tsbuildinfo vitest.config.ts
worker/92
src/87
__tests__/13ai-embedding.test.ts ai-request.test.ts ai-usage-budget.test.ts audit-export.test.ts email-decision.test.ts encrypt.test.ts health-search-index-lag.test.ts mime.test.ts retention.test.ts s3-sign.test.ts search-index.test.ts smtp.test.ts webhook-sign.test.ts
health/1server.ts
lib/12
__tests__/1clamav.test.ts
clamav.ts encrypt.ts job-otel.ts mime.ts notify.ts prisma.ts redis-stream.ts redis.ts s3.ts sharp.ts smtp.ts
processors/56
automations/28
__tests__/3condition-evaluator.spec.ts executors.spec.ts run-pipeline.spec.ts
executors/17assign-user.ts call-integration.ts change-status.ts classify.ts create-notification.ts create-page.ts email-cap.ts helpers.ts integration-registry.ts permission.ts record-write.ts registry.ts run-ai-action.ts send-email.ts send-webhook.ts types.ts update-record.ts
condition-evaluator.ts dead-letter.ts log.ts matcher.ts pipeline.ts run-state.ts schedule.ts watchdog.ts
files/15
__tests__/4errors.test.ts scan-gate.test.ts status.test.ts temp.test.ts
steps/4finalize.ts metadata.ts scan.ts thumbnails.ts
cleanup.ts errors.ts generateThumbs.ts processor.ts status.ts temp.ts types.ts
search-index/4builders.ts grants.ts index.ts processor.ts
ai-embedding.ts ai.processor.ts audit-export.ts automations.processor.ts email-decision.processor.ts email.processor.ts maintenance.processor.ts retention.ts webhooks.processor.ts
config.ts events.ts index.ts logger.ts queues.ts
.env.example Dockerfile package.json README.md tsconfig.json
docs/73
api/17ai.md authentication.md automations.md blocks.md comments.md databases.md files.md forms.md openapi.yaml org-admin.md pages.md scim.md search.md sso.md versioning.md webhooks.md workspaces.md
architecture/10ai-architecture.md automation.md backend-architecture.md database-architecture.md frontend-architecture.md realtime-architecture.md search-architecture.md security-architecture.md storage.md system-architecture.md
database/2database-architecture.md redis-keys.md
development/8admin-guide.md coding-standards.md deployment.md development-guide.md files-pipeline.md perf-report.md performance.md testing.md
product/4functional-requirements.md non-functional-requirements.md product-requirements.md ux-requirements.md
requirements/1requirements-matrix.md
security/5audit-report.md files.md forms.md license-allowlist.md security-architecture.md
tasks/16phase-00-foundation.md phase-01-authentication.md phase-02-workspace.md phase-03-pages.md phase-04-editor.md phase-05-databases.md phase-06-relations.md phase-07-collaboration.md phase-08-search.md phase-09-files.md phase-10-forms.md phase-11-automation.md phase-12-ai.md phase-13-api.md phase-14-enterprise.md phase-15-production.md
ui/9ai.md automations.md design-system.md developers.md files.md forms.md org-admin.md scim.md sso.md
support-matrix.md
infrastructure/71
caddy/1Caddyfile
docker/6
init/101-extensions.sql
opensearch/1init-indices.json
.env.docker docker-compose.yml opensearch-init.sh README.md
kubernetes/40
observability/20
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1sync-sources.mjs
alertmanager.yaml grafana.yaml kustomization.yaml namespace.yaml node-exporter.yaml otel-collector.yaml prometheus.yaml
postgres/7configmap.yaml cronjob-backup.yaml cronjob-opensearch-snapshot.yaml pgbouncer.yaml replica.yaml statefulset.yaml walg-iam-policy.yaml
api.yaml configmap.yaml ingress.yaml kustomization.yaml namespace.yaml networkpolicy.yaml pdb.yaml README.md realtime.yaml redis.yaml secret.example.yaml web.yaml worker.yaml
nginx/2nginx.conf README.md
observability/14
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1dashboard-completeness.mjs
README.md
terraform/8compute.tf data.tf main.tf network.tf outputs.tf README.md security.tf variables.tf
packages/289
ai/24
src/20
providers/7anthropic.ts google.ts http.ts index.ts openai-compatible.ts rag.test.ts writing.test.ts
schemas/2database.ts generate.ts
adapters.test.ts config.test.ts config.ts gateway.test.ts gateway.ts index.ts tools.test.ts tools.ts types.ts usage.test.ts usage.ts
package.json README.md tsconfig.json vitest.config.ts
auth/11
src/7index.ts passwords.test.ts passwords.ts sessions.test.ts sessions.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
collaboration/17
src/13awareness.ts codec.test.ts codec.ts index.ts lww.test.ts lww.ts mapping.ts offline.ts outbox.test.ts outbox.ts provider.test.ts provider.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
config/7
src/4eslint.base.js index.ts prettier.base.json tsconfig.base.json
package.json README.md tsconfig.json
database-client/5
src/2index.test.ts index.ts
package.json README.md tsconfig.json
database-engine/33
src/29
filter/2filter.test.ts index.ts
formula/8evaluator.ts formula.test.ts functions.ts index.ts lexer.ts parser.ts types.ts validate.ts
query/6group.ts index.ts paginate.ts pivot.ts query.test.ts sort.ts
relations/2index.ts relations.test.ts
rollups/2index.ts rollups.test.ts
values/5dates.ts index.ts registry.ts types.ts values.test.ts
views/3buttons.ts index.ts views.test.ts
index.ts
package.json README.md tsconfig.json vitest.config.ts
editor/25
src/21
marks/1highlight.ts
nodes/2equation.ts mention.ts
autosave.test.ts autosave.ts clipboard.test.ts clipboard.ts codec.test.ts codec.ts context-menu.test.ts context-menu.ts diff.test.ts diff.ts factory.ts history.test.ts history.ts index.ts positions.ts slash.test.ts slash.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
observability/20
src/16
__tests__/8db-redis-metrics.test.ts metrics.test.ts otel.test.ts pino-otel.test.ts prometheus.test.ts resource.test.ts scrubber.test.ts traceparent.test.ts
index.ts metrics.ts otel.ts pino-otel.ts prometheus.ts resource.ts scrubber.ts traceparent.ts
package.json README.md tsconfig.json vitest.config.ts
permissions/11
src/7index.ts levels.test.ts levels.ts resolve.test.ts resolve.ts rules.test.ts rules.ts
package.json README.md tsconfig.json vitest.config.ts
search/22
src/18chunking.test.ts chunking.ts client.test.ts client.ts embeddings.test.ts embeddings.ts hybrid.test.ts hybrid.ts index.ts indexer.test.ts indexer.ts mapping.test.ts mapping.ts queries.test.ts queries.ts rrf.test.ts rrf.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
shared/61
src/57
dto/17ai.dto.ts auth.dto.test.ts auth.dto.ts block.dto.ts browser-safety.test.ts comment.dto.ts database.dto.test.ts database.dto.ts file.dto.ts formula-ast.ts index.ts notification.dto.ts page.dto.ts search.dto.ts share.dto.ts webhook.dto.ts workspace.dto.ts
events/8audit-catalog.test.ts audit-catalog.ts audit-csv.test.ts audit-csv.ts audit-scope.test.ts audit-scope.ts catalog.test.ts catalog.ts
realtime/6comment-events.ts frames.test.ts frames.ts notification-policy.ts notification-prefs.ts task-0094.test.ts
automation.test.ts automation.ts constants.ts crypto.test.ts crypto.ts env.ts errors.ts events.test.ts events.ts forms.test.ts forms.ts index.ts markdown.test.ts markdown.ts mentions.test.ts mentions.ts net.test.ts net.ts positions.test.ts positions.ts presence.test.ts presence.ts security-headers.ts webhook-delivery.test.ts webhook-delivery.ts webhook-events.ts
package.json README.md tsconfig.json vitest.config.ts
storage/16
src/12client.test.ts client.ts index.ts keys.test.ts keys.ts mime.test.ts mime.ts presign.test.ts presign.ts thumbnails.test.ts thumbnails.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
ui/37
src/33
components/19avatar.tsx badge.tsx button.tsx checkbox.tsx command.tsx context-menu.tsx dialog.tsx dropdown-menu.tsx empty-state.tsx index.ts popover.tsx separator.tsx skeleton.tsx slider.tsx spinner.tsx switch.tsx tabs.tsx toast.tsx tooltip.tsx
color.ts command-filter.test.ts command-filter.ts focus.test.ts focus.ts formatters.test.ts formatters.ts index.ts theme.test.ts theme.ts toast-store.test.ts toast-store.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
prisma/16
migrations/13
20260927000000_init/1migration.sql
20260928000000_search_documents_content_hash/1migration.sql
20260929000000_workspace_usage_storage_bytes/1migration.sql
20260930000000_form_submitted_notification_type/1migration.sql
20261001000000_ai_phase12/1migration.sql
20261002000000_webhooks_secret_encrypted_dlq/1migration.sql
20261002111436_webhook_delivery_latency/1migration.sql
20261002120000_org_admin_2fa_retention_unique/1migration.sql
20261002215124_audit_export/1migration.sql
20261003210000_perf_indexes_hot_paths/1migration.sql
20261004004500_perf_indexes_hot_paths_2/1migration.sql
migration_lock.toml README.md
schema.prisma seed.ts verify.ts
scripts/7backup-health.ts docs-completeness.mjs generate-openapi.mjs minio-init.ts opensearch-reindex.ts storage-orphans.ts verify-clean-clone.sh
tests/169
e2e/63
.auth/1user.json
fixtures/7
files/6clip.mp4 clip.wav clip.webm hero-1400.png note.docx note.txt
realtime.ts
mocks/2oidc-issuer.ts saml-idp.ts
playwright-report/3
data/2a6557bae9c2714a658968d4eb8534195c08ebda8.png e3092d07d00a16063e46385c02a559a9054fa847.webm
index.html
reports/1files-security.json
specs/34ai-database.spec.ts ai-generate.spec.ts ai-rag.spec.ts ai-security.spec.ts ai-writing.spec.ts ai.spec.ts api-keys.spec.ts appearance-theme.spec.ts auth.spec.ts automation.spec.ts collab-two-tab.spec.ts comments.spec.ts databases.spec.ts default-workspace-alias.spec.ts developers.spec.ts editor.spec.ts files.spec.ts final-flow.spec.ts forms.spec.ts oauth.spec.ts org-admin.spec.ts pages.spec.ts performance.spec.ts presence-cursors.spec.ts qa-paste-toggle.spec.ts realtime.spec.ts scim.spec.ts search-index-lag.spec.ts search.spec.ts sharing.spec.ts sso.spec.ts task-0094-comments-notifications.spec.ts webhooks.spec.ts workspace.spec.ts
test-results-final-flow/3
final-flow-Release-v1-0-fi-6c06b-w-state-carries-sequential--chromium-desktop/2test-failed-1.png trace.zip
.last-run.json
utils/4api.ts dnd.ts search-lag.ts wait.ts
.gitignore fixtures.ts global-setup.ts package.json playwright.config.ts playwright.final-flow.config.ts README.md tsconfig.json
fixtures/1
files/1make-sparse.ts
integration/60
ai-capability/2ai-app.capability.test.ts gateway.capability.test.ts
drill/2seed-drill.mjs verify-restore.mjs
fixtures/5
files/5clip.mp4 clip.wav doc.pdf hero-1400.png note.txt
helpers/3api.ts containers.ts seed.ts
realtime/11control-channel.test.ts convergence.test.ts flush-rebase-http.test.ts handshake.test.ts heartbeat-limits.test.ts lifecycle.test.ts presence.test.ts rebase.test.ts resume.test.ts rt-helpers.ts task-0094-fanout.test.ts
ai-chat.integration.test.ts ai-database.integration.test.ts ai-generate.integration.test.ts ai-rag.integration.test.ts ai-writing.integration.test.ts api-keys.integration.test.ts audit-completeness.integration.test.ts audit-export-s3.integration.test.ts auth.integration.test.ts automation-pipeline.integration.test.ts automations.integration.test.ts blocks.integration.test.ts cache-invalidation.integration.test.ts comments.integration.test.ts databases.integration.test.ts files-lifecycle.integration.test.ts files-pipeline.integration.test.ts files-security.integration.test.ts files.integration.test.ts forms-responses.integration.test.ts forms.integration.test.ts oauth.integration.test.ts openapi.contract.test.ts org-admin.integration.test.ts package.json pages.integration.test.ts permissions.integration.test.ts README.md replica-routing.integration.test.ts retention-engine.integration.test.ts scim.integration.test.ts search.integration.test.ts sso.integration.test.ts tsconfig.json vitest.config.ts webhooks.integration.test.ts workspace.integration.test.ts
performance/28
fixtures/1search-100k.ts
helpers/2seed-100k.mjs seed-1k-load.mjs
reports/7api-load-smoke.json database-load-smoke.json editor-local.json realtime-load.json search-full.json search-last.json search-reduced.json
api-load.js api.js archive.tar database-load.js database.js editor-local.mjs extract-100k-env.mjs package.json README.md realtime-1k.js realtime-load.js realtime.js run-realtime-load.sh search-search.k6.js search-summary.json seed-100k.mjs threshold-breach-demo.js verify-zero-op-loss.mjs
security/16
helpers/1app.ts
audit-report.test.ts auth-paths.test.ts deps.test.ts formula-fuzz.test.ts gitleaks-config.test.ts headers.test.ts images.test.ts oracle.ts package.json permission-fuzz.test.ts rate-limits.test.ts README.md tsconfig.json upload-abuse.test.ts vitest.config.ts
README.md
.dockerignore .env .env.bak-task0125 .env.example .eslintrc.json .gitignore .npmrc .prettierignore .prettierrc.json AGENTS.md ARCHITECTURE.md CHANGELOG.md CONTRIBUTING.md gitleaks.toml LICENSE package.json pnpm-lock.yaml pnpm-workspace.yaml PRODUCT-SPEC.md README.md ROADMAP.md SECURITY.md tsconfig.json turbo.json
gateway.capability.test.tstests/integration/ai-capability/gateway.capability.test.tsraw← ai-capability20,170 B← Back to the app
1/**
2 * TASK-0121 — AI capability suite, part 1: the GATEWAY (provider-swap proof).
3 *
4 * This is the half of the Phase-12 capability contract that needs NO app
5 * stack: it drives the `@factory/ai` `AiGateway` directly and asserts an
6 * IDENTICAL capability set — echo chat, streaming chunk order, tool-call
7 * roundtrip, embedding dimension stability, error mapping — across every
8 * provider (`openai` / `anthropic` / `google` / `oss` / `custom`) AND the
9 * deterministic `MockProvider` (the CI/dev `AI_PROVIDER=mock` path).
10 *
11 * That is the ROADMAP exit criterion: "swapping `AI_PROVIDER` … passes the
12 * full capability suite with no code change in `apps/*`." The provider is
13 * selected purely by config; this file branches on NOTHING except which
14 * adapter it is asked to build. Real provider behavior is faked with an
15 * injectable `fetchImpl` (the adapters accept it), so the whole surface runs
16 * offline and deterministically. The real-provider branch (`AI_REAL_TEST=1`)
17 * reuses the identical assertion set against the real HTTP endpoint.
18 *
19 * Budget exhaustion (402 `AI_BUDGET_EXCEEDED` + admin Notification) and the
20 * per-workspace config override are asserted against the real app in
21 * `ai-app.capability.test.ts` (they require the Postgres ledger + Redis slot).
22 */
23import { beforeAll, describe, expect, it } from "vitest";
24import {
25  AiGateway,
26  AnthropicAdapter,
27  BudgetService,
28  CustomAdapter,
29  GoogleAdapter,
30  MockProvider,
31  OpenAIAdapter,
32  OssAdapter,
33  UsageLedger,
34  type ProviderAdapter,
35  type ProviderId,
36  type ResolvedAiConfig
37} from "@factory/ai";
38import { ApiError, ErrorCodes } from "@factory/shared";
39 
40// ---------------------------------------------------------------------------
41// Deterministic fake providers (injectable fetch) per wire protocol
42// ---------------------------------------------------------------------------
43 
44/** Pull the last user-message text out of any of the three wire protocols. */
45function lastUserText(body: Record<string, unknown>): string {
46  const messages = body.messages as Array<{ role: string; content: unknown }> | undefined;
47  if (messages) {
48    const last = [...messages].reverse().find((m) => m.role === "user");
49    if (typeof last?.content === "string") return last.content;
50  }
51  const contents = body.contents as Array<{ parts?: Array<{ text?: string }> }> | undefined;
52  if (contents) {
53    for (const c of [...contents].reverse()) {
54      const t = c.parts?.find((p) => typeof p.text === "string")?.text;
55      if (t) return t;
56    }
57  }
58  return "hello";
59}
60 
61const jsonResponse = (obj: unknown) =>
62  new Response(JSON.stringify(obj), { status: 200, headers: { "content-type": "application/json" } });
63const sseResponse = (text: string) =>
64  new Response(text, { status: 200, headers: { "content-type": "text/event-stream" } });
65 
66const TOOL_NAME = "create_page";
67const TOOL_ARGS = { title: "Mocked Page" };
68 
69/**
70 * A fetch that answers each provider's REAL wire protocol (chat SSE +
71 * embeddings), so the identical assertion set can drive every adapter. When
72 * the request carries `tools`, it emits a single tool call for
73 * `${TOOL_NAME}` with `${TOOL_ARGS}`.
74 */
75function makeProviderFetch(providerId: ProviderId): { fetchImpl: typeof fetch } {
76  const fetchImpl: typeof fetch = async (input, init) => {
77    const url = String(typeof input === "string" ? input : input.url);
78    const body = JSON.parse((init?.body as string) ?? "{}");
79    const hasTools = Array.isArray(body.tools) && body.tools.length > 0;
80 
81    // --- embeddings -------------------------------------------------------
82    if (providerId === "google" && url.includes(":batchEmbedContents")) {
83      const n = (body.requests as unknown[] | undefined)?.length ?? 1;
84      return jsonResponse({
85        embeddings: Array.from({ length: n }, () => ({ values: new Array(8).fill(0.1) }))
86      });
87    }
88    if (providerId !== "google" && url.includes("/embeddings")) {
89      const n = Array.isArray(body.input) ? body.input.length : 1;
90      return jsonResponse({
91        model: body.model ?? "embed",
92        data: Array.from({ length: n }, (_, i) => ({
93          index: i,
94          embedding: new Array(8).fill(0.1),
95          object: "embedding"
96        })),
97        usage: { prompt_tokens: n, total_tokens: n }
98      });
99    }
100 
101    // --- chat -------------------------------------------------------------
102    const text = lastUserText(body);
103    const words = text.split(" ");
104    const half = Math.max(1, Math.floor(words.length / 2));
105    const p1 = words.slice(0, half).join(" ");
106    const p2 = words.slice(half).join(" ");
107 
108    if (providerId === "anthropic") {
109      if (hasTools) {
110        const events = [
111          `event: content_block_start\ndata: ${JSON.stringify({
112            type: "content_block_start",
113            index: 0,
114            content_block: { type: "tool_use", id: "toolu_mock0", name: TOOL_NAME }
115          })}\n\n`,
116          `event: content_block_delta\ndata: ${JSON.stringify({
117            type: "content_block_delta",
118            index: 0,
119            delta: { type: "input_json_delta", partial_json: JSON.stringify(TOOL_ARGS) }
120          })}\n\n`,
121          `event: message_delta\ndata: ${JSON.stringify({
122            type: "message_delta",
123            delta: { stop_reason: "tool_use" },
124            usage: { output_tokens: words.length }
125          })}\n\n`,
126          `event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`
127        ].join("");
128        return sseResponse(events);
129      }
130      const events = [
131        `event: content_block_delta\ndata: ${JSON.stringify({
132          type: "content_block_delta",
133          index: 0,
134          delta: { type: "text_delta", text: p1 }
135        })}\n\n`,
136        `event: content_block_delta\ndata: ${JSON.stringify({
137          type: "content_block_delta",
138          index: 0,
139          delta: { type: "text_delta", text: " " + p2 }
140        })}\n\n`,
141        `event: message_delta\ndata: ${JSON.stringify({
142          type: "message_delta",
143          delta: { stop_reason: "end_turn" },
144          usage: { output_tokens: words.length }
145        })}\n\n`,
146        `event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`
147      ].join("");
148      return sseResponse(events);
149    }
150 
151    if (providerId === "google") {
152      if (hasTools) {
153        const events = [
154          `data: ${JSON.stringify({
155            candidates: [
156              {
157                content: { parts: [{ functionCall: { name: TOOL_NAME, args: TOOL_ARGS } }] },
158                finishReason: "STOP"
159              }
160            ],
161            usageMetadata: { promptTokenCount: words.length, candidatesTokenCount: words.length }
162          })}\n\n`
163        ].join("");
164        return sseResponse(events);
165      }
166      const events = [
167        `data: ${JSON.stringify({
168          candidates: [{ content: { parts: [{ text: p1 }] } }],
169          usageMetadata: { promptTokenCount: words.length, candidatesTokenCount: 0 }
170        })}\n\n`,
171        `data: ${JSON.stringify({
172          candidates: [{ content: { parts: [{ text: " " + p2 }] }, finishReason: "STOP" }],
173          usageMetadata: { promptTokenCount: words.length, candidatesTokenCount: words.length }
174        })}\n\n`
175      ].join("");
176      return sseResponse(events);
177    }
178 
179    // openai / oss / custom
180    if (hasTools) {
181      const events = [
182        `data: ${JSON.stringify({
183          choices: [
184            {
185              delta: {
186                tool_calls: [
187                  { index: 0, id: "call_mock0", type: "function", function: { name: TOOL_NAME } }
188                ]
189              }
190            }
191          ]
192        })}\n\n`,
193        `data: ${JSON.stringify({
194          choices: [
195            {
196              delta: {
197                tool_calls: [
198                  { index: 0, function: { arguments: JSON.stringify(TOOL_ARGS) } }
199                ]
200              },
201              finish_reason: "tool_calls"
202            }
203          ],
204          usage: { prompt_tokens: words.length, completion_tokens: words.length, total_tokens: words.length * 2 }
205        })}\n\n`,
206        "data: [DONE]\n\n"
207      ].join("");
208      return sseResponse(events);
209    }
210    const events = [
211      `data: ${JSON.stringify({ choices: [{ delta: { content: p1 } }] })}\n\n`,
212      `data: ${JSON.stringify({ choices: [{ delta: { content: " " + p2 } }] })}\n\n`,
213      `data: ${JSON.stringify({
214        choices: [{ delta: {}, finish_reason: "stop" }],
215        usage: { prompt_tokens: words.length, completion_tokens: words.length, total_tokens: words.length * 2 }
216      })}\n\n`,
217      "data: [DONE]\n\n"
218    ].join("");
219    return sseResponse(events);
220  };
221  return { fetchImpl };
222}
223 
224// ---------------------------------------------------------------------------
225// Gateway builders (one per provider; config is the ONLY difference)
226// ---------------------------------------------------------------------------
227 
228function configFor(providerId: ProviderId): ResolvedAiConfig {
229  return {
230    enabled: true,
231    provider: providerId,
232    chatModel: `${providerId}-chat`,
233    embedModel: `${providerId}-embed`,
234    embedFallbackProvider: "openai",
235    fallback: null,
236    budgets: { tokensPerMonth: 0, requestsPerMinute: 1000, maxOutputTokens: 4096, dailySpendUsd: 0 },
237    policies: { redactPii: false, logPrompts: true, allowUserContentInPrompts: true }
238  } as ResolvedAiConfig;
239}
240 
241function buildFakeGateway(providerId: ProviderId, embedDimension = 8): { gateway: AiGateway; ledger: UsageLedger } {
242  const { fetchImpl } = makeProviderFetch(providerId);
243  let adapter: ProviderAdapter;
244  switch (providerId) {
245    case "openai":
246      adapter = new OpenAIAdapter({ baseUrl: "http://x/v1", fetchImpl });
247      break;
248    case "custom":
249      adapter = new CustomAdapter({ baseUrl: "http://x/v1", fetchImpl });
250      break;
251    case "oss":
252      adapter = new OssAdapter({ baseUrl: "http://x/v1", fetchImpl, assumeDimension: embedDimension });
253      break;
254    case "anthropic":
255      adapter = new AnthropicAdapter({ apiKey: "", fetchImpl });
256      break;
257    case "google":
258      adapter = new GoogleAdapter({ apiKey: "", fetchImpl });
259      break;
260  }
261  // The anthropic entry has no embed API; the gateway routes embed to the
262  // "openai" fallback, so that adapter must also be resolvable.
263  const openaiFallback =
264    providerId === "openai" ? adapter : new OpenAIAdapter({ baseUrl: "http://x/v1", fetchImpl: makeProviderFetch("openai").fetchImpl });
265  const ledger = new UsageLedger();
266  const gateway = new AiGateway({
267    resolveConfig: () => configFor(providerId),
268    registry: {
269      get: (id) => (id === providerId ? adapter : id === "openai" ? openaiFallback : adapter)
270    },
271    budgets: new BudgetService(ledger),
272    ledger,
273    sleep: async () => {}
274  });
275  return { gateway, ledger };
276}
277 
278function buildMockGateway(): { gateway: AiGateway; ledger: UsageLedger; mock: MockProvider } {
279  const mock = new MockProvider("custom", 8);
280  const ledger = new UsageLedger();
281  const config = { ...configFor("custom"), chatModel: "mock-chat" } as ResolvedAiConfig;
282  const gateway = new AiGateway({
283    resolveConfig: () => config,
284    registry: { get: () => mock },
285    budgets: new BudgetService(ledger),
286    ledger,
287    sleep: async () => {}
288  });
289  return { gateway, ledger, mock };
290}
291 
292/**
293 * The five real provider ids, each driving a gateway whose ONLY difference
294 * from the others is config + adapter. (The mock provider is covered in its
295 * own contract block below — it is a dev/test adapter, not a production id.)
296 */
297const PROVIDER_IDS = ["openai", "anthropic", "google", "oss", "custom"] as ProviderId[];
298 
299// ---------------------------------------------------------------------------
300// THE IDENTICAL ASSERTION SET — run against every provider (zero branching)
301// ---------------------------------------------------------------------------
302 
303const ECHO_MSG = "the quick brown fox";
304const CHAT_PARAMS = { workspaceId: "ws-cap", messages: [{ role: "user", content: ECHO_MSG }] };
305 
306function assertCapabilitySet(title: string, run: () => Promise<{ gateway: AiGateway; ledger: UsageLedger }>) {
307  describe(`capability: ${title}`, () => {
308    let gateway: AiGateway;
309    let ledger: UsageLedger;
310    beforeAll(async () => {
311      ({ gateway, ledger } = await run());
312    });
313 
314    it("echo: chat returns the deterministic reply with usage", async () => {
315      const res = await gateway.chatCollected(CHAT_PARAMS);
316      expect(res.content).toBe(ECHO_MSG);
317      expect(res.finishReason).toBe("stop");
318      expect(res.usage.total).toBeGreaterThan(0);
319    });
320 
321    it("stream: deltas arrive in provider order; final chunk carries usage + done", async () => {
322      const deltas: string[] = [];
323      let doneChunk: { usage?: { in: number; out: number; total: number }; finishReason?: string } | undefined;
324      for await (const chunk of gateway.chat(CHAT_PARAMS)) {
325        if (chunk.delta) deltas.push(chunk.delta);
326        if (chunk.done) doneChunk = chunk;
327      }
328      expect(deltas.join("")).toBe(ECHO_MSG);
329      expect(doneChunk?.finishReason).toBe("stop");
330      expect(doneChunk?.usage?.total).toBeGreaterThan(0);
331    });
332 
333    it("tools: a tool-capable chat returns the tool call with parsed args", async () => {
334      const res = await gateway.chatCollected({
335        workspaceId: "ws-cap",
336        messages: [{ role: "user", content: "please create a page" }],
337        tools: [{ name: TOOL_NAME, description: "create a page", parameters: { type: "object" } }]
338      });
339      expect(res.toolCalls.length).toBe(1);
340      expect(res.toolCalls[0]?.name).toBe(TOOL_NAME);
341      expect(res.toolCalls[0]?.arguments).toEqual(TOOL_ARGS);
342      expect(res.finishReason).toBe("tool_calls");
343    });
344 
345    it("embed: dimension is stable and matches the recorded contract", async () => {
346      const a = await gateway.embed({ workspaceId: "ws-cap", texts: ["alpha", "beta"] });
347      const b = await gateway.embed({ workspaceId: "ws-cap", texts: ["alpha", "beta"] });
348      expect(a.vectors.length).toBe(2);
349      expect(a.dimension).toBe(8);
350      expect(b.dimension).toBe(a.dimension);
351      expect(b.vectors[0]?.[0]).toBe(a.vectors[0]?.[0]);
352    });
353 
354    it("ledger: a completed chat appends a kind=chat usage record", async () => {
355      const before = ledger.records.length;
356      await gateway.chatCollected(CHAT_PARAMS);
357      expect(ledger.records.length).toBe(before + 1);
358      const rec = ledger.records[ledger.records.length - 1]!;
359      expect(rec.kind).toBe("chat");
360      expect(rec.workspaceId).toBe("ws-cap");
361      expect(rec.status).toBe("ok");
362    });
363 
364    it("errors: 429 / 401 / 500 map to the canonical envelope", async () => {
365      const make = (status: number) => {
366        const fetchImpl: typeof fetch = async () =>
367          new Response(JSON.stringify({ error: "boom" }), {
368            status,
369            headers: { "content-type": "application/json" }
370          });
371        const adapter = new OpenAIAdapter({ baseUrl: "http://x/v1", fetchImpl });
372        const l = new UsageLedger();
373        return new AiGateway({
374          resolveConfig: () => configFor("openai"),
375          registry: { get: () => adapter },
376          budgets: new BudgetService(l),
377          ledger: l,
378          sleep: async () => {}
379        });
380      };
381 
382      const limited = await make(429).chatCollected(CHAT_PARAMS).catch((e) => e);
383      expect(limited).toBeInstanceOf(ApiError);
384      expect((limited as ApiError).code).toBe(ErrorCodes.RATE_LIMITED);
385 
386      const auth = await make(401).chatCollected(CHAT_PARAMS).catch((e) => e);
387      expect((auth as ApiError).code).toBe(ErrorCodes.AI_ERROR);
388      const authDetails = (auth as ApiError).details as { code?: string; reason?: string };
389      expect(authDetails?.code).toBe("SERVICE_UNAVAILABLE");
390      expect(authDetails?.reason).toBe("auth");
391 
392      const server = await make(500).chatCollected(CHAT_PARAMS).catch((e) => e);
393      expect((server as ApiError).code).toBe(ErrorCodes.AI_ERROR);
394      const serverDetails = (server as ApiError).details as { code?: string; reason?: string; retryable?: boolean };
395      expect(serverDetails?.code).toBe("SERVICE_UNAVAILABLE");
396      expect(serverDetails?.reason).toBe("provider_error");
397      expect(serverDetails?.retryable).toBe(true);
398    });
399  });
400}
401 
402describe("TASK-0121 gateway capability — provider swap = config only", () => {
403  for (const id of PROVIDER_IDS) {
404    assertCapabilitySet(`provider=${id}`, async () => buildFakeGateway(id));
405  }
406});
407 
408// The mock provider's tool-call roundtrip is deterministic (it echoes the
409// last user message into the tool args) — the identical set already covers
410// the roundtrip shape; this pins the mock's specific echo contract.
411describe("TASK-0121 gateway capability — mock provider echo contract", () => {
412  it("a tool call echoes the last user message into its arguments", async () => {
413    const { gateway } = buildMockGateway();
414    const res = await gateway.chatCollected({
415      workspaceId: "ws-cap",
416      messages: [{ role: "user", content: "make a page called X" }],
417      tools: [{ name: "create_page", description: "create", parameters: { type: "object" } }]
418    });
419    expect(res.toolCalls[0]?.name).toBe("create_page");
420    expect((res.toolCalls[0]?.arguments as { echoed?: string })?.echoed).toBe("make a page called X");
421  });
422});
423 
424// ---------------------------------------------------------------------------
425// Conditional REAL provider gate (AI_REAL_TEST=1) — identical assertion set
426// ---------------------------------------------------------------------------
427 
428/**
429 * When `AI_REAL_TEST=1` the identical set runs against the REAL provider from
430 * env (`AI_PROVIDER`/`AI_BASE_URL`/`AI_API_KEY`/`AI_MODEL`). Otherwise it is
431 * skipped with a warning. The assertion set is the SAME function — the gate
432 * only changes which adapter is constructed, never what is asserted.
433 */
434const REAL = process.env.AI_REAL_TEST === "1";
435 
436describe.runIf(REAL)("TASK-0121 gateway capability — real provider (AI_REAL_TEST=1)", () => {
437  const providerId = (process.env.AI_PROVIDER ?? "openai") as ProviderId;
438  it("runs the identical assertion set against the live endpoint", async () => {
439    let adapter: ProviderAdapter;
440    const baseUrl = process.env.AI_BASE_URL;
441    const key = process.env.AI_API_KEY ?? "";
442    switch (providerId) {
443      case "anthropic":
444        adapter = new AnthropicAdapter({ apiKey: key });
445        break;
446      case "google":
447        adapter = new GoogleAdapter({ apiKey: key });
448        break;
449      case "oss":
450        adapter = new OssAdapter({ baseUrl: baseUrl ?? "http://localhost:11434/v1", apiKey: key });
451        break;
452      case "custom":
453        adapter = new CustomAdapter({ baseUrl: baseUrl ?? "http://localhost:11434/v1", apiKey: key });
454        break;
455      default:
456        adapter = new OpenAIAdapter({ baseUrl: baseUrl ?? "https://api.openai.com/v1", apiKey: key });
457        break;
458    }
459    const ledger = new UsageLedger();
460    const gateway = new AiGateway({
461      resolveConfig: () => ({
462        ...configFor(providerId),
463        chatModel: process.env.AI_MODEL ?? configFor(providerId).chatModel
464      }),
465      registry: { get: (id) => (id === providerId ? adapter : new OpenAIAdapter({ apiKey: key })) },
466      budgets: new BudgetService(ledger),
467      ledger,
468      sleep: async (ms) => new Promise((r) => setTimeout(r, ms))
469    });
470    // Echo is provider-specific; assert the universal invariants only.
471    const res = await gateway.chatCollected(CHAT_PARAMS);
472    expect(res.content.length).toBeGreaterThan(0);
473    expect(res.finishReason).toBe("stop");
474    expect(res.usage.total).toBeGreaterThan(0);
475    const emb = await gateway.embed({ workspaceId: "ws-cap", texts: ["alpha"] });
476    expect(emb.vectors.length).toBe(1);
477    expect(emb.dimension).toBeGreaterThan(0);
478  });
479});
480 
481if (!REAL) {
482  describe("TASK-0121 gateway capability — real provider gate", () => {
483    it("is skipped unless AI_REAL_TEST=1 (warns)", () => {
484      // Document the gate; the real run is conditional above.
485      console.warn("[ai-capability] real-provider suite skipped (set AI_REAL_TEST=1 to run it)");
486    });
487  });
488}
489