spine-framework 0.4.3 → 0.5.0-rc.4
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/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-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/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +48 -31
- package/.framework/functions/ai-agents.ts +103 -33
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- 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 +145 -76
- 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 +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- 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 +250 -128
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- 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/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- 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 +92 -81
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +304 -179
- 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/hooks/useBranding.ts +47 -0
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -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 +162 -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/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.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 +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -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 +150 -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/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -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-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/package-and-install-an-app.md +127 -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 +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/SetupPage.tsx +5 -2
- 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/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/agent/AGENTS.md +150 -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 +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -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 +5 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +496 -47
- 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/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/episode-lease.test.ts +695 -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/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -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/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/netlify.toml +5 -0
- 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
|
@@ -262,6 +262,47 @@ interface PipelineStage {
|
|
|
262
262
|
* @errorHandling Handles errors at each stage and decides whether to continue based on stage configuration.
|
|
263
263
|
* @cil-generated
|
|
264
264
|
*/
|
|
265
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/pipeline-runner.ts:persistExecutionResult:function" type="function" identity="persistExecutionResult" parent="~/.framework/functions/_shared/pipeline-runner.ts"]
|
|
266
|
+
/**
|
|
267
|
+
* Writes the terminal status of a pipeline execution. The update result
|
|
268
|
+
* is checked and retried once — an unchecked failure is what previously
|
|
269
|
+
* left rows stuck 'running' forever while the audit span still claimed
|
|
270
|
+
* 'pipeline.completed'. A retry failure is logged loudly so the row can
|
|
271
|
+
* be found and reconciled.
|
|
272
|
+
*
|
|
273
|
+
* @inputSpec
|
|
274
|
+
* ctx: CoreContext (required) — context for request-id logging.
|
|
275
|
+
* executionId: string (required) — the pipeline_executions row to update.
|
|
276
|
+
* patch: Record<string, any> (required) — terminal status payload.
|
|
277
|
+
* @outputSpec Promise<void>
|
|
278
|
+
* @sideEffects Updates public.pipeline_executions; logs on failure.
|
|
279
|
+
* @stability stable
|
|
280
|
+
* @calls adminDb.from('pipeline_executions').update
|
|
281
|
+
* @cil-generated
|
|
282
|
+
*/
|
|
283
|
+
async function persistExecutionResult(
|
|
284
|
+
ctx: CoreContext,
|
|
285
|
+
executionId: string,
|
|
286
|
+
patch: Record<string, any>
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
let { error } = await adminDb
|
|
289
|
+
.from('pipeline_executions')
|
|
290
|
+
.update(patch)
|
|
291
|
+
.eq('id', executionId)
|
|
292
|
+
if (error) {
|
|
293
|
+
console.error(`[${ctx.requestId}] pipeline_executions final update failed for ${executionId}: ${error.message} — retrying`)
|
|
294
|
+
const retry = await adminDb
|
|
295
|
+
.from('pipeline_executions')
|
|
296
|
+
.update(patch)
|
|
297
|
+
.eq('id', executionId)
|
|
298
|
+
error = retry.error
|
|
299
|
+
if (error) {
|
|
300
|
+
console.error(`[${ctx.requestId}] pipeline_executions final update retry failed for ${executionId}: ${error.message}`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// CIL[END stable_id="~/.framework/functions/_shared/pipeline-runner.ts:persistExecutionResult:function"]
|
|
305
|
+
|
|
265
306
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/pipeline-runner.ts:runpipeline:function" type="function" identity="runPipeline" parent="~/.framework/functions/_shared/pipeline-runner.ts"]
|
|
266
307
|
// CIL[DOC-MACRO purpose="runPipeline function"]
|
|
267
308
|
// CIL[DOC-RESPONSIBILITY responsibility="Implements the runPipeline function."]
|
|
@@ -270,14 +311,40 @@ interface PipelineStage {
|
|
|
270
311
|
// CIL[DOC-MICRO logic_summary="Implements the runPipeline function." notable_decisions="[]"]
|
|
271
312
|
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
272
313
|
// CIL[DOC-STABILITY stability="stable"]
|
|
314
|
+
// An execution row whose runner died mid-flight (detached process killed,
|
|
315
|
+
// function timeout) can never write its own terminal status — it wedges
|
|
316
|
+
// 'running' forever. Sweep rows that have not been touched within the
|
|
317
|
+
// staleness window whenever a new pipeline run starts. Fire-and-forget:
|
|
318
|
+
// the sweep is bookkeeping, never on the critical path, and a failed
|
|
319
|
+
// sweep retries on the next run.
|
|
320
|
+
const STALE_EXECUTION_MS = 10 * 60 * 1000
|
|
321
|
+
|
|
322
|
+
export async function sweepStaleExecutions(): Promise<void> {
|
|
323
|
+
const cutoff = new Date(Date.now() - STALE_EXECUTION_MS).toISOString()
|
|
324
|
+
const { error } = await adminDb
|
|
325
|
+
.from('pipeline_executions')
|
|
326
|
+
.update({
|
|
327
|
+
status: 'failed',
|
|
328
|
+
error_message: 'stale execution: runner died before terminal write (swept)',
|
|
329
|
+
completed_at: new Date().toISOString(),
|
|
330
|
+
updated_at: new Date().toISOString()
|
|
331
|
+
})
|
|
332
|
+
.in('status', ['running', 'pending', 'queued'])
|
|
333
|
+
.lt('updated_at', cutoff)
|
|
334
|
+
if (error) console.warn('pipeline_executions stale sweep failed:', error.message)
|
|
335
|
+
}
|
|
336
|
+
|
|
273
337
|
export async function runPipeline(
|
|
274
338
|
pipelineId: string,
|
|
275
339
|
triggerData: any,
|
|
276
|
-
ctx: CoreContext
|
|
340
|
+
ctx: CoreContext,
|
|
341
|
+
opts?: { executionId?: string }
|
|
277
342
|
): Promise<ExecutionResult> {
|
|
278
343
|
return withSpan(ctx, 'pipeline.run', async (ctx) => {
|
|
279
344
|
const startTime = Date.now()
|
|
280
345
|
|
|
346
|
+
void sweepStaleExecutions().catch(() => {})
|
|
347
|
+
|
|
281
348
|
const { data: pipeline, error: pipelineError } = await adminDb
|
|
282
349
|
.from('pipelines')
|
|
283
350
|
.select('*')
|
|
@@ -289,24 +356,76 @@ export async function runPipeline(
|
|
|
289
356
|
throw new Error(`Pipeline not found or inactive: ${pipelineId}`)
|
|
290
357
|
}
|
|
291
358
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
359
|
+
// When a pre-created execution ID is supplied (dispatched trigger
|
|
360
|
+
// pipelines), claim that row — transitioning queued → running — rather
|
|
361
|
+
// than inserting a duplicate. The row was created by checkAndFireTriggers
|
|
362
|
+
// with status='queued' immediately before dispatch.
|
|
363
|
+
let executionId: string
|
|
364
|
+
if (opts?.executionId) {
|
|
365
|
+
const { data: claimed, error: claimError } = await adminDb
|
|
366
|
+
.from('pipeline_executions')
|
|
367
|
+
.update({
|
|
368
|
+
status: 'running',
|
|
369
|
+
trigger_data: triggerData || {},
|
|
370
|
+
started_at: new Date().toISOString(),
|
|
371
|
+
updated_at: new Date().toISOString()
|
|
372
|
+
})
|
|
373
|
+
.eq('id', opts.executionId)
|
|
374
|
+
.eq('status', 'queued')
|
|
375
|
+
.select('id')
|
|
304
376
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
377
|
+
if (claimError) {
|
|
378
|
+
throw new Error(`Failed to claim queued execution ${opts.executionId}: ${claimError.message}`)
|
|
379
|
+
}
|
|
380
|
+
if (claimed && claimed.length > 0) {
|
|
381
|
+
executionId = opts.executionId
|
|
382
|
+
} else {
|
|
383
|
+
// Claimed nothing — either the queued insert never landed (caller's
|
|
384
|
+
// fallback UUID) or a duplicate/swept dispatch arrived. Try inserting
|
|
385
|
+
// the row ourselves with the same ID: succeeds only when the row is
|
|
386
|
+
// genuinely absent, so duplicate deliveries die on the PK conflict
|
|
387
|
+
// instead of running the pipeline twice.
|
|
388
|
+
const { data: execution, error: execError } = await adminDb
|
|
389
|
+
.from('pipeline_executions')
|
|
390
|
+
.insert({
|
|
391
|
+
id: opts.executionId,
|
|
392
|
+
pipeline_id: pipelineId,
|
|
393
|
+
status: 'running',
|
|
394
|
+
trigger_data: triggerData || {},
|
|
395
|
+
account_id: ctx.accountId,
|
|
396
|
+
created_by: isUuid(ctx.principal?.id) ? ctx.principal?.id : null,
|
|
397
|
+
started_at: new Date().toISOString(),
|
|
398
|
+
updated_at: new Date().toISOString()
|
|
399
|
+
})
|
|
400
|
+
.select()
|
|
401
|
+
.single()
|
|
402
|
+
if (execError || !execution) {
|
|
403
|
+
throw new Error(`Failed to create execution: ${execError?.message}`)
|
|
404
|
+
}
|
|
405
|
+
executionId = execution.id
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
const { data: execution, error: execError } = await adminDb
|
|
409
|
+
.from('pipeline_executions')
|
|
410
|
+
.insert({
|
|
411
|
+
pipeline_id: pipelineId,
|
|
412
|
+
status: 'running',
|
|
413
|
+
trigger_data: triggerData || {},
|
|
414
|
+
account_id: ctx.accountId,
|
|
415
|
+
created_by: isUuid(ctx.principal?.id) ? ctx.principal?.id : null,
|
|
416
|
+
started_at: new Date().toISOString(),
|
|
417
|
+
// Seed updated_at so the stale-execution sweep has a baseline from
|
|
418
|
+
// t=0 — the row is 'running' the moment it exists.
|
|
419
|
+
updated_at: new Date().toISOString()
|
|
420
|
+
})
|
|
421
|
+
.select()
|
|
422
|
+
.single()
|
|
308
423
|
|
|
309
|
-
|
|
424
|
+
if (execError || !execution) {
|
|
425
|
+
throw new Error(`Failed to create execution: ${execError?.message}`)
|
|
426
|
+
}
|
|
427
|
+
executionId = execution.id
|
|
428
|
+
}
|
|
310
429
|
const stageResults: StageResult[] = []
|
|
311
430
|
const stages: PipelineStage[] = pipeline.stages || []
|
|
312
431
|
|
|
@@ -365,15 +484,12 @@ export async function runPipeline(
|
|
|
365
484
|
|
|
366
485
|
const durationMs = Date.now() - startTime
|
|
367
486
|
|
|
368
|
-
await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
duration_ms: durationMs
|
|
375
|
-
})
|
|
376
|
-
.eq('id', executionId)
|
|
487
|
+
await persistExecutionResult(ctx, executionId, {
|
|
488
|
+
status: 'completed',
|
|
489
|
+
result: { stages: stageResults },
|
|
490
|
+
completed_at: new Date().toISOString(),
|
|
491
|
+
duration_ms: durationMs
|
|
492
|
+
})
|
|
377
493
|
|
|
378
494
|
await emitAudit(ctx, 'pipeline.completed', {
|
|
379
495
|
type: 'pipeline_execution',
|
|
@@ -402,16 +518,13 @@ export async function runPipeline(
|
|
|
402
518
|
} catch (error: any) {
|
|
403
519
|
const durationMs = Date.now() - startTime
|
|
404
520
|
|
|
405
|
-
await
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
duration_ms: durationMs
|
|
413
|
-
})
|
|
414
|
-
.eq('id', executionId)
|
|
521
|
+
await persistExecutionResult(ctx, executionId, {
|
|
522
|
+
status: 'failed',
|
|
523
|
+
result: { stages: stageResults },
|
|
524
|
+
error_message: error.message,
|
|
525
|
+
completed_at: new Date().toISOString(),
|
|
526
|
+
duration_ms: durationMs
|
|
527
|
+
})
|
|
415
528
|
|
|
416
529
|
await emitAudit(ctx, 'pipeline.failed', {
|
|
417
530
|
type: 'pipeline_execution',
|
|
@@ -1192,7 +1305,7 @@ const stageHandlers: Record<string, Function> = {
|
|
|
1192
1305
|
if (!itemId) throw new Error('kb_ingest: could not resolve item_id from trigger data')
|
|
1193
1306
|
|
|
1194
1307
|
const { kbIngest } = await import('./kb')
|
|
1195
|
-
return await kbIngest(ctx, { id: itemId })
|
|
1308
|
+
return await kbIngest(ctx, { id: itemId, account_id: triggerData?.entity?.data?.account_id })
|
|
1196
1309
|
},
|
|
1197
1310
|
|
|
1198
1311
|
'kb_revoke': async (ctx: CoreContext, config: any) => {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { getUserDb, adminDb } from './db'
|
|
29
29
|
import { createClient } from '@supabase/supabase-js'
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
import { timingSafeEqual, createHash } from 'crypto'
|
|
32
32
|
// CIL[DOC-VECTOR type="context" content="purpose='Shared principal and authorization module for Spine functions'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
|
|
33
33
|
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
|
|
@@ -96,10 +96,10 @@ const env = (globalThis as any).process?.env || {}
|
|
|
96
96
|
|
|
97
97
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/principal.ts:supabase:constant" type="constant" identity="supabase" parent="~/.framework/functions/_shared/principal.ts"]
|
|
98
98
|
// CIL[DOC-MACRO purpose="Create a Supabase client configured for function use"]
|
|
99
|
-
// CIL[DOC-RESPONSIBILITY responsibility="Provide an
|
|
99
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Provide an anon-key Supabase client for auth.getUser JWT verification"]
|
|
100
100
|
// CIL[DOC-CONTEXT module="functions/_shared/principal" layer="shared" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/principal.ts"]
|
|
101
101
|
// CIL[DOC-CONTRACT inputs="none" outputs="Supabase client" errors="none" side_effects="none"]
|
|
102
|
-
// CIL[DOC-MICRO logic_summary="Instantiate createClient with URL, anon key and auth
|
|
102
|
+
// CIL[DOC-MICRO logic_summary="Instantiate createClient with URL, anon key, and auth options. Realtime transport omitted — this client is used only for supabase.auth.getUser; no realtime channels are opened server-side." notable_decisions="ws realtime transport removed to reduce bundle size and eliminate unnecessary module-level init overhead across every function"]
|
|
103
103
|
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
104
104
|
// CIL[DOC-STABILITY stability="stable"]
|
|
105
105
|
const supabase = createClient(
|
|
@@ -107,10 +107,7 @@ const supabase = createClient(
|
|
|
107
107
|
env.SUPABASE_ANON_KEY || 'placeholder',
|
|
108
108
|
{
|
|
109
109
|
auth: { autoRefreshToken: false, persistSession: false },
|
|
110
|
-
realtime: { transport: WebSocket as any }
|
|
111
110
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
111
|
)
|
|
115
112
|
// CIL[DOC-VECTOR type="context" content="purpose='Create a Supabase client configured for function use'; No verified external callers; imports: .framework/functions/_shared/db.ts"]
|
|
116
113
|
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
|
|
@@ -269,6 +266,12 @@ export async function resolvePrincipal(event: any): Promise<Principal> {
|
|
|
269
266
|
if (triggerId) {
|
|
270
267
|
return resolveTriggerPrincipal(triggerId, event)
|
|
271
268
|
}
|
|
269
|
+
|
|
270
|
+
// A bare internal-invoke secret is itself a first-class credential —
|
|
271
|
+
// function-to-function calls without a cron/trigger principal to
|
|
272
|
+
// impersonate (e.g. manual system-cron runs) get the privileged
|
|
273
|
+
// system principal rather than falling through to anonymous.
|
|
274
|
+
return SYSTEM_PRINCIPAL
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
const authHeader = event.headers?.authorization || event.headers?.Authorization
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
import { CoreContext } from './middleware'
|
|
38
38
|
import { adminDb } from './db'
|
|
39
39
|
import { emitAudit } from './audit'
|
|
40
|
-
import { runPipeline } from './pipeline-runner'
|
|
41
40
|
// CIL[END stable_id="~/.framework/functions/_shared/trigger-engine.ts:module_header:module_header"]
|
|
42
41
|
// CIL[DOC-VECTOR type="context" content="purpose='This module serves as the trigger engine for processing events within the system, coordinating various actions based on incoming triggers.'; No verified external callers" status="ready"]
|
|
43
42
|
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
|
|
@@ -203,43 +202,102 @@ interface Trigger {
|
|
|
203
202
|
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
204
203
|
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
205
204
|
|
|
206
|
-
// CIL[DOC-MACRO purpose="
|
|
207
|
-
// CIL[DOC-RESPONSIBILITY responsibility="
|
|
208
|
-
// CIL[DOC-CONTEXT module="trigger-engine" layer="
|
|
209
|
-
// CIL[DOC-CONTRACT inputs="
|
|
210
|
-
// CIL[DOC-MICRO logic_summary="
|
|
211
|
-
// CIL[DOC-CONFIDENCE pass="
|
|
205
|
+
// CIL[DOC-MACRO purpose="Dispatch a trigger pipeline to the background pipeline-runner function, returning immediately without awaiting execution"]
|
|
206
|
+
// CIL[DOC-RESPONSIBILITY responsibility="POST the payload to /.netlify/functions/pipeline-runner-background with the internal invoke secret so the pipeline runs in an isolated process with its own connection budget"]
|
|
207
|
+
// CIL[DOC-CONTEXT module="trigger-engine" layer="shared-core" related_files=".framework/functions/pipeline-runner-background.ts"]
|
|
208
|
+
// CIL[DOC-CONTRACT inputs="pipelineId, triggerData, ctx, opts { executionId, triggerId }" outputs="Promise<void>" errors="Logs dispatch failure to console; caller's queued execution row is left for the sweeper" side_effects="HTTP POST to internal function"]
|
|
209
|
+
// CIL[DOC-MICRO logic_summary="Builds the dispatch URL from SITE_URL/URL env (falling back to localhost for netlify dev), sends X-Internal-Secret for machine auth, and awaits only the 202 ack bounded by a 5s AbortSignal timeout — the caller never waits for pipeline completion" notable_decisions="Does NOT await runPipeline — that was the connection-pool starvation bug; moving execution to a separate function invocation is the fix. The 5s timeout bounds the caller when Netlify's queue ack stalls; an aborted dispatch may still deliver server-side, and the pre-created 'queued' row is the sweeper's safety net either way"]
|
|
210
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
|
|
211
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
212
|
+
/**
|
|
213
|
+
* POST a pipeline dispatch to the internal background function.
|
|
214
|
+
* Returns immediately; the pipeline runs in a separate process.
|
|
215
|
+
* Any dispatch failure leaves the pre-created execution row 'queued' —
|
|
216
|
+
* the sweeper in pipeline-runner.ts marks it 'failed' after 10 minutes.
|
|
217
|
+
*/
|
|
218
|
+
export async function dispatchTriggerPipeline(
|
|
219
|
+
pipelineId: string,
|
|
220
|
+
triggerData: any,
|
|
221
|
+
ctx: CoreContext,
|
|
222
|
+
opts: { executionId: string; triggerId: string }
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const secret = (globalThis as any).process?.env?.INTERNAL_INVOKE_SECRET
|
|
225
|
+
if (!secret) {
|
|
226
|
+
console.error(`[${ctx.requestId}] INTERNAL_INVOKE_SECRET not set — cannot dispatch trigger pipeline ${pipelineId}`)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const siteUrl = (globalThis as any).process?.env?.SITE_URL ||
|
|
231
|
+
(globalThis as any).process?.env?.URL ||
|
|
232
|
+
'http://localhost:8888'
|
|
233
|
+
const url = `${siteUrl.replace(/\/$/, '')}/.netlify/functions/pipeline-runner-background`
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const response = await fetch(url, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: {
|
|
239
|
+
'Content-Type': 'application/json',
|
|
240
|
+
'X-Internal-Secret': secret,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
pipeline_id: pipelineId,
|
|
244
|
+
trigger_data: triggerData,
|
|
245
|
+
account_id: ctx.accountId,
|
|
246
|
+
execution_id: opts.executionId,
|
|
247
|
+
trigger_id: opts.triggerId,
|
|
248
|
+
}),
|
|
249
|
+
// Bound the caller's wait: the background function normally answers
|
|
250
|
+
// 202 in well under a second, but a stuck connection (deploy rollout,
|
|
251
|
+
// slow Netlify queue ack) once held a caller for ~34s. Aborting only
|
|
252
|
+
// releases this caller — if Netlify already received the POST the
|
|
253
|
+
// queued invocation still runs; if not, the pre-created 'queued'
|
|
254
|
+
// execution row is left for the sweeper.
|
|
255
|
+
signal: AbortSignal.timeout(5000),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const errText = await response.text().catch(() => `HTTP ${response.status}`)
|
|
260
|
+
console.error(`[${ctx.requestId}] trigger dispatch failed for pipeline ${pipelineId} (execution ${opts.executionId}): HTTP ${response.status} — ${errText}`)
|
|
261
|
+
}
|
|
262
|
+
} catch (err: any) {
|
|
263
|
+
const timedOut = err?.name === 'AbortError' || err?.name === 'TimeoutError'
|
|
264
|
+
console.error(`[${ctx.requestId}] trigger dispatch ${timedOut ? 'timed out after 5s' : 'error'} for pipeline ${pipelineId} (execution ${opts.executionId}):`, err.message || err)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// CIL[END stable_id="~/.framework/functions/_shared/trigger-engine.ts:dispatchtriggerpipeline:function"]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
// CIL[DOC-MACRO purpose="Check for active triggers matching the event type, pre-create a queued pipeline_executions row, and dispatch to the background function"]
|
|
271
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Evaluate trigger conditions, reserve a queued execution row, dispatch to pipeline-runner-background, record trigger fire, and emit audit — returns without waiting for pipeline completion"]
|
|
272
|
+
// CIL[DOC-CONTEXT module="trigger-engine" layer="shared-core" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/pipeline-runner-background.ts"]
|
|
273
|
+
// CIL[DOC-CONTRACT inputs="eventType, entityType, entityId, entityData, ctx, previousEntityData? (pre-update record snapshot for transition checks)" outputs="Promise<void>" errors="Logs errors; emits trigger.failed audit" side_effects="Inserts pipeline_executions row (status: queued), RPC record_trigger_fire, emits trigger.fired audit"]
|
|
274
|
+
// CIL[DOC-MICRO logic_summary="Queries active triggers for the event type, resolves type_slug when needed for trigger filters, evaluates each trigger's conditions, then for matches: INSERTs a queued pipeline_executions row so the execution ID exists before dispatch, calls dispatchTriggerPipeline (which returns immediately), records the trigger fire, and emits the trigger.fired audit with execution_status='queued'" notable_decisions="Pre-creating the execution row is the key to preserving execution_id in the audit trail without awaiting pipeline completion; 'queued' rows left orphaned are swept by the stale-execution reaper"]
|
|
275
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
|
|
212
276
|
// CIL[DOC-STABILITY stability="stable"]
|
|
213
277
|
/**
|
|
214
|
-
*
|
|
278
|
+
* Check for active triggers matching the event type and dispatch each to the
|
|
279
|
+
* background pipeline-runner function. Returns immediately — pipeline
|
|
280
|
+
* completion is tracked in pipeline_executions independently.
|
|
215
281
|
*
|
|
216
282
|
* @inputSpec
|
|
217
|
-
* - eventType: EventType
|
|
218
|
-
* - entityType: string
|
|
219
|
-
* - entityId: string
|
|
220
|
-
* - entityData: any
|
|
221
|
-
* - ctx: CoreContext
|
|
283
|
+
* - eventType: EventType — the event that occurred.
|
|
284
|
+
* - entityType: string — the entity type (plural runtime table name).
|
|
285
|
+
* - entityId: string — the affected entity ID.
|
|
286
|
+
* - entityData: any — the entity snapshot at the time of the event.
|
|
287
|
+
* - ctx: CoreContext — request context (account scope, request ID, principal).
|
|
222
288
|
* @outputSpec Promise<void>
|
|
223
|
-
* @sideEffects
|
|
289
|
+
* @sideEffects INSERTs pipeline_executions rows (status: queued),
|
|
290
|
+
* RPC record_trigger_fire per matching trigger,
|
|
291
|
+
* emits trigger.fired / trigger.failed audit events.
|
|
224
292
|
* @stability stable
|
|
225
|
-
* @calls adminDb.from('triggers'), adminDb.from('types'), runPipeline, emitAudit, adminDb.from('triggers').update
|
|
226
|
-
* @errorHandling Logs errors to the console and emits audit logs for failures.
|
|
227
|
-
* @cil-generated
|
|
228
293
|
*/
|
|
229
|
-
// CIL[BEGIN stable_id="~/.framework/functions/_shared/trigger-engine.ts:checkandfiretriggers:function" type="function" identity="checkAndFireTriggers" parent="~/.framework/functions/_shared/trigger-engine.ts"]
|
|
230
|
-
// CIL[DOC-MACRO purpose="checkAndFireTriggers function"]
|
|
231
|
-
// CIL[DOC-RESPONSIBILITY responsibility="Implements the checkAndFireTriggers function."]
|
|
232
|
-
// CIL[DOC-CONTEXT module=".framework/functions/_shared/trigger-engine.ts" layer="app-specific" related_files=""]
|
|
233
|
-
// CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
|
|
234
|
-
// CIL[DOC-MICRO logic_summary="Implements the checkAndFireTriggers function." notable_decisions="[]"]
|
|
235
|
-
// CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
|
|
236
|
-
// CIL[DOC-STABILITY stability="stable"]
|
|
237
294
|
export async function checkAndFireTriggers(
|
|
238
295
|
eventType: EventType,
|
|
239
296
|
entityType: string,
|
|
240
297
|
entityId: string,
|
|
241
298
|
entityData: any,
|
|
242
|
-
ctx: CoreContext
|
|
299
|
+
ctx: CoreContext,
|
|
300
|
+
previousEntityData?: any
|
|
243
301
|
): Promise<void> {
|
|
244
302
|
const { data: triggers, error } = await adminDb
|
|
245
303
|
.from('triggers')
|
|
@@ -270,11 +328,30 @@ export async function checkAndFireTriggers(
|
|
|
270
328
|
for (const trigger of triggers) {
|
|
271
329
|
try {
|
|
272
330
|
const shouldFire = evaluateTriggerConditions(trigger, entityType, entityData)
|
|
273
|
-
|
|
331
|
+
|
|
274
332
|
if (!shouldFire) {
|
|
275
333
|
continue
|
|
276
334
|
}
|
|
277
335
|
|
|
336
|
+
// Transition-only triggers (config.fire_on_transition): fire only when
|
|
337
|
+
// the record moves INTO the matching state. Without this, every
|
|
338
|
+
// update to an already-matching record re-fires the pipeline — e.g.
|
|
339
|
+
// each write to a published KB article re-dispatched auto_embed (3×
|
|
340
|
+
// per publish) and revoke fired on articles that never left
|
|
341
|
+
// 'published'. When no previous snapshot is available the trigger
|
|
342
|
+
// fires normally (conservative: matches pre-flag behavior).
|
|
343
|
+
if (trigger.config?.fire_on_transition && previousEntityData) {
|
|
344
|
+
const prevEntityData = {
|
|
345
|
+
...previousEntityData,
|
|
346
|
+
// Same record → same type; the slug was resolved on the new
|
|
347
|
+
// snapshot only, so carry it over for config.type_slug checks.
|
|
348
|
+
type_slug: previousEntityData.type_slug ?? entityData?.type_slug
|
|
349
|
+
}
|
|
350
|
+
if (evaluateTriggerConditions(trigger, entityType, prevEntityData)) {
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
278
355
|
const triggerData = {
|
|
279
356
|
event: eventType,
|
|
280
357
|
entity_id: entityId,
|
|
@@ -290,15 +367,46 @@ export async function checkAndFireTriggers(
|
|
|
290
367
|
fired_at: new Date().toISOString()
|
|
291
368
|
}
|
|
292
369
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
370
|
+
// Reserve the execution row before dispatch so the audit trail has a
|
|
371
|
+
// stable execution_id even though the pipeline has not started yet.
|
|
372
|
+
// The background function claims this row (queued → running) via
|
|
373
|
+
// runPipeline's opts.executionId parameter. If dispatch fails, this
|
|
374
|
+
// row stays 'queued' and the sweeper marks it 'failed' after 10 min.
|
|
375
|
+
const { data: execRow } = await adminDb
|
|
376
|
+
.from('pipeline_executions')
|
|
377
|
+
.insert({
|
|
378
|
+
pipeline_id: trigger.pipeline_id,
|
|
379
|
+
status: 'queued',
|
|
380
|
+
trigger_data: triggerData,
|
|
381
|
+
account_id: ctx.accountId,
|
|
382
|
+
// Same guard as runPipeline's own insert: created_by is uuid-typed,
|
|
383
|
+
// non-UUID principals (machine ids like 'system', 'timer:*') → null.
|
|
384
|
+
created_by: ctx.principal?.id &&
|
|
385
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ctx.principal.id)
|
|
386
|
+
? ctx.principal.id : null,
|
|
387
|
+
started_at: new Date().toISOString(),
|
|
388
|
+
updated_at: new Date().toISOString()
|
|
300
389
|
})
|
|
301
|
-
.
|
|
390
|
+
.select('id')
|
|
391
|
+
.single()
|
|
392
|
+
|
|
393
|
+
const executionId = execRow?.id ?? crypto.randomUUID()
|
|
394
|
+
|
|
395
|
+
await dispatchTriggerPipeline(trigger.pipeline_id, triggerData, ctx, {
|
|
396
|
+
executionId,
|
|
397
|
+
triggerId: trigger.id,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// Atomic bookkeeping: the previous update embedded an
|
|
401
|
+
// adminDb.rpc() Promise as a column value, so the whole write
|
|
402
|
+
// failed silently — trigger_count stayed 0 and last_triggered
|
|
403
|
+
// stayed null on every fire.
|
|
404
|
+
const { error: fireError } = await adminDb.rpc('record_trigger_fire', {
|
|
405
|
+
p_trigger_id: trigger.id
|
|
406
|
+
})
|
|
407
|
+
if (fireError) {
|
|
408
|
+
console.error(`[${ctx.requestId}] record_trigger_fire failed for ${trigger.id}:`, fireError.message)
|
|
409
|
+
}
|
|
302
410
|
|
|
303
411
|
await emitAudit(ctx, 'trigger.fired', {
|
|
304
412
|
type: 'trigger',
|
|
@@ -309,13 +417,13 @@ export async function checkAndFireTriggers(
|
|
|
309
417
|
entity_type: entityType,
|
|
310
418
|
entity_id: entityId,
|
|
311
419
|
pipeline_id: trigger.pipeline_id,
|
|
312
|
-
execution_id:
|
|
313
|
-
execution_status:
|
|
420
|
+
execution_id: executionId,
|
|
421
|
+
execution_status: 'queued'
|
|
314
422
|
})
|
|
315
423
|
|
|
316
424
|
} catch (error: any) {
|
|
317
425
|
console.error(`[${ctx.requestId}] Trigger ${trigger.id} failed:`, error)
|
|
318
|
-
|
|
426
|
+
|
|
319
427
|
await emitAudit(ctx, 'trigger.failed', {
|
|
320
428
|
type: 'trigger',
|
|
321
429
|
id: trigger.id,
|
|
@@ -602,10 +710,11 @@ export async function fireUpdateTriggers(
|
|
|
602
710
|
entityType: string,
|
|
603
711
|
entityId: string,
|
|
604
712
|
entityData: any,
|
|
605
|
-
ctx: CoreContext
|
|
713
|
+
ctx: CoreContext,
|
|
714
|
+
previousEntityData?: any
|
|
606
715
|
): Promise<void> {
|
|
607
716
|
const eventType = `${eventNameFor(entityType)}_updated` as EventType
|
|
608
|
-
await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx)
|
|
717
|
+
await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx, previousEntityData)
|
|
609
718
|
}
|
|
610
719
|
// CIL[END stable_id="~/.framework/functions/_shared/trigger-engine.ts:fireupdatetriggers:function"]
|
|
611
720
|
// CIL[DOC-VECTOR type="context" content="purpose='Triggers an update event for a specified entity type and ID, allowing for associated actions to be executed.'; No verified external callers" status="ready"]
|