.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
search-lag.tstests/e2e/utils/search-lag.tsraw← utils20,371 B← Back to the app
1/**
2 * AI Software Factory — E2E index-lag helpers (TASK-0100 req 1 + Backend
3 * Changes + Security).
4 *
5 * These are API / worker-surface helpers (the lag SLAs are measured without a
6 * browser; only `measureCreateLagViaSearchUi` drives the real UI, and
7 * `runSearchIndexBurst` publishes directly to the worker's Redis input stream)
8 * so the lag SLA can be measured deterministically against the REAL pipeline:
9 *
10 *   page write → API publishes `factory:events` → worker stream consumer →
11 *   OpenSearch upsert → query-time permission filter → POST /api/v1/search.
12 *
13 *   - `waitUntilSearchable`   create-side lag (t0 → first hit, 2 s polls, < 30 s)
14 *   - `waitUntilUnsearchable` delete-side  lag (t0 → last hit,  2 s polls, < 30 s)
15 *   - `assertNeverSearchable` permission negative (never visible in a window)
16 *   - `getSearchIndexLag`     the worker's `search-index:lag` metric (worker
17 *                             health server `/health/search-index:lag`)
18 *   - `runSearchIndexBurst`   1k-event burst → worker drains the backlog
19 *   - `measureCreateLagViaSearchUi` UI-side create lag (page via UI → found in
20 *                             the search dialog → exact-page URL)
21 *   - `setPagePrivateDirectDb`the one field no public API sets (Page.isPrivate)
22 *
23 * All lag helpers resolve the measured `lagMs` and the number of 2 s poll
24 * rounds, so a caller can log the curve / attach it to the completion note.
25 */
26import { PrismaClient } from "@prisma/client";
27import { expect, type APIRequestContext, type Locator, type Page } from "@playwright/test";
28 
29/** Index-lag SLA (spec req 1): create + delete must be < 30 s. */
30export const SEARCH_LAG_TIMEOUT_MS = 30_000;
31/** Spec-mandated poll cadence for POST /api/v1/search. */
32export const SEARCH_POLL_INTERVAL_MS = 2_000;
33/**
34 * TASK-0100 S5: the search dialog DEBOUNCES its input (SEARCH_DEBOUNCE_MS =
35 * 225 ms) and then runs the query + suggest fetches, so a result row renders
36 * ≈ 400 ms AFTER `input.fill(...)`. A one-shot `row.isVisible()` immediately
37 * after the fill therefore sees the pre-debounce state (no rows yet) even
38 * when the page IS already searchable (the API finds it in < 2 s) — the
39 * helpers must bound-wait for the row within each poll round.
40 */
41export const SEARCH_ROW_RENDER_WINDOW_MS = 2_500;
42 
43/**
44 * Bounded wait for a search-result row to render (dialog debounce + fetch +
45 * paint). Returns true when the row becomes visible within the window.
46 * Shared by the create-lag UI poll and the click-through re-open so both
47 * honour the dialog's debounce instead of racing it.
48 */
49export async function waitForSearchRow(row: Locator, windowMs: number = SEARCH_ROW_RENDER_WINDOW_MS): Promise<boolean> {
50  try {
51    await expect(row.first()).toBeVisible({ timeout: windowMs });
52    return true;
53  } catch {
54    return false;
55  }
56}
57 
58/**
59 * Plain sleep. `APIRequestContext` has NO `waitForTimeout` (that is a Page /
60 * Locator method) — the polling helpers are browser-free on purpose (they run
61 * against the API/worker surface), so they sleep on `setTimeout`.
62 */
63const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
64 
65/** Worker health server port (docs/development/development-guide.md). */
66export const WORKER_HEALTH_URL = process.env.WORKER_HEALTH_URL ?? "http://localhost:4003";
67 
68export interface LagResult {
69  /** Milliseconds from the caller's t0 to the terminal observation. */
70  lagMs: number;
71  /** How many 2 s poll rounds ran before the terminal observation. */
72  polls: number;
73  /** Server-side `tookMs` of the terminal query (cross-check, optional). */
74  tookMs?: number;
75}
76 
77interface SearchEnvelope {
78  data?: { id?: string; docId?: string; url?: string; title?: string; snippet?: string }[];
79  nextCursor?: string | null;
80  tookMs?: number;
81}
82 
83export interface SearchResponse {
84  status: number;
85  rows: SearchEnvelope["data"];
86  tookMs?: number;
87  /** True when the API answered `SEARCH_UNAVAILABLE` (fail-closed). */
88  unavailable?: boolean;
89}
90 
91/**
92 * One POST /api/v1/search (the live TASK-0097 contract: `{ q }` body,
93 * envelope `{ data, nextCursor, tookMs }`).
94 *
95 * "Unavailable" is detected by the `SEARCH_UNAVAILABLE` ERROR CODE, not the
96 * HTTP status: the API's `mapSharedError` propagates the upstream OpenSearch
97 * status (503 when OS is down, but e.g. 400 while the index alias still has
98 * no backing index, 429 on throttling — all fail-closed `SEARCH_UNAVAILABLE`).
99 * Unavailable responses return empty rows + `unavailable: true` — the
100 * polling helpers treat them as "not yet" and keep going to the SLA deadline
101 * (a persistent unavailability then surfaces as the SLA timeout).
102 */
103export async function search(ctx: APIRequestContext, q: string, limit = 10): Promise<SearchResponse> {
104  const res = await ctx.post("/api/v1/search", { data: { q, limit } });
105  if (res.status() !== 200) {
106    const text = await res.text();
107    let code: unknown;
108    try {
109      code = (JSON.parse(text) as { error?: { code?: unknown } }).error?.code;
110    } catch {
111      code = undefined; // non-JSON error body — treat as a hard failure below
112    }
113    if (code === "SEARCH_UNAVAILABLE") return { status: res.status(), rows: [], tookMs: 0, unavailable: true };
114    throw new Error(`search: POST /api/v1/search "${q}" → ${res.status}: ${text.slice(0, 300)}`);
115  }
116  const body = (await res.json()) as SearchEnvelope;
117  return { status: 200, rows: body.data ?? [], tookMs: body.tookMs };
118}
119 
120/**
121 * Does the result set contain the doc? The search `q` is a globally unique
122 * token (a uuid), so a substring match across the serialized rows cannot
123 * false-positive on unrelated docs.
124 */
125function rowsContain(rows: SearchResponse["rows"], q: string): boolean {
126  return (rows ?? []).some((r) => JSON.stringify(r).includes(q));
127}
128 
129/**
130 * Create-side lag (spec req 1 a–d): poll POST /api/v1/search every 2 s from
131 * `t0`, PASS when `q` appears, FAIL after the SLA. Returns the measured lag.
132 */
133export async function waitUntilSearchable(
134  ctx: APIRequestContext,
135  q: string,
136  t0: number,
137  opts: { timeoutMs?: number; intervalMs?: number } = {},
138): Promise<LagResult> {
139  const timeoutMs = opts.timeoutMs ?? SEARCH_LAG_TIMEOUT_MS;
140  const intervalMs = opts.intervalMs ?? SEARCH_POLL_INTERVAL_MS;
141  const deadline = Date.now() + timeoutMs;
142  let polls = 0;
143  let lastStatus = 0;
144  // eslint-disable-next-line no-constant-condition
145  while (true) {
146    polls += 1;
147    const { status, rows, tookMs } = await search(ctx, q).catch(() => ({
148      status: 0,
149      rows: [] as SearchResponse["rows"],
150      tookMs: 0,
151      unavailable: true,
152    }));
153    lastStatus = status;
154    if (rowsContain(rows, q)) {
155      return { lagMs: Date.now() - t0, polls, tookMs };
156    }
157    if (Date.now() >= deadline) {
158      throw new Error(
159        `index lag (create): "${q}" not searchable within ${timeoutMs} ms after ${polls} polls ` +
160          `(last search status ${lastStatus})`,
161      );
162    }
163    await sleep(intervalMs);
164  }
165}
166 
167/**
168 * Delete-side lag (spec req 1 / Tests): the doc was visible; poll until it is
169 * gone (the worker removes the doc on `page.deleted`). Returns the measured
170 * lag from the deletion t0.
171 */
172export async function waitUntilUnsearchable(
173  ctx: APIRequestContext,
174  q: string,
175  t0: number,
176  opts: { timeoutMs?: number; intervalMs?: number } = {},
177): Promise<LagResult> {
178  const timeoutMs = opts.timeoutMs ?? SEARCH_LAG_TIMEOUT_MS;
179  const intervalMs = opts.intervalMs ?? SEARCH_POLL_INTERVAL_MS;
180  const deadline = Date.now() + timeoutMs;
181  let polls = 0;
182  // eslint-disable-next-line no-constant-condition
183  while (true) {
184    polls += 1;
185    const { rows } = await search(ctx, q).catch(() => ({ rows: [] as SearchResponse["rows"] }));
186    if (!rowsContain(rows, q)) {
187      return { lagMs: Date.now() - t0, polls };
188    }
189    if (Date.now() >= deadline) {
190      throw new Error(
191        `index lag (delete): "${q}" still searchable ${timeoutMs} ms after deletion ` +
192          `(the worker must remove the doc on page.deleted)`,
193      );
194    }
195    await sleep(intervalMs);
196  }
197}
198 
199/**
200 * Permission negative (spec Security): a doc that must NEVER be visible to
201 * this viewer — e.g. another member's PRIVATE page — is polled for the whole
202 * window and must return ZERO rows the entire time. Fails the moment the doc
203 * appears.
204 */
205export async function assertNeverSearchable(
206  ctx: APIRequestContext,
207  q: string,
208  opts: { durationMs?: number; intervalMs?: number; description?: string } = {},
209): Promise<{ polls: number }> {
210  const durationMs = opts.durationMs ?? SEARCH_LAG_TIMEOUT_MS;
211  const intervalMs = opts.intervalMs ?? SEARCH_POLL_INTERVAL_MS;
212  const deadline = Date.now() + durationMs;
213  let polls = 0;
214  // eslint-disable-next-line no-constant-condition
215  while (true) {
216    polls += 1;
217    const { rows } = await search(ctx, q).catch(() => ({ rows: [] as SearchResponse["rows"] }));
218    if (rowsContain(rows, q)) {
219      throw new Error(
220        `${opts.description ?? "permission negative"}: "${q}" WAS visible after ${polls} poll(s) — ` +
221          `it must never be found`,
222      );
223    }
224    if (Date.now() >= deadline) break;
225    await sleep(intervalMs);
226  }
227  return { polls };
228}
229 
230// ---------------------------------------------------------------------------
231// UI-side create lag (spec req 1, test 1: "create page (UI) → searchable < 30 s")
232// ---------------------------------------------------------------------------
233 
234export interface CreateLagViaUiOptions {
235  /** Fired once the new page's id is known (read from the post-create URL). */
236  onCreated?: (pageId: string) => void;
237  /** SLA deadline (default 30 s). */
238  timeoutMs?: number;
239}
240 
241/**
242 * Create-side index lag measured through the REAL search UI (spec req 1 /
243 * Tests test 1). Drives the actual browser surface so the event path is the
244 * genuine one (page.created + the page.updated title write → `factory:events`
245 * → worker → OpenSearch):
246 *
247 *   1. navigate to the workspace (`/w/{id}` — the API resolves by id; the
248 *      search dialog scopes by the resolved workspace object, not the param)
249 *   2. click the sidebar "New page" button (aria-label="New page")
250 *   3. type the globally-unique `token` into the title input (aria-label="Page
251 *      title") and blur it so the optimistic title mutation flushes
252 *   4. open the ⌘K dialog, type `token`, and poll (2 s) until a `search-result`
253 *      row containing `token` is visible
254 *
255 * Returns the measured lag (t0 → first visible result row) and fires
256 * `onCreated(pageId)` once the sidebar's `router.push(/page/{id})` has made the
257 * id readable from the URL. The search dialog is CLOSED again before returning
258 * so a caller can re-open it deterministically (the button toggles).
259 */
260export async function measureCreateLagViaSearchUi(
261  page: Page,
262  workspaceId: string,
263  token: string,
264  opts: CreateLagViaUiOptions = {},
265): Promise<LagResult> {
266  const timeoutMs = opts.timeoutMs ?? SEARCH_LAG_TIMEOUT_MS;
267  const intervalMs = SEARCH_POLL_INTERVAL_MS;
268 
269  await page.goto(`/w/${workspaceId}`);
270  const newPageBtn = page.getByRole("button", { name: "New page" });
271  await expect(newPageBtn, 'the sidebar "New page" button').toBeVisible({ timeout: 15_000 });
272 
273  const t0 = Date.now();
274  await newPageBtn.click();
275 
276  // The sidebar creates the page (title "") and router.push's to /page/{id}.
277  const title = page.getByRole("textbox", { name: "Page title" });
278  await expect(title, 'the "Page title" input').toBeVisible({ timeout: 15_000 });
279  await title.click();
280  await title.fill(token);
281  await title.press("Tab"); // blur → flush the optimistic title mutation
282 
283  // The sidebar pushed /w/{slugOrId}/page/{id} — the id is the last segment.
284  const pageId = new URL(page.url()).pathname.split("/").filter(Boolean).pop() ?? "";
285  if (pageId) opts.onCreated?.(pageId);
286 
287  const deadline = t0 + timeoutMs;
288  let polls = 0;
289  const dialog = page.getByTestId("search-dialog");
290  const input = page.getByTestId("search-input");
291  const row = page.getByTestId("search-result").filter({ hasText: token });
292  // eslint-disable-next-line no-constant-condition
293  for (;;) {
294    polls += 1;
295    await page.getByTestId("search-button").click();
296    await expect(dialog).toBeVisible();
297    await input.fill(token);
298    // Bounded wait: the dialog debounces the fill (225 ms) + fetches, so a
299    // one-shot isVisible() always saw the pre-debounce state.
300    const visible = await waitForSearchRow(row);
301    if (visible) {
302      await page.keyboard.press("Escape"); // leave the dialog closed for the caller
303      return { lagMs: Date.now() - t0, polls };
304    }
305    await page.keyboard.press("Escape");
306    if (Date.now() >= deadline) {
307      throw new Error(
308        `measureCreateLagViaSearchUi: "${token}" not found in the search UI within ` +
309          `${timeoutMs} ms after ${polls} polls`,
310      );
311    }
312    await sleep(intervalMs);
313  }
314}
315 
316// ---------------------------------------------------------------------------
317// Worker consumer-lag metric (search-index:lag) — the burst assertion
318// ---------------------------------------------------------------------------
319 
320export interface SearchIndexLag {
321  metric: "search-index:lag";
322  /** False when the endpoint is missing / the worker predates TASK-0100. */
323  available: boolean;
324  /** Delivered to the group but not ACKed (in-flight / redelivery). */
325  pending: number;
326  /** Stream entries not yet delivered to the group (tail − last-delivered). */
327  lagEntries: number;
328  /** pending + lagEntries — the backlog the burst assertion drains. */
329  backlog: number;
330}
331 
332/**
333 * Read `GET /health/search-index:lag` from the worker health server.
334 * Resolves `{ available: false, ... }` (never throws) when the worker does not
335 * expose the metric — callers skip the assertion instead of failing the run.
336 */
337export async function getSearchIndexLag(): Promise<SearchIndexLag> {
338  const fallback: SearchIndexLag = {
339    metric: "search-index:lag",
340    available: false,
341    pending: 0,
342    lagEntries: 0,
343    backlog: 0
344  };
345  try {
346    const res = await fetch(`${WORKER_HEALTH_URL}/health/search-index:lag`, {
347      signal: AbortSignal.timeout(5_000)
348    });
349    if (res.status !== 200) return fallback;
350    const body = (await res.json()) as Partial<SearchIndexLag>;
351    if (typeof body.available !== "boolean") return fallback;
352    return {
353      metric: "search-index:lag",
354      available: body.available,
355      pending: body.pending ?? 0,
356      lagEntries: body.lagEntries ?? 0,
357      backlog: body.backlog ?? 0
358    };
359  } catch {
360    return fallback;
361  }
362}
363 
364export interface BurstResult {
365  /** Number of events published to the stream. */
366  published: number;
367  /** Wall-clock ms to XADD the whole burst. */
368  burstMs: number;
369  /** Wall-clock ms from burst start to backlog === 0 (the drain). */
370  drainMs: number;
371  /** Per-interval backlog samples (tMs from burst start; -1 = unavailable). */
372  samples: { tMs: number; backlog: number }[];
373}
374 
375/**
376 * Burst assertion (spec Backend Changes: "verify the worker consumes a 1k-event
377 * burst without backlog growth > 30 s — assert in the lag suite via the
378 * consumer-lag metric `search-index:lag`").
379 *
380 * The events are published DIRECTLY to the worker's input stream
381 * (`factory:events`) as real `page.updated` envelopes referencing `pageIds` —
382 * each one makes the worker run a genuine re-index (DB read + OpenSearch
383 * upsert), so this is real indexing load, not a no-op. Publishing to the
384 * stream (rather than through the public API) is deliberate: the API's
385 * per-user rate limit (100/min) makes a true 1k-event burst infeasible via
386 * HTTP, while the stream is the worker's actual input and exactly what the
387 * `search-index:lag` metric observes. `pageIds` must be REAL pages (created
388 * via the API first) so the events take the index path, not the
389 * missing-source (poison) path.
390 *
391 * The assertion: after XADD-ing `count` events, the worker's backlog
392 * (`pending + lagEntries` from `/health/search-index:lag`) must reach 0
393 * within the SLA (default 30 s). Throws with the backlog curve on failure.
394 */
395export async function runSearchIndexBurst(opts: {
396  /** Redis DSN (CI e2e job env / local dev .env; default redis://localhost:6379). */
397  redisUrl?: string;
398  /** Real page ids to cycle the `page.updated` events over. */
399  pageIds: string[];
400  workspaceId: string;
401  /** Burst size (spec: 1000). */
402  count?: number;
403  timeoutMs?: number;
404  intervalMs?: number;
405}): Promise<BurstResult> {
406  const { redisUrl, pageIds, workspaceId } = opts;
407  const count = opts.count ?? 1_000;
408  const timeoutMs = opts.timeoutMs ?? SEARCH_LAG_TIMEOUT_MS;
409  const intervalMs = opts.intervalMs ?? SEARCH_POLL_INTERVAL_MS;
410  if (pageIds.length === 0) throw new Error("runSearchIndexBurst: pageIds must be non-empty");
411 
412  // Lazy import so the rest of the helper (used by the non-burst tests) loads
413  // without a Redis dependency at module-eval time.
414  const { default: Redis } = await import("ioredis");
415  const redis = new Redis(redisUrl ?? process.env.REDIS_URL ?? "redis://localhost:6379", {
416    maxRetriesPerRequest: 2,
417    lazyConnect: true
418  });
419 
420  const t0 = Date.now();
421  let burstMs = 0;
422  try {
423    await redis.connect().catch(() => {
424      throw new Error(
425        `runSearchIndexBurst: cannot connect to Redis (${redisUrl ?? process.env.REDIS_URL ?? "redis://localhost:6379"}) ` +
426          `— the burst assertion needs the worker's Redis`,
427      );
428    });
429    const STREAM = "factory:events";
430    // One pipeline, `count` XADDs (fast — a single round-trip).
431    const pipe = redis.pipeline();
432    for (let i = 0; i < count; i += 1) {
433      const pageId = pageIds[i % pageIds.length];
434      const payload = JSON.stringify({ workspaceId, pageId, fields: ["title"] });
435      pipe.xadd(STREAM, "*", "event", "page.updated", "payload", payload, "ts", new Date().toISOString());
436    }
437    const started = Date.now();
438    await pipe.exec();
439    burstMs = Date.now() - started;
440  } finally {
441    redis.disconnect();
442  }
443 
444  // Drain: poll the worker's lag metric until the backlog is 0.
445  const samples: BurstResult["samples"] = [];
446  // eslint-disable-next-line no-constant-condition
447  while (true) {
448    const lag = await getSearchIndexLag();
449    samples.push({ tMs: Date.now() - t0, backlog: lag.available ? lag.backlog : -1 });
450    if (lag.available && lag.backlog === 0) {
451      return { published: count, burstMs, drainMs: Date.now() - t0, samples };
452    }
453    if (Date.now() - t0 >= timeoutMs) {
454      const curve = samples.map((s) => `${s.tMs}ms:${s.backlog}`).join(" ");
455      throw new Error(
456        `worker backlog did not drain within ${timeoutMs} ms after a ${count}-event burst ` +
457          `(search-index:lag samples tMs:backlog = ${curve}; -1 = endpoint unavailable)`,
458      );
459    }
460    await new Promise((r) => setTimeout(r, intervalMs));
461  }
462}
463 
464// ---------------------------------------------------------------------------
465// Private-page seam (TASK-0100 Security negative)
466// ---------------------------------------------------------------------------
467 
468/**
469 * Direct-DB write for the ONE field no public API sets: `Page.isPrivate`
470 * (UpdatePageDto has no privacy field — recorded quirk; the Phase-9 privacy UI
471 * is the intended surface). The spec's negative test needs a private page
472 * owned by one member that another member must NEVER find; only a source-row
473 * write produces the correct `isPrivate` grant snapshot (the worker stamps it
474 * from the row at index time; an API `page.updated` right after re-stamps it).
475 *
476 * Requires `DATABASE_URL` (CI e2e job env / local dev .env).
477 */
478export async function setPagePrivateDirectDb(pageId: string, isPrivate: boolean): Promise<boolean> {
479  const dsn = process.env.DATABASE_URL;
480  if (!dsn) {
481    throw new Error(
482      "setPagePrivateDirectDb: DATABASE_URL is not set — the private-page " +
483        "negative test needs the DB DSN (CI e2e job sets it; local dev .env).",
484    );
485  }
486  const prisma = new PrismaClient({ datasources: { db: { url: dsn } } });
487  try {
488    const updated = await prisma.page.update({
489      where: { id: pageId },
490      data: { isPrivate },
491      select: { isPrivate: true }
492    });
493    expect(updated.isPrivate, `setPagePrivateDirectDb(${pageId}): DB write`).toBe(isPrivate);
494    return updated.isPrivate;
495  } finally {
496    await prisma.$disconnect();
497  }
498}
499