spine-framework 0.4.4 → 0.5.0-rc.5
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 +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/install-app.ts +183 -13
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-app.ts +127 -1
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/app-manifest.ts +2 -0
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/permissions.ts +4 -0
- package/.framework/functions/admin-data.ts +51 -28
- package/.framework/functions/ai-agents.ts +10 -6
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +63 -29
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +149 -78
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +278 -148
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +240 -127
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/migrations/074_app_route_prefix_domains.sql +168 -0
- package/.framework/migrations/075_security_definer_grants.sql +157 -0
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/layout/Sidebar.tsx +300 -177
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/contexts/AppContext.tsx +5 -12
- package/.framework/src/contexts/AuthContext.tsx +17 -9
- package/.framework/src/hooks/useApps.ts +18 -22
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +74 -0
- package/.framework/src/instructions/accounts.md +97 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +228 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/internal/docs-contract.md +158 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/notifications.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +170 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/people.md +91 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +184 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/frontend-sdk.md +207 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +197 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-collaboration.md +106 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/document-your-app.md +117 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +136 -0
- package/.framework/src/instructions/tasks/register-an-app.md +137 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +84 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/app-mount.ts +88 -0
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/src/pages/auth/LoginPage.tsx +3 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +16 -8
- package/.framework/templates/agent/AGENTS.md +167 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +79 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +79 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +71 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +4 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +394 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +479 -44
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/app-mount.test.ts +80 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/security-definer-grants.test.ts +154 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- package/.framework/mcp/tools/observability.ts +0 -510
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Test your code
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- vitest.config.ts
|
|
9
|
+
- .framework/tests/setup.ts
|
|
10
|
+
- .framework/tests/reporter.ts
|
|
11
|
+
- package.json
|
|
12
|
+
- config/tsconfig.json
|
|
13
|
+
- config/tsconfig.functions.json
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Test your code
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Run the Vitest suite and the TypeScript checks, and place new tests where
|
|
21
|
+
the runner picks them up.
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
1. **Know the runners** (`package.json` scripts):
|
|
26
|
+
|
|
27
|
+
- `npm run test` → `vitest run` — everything matching the `include`
|
|
28
|
+
globs in `vitest.config.ts`: `.framework/tests/unit/**/*.test.ts`,
|
|
29
|
+
`.framework/tests/api/**/*.test.ts`, `.framework/tests/integration/**/*.test.ts`,
|
|
30
|
+
and `custom/tests/**/*.test.ts`.
|
|
31
|
+
- `npm run test:unit` — the unit suite only.
|
|
32
|
+
- `npm run test:integration` — the integration suite (needs real env,
|
|
33
|
+
see below).
|
|
34
|
+
- `npm run test:core` — the core-isolation suite.
|
|
35
|
+
- `npm run typecheck` → `tsc -b config/tsconfig.json` — frontend +
|
|
36
|
+
CLI. Functions are **not** covered here: check them separately with
|
|
37
|
+
`npx tsc --noEmit -p config/tsconfig.functions.json` after
|
|
38
|
+
`npm run assemble` (it typechecks the assembled layout).
|
|
39
|
+
- `npx spine-framework test [suite]` — the CLI wrapper
|
|
40
|
+
(`--watch`, `--reporter`, `--json`; see [CLI reference](../reference/cli.md)).
|
|
41
|
+
|
|
42
|
+
2. **Put tests where the globs look.** New project tests belong in
|
|
43
|
+
`custom/tests/**/*.test.ts` — picked up automatically, and they keep
|
|
44
|
+
`.framework/` untouched. Framework-level additions go under
|
|
45
|
+
`.framework/tests/unit/`. Test files inside `custom/functions/` are
|
|
46
|
+
excluded from function assembly (`*-test.ts`, `*.test.ts` are skipped
|
|
47
|
+
by `scripts/assemble-functions.sh`), but naming convention is still
|
|
48
|
+
`*.test.ts`.
|
|
49
|
+
|
|
50
|
+
3. **Understand the env layering.** `vitest.config.ts` loads a root
|
|
51
|
+
`.env.test` file (if present) into `process.env` **before** any module
|
|
52
|
+
graph runs — that is how integration/API tests get real Supabase
|
|
53
|
+
credentials before `db.ts`/`principal.ts` call `createClient` at import
|
|
54
|
+
time. `.framework/tests/setup.ts` then stubs `SUPABASE_URL`,
|
|
55
|
+
`SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` so unit tests never
|
|
56
|
+
crash on missing env. Integration helpers additionally require
|
|
57
|
+
`SPINE_TEST_ACCOUNT_ID`; API-surface tests use `SPINE_DEV_URL`,
|
|
58
|
+
`SPINE_DEV_JWT`, `SPINE_TEST_API_KEY` (see
|
|
59
|
+
[Environment variables](../reference/env-vars.md)).
|
|
60
|
+
|
|
61
|
+
4. **Mock at module boundaries.** The config sets `isolate: true` — every
|
|
62
|
+
test file gets a fresh module registry, so `vi.mock` on
|
|
63
|
+
`./_shared/db`-style imports works per file. Existing unit suites mock
|
|
64
|
+
the Supabase client, principal resolution, and the LLM/embedding calls
|
|
65
|
+
rather than hitting the network (browse `.framework/tests/unit/` for
|
|
66
|
+
patterns — `pipeline-runner.test.ts`, `permissions.test.ts`,
|
|
67
|
+
`principal-cache.test.ts`).
|
|
68
|
+
|
|
69
|
+
5. **Integration tests need a real project.** They exercise admin-data,
|
|
70
|
+
triggers, pipelines, and schema validation against a live Supabase
|
|
71
|
+
project using a service-role client and a seeded test account —
|
|
72
|
+
`custom/tests` and the unit suite run with no credentials, the
|
|
73
|
+
integration suite does not. `passWithNoTests: true` means a missing
|
|
74
|
+
env never fails the suite outright, so check that tests actually ran.
|
|
75
|
+
|
|
76
|
+
## Verify
|
|
77
|
+
|
|
78
|
+
- `npm run test` exits 0; `vitest` prints per-file pass counts. A custom
|
|
79
|
+
reporter that persists runs to `public.test_runs` exists at
|
|
80
|
+
`.framework/tests/reporter.ts` but is **not** registered — the config
|
|
81
|
+
uses `reporters: ['default']`, so opt in with `--reporter` if you want
|
|
82
|
+
persisted runs.
|
|
83
|
+
- `npm run typecheck` and
|
|
84
|
+
`npx tsc --noEmit -p config/tsconfig.functions.json` are both clean.
|
|
85
|
+
- `npx spine-framework doctor` sanity-checks the environment when tests
|
|
86
|
+
behave oddly ([CLI reference](../reference/cli.md)).
|
|
87
|
+
|
|
88
|
+
## Common mistakes
|
|
89
|
+
|
|
90
|
+
- **`typecheck` doesn't cover functions.** `config/tsconfig.json` excludes
|
|
91
|
+
the functions project by design — always run the
|
|
92
|
+
`tsconfig.functions.json` check too, and only after `npm run assemble`.
|
|
93
|
+
- **Assuming integration tests ran.** With no `.env.test`, integration
|
|
94
|
+
files that can't build a client may skip silently — grep the output for
|
|
95
|
+
your file's pass count.
|
|
96
|
+
- **Importing `createClient` at module top-level in a unit test.** Setup
|
|
97
|
+
stubs the env, but a module that builds a client at import time before
|
|
98
|
+
`setupFiles` runs still fails — mock the module or import lazily.
|
|
99
|
+
- **Writing repo-only paths into `custom/tests`** — tests there must run
|
|
100
|
+
in an installed project too; import from `custom/` and assembled
|
|
101
|
+
aliases, not from repo internals.
|
|
102
|
+
|
|
103
|
+
## Reference
|
|
104
|
+
|
|
105
|
+
- [CLI reference](../reference/cli.md) — `test`, `doctor`
|
|
106
|
+
- [Environment variables](../reference/env-vars.md) — `SPINE_TEST_*`,
|
|
107
|
+
`SPINE_DEV_*`, `SUPABASE_*`
|
|
108
|
+
- [Write a function](./write-a-function.md) — assembled-layout imports
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write a function
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/_shared/middleware.ts
|
|
9
|
+
- .framework/functions/_shared/db.ts
|
|
10
|
+
- .framework/functions/_shared/permissions.ts
|
|
11
|
+
- .framework/functions/_shared/logging.ts
|
|
12
|
+
- .framework/functions/timers.ts
|
|
13
|
+
- .framework/public/_redirects
|
|
14
|
+
- scripts/assemble-functions.sh
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Write a function
|
|
18
|
+
|
|
19
|
+
## Goal
|
|
20
|
+
|
|
21
|
+
Add a custom Netlify function when — and only when — the generic
|
|
22
|
+
`/api/admin-data` endpoint cannot cover the case: specialized auth, inbound
|
|
23
|
+
webhooks, heavy compute, or bespoke orchestration. Record CRUD must go
|
|
24
|
+
through `admin-data` so permission, validation, audit, and trigger logic
|
|
25
|
+
stay shared ([Runtime records](../concepts/runtime-records.md)).
|
|
26
|
+
|
|
27
|
+
## Steps
|
|
28
|
+
|
|
29
|
+
1. **Decide it is warranted.** `admin-data` already covers list/get/create/
|
|
30
|
+
update/remove/stats across every runtime table with permissions and
|
|
31
|
+
triggers enforced ([admin-data reference](../reference/functions/admin-data.md)).
|
|
32
|
+
Legitimate custom functions: public webhook receivers, scheduled
|
|
33
|
+
sweeps, compute-heavy transforms, auth-bridging endpoints.
|
|
34
|
+
|
|
35
|
+
2. **Create the file.** Put it in `custom/functions/` (or
|
|
36
|
+
`custom/apps/<slug>/functions/` for app-scoped code) with a `custom_`
|
|
37
|
+
prefix — assembly copies `.framework/functions`, `custom/functions`,
|
|
38
|
+
and `custom/apps/*/functions` into one flat Netlify dir and **fails the
|
|
39
|
+
build on a basename collision**
|
|
40
|
+
(`scripts/assemble-functions.sh`). All functions are reachable at
|
|
41
|
+
`/api/<basename>` via the `_redirects` rule
|
|
42
|
+
(`.framework/public/_redirects`).
|
|
43
|
+
|
|
44
|
+
Imports resolve in the **assembled** layout, not the source tree:
|
|
45
|
+
`./_shared/*` and `./<other-function>` are sibling paths inside
|
|
46
|
+
`.assembled/netlify/functions/`, and `@shared/*` is an alias to the
|
|
47
|
+
same dir (`config/tsconfig.functions.json`). A function's own
|
|
48
|
+
`functions/_shared -> .framework/functions/_shared` symlink (as the
|
|
49
|
+
existing app functions dirs carry) makes `./_shared` resolve in the
|
|
50
|
+
source tree too.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// custom/functions/custom_my-endpoint.ts
|
|
54
|
+
import { createHandler, error, json } from './_shared/middleware'
|
|
55
|
+
import { adminDb } from './_shared/db'
|
|
56
|
+
import { PermissionEngine } from './_shared/permissions'
|
|
57
|
+
import { emitLog } from './_shared/logging'
|
|
58
|
+
|
|
59
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
60
|
+
// ctx.requestId, ctx.principal, ctx.db, ctx.accountId, ctx.appId,
|
|
61
|
+
// ctx.query, ctx.headers, ctx.trace — all populated by middleware
|
|
62
|
+
if (!PermissionEngine.isSystemAdmin(ctx)) {
|
|
63
|
+
throw new Error('System admin required')
|
|
64
|
+
}
|
|
65
|
+
const { data, error: err } = await ctx.db
|
|
66
|
+
.from('items').select('id,title').eq('is_active', true).limit(10)
|
|
67
|
+
if (err) throw err
|
|
68
|
+
return data
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. **Follow the `createHandler` contract** (`.framework/functions/_shared/middleware.ts`):
|
|
73
|
+
|
|
74
|
+
- The middleware resolves the principal (`x-api-key` → internal secret →
|
|
75
|
+
Bearer JWT), builds `RequestContext`, and wraps your return value in
|
|
76
|
+
the `{ data, error, meta }` envelope. Thrown `Error`s become
|
|
77
|
+
`{ error }` responses — don't build response objects yourself.
|
|
78
|
+
- `ctx.db` is the principal-scoped client (humans get a user-scoped,
|
|
79
|
+
RLS-bound client; machines/anonymous get `adminDb`).
|
|
80
|
+
- `ctx.query` merges `queryStringParameters`; `ctx.query.method` carries
|
|
81
|
+
the HTTP method. Functions dispatching on `?action=` use a
|
|
82
|
+
`switch (ctx.query.action)` (see `.framework/functions/timers.ts`).
|
|
83
|
+
- For endpoints that must accept anonymous or raw requests, use
|
|
84
|
+
`createPublicHandler` instead (same file, `createPublicHandler`).
|
|
85
|
+
|
|
86
|
+
4. **Use the `_shared` modules, don't reinvent them** — `db` (`adminDb`,
|
|
87
|
+
`getUserDb`), `permissions` (`PermissionEngine`, `isSystemAdmin`,
|
|
88
|
+
`sanitizeRecordData`), `principal` (`resolvePrincipal`,
|
|
89
|
+
`SYSTEM_PRINCIPAL`), `audit`/`logging` (`emitLog`, `emitAudit`),
|
|
90
|
+
`resolve-ids`, `rate-limit` — see
|
|
91
|
+
[Shared modules reference](../reference/shared-modules.md) and
|
|
92
|
+
[Functions index](../reference/functions/index.md).
|
|
93
|
+
|
|
94
|
+
5. **Assemble and run.** `npm run assemble && npm run dev:netlify`, then
|
|
95
|
+
`curl http://localhost:8888/api/custom_my-endpoint` — see
|
|
96
|
+
[Install and first run](../guide/01-install-and-first-run.md).
|
|
97
|
+
Function typecheck is separate from `npm run typecheck`:
|
|
98
|
+
`npx tsc --noEmit -p config/tsconfig.functions.json` checks the
|
|
99
|
+
assembled output (run it after `assemble`).
|
|
100
|
+
|
|
101
|
+
## Verify
|
|
102
|
+
|
|
103
|
+
- `GET /api/custom_my-endpoint` returns the `{ data, error, meta }`
|
|
104
|
+
envelope; unauthenticated calls return `401 Authentication required`
|
|
105
|
+
from the middleware, not from your code.
|
|
106
|
+
- `npm run assemble` passes (no basename collision error) and
|
|
107
|
+
`npm run typecheck` compiles.
|
|
108
|
+
|
|
109
|
+
## Common mistakes
|
|
110
|
+
|
|
111
|
+
- **Duplicating a framework basename.** Assembly hard-fails naming both
|
|
112
|
+
sources — prefix custom files `custom_`.
|
|
113
|
+
- **Returning raw Response objects.** `createHandler` expects a return
|
|
114
|
+
value (serialized into `data`) or a thrown error — don't emit Netlify
|
|
115
|
+
`{statusCode, body}` shapes yourself inside a `createHandler` function.
|
|
116
|
+
- **Using `adminDb` for user-facing reads.** It bypasses RLS — prefer
|
|
117
|
+
`ctx.db` unless you intentionally need service-role access.
|
|
118
|
+
- **Putting secrets in the response.** `sanitizeRecordData` /
|
|
119
|
+
`omitSecretFields` exist for a reason — strip credentials before
|
|
120
|
+
returning.
|
|
121
|
+
- **Writing record CRUD** — if the function creates/updates `items`,
|
|
122
|
+
`threads`, etc., you are bypassing `admin-data`; call it via
|
|
123
|
+
`adminDataCreate`/`/api/admin-data` instead.
|
|
124
|
+
|
|
125
|
+
## Reference
|
|
126
|
+
|
|
127
|
+
- [Shared modules reference](../reference/shared-modules.md) ·
|
|
128
|
+
[Functions index](../reference/functions/index.md) ·
|
|
129
|
+
[Auth and API keys](../concepts/auth-and-api-keys.md)
|
|
130
|
+
- [admin-data reference](../reference/functions/admin-data.md) ·
|
|
131
|
+
[Build a custom page](./build-a-custom-page.md) ·
|
|
132
|
+
[Anatomy of an installed project](../guide/02-project-map.md)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write a migration
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/cli/commands/migrate.ts
|
|
9
|
+
- .framework/migrations/068_cron_schedule_timer_rpcs.sql
|
|
10
|
+
- .framework/migrations/053_builtin_action_seeds.sql
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Write a migration
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
|
|
17
|
+
Add a `.sql` file that the `migrate` command applies exactly once, in order,
|
|
18
|
+
tracked in the `_spine_migrations` ledger table. See
|
|
19
|
+
[Database reference](../reference/database.md) for the table map.
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. **Pick the directory.**
|
|
24
|
+
|
|
25
|
+
- `custom/migrations/` — project/app migrations (**default choice**).
|
|
26
|
+
`collectMigrations` reads `.framework/migrations/*.sql` first, then
|
|
27
|
+
`custom/migrations/*.sql`, each sorted lexically — custom migrations
|
|
28
|
+
always run after every framework migration
|
|
29
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
30
|
+
- `.framework/migrations/` — only for framework-owned changes (you are
|
|
31
|
+
editing the framework itself).
|
|
32
|
+
|
|
33
|
+
2. **Pick the number.** Framework files use `NNN_name.sql` with zero-padded
|
|
34
|
+
sequence numbers. Occupied ranges today: `001`–`021`, `032`–`038`,
|
|
35
|
+
`042`–`062`, `064`–`069`; gaps `22`–`31`, `39`–`41`, `63` are reserved/
|
|
36
|
+
consolidated — **do not reuse gap numbers** (see
|
|
37
|
+
[Database reference](../reference/database.md)). Custom migrations get
|
|
38
|
+
their own files in `custom/migrations/`; filenames are tracked verbatim
|
|
39
|
+
in `_spine_migrations`, so keep them globally unique.
|
|
40
|
+
|
|
41
|
+
3. **Write idempotent SQL.** Migrations run once per filename — but
|
|
42
|
+
installs re-run them against databases in unknown drifted states, so
|
|
43
|
+
guard everything:
|
|
44
|
+
|
|
45
|
+
```sql
|
|
46
|
+
ALTER TABLE public.my_table
|
|
47
|
+
ADD COLUMN IF NOT EXISTS my_col text;
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_my_table_col
|
|
50
|
+
ON public.my_table (my_col);
|
|
51
|
+
|
|
52
|
+
INSERT INTO public.roles (slug, name, is_active)
|
|
53
|
+
SELECT 'my_role', 'My Role', true
|
|
54
|
+
WHERE NOT EXISTS (SELECT 1 FROM public.roles WHERE slug = 'my_role');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`,
|
|
58
|
+
`DROP … IF EXISTS`, `ON CONFLICT … DO NOTHING`, and
|
|
59
|
+
`INSERT … SELECT … WHERE NOT EXISTS` are the house patterns (see
|
|
60
|
+
`.framework/migrations/053_builtin_action_seeds.sql`).
|
|
61
|
+
|
|
62
|
+
4. **Apply it.**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx spine-framework migrate # framework + custom
|
|
66
|
+
npx spine-framework migrate --dry-run # list pending only
|
|
67
|
+
npx spine-framework migrate --framework-only
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Requires `SUPABASE_URL` (a hosted `https://<ref>.supabase.co` URL — the
|
|
71
|
+
CLI rejects anything else before connecting) and
|
|
72
|
+
`SUPABASE_DB_PASSWORD` or `--db-password`
|
|
73
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
74
|
+
|
|
75
|
+
5. **Check state.** `npx spine-framework migrations list` reads the
|
|
76
|
+
`_spine_migrations` ledger; `migrations status` diffs local files vs
|
|
77
|
+
applied — see [CLI reference](../reference/cli.md).
|
|
78
|
+
|
|
79
|
+
## Verify
|
|
80
|
+
|
|
81
|
+
- `_spine_migrations` contains the filename: each file runs in a
|
|
82
|
+
transaction, records `INSERT INTO _spine_migrations (filename)`, and the
|
|
83
|
+
runner **stops at the first failure** — fix and re-run
|
|
84
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
85
|
+
- Re-running `migrate` reports the file as skipped.
|
|
86
|
+
|
|
87
|
+
## Common mistakes
|
|
88
|
+
|
|
89
|
+
- **Renaming a file after it ran.** Tracking is by filename — a rename
|
|
90
|
+
makes it a new migration and re-applies the SQL.
|
|
91
|
+
- **Non-idempotent DDL.** `ALTER TABLE … ADD COLUMN` without
|
|
92
|
+
`IF NOT EXISTS` fails on the second run (e.g. a fresh install that
|
|
93
|
+
already got the column from a later reconciliation migration).
|
|
94
|
+
- **Gap numbers.** `022`–`031`, `039`–`041`, `063` are reserved — using
|
|
95
|
+
them collides with framework history.
|
|
96
|
+
- **Depending on framework ordering.** Custom files all sort *after* every
|
|
97
|
+
framework file; you cannot interleave a custom migration between
|
|
98
|
+
framework ones — express dependencies inside the SQL (e.g.
|
|
99
|
+
`CREATE OR REPLACE`, existence checks).
|
|
100
|
+
- **`SUPABASE_URL` with a port, path, or credentials** — rejected by the
|
|
101
|
+
CLI before it ever connects.
|
|
102
|
+
|
|
103
|
+
## Reference
|
|
104
|
+
|
|
105
|
+
- [Database reference](../reference/database.md) — table map, numbering
|
|
106
|
+
gaps
|
|
107
|
+
- [CLI reference](../reference/cli.md) — `migrate`, `migrations list`,
|
|
108
|
+
`migrations status`
|
|
109
|
+
- [Package and install an app](./package-and-install-an-app.md) — app
|
|
110
|
+
installers run `migrate` automatically before seeding
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/pages/admin/TestingDashboard.tsx
|
|
9
|
+
- .framework/src/pages/admin/TestRunDetailPage.tsx
|
|
10
|
+
- .framework/functions/tests.ts
|
|
11
|
+
- .framework/migrations/001_core_schema.sql
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this is for
|
|
15
|
+
|
|
16
|
+
The **Testing** page (`/spine-framework/admin/testing`) is a dashboard over `test_runs`/`test_results` — rows written by external test harnesses (e.g. suite runs triggered from the IDE). It summarizes per-suite health and lists recent runs so you can spot a red suite without opening the CI output.
|
|
17
|
+
|
|
18
|
+
## Fields and settings
|
|
19
|
+
|
|
20
|
+
- **Suite cards** — one per suite (`unit`, `integration`, `api`, `ui`): last status, last run time, last pass/fail/total counts, and total runs (last 30 days).
|
|
21
|
+
- **Recent runs table** — last 20 runs: suite, status (`passed`/`failed`/`running`/etc.), started time, passed, failed, total, duration.
|
|
22
|
+
- **Row click** — navigates to `testing/:run_id` for the individual test-case results (`name`, `status`, `message`, `duration_ms` per case).
|
|
23
|
+
- No actions on this page — runs are produced by harnesses, not created here.
|
|
24
|
+
|
|
25
|
+
**Downstream effects**
|
|
26
|
+
|
|
27
|
+
- Backed by `/api/tests` (function `tests`): `action=stats` aggregates the last 30 days by suite; `action=list` returns recent runs; `action=get&id=` returns one run plus its ordered `test_results`. All require system context.
|
|
28
|
+
- `system` health checks (`/api/system`) and `spine-framework doctor` also read `test_runs` — a stale suite shows up in deploy health.
|
|
29
|
+
- Rows are append-only; there is no cleanup action on this API today.
|
|
30
|
+
|
|
31
|
+
## Minimal configuration
|
|
32
|
+
|
|
33
|
+
Nothing on the page — have your harness insert `test_runs` (+ `test_results` per case) and the dashboard populates itself.
|
|
34
|
+
|
|
35
|
+
## Full configuration
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
GET /.netlify/functions/tests?action=stats # per-suite 30-day rollup
|
|
39
|
+
GET /.netlify/functions/tests?action=list&limit=20
|
|
40
|
+
GET /.netlify/functions/tests?action=get&id=<run uuid> # run + ordered test_results
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
A nightly job runs the `integration` suite and writes a `test_runs` row with `failed: 2`. The dashboard's Integration card flips red with last-run counts; click the row to see which two `test_results` failed and their messages.
|
|
46
|
+
|
|
47
|
+
## Core files
|
|
48
|
+
|
|
49
|
+
- `.framework/src/pages/admin/TestingDashboard.tsx` — this page
|
|
50
|
+
- `.framework/src/pages/admin/TestRunDetailPage.tsx` — run detail route (`testing/:run_id`)
|
|
51
|
+
- `.framework/functions/tests.ts` — API (`stats`, `list`, `get`)
|
|
52
|
+
- Tables: `public.test_runs`, `public.test_results`
|
|
53
|
+
|
|
54
|
+
## Agent notes
|
|
55
|
+
|
|
56
|
+
- Known mismatch: `TestRunDetailPage` fetches `/api/test-runs?action=...`, but the shipped function is `tests` — the detail route currently fails to load. Use `tests?action=get&id=` until the page is repointed.
|
|
57
|
+
- The suites list is hardcoded (`unit`, `integration`, `api`, `ui`) — a run with a different `suite` value won't get a card, though it still appears in Recent runs.
|
|
58
|
+
- `stats` is a fixed 30-day window capped at 200 rows — high-volume suites need direct table queries for longer trends.
|
|
59
|
+
- Nothing here triggers runs; the "no runs yet" empty state points at running suites from your IDE/CI.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Timers
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: b3b3ae0
|
|
6
|
+
verified_on: 2026-11-01
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/timers.ts
|
|
9
|
+
- .framework/functions/system-cron.ts
|
|
10
|
+
- .framework/functions/trigger-scheduler.ts
|
|
11
|
+
- .framework/src/pages/admin/TimersPage.tsx
|
|
12
|
+
- .framework/src/pages/admin/TimerDetailPage.tsx
|
|
13
|
+
- .framework/migrations/068_cron_schedule_timer_rpcs.sql
|
|
14
|
+
- .framework/migrations/020_schema_reconciliation.sql
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What this is for
|
|
18
|
+
|
|
19
|
+
A **timer** is a scheduled job that invokes a pipeline when it comes due — Spine's recurring/one-off background task primitive. `system-cron` (invoked every minute by the `trigger-scheduler` Netlify scheduled function) asks the `get_due_timers` RPC for due timers and runs each one's pipeline.
|
|
20
|
+
|
|
21
|
+
## Fields and settings
|
|
22
|
+
|
|
23
|
+
- **Name** — string. Display name; required (`name` and `timer_type` are the API's required fields).
|
|
24
|
+
- **Type** — select: `interval`, `cron`, `once`. **Stored but not read by the scheduler** — it labels intent only; see Downstream effects.
|
|
25
|
+
- **App ID** — select, optional. Owning app; blank = account-scoped.
|
|
26
|
+
- **Pipeline ID** — select. The pipeline invoked when the timer fires. `get_due_timers` skips rows where `pipeline_id IS NULL`, so a blank pipeline means the timer never runs.
|
|
27
|
+
- **Active** — checkbox. Inactive timers are skipped by `get_due_timers`.
|
|
28
|
+
- **Description** — string, optional.
|
|
29
|
+
- **Config (JSON)** — the schedule. The only key the rescheduler reads today is `interval_minutes` (minutes between runs). The form seeds `{ "schedule": "", "interval_seconds": 3600 }` — neither key is honored by the current RPCs.
|
|
30
|
+
- **Metadata (JSON)** — optional metadata.
|
|
31
|
+
|
|
32
|
+
**Downstream effects**
|
|
33
|
+
|
|
34
|
+
- `get_due_timers` returns active timers with `pipeline_id IS NOT NULL` and `next_execution <= now()`. The timers API arms `next_execution` automatically on create/activate when `config.interval_minutes` (or `interval_seconds`) resolves to a positive interval — or pass `next_execution` explicitly.
|
|
35
|
+
- `update_timer_after_run` reschedules `next_execution = now() + interval` only when `config.interval_minutes` is a positive number; otherwise it sets `next_execution = NULL` and the timer becomes one-shot/dead. `timer_type`, `interval_seconds`, `schedule`, and `run_at` are ignored by the RPC.
|
|
36
|
+
- Each firing also stamps `last_execution` and increments `execution_count` / `success_count` / `failure_count` — the source of the list page's Executions and Success Rate columns.
|
|
37
|
+
- True cron expressions live on the separate `public.schedules` table (read by `get_due_schedules`), not on `timers` — a `cron`-type timer is a label, not a working cron.
|
|
38
|
+
|
|
39
|
+
## Minimal configuration
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"name": "Hourly sync",
|
|
44
|
+
"timer_type": "interval",
|
|
45
|
+
"pipeline_id": "<pipeline uuid>",
|
|
46
|
+
"is_active": true,
|
|
47
|
+
"config": { "interval_minutes": 60 }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Full configuration
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "Nightly-ish rollup",
|
|
56
|
+
"timer_type": "interval",
|
|
57
|
+
"pipeline_id": "<pipeline uuid>",
|
|
58
|
+
"is_active": true,
|
|
59
|
+
"config": { "interval_minutes": 1440 },
|
|
60
|
+
"metadata": { "owner": "platform", "notes": "fires ~daily once next_execution is seeded" }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
A support app keeps an `interval` timer with `interval_minutes: 360` to re-check stale tickets every six hours, and a second timer whose `interval_minutes` was left blank to run a one-off backfill — after its first fire, `update_timer_after_run` writes `next_execution = NULL` so it never re-fires.
|
|
67
|
+
|
|
68
|
+
## Core files
|
|
69
|
+
|
|
70
|
+
- `.framework/functions/timers.ts` — API (list/get/create/update/delete; does not set `next_execution`)
|
|
71
|
+
- `.framework/functions/system-cron.ts` — per-tick driver: calls `get_due_timers`, runs `runPipeline` as a `timer:<id>` principal, then `update_timer_after_run`
|
|
72
|
+
- `.framework/functions/trigger-scheduler.ts` — Netlify scheduled entry point that delegates to `system-cron`
|
|
73
|
+
- `.framework/migrations/068_cron_schedule_timer_rpcs.sql` — the `get_due_timers` / `update_timer_after_run` RPCs
|
|
74
|
+
- `.framework/src/pages/admin/TimersPage.tsx` / `TimerDetailPage.tsx` — admin UI
|
|
75
|
+
- Table: `public.timers` (`timer_type`, `pipeline_id`, `next_execution`, `execution_count` added by migration 020)
|
|
76
|
+
|
|
77
|
+
## Agent notes
|
|
78
|
+
|
|
79
|
+
- The list's "Next Execution" column shows `Not scheduled` when `next_execution` is NULL — an active timer in that state cannot fire until it is re-armed (edit its interval or toggle it off/on).
|
|
80
|
+
- A timer with a blank `pipeline_id` saves fine but is filtered out by `get_due_timers` — always pick the pipeline.
|
|
81
|
+
- **Under `netlify dev`, timers arm but never fire.** Netlify scheduled functions only run on deployed sites — locally, `trigger-scheduler` is loaded but never invoked on schedule. Emulate a tick with `netlify functions:invoke trigger-scheduler` or `curl -X POST localhost:8888/.netlify/functions/trigger-scheduler -H 'x-scheduler-key: <SCHEDULER_API_KEY>'`. A timer that schedules but never executes locally is an environment limitation, not a defect — unless manual invoke also fails.
|
|
82
|
+
- Trigger dispatch requires `INTERNAL_INVOKE_SECRET` set **and a dev-server restart** — a queued execution that never runs usually means the secret is missing (see [Add a trigger](tasks/add-a-trigger.md)).
|
|
83
|
+
- To arm a new timer, set `next_execution` in the DB (e.g. `now() + interval`); after the first successful run, `interval_minutes` keeps it rescheduling.
|
|
84
|
+
- "Success Rate" is `success_count / execution_count`; failures still reschedule — a broken pipeline does not stop the cadence.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Triggers
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/triggers.ts
|
|
9
|
+
- .framework/functions/_shared/trigger-engine.ts
|
|
10
|
+
- .framework/functions/trigger-scheduler.ts
|
|
11
|
+
- .framework/src/pages/admin/TriggersPage.tsx
|
|
12
|
+
- .framework/src/pages/admin/TriggerDetailPage.tsx
|
|
13
|
+
- .framework/migrations/064_episode_leases.sql
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What this is for
|
|
17
|
+
|
|
18
|
+
A **trigger** listens for a Spine event and, when active, hands the event payload to a pipeline for processing. It is the link between emitted platform events (e.g. `item_created`) and the workflows that react to them.
|
|
19
|
+
|
|
20
|
+
## Fields and settings
|
|
21
|
+
|
|
22
|
+
- **Name** — string. Display name; missing name prevents saving.
|
|
23
|
+
- **Event Type** — string. The event to listen to, e.g. `item_created`. Must match the emitted event name **exactly** — a typo means the trigger never fires.
|
|
24
|
+
- **App ID** — select, optional. Owning app; blank = account-scoped.
|
|
25
|
+
- **Pipeline ID** — select, required at create (`name`, `event_type`, `pipeline_id` are all required by the API). The pipeline invoked when the trigger fires; a stale ID means the queued execution fails instead of running.
|
|
26
|
+
- **Active** — checkbox. Inactive triggers are skipped at fire time — matching events produce no run.
|
|
27
|
+
- **Description** — string, optional.
|
|
28
|
+
- **Config (JSON)** — trigger-engine configuration, e.g. filters or payload mapping.
|
|
29
|
+
- **Metadata (JSON)** — optional metadata.
|
|
30
|
+
|
|
31
|
+
**Downstream effects**
|
|
32
|
+
|
|
33
|
+
- The trigger engine (`admin-data` writes → `_shared/trigger-engine`) matches emitted events to active triggers by `event_type`; real event names use underscores — `item_created`, `item_updated`, `item_deleted`, `thread_created`, `message_created`, etc. (see the `EventType` union in `_shared/trigger-engine.ts`).
|
|
34
|
+
- `config` filters narrow which matching events actually invoke the pipeline.
|
|
35
|
+
- A wrong or stale `pipeline_id` means the trigger fires, queues an execution row, and the run fails — check Pipeline Executions, not just the trigger, when "nothing happened".
|
|
36
|
+
|
|
37
|
+
## Minimal configuration
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"name": "On item created",
|
|
42
|
+
"event_type": "item_created",
|
|
43
|
+
"pipeline_id": "<pipeline uuid>",
|
|
44
|
+
"is_active": true
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Full configuration
|
|
49
|
+
|
|
50
|
+
Adds `config` with payload filters (e.g. match only `item_created` where `type_slug = "ticket"`), field mappings into the pipeline input, and `metadata` documenting the contract the event producer is expected to emit.
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
A support app fires `item_created` filtered to `type_slug = "ticket"`; the trigger invokes its triage pipeline, which assigns the ticket and posts a thread message. A second trigger on `item_updated` handles SLA warnings — both share the same pipeline pattern but different filters.
|
|
55
|
+
|
|
56
|
+
## Core files
|
|
57
|
+
|
|
58
|
+
- `.framework/functions/triggers.ts` — API
|
|
59
|
+
- `.framework/functions/_shared/trigger-engine.ts` — event matching and dispatch to `pipeline-runner-background`
|
|
60
|
+
- `.framework/functions/trigger-scheduler.ts` — Netlify scheduled entry point (delegates to `system-cron`)
|
|
61
|
+
- `.framework/src/pages/admin/TriggersPage.tsx` / `TriggerDetailPage.tsx` — admin UI
|
|
62
|
+
- Table: `public.triggers`
|
|
63
|
+
|
|
64
|
+
## Agent notes
|
|
65
|
+
|
|
66
|
+
- Event names are `<entity>_<past tense>` (`item_created`, `message_deleted`, ...) — the Event Type input's own placeholder still shows the stale `item.created` form; use underscores.
|
|
67
|
+
- `config` filters (`entity_type`, `type_slug`, `filters` with `$eq`/`$ne`/`$gt`/etc.) narrow which matching events actually invoke the pipeline.
|
|
68
|
+
- The list's "Total Fires" stat and "Fires"/"Last Fired" columns count trigger firings (the `record_trigger_fire` RPC), not successful pipeline runs — a trigger can fire while its pipeline fails.
|
|
69
|
+
- `config` must be a JSON **object**, not a string — the second most common save failure after event-type typos.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Types
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/types.ts
|
|
9
|
+
- .framework/src/pages/admin/TypesPage.tsx
|
|
10
|
+
- .framework/src/pages/admin/TypeDetailPage.tsx
|
|
11
|
+
- .framework/functions/_shared/permissions.ts
|
|
12
|
+
- .framework/functions/_shared/resolve-ids.ts
|
|
13
|
+
- .framework/functions/_shared/schema-utils.ts
|
|
14
|
+
- .framework/cli/commands/install-app.ts
|
|
15
|
+
- .framework/migrations/015_types_kind_slug_unique.sql
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What this is for
|
|
19
|
+
|
|
20
|
+
A **type** is the design schema for a runtime record kind. It defines the fields, default list/detail views, record permissions, icon, and color that the generic admin and runtime UIs use to create, validate, and display records. Every `items`/`threads`/`messages`/etc. record is stamped with its type's `design_schema` at creation.
|
|
21
|
+
|
|
22
|
+
## Fields and settings
|
|
23
|
+
|
|
24
|
+
- **Name** — string. Display label for the type. Empty name blocks save.
|
|
25
|
+
- **Slug** — string. URL-friendly identifier, unique per `kind`. Callers resolve types by slug; duplicates break `resolveTypeId`/`.single()` lookups. Rarely change after records exist.
|
|
26
|
+
- **Description** — string, optional. Human-readable context.
|
|
27
|
+
- **Kind** — select: `item`, `account`, `person`, `thread`, `message`, `link`, `attachment`, `watcher`. Determines which runtime table the type governs.
|
|
28
|
+
- **Ownership** — select: `system` (platform types, protected from deletion), `app` (owned by an app), `tenant` (account-specific).
|
|
29
|
+
- **Is Active** — checkbox. Inactive types are hidden from lists and cannot have records created.
|
|
30
|
+
- **App ID** — select. Required whenever ownership is not `system` — the form rejects save otherwise.
|
|
31
|
+
- **Icon / Color** — UI presentation stored on the `types` row.
|
|
32
|
+
- **Design Schema (JSON)** — the full schema: `fields`, `views` (`default_list`, `default_detail`), `record_permissions`, `scope`, `functionality`.
|
|
33
|
+
- **Validation Schema** — read-only, regenerated from `design_schema` on every save by the API.
|
|
34
|
+
|
|
35
|
+
**Downstream effects**
|
|
36
|
+
|
|
37
|
+
- Generic list/detail pages render directly from `views`. A missing `default_list`/`default_detail` or unknown `display_type` breaks those pages.
|
|
38
|
+
- `record_permissions` is the **base, most restrictive grant**: if a role can read/update a record, all fields default to read/write. `fields.*.permissions` only *loosen* access per field or restrict `system: true` fields.
|
|
39
|
+
- `system: true` fields are read from `record.<field>`; non-system fields from `record.data.<field>`.
|
|
40
|
+
- Changing `data_type` on a field with existing records does not migrate data — old values remain until rewritten.
|
|
41
|
+
- Type seeds use `conflict: "kind,slug"` with `inject_app_id: false` — types are global, not duplicated per app install.
|
|
42
|
+
|
|
43
|
+
## Minimal configuration
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"scope": "account",
|
|
48
|
+
"fields": {
|
|
49
|
+
"title": { "data_type": "text", "label": "Title", "required": true, "system": true }
|
|
50
|
+
},
|
|
51
|
+
"views": {
|
|
52
|
+
"default_list": {
|
|
53
|
+
"type": "list", "display": "table", "label": "List",
|
|
54
|
+
"fields": {
|
|
55
|
+
"title": { "sortable": true, "display_type": "text" },
|
|
56
|
+
"created_at": { "sortable": true, "display_type": "timestamp" }
|
|
57
|
+
},
|
|
58
|
+
"default_sort": { "field": "created_at", "direction": "desc" }
|
|
59
|
+
},
|
|
60
|
+
"default_detail": {
|
|
61
|
+
"type": "detail", "label": "Detail",
|
|
62
|
+
"sections": [{ "title": "Details", "fields": { "title": { "display_type": "input" } } }]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"record_permissions": { "system_admin": ["create", "read", "update", "delete"] },
|
|
66
|
+
"functionality": null
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Full configuration
|
|
71
|
+
|
|
72
|
+
Adds typed fields, select `options`, per-field `permissions` overrides, multiple views with sorting/badges, and per-role record grants (`admin`, `member`). See the [design schema reference](reference/design-schema.md) for the full `data_type` list (`text`, `number`, `select`, `multiselect`, `reference`, `json`, `date`, `boolean`, ...) and `display_type` list (`badge`, `timestamp`, `select`, `currency`, ...).
|
|
73
|
+
|
|
74
|
+
## Example
|
|
75
|
+
|
|
76
|
+
A support app defines a `ticket` type (kind `item`, ownership `app`): `title` (system text), `status` (select: open/pending/closed), `priority` (select), `assignee_id` (reference). `record_permissions` grants `system_admin` all actions, `admin` create/read/update, `member` read. `fields.assignee_id.permissions` loosens `write` to `member` so agents can self-assign.
|
|
77
|
+
|
|
78
|
+
## Core files
|
|
79
|
+
|
|
80
|
+
- `.framework/functions/types.ts` — API (list/get/create/update/delete; regenerates `validation_schema`)
|
|
81
|
+
- `.framework/src/pages/admin/TypesPage.tsx` / `TypeDetailPage.tsx` — admin UI
|
|
82
|
+
- `.framework/src/components/app-shell/GenericListPage.tsx` / `GenericDetailPage.tsx` — generic renderer
|
|
83
|
+
- `.framework/functions/_shared/permissions.ts` — record/field permission evaluation
|
|
84
|
+
- [Design schema reference](reference/design-schema.md) — full field/view schema reference in this book
|
|
85
|
+
- Table: `public.types`
|
|
86
|
+
|
|
87
|
+
## Agent notes
|
|
88
|
+
|
|
89
|
+
- Never hand-edit `validation_schema`; change `design_schema` and the API regenerates it.
|
|
90
|
+
- Slug uniqueness is per `(kind, slug)` — reuse across kinds is fine, reuse within a kind breaks lookups.
|
|
91
|
+
- Every field needs `data_type`, `label`, `required`; `select`/`multiselect`/`radio` also need `options`.
|
|
92
|
+
- Don't add `fields.*.permissions` unless overriding the base grant — the common mistake is re-stating record-level permissions per field.
|
|
93
|
+
- Custom apps CRUD runtime records through `/api/admin-data` — never custom wrappers.
|