spine-framework 0.4.4 → 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.
Files changed (283) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  15. package/.framework/functions/_shared/logging.ts +562 -224
  16. package/.framework/functions/_shared/middleware.ts +47 -44
  17. package/.framework/functions/admin-data.ts +39 -28
  18. package/.framework/functions/ai-agents.ts +10 -6
  19. package/.framework/functions/api-keys.ts +301 -122
  20. package/.framework/functions/apps.ts +43 -26
  21. package/.framework/functions/cil-ingest.ts +48 -15
  22. package/.framework/functions/debug-auth.ts +14 -8
  23. package/.framework/functions/deliveries.ts +39 -12
  24. package/.framework/functions/embeddings.ts +145 -76
  25. package/.framework/functions/integration-outbound.ts +67 -27
  26. package/.framework/functions/integrations.ts +254 -127
  27. package/.framework/functions/logs.ts +273 -133
  28. package/.framework/functions/observability.ts +277 -147
  29. package/.framework/functions/pipeline-executions.ts +1 -1
  30. package/.framework/functions/pipelines.ts +14 -9
  31. package/.framework/functions/prompt-configs.ts +7 -4
  32. package/.framework/functions/roles.ts +8 -5
  33. package/.framework/functions/security.ts +30 -17
  34. package/.framework/functions/system-cron.ts +240 -127
  35. package/.framework/functions/tests.ts +7 -6
  36. package/.framework/functions/timers.ts +310 -107
  37. package/.framework/functions/triggers.ts +28 -13
  38. package/.framework/functions/types.ts +13 -10
  39. package/.framework/migrations/069_log_stats_window.sql +54 -0
  40. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  41. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  42. package/.framework/migrations/072_slug_autofill.sql +77 -0
  43. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  44. package/.framework/src/apps/admin/index.tsx +1141 -775
  45. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  46. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  47. package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
  48. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  49. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  50. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  51. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  52. package/.framework/src/components/layout/Sidebar.tsx +300 -177
  53. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  54. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  55. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  56. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  57. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  58. package/.framework/src/components/ui/sheet.tsx +24 -26
  59. package/.framework/src/components/ui/textarea.tsx +74 -48
  60. package/.framework/src/hooks/useEntityList.ts +12 -9
  61. package/.framework/src/hooks/useListSchema.ts +108 -58
  62. package/.framework/src/instructions/README.md +57 -0
  63. package/.framework/src/instructions/ai-agents.md +69 -0
  64. package/.framework/src/instructions/alerts.md +80 -0
  65. package/.framework/src/instructions/api-keys.md +87 -0
  66. package/.framework/src/instructions/apps.md +77 -0
  67. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  68. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  69. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  70. package/.framework/src/instructions/concepts/automation.md +186 -0
  71. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  72. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  73. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  74. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  75. package/.framework/src/instructions/concepts/observability.md +116 -0
  76. package/.framework/src/instructions/concepts/permissions.md +150 -0
  77. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  78. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  79. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  80. package/.framework/src/instructions/deliveries.md +59 -0
  81. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  82. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  83. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  84. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  85. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  86. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  87. package/.framework/src/instructions/integrations.md +96 -0
  88. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  89. package/.framework/src/instructions/invites.md +60 -0
  90. package/.framework/src/instructions/kb.md +80 -0
  91. package/.framework/src/instructions/logs.md +77 -0
  92. package/.framework/src/instructions/observability.md +61 -0
  93. package/.framework/src/instructions/operations/deployment.md +115 -0
  94. package/.framework/src/instructions/operations/migrations.md +125 -0
  95. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  96. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  97. package/.framework/src/instructions/operations/upgrade.md +116 -0
  98. package/.framework/src/instructions/pipeline-executions.md +62 -0
  99. package/.framework/src/instructions/pipelines.md +78 -0
  100. package/.framework/src/instructions/prompts.md +95 -0
  101. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  102. package/.framework/src/instructions/reference/api/auth.md +113 -0
  103. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  104. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  105. package/.framework/src/instructions/reference/cli.md +124 -0
  106. package/.framework/src/instructions/reference/database.md +132 -0
  107. package/.framework/src/instructions/reference/design-schema.md +280 -0
  108. package/.framework/src/instructions/reference/env-vars.md +133 -0
  109. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  110. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  111. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  112. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  113. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  114. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  115. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  116. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  117. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  118. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  119. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  120. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  121. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  122. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  123. package/.framework/src/instructions/reference/functions/index.md +103 -0
  124. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  125. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  126. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  127. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  128. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  129. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  130. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  131. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  132. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  133. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  134. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  135. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  136. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  137. package/.framework/src/instructions/reference/functions/security.md +140 -0
  138. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  139. package/.framework/src/instructions/reference/functions/system.md +106 -0
  140. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  141. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  142. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  143. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  144. package/.framework/src/instructions/reference/functions/types.md +146 -0
  145. package/.framework/src/instructions/reference/seeds.md +126 -0
  146. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  147. package/.framework/src/instructions/reference/spine-config.md +114 -0
  148. package/.framework/src/instructions/roles.md +80 -0
  149. package/.framework/src/instructions/runtime-detail.md +70 -0
  150. package/.framework/src/instructions/runtime-list.md +77 -0
  151. package/.framework/src/instructions/security.md +67 -0
  152. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  153. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  154. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  155. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  156. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  157. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  158. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  159. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  160. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  161. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  162. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  163. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  164. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  165. package/.framework/src/instructions/testing.md +59 -0
  166. package/.framework/src/instructions/timers.md +82 -0
  167. package/.framework/src/instructions/triggers.md +69 -0
  168. package/.framework/src/instructions/types.md +93 -0
  169. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  170. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  171. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  172. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  173. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  174. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  175. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  176. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  177. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  178. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  179. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  180. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  181. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  182. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  183. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  184. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  185. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  186. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  187. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  188. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  189. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  190. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  191. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  192. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  193. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  194. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  195. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  196. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  197. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  198. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  199. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  200. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  201. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  202. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  203. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  204. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  205. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  206. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  207. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  208. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  209. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  210. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  211. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  212. package/.framework/templates/agent/AGENTS.md +150 -0
  213. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  214. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  215. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  216. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  217. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  218. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  219. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  220. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  221. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  222. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  223. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  224. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  225. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  226. package/.framework/templates/project.json +4 -1
  227. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  228. package/.framework/tests/unit/agent-files.test.ts +355 -0
  229. package/.framework/tests/unit/agent-runner.test.ts +479 -44
  230. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  231. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  232. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  233. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  234. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  235. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  236. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  237. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  238. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  239. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  240. package/.framework/tests/unit/system-cron.test.ts +240 -0
  241. package/.framework/tests/unit/timers.test.ts +159 -0
  242. package/README.md +125 -166
  243. package/config/tsconfig.cli.json +2 -2
  244. package/package.json +15 -4
  245. package/templates/project/package-lock.json +2 -2
  246. package/.framework/docs/admin-routes.md +0 -76
  247. package/.framework/docs/api-docs-progress.md +0 -38
  248. package/.framework/docs/api-governance.md +0 -146
  249. package/.framework/docs/api-testing-results.md +0 -212
  250. package/.framework/docs/assembly-launch-guide.md +0 -138
  251. package/.framework/docs/audit-results.md +0 -590
  252. package/.framework/docs/db-api-inventory.md +0 -95
  253. package/.framework/docs/gap-remediation-backlog.md +0 -103
  254. package/.framework/docs/ui-adoption-verification.md +0 -111
  255. package/.framework/docs/ui-api-coverage.md +0 -84
  256. package/.framework/docs/v2-compatibility-audit.md +0 -228
  257. package/.framework/mcp/index.ts +0 -133
  258. package/.framework/mcp/load-env.ts +0 -60
  259. package/.framework/mcp/shared/app-manifest.ts +0 -275
  260. package/.framework/mcp/shared/db.ts +0 -477
  261. package/.framework/mcp/shared/kb.ts +0 -118
  262. package/.framework/mcp/tools/architect.ts +0 -431
  263. package/.framework/mcp/tools/cil/context.ts +0 -345
  264. package/.framework/mcp/tools/cil/graph.ts +0 -242
  265. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  266. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  267. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  268. package/.framework/mcp/tools/cil/project.ts +0 -791
  269. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  270. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  271. package/.framework/mcp/tools/cil/search.ts +0 -97
  272. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  273. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  274. package/.framework/mcp/tools/kb/delete.ts +0 -139
  275. package/.framework/mcp/tools/kb/index.ts +0 -52
  276. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  277. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  278. package/.framework/mcp/tools/kb/publish.ts +0 -100
  279. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  280. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  281. package/.framework/mcp/tools/kb/search.ts +0 -101
  282. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  283. package/.framework/mcp/tools/observability.ts +0 -510
@@ -1,1217 +0,0 @@
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"]