spine-framework 0.3.145 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/.env.example +76 -0
  2. package/.framework/.spine-manifest.json +19 -0
  3. package/.framework/cli/commands/agents.ts +61 -33
  4. package/.framework/cli/commands/auth.ts +60 -28
  5. package/.framework/cli/commands/create-app.ts +212 -80
  6. package/.framework/cli/commands/dev.ts +184 -68
  7. package/.framework/cli/commands/doctor.ts +324 -152
  8. package/.framework/cli/commands/generate.ts +119 -68
  9. package/.framework/cli/commands/init.ts +141 -242
  10. package/.framework/cli/commands/install-app.ts +1336 -314
  11. package/.framework/cli/commands/items.ts +31 -29
  12. package/.framework/cli/commands/migrate.ts +299 -156
  13. package/.framework/cli/commands/migrations.ts +62 -30
  14. package/.framework/cli/commands/pipelines.ts +31 -29
  15. package/.framework/cli/commands/status.ts +105 -45
  16. package/.framework/cli/commands/system.ts +47 -42
  17. package/.framework/cli/commands/test.ts +161 -102
  18. package/.framework/cli/commands/upgrade.ts +260 -0
  19. package/.framework/cli/commands/validate-app.ts +517 -161
  20. package/.framework/cli/context.ts +117 -85
  21. package/.framework/cli/env-loader.ts +74 -43
  22. package/.framework/cli/index.ts +70 -46
  23. package/.framework/cli/lib/project-files.ts +404 -0
  24. package/.framework/docs/guides/testing-guide.md +1 -1
  25. package/.framework/docs/runtime-types.md +72 -72
  26. package/.framework/functions/_shared/access.ts +84 -0
  27. package/.framework/functions/_shared/agent-runner.ts +5051 -1026
  28. package/.framework/functions/_shared/app-manifest.ts +214 -94
  29. package/.framework/functions/_shared/ast.ts +214 -0
  30. package/.framework/functions/_shared/audit.ts +91 -55
  31. package/.framework/functions/_shared/cil.ts +368 -127
  32. package/.framework/functions/_shared/db.ts +337 -68
  33. package/.framework/functions/_shared/funnel.ts +228 -0
  34. package/.framework/functions/_shared/geo.ts +105 -0
  35. package/.framework/functions/_shared/hash-api-key.ts +94 -17
  36. package/.framework/functions/_shared/index.ts +22 -224
  37. package/.framework/functions/_shared/kb.ts +832 -58
  38. package/.framework/functions/_shared/llm.ts +118 -18
  39. package/.framework/functions/_shared/logging.ts +317 -19
  40. package/.framework/functions/_shared/middleware.ts +701 -131
  41. package/.framework/functions/_shared/notify.ts +70 -0
  42. package/.framework/functions/_shared/outcomes.ts +34 -19
  43. package/.framework/functions/_shared/permissions.ts +501 -315
  44. package/.framework/functions/_shared/pipeline-runner.ts +704 -155
  45. package/.framework/functions/_shared/principal.ts +436 -263
  46. package/.framework/functions/_shared/rate-limit.ts +233 -0
  47. package/.framework/functions/_shared/resolve-credentials.ts +86 -19
  48. package/.framework/functions/_shared/resolve-ids.ts +203 -139
  49. package/.framework/functions/_shared/retry-deliveries.ts +51 -16
  50. package/.framework/functions/_shared/schema-utils.ts +602 -521
  51. package/.framework/functions/_shared/security.ts +345 -0
  52. package/.framework/functions/_shared/testing.ts +197 -172
  53. package/.framework/functions/_shared/tracing.ts +351 -21
  54. package/.framework/functions/_shared/trigger-engine.ts +425 -136
  55. package/.framework/functions/_shared/webhook-registration.ts +71 -70
  56. package/.framework/functions/_shared/webhook-registry.ts +126 -83
  57. package/.framework/functions/account-nodes.ts +30 -30
  58. package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
  59. package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
  60. package/.framework/functions/adapters/openai-outbound.ts +15 -16
  61. package/.framework/functions/admin-data.ts +596 -173
  62. package/.framework/functions/admin-funnel.ts +64 -60
  63. package/.framework/functions/admin-install-funnel.ts +84 -66
  64. package/.framework/functions/admin-signals.ts +69 -51
  65. package/.framework/functions/ai-agents.ts +431 -106
  66. package/.framework/functions/ai-interactions.ts +186 -112
  67. package/.framework/functions/api-keys.ts +107 -107
  68. package/.framework/functions/apps.ts +154 -136
  69. package/.framework/functions/auth.ts +83 -65
  70. package/.framework/functions/cil-agent-ingest.ts +189 -232
  71. package/.framework/functions/cil-agent.ts +433 -177
  72. package/.framework/functions/cil-callmap.ts +642 -430
  73. package/.framework/functions/cil-config.ts +139 -135
  74. package/.framework/functions/cil-context.ts +214 -127
  75. package/.framework/functions/cil-doc.ts +587 -458
  76. package/.framework/functions/cil-embeddings.ts +453 -364
  77. package/.framework/functions/cil-graph.ts +427 -142
  78. package/.framework/functions/cil-ingest.ts +1418 -1154
  79. package/.framework/functions/cil-pipeline.ts +276 -21
  80. package/.framework/functions/cil-project.ts +659 -132
  81. package/.framework/functions/cil-validate.ts +198 -164
  82. package/.framework/functions/debug-auth.ts +57 -55
  83. package/.framework/functions/deliveries.ts +80 -21
  84. package/.framework/functions/embeddings.ts +1023 -318
  85. package/.framework/functions/eval-harness.ts +146 -108
  86. package/.framework/functions/integration-outbound.ts +205 -75
  87. package/.framework/functions/integration-routes.ts +128 -23
  88. package/.framework/functions/integrations.ts +125 -100
  89. package/.framework/functions/invites.ts +1095 -460
  90. package/.framework/functions/item-progress.ts +158 -102
  91. package/.framework/functions/kb.ts +34 -10
  92. package/.framework/functions/logs.ts +133 -133
  93. package/.framework/functions/observability.ts +152 -122
  94. package/.framework/functions/pipeline-executions.ts +147 -132
  95. package/.framework/functions/pipelines.ts +134 -119
  96. package/.framework/functions/prompt-configs.ts +96 -80
  97. package/.framework/functions/roles.ts +112 -82
  98. package/.framework/functions/security.ts +450 -0
  99. package/.framework/functions/system-cron.ts +117 -80
  100. package/.framework/functions/system.ts +158 -98
  101. package/.framework/functions/tests.ts +29 -29
  102. package/.framework/functions/timers.ts +94 -95
  103. package/.framework/functions/trigger-scheduler.ts +10 -0
  104. package/.framework/functions/triggers.ts +179 -149
  105. package/.framework/functions/types.ts +137 -125
  106. package/.framework/mcp/index.ts +133 -0
  107. package/.framework/mcp/load-env.ts +60 -0
  108. package/.framework/mcp/shared/app-manifest.ts +275 -0
  109. package/.framework/mcp/shared/db.ts +477 -0
  110. package/.framework/mcp/shared/kb.ts +118 -0
  111. package/.framework/mcp/tools/architect.ts +431 -0
  112. package/.framework/mcp/tools/cil/context.ts +345 -0
  113. package/.framework/mcp/tools/cil/graph.ts +242 -0
  114. package/.framework/mcp/tools/cil/ingest.ts +1217 -0
  115. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
  116. package/.framework/mcp/tools/cil/pipeline.ts +767 -0
  117. package/.framework/mcp/tools/cil/project.ts +791 -0
  118. package/.framework/mcp/tools/cil/retrieval.ts +336 -0
  119. package/.framework/mcp/tools/cil/search-core.ts +454 -0
  120. package/.framework/mcp/tools/cil/search.ts +97 -0
  121. package/.framework/mcp/tools/cil/suggest.ts +186 -0
  122. package/.framework/mcp/tools/cil/validate.ts +1369 -0
  123. package/.framework/mcp/tools/kb/delete.ts +139 -0
  124. package/.framework/mcp/tools/kb/index.ts +52 -0
  125. package/.framework/mcp/tools/kb/ingest.ts +84 -0
  126. package/.framework/mcp/tools/kb/list-types.ts +102 -0
  127. package/.framework/mcp/tools/kb/publish.ts +100 -0
  128. package/.framework/mcp/tools/kb/reindex.ts +86 -0
  129. package/.framework/mcp/tools/kb/revoke.ts +85 -0
  130. package/.framework/mcp/tools/kb/search.ts +101 -0
  131. package/.framework/mcp/tools/kb/upsert.ts +125 -0
  132. package/.framework/mcp/tools/observability.ts +510 -0
  133. package/.framework/migrations/001_core_schema.sql +15 -0
  134. package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
  135. package/.framework/migrations/003_core_system_data.sql +16 -1
  136. package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
  137. package/.framework/migrations/005_core_observability.sql +17 -1
  138. package/.framework/migrations/006_security_hardening_rls.sql +15 -0
  139. package/.framework/migrations/007_kb_article_list.sql +17 -1
  140. package/.framework/migrations/008_items_target.sql +15 -0
  141. package/.framework/migrations/009_roles_slug_unique.sql +15 -0
  142. package/.framework/migrations/010_auth_user_trigger.sql +15 -0
  143. package/.framework/migrations/011_spine_system_account.sql +16 -0
  144. package/.framework/migrations/012_service_role_grants.sql +16 -0
  145. package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
  146. package/.framework/migrations/014_apps_required_roles.sql +16 -0
  147. package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
  148. package/.framework/migrations/016_cil_types.sql +281 -0
  149. package/.framework/migrations/017_cil_stable_ids.sql +37 -0
  150. package/.framework/migrations/018_runtime_tables.sql +129 -0
  151. package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
  152. package/.framework/migrations/020_schema_reconciliation.sql +67 -0
  153. package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
  154. package/.framework/migrations/032_cil_status_migration.sql +73 -0
  155. package/.framework/migrations/033_cil_versioning.sql +73 -0
  156. package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
  157. package/.framework/migrations/035_rate_limits.sql +55 -0
  158. package/.framework/migrations/036_demo_record_flags.sql +72 -0
  159. package/.framework/migrations/037_kb_types_canonical.sql +244 -0
  160. package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
  161. package/.framework/migrations/042_registration_hierarchy.sql +309 -0
  162. package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
  163. package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
  164. package/.framework/migrations/045_security_critical_rls.sql +139 -0
  165. package/.framework/migrations/046_security_missing_rls.sql +79 -0
  166. package/.framework/migrations/047_people_updated_by.sql +27 -0
  167. package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
  168. package/.framework/migrations/049_security_blocklist.sql +148 -0
  169. package/.framework/migrations/050_notifications.sql +49 -0
  170. package/.framework/migrations/051_message_agent_fields.sql +74 -0
  171. package/.framework/migrations/052_registration_security.sql +421 -0
  172. package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
  173. package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
  174. package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
  175. package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
  176. package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
  177. package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
  178. package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
  179. package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
  180. package/.framework/migrations/061_fk_dedup.sql +32 -0
  181. package/.framework/public/spine-logo.svg +10 -3
  182. package/.framework/scripts/app-install-cli.ts +190 -93
  183. package/.framework/scripts/assemble-frontend.sh +127 -44
  184. package/.framework/scripts/assemble-functions.sh +128 -30
  185. package/.framework/scripts/boundary-check.sh +6 -2
  186. package/.framework/scripts/build-manifest.sh +24 -68
  187. package/.framework/scripts/check-core-integrity.sh +15 -76
  188. package/.framework/scripts/checksum-manifest.mts +343 -0
  189. package/.framework/scripts/cil-migrate-markers.ts +403 -0
  190. package/.framework/scripts/fix-cil-markers.mjs +176 -0
  191. package/.framework/scripts/verify-integrity.sh +21 -57
  192. package/.framework/scripts/verify-manifest.mts +305 -0
  193. package/.framework/seeds/types/funnel_signal_use.json +74 -21
  194. package/.framework/seeds/types/spine_instance.json +73 -21
  195. package/.framework/src/App.tsx +118 -72
  196. package/.framework/src/apps/admin/index.tsx +855 -58
  197. package/.framework/src/components/AppWrapper.tsx +43 -45
  198. package/.framework/src/components/CustomAppLoader.tsx +160 -116
  199. package/.framework/src/components/admin/AdminListPage.tsx +46 -44
  200. package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
  201. package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
  202. package/.framework/src/components/admin/PageInstructions.tsx +98 -2
  203. package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
  204. package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
  205. package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
  206. package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
  207. package/.framework/src/components/app-sidebar.tsx +42 -45
  208. package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
  209. package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
  210. package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
  211. package/.framework/src/components/kb/KBArticleList.tsx +251 -10
  212. package/.framework/src/components/layout/AppShell.tsx +51 -45
  213. package/.framework/src/components/layout/Header.tsx +42 -43
  214. package/.framework/src/components/layout/Layout.tsx +46 -46
  215. package/.framework/src/components/layout/Sidebar.tsx +196 -96
  216. package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
  217. package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
  218. package/.framework/src/components/runtime/DataFilters.tsx +49 -74
  219. package/.framework/src/components/runtime/DataHeader.tsx +53 -32
  220. package/.framework/src/components/runtime/DataListPage.tsx +51 -155
  221. package/.framework/src/components/runtime/DataStats.tsx +66 -44
  222. package/.framework/src/components/runtime/DataTable.tsx +49 -149
  223. package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
  224. package/.framework/src/components/runtime/index.ts +119 -16
  225. package/.framework/src/components/search-form.tsx +46 -29
  226. package/.framework/src/components/shared/AgentView.tsx +65 -48
  227. package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
  228. package/.framework/src/components/shared/InviteManager.tsx +207 -97
  229. package/.framework/src/components/shared/MemberList.tsx +97 -0
  230. package/.framework/src/components/shared/SchemaFields.tsx +107 -86
  231. package/.framework/src/components/ui/DataTable.tsx +80 -68
  232. package/.framework/src/components/ui/Form.tsx +157 -146
  233. package/.framework/src/components/ui/ItemCard.tsx +74 -67
  234. package/.framework/src/components/ui/ItemListView.tsx +73 -68
  235. package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
  236. package/.framework/src/components/ui/Modal.tsx +78 -68
  237. package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
  238. package/.framework/src/components/ui/accordion.tsx +85 -86
  239. package/.framework/src/components/ui/alert-dialog.tsx +197 -184
  240. package/.framework/src/components/ui/alert.tsx +105 -94
  241. package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
  242. package/.framework/src/components/ui/avatar.tsx +122 -107
  243. package/.framework/src/components/ui/badge.tsx +60 -55
  244. package/.framework/src/components/ui/breadcrumb.tsx +127 -128
  245. package/.framework/src/components/ui/button-group.tsx +86 -72
  246. package/.framework/src/components/ui/button.tsx +71 -50
  247. package/.framework/src/components/ui/calendar.tsx +72 -57
  248. package/.framework/src/components/ui/card.tsx +136 -129
  249. package/.framework/src/components/ui/carousel.tsx +226 -107
  250. package/.framework/src/components/ui/chart.tsx +252 -147
  251. package/.framework/src/components/ui/checkbox.tsx +43 -44
  252. package/.framework/src/components/ui/collapsible.tsx +86 -71
  253. package/.framework/src/components/ui/combobox.tsx +282 -249
  254. package/.framework/src/components/ui/command.tsx +158 -146
  255. package/.framework/src/components/ui/context-menu.tsx +239 -240
  256. package/.framework/src/components/ui/dialog.tsx +174 -157
  257. package/.framework/src/components/ui/direction.tsx +72 -56
  258. package/.framework/src/components/ui/drawer.tsx +168 -169
  259. package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
  260. package/.framework/src/components/ui/empty.tsx +131 -120
  261. package/.framework/src/components/ui/field.tsx +205 -173
  262. package/.framework/src/components/ui/hover-card.tsx +71 -72
  263. package/.framework/src/components/ui/input-group.tsx +148 -133
  264. package/.framework/src/components/ui/input-otp.tsx +99 -86
  265. package/.framework/src/components/ui/input.tsx +43 -41
  266. package/.framework/src/components/ui/item.tsx +207 -185
  267. package/.framework/src/components/ui/kbd.tsx +57 -56
  268. package/.framework/src/components/ui/label.tsx +45 -41
  269. package/.framework/src/components/ui/menubar.tsx +268 -236
  270. package/.framework/src/components/ui/native-select.tsx +85 -86
  271. package/.framework/src/components/ui/navigation-menu.tsx +166 -156
  272. package/.framework/src/components/ui/pagination.tsx +131 -141
  273. package/.framework/src/components/ui/popover.tsx +125 -128
  274. package/.framework/src/components/ui/progress.tsx +47 -44
  275. package/.framework/src/components/ui/radio-group.tsx +60 -55
  276. package/.framework/src/components/ui/resizable.tsx +85 -72
  277. package/.framework/src/components/ui/scroll-area.tsx +57 -58
  278. package/.framework/src/components/ui/select.tsx +182 -169
  279. package/.framework/src/components/ui/separator.tsx +42 -43
  280. package/.framework/src/components/ui/sheet.tsx +170 -159
  281. package/.framework/src/components/ui/sidebar.tsx +395 -398
  282. package/.framework/src/components/ui/skeleton.tsx +42 -43
  283. package/.framework/src/components/ui/slider.tsx +61 -44
  284. package/.framework/src/components/ui/sonner.tsx +44 -44
  285. package/.framework/src/components/ui/spinner.tsx +47 -42
  286. package/.framework/src/components/ui/switch.tsx +56 -42
  287. package/.framework/src/components/ui/table-primitives.tsx +272 -132
  288. package/.framework/src/components/ui/table.tsx +140 -142
  289. package/.framework/src/components/ui/tabs.tsx +110 -93
  290. package/.framework/src/components/ui/textarea.tsx +43 -44
  291. package/.framework/src/components/ui/toggle-group.tsx +87 -71
  292. package/.framework/src/components/ui/toggle.tsx +56 -56
  293. package/.framework/src/components/ui/tooltip.tsx +99 -85
  294. package/.framework/src/contexts/AppContext.tsx +166 -114
  295. package/.framework/src/contexts/AuthContext.tsx +232 -137
  296. package/.framework/src/hooks/use-mobile.ts +43 -44
  297. package/.framework/src/hooks/useApi.ts +185 -172
  298. package/.framework/src/hooks/useAppPath.ts +31 -29
  299. package/.framework/src/hooks/useApps.ts +77 -42
  300. package/.framework/src/hooks/useEntityList.ts +66 -29
  301. package/.framework/src/hooks/useEntityRecord.ts +76 -32
  302. package/.framework/src/hooks/useForm.ts +62 -30
  303. package/.framework/src/hooks/useListSchema.ts +60 -43
  304. package/.framework/src/hooks/useSchemaRecord.ts +115 -95
  305. package/.framework/src/lib/api.ts +258 -134
  306. package/.framework/src/lib/registration.ts +181 -0
  307. package/.framework/src/lib/supabase.ts +118 -81
  308. package/.framework/src/lib/utils.ts +266 -266
  309. package/.framework/src/main.tsx +43 -44
  310. package/.framework/src/pages/DashboardPage.tsx +44 -43
  311. package/.framework/src/pages/NoAppPage.tsx +50 -0
  312. package/.framework/src/pages/NotFoundPage.tsx +32 -29
  313. package/.framework/src/pages/SetupPage.tsx +34 -33
  314. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
  315. package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
  316. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
  317. package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
  318. package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
  319. package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
  320. package/.framework/src/pages/admin/AppsPage.tsx +45 -42
  321. package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
  322. package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
  323. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
  324. package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
  325. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
  326. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
  327. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
  328. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
  329. package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
  330. package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
  331. package/.framework/src/pages/admin/LogsPage.tsx +87 -137
  332. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
  333. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
  334. package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
  335. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
  336. package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
  337. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
  338. package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
  339. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
  340. package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
  341. package/.framework/src/pages/admin/RolesPage.tsx +47 -164
  342. package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
  343. package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
  344. package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
  345. package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
  346. package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
  347. package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
  348. package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
  349. package/.framework/src/pages/admin/TimersPage.tsx +44 -42
  350. package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
  351. package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
  352. package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
  353. package/.framework/src/pages/admin/TypesPage.tsx +45 -42
  354. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
  355. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
  356. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
  357. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
  358. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
  359. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
  360. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
  361. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
  362. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
  363. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
  364. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
  365. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
  366. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  367. package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
  368. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
  369. package/.framework/src/pages/auth/LoginPage.tsx +38 -32
  370. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
  371. package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
  372. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
  373. package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
  374. package/.framework/src/types/auth.ts +101 -93
  375. package/.framework/src/types/types.ts +283 -262
  376. package/.framework/templates/project.json +87 -0
  377. package/.framework/templates/spine.config.neutral.json +7 -0
  378. package/.framework/tests/api/api-surface.test.ts +381 -0
  379. package/.framework/tests/fixtures/seed.ts +427 -0
  380. package/.framework/tests/fixtures/teardown.ts +228 -0
  381. package/.framework/tests/reporter.ts +351 -0
  382. package/.framework/tests/setup.ts +135 -0
  383. package/.framework/tests/ui/ui-sweep.ts +278 -0
  384. package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
  385. package/.framework/tests/unit/agent-runner.test.ts +2389 -0
  386. package/.framework/tests/unit/ai-interactions.test.ts +81 -0
  387. package/.framework/tests/unit/app-manifest.test.ts +359 -0
  388. package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
  389. package/.framework/tests/unit/cil-determinism.test.ts +85 -0
  390. package/.framework/tests/unit/core-isolation.test.ts +153 -0
  391. package/.framework/tests/unit/funnel-signals.test.ts +135 -0
  392. package/.framework/tests/unit/install-app.test.ts +1759 -0
  393. package/.framework/tests/unit/invites-security.test.ts +572 -0
  394. package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
  395. package/.framework/tests/unit/kb-search.test.ts +119 -0
  396. package/.framework/tests/unit/migrate.test.ts +509 -0
  397. package/.framework/tests/unit/notify.test.ts +136 -0
  398. package/.framework/tests/unit/permissions.test.ts +269 -0
  399. package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
  400. package/.framework/tests/unit/principal-cache.test.ts +138 -0
  401. package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
  402. package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
  403. package/.framework/tests/unit/schema-utils.test.ts +225 -0
  404. package/.framework/tests/unit/trigger-engine.test.ts +513 -0
  405. package/.framework/tests/unit/validate-app.test.ts +527 -0
  406. package/README.md +34 -3
  407. package/bin/spine-framework.cjs +1 -0
  408. package/config/tailwind.config.cjs +7 -0
  409. package/config/tsconfig.functions.json +42 -0
  410. package/config/tsconfig.json +37 -9
  411. package/config/vite.config.ts +31 -2
  412. package/netlify.toml +31 -3
  413. package/package.json +46 -29
  414. package/scripts/assemble-frontend.sh +127 -44
  415. package/scripts/assemble-functions.sh +127 -29
  416. package/scripts/boundary-check.sh +6 -2
  417. package/scripts/build-manifest.sh +24 -68
  418. package/scripts/checksum-manifest.mts +345 -0
  419. package/scripts/dev-clean.sh +86 -0
  420. package/scripts/dev-netlify.sh +49 -0
  421. package/scripts/netlify-dev-wrapper.sh +2 -0
  422. package/scripts/verify-install.ts +358 -0
  423. package/scripts/verify-integrity.sh +21 -57
  424. package/scripts/verify-manifest.mts +305 -0
  425. package/templates/project/gitignore +45 -0
  426. package/{config → templates/project}/package-lock.json +22335 -9366
  427. package/vitest.config.ts +46 -0
  428. package/.framework/public/spine.config.json +0 -91
  429. package/config/deno.lock +0 -108
  430. package/config/tsconfig.node.tsbuildinfo +0 -1
  431. package/config/tsconfig.tsbuildinfo +0 -1
  432. package/config/vite.config.d.ts +0 -2
  433. package/custom/.devin/AGENT.md +0 -357
  434. package/custom/.devin/skills/install-app/SKILL.md +0 -39
  435. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
  436. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
  437. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
  438. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
  439. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
  440. package/custom/.devin/workflows/app-install.yaml +0 -83
  441. package/custom/README.md +0 -79
  442. package/docs/README.md +0 -15
  443. package/scripts/app-install-cli.ts +0 -286
  444. package/scripts/check-core-integrity.sh +0 -82
  445. package/scripts/ingest-chunks.cjs +0 -202
  446. package/scripts/kb-chunk-parser.cjs +0 -312
  447. package/scripts/kb-chunk-parser.ts +0 -330
  448. package/scripts/kb-ingest-docs.ts +0 -203
  449. package/scripts/load-test-app-install.ts +0 -484
  450. package/scripts/test-login.js +0 -33
  451. package/spine.config.json +0 -91
@@ -0,0 +1,1217 @@
1
+ // CIL[BEGIN stable_id="~/.framework/mcp/tools/cil/ingest.ts:mcp_ingest_tools:file" type="file" identity="mcp_ingest_tools" parent="~/.framework/mcp/tools/cil/ingest.ts"]
2
+ // CIL[DOC-MACRO purpose="Register all CIL ingest, discovery, chunking, embedding, mapping, and pipeline orchestration MCP tools on a shared McpServer"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Expose the MCP-facing surface for CIL ingestion, embedding, mapping, and pipeline orchestration"]
4
+ // CIL[DOC-CONTEXT module="cil-ingest" layer="mcp-tools" related_files=".framework/functions/_shared/cil.ts,.framework/mcp/shared/db.ts,.framework/mcp/tools/cil/ingest.ts"]
5
+ // CIL[DOC-CONTRACT inputs="McpServer instance, raw chunk content strings" outputs="Registered tool handlers" errors="Database and function call failures" side_effects="Registers handlers on McpServer; reads/writes CIL project, chunk, and file records"]
6
+ // CIL[DOC-MICRO logic_summary="Binds one handler per CIL tool, delegating to adminDb, fetchFunction, and helpers for chunking, embedding, mapping, and pipeline steps" notable_decisions="Uses helper normaliseAgentChunkContent for chunk hashing; pipeline steps 3-8 dispatch to cil-doc, cil-embeddings, cil-callmap, and cil-config functions"]
7
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
11
+ import { z } from 'zod'
12
+ import { adminDb, resolveLinkTypeId, resolveTypeId, fetchFunction } from '../../shared/db.js'
13
+ import { handler as callmapHandler } from '../../../functions/cil-callmap.js'
14
+ import { SYSTEM_PRINCIPAL } from '../../../functions/_shared/principal.js'
15
+ import { adminDb as functionsAdminDb } from '../../../functions/_shared/db.js'
16
+ import fs from 'node:fs'
17
+ import crypto from 'node:crypto'
18
+ import path from 'node:path'
19
+ import { computeContentHash, isSelectedPath } from '../../../functions/_shared/cil.js'
20
+
21
+ function normaliseAgentChunkContent(raw: string): string {
22
+ const lines = raw.replace(/\r\n/g, '\n').split('\n')
23
+ const filtered = lines.filter(line => {
24
+ const trimmed = line.trim()
25
+ return !trimmed.match(/^(?:(?:\/\/|--|#)\s*|<!--\s*)CIL\[(BEGIN|END)/) && !trimmed.match(/^(?:(?:\/\/|--|#)\s*|<!--\s*)CIL\[DOC-/)
26
+ })
27
+ const collapsed: string[] = []
28
+ let lastBlank = false
29
+ for (const line of filtered) {
30
+ const isBlank = line.trim() === ''
31
+ if (isBlank && lastBlank) continue
32
+ collapsed.push(line)
33
+ lastBlank = isBlank
34
+ }
35
+ return collapsed.join('\n').trim()
36
+ }
37
+
38
+ function computeChunkHash(content: string): string {
39
+ return crypto.createHash('sha256').update(normaliseAgentChunkContent(content), 'utf8').digest('hex').slice(0, 16)
40
+ }
41
+
42
+ function computeFileHash(content: string): string {
43
+ return crypto.createHash('sha256').update(content, 'utf8').digest('hex')
44
+ }
45
+
46
+ async function getChunkByStableId(projectId: string, stableId: string, versionId?: string) {
47
+ let q = adminDb.from('items').select('id, title, data')
48
+ .filter('data->>kb_type', 'eq', 'cil_chunk')
49
+ .filter('data->>project_id', 'eq', projectId)
50
+ .filter('data->>stable_id', 'eq', stableId)
51
+ .eq('is_active', true)
52
+ if (versionId) q = (q as any).filter('data->>version_id', 'eq', versionId)
53
+ const { data } = await (q as any).maybeSingle()
54
+ return data
55
+ }
56
+
57
+ async function callCallmapMapProject(
58
+ accountId: string,
59
+ project_id: string,
60
+ file_path: string | undefined,
61
+ force_regenerate: boolean,
62
+ fast: boolean,
63
+ limit: number,
64
+ offset: number
65
+ ): Promise<any> {
66
+ const event = {
67
+ requestId: crypto.randomUUID(),
68
+ principal: {
69
+ ...SYSTEM_PRINCIPAL,
70
+ accountId,
71
+ provenance: { sourceType: 'mcp', createdBy: null, invokedAt: new Date().toISOString() },
72
+ },
73
+ db: functionsAdminDb,
74
+ accountId,
75
+ appId: null,
76
+ query: { action: 'map_project', method: 'POST' },
77
+ httpMethod: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ requestPath: '/.netlify/functions/cil-callmap',
80
+ path: '/.netlify/functions/cil-callmap',
81
+ source: 'spine-framework-mcp',
82
+ trace: {},
83
+ body: null,
84
+ } as any
85
+ const body = {
86
+ project_id,
87
+ file_path,
88
+ force_regenerate,
89
+ fast,
90
+ limit,
91
+ offset,
92
+ }
93
+ return callmapHandler(event, body)
94
+ }
95
+
96
+ export function register(server: McpServer) {
97
+
98
+ server.registerTool(
99
+ 'spine_cil_detect_changes',
100
+ {
101
+ description: `Detect which selected files have drifted since their last ingestion by comparing per-chunk hashes on disk to stored hashes.
102
+ Returns three lists: drifted (file changed — re-run spine_cil_store_markers), missing (file deleted or renamed — run spine_cil_mark_dead), current (no changes).
103
+ Run this before starting an incremental ingestion pass to build an accurate work queue.`,
104
+ inputSchema: z.object({
105
+ project_id: z.string().uuid().describe('The cil_project UUID'),
106
+ }),
107
+ },
108
+ async ({ project_id }) => {
109
+ const { data: projectRows } = await adminDb.from('items').select('data')
110
+ .filter('data->>kb_type', 'eq', 'cil_project')
111
+ .filter('id', 'eq', project_id)
112
+ .eq('is_active', true)
113
+
114
+ const project = (projectRows || [])[0]
115
+
116
+ if (!project) {
117
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
118
+ }
119
+
120
+ const repoPath: string = project.data?.repo_path || ''
121
+
122
+ if (!repoPath) {
123
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Project has no repo_path' }) }] }
124
+ }
125
+
126
+ const selectedPaths: string[] = project.data?.selected_paths || []
127
+
128
+ if (selectedPaths.length === 0) {
129
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, drifted: [], missing: [], current: [], message: 'No selected_paths on project' }) }] }
130
+ }
131
+
132
+ const activeVersionId = project.data?.active_version_id
133
+
134
+
135
+
136
+ let chunkQuery = adminDb.from('items').select('id, data')
137
+ .filter('data->>kb_type', 'eq', 'cil_chunk')
138
+ .filter('data->>project_id', 'eq', project_id)
139
+ .eq('is_active', true)
140
+
141
+
142
+ if (activeVersionId) {
143
+ chunkQuery = chunkQuery.filter('data->>version_id', 'eq', activeVersionId)
144
+ }
145
+
146
+
147
+ const { data: chunks } = await chunkQuery
148
+
149
+ const chunksByFile: Record<string, any[]> = {}
150
+
151
+ for (const c of chunks || []) {
152
+
153
+ const fp = c.data?.file_path
154
+
155
+ if (fp) {
156
+ if (!chunksByFile[fp]) chunksByFile[fp] = []
157
+ chunksByFile[fp].push(c)
158
+ }
159
+
160
+ }
161
+
162
+ const drifted: { file_path: string; drifted_chunks: number; total_chunks: number; drifted_chunk_ids?: string[] }[] = []
163
+
164
+ const missing: { file_path: string; reason: string }[] = []
165
+
166
+ const current: { file_path: string; chunk_count: number }[] = []
167
+
168
+ for (const filePath of selectedPaths) {
169
+
170
+ const absolutePath = path.join(repoPath, filePath.replace(/^\//, ''))
171
+
172
+ const fileChunks = chunksByFile[filePath] || []
173
+
174
+ if (!fs.existsSync(absolutePath)) {
175
+ missing.push({ file_path: filePath, reason: 'file not found on disk' })
176
+ continue
177
+ }
178
+
179
+ if (fileChunks.length === 0) {
180
+ drifted.push({ file_path: filePath, drifted_chunks: 0, total_chunks: 0 })
181
+ continue
182
+ }
183
+
184
+ const fileContent = fs.readFileSync(absolutePath, 'utf-8')
185
+
186
+ const lines = fileContent.split('\n')
187
+
188
+
189
+
190
+ let driftCount = 0
191
+
192
+
193
+ const driftedStableIds: string[] = []
194
+
195
+ for (const chunk of fileChunks) {
196
+
197
+ const cd = chunk.data || {}
198
+
199
+ const lineStart = (cd.line_start || 1) - 1
200
+
201
+ const lineEnd = (cd.line_end || 1) - 1
202
+
203
+ if (lineEnd >= lines.length) {
204
+ driftCount++
205
+ driftedStableIds.push(cd.stable_id || cd.legacy_chunk_id || cd.chunk_id || chunk.id)
206
+ continue
207
+ }
208
+
209
+ const extracted = lines.slice(lineStart, lineEnd + 1).join('\n')
210
+
211
+ const computed = cd.content_hash ? computeContentHash(extracted) : computeChunkHash(extracted)
212
+
213
+ const storedHash = cd.content_hash || cd.stored_hash
214
+
215
+ if (computed !== storedHash) {
216
+ driftCount++
217
+ driftedStableIds.push(cd.stable_id || cd.legacy_chunk_id || cd.chunk_id || chunk.id)
218
+ }
219
+
220
+ }
221
+
222
+
223
+ if (driftCount > 0) {
224
+ drifted.push({ file_path: filePath, drifted_chunks: driftCount, total_chunks: fileChunks.length, drifted_chunk_ids: driftedStableIds })
225
+ } else {
226
+ current.push({ file_path: filePath, chunk_count: fileChunks.length })
227
+ }
228
+ }
229
+
230
+ return {
231
+ content: [{
232
+ type: 'text',
233
+ text: JSON.stringify({
234
+ ok: true,
235
+ project_id,
236
+ summary: { drifted: drifted.length, missing: missing.length, current: current.length },
237
+ drifted,
238
+ missing,
239
+ current,
240
+ }, null, 2),
241
+ }],
242
+ }
243
+ }
244
+ )
245
+
246
+ server.registerTool(
247
+ 'spine_cil_discover',
248
+ {
249
+ description: 'Discover source files for a CIL project by walking repo_path with its configured globs and exclude_globs. Creates cil_file records with status 0-discovered and updates the project file_tree.',
250
+ inputSchema: z.object({
251
+ project_id: z.string().uuid().describe('The cil_project UUID'),
252
+ file_path: z.string().optional().describe('Optional single path or glob to discover within the project'),
253
+ }),
254
+ },
255
+ async ({ project_id, file_path }) => {
256
+ try {
257
+ const result = await fetchFunction('cil-ingest', 'discover', {}, { project_id, file_path })
258
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, ...result }) }] }
259
+ } catch (err: any) {
260
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err?.message || String(err) }) }] }
261
+ }
262
+ }
263
+ )
264
+
265
+ server.registerTool(
266
+ 'spine_cil_mark_dead',
267
+ {
268
+ description: `Mark all chunks for a given file_path as inactive (is_active: false).
269
+ Use this when a file has been deleted or renamed and its chunks should be retired from the index.
270
+ For renames: first call spine_cil_store_markers on the new path to create fresh chunks, then call spine_cil_mark_dead on the old path.
271
+ IMPORTANT: this is irreversible via the MCP — deleted chunks cannot be restored without re-running spine_cil_store_markers.`,
272
+ inputSchema: z.object({
273
+ project_id: z.string().uuid().describe('The cil_project UUID'),
274
+ file_path: z.string().describe('Relative file path from repo root whose chunks should be marked inactive'),
275
+ reason: z.string().optional().describe('Optional reason for marking dead, e.g. "file deleted", "file renamed to X"'),
276
+ }),
277
+ },
278
+ async ({ project_id, file_path, reason }) => {
279
+ const { data: chunks } = await adminDb.from('items').select('id, data')
280
+ .filter('data->>kb_type', 'eq', 'cil_chunk')
281
+ .filter('data->>project_id', 'eq', project_id)
282
+ .filter('data->>file_path', 'eq', file_path)
283
+ .eq('is_active', true)
284
+ .limit(1000)
285
+
286
+ if (!chunks || chunks.length === 0) {
287
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, file_path, marked_inactive: 0, message: 'No active chunks found for this file' }) }] }
288
+ }
289
+
290
+ const now = new Date().toISOString()
291
+
292
+ const errors: string[] = []
293
+
294
+
295
+
296
+ let marked = 0
297
+
298
+
299
+ for (const chunk of chunks) {
300
+
301
+ const newData = { ...chunk.data, is_dead_code: true, dead_reason: reason || 'marked via spine_cil_mark_dead', dead_at: now }
302
+
303
+ const { error: updateError } = await adminDb.from('items')
304
+ .update({ is_active: false, data: newData, updated_at: now })
305
+ .eq('id', chunk.id)
306
+ if (updateError) errors.push(`${chunk.id}: ${updateError.message}`)
307
+ else marked++
308
+ }
309
+
310
+
311
+ return {
312
+ content: [{
313
+ type: 'text',
314
+ text: JSON.stringify({
315
+ ok: errors.length === 0,
316
+ project_id,
317
+ file_path,
318
+ marked_inactive: marked,
319
+ reason: reason || 'not specified',
320
+ errors: errors.length > 0 ? errors : undefined,
321
+ }, null, 2),
322
+ }],
323
+ }
324
+ }
325
+ )
326
+
327
+ server.registerTool(
328
+ 'spine_cil_generate_embeddings',
329
+ {
330
+ description: `DO NOT call this tool during the pipeline. Steps 6 and 9 (embed) are triggered by the user clicking the UI button — the server runs embeddings automatically and advances file_statuses.
331
+
332
+ This tool exists for direct/manual embedding outside the standard pipeline flow only. If you are running Steps 4, 5, or 8, use spine_cil_store_markers instead.`,
333
+ inputSchema: z.object({
334
+ project_id: z.string().uuid().describe('The cil_project UUID'),
335
+ file_path: z.string().optional().describe('Relative file path from repo root. If omitted, embeds all approved chunks in the project.'),
336
+ force_regenerate: z.boolean().optional().describe('Force regeneration of existing embeddings'),
337
+ batch_size: z.number().int().min(1).max(50).optional().describe('Batch size for embedding API calls'),
338
+ }),
339
+ },
340
+ async ({ project_id, file_path, force_regenerate = false, batch_size = 20 }) => {
341
+
342
+ const { data: project } = await adminDb.from('items').select('account_id, data')
343
+ .eq('id', project_id).maybeSingle()
344
+ if (!project) {
345
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
346
+ }
347
+
348
+ const accountId = project.account_id
349
+
350
+ if (!accountId) {
351
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Project has no account_id' }) }] }
352
+ }
353
+
354
+
355
+
356
+ let query = adminDb.from('items').select('id, title, account_id, data')
357
+ .filter('data->>kb_type', 'eq', 'cil_chunk')
358
+ .filter('data->>project_id', 'eq', project_id)
359
+ .eq('is_active', true)
360
+ .limit(1000)
361
+
362
+
363
+ if (file_path) {
364
+ query = query.filter('data->>file_path', 'eq', file_path)
365
+ }
366
+
367
+
368
+ const { data: chunks, error: fetchError } = await query
369
+ if (fetchError) {
370
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch chunks: ${fetchError.message}` }) }] }
371
+ }
372
+
373
+ if (!chunks || chunks.length === 0) {
374
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'No chunks found to embed', project_id, file_path, embedded_count: 0 }) }] }
375
+ }
376
+
377
+ const { embedChunk } = await import('../../../functions/cil-embeddings.ts')
378
+
379
+ const results: any[] = []
380
+
381
+ const errors: string[] = []
382
+
383
+ for (const chunk of chunks) {
384
+ try {
385
+
386
+ const r = await embedChunk(chunk, accountId, force_regenerate)
387
+
388
+ results.push({
389
+ chunk_id: chunk.id,
390
+ chunk_name: chunk.data?.chunk_name || '',
391
+ created: r.created,
392
+ skipped: r.skipped,
393
+ })
394
+ if (r.error) errors.push(`${chunk.id}: ${r.error}`)
395
+ } catch (e: any) {
396
+ errors.push(`${chunk.id}: ${e.message}`)
397
+ }
398
+
399
+ }
400
+
401
+ return {
402
+ content: [{
403
+ type: 'text',
404
+ text: JSON.stringify({
405
+ ok: errors.length === 0,
406
+ project_id,
407
+ file_path: file_path || '(all files)',
408
+ chunk_count: chunks.length,
409
+ processed: results.length,
410
+ errors: errors.length > 0 ? errors : undefined,
411
+ results,
412
+ }, null, 2),
413
+ }],
414
+ }
415
+ }
416
+ )
417
+
418
+ server.registerTool(
419
+ 'spine_cil_generate_mappings',
420
+ {
421
+ description: `DO NOT call this tool during the pipeline. Steps 7 and 10 (map) are triggered by the user clicking the UI button — the server runs callgraph analysis automatically and advances file_statuses.
422
+
423
+ This tool exists for direct/manual mapping outside the standard pipeline flow only. If you are running Steps 4, 5, or 8, use spine_cil_store_markers instead.`,
424
+ inputSchema: z.object({
425
+ project_id: z.string().uuid().describe('The cil_project UUID'),
426
+ file_path: z.string().optional().describe('Limit mapping to a single file. If omitted, maps the whole project.'),
427
+ force_regenerate: z.boolean().optional().describe('Force regeneration of existing links'),
428
+ fast: z.boolean().optional().describe('Skip updating file_statuses and file_tree; only create links.'),
429
+ }),
430
+ },
431
+ async ({ project_id, file_path, force_regenerate = false, fast = false }) => {
432
+
433
+ const { data: project } = await adminDb.from('items').select('account_id, data')
434
+ .eq('id', project_id).maybeSingle()
435
+ if (!project) {
436
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
437
+ }
438
+
439
+ const accountId = project.account_id
440
+
441
+ if (!accountId) {
442
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Project has no account_id' }) }] }
443
+ }
444
+
445
+
446
+
447
+ let fileQuery = adminDb.from('items').select('id, data')
448
+ .filter('data->>kb_type', 'eq', 'cil_file')
449
+ .filter('data->>project_id', 'eq', project_id)
450
+ .eq('is_active', true)
451
+
452
+
453
+ if (file_path) {
454
+ fileQuery = fileQuery.filter('data->>file_path', 'eq', file_path)
455
+ }
456
+
457
+
458
+ const { data: files, error: fileError } = await fileQuery
459
+ if (fileError) {
460
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch files: ${fileError.message}` }) }] }
461
+ }
462
+
463
+ if (!files || files.length === 0) {
464
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'No files found to map', project_id, file_path, mapped_files: 0 }) }] }
465
+ }
466
+
467
+ const results: any[] = []
468
+
469
+ const errors: string[] = []
470
+
471
+ if (file_path) {
472
+ const { data: fileItems } = await adminDb.from('items').select('id, title, data')
473
+ .filter('data->>project_id', 'eq', project_id)
474
+ .filter('data->>kb_type', 'eq', 'cil_file')
475
+ .filter('data->>file_path', 'eq', file_path)
476
+ .eq('is_active', true)
477
+ .limit(1)
478
+
479
+ const fileItem = (fileItems || [])[0]
480
+
481
+ if (!fileItem) {
482
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `No cil_file for path: ${file_path}` }) }] }
483
+ }
484
+
485
+ let r: any = {}
486
+ try {
487
+ r = await callCallmapMapProject(accountId, project_id, file_path, force_regenerate, fast, 1, 0)
488
+ } catch (err: any) {
489
+ errors.push(`map_project(${file_path}): ${err?.message || String(err)}`)
490
+ }
491
+
492
+ results.push({
493
+ file_path,
494
+ file_id: fileItem.id,
495
+ ...r,
496
+ })
497
+ if (r.errors) errors.push(...r.errors)
498
+ } else {
499
+
500
+
501
+
502
+ let offset = 0
503
+
504
+
505
+
506
+
507
+ let hasMore = true
508
+
509
+
510
+ while (hasMore) {
511
+
512
+ let r: any = { files_processed: 0 }
513
+ try {
514
+ r = await callCallmapMapProject(accountId, project_id, undefined, force_regenerate, fast, 15, offset)
515
+ } catch (err: any) {
516
+ errors.push(`map_project(offset ${offset}): ${err?.message || String(err)}`)
517
+ hasMore = false
518
+ continue
519
+ }
520
+
521
+ results.push(r)
522
+ if (r.errors) errors.push(...r.errors)
523
+ hasMore = r.has_more === true
524
+ offset = r.next_offset ?? offset + (r.files_processed || 0)
525
+ }
526
+
527
+
528
+
529
+ }
530
+
531
+ return {
532
+ content: [{
533
+ type: 'text',
534
+ text: JSON.stringify({
535
+ ok: errors.length === 0,
536
+ project_id,
537
+ file_path: file_path || '(all files)',
538
+ file_count: file_path ? 1 : results.reduce((sum, r) => sum + (r.files_processed || 0), 0),
539
+ processed: file_path ? results.length : results.reduce((sum, r) => sum + (r.files_processed || 0), 0),
540
+ errors: errors.length > 0 ? errors : undefined,
541
+ results,
542
+ }, null, 2),
543
+ }],
544
+ }
545
+ }
546
+ )
547
+
548
+ server.registerTool(
549
+ 'spine_cil_get_pending_vectors',
550
+ {
551
+ description: `List all chunks that have vectors awaiting human-authored content (status="pending_human_input").
552
+ Used by the CIL UI to surface chunks needing human input for implementation_pattern and user_journey vectors.
553
+ Returns stable_id, version_id, chunk_name, file_path, and which vector types are pending for each chunk.`,
554
+ inputSchema: z.object({
555
+ project_id: z.string().uuid().describe('The cil_project UUID'),
556
+ vector_type: z.string().optional().describe('Filter to a specific vector type, e.g. "user_journey". If omitted, returns all pending.'),
557
+ }),
558
+ },
559
+ async ({ project_id, vector_type }) => {
560
+ const { data: chunks } = await adminDb.from('items').select('id, data')
561
+ .filter('data->>kb_type', 'eq', 'cil_chunk')
562
+ .filter('data->>project_id', 'eq', project_id)
563
+ .eq('is_active', true)
564
+
565
+ const pending = (chunks || [])
566
+ .filter((c: any) => {
567
+ const pendingList: string[] = c.data?.vectors_pending_human_review || []
568
+ if (pendingList.length === 0) return false
569
+ if (vector_type) return pendingList.includes(vector_type)
570
+ return true
571
+ })
572
+ .map((c: any) => ({
573
+ id: c.id,
574
+ stable_id: c.data?.stable_id || '',
575
+ version_id: c.data?.version_id || null,
576
+ chunk_name: c.data?.chunk_name || '',
577
+ file_path: c.data?.file_path || '',
578
+ chunk_type: c.data?.chunk_type || '',
579
+ macro: c.data?.macro || '',
580
+ pending_vectors: vector_type
581
+ ? [vector_type]
582
+ : (c.data?.vectors_pending_human_review || []),
583
+ }))
584
+
585
+ return {
586
+ content: [{
587
+ type: 'text',
588
+ text: JSON.stringify({
589
+ ok: true,
590
+ project_id,
591
+ filter: vector_type || 'all',
592
+ pending_count: pending.length,
593
+ chunks: pending,
594
+ }, null, 2),
595
+ }],
596
+ }
597
+
598
+ }
599
+ )
600
+
601
+ server.registerTool(
602
+ 'spine_cil_set_vector_content',
603
+ {
604
+ description: `Store human-authored content for an implementation_pattern or user_journey vector on a chunk.
605
+
606
+ REQUIRED sequence — the file is always source of truth:
607
+ 1. Update the DOC-VECTOR tag in the source file on disk first
608
+ 2. Run spine_cil_store_markers to sync the file to the DB
609
+ 3. THEN call this tool to mark the vector ready for embedding
610
+
611
+ Do NOT call this tool before updating the file — that would create DB/file drift.`,
612
+ inputSchema: z.object({
613
+ project_id: z.string().uuid().describe('The cil_project UUID'),
614
+ stable_id: z.string().describe('The stable_id of the chunk to update'),
615
+ version_id: z.string().optional().describe('Optional version UUID to scope the lookup'),
616
+ vector_type: z.string().describe('The vector type to update, e.g. "implementation_pattern", "user_journey"'),
617
+ content: z.string().describe('The human-authored content for this vector'),
618
+ }),
619
+ },
620
+ async ({ project_id, stable_id, version_id, vector_type, content }) => {
621
+
622
+ const chunk = await getChunkByStableId(project_id, stable_id, version_id)
623
+
624
+ if (!chunk) {
625
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Chunk not found: ${stable_id}` }) }] }
626
+ }
627
+
628
+ const cd = chunk.data || {}
629
+
630
+ const vectorContent: Record<string, { content: string; status: string }> = { ...(cd.vector_content || {}) }
631
+
632
+ vectorContent[vector_type] = { content, status: 'ready' }
633
+
634
+ const pendingList: string[] = (cd.vectors_pending_human_review || []).filter((v: string) => v !== vector_type)
635
+
636
+ const now = new Date().toISOString()
637
+
638
+ const newData = {
639
+ ...cd,
640
+ vector_content: vectorContent,
641
+ vectors_pending_human_review: pendingList,
642
+ vector_content_updated_at: now,
643
+ }
644
+
645
+ const { error } = await adminDb.from('items')
646
+ .update({ data: newData, updated_at: now })
647
+ .eq('id', chunk.id)
648
+
649
+ if (error) {
650
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: error.message }) }] }
651
+ }
652
+
653
+ return {
654
+ content: [{
655
+ type: 'text',
656
+ text: JSON.stringify({
657
+ ok: true,
658
+ chunk_id: chunk.id,
659
+ stable_id,
660
+ vector_type,
661
+ content_length: content.length,
662
+ remaining_pending: pendingList,
663
+ note: 'DB updated. Also update the DOC-VECTOR tag in the source file and re-run spine_cil_store_markers to keep file as source of truth.',
664
+ }, null, 2),
665
+ }],
666
+ }
667
+ }
668
+ )
669
+
670
+ server.registerTool(
671
+ 'spine_cil_unmapped_files',
672
+ {
673
+ description: `List selected cil_file records for a CIL project that currently have no mapping links. A file is considered unmapped when neither the file id nor any of its chunk ids appear as source_id in links of type cil_imports, cil_calls, cil_exports, or cil_tested_by.`,
674
+ inputSchema: z.object({
675
+ project_id: z.string().uuid().describe('The cil_project UUID'),
676
+ limit: z.number().int().min(1).optional().describe('Maximum number of unmapped files to return (default 50)'),
677
+ }),
678
+ },
679
+ async ({ project_id, limit = 50 }) => {
680
+ const { data: projectRows } = await adminDb.from('items').select('data')
681
+ .filter('data->>kb_type', 'eq', 'cil_project')
682
+ .eq('id', project_id)
683
+ .eq('is_active', true)
684
+ .limit(1)
685
+
686
+ const project = (projectRows || [])[0]
687
+
688
+ if (!project) {
689
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
690
+ }
691
+
692
+ const selectedPaths: string[] = project.data?.selected_paths || []
693
+
694
+ if (selectedPaths.length === 0) {
695
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, total_selected: 0, unmapped_count: 0, unmapped: [], message: 'No selected_paths on project' }) }] }
696
+ }
697
+
698
+ const { data: files, error: fileError } = await adminDb.from('items').select('id, data')
699
+ .filter('data->>kb_type', 'eq', 'cil_file')
700
+ .filter('data->>project_id', 'eq', project_id)
701
+ .eq('is_active', true)
702
+ .limit(100000)
703
+
704
+ if (fileError) {
705
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch files: ${fileError.message}` }) }] }
706
+ }
707
+
708
+ const rawFiles = files || []
709
+
710
+ const selectedFiles = rawFiles.filter((f: any) => isSelectedPath(f.data?.file_path || f.title || '', selectedPaths))
711
+
712
+ const total_selected = selectedFiles.length
713
+
714
+ if (total_selected === 0) {
715
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, total_selected: 0, unmapped_count: 0, unmapped: [] }) }] }
716
+ }
717
+
718
+ const { data: chunks, error: chunkError } = await adminDb.from('items').select('id, data')
719
+ .filter('data->>kb_type', 'eq', 'cil_chunk')
720
+ .filter('data->>project_id', 'eq', project_id)
721
+ .eq('is_active', true)
722
+ .limit(100000)
723
+
724
+ if (chunkError) {
725
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch chunks: ${chunkError.message}` }) }] }
726
+ }
727
+
728
+ const chunksByFile: Record<string, any[]> = {}
729
+
730
+ for (const c of chunks || []) {
731
+
732
+ const fp = c.data?.file_path
733
+
734
+ if (fp) {
735
+ if (!chunksByFile[fp]) chunksByFile[fp] = []
736
+ chunksByFile[fp].push(c)
737
+ }
738
+
739
+ }
740
+
741
+ const linkTypeMap: Record<string, string> = {}
742
+
743
+ for (const slug of ['cil_imports', 'cil_calls', 'cil_exports', 'cil_tested_by']) {
744
+ try {
745
+ linkTypeMap[slug] = await resolveLinkTypeId(slug)
746
+ } catch {
747
+ // ignore missing link types
748
+ }
749
+
750
+ }
751
+
752
+ const linkTypeIds = Object.values(linkTypeMap).filter(Boolean)
753
+
754
+ const allSourceIds: string[] = []
755
+
756
+ for (const f of selectedFiles) {
757
+ allSourceIds.push(f.id)
758
+
759
+ const filePath = f.data?.file_path || f.title || ''
760
+
761
+ for (const c of chunksByFile[filePath] || []) {
762
+ allSourceIds.push(c.id)
763
+ }
764
+
765
+ }
766
+
767
+ const linkCounts: Record<string, number> = {}
768
+
769
+ const UUID_IN_LIMIT = 100
770
+
771
+ if (linkTypeIds.length > 0 && allSourceIds.length > 0) {
772
+ for (let i = 0; i < allSourceIds.length; i += UUID_IN_LIMIT) {
773
+
774
+ const batch = allSourceIds.slice(i, i + UUID_IN_LIMIT)
775
+
776
+ const { data: links, error: linkError } = await adminDb.from('links').select('id, link_type_id, source_id')
777
+ .in('link_type_id', linkTypeIds)
778
+ .in('source_id', batch)
779
+
780
+ if (linkError) {
781
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch links: ${linkError.message}` }) }] }
782
+ }
783
+
784
+ for (const l of links || []) {
785
+ linkCounts[l.source_id] = (linkCounts[l.source_id] || 0) + 1
786
+ }
787
+ }
788
+ }
789
+
790
+ const unmapped: { file_path: string; file_id: string; chunk_count: number; link_count: number }[] = []
791
+
792
+ for (const f of selectedFiles) {
793
+
794
+ const filePath = f.data?.file_path || f.title || ''
795
+
796
+ const fileChunks = chunksByFile[filePath] || []
797
+
798
+
799
+
800
+ let totalLinks = linkCounts[f.id] || 0
801
+
802
+
803
+ for (const c of fileChunks) {
804
+ totalLinks += linkCounts[c.id] || 0
805
+ }
806
+
807
+
808
+ if (totalLinks === 0) {
809
+ unmapped.push({
810
+ file_path: filePath,
811
+ file_id: f.id,
812
+ chunk_count: fileChunks.length,
813
+ link_count: 0,
814
+ })
815
+ }
816
+ }
817
+
818
+ unmapped.sort((a, b) => a.file_path.localeCompare(b.file_path))
819
+
820
+ const limited = unmapped.slice(0, limit)
821
+
822
+ return {
823
+ content: [{
824
+ type: 'text',
825
+ text: JSON.stringify({
826
+ ok: true,
827
+ project_id,
828
+ raw_file_count: rawFiles.length,
829
+ selected_paths_count: selectedPaths.length,
830
+ total_selected,
831
+ unmapped_count: unmapped.length,
832
+ limit,
833
+ unmapped: limited,
834
+ }, null, 2),
835
+ }],
836
+ }
837
+
838
+ }
839
+ )
840
+
841
+ server.registerTool(
842
+ 'spine_cil_validate_chunking',
843
+ {
844
+ description: `Validate whether a source file's CIL markers still match the current AST. Returns not_chunked, valid, or needs_rechunk, plus marker agreement details. Use this before deciding to chunk or rechunk a file.`,
845
+ inputSchema: z.object({
846
+ project_id: z.string().uuid().describe('The cil_project UUID'),
847
+ file_path: z.string().describe('Relative file path from repo root, e.g. ".framework/functions/cil-ingest.ts"'),
848
+ }),
849
+ },
850
+ async ({ project_id, file_path }) => {
851
+ try {
852
+
853
+ const result = await fetchFunction('cil-ingest', 'validate_chunking', {}, { project_id, file_path })
854
+
855
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
856
+ } catch (e: any) {
857
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: e.message || String(e) }) }] }
858
+ }
859
+
860
+ }
861
+ )
862
+
863
+ async function getCilFileId(project_id: string, file_path: string) {
864
+ const { data } = await adminDb
865
+ .from('items')
866
+ .select('id')
867
+ .filter('data->>project_id', 'eq', project_id)
868
+ .filter('data->>kb_type', 'eq', 'cil_file')
869
+ .filter('data->>file_path', 'eq', file_path)
870
+ .eq('is_active', true)
871
+ .maybeSingle()
872
+ if (!data) throw new Error(`No cil_file for path ${file_path}`)
873
+ return data.id
874
+ }
875
+
876
+ const STEP_SOURCE_STATUS: Record<number, string> = {
877
+ 3: '2-chunked',
878
+ 4: '3-documented',
879
+ 5: '4-chunk-validated',
880
+ 6: '5-doc-validated',
881
+ 7: '6-enriched',
882
+ 8: '7-embedded-mapped',
883
+ }
884
+
885
+ async function findNextFileForStep(project_id: string, sourceStatus: string, selectedPaths: string[]) {
886
+ const { data } = await adminDb
887
+ .from('items')
888
+ .select('id, data')
889
+ .filter('data->>project_id', 'eq', project_id)
890
+ .filter('data->>kb_type', 'eq', 'cil_file')
891
+ .filter('data->>status', 'eq', sourceStatus)
892
+ .eq('is_active', true)
893
+ const ready = (data || [])
894
+ .map((f: any) => f.data?.file_path || f.title)
895
+ .filter((fp: string) => isSelectedPath(fp, selectedPaths))
896
+ .sort((a: string, b: string) => a.localeCompare(b))
897
+ return ready[0] || null
898
+ }
899
+
900
+ async function runPipelineStep(project_id: string, step: number, limit: number, file_path?: string, force_reprocess?: boolean) {
901
+ const { data: projectRows } = await adminDb
902
+ .from('items')
903
+ .select('data')
904
+ .filter('data->>kb_type', 'eq', 'cil_project')
905
+ .eq('id', project_id)
906
+ .maybeSingle()
907
+
908
+ if (!projectRows) throw new Error('Project not found')
909
+ const project = projectRows.data || {}
910
+ const selectedPaths: string[] = project.selected_paths || []
911
+ const stepCursors: Record<string, any> = project.step_cursors || {}
912
+
913
+ if (step === 0 || step === 1 || step === 2) {
914
+ return await fetchFunction('cil-ingest', 'step', {}, { project_id, step, limit, file_path, force_reprocess })
915
+ }
916
+
917
+ const sourceStatus = STEP_SOURCE_STATUS[step]
918
+ if (!sourceStatus) throw new Error(`step ${step} is not supported`)
919
+
920
+ const stepCursorsKey = String(step)
921
+ const cursor = stepCursors[stepCursorsKey] || null
922
+ const targetPath: string = file_path || (cursor && typeof cursor === 'object' ? cursor.file_path : '')
923
+ const offset: number = cursor && typeof cursor === 'object' ? (cursor.offset || 0) : 0
924
+
925
+ if (!targetPath) {
926
+ if (step === 3) {
927
+ file_path = ''
928
+ } else {
929
+ const nextPath = await findNextFileForStep(project_id, sourceStatus, selectedPaths)
930
+ if (!nextPath) {
931
+ return { ok: true, step, status: 'complete', processed: 0, has_more: false, message: 'No files ready for this step' }
932
+ }
933
+ file_path = nextPath
934
+ }
935
+ } else {
936
+ file_path = targetPath
937
+ }
938
+
939
+ let result: any
940
+ if (step === 3) {
941
+ result = await fetchFunction('cil-doc', 'document', {}, { project_id, limit, offset })
942
+ } else if (step === 4 || step === 7) {
943
+ result = await fetchFunction('cil-embeddings', 'embed_approved', {}, { project_id, file_path, limit, offset, second_pass: step === 7 })
944
+ } else if (step === 5 || step === 8) {
945
+ const fileId = await getCilFileId(project_id, file_path)
946
+ result = await fetchFunction('cil-callmap', 'map_file', {}, { project_id, file_id: fileId })
947
+ } else if (step === 6) {
948
+ const repoPath = project.repo_path
949
+ if (!repoPath) throw new Error('Project has no repo_path')
950
+ result = await fetchFunction('cil-config', 'run', {}, { project_id, repo_path, file_path })
951
+ } else {
952
+ throw new Error(`step ${step} is not supported`)
953
+ }
954
+
955
+ const hasMore = result?.has_more === true
956
+ const nextOffset = hasMore ? (result?.next_offset ?? (offset + limit)) : 0
957
+ const newCursor = hasMore ? { file_path, offset: nextOffset } : null
958
+
959
+ const { data: projectAfter } = await adminDb
960
+ .from('items')
961
+ .select('data')
962
+ .eq('id', project_id)
963
+ .maybeSingle()
964
+
965
+ if (projectAfter) {
966
+ const now = new Date().toISOString()
967
+ const newCursors = { ...stepCursors, [stepCursorsKey]: newCursor }
968
+ await adminDb.from('items')
969
+ .update({ data: { ...projectAfter.data, step_cursors: newCursors }, updated_at: now })
970
+ .eq('id', project_id)
971
+ }
972
+
973
+ return {
974
+ ok: true,
975
+ step,
976
+ file_path,
977
+ status: hasMore ? 'in_progress' : 'in_progress',
978
+ has_more: hasMore,
979
+ next_offset: nextOffset,
980
+ ...result,
981
+ }
982
+ }
983
+
984
+ server.registerTool(
985
+ 'spine_cil_run_step',
986
+ {
987
+ description: `Run one unit of a CIL ingestion step (0-8). Steps 0-2 go through cil-ingest. Steps 3-8 dispatch to cil-doc, cil-embeddings, cil-callmap, or cil-config. For steps 3, 4, 7 the tool tracks page offset in project.data.step_cursors. Repeat calls until has_more is false and no more files are found.`,
988
+ inputSchema: z.object({
989
+ project_id: z.string().uuid().describe('The cil_project UUID'),
990
+ step: z.number().int().min(0).max(8).default(2).describe('Pipeline step number to run (0-8)'),
991
+ limit: z.number().int().min(1).default(1).describe('Page/batch size for steps 3, 4, 7. Steps 2, 5, 6, 8 process one file per call.'),
992
+ file_path: z.string().optional().describe('Optional specific file path to process; if omitted the next pending file is chosen from the cursor or the work queue'),
993
+ force_reprocess: z.boolean().optional().default(false).describe('Force reprocessing even if the file has already been chunked (step 2)'),
994
+ }),
995
+ },
996
+ async ({ project_id, step, limit, file_path, force_reprocess }) => {
997
+ try {
998
+
999
+ const result = await runPipelineStep(project_id, step, limit, file_path, force_reprocess)
1000
+
1001
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
1002
+ } catch (e: any) {
1003
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: e.message || String(e) }) }] }
1004
+ }
1005
+
1006
+ }
1007
+ )
1008
+
1009
+ server.registerTool(
1010
+ 'spine_cil_get_project_items',
1011
+ {
1012
+ description: `List active CIL file and/or chunk records for a project, optionally filtered by kb_type and file_path. Optionally include the raw chunk source (code_content) when include_source is true. Use this to build the id map for manual mapping or to fetch all chunks for a file in one call.`,
1013
+ inputSchema: z.object({
1014
+ project_id: z.string().uuid().describe('The cil_project UUID'),
1015
+ kb_type: z.enum(['cil_file', 'cil_chunk']).optional().describe('Filter by kb_type; omit to return both'),
1016
+ file_path: z.string().optional().describe('Filter by file_path'),
1017
+ chunk_name: z.string().optional().describe('Filter by chunk_name (exact match)'),
1018
+ include_source: z.boolean().optional().default(false).describe('Include the raw code_content for chunks; can be large'),
1019
+ limit: z.number().int().min(1).max(1000).optional().default(1000).describe('Maximum items to return (max 1000)'),
1020
+ offset: z.number().int().min(0).optional().default(0).describe('Pagination offset'),
1021
+ }),
1022
+ },
1023
+ async ({ project_id, kb_type, file_path, chunk_name, include_source = false, limit = 1000, offset = 0 }) => {
1024
+ const { data: project } = await adminDb.from('items').select('id, data, account_id')
1025
+ .filter('data->>kb_type', 'eq', 'cil_project')
1026
+ .eq('id', project_id)
1027
+ .maybeSingle()
1028
+
1029
+ if (!project) {
1030
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
1031
+ }
1032
+
1033
+ const repoPath: string = project.data?.repo_path || ''
1034
+ const accountId: string = project.account_id || ''
1035
+
1036
+ let q = adminDb.from('items').select('id, title, data')
1037
+ .filter('data->>project_id', 'eq', project_id)
1038
+ .eq('is_active', true)
1039
+ .order('id')
1040
+ .range(offset, offset + limit)
1041
+
1042
+ if (kb_type) q = q.filter('data->>kb_type', 'eq', kb_type)
1043
+ if (file_path) q = q.filter('data->>file_path', 'eq', file_path)
1044
+ if (chunk_name) q = q.filter('data->>chunk_name', 'eq', chunk_name)
1045
+
1046
+ const { data: rows, error } = await q
1047
+
1048
+ if (error) {
1049
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `DB query failed: ${error.message}` }) }] }
1050
+ }
1051
+
1052
+ const hasMore = (rows || []).length > limit
1053
+ const pageRows = (rows || []).slice(0, limit)
1054
+
1055
+ const items = pageRows.map((r: any) => {
1056
+ const cd = r.data || {}
1057
+ const out: any = {
1058
+ id: r.id,
1059
+ kb_type: cd.kb_type || '',
1060
+ file_path: cd.file_path || r.title || '',
1061
+ chunk_name: cd.chunk_name || '',
1062
+ stable_id: cd.stable_id || '',
1063
+ chunk_type: cd.chunk_type || '',
1064
+ line_start: cd.line_start || 0,
1065
+ line_end: cd.line_end || 0,
1066
+ macro: cd.macro || '',
1067
+ doc_confidence: cd.doc_confidence || '',
1068
+ }
1069
+ if (cd.kb_type === 'cil_chunk' && include_source) {
1070
+ if (cd.code_content) {
1071
+ out.code_content = cd.code_content
1072
+ } else if (repoPath && cd.file_path && cd.line_start && cd.line_end) {
1073
+ try {
1074
+ const abs = path.join(repoPath, cd.file_path.replace(/^\//, ''))
1075
+ const lines = fs.readFileSync(abs, 'utf-8').split('\n')
1076
+ const start = Math.max(0, (cd.line_start || 1) - 1)
1077
+ const end = Math.min(lines.length, cd.line_end || lines.length)
1078
+ out.code_content = lines.slice(start, end).join('\n')
1079
+ } catch {
1080
+ out.code_content = ''
1081
+ }
1082
+ } else {
1083
+ out.code_content = ''
1084
+ }
1085
+ }
1086
+ return out
1087
+ })
1088
+
1089
+ return {
1090
+ content: [{
1091
+ type: 'text',
1092
+ text: JSON.stringify({
1093
+ ok: true,
1094
+ project_id,
1095
+ account_id: accountId,
1096
+ repo_path: repoPath,
1097
+ kb_type: kb_type || null,
1098
+ file_path: file_path || null,
1099
+ include_source,
1100
+ limit,
1101
+ offset,
1102
+ returned: items.length,
1103
+ has_more: hasMore,
1104
+ next_offset: hasMore ? offset + limit : null,
1105
+ items,
1106
+ }, null, 2),
1107
+ }],
1108
+ }
1109
+ }
1110
+ )
1111
+
1112
+ server.registerTool(
1113
+ 'spine_cil_upsert_links',
1114
+ {
1115
+ description: `Batch upsert CIL mapping links (cil_calls, cil_imports, cil_exports) for a project. Provide source_id and target_id as CIL item UUIDs. If force is true, existing links for each source_id/link_type pair are cleared before inserting. Use this with spine_cil_get_project_items for manual, chunk-level mapping that does not wipe the whole graph.`,
1116
+ inputSchema: z.object({
1117
+ project_id: z.string().uuid().describe('The cil_project UUID'),
1118
+ links: z.array(z.object({
1119
+ source_id: z.string().uuid(),
1120
+ source_type: z.enum(['cil_file', 'cil_chunk']),
1121
+ target_id: z.string().uuid(),
1122
+ target_type: z.enum(['cil_file', 'cil_chunk']),
1123
+ link_type: z.enum(['cil_calls', 'cil_imports', 'cil_exports', 'cil_tested_by']),
1124
+ metadata: z.record(z.any()).optional(),
1125
+ })).min(1).max(1000).describe('Links to create'),
1126
+ force: z.boolean().optional().default(false).describe('When true, delete existing links for each source_id/link_type before creating new ones'),
1127
+ }),
1128
+ },
1129
+ async ({ project_id, links, force = false }) => {
1130
+ const { data: project } = await adminDb.from('items').select('id, data, account_id')
1131
+ .filter('data->>kb_type', 'eq', 'cil_project')
1132
+ .eq('id', project_id)
1133
+ .maybeSingle()
1134
+
1135
+ if (!project) {
1136
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
1137
+ }
1138
+
1139
+ const accountId: string = project.account_id || ''
1140
+ const linksTypeId = await resolveTypeId('link', 'link')
1141
+
1142
+ const linkTypeIds: Record<string, string> = {}
1143
+ for (const slug of ['cil_calls', 'cil_imports', 'cil_exports', 'cil_tested_by']) {
1144
+ try {
1145
+ linkTypeIds[slug] = await resolveLinkTypeId(slug)
1146
+ } catch {
1147
+ // ignore missing link types
1148
+ }
1149
+ }
1150
+
1151
+ const invalid = links.filter(l => !linkTypeIds[l.link_type])
1152
+ if (invalid.length > 0) {
1153
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Some links use an unknown link type', invalid }, null, 2) }] }
1154
+ }
1155
+
1156
+ if (force) {
1157
+ const linksByType: Record<string, string[]> = {}
1158
+ for (const l of links) {
1159
+ if (!linksByType[l.link_type]) linksByType[l.link_type] = []
1160
+ if (!linksByType[l.link_type].includes(l.source_id)) linksByType[l.link_type].push(l.source_id)
1161
+ }
1162
+ for (const [slug, ltId] of Object.entries(linkTypeIds)) {
1163
+ const sourceIds = linksByType[slug]
1164
+ if (!sourceIds || !ltId) continue
1165
+ for (let i = 0; i < sourceIds.length; i += 100) {
1166
+ await adminDb.from('links')
1167
+ .delete()
1168
+ .eq('link_type_id', ltId)
1169
+ .in('source_id', sourceIds.slice(i, i + 100))
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ const inserts = links.map((l: any) => ({
1175
+ type_id: linksTypeId,
1176
+ account_id: accountId,
1177
+ link_type_id: linkTypeIds[l.link_type],
1178
+ source_type: l.source_type,
1179
+ source_id: l.source_id,
1180
+ target_type: l.target_type,
1181
+ target_id: l.target_id,
1182
+ link_type: l.link_type,
1183
+ metadata: l.metadata || {},
1184
+ }))
1185
+
1186
+ let created = 0
1187
+ for (const row of inserts) {
1188
+ const { data, error } = await adminDb.from('links').insert(row).select('id')
1189
+ if (error) {
1190
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Insert failed: ${error.message}` }) }] }
1191
+ }
1192
+ if (data?.id) created++
1193
+ }
1194
+
1195
+ return {
1196
+ content: [{
1197
+ type: 'text',
1198
+ text: JSON.stringify({
1199
+ ok: true,
1200
+ project_id,
1201
+ requested: links.length,
1202
+ created,
1203
+ force,
1204
+ }, null, 2),
1205
+ }],
1206
+ }
1207
+ }
1208
+ )
1209
+ }
1210
+
1211
+ // CIL[DOC-VECTOR type="context" content="purpose='Register all CIL ingest, discovery, chunking, embedding, mapping, and pipeline orchestration MCP tools on a shared McpServer'; No verified external callers; imports: .framework/mcp/shared/db.ts,.framework/functions/_shared/cil.ts"]
1212
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
1213
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
1214
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
1215
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1216
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1217
+ // CIL[END stable_id="~/.framework/mcp/tools/cil/ingest.ts:mcp_ingest_tools:file"]