neotoma 0.3.0 → 0.3.2

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 (1699) hide show
  1. package/README.md +9 -9
  2. package/dist/actions.js +2 -2
  3. package/dist/actions.js.map +1 -1
  4. package/dist/cli/bootstrap.js +0 -0
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +158 -5
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/crypto/auth.js +1 -1
  9. package/dist/crypto/auth.js.map +1 -1
  10. package/dist/mcp_ws_bridge.js +1 -1
  11. package/dist/mcp_ws_bridge.js.map +1 -1
  12. package/dist/server.js +2 -2
  13. package/dist/server.js.map +1 -1
  14. package/dist/services/field_converters.js +3 -3
  15. package/dist/services/field_converters.js.map +1 -1
  16. package/dist/services/field_validation.js +1 -1
  17. package/dist/services/field_validation.js.map +1 -1
  18. package/dist/services/mcp_auth.js +1 -1
  19. package/dist/services/mcp_auth.js.map +1 -1
  20. package/dist/services/mcp_oauth.js +1 -1
  21. package/dist/services/mcp_oauth.js.map +1 -1
  22. package/dist/services/parquet_reader.js +1 -1
  23. package/dist/services/parquet_reader.js.map +1 -1
  24. package/dist/services/public_key_registry.js +2 -2
  25. package/dist/services/public_key_registry.js.map +1 -1
  26. package/package.json +27 -7
  27. package/.claude/CLAUDE.md +0 -81
  28. package/.claude/rules/agent_constraints.md +0 -201
  29. package/.claude/rules/agent_instructions_sync_rules.md +0 -100
  30. package/.claude/rules/agent_test_execution.md +0 -358
  31. package/.claude/rules/autonomous_execution.md +0 -119
  32. package/.claude/rules/bug_fix_detection.md +0 -145
  33. package/.claude/rules/bug_learning.md +0 -154
  34. package/.claude/rules/checkpoint_management.md +0 -194
  35. package/.claude/rules/configuration_management.md +0 -159
  36. package/.claude/rules/content_style_enforcement.md +0 -141
  37. package/.claude/rules/dependency_installation.md +0 -148
  38. package/.claude/rules/document_loading_order.md +0 -197
  39. package/.claude/rules/documentation_rules.md +0 -169
  40. package/.claude/rules/downstream_doc_updates.md +0 -269
  41. package/.claude/rules/env_check.md +0 -55
  42. package/.claude/rules/environment_variables_1password.md +0 -315
  43. package/.claude/rules/environment_variables_env_file.md +0 -124
  44. package/.claude/rules/feature_unit_detection.md +0 -132
  45. package/.claude/rules/file_naming.md +0 -34
  46. package/.claude/rules/git_remote_sync.md +0 -242
  47. package/.claude/rules/instruction_documentation.md +0 -287
  48. package/.claude/rules/native_browser_debugging.md +0 -68
  49. package/.claude/rules/plan_execution_testing.md +0 -101
  50. package/.claude/rules/plan_format.md +0 -38
  51. package/.claude/rules/post_build_testing.md +0 -162
  52. package/.claude/rules/prefer_cli_tools.md +0 -233
  53. package/.claude/rules/readme_maintenance.md +0 -156
  54. package/.claude/rules/release_detection.md +0 -95
  55. package/.claude/rules/release_status_readme_update.md +0 -62
  56. package/.claude/rules/risk_management.md +0 -170
  57. package/.claude/rules/security.md +0 -168
  58. package/.claude/rules/social_media_conventions.md +0 -42
  59. package/.claude/rules/test_first_workflow.md +0 -344
  60. package/.claude/rules/worktree_env.md +0 -56
  61. package/.claude/settings.json +0 -5
  62. package/.claude/skills/commit.md +0 -762
  63. package/.claude/skills/create_feature_unit.md +0 -176
  64. package/.claude/skills/create_release.md +0 -428
  65. package/.claude/skills/fix_feature_bug.md +0 -93
  66. package/.claude/skills/pull.md +0 -314
  67. package/.claude/skills/setup_symlinks.md +0 -228
  68. package/.codex/config.toml +0 -92
  69. package/.cursor/commands/analyze.md +0 -574
  70. package/.cursor/commands/commit.md +0 -768
  71. package/.cursor/commands/create_feature_unit.md +0 -182
  72. package/.cursor/commands/create_prototype.md +0 -95
  73. package/.cursor/commands/create_release.md +0 -434
  74. package/.cursor/commands/create_rule.md +0 -296
  75. package/.cursor/commands/debug.md +0 -985
  76. package/.cursor/commands/final_review.md +0 -97
  77. package/.cursor/commands/fix_feature_bug.md +0 -99
  78. package/.cursor/commands/manage_error_debugging.md +0 -186
  79. package/.cursor/commands/migrate_plans.md +0 -499
  80. package/.cursor/commands/publish.md +0 -559
  81. package/.cursor/commands/pull.md +0 -320
  82. package/.cursor/commands/push.md +0 -259
  83. package/.cursor/commands/report.md +0 -1506
  84. package/.cursor/commands/report_error.md +0 -1505
  85. package/.cursor/commands/run_feature_workflow.md +0 -83
  86. package/.cursor/commands/setup_commands.md +0 -87
  87. package/.cursor/commands/setup_symlinks.md +0 -234
  88. package/.cursor/commands/sync_env_from_1password.md +0 -321
  89. package/.cursor/commands.json +0 -100
  90. package/.cursor/environment.json +0 -5
  91. package/.cursor/mcp.json.template +0 -16
  92. package/.cursor/plans/agent_skills_analysis_aac0f1b6.plan.md +0 -949
  93. package/.cursor/plans/agent_trust_framework_eb38ca7d.plan.md +0 -634
  94. package/.cursor/plans/all-storage-unstructured_provenance_df0b7a8f.plan.md +0 -112
  95. package/.cursor/plans/automated_error_debugging_cloud_agents.plan.md +0 -526
  96. package/.cursor/plans/automated_error_debugging_cursor_cli.plan.md +0 -599
  97. package/.cursor/plans/automated_error_debugging_localhost_clarified.plan.md +0 -510
  98. package/.cursor/plans/bug_report_and_contribution_client_guidance.plan.md +0 -143
  99. package/.cursor/plans/convert_1password_sync_to_mcp_343ca61c.plan.md +0 -282
  100. package/.cursor/plans/debug_pending_errors_command.plan.md +0 -531
  101. package/.cursor/plans/error_reporting_command_8868f27b.plan.md +0 -255
  102. package/.cursor/plans/mcp-cli-action-item-01-openapi-core.plan.md +0 -30
  103. package/.cursor/plans/mcp-cli-action-item-02-openapi-mcp-cli-mapping.plan.md +0 -18
  104. package/.cursor/plans/mcp-cli-action-item-03-cli-equivalent-logging.plan.md +0 -18
  105. package/.cursor/plans/mcp-cli-action-item-06-error-taxonomy.plan.md +0 -18
  106. package/.cursor/plans/mcp-cli-action-item-07-fixture-replay-graphs.plan.md +0 -18
  107. package/.cursor/plans/mcp-cli-action-item-08-cli-reference-executor.plan.md +0 -18
  108. package/.cursor/plans/mcp-cli-action-item-09-mcp-tools-atomic.plan.md +0 -18
  109. package/.cursor/plans/mcp-cli-action-item-10-canonical-walkthrough.plan.md +0 -18
  110. package/.cursor/plans/mcp-cli-action-item-11-fixtures-expected-outputs.plan.md +0 -18
  111. package/.cursor/plans/mcp-cli-action-item-12-failure-gallery.plan.md +0 -18
  112. package/.cursor/plans/mcp_source-path_documentation_5ce48609.plan.md +0 -83
  113. package/.cursor/plans/me_and_get_authenticated_user_response_updates.plan.md +0 -125
  114. package/.cursor/plans/multi-llm_provider_support_3fdeb361.plan.md +0 -120
  115. package/.cursor/plans/plan_migration_command_integrated.plan.md +0 -393
  116. package/.cursor/plans/schema_icon_enhancements_plan.md +0 -533
  117. package/.cursor/plans/site-react-tailwind-parity.plan.md +0 -82
  118. package/.cursor/plans/sources-first-ingestion-v8.plan.md +0 -0
  119. package/.cursor/plans/sources-first_ingestion_v10_baef2ba2.plan.md +0 -363
  120. package/.cursor/plans/test_coverage_gap_analysis_9cb70022.plan.md +0 -39
  121. package/.cursor/port_to_foundation_analysis.md +0 -217
  122. package/.cursor/rules/agent_constraints.mdc +0 -210
  123. package/.cursor/rules/autonomous_execution.mdc +0 -128
  124. package/.cursor/rules/configuration_management.mdc +0 -168
  125. package/.cursor/rules/cursor_rules_editing.mdc +0 -99
  126. package/.cursor/rules/cursor_rules_sync.mdc +0 -74
  127. package/.cursor/rules/cursor_rules_sync_requirement.mdc +0 -156
  128. package/.cursor/rules/developer_agent_instructions_sync_rules.mdc +0 -109
  129. package/.cursor/rules/document_loading_order.mdc +0 -206
  130. package/.cursor/rules/instruction_documentation.mdc +0 -296
  131. package/.cursor/rules/risk_management.mdc +0 -179
  132. package/.cursor/rules/security.mdc +0 -177
  133. package/.cursor/skipped_records/companies_skipped.json +0 -27
  134. package/.cursor/skipped_records/contacts_skipped.json +0 -23
  135. package/.cursor/skipped_records/goals_skipped.json +0 -7
  136. package/.cursor/skipped_records/people_skipped.json +0 -14
  137. package/.cursor/skipped_records/projects_skipped.json +0 -39
  138. package/.cursor/worktrees.json +0 -12
  139. package/.dockerignore +0 -9
  140. package/.env.example +0 -84
  141. package/.eslintrc.json +0 -11
  142. package/.github/workflows/deploy-pages-site.yml +0 -55
  143. package/.gitmodules +0 -12
  144. package/.husky/pre-commit +0 -42
  145. package/.mcp.json +0 -13
  146. package/.nvmrc +0 -1
  147. package/.prettierrc.json +0 -7
  148. package/.vscode/extensions.json +0 -5
  149. package/.vscode/settings.json +0 -47
  150. package/CONTRIBUTING.md +0 -23
  151. package/Dockerfile +0 -28
  152. package/SECURITY.md +0 -41
  153. package/components.json +0 -21
  154. package/cursor_rules_manifest.json +0 -28
  155. package/dist/__fixtures__/helpers.d.ts +0 -91
  156. package/dist/__fixtures__/helpers.d.ts.map +0 -1
  157. package/dist/__fixtures__/helpers.js +0 -384
  158. package/dist/__fixtures__/helpers.js.map +0 -1
  159. package/dist/__fixtures__/types.d.ts +0 -285
  160. package/dist/__fixtures__/types.d.ts.map +0 -1
  161. package/dist/__fixtures__/types.js +0 -7
  162. package/dist/__fixtures__/types.js.map +0 -1
  163. package/dist/__fixtures__/validation.d.ts +0 -45
  164. package/dist/__fixtures__/validation.d.ts.map +0 -1
  165. package/dist/__fixtures__/validation.js +0 -311
  166. package/dist/__fixtures__/validation.js.map +0 -1
  167. package/dist/config/plaid.d.ts +0 -21
  168. package/dist/config/plaid.d.ts.map +0 -1
  169. package/dist/config/plaid.js +0 -38
  170. package/dist/config/plaid.js.map +0 -1
  171. package/dist/events/event_emitter.d.ts +0 -24
  172. package/dist/events/event_emitter.d.ts.map +0 -1
  173. package/dist/events/event_emitter.js +0 -85
  174. package/dist/events/event_emitter.js.map +0 -1
  175. package/dist/events/event_log.d.ts +0 -23
  176. package/dist/events/event_log.d.ts.map +0 -1
  177. package/dist/events/event_log.js +0 -77
  178. package/dist/events/event_log.js.map +0 -1
  179. package/dist/events/event_schema.d.ts +0 -58
  180. package/dist/events/event_schema.d.ts.map +0 -1
  181. package/dist/events/event_schema.js +0 -43
  182. package/dist/events/event_schema.js.map +0 -1
  183. package/dist/events/event_validator.d.ts +0 -23
  184. package/dist/events/event_validator.d.ts.map +0 -1
  185. package/dist/events/event_validator.js +0 -90
  186. package/dist/events/event_validator.js.map +0 -1
  187. package/dist/events/index.d.ts +0 -9
  188. package/dist/events/index.d.ts.map +0 -1
  189. package/dist/events/index.js +0 -9
  190. package/dist/events/index.js.map +0 -1
  191. package/dist/events/replay.d.ts +0 -27
  192. package/dist/events/replay.d.ts.map +0 -1
  193. package/dist/events/replay.js +0 -62
  194. package/dist/events/replay.js.map +0 -1
  195. package/dist/hashing/hash_chain.d.ts +0 -31
  196. package/dist/hashing/hash_chain.d.ts.map +0 -1
  197. package/dist/hashing/hash_chain.js +0 -52
  198. package/dist/hashing/hash_chain.js.map +0 -1
  199. package/dist/integrations/plaid/client.d.ts +0 -53
  200. package/dist/integrations/plaid/client.d.ts.map +0 -1
  201. package/dist/integrations/plaid/client.js +0 -224
  202. package/dist/integrations/plaid/client.js.map +0 -1
  203. package/dist/integrations/plaid/normalizers.d.ts +0 -26
  204. package/dist/integrations/plaid/normalizers.d.ts.map +0 -1
  205. package/dist/integrations/plaid/normalizers.js +0 -166
  206. package/dist/integrations/plaid/normalizers.js.map +0 -1
  207. package/dist/integrations/providers/asana.d.ts +0 -9
  208. package/dist/integrations/providers/asana.d.ts.map +0 -1
  209. package/dist/integrations/providers/asana.js +0 -62
  210. package/dist/integrations/providers/asana.js.map +0 -1
  211. package/dist/integrations/providers/base.d.ts +0 -17
  212. package/dist/integrations/providers/base.d.ts.map +0 -1
  213. package/dist/integrations/providers/base.js +0 -64
  214. package/dist/integrations/providers/base.js.map +0 -1
  215. package/dist/integrations/providers/facebook.d.ts +0 -9
  216. package/dist/integrations/providers/facebook.d.ts.map +0 -1
  217. package/dist/integrations/providers/facebook.js +0 -58
  218. package/dist/integrations/providers/facebook.js.map +0 -1
  219. package/dist/integrations/providers/gmail.d.ts +0 -10
  220. package/dist/integrations/providers/gmail.d.ts.map +0 -1
  221. package/dist/integrations/providers/gmail.js +0 -67
  222. package/dist/integrations/providers/gmail.js.map +0 -1
  223. package/dist/integrations/providers/index.d.ts +0 -6
  224. package/dist/integrations/providers/index.d.ts.map +0 -1
  225. package/dist/integrations/providers/index.js +0 -25
  226. package/dist/integrations/providers/index.js.map +0 -1
  227. package/dist/integrations/providers/instagram.d.ts +0 -9
  228. package/dist/integrations/providers/instagram.d.ts.map +0 -1
  229. package/dist/integrations/providers/instagram.js +0 -47
  230. package/dist/integrations/providers/instagram.js.map +0 -1
  231. package/dist/integrations/providers/metadata.d.ts +0 -25
  232. package/dist/integrations/providers/metadata.d.ts.map +0 -1
  233. package/dist/integrations/providers/metadata.js +0 -407
  234. package/dist/integrations/providers/metadata.js.map +0 -1
  235. package/dist/integrations/providers/types.d.ts +0 -39
  236. package/dist/integrations/providers/types.d.ts.map +0 -1
  237. package/dist/integrations/providers/types.js +0 -2
  238. package/dist/integrations/providers/types.js.map +0 -1
  239. package/dist/integrations/providers/x.d.ts +0 -9
  240. package/dist/integrations/providers/x.d.ts.map +0 -1
  241. package/dist/integrations/providers/x.js +0 -78
  242. package/dist/integrations/providers/x.js.map +0 -1
  243. package/dist/reducers/record_reducer.d.ts +0 -25
  244. package/dist/reducers/record_reducer.d.ts.map +0 -1
  245. package/dist/reducers/record_reducer.js +0 -93
  246. package/dist/reducers/record_reducer.js.map +0 -1
  247. package/dist/reducers/reducer_registry.d.ts +0 -47
  248. package/dist/reducers/reducer_registry.d.ts.map +0 -1
  249. package/dist/reducers/reducer_registry.js +0 -82
  250. package/dist/reducers/reducer_registry.js.map +0 -1
  251. package/dist/repositories/db/event_repo.d.ts +0 -14
  252. package/dist/repositories/db/event_repo.d.ts.map +0 -1
  253. package/dist/repositories/db/event_repo.js +0 -67
  254. package/dist/repositories/db/event_repo.js.map +0 -1
  255. package/dist/repositories/db/state_repo.d.ts +0 -14
  256. package/dist/repositories/db/state_repo.d.ts.map +0 -1
  257. package/dist/repositories/db/state_repo.js +0 -61
  258. package/dist/repositories/db/state_repo.js.map +0 -1
  259. package/dist/repositories/file/event_repo.d.ts +0 -18
  260. package/dist/repositories/file/event_repo.d.ts.map +0 -1
  261. package/dist/repositories/file/event_repo.js +0 -148
  262. package/dist/repositories/file/event_repo.js.map +0 -1
  263. package/dist/repositories/file/state_repo.d.ts +0 -21
  264. package/dist/repositories/file/state_repo.d.ts.map +0 -1
  265. package/dist/repositories/file/state_repo.js +0 -89
  266. package/dist/repositories/file/state_repo.js.map +0 -1
  267. package/dist/repositories/sqlite/supabase_adapter.d.ts +0 -135
  268. package/dist/repositories/sqlite/supabase_adapter.d.ts.map +0 -1
  269. package/dist/repositories/sqlite/supabase_adapter.js +0 -810
  270. package/dist/repositories/sqlite/supabase_adapter.js.map +0 -1
  271. package/dist/services/connectors.d.ts +0 -89
  272. package/dist/services/connectors.d.ts.map +0 -1
  273. package/dist/services/connectors.js +0 -248
  274. package/dist/services/connectors.js.map +0 -1
  275. package/dist/services/event_generation.d.ts +0 -37
  276. package/dist/services/event_generation.d.ts.map +0 -1
  277. package/dist/services/event_generation.js +0 -236
  278. package/dist/services/event_generation.js.map +0 -1
  279. package/dist/services/extraction/rules.d.ts +0 -26
  280. package/dist/services/extraction/rules.d.ts.map +0 -1
  281. package/dist/services/extraction/rules.js +0 -691
  282. package/dist/services/extraction/rules.js.map +0 -1
  283. package/dist/services/file_analysis.d.ts +0 -38
  284. package/dist/services/file_analysis.d.ts.map +0 -1
  285. package/dist/services/file_analysis.js +0 -325
  286. package/dist/services/file_analysis.js.map +0 -1
  287. package/dist/services/graph_builder.d.ts +0 -51
  288. package/dist/services/graph_builder.d.ts.map +0 -1
  289. package/dist/services/graph_builder.js +0 -279
  290. package/dist/services/graph_builder.js.map +0 -1
  291. package/dist/services/importers.d.ts +0 -28
  292. package/dist/services/importers.d.ts.map +0 -1
  293. package/dist/services/importers.js +0 -174
  294. package/dist/services/importers.js.map +0 -1
  295. package/dist/services/observation_ingestion.d.ts +0 -26
  296. package/dist/services/observation_ingestion.d.ts.map +0 -1
  297. package/dist/services/observation_ingestion.js +0 -364
  298. package/dist/services/observation_ingestion.js.map +0 -1
  299. package/dist/services/payload_compiler.d.ts +0 -36
  300. package/dist/services/payload_compiler.d.ts.map +0 -1
  301. package/dist/services/payload_compiler.js +0 -121
  302. package/dist/services/payload_compiler.js.map +0 -1
  303. package/dist/services/payload_identity.d.ts +0 -35
  304. package/dist/services/payload_identity.d.ts.map +0 -1
  305. package/dist/services/payload_identity.js +0 -101
  306. package/dist/services/payload_identity.js.map +0 -1
  307. package/dist/services/payload_schema.d.ts +0 -104
  308. package/dist/services/payload_schema.d.ts.map +0 -1
  309. package/dist/services/payload_schema.js +0 -46
  310. package/dist/services/payload_schema.js.map +0 -1
  311. package/dist/services/plaid_sync.d.ts +0 -77
  312. package/dist/services/plaid_sync.d.ts.map +0 -1
  313. package/dist/services/plaid_sync.js +0 -273
  314. package/dist/services/plaid_sync.js.map +0 -1
  315. package/dist/services/record_comparison.d.ts +0 -29
  316. package/dist/services/record_comparison.d.ts.map +0 -1
  317. package/dist/services/record_comparison.js +0 -84
  318. package/dist/services/record_comparison.js.map +0 -1
  319. package/dist/services/records.d.ts +0 -31
  320. package/dist/services/records.d.ts.map +0 -1
  321. package/dist/services/records.js +0 -184
  322. package/dist/services/records.js.map +0 -1
  323. package/dist/services/search.d.ts +0 -15
  324. package/dist/services/search.d.ts.map +0 -1
  325. package/dist/services/search.js +0 -81
  326. package/dist/services/search.js.map +0 -1
  327. package/dist/services/summary.d.ts +0 -5
  328. package/dist/services/summary.d.ts.map +0 -1
  329. package/dist/services/summary.js +0 -118
  330. package/dist/services/summary.js.map +0 -1
  331. package/docs/NEOTOMA_MANIFEST.md +0 -152
  332. package/docs/api/rest_api.md +0 -466
  333. package/docs/architecture/agentic_portfolio_overview.md +0 -99
  334. package/docs/architecture/agentic_wallet_overview.md +0 -93
  335. package/docs/architecture/architectural_decisions.md +0 -462
  336. package/docs/architecture/architecture.md +0 -842
  337. package/docs/architecture/blockchain_readiness_assessment.md +0 -248
  338. package/docs/architecture/consistency.md +0 -539
  339. package/docs/architecture/conversational_ux_architecture.md +0 -240
  340. package/docs/architecture/determinism.md +0 -762
  341. package/docs/architecture/idempotence_pattern.md +0 -139
  342. package/docs/architecture/mcp_actions_assessment.md +0 -182
  343. package/docs/architecture/ner_vs_schema_first_extraction.md +0 -280
  344. package/docs/architecture/schema_expansion.md +0 -260
  345. package/docs/architecture/schema_handling.md +0 -209
  346. package/docs/architecture/source_material_model.md +0 -281
  347. package/docs/assets/neotoma_banner.png +0 -0
  348. package/docs/conventions/TEST_EXECUTION_IMPROVEMENTS.md +0 -205
  349. package/docs/conventions/always_on_rules_review.md +0 -66
  350. package/docs/conventions/code_conventions_implementation.md +0 -111
  351. package/docs/conventions/code_conventions_plan.md +0 -124
  352. package/docs/conventions/code_review_summary.md +0 -96
  353. package/docs/conventions/documentation_standards.md +0 -104
  354. package/docs/conventions/estimation_methodology.md +0 -146
  355. package/docs/conventions/file_naming_migration_status.md +0 -94
  356. package/docs/conventions/readme_generation_framework.md +0 -230
  357. package/docs/conventions/time_tracking_template.md +0 -60
  358. package/docs/conventions/writing_style_guide.md +0 -252
  359. package/docs/developer/agent_cli_configuration.md +0 -140
  360. package/docs/developer/agent_instructions_sync_rules.mdc +0 -97
  361. package/docs/developer/canonical_walkthrough.md +0 -137
  362. package/docs/developer/cli_agent_instructions.md +0 -141
  363. package/docs/developer/cli_overview.md +0 -207
  364. package/docs/developer/cli_reference.md +0 -518
  365. package/docs/developer/developer_preview_launch_checklist.md +0 -128
  366. package/docs/developer/developer_preview_storage.md +0 -35
  367. package/docs/developer/developer_release_manual_test_checklist.md +0 -209
  368. package/docs/developer/development_workflow.md +0 -308
  369. package/docs/developer/environment/CONNECTOR_SECRET_KEY_SETUP.md +0 -96
  370. package/docs/developer/environment/DEV_CONNECTOR_SECRET_KEY_EXPLANATION.md +0 -80
  371. package/docs/developer/environment/ENV_MAPPINGS_STATUS.md +0 -86
  372. package/docs/developer/environment/ENV_VAR_NAMING_STRATEGY.md +0 -145
  373. package/docs/developer/environment/MIGRATION_TO_SINGLE_VAR_NAMES.md +0 -89
  374. package/docs/developer/environment/QUICK_START_1PASSWORD.md +0 -126
  375. package/docs/developer/environment/README.md +0 -27
  376. package/docs/developer/environment/SETUP_ENV_MAPPINGS.md +0 -256
  377. package/docs/developer/environment_distinction_audit.md +0 -43
  378. package/docs/developer/fix_doc_inconsistencies_plan.md +0 -63
  379. package/docs/developer/getting_started.md +0 -100
  380. package/docs/developer/import_interpretation_debug.md +0 -69
  381. package/docs/developer/launchd_dev_servers.md +0 -51
  382. package/docs/developer/launchd_watch_build.md +0 -37
  383. package/docs/developer/mcp/instructions.md +0 -71
  384. package/docs/developer/mcp/invalid_connection_id_handling.md +0 -181
  385. package/docs/developer/mcp/tool_descriptions.yaml +0 -41
  386. package/docs/developer/mcp/unauthenticated.md +0 -30
  387. package/docs/developer/mcp_chatgpt_setup.md +0 -159
  388. package/docs/developer/mcp_claude_code_setup.md +0 -519
  389. package/docs/developer/mcp_https_testing.md +0 -178
  390. package/docs/developer/mcp_https_testing_status.md +0 -130
  391. package/docs/developer/mcp_https_tunnel_status.md +0 -147
  392. package/docs/developer/mcp_oauth_migration_guide.md +0 -196
  393. package/docs/developer/mcp_overview.md +0 -145
  394. package/docs/developer/mcp_server_examples_from_cursor_docs.md +0 -485
  395. package/docs/developer/observation_architecture_documentation_integration.md +0 -368
  396. package/docs/developer/release_orchestrator.md +0 -197
  397. package/docs/developer/repository_cleanup_plan.md +0 -87
  398. package/docs/developer/resolved_blockers.md +0 -49
  399. package/docs/developer/schema_initialization.md +0 -182
  400. package/docs/developer/secrets/README.md +0 -15
  401. package/docs/developer/secrets/secrets_architecture.md +0 -95
  402. package/docs/developer/secrets/secrets_manager_value.md +0 -53
  403. package/docs/developer/secrets/secrets_secure_approach.md +0 -103
  404. package/docs/developer/secrets/secrets_setup.md +0 -116
  405. package/docs/developer/shared_components_submodule_setup.md +0 -273
  406. package/docs/developer/spec_compliance_validation_system.md +0 -261
  407. package/docs/developer/test_coverage_gap_analysis_plan.md +0 -36
  408. package/docs/developer/tunnels.md +0 -327
  409. package/docs/developer/vite_troubleshooting.md +0 -111
  410. package/docs/doc_dependencies.yaml +0 -497
  411. package/docs/examples/schema_breaking_change_reconciliation.md +0 -515
  412. package/docs/feature_units/completed/FU-050/FU-050_spec.md +0 -341
  413. package/docs/feature_units/completed/FU-050/manifest.yaml +0 -330
  414. package/docs/feature_units/completed/FU-051/FU-051_spec.md +0 -173
  415. package/docs/feature_units/completed/FU-051/manifest.yaml +0 -229
  416. package/docs/feature_units/completed/FU-052/FU-052_spec.md +0 -158
  417. package/docs/feature_units/completed/FU-052/manifest.yaml +0 -188
  418. package/docs/feature_units/completed/FU-053/FU-053_spec.md +0 -144
  419. package/docs/feature_units/completed/FU-053/manifest.yaml +0 -145
  420. package/docs/feature_units/completed/FU-054/FU-054_spec.md +0 -144
  421. package/docs/feature_units/completed/FU-054/manifest.yaml +0 -139
  422. package/docs/feature_units/completed/FU-702/billing_spec.md +0 -413
  423. package/docs/feature_units/completed/FU-702/manifest.yaml +0 -418
  424. package/docs/feature_units/standards/DISCOVERY_CAGAN_ALIGNMENT.md +0 -214
  425. package/docs/feature_units/standards/creating_feature_units.md +0 -385
  426. package/docs/feature_units/standards/discovery_process.md +0 -821
  427. package/docs/feature_units/standards/error_protocol.md +0 -60
  428. package/docs/feature_units/standards/execution_instructions.md +0 -87
  429. package/docs/feature_units/standards/feature_unit_spec.md +0 -325
  430. package/docs/feature_units/standards/integration_test_execution.md +0 -127
  431. package/docs/feature_units/standards/manifest_template.yaml +0 -370
  432. package/docs/feature_units/standards/model_selection.md +0 -207
  433. package/docs/feature_units/standards/multi_agent_orchestration.md +0 -411
  434. package/docs/feature_units/standards/release_report_generation.md +0 -448
  435. package/docs/feature_units/standards/release_report_spec.md +0 -444
  436. package/docs/feature_units/standards/release_report_template.md +0 -76
  437. package/docs/feature_units/standards/spec_compliance_validation.md +0 -147
  438. package/docs/feature_units/standards/worker_agent_template.md +0 -191
  439. package/docs/foundation/ai_safety.md +0 -10
  440. package/docs/foundation/composability_analysis.md +0 -308
  441. package/docs/foundation/core_identity.md +0 -50
  442. package/docs/foundation/documentation_design_system.md +0 -505
  443. package/docs/foundation/entity_resolution.md +0 -34
  444. package/docs/foundation/layered_architecture.md +0 -99
  445. package/docs/foundation/mmry_comparison.md +0 -176
  446. package/docs/foundation/philosophy.md +0 -103
  447. package/docs/foundation/problem_statement.md +0 -41
  448. package/docs/foundation/product_positioning.md +0 -69
  449. package/docs/foundation/product_principles.md +0 -18
  450. package/docs/foundation/quality_requirements.md +0 -68
  451. package/docs/foundation/timeline_events.md +0 -19
  452. package/docs/foundation/user_workflows.md +0 -38
  453. package/docs/implementation/PARQUET_MCP_SERVER_FIXES.md +0 -373
  454. package/docs/implementation/PARQUET_SUPPORT_IMPLEMENTATION.md +0 -207
  455. package/docs/infrastructure/deployment.md +0 -372
  456. package/docs/legal/README.md +0 -51
  457. package/docs/legal/compliance.md +0 -854
  458. package/docs/legal/privacy_policy.md +0 -114
  459. package/docs/legal/privacy_policy_changelog.md +0 -49
  460. package/docs/legal/terms_of_service.md +0 -185
  461. package/docs/legal/terms_of_service_changelog.md +0 -62
  462. package/docs/migration/llm_extraction_removal.md +0 -387
  463. package/docs/migration/llm_interpretation_docs_update.md +0 -130
  464. package/docs/observability/logging.md +0 -50
  465. package/docs/observability/metrics_standard.md +0 -38
  466. package/docs/observability/tracing.md +0 -41
  467. package/docs/operations/debugging/README.md +0 -11
  468. package/docs/operations/debugging/error_debug_instructions.md +0 -55
  469. package/docs/operations/health_check.md +0 -73
  470. package/docs/operations/runbook.md +0 -88
  471. package/docs/prompts/llm_extraction_system_prompt.md +0 -31
  472. package/docs/proposals/IMPLEMENTATION_SUMMARY.md +0 -198
  473. package/docs/proposals/MIGRATION_REPORT.md +0 -65
  474. package/docs/proposals/PROPOSAL_TEMPLATE.md +0 -58
  475. package/docs/proposals/README.md +0 -57
  476. package/docs/proposals/agent-trust-framework.md +0 -621
  477. package/docs/proposals/conversation_turn_identity_reverts_forks.md +0 -144
  478. package/docs/proposals/document-v023-release.md +0 -898
  479. package/docs/proposals/iterative_chat_store_mcp_instructions.md +0 -155
  480. package/docs/proposals/llm_sampling_parameters_interpretation_config.md +0 -196
  481. package/docs/proposals/mcp-cli-action-items.md +0 -207
  482. package/docs/proposals/plan-migration-command-integrated-with-commit.md +0 -386
  483. package/docs/proposals/sources-first-ingestion-v10.md +0 -348
  484. package/docs/proposals/test-coverage-gap-analysis.md +0 -26
  485. package/docs/prototypes/PROTOTYPE_QUICKSTART.md +0 -56
  486. package/docs/prototypes/README.md +0 -74
  487. package/docs/reference/error_codes.md +0 -223
  488. package/docs/releases/archived/aspirational/README.md +0 -57
  489. package/docs/releases/archived/aspirational/v0.4.0/manifest.yaml +0 -94
  490. package/docs/releases/archived/aspirational/v0.4.0/release_plan.md +0 -51
  491. package/docs/releases/archived/aspirational/v0.5.0/manifest.yaml +0 -152
  492. package/docs/releases/archived/aspirational/v0.5.0/release_plan.md +0 -236
  493. package/docs/releases/archived/aspirational/v0.9.0/README.md +0 -78
  494. package/docs/releases/archived/aspirational/v0.9.0/manifest.yaml +0 -111
  495. package/docs/releases/archived/aspirational/v0.9.0/release_plan.md +0 -238
  496. package/docs/releases/archived/aspirational/v0.9.0/status.md +0 -89
  497. package/docs/releases/archived/aspirational/v1.0.0/UI_REFACTORING_PLAN.md +0 -239
  498. package/docs/releases/archived/aspirational/v1.0.0/acceptance_criteria.md +0 -88
  499. package/docs/releases/archived/aspirational/v1.0.0/approach_comparison.md +0 -558
  500. package/docs/releases/archived/aspirational/v1.0.0/architectural_impact_chat_ui.md +0 -195
  501. package/docs/releases/archived/aspirational/v1.0.0/blog_post_update_directives.md +0 -491
  502. package/docs/releases/archived/aspirational/v1.0.0/business_viability_discovery_plan.md +0 -135
  503. package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-100_compliance.md +0 -23
  504. package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-101_compliance.md +0 -23
  505. package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-102_compliance.md +0 -23
  506. package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-103_compliance.md +0 -23
  507. package/docs/releases/archived/aspirational/v1.0.0/compliance_reports/FU-105_compliance.md +0 -23
  508. package/docs/releases/archived/aspirational/v1.0.0/continuous_discovery_log.md +0 -77
  509. package/docs/releases/archived/aspirational/v1.0.0/continuous_discovery_plan.md +0 -145
  510. package/docs/releases/archived/aspirational/v1.0.0/cross_platform_signal_detection.md +0 -181
  511. package/docs/releases/archived/aspirational/v1.0.0/deployment_strategy.md +0 -115
  512. package/docs/releases/archived/aspirational/v1.0.0/discovery_checklist.md +0 -195
  513. package/docs/releases/archived/aspirational/v1.0.0/discovery_filtering_criteria.md +0 -197
  514. package/docs/releases/archived/aspirational/v1.0.0/discovery_filtering_keywords.md +0 -143
  515. package/docs/releases/archived/aspirational/v1.0.0/discovery_lead_sourcing_tools.md +0 -438
  516. package/docs/releases/archived/aspirational/v1.0.0/discovery_plan.md +0 -185
  517. package/docs/releases/archived/aspirational/v1.0.0/execution_schedule.md +0 -130
  518. package/docs/releases/archived/aspirational/v1.0.0/feasibility_validation_plan.md +0 -116
  519. package/docs/releases/archived/aspirational/v1.0.0/handle_registration.md +0 -263
  520. package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-100_implementation_log.md +0 -51
  521. package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-101_implementation_log.md +0 -51
  522. package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-102_implementation_log.md +0 -51
  523. package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-103_implementation_log.md +0 -51
  524. package/docs/releases/archived/aspirational/v1.0.0/implementation_logs/FU-105_implementation_log.md +0 -51
  525. package/docs/releases/archived/aspirational/v1.0.0/manifest.yaml +0 -160
  526. package/docs/releases/archived/aspirational/v1.0.0/marketing_agent_instructions.md +0 -209
  527. package/docs/releases/archived/aspirational/v1.0.0/marketing_automation_implementation.md +0 -996
  528. package/docs/releases/archived/aspirational/v1.0.0/marketing_automation_plan.md +0 -291
  529. package/docs/releases/archived/aspirational/v1.0.0/marketing_dogfooding_analysis.md +0 -295
  530. package/docs/releases/archived/aspirational/v1.0.0/marketing_metrics_plan.md +0 -120
  531. package/docs/releases/archived/aspirational/v1.0.0/marketing_plan.md +0 -280
  532. package/docs/releases/archived/aspirational/v1.0.0/marketing_segments_plan.md +0 -128
  533. package/docs/releases/archived/aspirational/v1.0.0/participant_recruitment_log.md +0 -392
  534. package/docs/releases/archived/aspirational/v1.0.0/post_launch_marketing_plan.md +0 -252
  535. package/docs/releases/archived/aspirational/v1.0.0/pre_launch_marketing_plan.md +0 -748
  536. package/docs/releases/archived/aspirational/v1.0.0/pre_mortem.md +0 -122
  537. package/docs/releases/archived/aspirational/v1.0.0/subscription_detection_strategy.md +0 -160
  538. package/docs/releases/archived/aspirational/v1.0.0/usability_discovery_plan.md +0 -115
  539. package/docs/releases/archived/aspirational/v1.0.0/value_discovery_plan.md +0 -279
  540. package/docs/releases/archived/aspirational/v1.0.0/visibility_timing_analysis.md +0 -402
  541. package/docs/releases/archived/aspirational/v2.0.0/status.md +0 -120
  542. package/docs/releases/archived/aspirational/v2.1.0/manifest.yaml +0 -469
  543. package/docs/releases/archived/aspirational/v2.2.0/manifest.yaml +0 -165
  544. package/docs/releases/archived/aspirational/v2.2.0/status.md +0 -283
  545. package/docs/releases/compliance_reports/all_fus_compliance_summary.md +0 -112
  546. package/docs/releases/in_progress/v0.2.0/agent_status.json +0 -420
  547. package/docs/releases/in_progress/v0.2.0/status.md +0 -90
  548. package/docs/releases/v0.1.0/agent_status.json +0 -242
  549. package/docs/releases/v0.1.0/agent_status.json.example +0 -76
  550. package/docs/releases/v0.1.0/compliance_reports/FU-000_compliance.md +0 -23
  551. package/docs/releases/v0.1.0/compliance_reports/FU-002_compliance.md +0 -23
  552. package/docs/releases/v0.1.0/compliance_reports/FU-050_compliance.md +0 -59
  553. package/docs/releases/v0.1.0/compliance_reports/FU-051_compliance.md +0 -39
  554. package/docs/releases/v0.1.0/compliance_reports/FU-052_compliance.md +0 -44
  555. package/docs/releases/v0.1.0/compliance_reports/FU-053_compliance.md +0 -25
  556. package/docs/releases/v0.1.0/compliance_reports/FU-054_compliance.md +0 -31
  557. package/docs/releases/v0.1.0/compliance_reports/FU-055_compliance.md +0 -23
  558. package/docs/releases/v0.1.0/compliance_reports/FU-056_compliance.md +0 -23
  559. package/docs/releases/v0.1.0/compliance_reports/FU-057_compliance.md +0 -23
  560. package/docs/releases/v0.1.0/compliance_reports/FU-058_compliance.md +0 -23
  561. package/docs/releases/v0.1.0/compliance_reports/FU-059_compliance.md +0 -23
  562. package/docs/releases/v0.1.0/compliance_reports/FU-061_compliance.md +0 -23
  563. package/docs/releases/v0.1.0/compliance_reports/FU-100_compliance.md +0 -97
  564. package/docs/releases/v0.1.0/compliance_reports/FU-104_compliance.md +0 -23
  565. package/docs/releases/v0.1.0/compliance_reports/FU-200_compliance.md +0 -23
  566. package/docs/releases/v0.1.0/compliance_reports/FU-201_compliance.md +0 -23
  567. package/docs/releases/v0.1.0/compliance_reports/FU-202_compliance.md +0 -23
  568. package/docs/releases/v0.1.0/compliance_reports/FU-203_compliance.md +0 -23
  569. package/docs/releases/v0.1.0/compliance_reports/FU-204_compliance.md +0 -23
  570. package/docs/releases/v0.1.0/compliance_reports/FU-205_compliance.md +0 -23
  571. package/docs/releases/v0.1.0/compliance_reports/FU-206_compliance.md +0 -23
  572. package/docs/releases/v0.1.0/execution_schedule.md +0 -174
  573. package/docs/releases/v0.1.0/fu_spec_status.md +0 -83
  574. package/docs/releases/v0.1.0/implementation_logs/FU-000_implementation_log.md +0 -51
  575. package/docs/releases/v0.1.0/implementation_logs/FU-002_implementation_log.md +0 -51
  576. package/docs/releases/v0.1.0/implementation_logs/FU-050_implementation_log.md +0 -111
  577. package/docs/releases/v0.1.0/implementation_logs/FU-051_implementation_log.md +0 -91
  578. package/docs/releases/v0.1.0/implementation_logs/FU-052_implementation_log.md +0 -81
  579. package/docs/releases/v0.1.0/implementation_logs/FU-053_implementation_log.md +0 -71
  580. package/docs/releases/v0.1.0/implementation_logs/FU-054_implementation_log.md +0 -71
  581. package/docs/releases/v0.1.0/implementation_logs/FU-055_implementation_log.md +0 -51
  582. package/docs/releases/v0.1.0/implementation_logs/FU-056_implementation_log.md +0 -51
  583. package/docs/releases/v0.1.0/implementation_logs/FU-057_implementation_log.md +0 -51
  584. package/docs/releases/v0.1.0/implementation_logs/FU-058_implementation_log.md +0 -51
  585. package/docs/releases/v0.1.0/implementation_logs/FU-059_implementation_log.md +0 -51
  586. package/docs/releases/v0.1.0/implementation_logs/FU-061_implementation_log.md +0 -51
  587. package/docs/releases/v0.1.0/implementation_logs/FU-100_implementation_log.md +0 -251
  588. package/docs/releases/v0.1.0/implementation_logs/FU-104_implementation_log.md +0 -51
  589. package/docs/releases/v0.1.0/implementation_logs/FU-200_implementation_log.md +0 -51
  590. package/docs/releases/v0.1.0/implementation_logs/FU-201_implementation_log.md +0 -51
  591. package/docs/releases/v0.1.0/implementation_logs/FU-202_implementation_log.md +0 -51
  592. package/docs/releases/v0.1.0/implementation_logs/FU-203_implementation_log.md +0 -51
  593. package/docs/releases/v0.1.0/implementation_logs/FU-204_implementation_log.md +0 -51
  594. package/docs/releases/v0.1.0/implementation_logs/FU-205_implementation_log.md +0 -51
  595. package/docs/releases/v0.1.0/implementation_logs/FU-206_implementation_log.md +0 -51
  596. package/docs/releases/v0.1.0/integration_tests.md +0 -316
  597. package/docs/releases/v0.1.0/manifest.yaml +0 -291
  598. package/docs/releases/v0.1.0/mcp_actions_status.md +0 -101
  599. package/docs/releases/v0.1.0/release_plan.md +0 -196
  600. package/docs/releases/v0.1.0/status.md +0 -148
  601. package/docs/releases/v0.1.0/test_coverage_setup_notes.md +0 -39
  602. package/docs/releases/v0.1.1/manifest.yaml +0 -153
  603. package/docs/releases/v0.2.0/status.md +0 -167
  604. package/docs/releases/v0.2.1/acceptance_criteria.md +0 -200
  605. package/docs/releases/v0.2.1/execution_schedule.md +0 -114
  606. package/docs/releases/v0.2.1/feature_unit_overview.md +0 -246
  607. package/docs/releases/v0.2.1/integration_tests.md +0 -281
  608. package/docs/releases/v0.2.1/manifest.yaml +0 -141
  609. package/docs/releases/v0.2.1/release_plan.md +0 -211
  610. package/docs/releases/v0.2.1/status.md +0 -106
  611. package/docs/releases/v0.2.15/README.md +0 -79
  612. package/docs/releases/v0.2.15/migrations/01_add_source_graph_edges.sql +0 -109
  613. package/docs/releases/v0.2.15/migrations/02_drop_legacy_tables.sql +0 -130
  614. package/docs/releases/v0.2.2/execution_schedule.md +0 -123
  615. package/docs/releases/v0.2.2/integration_tests.md +0 -258
  616. package/docs/releases/v0.2.2/manifest.yaml +0 -108
  617. package/docs/releases/v0.2.2/release_plan.md +0 -246
  618. package/docs/releases/v0.2.2/status.md +0 -113
  619. package/docs/releases/v0.2.3/agent_skills_alignment.md +0 -738
  620. package/docs/releases/v0.2.3/enhancements_summary.md +0 -309
  621. package/docs/releases/v0.2.3/integration_guide.md +0 -3400
  622. package/docs/releases/v0.2.3/manifest.yaml +0 -212
  623. package/docs/releases/v0.2.3/mcp_actions.md +0 -491
  624. package/docs/releases/v0.2.3/plan_completion_review.md +0 -410
  625. package/docs/releases/v0.2.3/release_overview.md +0 -13
  626. package/docs/releases/v0.2.3/schema_extensions/agent_decision.md +0 -311
  627. package/docs/releases/v0.2.3/schema_extensions/agent_session.md +0 -464
  628. package/docs/releases/v0.2.3/schema_extensions/architectural_decision.md +0 -390
  629. package/docs/releases/v0.2.3/schema_extensions/codebase_entity.md +0 -404
  630. package/docs/releases/v0.2.3/schema_extensions/feature_unit.md +0 -307
  631. package/docs/releases/v0.2.3/schema_extensions/release.md +0 -338
  632. package/docs/releases/v0.2.3/schema_extensions/validation_result.md +0 -377
  633. package/docs/releases/v0.2.3/spec.md +0 -569
  634. package/docs/releases/v0.3.0/manifest.yaml +0 -77
  635. package/docs/releases/v0.3.0/migrations/01_add_source_graph_edges.sql +0 -110
  636. package/docs/releases/v0.3.0/migrations/02_drop_legacy_tables.sql +0 -131
  637. package/docs/releases/v0.3.0/status.md +0 -193
  638. package/docs/reports/AUTO_ENHANCEMENT_TEST_RESULTS.md +0 -51
  639. package/docs/reports/AUTO_ENHANCEMENT_TEST_SUMMARY.md +0 -64
  640. package/docs/reports/BIGINT_SERIALIZATION_DEBUG.md +0 -75
  641. package/docs/reports/BUG_LEARNING_PROTOCOL_ESTABLISHED.md +0 -179
  642. package/docs/reports/CONSOLE_LOG_MCP_PROTOCOL_FIX.md +0 -58
  643. package/docs/reports/CRYPTO_TRANSACTION_DISCREPANCY_RESOLUTION.md +0 -108
  644. package/docs/reports/CRYPTO_TRANSACTION_SIDE_BY_SIDE_COMPARISON.md +0 -165
  645. package/docs/reports/FAILING_TESTS_ANALYSIS.md +0 -140
  646. package/docs/reports/FAILING_TESTS_SUMMARY.md +0 -106
  647. package/docs/reports/FINAL_PARQUET_COMPARISON.md +0 -226
  648. package/docs/reports/FINAL_TEST_STATUS.md +0 -145
  649. package/docs/reports/LEGACY_REMOVAL_SUMMARY.md +0 -108
  650. package/docs/reports/MCP_ACTIONS_SUMMARY.md +0 -329
  651. package/docs/reports/MCP_ACTION_TEST_COVERAGE.md +0 -132
  652. package/docs/reports/MCP_SPEC_COMPARISON.md +0 -306
  653. package/docs/reports/MCP_SPEC_COMPARISON_UPDATED.md +0 -247
  654. package/docs/reports/MCP_SPEC_VOCABULARY_CONFORMANCE.md +0 -220
  655. package/docs/reports/MCP_TEST_SUITE_IMPLEMENTATION.md +0 -180
  656. package/docs/reports/MCP_TEST_SUITE_RESULTS.md +0 -182
  657. package/docs/reports/PARQUET_MCP_BUG_REPORT.md +0 -70
  658. package/docs/reports/PARQUET_MCP_STORAGE_COMPLETE.md +0 -214
  659. package/docs/reports/PARQUET_SAMPLE_FILES_CREATED.md +0 -49
  660. package/docs/reports/PARQUET_STORAGE_SUCCESS.md +0 -263
  661. package/docs/reports/PARQUET_VS_NEOTOMA_COMPARISON.md +0 -241
  662. package/docs/reports/REIMPORT_TASKS_BLOCKER.md +0 -50
  663. package/docs/reports/SAMPLE_FILE_STRUCTURE_UPDATE.md +0 -68
  664. package/docs/reports/SAMPLE_PARQUET_IMPORT_TEST_RESULTS.md +0 -91
  665. package/docs/reports/SCHEMA_ENHANCEMENT_STATUS.md +0 -84
  666. package/docs/reports/SCHEMA_ENHANCEMENT_TEST_RESULTS.md +0 -78
  667. package/docs/reports/TASK_RELOAD_BLOCKER.md +0 -40
  668. package/docs/reports/TEST_FIXES_SUMMARY.md +0 -96
  669. package/docs/reports/VOCABULARY_CONFORMANCE_REVIEW.md +0 -177
  670. package/docs/reports/WHY_TESTS_MISSED_BIGINT_ISSUE.md +0 -144
  671. package/docs/reports/beyond_rag_agent_memory_neotoma_architecture_report.md +0 -103
  672. package/docs/reports/dhh_clankers_with_claws_neotoma_report.md +0 -102
  673. package/docs/reports/dr_cintas_pageindex_structure_over_embeddings_report.md +0 -77
  674. package/docs/reports/failure_gallery.md +0 -97
  675. package/docs/reports/fixture_expansion_summary.md +0 -204
  676. package/docs/reports/mcp_action_names_review.md +0 -131
  677. package/docs/reports/mcp_cli_action_items_fidelity_report.md +0 -189
  678. package/docs/reports/neotoma_pdf_interpreted_as_note_investigation_2026_02_23.md +0 -89
  679. package/docs/reports/plan_execution_changes_report.md +0 -122
  680. package/docs/reports/schema_distinctions.md +0 -210
  681. package/docs/reports/schema_redundancy_analysis.md +0 -304
  682. package/docs/specs/FUNCTIONAL_REQUIREMENTS.md +0 -160
  683. package/docs/specs/GENERAL_REQUIREMENTS.md +0 -30
  684. package/docs/specs/ICP_PRIORITY_TIERS.md +0 -132
  685. package/docs/specs/ICP_PROFILES.md +0 -2345
  686. package/docs/specs/INTERNAL_MCP_RELEASE.md +0 -300
  687. package/docs/specs/MANIFEST_ALIGNMENT_SUMMARY.md +0 -162
  688. package/docs/specs/MCP_SPEC.md +0 -2275
  689. package/docs/specs/METRICS_REQUIREMENTS.md +0 -386
  690. package/docs/specs/MVP_EXECUTION_PLAN.md +0 -956
  691. package/docs/specs/MVP_FEATURE_UNITS.md +0 -1821
  692. package/docs/specs/MVP_OVERVIEW.md +0 -184
  693. package/docs/specs/NONFUNCTIONAL_REQUIREMENTS.md +0 -98
  694. package/docs/specs/ONBOARDING_SPEC.md +0 -591
  695. package/docs/specs/PARQUET_MCP_RESOURCES_SPEC.md +0 -328
  696. package/docs/specs/TEST_PLAN.md +0 -116
  697. package/docs/specs/UI_SPEC.md +0 -93
  698. package/docs/subsystems/accessibility.md +0 -48
  699. package/docs/subsystems/auth.md +0 -170
  700. package/docs/subsystems/deletion.md +0 -494
  701. package/docs/subsystems/entity_merge.md +0 -300
  702. package/docs/subsystems/errors.md +0 -82
  703. package/docs/subsystems/events.md +0 -50
  704. package/docs/subsystems/i18n.md +0 -42
  705. package/docs/subsystems/ingestion/ingestion.md +0 -772
  706. package/docs/subsystems/ingestion/state_machines.md +0 -49
  707. package/docs/subsystems/observation_architecture.md +0 -379
  708. package/docs/subsystems/privacy.md +0 -71
  709. package/docs/subsystems/record_types.md +0 -753
  710. package/docs/subsystems/reducer.md +0 -436
  711. package/docs/subsystems/relationships.md +0 -455
  712. package/docs/subsystems/schema.md +0 -1213
  713. package/docs/subsystems/schema_registry.md +0 -588
  714. package/docs/subsystems/schema_snapshots/.gitkeep +0 -6
  715. package/docs/subsystems/schema_snapshots/README.md +0 -209
  716. package/docs/subsystems/schema_snapshots/account/v1.0.json +0 -52
  717. package/docs/subsystems/schema_snapshots/address/v1.0.json +0 -79
  718. package/docs/subsystems/schema_snapshots/agent_message/v1.0.json +0 -96
  719. package/docs/subsystems/schema_snapshots/balance/v1.0.json +0 -64
  720. package/docs/subsystems/schema_snapshots/company/v1.0.json +0 -86
  721. package/docs/subsystems/schema_snapshots/contact/v1.0.json +0 -68
  722. package/docs/subsystems/schema_snapshots/contact/v1.1.json +0 -112
  723. package/docs/subsystems/schema_snapshots/contract/v1.0.json +0 -48
  724. package/docs/subsystems/schema_snapshots/conversation/v1.0.json +0 -102
  725. package/docs/subsystems/schema_snapshots/crypto_transaction/v1.0.json +0 -76
  726. package/docs/subsystems/schema_snapshots/email/v1.0.json +0 -87
  727. package/docs/subsystems/schema_snapshots/event/v1.0.json +0 -79
  728. package/docs/subsystems/schema_snapshots/exercise/v1.0.json +0 -72
  729. package/docs/subsystems/schema_snapshots/fixed_cost/v1.0.json +0 -112
  730. package/docs/subsystems/schema_snapshots/flow/v1.0.json +0 -84
  731. package/docs/subsystems/schema_snapshots/goal/v1.0.json +0 -71
  732. package/docs/subsystems/schema_snapshots/holding/v1.0.json +0 -75
  733. package/docs/subsystems/schema_snapshots/income/v1.0.json +0 -68
  734. package/docs/subsystems/schema_snapshots/liability/v1.0.json +0 -60
  735. package/docs/subsystems/schema_snapshots/location/v1.0.json +0 -79
  736. package/docs/subsystems/schema_snapshots/meal/v1.0.json +0 -68
  737. package/docs/subsystems/schema_snapshots/message/v1.0.json +0 -72
  738. package/docs/subsystems/schema_snapshots/note/v1.0.json +0 -64
  739. package/docs/subsystems/schema_snapshots/order/v1.0.json +0 -72
  740. package/docs/subsystems/schema_snapshots/person/v1.0.json +0 -86
  741. package/docs/subsystems/schema_snapshots/project/v1.0.json +0 -75
  742. package/docs/subsystems/schema_snapshots/property/v1.0.json +0 -64
  743. package/docs/subsystems/schema_snapshots/purchase/v1.0.json +0 -75
  744. package/docs/subsystems/schema_snapshots/relationship/v1.0.json +0 -63
  745. package/docs/subsystems/schema_snapshots/task/v1.0.json +0 -91
  746. package/docs/subsystems/schema_snapshots/tax_event/v1.0.json +0 -72
  747. package/docs/subsystems/schema_snapshots/tax_filing/v1.0.json +0 -83
  748. package/docs/subsystems/schema_snapshots/transaction/v1.0.json +0 -40
  749. package/docs/subsystems/schema_snapshots/transfer/v1.0.json +0 -76
  750. package/docs/subsystems/schema_snapshots/wallet/v1.0.json +0 -68
  751. package/docs/subsystems/search/search.md +0 -63
  752. package/docs/subsystems/sources.md +0 -493
  753. package/docs/subsystems/vector_ops.md +0 -72
  754. package/docs/templates/issue_template.md +0 -36
  755. package/docs/templates/pr_template.md +0 -74
  756. package/docs/testing/ROUTE_COVERAGE_MATRIX.md +0 -104
  757. package/docs/testing/WHY_PARTIAL_ROUTE_COVERAGE.md +0 -183
  758. package/docs/testing/automated_test_catalog.md +0 -353
  759. package/docs/testing/cli_command_coverage_review.md +0 -130
  760. package/docs/testing/fixtures_standard.md +0 -202
  761. package/docs/testing/test_coverage_audit_summary.md +0 -571
  762. package/docs/testing/testing_standard.md +0 -146
  763. package/docs/ui/DESIGN_SYSTEM_UPDATE_SUMMARY.md +0 -152
  764. package/docs/ui/SHADCN_INTEGRATION_COMPLETE.md +0 -255
  765. package/docs/ui/design_constraints_template.yaml +0 -228
  766. package/docs/ui/design_system/accessibility.md +0 -38
  767. package/docs/ui/design_system/authentication_components.md +0 -38
  768. package/docs/ui/design_system/billing_components.md +0 -36
  769. package/docs/ui/design_system/brand_alignment.md +0 -35
  770. package/docs/ui/design_system/color_palette.md +0 -95
  771. package/docs/ui/design_system/component_styles.md +0 -241
  772. package/docs/ui/design_system/dark_mode.md +0 -30
  773. package/docs/ui/design_system/empty_states.md +0 -47
  774. package/docs/ui/design_system/error_states.md +0 -35
  775. package/docs/ui/design_system/file_upload_components.md +0 -60
  776. package/docs/ui/design_system/future_considerations.md +0 -19
  777. package/docs/ui/design_system/iconography.md +0 -21
  778. package/docs/ui/design_system/icp_aesthetic_analysis.md +0 -35
  779. package/docs/ui/design_system/icp_preference_alignment.md +0 -33
  780. package/docs/ui/design_system/implementation_notes.md +0 -119
  781. package/docs/ui/design_system/loading_states.md +0 -40
  782. package/docs/ui/design_system/motion_and_animation.md +0 -45
  783. package/docs/ui/design_system/navigation_components.md +0 -63
  784. package/docs/ui/design_system/onboarding_components.md +0 -48
  785. package/docs/ui/design_system/responsive_design.md +0 -26
  786. package/docs/ui/design_system/search_components.md +0 -32
  787. package/docs/ui/design_system/settings_components.md +0 -29
  788. package/docs/ui/design_system/spacing_and_layout.md +0 -67
  789. package/docs/ui/design_system/style_guide.md +0 -395
  790. package/docs/ui/design_system/typography.md +0 -88
  791. package/docs/ui/design_system/visual_hierarchy.md +0 -64
  792. package/docs/ui/design_system.md +0 -124
  793. package/docs/ui/dsl_spec.md +0 -97
  794. package/docs/ui/patterns/dashboard.md +0 -58
  795. package/docs/ui/patterns/detail.md +0 -68
  796. package/docs/ui/patterns/list.md +0 -72
  797. package/docs/ui/patterns/navigation.md +0 -314
  798. package/docs/ui/patterns/settings.md +0 -52
  799. package/docs/ui/patterns/wizard.md +0 -53
  800. package/docs/ui/prototype_guide.md +0 -300
  801. package/docs/ui/shadcn_components.md +0 -189
  802. package/docs/ui/style_guide_updates.md +0 -74
  803. package/docs/vocabulary/canonical_terms.md +0 -318
  804. package/fly.toml +0 -29
  805. package/foundation/.cursor/rules/agent_constraints.mdc +0 -198
  806. package/foundation/.cursor/rules/agent_test_execution.mdc +0 -356
  807. package/foundation/.cursor/rules/autonomous_execution.mdc +0 -117
  808. package/foundation/.cursor/rules/behavioral_self_adaptation.mdc +0 -531
  809. package/foundation/.cursor/rules/bug_fix_detection.mdc +0 -142
  810. package/foundation/.cursor/rules/checkpoint_management.mdc +0 -191
  811. package/foundation/.cursor/rules/configuration_management.mdc +0 -156
  812. package/foundation/.cursor/rules/content_style_enforcement.mdc +0 -138
  813. package/foundation/.cursor/rules/dependency_installation.mdc +0 -146
  814. package/foundation/.cursor/rules/document_loading_order.mdc +0 -194
  815. package/foundation/.cursor/rules/documentation_rules.mdc +0 -166
  816. package/foundation/.cursor/rules/downstream_doc_updates.mdc +0 -266
  817. package/foundation/.cursor/rules/environment_variables_1password.mdc +0 -307
  818. package/foundation/.cursor/rules/environment_variables_env_file.mdc +0 -121
  819. package/foundation/.cursor/rules/feature_unit_detection.mdc +0 -129
  820. package/foundation/.cursor/rules/file_naming.mdc +0 -31
  821. package/foundation/.cursor/rules/git_remote_sync.mdc +0 -240
  822. package/foundation/.cursor/rules/instruction_documentation.mdc +0 -302
  823. package/foundation/.cursor/rules/native_browser_debugging.mdc +0 -65
  824. package/foundation/.cursor/rules/plan_execution_testing.mdc +0 -98
  825. package/foundation/.cursor/rules/post_build_testing.mdc +0 -159
  826. package/foundation/.cursor/rules/prefer_cli_tools.mdc +0 -234
  827. package/foundation/.cursor/rules/readme_maintenance.mdc +0 -153
  828. package/foundation/.cursor/rules/release_detection.mdc +0 -92
  829. package/foundation/.cursor/rules/release_status_readme_update.mdc +0 -64
  830. package/foundation/.cursor/rules/risk_management.mdc +0 -167
  831. package/foundation/.cursor/rules/security.mdc +0 -158
  832. package/foundation/.cursor/rules/test_first_workflow.mdc +0 -341
  833. package/foundation/.cursor/rules/worktree_env.mdc +0 -53
  834. package/foundation/.cursor/skills/analyze/SKILL.md +0 -566
  835. package/foundation/.cursor/skills/commit/SKILL.md +0 -782
  836. package/foundation/.cursor/skills/create-feature-unit/SKILL.md +0 -180
  837. package/foundation/.cursor/skills/create-prototype/SKILL.md +0 -91
  838. package/foundation/.cursor/skills/create-release/SKILL.md +0 -432
  839. package/foundation/.cursor/skills/create-rule/SKILL.md +0 -292
  840. package/foundation/.cursor/skills/debug/SKILL.md +0 -980
  841. package/foundation/.cursor/skills/final-review/SKILL.md +0 -93
  842. package/foundation/.cursor/skills/fix-feature-bug/SKILL.md +0 -100
  843. package/foundation/.cursor/skills/manage-error-debugging/SKILL.md +0 -182
  844. package/foundation/.cursor/skills/publish/SKILL.md +0 -554
  845. package/foundation/.cursor/skills/pull/SKILL.md +0 -315
  846. package/foundation/.cursor/skills/push/SKILL.md +0 -254
  847. package/foundation/.cursor/skills/report/SKILL.md +0 -1501
  848. package/foundation/.cursor/skills/report-error/SKILL.md +0 -1501
  849. package/foundation/.cursor/skills/run-feature-workflow/SKILL.md +0 -79
  850. package/foundation/.cursor/skills/setup-commands/SKILL.md +0 -28
  851. package/foundation/.cursor/skills/setup-cursor-copies/SKILL.md +0 -233
  852. package/foundation/.cursor/skills/sync-env-from-1password/SKILL.md +0 -302
  853. package/foundation/CONTRIBUTING.md +0 -202
  854. package/foundation/LICENSE +0 -22
  855. package/foundation/MIGRATION.md +0 -396
  856. package/foundation/README.md +0 -391
  857. package/foundation/agent_instructions/README.md +0 -559
  858. package/foundation/config/foundation-config.yaml +0 -498
  859. package/foundation/config/foundation_config.yaml +0 -475
  860. package/foundation/config/repo_adapters/personal.yaml +0 -40
  861. package/foundation/config/repo_adapters/template.yaml +0 -45
  862. package/foundation/conventions/code_conventions.md +0 -908
  863. package/foundation/conventions/documentation_standards.md +0 -459
  864. package/foundation/conventions/naming_patterns.yaml +0 -139
  865. package/foundation/conventions/testing_conventions.md +0 -374
  866. package/foundation/conventions/writing_style_guide.md +0 -221
  867. package/foundation/development/branch_strategy.md +0 -266
  868. package/foundation/development/feature_unit_workflow.md +0 -423
  869. package/foundation/development/pr_process.md +0 -385
  870. package/foundation/development/release_workflow.md +0 -2296
  871. package/foundation/development/templates/feature_unit_spec_template.md +0 -312
  872. package/foundation/development/templates/manifest_template_extended.yaml +0 -303
  873. package/foundation/development/templates/manifest_template_simple.yaml +0 -148
  874. package/foundation/development/workflow.md +0 -509
  875. package/foundation/development/worktree_setup.sh +0 -207
  876. package/foundation/scripts/README.md +0 -230
  877. package/foundation/scripts/__pycache__/op_sync_env_from_1password.cpython-314.pyc +0 -0
  878. package/foundation/scripts/install-foundation.sh +0 -232
  879. package/foundation/scripts/install_foundation.sh +0 -232
  880. package/foundation/scripts/op_sync_env_from_1password.py +0 -1007
  881. package/foundation/scripts/report_error_create_dirs.sh +0 -14
  882. package/foundation/scripts/report_error_generate_id.sh +0 -10
  883. package/foundation/scripts/report_error_update_queue.sh +0 -24
  884. package/foundation/scripts/report_error_validate_target.sh +0 -34
  885. package/foundation/scripts/report_error_wait_resolution.sh +0 -69
  886. package/foundation/scripts/validate_setup.sh +0 -147
  887. package/foundation/security/credential_management.md +0 -263
  888. package/foundation/security/pre_commit_audit.sh +0 -78
  889. package/foundation/security/security_rules.md +0 -163
  890. package/foundation/strategy/README.md +0 -279
  891. package/foundation/strategy/competitive_analysis_template.md +0 -271
  892. package/foundation/strategy/partnership_analysis_template.md +0 -423
  893. package/foundation/strategy/project_assessment_framework.md +0 -485
  894. package/foundation/strategy/relevance_analysis_template.md +0 -297
  895. package/foundation/tooling/README.md +0 -198
  896. package/foundation/validation/README.md +0 -120
  897. package/foundation-config.yaml +0 -543
  898. package/frontend/docs.html +0 -43
  899. package/frontend/frontend/.vite-prototype/deps/@noble_curves_ed25519__js.js +0 -2859
  900. package/frontend/frontend/.vite-prototype/deps/@noble_curves_ed25519__js.js.map +0 -7
  901. package/frontend/frontend/.vite-prototype/deps/@noble_hashes_utils__js.js +0 -62
  902. package/frontend/frontend/.vite-prototype/deps/@noble_hashes_utils__js.js.map +0 -7
  903. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dialog.js +0 -464
  904. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dialog.js.map +0 -7
  905. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dropdown-menu.js +0 -1488
  906. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-dropdown-menu.js.map +0 -7
  907. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-label.js +0 -84
  908. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-label.js.map +0 -7
  909. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-scroll-area.js +0 -757
  910. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-scroll-area.js.map +0 -7
  911. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-select.js +0 -1351
  912. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-select.js.map +0 -7
  913. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-slot.js +0 -18
  914. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-slot.js.map +0 -7
  915. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-toast.js +0 -675
  916. package/frontend/frontend/.vite-prototype/deps/@radix-ui_react-toast.js.map +0 -7
  917. package/frontend/frontend/.vite-prototype/deps/@tanstack_react-table.js +0 -3254
  918. package/frontend/frontend/.vite-prototype/deps/@tanstack_react-table.js.map +0 -7
  919. package/frontend/frontend/.vite-prototype/deps/_metadata.json +0 -187
  920. package/frontend/frontend/.vite-prototype/deps/chunk-557GXNYU.js +0 -23
  921. package/frontend/frontend/.vite-prototype/deps/chunk-557GXNYU.js.map +0 -7
  922. package/frontend/frontend/.vite-prototype/deps/chunk-6FYWT56J.js.map +0 -7
  923. package/frontend/frontend/.vite-prototype/deps/chunk-C7HFWHWH.js +0 -271
  924. package/frontend/frontend/.vite-prototype/deps/chunk-C7HFWHWH.js.map +0 -7
  925. package/frontend/frontend/.vite-prototype/deps/chunk-CJDTXCAB.js +0 -49
  926. package/frontend/frontend/.vite-prototype/deps/chunk-CJDTXCAB.js.map +0 -7
  927. package/frontend/frontend/.vite-prototype/deps/chunk-DC5AMYBS.js +0 -39
  928. package/frontend/frontend/.vite-prototype/deps/chunk-DC5AMYBS.js.map +0 -7
  929. package/frontend/frontend/.vite-prototype/deps/chunk-F4FBYZZO.js +0 -2258
  930. package/frontend/frontend/.vite-prototype/deps/chunk-F4FBYZZO.js.map +0 -7
  931. package/frontend/frontend/.vite-prototype/deps/chunk-GIBJ3MQP.js +0 -129
  932. package/frontend/frontend/.vite-prototype/deps/chunk-GIBJ3MQP.js.map +0 -7
  933. package/frontend/frontend/.vite-prototype/deps/chunk-IMIZXM4S.js +0 -218
  934. package/frontend/frontend/.vite-prototype/deps/chunk-IMIZXM4S.js.map +0 -7
  935. package/frontend/frontend/.vite-prototype/deps/chunk-K236IVZX.js +0 -21628
  936. package/frontend/frontend/.vite-prototype/deps/chunk-K236IVZX.js.map +0 -7
  937. package/frontend/frontend/.vite-prototype/deps/chunk-LPO6FEV6.js +0 -21
  938. package/frontend/frontend/.vite-prototype/deps/chunk-LPO6FEV6.js.map +0 -7
  939. package/frontend/frontend/.vite-prototype/deps/chunk-PZJDTVNM.js +0 -144
  940. package/frontend/frontend/.vite-prototype/deps/chunk-PZJDTVNM.js.map +0 -7
  941. package/frontend/frontend/.vite-prototype/deps/chunk-RGYP7AMI.js +0 -1116
  942. package/frontend/frontend/.vite-prototype/deps/chunk-RGYP7AMI.js.map +0 -7
  943. package/frontend/frontend/.vite-prototype/deps/chunk-SMVRXQSC.js +0 -928
  944. package/frontend/frontend/.vite-prototype/deps/chunk-SMVRXQSC.js.map +0 -7
  945. package/frontend/frontend/.vite-prototype/deps/chunk-SPBFJIQZ.js +0 -49
  946. package/frontend/frontend/.vite-prototype/deps/chunk-SPBFJIQZ.js.map +0 -7
  947. package/frontend/frontend/.vite-prototype/deps/chunk-UQF5EARV.js +0 -9
  948. package/frontend/frontend/.vite-prototype/deps/chunk-UQF5EARV.js.map +0 -7
  949. package/frontend/frontend/.vite-prototype/deps/chunk-VEFHXNY4.js +0 -186
  950. package/frontend/frontend/.vite-prototype/deps/chunk-VEFHXNY4.js.map +0 -7
  951. package/frontend/frontend/.vite-prototype/deps/chunk-Z55WIHBM.js +0 -349
  952. package/frontend/frontend/.vite-prototype/deps/chunk-Z55WIHBM.js.map +0 -7
  953. package/frontend/frontend/.vite-prototype/deps/class-variance-authority.js +0 -51
  954. package/frontend/frontend/.vite-prototype/deps/class-variance-authority.js.map +0 -7
  955. package/frontend/frontend/.vite-prototype/deps/clsx.js +0 -10
  956. package/frontend/frontend/.vite-prototype/deps/clsx.js.map +0 -7
  957. package/frontend/frontend/.vite-prototype/deps/lucide-react.js +0 -31586
  958. package/frontend/frontend/.vite-prototype/deps/lucide-react.js.map +0 -7
  959. package/frontend/frontend/.vite-prototype/deps/package.json +0 -3
  960. package/frontend/frontend/.vite-prototype/deps/papaparse.js +0 -431
  961. package/frontend/frontend/.vite-prototype/deps/papaparse.js.map +0 -7
  962. package/frontend/frontend/.vite-prototype/deps/react-dom.js +0 -7
  963. package/frontend/frontend/.vite-prototype/deps/react-dom.js.map +0 -7
  964. package/frontend/frontend/.vite-prototype/deps/react-dom_client.js +0 -39
  965. package/frontend/frontend/.vite-prototype/deps/react-dom_client.js.map +0 -7
  966. package/frontend/frontend/.vite-prototype/deps/react.js +0 -6
  967. package/frontend/frontend/.vite-prototype/deps/react.js.map +0 -7
  968. package/frontend/frontend/.vite-prototype/deps/react_jsx-dev-runtime.js +0 -913
  969. package/frontend/frontend/.vite-prototype/deps/react_jsx-dev-runtime.js.map +0 -7
  970. package/frontend/frontend/.vite-prototype/deps/react_jsx-runtime.js +0 -7
  971. package/frontend/frontend/.vite-prototype/deps/react_jsx-runtime.js.map +0 -7
  972. package/frontend/frontend/.vite-prototype/deps/sonner.js +0 -1139
  973. package/frontend/frontend/.vite-prototype/deps/sonner.js.map +0 -7
  974. package/frontend/frontend/.vite-prototype/deps/tailwind-merge.js +0 -2534
  975. package/frontend/frontend/.vite-prototype/deps/tailwind-merge.js.map +0 -7
  976. package/frontend/index.html +0 -38
  977. package/frontend/src/App.tsx +0 -15
  978. package/frontend/src/bridge/discovery.ts +0 -126
  979. package/frontend/src/bridge/index.ts +0 -9
  980. package/frontend/src/bridge/mcp.ts +0 -85
  981. package/frontend/src/bridge/types.ts +0 -24
  982. package/frontend/src/bridge/websocket.ts +0 -349
  983. package/frontend/src/components/AIAgentBridge.tsx +0 -75
  984. package/frontend/src/components/AboutPage.tsx +0 -102
  985. package/frontend/src/components/AppNavigationSidebar.tsx +0 -181
  986. package/frontend/src/components/AppSidebar.tsx +0 -206
  987. package/frontend/src/components/AuthErrorHandler.tsx +0 -23
  988. package/frontend/src/components/Dashboard.tsx +0 -303
  989. package/frontend/src/components/DashboardPage.tsx +0 -78
  990. package/frontend/src/components/EmptyPlaceholder.tsx +0 -58
  991. package/frontend/src/components/EntityDetail.tsx +0 -801
  992. package/frontend/src/components/EntityList.tsx +0 -409
  993. package/frontend/src/components/ErrorBoundary.tsx +0 -131
  994. package/frontend/src/components/FileUploadView.tsx +0 -246
  995. package/frontend/src/components/FloatingSettingsButton.tsx +0 -46
  996. package/frontend/src/components/Header.tsx +0 -43
  997. package/frontend/src/components/IntegrationsPage.tsx +0 -46
  998. package/frontend/src/components/InterpretationList.tsx +0 -246
  999. package/frontend/src/components/KeyManagementDialog.tsx +0 -227
  1000. package/frontend/src/components/Layout.tsx +0 -67
  1001. package/frontend/src/components/MCPChatGPTPage.tsx +0 -128
  1002. package/frontend/src/components/MCPClaudePage.tsx +0 -112
  1003. package/frontend/src/components/MCPCodeiumPage.tsx +0 -100
  1004. package/frontend/src/components/MCPConfigurationPage.tsx +0 -624
  1005. package/frontend/src/components/MCPConnectionDialog.tsx +0 -177
  1006. package/frontend/src/components/MCPConnectionsList.tsx +0 -197
  1007. package/frontend/src/components/MCPContinuePage.tsx +0 -90
  1008. package/frontend/src/components/MCPCursorPage.tsx +0 -379
  1009. package/frontend/src/components/MCPGeminiPage.tsx +0 -102
  1010. package/frontend/src/components/MCPGitHubCopilotPage.tsx +0 -100
  1011. package/frontend/src/components/MCPGrokPage.tsx +0 -99
  1012. package/frontend/src/components/MCPJetBrainsPage.tsx +0 -107
  1013. package/frontend/src/components/MCPManusPage.tsx +0 -104
  1014. package/frontend/src/components/MCPSetupDialog.tsx +0 -268
  1015. package/frontend/src/components/MCPVSCodePage.tsx +0 -100
  1016. package/frontend/src/components/MCPWindsurfPage.tsx +0 -99
  1017. package/frontend/src/components/MainApp.tsx +0 -19
  1018. package/frontend/src/components/NotFound.tsx +0 -46
  1019. package/frontend/src/components/OAuthPage.tsx +0 -426
  1020. package/frontend/src/components/ObservationList.tsx +0 -277
  1021. package/frontend/src/components/ProtectedRoute.tsx +0 -38
  1022. package/frontend/src/components/QuickEntryPanel.tsx +0 -99
  1023. package/frontend/src/components/RealtimeStatusIndicator.tsx +0 -32
  1024. package/frontend/src/components/RecentActivityFeed.tsx +0 -198
  1025. package/frontend/src/components/RelationshipDetail.tsx +0 -232
  1026. package/frontend/src/components/RelationshipList.tsx +0 -269
  1027. package/frontend/src/components/SchemaDetail.test.tsx +0 -53
  1028. package/frontend/src/components/SchemaDetail.tsx +0 -230
  1029. package/frontend/src/components/SchemaList.tsx +0 -228
  1030. package/frontend/src/components/SearchResults.tsx +0 -318
  1031. package/frontend/src/components/SeoHead.tsx +0 -40
  1032. package/frontend/src/components/SitePage.tsx +0 -654
  1033. package/frontend/src/components/SourceDetail.tsx +0 -382
  1034. package/frontend/src/components/SourceTable.tsx +0 -243
  1035. package/frontend/src/components/StyleGuide.md +0 -48
  1036. package/frontend/src/components/StyleGuide.tsx +0 -2316
  1037. package/frontend/src/components/TimelineView.tsx +0 -321
  1038. package/frontend/src/components/UniversalSearch.tsx +0 -518
  1039. package/frontend/src/components/design-system/BadgesPage.tsx +0 -47
  1040. package/frontend/src/components/design-system/ButtonsPage.tsx +0 -67
  1041. package/frontend/src/components/design-system/CardsPage.tsx +0 -31
  1042. package/frontend/src/components/design-system/CollapsiblePage.tsx +0 -84
  1043. package/frontend/src/components/design-system/ColorsPage.tsx +0 -125
  1044. package/frontend/src/components/design-system/DesignSystemIndex.tsx +0 -73
  1045. package/frontend/src/components/design-system/DesignSystemLayout.tsx +0 -22
  1046. package/frontend/src/components/design-system/DesignSystemRouter.tsx +0 -43
  1047. package/frontend/src/components/design-system/InputsPage.tsx +0 -40
  1048. package/frontend/src/components/design-system/PageFormatsPage.tsx +0 -263
  1049. package/frontend/src/components/design-system/ProgressPage.tsx +0 -54
  1050. package/frontend/src/components/design-system/SkeletonPage.tsx +0 -68
  1051. package/frontend/src/components/design-system/SpacingPage.tsx +0 -45
  1052. package/frontend/src/components/design-system/StyleGuidePage.tsx +0 -313
  1053. package/frontend/src/components/design-system/SwitchPage.tsx +0 -60
  1054. package/frontend/src/components/design-system/TablesPage.tsx +0 -206
  1055. package/frontend/src/components/design-system/TabsPage.tsx +0 -88
  1056. package/frontend/src/components/design-system/TooltipPage.tsx +0 -94
  1057. package/frontend/src/components/design-system/TypographyPage.tsx +0 -81
  1058. package/frontend/src/components/icons/CursorIcon.tsx +0 -26
  1059. package/frontend/src/components/quick-entry/ContactDialog.tsx +0 -177
  1060. package/frontend/src/components/quick-entry/EventDialog.tsx +0 -204
  1061. package/frontend/src/components/quick-entry/TaskDialog.tsx +0 -204
  1062. package/frontend/src/components/quick-entry/TransactionDialog.tsx +0 -188
  1063. package/frontend/src/components/ui/alert.tsx +0 -59
  1064. package/frontend/src/components/ui/badge.tsx +0 -36
  1065. package/frontend/src/components/ui/breadcrumb.tsx +0 -120
  1066. package/frontend/src/components/ui/button.tsx +0 -56
  1067. package/frontend/src/components/ui/card.tsx +0 -68
  1068. package/frontend/src/components/ui/collapsible.tsx +0 -9
  1069. package/frontend/src/components/ui/dialog.tsx +0 -81
  1070. package/frontend/src/components/ui/dropdown-menu.tsx +0 -195
  1071. package/frontend/src/components/ui/input-group.tsx +0 -26
  1072. package/frontend/src/components/ui/input.tsx +0 -24
  1073. package/frontend/src/components/ui/label.tsx +0 -19
  1074. package/frontend/src/components/ui/progress.tsx +0 -26
  1075. package/frontend/src/components/ui/scroll-area.tsx +0 -35
  1076. package/frontend/src/components/ui/select.tsx +0 -173
  1077. package/frontend/src/components/ui/separator.tsx +0 -31
  1078. package/frontend/src/components/ui/sheet.tsx +0 -139
  1079. package/frontend/src/components/ui/sidebar.tsx +0 -851
  1080. package/frontend/src/components/ui/skeleton.tsx +0 -15
  1081. package/frontend/src/components/ui/switch.tsx +0 -35
  1082. package/frontend/src/components/ui/table-scroll-wrapper.tsx +0 -96
  1083. package/frontend/src/components/ui/table.tsx +0 -127
  1084. package/frontend/src/components/ui/tabs.tsx +0 -53
  1085. package/frontend/src/components/ui/textarea.tsx +0 -23
  1086. package/frontend/src/components/ui/toast.tsx +0 -122
  1087. package/frontend/src/components/ui/toaster.tsx +0 -32
  1088. package/frontend/src/components/ui/tooltip.tsx +0 -28
  1089. package/frontend/src/components/ui/use-toast.ts +0 -194
  1090. package/frontend/src/constants/integrations.ts +0 -51
  1091. package/frontend/src/contexts/AuthContext.tsx +0 -205
  1092. package/frontend/src/contexts/RealtimeContext.tsx +0 -119
  1093. package/frontend/src/docs/DocumentationApp.tsx +0 -104
  1094. package/frontend/src/docs/DocumentationDirectory.tsx +0 -159
  1095. package/frontend/src/docs/DocumentationLayout.tsx +0 -31
  1096. package/frontend/src/docs/DocumentationPage.tsx +0 -38
  1097. package/frontend/src/docs/DocumentationSidebar.tsx +0 -15
  1098. package/frontend/src/docs/MarkdownContent.tsx +0 -212
  1099. package/frontend/src/docs/README.md +0 -198
  1100. package/frontend/src/docs/main.tsx +0 -12
  1101. package/frontend/src/hooks/use-mobile.tsx +0 -19
  1102. package/frontend/src/hooks/useKeys.ts +0 -202
  1103. package/frontend/src/hooks/useMCPConfig.ts +0 -175
  1104. package/frontend/src/hooks/useRealtimeEntities.ts +0 -56
  1105. package/frontend/src/hooks/useRealtimeObservations.ts +0 -56
  1106. package/frontend/src/hooks/useRealtimeRelationships.ts +0 -56
  1107. package/frontend/src/hooks/useRealtimeSources.ts +0 -53
  1108. package/frontend/src/hooks/useRealtimeTimeline.ts +0 -53
  1109. package/frontend/src/hooks/useSettings.ts +0 -95
  1110. package/frontend/src/hooks/useStorageQuota.ts +0 -65
  1111. package/frontend/src/hooks/useTheme.tsx +0 -42
  1112. package/frontend/src/index.css +0 -247
  1113. package/frontend/src/lib/api_client.ts +0 -8
  1114. package/frontend/src/lib/auth.ts +0 -100
  1115. package/frontend/src/lib/idempotency.ts +0 -25
  1116. package/frontend/src/lib/utils.ts +0 -7
  1117. package/frontend/src/main.tsx +0 -40
  1118. package/frontend/src/sample-data/sets-medium.csv +0 -55
  1119. package/frontend/src/sample-data/sets-medium.ts +0 -45
  1120. package/frontend/src/shared/.eslintrc.json +0 -35
  1121. package/frontend/src/shared/.github/README.md +0 -55
  1122. package/frontend/src/shared/.github/workflows/ci.yml +0 -88
  1123. package/frontend/src/shared/.github/workflows/release.yml +0 -34
  1124. package/frontend/src/shared/.github/workflows/setup.yml +0 -56
  1125. package/frontend/src/shared/README.md +0 -163
  1126. package/frontend/src/shared/SETUP.md +0 -121
  1127. package/frontend/src/shared/components.json +0 -20
  1128. package/frontend/src/shared/package-lock.json +0 -4508
  1129. package/frontend/src/shared/package.json +0 -78
  1130. package/frontend/src/shared/src/components/AppSidebar.tsx +0 -77
  1131. package/frontend/src/shared/src/components/ErrorBoundary.tsx +0 -87
  1132. package/frontend/src/shared/src/components/Layout.tsx +0 -262
  1133. package/frontend/src/shared/src/components/ui/breadcrumb.tsx +0 -115
  1134. package/frontend/src/shared/src/components/ui/button.tsx +0 -56
  1135. package/frontend/src/shared/src/components/ui/calendar.tsx +0 -211
  1136. package/frontend/src/shared/src/components/ui/input.tsx +0 -22
  1137. package/frontend/src/shared/src/components/ui/popover.tsx +0 -29
  1138. package/frontend/src/shared/src/components/ui/separator.tsx +0 -29
  1139. package/frontend/src/shared/src/components/ui/sheet.tsx +0 -140
  1140. package/frontend/src/shared/src/components/ui/sidebar.tsx +0 -519
  1141. package/frontend/src/shared/src/components/ui/skeleton.tsx +0 -15
  1142. package/frontend/src/shared/src/components/ui/tooltip.tsx +0 -28
  1143. package/frontend/src/shared/src/hooks/use-mobile.tsx +0 -19
  1144. package/frontend/src/shared/src/index.css +0 -75
  1145. package/frontend/src/shared/src/index.ts +0 -34
  1146. package/frontend/src/shared/src/lib/utils.ts +0 -6
  1147. package/frontend/src/shared/tailwind.config.mjs +0 -67
  1148. package/frontend/src/shared/tsconfig.json +0 -29
  1149. package/frontend/src/site/seo_metadata.ts +0 -133
  1150. package/frontend/src/site/site_data.test.ts +0 -36
  1151. package/frontend/src/site/site_data.ts +0 -575
  1152. package/frontend/src/store/README.md +0 -10
  1153. package/frontend/src/utils/api_access.ts +0 -24
  1154. package/frontend/src/utils/csv.test.ts +0 -42
  1155. package/frontend/src/utils/csv.ts +0 -45
  1156. package/frontend/src/utils/csv_summary.ts +0 -139
  1157. package/frontend/src/utils/debounce.ts +0 -24
  1158. package/frontend/src/utils/entity_display.test.ts +0 -334
  1159. package/frontend/src/utils/entity_display.ts +0 -23
  1160. package/frontend/src/utils/entity_type_formatter.ts +0 -56
  1161. package/frontend/src/utils/error_utils.ts +0 -137
  1162. package/frontend/src/utils/property_keys.ts +0 -36
  1163. package/frontend/src/utils/schema_icons.test.ts +0 -59
  1164. package/frontend/src/utils/schema_icons.ts +0 -229
  1165. package/frontend/src/utils/time.ts +0 -37
  1166. package/frontend/src/vite-env.d.ts +0 -14
  1167. package/frontend/tsconfig.json +0 -27
  1168. package/frontend/tsconfig.node.json +0 -11
  1169. package/mcp/web-scraper/README.md +0 -475
  1170. package/mcp/web-scraper/SETUP.md +0 -150
  1171. package/mcp/web-scraper/__init__.py +0 -3
  1172. package/mcp/web-scraper/chatgpt_scraper_mcp_server.py +0 -435
  1173. package/mcp/web-scraper/create_repo.sh +0 -50
  1174. package/mcp/web-scraper/imports/spotify/playlist_3i0FY58V7n8XIUyWDm3AYA.json +0 -1052
  1175. package/mcp/web-scraper/nytimes_article.json +0 -7
  1176. package/mcp/web-scraper/plugins/__init__.py +0 -3
  1177. package/mcp/web-scraper/plugins/chatgpt_scraper.py +0 -105
  1178. package/mcp/web-scraper/plugins/metacast_scraper.py +0 -828
  1179. package/mcp/web-scraper/plugins/nyt_podcast_scraper.py +0 -538
  1180. package/mcp/web-scraper/plugins/spotify_scraper.py +0 -618
  1181. package/mcp/web-scraper/plugins/twitter_scraper.py +0 -576
  1182. package/mcp/web-scraper/plugins/url_body_fetcher.py +0 -172
  1183. package/mcp/web-scraper/pytest.ini +0 -11
  1184. package/mcp/web-scraper/requirements-test.txt +0 -6
  1185. package/mcp/web-scraper/requirements.txt +0 -6
  1186. package/mcp/web-scraper/run-web-scraper-mcp.sh +0 -38
  1187. package/mcp/web-scraper/scrape_spotify_playlist.py +0 -77
  1188. package/mcp/web-scraper/scraper.py +0 -819
  1189. package/mcp/web-scraper/scraper_base.py +0 -88
  1190. package/mcp/web-scraper/scraper_registry.py +0 -32
  1191. package/mcp/web-scraper/scripts/scrape_tweet_and_verify_referenced.py +0 -65
  1192. package/mcp/web-scraper/store_songs_to_parquet.py +0 -120
  1193. package/mcp/web-scraper/tests/__init__.py +0 -0
  1194. package/mcp/web-scraper/tests/conftest.py +0 -40
  1195. package/mcp/web-scraper/tests/integration/__init__.py +0 -0
  1196. package/mcp/web-scraper/tests/unit/__init__.py +0 -0
  1197. package/mcp/web-scraper/tests/unit/test_source_detection.py +0 -60
  1198. package/mcp/web-scraper/web_scraper_mcp_server.py +0 -639
  1199. package/openapi.yaml +0 -2071
  1200. package/playwright/fixtures/servers.ts +0 -180
  1201. package/playwright/global_setup.ts +0 -3
  1202. package/playwright/tests/auto-enhancement.spec.ts +0 -370
  1203. package/playwright/tests/coverage-map.json +0 -135
  1204. package/playwright/tests/design-system.spec.ts +0 -216
  1205. package/playwright/tests/entity-detail.spec.ts +0 -268
  1206. package/playwright/tests/entity-list.spec.ts +0 -266
  1207. package/playwright/tests/floating-settings-button.spec.ts +0 -95
  1208. package/playwright/tests/graph-integrity.spec.ts +0 -344
  1209. package/playwright/tests/helpers.ts +0 -560
  1210. package/playwright/tests/interpretations.spec.ts +0 -285
  1211. package/playwright/tests/mcp-configuration.spec.ts +0 -208
  1212. package/playwright/tests/mcp-relationships.spec.ts +0 -631
  1213. package/playwright/tests/mcp-store-retrieve.spec.ts +0 -288
  1214. package/playwright/tests/not-found.spec.ts +0 -30
  1215. package/playwright/tests/oauth-flow.spec.ts +0 -790
  1216. package/playwright/tests/observations.spec.ts +0 -295
  1217. package/playwright/tests/relationship-detail.spec.ts +0 -305
  1218. package/playwright/tests/relationships-list.spec.ts +0 -275
  1219. package/playwright/tests/schema-detail.spec.ts +0 -300
  1220. package/playwright/tests/schemas-list.spec.ts +0 -262
  1221. package/playwright/tests/search-flow.spec.ts +0 -216
  1222. package/playwright/tests/site-page.spec.ts +0 -23
  1223. package/playwright/tests/source-detail.spec.ts +0 -300
  1224. package/playwright/tests/sources-list.spec.ts +0 -254
  1225. package/playwright/tests/upload-flow.spec.ts +0 -279
  1226. package/playwright/tsconfig.json +0 -17
  1227. package/playwright/types.d.ts +0 -14
  1228. package/playwright/utils/servers.ts +0 -435
  1229. package/playwright.config.ts +0 -77
  1230. package/postcss.config.mjs +0 -7
  1231. package/scripts/add_ngrok_mapping.py +0 -99
  1232. package/scripts/add_required_observation_fields.js +0 -96
  1233. package/scripts/add_required_source_fields.js +0 -95
  1234. package/scripts/analyze-data-dir-schemas.js +0 -256
  1235. package/scripts/analyze_data_dir_for_schemas.ts +0 -378
  1236. package/scripts/analyze_low_confidence_fields.ts +0 -235
  1237. package/scripts/analyze_schema_redundancies.ts +0 -228
  1238. package/scripts/apply_documentation_rules.sh +0 -81
  1239. package/scripts/apply_file_naming_convention.sh +0 -75
  1240. package/scripts/backfill_entity_embeddings.ts +0 -95
  1241. package/scripts/branch.js +0 -47
  1242. package/scripts/build_github_pages_site.tsx +0 -65
  1243. package/scripts/check-playwright-coverage.ts +0 -159
  1244. package/scripts/check_agent_conversations.js +0 -72
  1245. package/scripts/check_agent_status.js +0 -156
  1246. package/scripts/check_auto_enhancement_status.ts +0 -215
  1247. package/scripts/check_auto_test_schemas.ts +0 -43
  1248. package/scripts/check_claude_sync.sh +0 -35
  1249. package/scripts/check_raw_fragments.ts +0 -46
  1250. package/scripts/check_remaining_eligibility.ts +0 -46
  1251. package/scripts/check_remaining_fields.ts +0 -58
  1252. package/scripts/check_sendgrid_credits.sh +0 -122
  1253. package/scripts/check_sendgrid_email_logs.sh +0 -150
  1254. package/scripts/check_snapshot_schema.ts +0 -18
  1255. package/scripts/check_sources.ts +0 -34
  1256. package/scripts/check_stale_snapshots.ts +0 -80
  1257. package/scripts/cleanup-auto-test-schemas.ts +0 -80
  1258. package/scripts/cleanup_test_schemas.ts +0 -275
  1259. package/scripts/com.neotoma.dev-servers.plist.template +0 -27
  1260. package/scripts/com.neotoma.watch-build.plist.template +0 -27
  1261. package/scripts/complete_submodule_setup.sh +0 -101
  1262. package/scripts/configure_sendgrid_dns.sh +0 -162
  1263. package/scripts/copy-env-to-worktree.js +0 -109
  1264. package/scripts/copy_pdf_worker_wrapper.js +0 -19
  1265. package/scripts/create_sample_parquet_files.py +0 -120
  1266. package/scripts/create_sample_parquet_files.ts +0 -146
  1267. package/scripts/create_sample_parquet_files_mcp.ts +0 -196
  1268. package/scripts/create_sample_parquet_files_simple.sh +0 -132
  1269. package/scripts/cursor-worktree-init.sh +0 -24
  1270. package/scripts/debug-ssl.sh +0 -86
  1271. package/scripts/debug_pdf_extraction.ts +0 -69
  1272. package/scripts/debug_vec_knn.ts +0 -69
  1273. package/scripts/dev-proxy.js +0 -406
  1274. package/scripts/dev-serve.js +0 -349
  1275. package/scripts/disable-dns.sh +0 -47
  1276. package/scripts/doctor.ts +0 -97
  1277. package/scripts/export_schema_snapshots.ts +0 -253
  1278. package/scripts/fix-ssl-cert.sh +0 -72
  1279. package/scripts/fix_mcp_phase1_tests.sh +0 -51
  1280. package/scripts/fix_observation_column_names.js +0 -51
  1281. package/scripts/fix_observation_priority_ordering.js +0 -45
  1282. package/scripts/fix_observation_properties_to_fields.js +0 -48
  1283. package/scripts/fix_sqlite_schema_mismatch.js +0 -75
  1284. package/scripts/generate-dev-cert.sh +0 -69
  1285. package/scripts/generate_pdf_fixtures.ts +0 -330
  1286. package/scripts/generate_schema_icons.ts +0 -216
  1287. package/scripts/get_branch_ports.js +0 -56
  1288. package/scripts/get_mcp_token.sh +0 -47
  1289. package/scripts/get_secrets_for_agents.js +0 -45
  1290. package/scripts/ingest_data_dir.ts +0 -215
  1291. package/scripts/ingest_finances_data.ts +0 -255
  1292. package/scripts/initialize-schemas.ts +0 -350
  1293. package/scripts/install_launchd_dev_servers.js +0 -54
  1294. package/scripts/install_launchd_watch_build.js +0 -53
  1295. package/scripts/instruct_agents_credential_setup.js +0 -130
  1296. package/scripts/instruct_agents_env_check.js +0 -99
  1297. package/scripts/instruct_agents_run_tests_until_passing.js +0 -128
  1298. package/scripts/kill_port.js +0 -174
  1299. package/scripts/linters/sync_agent_instructions_pre_commit.sh +0 -34
  1300. package/scripts/manage_error_debugging.sh +0 -228
  1301. package/scripts/manually_queue_enhancements.ts +0 -156
  1302. package/scripts/merge-dev.js +0 -319
  1303. package/scripts/migrate_auto_enhanced_fields.ts +0 -83
  1304. package/scripts/migrate_env_to_secrets.js +0 -69
  1305. package/scripts/migrate_plans.ts +0 -870
  1306. package/scripts/monitor_and_fix_snapshots.ts +0 -165
  1307. package/scripts/move_docs_rules_to_cursor.js +0 -54
  1308. package/scripts/move_docs_rules_to_cursor.sh +0 -24
  1309. package/scripts/neotoma-npm-reserve/README.md +0 -8
  1310. package/scripts/neotoma-npm-reserve/package.json +0 -10
  1311. package/scripts/notify_agents_env_vars.js +0 -109
  1312. package/scripts/notify_agents_setup_script.js +0 -148
  1313. package/scripts/notify_running_agents_credentials.js +0 -177
  1314. package/scripts/pdf_worker_polyfill.mjs +0 -27
  1315. package/scripts/pick-port.js +0 -108
  1316. package/scripts/postinstall.js +0 -84
  1317. package/scripts/process_remaining_eligible.ts +0 -70
  1318. package/scripts/prototypes/run-prototype.sh +0 -40
  1319. package/scripts/prune-merged-branches.js +0 -115
  1320. package/scripts/recompute_bob.ts +0 -64
  1321. package/scripts/recompute_snapshots.ts +0 -72
  1322. package/scripts/release_orchestrator.js +0 -1995
  1323. package/scripts/remove_duplicate_source_fields.js +0 -61
  1324. package/scripts/rename-branch-from-commit.js +0 -111
  1325. package/scripts/respawn_agents_with_credentials.js +0 -358
  1326. package/scripts/respawn_single_agent.js +0 -324
  1327. package/scripts/run-dev-server-with-tunnel-url.sh +0 -16
  1328. package/scripts/run-dev-task.js +0 -160
  1329. package/scripts/run_dev_servers_launchd.sh +0 -14
  1330. package/scripts/run_integration_tests.js +0 -523
  1331. package/scripts/run_migrations.js +0 -55
  1332. package/scripts/run_neotoma_mcp_stdio.sh +0 -11
  1333. package/scripts/run_neotoma_mcp_stdio_dev_watch.sh +0 -10
  1334. package/scripts/run_neotoma_mcp_stdio_prod.sh +0 -11
  1335. package/scripts/run_neotoma_mcp_stdio_prod_watch.sh +0 -12
  1336. package/scripts/run_watch_build_launchd.sh +0 -18
  1337. package/scripts/scrape_nytimes_article.py +0 -182
  1338. package/scripts/scrape_nytimes_simple.py +0 -152
  1339. package/scripts/secrets_manager.js +0 -232
  1340. package/scripts/send_agent_followup.js +0 -136
  1341. package/scripts/setup-data-symlink.js +0 -88
  1342. package/scripts/setup-dns.sh +0 -54
  1343. package/scripts/setup-env-copy-hook.sh +0 -67
  1344. package/scripts/setup-foundation-symlink.js +0 -45
  1345. package/scripts/setup-https-tunnel.sh +0 -409
  1346. package/scripts/setup-test-env.sh +0 -18
  1347. package/scripts/setup_agent_credentials.sh +0 -27
  1348. package/scripts/setup_agent_environment.sh +0 -39
  1349. package/scripts/setup_claude_instructions.sh +0 -437
  1350. package/scripts/setup_cloudflare_email_routing.sh +0 -158
  1351. package/scripts/setup_shared_submodule.sh +0 -100
  1352. package/scripts/simulate_npm_install.js +0 -70
  1353. package/scripts/spec_compliance_patterns.js +0 -260
  1354. package/scripts/sync-env-from-1password.sh +0 -100
  1355. package/scripts/sync_mcp_configs.js +0 -106
  1356. package/scripts/test-base64-store.ts +0 -53
  1357. package/scripts/test-error-handling.ts +0 -44
  1358. package/scripts/test-file-path-store.ts +0 -142
  1359. package/scripts/test-raw-fragments-idempotence.ts +0 -157
  1360. package/scripts/test-raw-fragments-insert.ts +0 -88
  1361. package/scripts/test-store-response-structure.ts +0 -125
  1362. package/scripts/test-store-structured-response.ts +0 -126
  1363. package/scripts/test_agent_env_spawn.js +0 -82
  1364. package/scripts/test_confidence.ts +0 -36
  1365. package/scripts/test_dsnp_parquet_reading.ts +0 -72
  1366. package/scripts/test_mcp_semantic_search.ts +0 -36
  1367. package/scripts/test_parquet_ingestion.ts +0 -52
  1368. package/scripts/test_raw_fragments_in_response.ts +0 -31
  1369. package/scripts/test_sendgrid_smtp.sh +0 -45
  1370. package/scripts/test_store_parquet_via_mcp.ts +0 -66
  1371. package/scripts/trigger_error_debug_cli.js +0 -271
  1372. package/scripts/unblock_agents.js +0 -208
  1373. package/scripts/validate-coverage-map.ts +0 -187
  1374. package/scripts/validate-doc-dependencies.js +0 -410
  1375. package/scripts/validate_all_fu_compliance.js +0 -752
  1376. package/scripts/validate_schema_sync.ts +0 -135
  1377. package/scripts/validate_spec_compliance.js +0 -669
  1378. package/scripts/watch_site.js +0 -68
  1379. package/scripts/wipe-local-database.js +0 -191
  1380. package/scripts/with_branch_ports.js +0 -496
  1381. package/site_pages/.nojekyll +0 -0
  1382. package/site_pages/FAVICON_CANDIDATES.md +0 -22
  1383. package/site_pages/favicon-1-diamond.svg +0 -3
  1384. package/site_pages/favicon-2-n-box.svg +0 -10
  1385. package/site_pages/favicon-3-waves.svg +0 -3
  1386. package/site_pages/favicon-4-n-minimal.svg +0 -3
  1387. package/site_pages/favicon-5-bars.svg +0 -5
  1388. package/site_pages/favicon.svg +0 -10
  1389. package/site_pages/index.html +0 -40
  1390. package/site_pages/robots.txt +0 -4
  1391. package/site_pages/sitemap.xml +0 -5
  1392. package/src/actions.ts +0 -4468
  1393. package/src/cli/agent_instructions_scan.ts +0 -793
  1394. package/src/cli/bootstrap.ts +0 -35
  1395. package/src/cli/config.ts +0 -338
  1396. package/src/cli/format.ts +0 -337
  1397. package/src/cli/index.ts +0 -11417
  1398. package/src/cli/init_abort.ts +0 -7
  1399. package/src/cli/mcp_config_scan.ts +0 -1359
  1400. package/src/cli/pack_rat.ts +0 -134
  1401. package/src/config/record_types.ts +0 -562
  1402. package/src/config.ts +0 -164
  1403. package/src/crypto/agent_identity.ts +0 -46
  1404. package/src/crypto/auth.ts +0 -110
  1405. package/src/crypto/column_encryption.ts +0 -109
  1406. package/src/crypto/crypto.test.ts +0 -311
  1407. package/src/crypto/envelope.ts +0 -240
  1408. package/src/crypto/export.ts +0 -131
  1409. package/src/crypto/index.ts +0 -13
  1410. package/src/crypto/key_derivation.ts +0 -176
  1411. package/src/crypto/keys.ts +0 -85
  1412. package/src/crypto/mcp_auth_token.ts +0 -35
  1413. package/src/crypto/signature.ts +0 -50
  1414. package/src/crypto/types.ts +0 -48
  1415. package/src/db.ts +0 -66
  1416. package/src/embeddings.ts +0 -76
  1417. package/src/index.ts +0 -25
  1418. package/src/mcp_ws_bridge.ts +0 -222
  1419. package/src/middleware/encrypt_response.ts +0 -39
  1420. package/src/normalize.ts +0 -351
  1421. package/src/record_types.test.ts +0 -36
  1422. package/src/reducers/index.ts +0 -5
  1423. package/src/reducers/observation_reducer.ts +0 -458
  1424. package/src/reducers/relationship_reducer.ts +0 -291
  1425. package/src/repositories/db/capability_repo.ts +0 -29
  1426. package/src/repositories/file/capability_repo.ts +0 -29
  1427. package/src/repositories/index.ts +0 -7
  1428. package/src/repositories/interfaces.ts +0 -112
  1429. package/src/repositories/sqlite/__tests__/local_db_adapter.test.ts +0 -138
  1430. package/src/repositories/sqlite/local_db_adapter.ts +0 -961
  1431. package/src/repositories/sqlite/sqlite_client.ts +0 -378
  1432. package/src/server.ts +0 -6157
  1433. package/src/services/__tests__/csv_chunking.test.ts +0 -125
  1434. package/src/services/__tests__/deletion.test.ts +0 -358
  1435. package/src/services/__tests__/entity_semantic_search.test.ts +0 -45
  1436. package/src/services/__tests__/local_auth.test.ts +0 -52
  1437. package/src/services/__tests__/local_entity_embedding.test.ts +0 -108
  1438. package/src/services/__tests__/mcp_oauth.test.ts +0 -407
  1439. package/src/services/__tests__/oauth_key_gate.test.ts +0 -50
  1440. package/src/services/__tests__/oauth_state.test.ts +0 -40
  1441. package/src/services/__tests__/schema_icon_service.test.ts +0 -151
  1442. package/src/services/auto_enhancement_processor.ts +0 -237
  1443. package/src/services/capability_registry.ts +0 -202
  1444. package/src/services/correction.ts +0 -87
  1445. package/src/services/csv_chunking.ts +0 -128
  1446. package/src/services/csv_row_extraction.ts +0 -124
  1447. package/src/services/dashboard_stats.ts +0 -115
  1448. package/src/services/deletion.ts +0 -482
  1449. package/src/services/deletion_monitor.ts +0 -314
  1450. package/src/services/encryption_service.ts +0 -110
  1451. package/src/services/entity_merge.ts +0 -100
  1452. package/src/services/entity_queries.ts +0 -518
  1453. package/src/services/entity_resolution.ts +0 -367
  1454. package/src/services/entity_semantic_search.ts +0 -64
  1455. package/src/services/entity_snapshot_embedding.ts +0 -115
  1456. package/src/services/field_canonicalization.ts +0 -393
  1457. package/src/services/field_converters.ts +0 -173
  1458. package/src/services/field_validation.ts +0 -173
  1459. package/src/services/file_text_extraction.ts +0 -213
  1460. package/src/services/finances_field_mapping.ts +0 -473
  1461. package/src/services/gdpr_deletion.ts +0 -497
  1462. package/src/services/interpretation.ts +0 -939
  1463. package/src/services/llm_extraction.ts +0 -548
  1464. package/src/services/local_auth.ts +0 -154
  1465. package/src/services/local_entity_embedding.ts +0 -196
  1466. package/src/services/mcp_auth.ts +0 -75
  1467. package/src/services/mcp_oauth.ts +0 -1625
  1468. package/src/services/mcp_oauth_errors.ts +0 -139
  1469. package/src/services/oauth_key_gate.ts +0 -116
  1470. package/src/services/oauth_state.ts +0 -70
  1471. package/src/services/observation_identity.ts +0 -161
  1472. package/src/services/observation_storage.ts +0 -162
  1473. package/src/services/parquet_reader.ts +0 -375
  1474. package/src/services/public_key_registry.ts +0 -103
  1475. package/src/services/raw_fragments.ts +0 -217
  1476. package/src/services/raw_storage.ts +0 -267
  1477. package/src/services/relationships.ts +0 -495
  1478. package/src/services/schema_definitions.ts +0 -2041
  1479. package/src/services/schema_icon_service.ts +0 -260
  1480. package/src/services/schema_inference.ts +0 -401
  1481. package/src/services/schema_recommendation.ts +0 -1475
  1482. package/src/services/schema_registry.ts +0 -1403
  1483. package/src/services/snapshot_computation.ts +0 -92
  1484. package/src/services/source_identity.ts +0 -22
  1485. package/src/services/timeline_events.ts +0 -198
  1486. package/src/shared/action_handlers/entity_handlers.ts +0 -115
  1487. package/src/shared/action_schemas.test.ts +0 -59
  1488. package/src/shared/action_schemas.ts +0 -317
  1489. package/src/shared/api_client.ts +0 -86
  1490. package/src/shared/contract_mappings.ts +0 -646
  1491. package/src/shared/entity_display_name.ts +0 -64
  1492. package/src/shared/local_transport.ts +0 -126
  1493. package/src/shared/openapi_schema.ts +0 -175
  1494. package/src/shared/openapi_types.ts +0 -2810
  1495. package/src/shared/record_display_summary.ts +0 -133
  1496. package/src/utils/__tests__/csv_summary.test.ts +0 -53
  1497. package/src/utils/__tests__/lucide_icons.test.ts +0 -133
  1498. package/src/utils/chat.test.ts +0 -42
  1499. package/src/utils/chat.ts +0 -35
  1500. package/src/utils/csv.test.ts +0 -41
  1501. package/src/utils/csv.ts +0 -77
  1502. package/src/utils/csv_summary.ts +0 -195
  1503. package/src/utils/log_encrypt.ts +0 -144
  1504. package/src/utils/logger.ts +0 -94
  1505. package/src/utils/lucide_icons.ts +0 -279
  1506. package/src/utils/property_keys.test.ts +0 -109
  1507. package/src/utils/property_keys.ts +0 -66
  1508. package/src/utils/property_sanitizer.test.ts +0 -105
  1509. package/src/utils/property_sanitizer.ts +0 -135
  1510. package/src/version_check.test.ts +0 -107
  1511. package/src/version_check.ts +0 -58
  1512. package/supabase/.temp/cli-latest +0 -0
  1513. package/tailwind.config.mjs +0 -134
  1514. package/tests/TEST_ACTION_MATRIX.md +0 -387
  1515. package/tests/agent/mcp_instruction_behavior.test.ts +0 -72
  1516. package/tests/agent/runner/agent_runner.ts +0 -27
  1517. package/tests/agent/runner/claude_code_runner.ts +0 -128
  1518. package/tests/agent/runner/cursor_cli_runner.ts +0 -7
  1519. package/tests/cli/api_client_offline_fallback.test.ts +0 -89
  1520. package/tests/cli/cli_admin_commands.test.ts +0 -268
  1521. package/tests/cli/cli_api_commands.test.ts +0 -75
  1522. package/tests/cli/cli_auth_commands.test.ts +0 -67
  1523. package/tests/cli/cli_command_coverage_guard.test.ts +0 -50
  1524. package/tests/cli/cli_correction_commands.test.ts +0 -277
  1525. package/tests/cli/cli_direct_invocation_parity.test.ts +0 -46
  1526. package/tests/cli/cli_entity_commands.test.ts +0 -324
  1527. package/tests/cli/cli_entity_subcommands.test.ts +0 -422
  1528. package/tests/cli/cli_infra_commands.test.ts +0 -475
  1529. package/tests/cli/cli_mcp_commands.test.ts +0 -51
  1530. package/tests/cli/cli_observation_commands.test.ts +0 -232
  1531. package/tests/cli/cli_query_commands.test.ts +0 -369
  1532. package/tests/cli/cli_relationship_commands.test.ts +0 -410
  1533. package/tests/cli/cli_schema_commands.test.ts +0 -296
  1534. package/tests/cli/cli_session_startup_ux.test.ts +0 -112
  1535. package/tests/cli/cli_smoke.test.ts +0 -456
  1536. package/tests/cli/cli_source_commands.test.ts +0 -214
  1537. package/tests/cli/cli_stats_commands.test.ts +0 -51
  1538. package/tests/cli/cli_store_commands.test.ts +0 -339
  1539. package/tests/cli/cli_timeline_commands.test.ts +0 -294
  1540. package/tests/cli/config.test.ts +0 -36
  1541. package/tests/cli/config_api_discovery.test.ts +0 -91
  1542. package/tests/cli/test_command_detection.test.ts +0 -128
  1543. package/tests/cli/test_debug_tty.test.ts +0 -32
  1544. package/tests/contract/contract_mapping.test.ts +0 -131
  1545. package/tests/contract/contract_mcp_cli_parity.test.ts +0 -223
  1546. package/tests/contract/openapi_schema.test.ts +0 -15
  1547. package/tests/fixtures/README.md +0 -22
  1548. package/tests/fixtures/agent_mcp/insurance_policy.txt +0 -5
  1549. package/tests/fixtures/agent_mcp_cases.json +0 -137
  1550. package/tests/fixtures/csv/sample_balances.csv +0 -7
  1551. package/tests/fixtures/csv/sample_contacts.csv +0 -6
  1552. package/tests/fixtures/csv/sample_crypto_transactions.csv +0 -6
  1553. package/tests/fixtures/csv/sample_flows.csv +0 -7
  1554. package/tests/fixtures/csv/sample_holdings.csv +0 -7
  1555. package/tests/fixtures/csv/sample_income.csv +0 -7
  1556. package/tests/fixtures/csv/sample_purchases.csv +0 -7
  1557. package/tests/fixtures/csv/sample_tax_events.csv +0 -7
  1558. package/tests/fixtures/csv/sample_transactions.csv +0 -7
  1559. package/tests/fixtures/csv/sample_transfers.csv +0 -6
  1560. package/tests/fixtures/expected/contact_snapshot.json +0 -19
  1561. package/tests/fixtures/expected/transaction_snapshot.json +0 -21
  1562. package/tests/fixtures/helpers.ts +0 -522
  1563. package/tests/fixtures/json/account.json +0 -63
  1564. package/tests/fixtures/json/argument.json +0 -39
  1565. package/tests/fixtures/json/balance.json +0 -56
  1566. package/tests/fixtures/json/belief.json +0 -44
  1567. package/tests/fixtures/json/contact.json +0 -83
  1568. package/tests/fixtures/json/contract.json +0 -62
  1569. package/tests/fixtures/json/crypto_transaction.json +0 -67
  1570. package/tests/fixtures/json/domain.json +0 -41
  1571. package/tests/fixtures/json/emotion.json +0 -31
  1572. package/tests/fixtures/json/fixed_cost.json +0 -75
  1573. package/tests/fixtures/json/flow.json +0 -71
  1574. package/tests/fixtures/json/habit.json +0 -54
  1575. package/tests/fixtures/json/habit_completion.json +0 -31
  1576. package/tests/fixtures/json/habit_objective.json +0 -40
  1577. package/tests/fixtures/json/holding.json +0 -65
  1578. package/tests/fixtures/json/income.json +0 -59
  1579. package/tests/fixtures/json/liability.json +0 -50
  1580. package/tests/fixtures/json/order.json +0 -62
  1581. package/tests/fixtures/json/outcome.json +0 -56
  1582. package/tests/fixtures/json/process.json +0 -43
  1583. package/tests/fixtures/json/property.json +0 -52
  1584. package/tests/fixtures/json/purchase.json +0 -66
  1585. package/tests/fixtures/json/research.json +0 -53
  1586. package/tests/fixtures/json/strategy.json +0 -68
  1587. package/tests/fixtures/json/task_attachment.json +0 -62
  1588. package/tests/fixtures/json/task_comment.json +0 -54
  1589. package/tests/fixtures/json/task_dependency.json +0 -51
  1590. package/tests/fixtures/json/task_story.json +0 -53
  1591. package/tests/fixtures/json/tax_event.json +0 -60
  1592. package/tests/fixtures/json/tax_filing.json +0 -61
  1593. package/tests/fixtures/json/transaction.json +0 -62
  1594. package/tests/fixtures/json/transfer.json +0 -60
  1595. package/tests/fixtures/json/wallet.json +0 -51
  1596. package/tests/fixtures/json/workout.json +0 -45
  1597. package/tests/fixtures/pdf/sample-upload.txt +0 -2
  1598. package/tests/fixtures/pdf/sample_bank_statement.md +0 -32
  1599. package/tests/fixtures/pdf/sample_bank_statement.pdf +0 -0
  1600. package/tests/fixtures/pdf/sample_contract.md +0 -27
  1601. package/tests/fixtures/pdf/sample_contract.pdf +0 -0
  1602. package/tests/fixtures/pdf/sample_exercise.md +0 -31
  1603. package/tests/fixtures/pdf/sample_exercise.pdf +0 -0
  1604. package/tests/fixtures/pdf/sample_holding_statement.md +0 -29
  1605. package/tests/fixtures/pdf/sample_holding_statement.pdf +0 -0
  1606. package/tests/fixtures/pdf/sample_invoice.md +0 -27
  1607. package/tests/fixtures/pdf/sample_invoice.pdf +0 -108
  1608. package/tests/fixtures/pdf/sample_meal.md +0 -28
  1609. package/tests/fixtures/pdf/sample_meal.pdf +0 -111
  1610. package/tests/fixtures/pdf/sample_note.md +0 -28
  1611. package/tests/fixtures/pdf/sample_note.pdf +0 -0
  1612. package/tests/fixtures/pdf/sample_receipt.md +0 -28
  1613. package/tests/fixtures/pdf/sample_receipt.pdf +0 -0
  1614. package/tests/fixtures/pdf/sample_research.md +0 -31
  1615. package/tests/fixtures/pdf/sample_research.pdf +0 -0
  1616. package/tests/fixtures/pdf/sample_tax_form.md +0 -38
  1617. package/tests/fixtures/pdf/sample_tax_form.pdf +0 -0
  1618. package/tests/fixtures/pdf/sample_transaction_receipt.md +0 -28
  1619. package/tests/fixtures/pdf/sample_transaction_receipt.pdf +0 -0
  1620. package/tests/fixtures/replay_graph.test.ts +0 -125
  1621. package/tests/fixtures/sample_invoice.pdf +0 -1
  1622. package/tests/fixtures/types.ts +0 -321
  1623. package/tests/fixtures/validation.ts +0 -333
  1624. package/tests/helpers/cleanup_helpers.ts +0 -550
  1625. package/tests/helpers/create_test_parquet.ts +0 -187
  1626. package/tests/helpers/cross_layer_helpers.ts +0 -130
  1627. package/tests/helpers/mcp_action_helper.ts +0 -55
  1628. package/tests/helpers/mcp_spec_validators.ts +0 -433
  1629. package/tests/helpers/test_data_helpers.ts +0 -226
  1630. package/tests/helpers/test_schema_helpers.ts +0 -264
  1631. package/tests/integration/cli_to_mcp_entities.test.ts +0 -211
  1632. package/tests/integration/cli_to_mcp_relationships.test.ts +0 -196
  1633. package/tests/integration/cli_to_mcp_schemas.test.ts +0 -232
  1634. package/tests/integration/cli_to_mcp_stats_snapshots.test.ts +0 -67
  1635. package/tests/integration/cli_to_mcp_store.test.ts +0 -249
  1636. package/tests/integration/dashboard_stats.test.ts +0 -180
  1637. package/tests/integration/entity_queries.test.ts +0 -666
  1638. package/tests/integration/field_converters.test.ts +0 -266
  1639. package/tests/integration/fixture_mcp_store_replay.test.ts +0 -255
  1640. package/tests/integration/gdpr_deletion.test.ts +0 -433
  1641. package/tests/integration/llm_extraction.test.ts +0 -554
  1642. package/tests/integration/mcp_actions_matrix.test.ts +0 -969
  1643. package/tests/integration/mcp_auto_enhancement.test.ts +0 -659
  1644. package/tests/integration/mcp_auto_schema_creation.test.ts +0 -658
  1645. package/tests/integration/mcp_correction_variations.test.ts +0 -641
  1646. package/tests/integration/mcp_entity_creation.test.ts +0 -333
  1647. package/tests/integration/mcp_entity_variations.test.ts +0 -474
  1648. package/tests/integration/mcp_graph_variations.test.ts +0 -380
  1649. package/tests/integration/mcp_npm_check_update.test.ts +0 -54
  1650. package/tests/integration/mcp_query_variations.test.ts +0 -392
  1651. package/tests/integration/mcp_relationship_variations.test.ts +0 -433
  1652. package/tests/integration/mcp_resource_variations.test.ts +0 -423
  1653. package/tests/integration/mcp_resources.test.ts +0 -452
  1654. package/tests/integration/mcp_schema_actions.test.ts +0 -673
  1655. package/tests/integration/mcp_schema_variations.test.ts +0 -428
  1656. package/tests/integration/mcp_store_parquet.test.ts +0 -466
  1657. package/tests/integration/mcp_store_unstructured.test.ts +0 -413
  1658. package/tests/integration/mcp_store_variations.test.ts +0 -452
  1659. package/tests/integration/nonjson_csv_store_behavior.test.ts +0 -348
  1660. package/tests/integration/nonjson_fixtures_mcp_replay.test.ts +0 -253
  1661. package/tests/integration/observation_ingestion.test.ts +0 -362
  1662. package/tests/integration/payload/payload_submission.test.ts +0 -296
  1663. package/tests/integration/payload_compiler.test.ts +0 -347
  1664. package/tests/integration/public_key_registry.test.ts +0 -204
  1665. package/tests/integration/relationship_snapshots.test.ts +0 -235
  1666. package/tests/integration/schema_recommendation_integration.test.ts +0 -418
  1667. package/tests/integration/setup_v0.2.0_schemas.ts +0 -61
  1668. package/tests/integration/v0.2.0_ingestion.test.ts +0 -527
  1669. package/tests/services/auto_enhancement_converter_detection.test.ts +0 -663
  1670. package/tests/services/auto_enhancement_processor.test.ts +0 -445
  1671. package/tests/services/capability_registry.test.ts +0 -254
  1672. package/tests/services/converter_detection_unit.test.ts +0 -168
  1673. package/tests/services/encryption_service.test.ts +0 -147
  1674. package/tests/services/entity_resolution.test.ts +0 -285
  1675. package/tests/services/field_canonicalization.test.ts +0 -215
  1676. package/tests/services/field_converters.test.ts +0 -211
  1677. package/tests/services/field_validation.test.ts +0 -318
  1678. package/tests/services/finances_field_mapping.test.ts +0 -245
  1679. package/tests/services/interpretation.test.ts +0 -144
  1680. package/tests/services/mcp_auth.test.ts +0 -34
  1681. package/tests/services/observation_identity.test.ts +0 -154
  1682. package/tests/services/payload_identity.test.ts +0 -325
  1683. package/tests/services/payload_schema.test.ts +0 -282
  1684. package/tests/services/raw_storage.test.ts +0 -546
  1685. package/tests/services/schema_definitions.test.ts +0 -119
  1686. package/tests/services/schema_recommendation.test.ts +0 -590
  1687. package/tests/services/schema_registry_incremental.test.ts +0 -882
  1688. package/tests/services/summary.test.ts +0 -26
  1689. package/tests/unit/bigint_serialization.test.ts +0 -220
  1690. package/tests/unit/observation_reducer_converters.test.ts +0 -430
  1691. package/tests/unit/parquet_reader.test.ts +0 -155
  1692. package/tests/unit/relationship_reducer.test.ts +0 -208
  1693. package/tests/unit/schema_inference.test.ts +0 -358
  1694. package/tests/unit/seo_metadata.test.ts +0 -52
  1695. package/tsconfig.json +0 -22
  1696. package/vite.config.ts +0 -165
  1697. package/vitest.config.ts +0 -111
  1698. package/vitest.global_setup.ts +0 -42
  1699. package/vitest.setup.ts +0 -89
package/src/actions.ts DELETED
@@ -1,4468 +0,0 @@
1
- import express from "express";
2
- import cors from "cors";
3
- import helmet from "helmet";
4
- import morgan from "morgan";
5
- import rateLimit from "express-rate-limit";
6
- import { z } from "zod";
7
- import { randomUUID } from "node:crypto";
8
- import { db } from "./db.js";
9
- import { config } from "./config.js";
10
- import fs from "fs";
11
- import path from "path";
12
- import yaml from "js-yaml";
13
- import {
14
- ensurePublicKeyRegistered,
15
- getPublicKey,
16
- getUserIdFromBearerToken,
17
- isBearerTokenValid,
18
- } from "./services/public_key_registry.js";
19
- import { verifyRequest, parseAuthHeader } from "./crypto/auth.js";
20
- import { encryptResponseMiddleware } from "./middleware/encrypt_response.js";
21
- import { initServerKeys } from "./services/encryption_service.js";
22
- import { storeRawContent } from "./services/raw_storage.js";
23
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
24
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
25
- import { NeotomaServer } from "./server.js";
26
- import { logger } from "./utils/logger.js";
27
- import { OAuthError } from "./services/mcp_oauth_errors.js";
28
- import { ensureLocalDevUser, LOCAL_DEV_USER_ID } from "./services/local_auth.js";
29
- import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
30
- import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
31
- import {
32
- isOauthKeyCredentialValid,
33
- normalizeOauthNextPath,
34
- OAuthKeySessionStore,
35
- } from "./services/oauth_key_gate.js";
36
- import {
37
- AnalyzeSchemaCandidatesRequestSchema,
38
- CorrectEntityRequestSchema,
39
- CreateRelationshipRequestSchema,
40
- DeleteEntityRequestSchema,
41
- DeleteRelationshipRequestSchema,
42
- EntitiesQueryRequestSchema,
43
- EntitySnapshotRequestSchema,
44
- FieldProvenanceRequestSchema,
45
- GetSchemaRecommendationsRequestSchema,
46
- ListObservationsRequestSchema,
47
- ListRelationshipsRequestSchema,
48
- MergeEntitiesRequestSchema,
49
- ObservationsQueryRequestSchema,
50
- RegisterSchemaRequestSchema,
51
- RelationshipSnapshotRequestSchema,
52
- ReinterpretRequestSchema,
53
- InterpretUninterpretedRequestSchema,
54
- RestoreEntityRequestSchema,
55
- RestoreRelationshipRequestSchema,
56
- RetrieveEntityByIdentifierSchema,
57
- RetrieveGraphNeighborhoodSchema,
58
- RetrieveRelatedEntitiesSchema,
59
- StoreRequestSchema,
60
- StoreUnstructuredRequestSchema,
61
- UpdateSchemaIncrementalRequestSchema,
62
- } from "./shared/action_schemas.js";
63
- import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
64
- import {
65
- prepareEntitySnapshotWithEmbedding,
66
- upsertEntitySnapshotWithEmbedding,
67
- } from "./services/entity_snapshot_embedding.js";
68
- // import { setupDocumentationRoutes } from "./routes/documentation.js";
69
-
70
- type ErrorEnvelope = {
71
- error_code: string;
72
- message: string;
73
- details?: Record<string, unknown>;
74
- trace_id?: string;
75
- timestamp: string;
76
- };
77
-
78
- export const app = express();
79
- // Trust proxy headers (required for express-rate-limit when X-Forwarded-For is present)
80
- app.set("trust proxy", true);
81
- // Configure CSP to allow CDN scripts for the uploader and API connects
82
- app.use(
83
- helmet({
84
- contentSecurityPolicy: {
85
- useDefaults: true,
86
- directives: {
87
- defaultSrc: ["'self'"],
88
- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://unpkg.com"],
89
- connectSrc: ["'self'", "http:", "https:"],
90
- imgSrc: ["'self'", "data:"],
91
- styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"],
92
- objectSrc: ["'none'"],
93
- frameSrc: ["'self'"],
94
- },
95
- },
96
- })
97
- );
98
- // Configure CORS to allow frontend origin
99
- const corsOptions = {
100
- origin:
101
- process.env.NEOTOMA_FRONTEND_URL ||
102
- process.env.FRONTEND_URL ||
103
- "http://localhost:5195",
104
- credentials: true,
105
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
106
- allowedHeaders: [
107
- "Content-Type",
108
- "Authorization",
109
- "X-Requested-With",
110
- "X-Connection-Id",
111
- "mcp-session-id",
112
- ],
113
- exposedHeaders: ["Content-Type"],
114
- optionsSuccessStatus: 200, // Some legacy browsers (IE11, various SmartTVs) choke on 204
115
- };
116
- app.use(cors(corsOptions));
117
- app.use(express.json({ limit: "10mb" }));
118
- app.use(morgan("dev"));
119
-
120
- // Rate limiters for OAuth endpoints
121
- // validate.trustProxy: false — we use trust proxy behind one proxy; skip strict IP check
122
- const rateLimitOptions = {
123
- standardHeaders: true,
124
- legacyHeaders: false,
125
- validate: { trustProxy: false } as const,
126
- };
127
- const oauthInitiateLimit = rateLimit({
128
- windowMs: 60 * 1000, // 1 minute
129
- max: 5,
130
- message: "Too many OAuth initiation requests, please try again later",
131
- ...rateLimitOptions,
132
- });
133
-
134
- const oauthCallbackLimit = rateLimit({
135
- windowMs: 60 * 1000,
136
- max: 10,
137
- message: "Too many OAuth callback requests, please try again later",
138
- ...rateLimitOptions,
139
- });
140
-
141
- const oauthTokenLimit = rateLimit({
142
- windowMs: 60 * 1000,
143
- max: 20,
144
- message: "Too many token requests, please try again later",
145
- ...rateLimitOptions,
146
- });
147
-
148
- const oauthRegisterLimit = rateLimit({
149
- windowMs: 60 * 1000,
150
- max: 10,
151
- message: "Too many registration requests, please try again later",
152
- ...rateLimitOptions,
153
- });
154
-
155
- // Favicon (no-auth) to avoid 401 noise when not present on disk
156
- app.get("/favicon.ico", (_req, res) => res.status(204).end());
157
-
158
- // ============================================================================
159
- // OAuth discovery (RFC 8414 / MCP Authorization) for Cursor and other clients
160
- // ============================================================================
161
-
162
- app.get("/.well-known/oauth-authorization-server", (req, res) => {
163
- const base = config.apiBase;
164
- res.setHeader("Content-Type", "application/json");
165
- res.json({
166
- issuer: base,
167
- authorization_endpoint: `${base}/mcp/oauth/authorize`,
168
- token_endpoint: `${base}/mcp/oauth/token`,
169
- registration_endpoint: `${base}/mcp/oauth/register`,
170
- scopes_supported: ["openid", "email"],
171
- response_types_supported: ["code"],
172
- code_challenge_methods_supported: ["S256"],
173
- grant_types_supported: ["authorization_code", "refresh_token"],
174
- token_endpoint_auth_methods_supported: ["none"],
175
- });
176
- });
177
-
178
- app.get("/.well-known/oauth-protected-resource", async (req, res) => {
179
- // oauth-repro pattern: return 401 on protected resource endpoint when unauthenticated
180
- // Validate X-Connection-Id here so Cursor gets consistent invalid_token signal (helps trigger Connect prompt)
181
- const authHeader = (req.headers["authorization"] || req.headers["Authorization"]) as
182
- | string
183
- | undefined;
184
- const connectionIdHeader = req.headers["x-connection-id"] || req.headers["X-Connection-Id"];
185
-
186
- // If X-Connection-Id is sent, validate it. Invalid/expired connection → 401 with error=invalid_token
187
- // so Cursor may clear stored credentials and show Connect button.
188
- if (connectionIdHeader && !authHeader?.startsWith("Bearer ")) {
189
- try {
190
- const { getAccessTokenForConnection } = await import("./services/mcp_oauth.js");
191
- await getAccessTokenForConnection(connectionIdHeader as string);
192
- } catch {
193
- const wwwAuth = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource", error="invalid_token", error_description="Connection invalid or expired. Remove X-Connection-Id from mcp.json and click Connect to re-authenticate."`;
194
- res.setHeader("WWW-Authenticate", wwwAuth);
195
- return res.status(401).json({
196
- error: "invalid_token",
197
- error_description:
198
- "Connection invalid or expired. Remove X-Connection-Id from mcp.json and click Connect to re-authenticate.",
199
- });
200
- }
201
- }
202
-
203
- if (!authHeader?.startsWith("Bearer ") && !connectionIdHeader) {
204
- res.setHeader(
205
- "WWW-Authenticate",
206
- `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource"`
207
- );
208
- return res.status(401).json({
209
- error: "Unauthorized: Authentication required",
210
- });
211
- }
212
-
213
- res.setHeader("Content-Type", "application/json");
214
- res.json({
215
- authorization_servers: [config.apiBase],
216
- });
217
- });
218
-
219
- // Server info endpoint (no-auth) - exposes server port for MCP configuration
220
- // When NEOTOMA_MCP_PROXY_URL or MCP_PROXY_URL is set (e.g. ngrok tunnel), mcpUrl uses it so "Add to Cursor" uses the proxy.
221
- app.get("/server-info", (_req, res) => {
222
- const httpPortEnv =
223
- process.env.NEOTOMA_HTTP_PORT || process.env.HTTP_PORT;
224
- const httpPort = httpPortEnv
225
- ? parseInt(httpPortEnv, 10)
226
- : config.httpPort || 8080;
227
- const mcpBase =
228
- process.env.NEOTOMA_MCP_PROXY_URL ||
229
- process.env.MCP_PROXY_URL ||
230
- config.apiBase;
231
- const base = mcpBase.replace(/\/$/, "");
232
- const mcpUrl = base.endsWith("/mcp") ? base : `${base}/mcp`;
233
- res.json({
234
- httpPort,
235
- apiBase: config.apiBase,
236
- mcpUrl,
237
- });
238
- });
239
-
240
- // ============================================================================
241
- // MCP StreamableHTTP Endpoint (OAuth-enabled MCP transport)
242
- // ============================================================================
243
-
244
- // Store MCP transports by session ID
245
- const mcpTransports = new Map<string, StreamableHTTPServerTransport>();
246
- // Store server instances by session ID to preserve authentication state
247
- const mcpServerInstances = new Map<string, NeotomaServer>();
248
-
249
- /** True when request is to localhost/127.0.0.1 (local access). Tunnel requests have a non-local Host. */
250
- function isLocalRequest(req: express.Request): boolean {
251
- const host = (((req.headers["host"] || req.headers["Host"]) as string) || "")
252
- .split(":")[0]
253
- .toLowerCase();
254
- return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
255
- }
256
-
257
- const OAUTH_KEY_SESSION_COOKIE = "neotoma_oauth_key_session";
258
- const oauthKeySessions = new OAuthKeySessionStore();
259
-
260
- function readCookie(req: express.Request, name: string): string | undefined {
261
- const header = (req.headers["cookie"] || req.headers["Cookie"] || "") as string;
262
- if (!header) return undefined;
263
- const parts = header.split(";").map((part) => part.trim());
264
- for (const part of parts) {
265
- const eq = part.indexOf("=");
266
- if (eq <= 0) continue;
267
- const key = part.slice(0, eq);
268
- if (key !== name) continue;
269
- return decodeURIComponent(part.slice(eq + 1));
270
- }
271
- return undefined;
272
- }
273
-
274
- function hasValidOAuthKeySession(req: express.Request): boolean {
275
- const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
276
- return oauthKeySessions.isValid(token);
277
- }
278
-
279
- function setOAuthKeySessionCookie(req: express.Request, res: express.Response): void {
280
- const token = oauthKeySessions.create();
281
- const forwardedProto =
282
- ((req.headers["x-forwarded-proto"] || req.headers["X-Forwarded-Proto"]) as string | undefined)
283
- ?.split(",")[0]
284
- ?.trim()
285
- ?.toLowerCase() || "";
286
- const secure = req.secure || req.protocol === "https" || forwardedProto === "https";
287
- res.cookie(OAUTH_KEY_SESSION_COOKIE, token, {
288
- httpOnly: true,
289
- sameSite: "lax",
290
- secure,
291
- path: "/mcp/oauth",
292
- maxAge: 15 * 60 * 1000,
293
- });
294
- }
295
-
296
- // MCP StreamableHTTP endpoint (GET, POST, DELETE)
297
- // This endpoint enables Cursor's "Connect" button for OAuth authentication
298
- app.all("/mcp", async (req, res) => {
299
- try {
300
- const sessionId = req.headers["mcp-session-id"] as string | undefined;
301
-
302
- // Check for authentication BEFORE processing MCP requests
303
- // When encryption off: no auth by default; optional NEOTOMA_BEARER_TOKEN (or OAuth)
304
- // When encryption on: require Bearer token derived from private key (same key as data encryption)
305
- const authHeader = (req.headers["authorization"] || req.headers["Authorization"]) as
306
- | string
307
- | undefined;
308
- let connectionIdHeader = (req.headers["x-connection-id"] || req.headers["X-Connection-Id"]) as
309
- | string
310
- | undefined;
311
-
312
- if (config.encryption.enabled) {
313
- // Encryption on: require static token derived from user's private key
314
- const expectedToken = getMcpAuthToken();
315
- if (authHeader?.startsWith("Bearer ") && expectedToken) {
316
- const token = authHeader.slice(7).trim();
317
- if (token === expectedToken) {
318
- req.headers["x-connection-id"] = "dev-local";
319
- connectionIdHeader = "dev-local";
320
- }
321
- }
322
- } else {
323
- // Encryption off: local requests can use no-auth. HTTP (insecure) defaults to anonymous 000... user; HTTPS/localhost can use dev-local.
324
- if (!authHeader?.startsWith("Bearer ") && !connectionIdHeader) {
325
- if (isLocalRequest(req)) {
326
- const isInsecure = req.protocol === "http" || !(req as any).secure;
327
- if (isInsecure) {
328
- req.headers["x-connection-id"] = "dev-local-http";
329
- connectionIdHeader = "dev-local-http";
330
- } else {
331
- req.headers["x-connection-id"] = "dev-local";
332
- connectionIdHeader = "dev-local";
333
- }
334
- }
335
- }
336
- if (authHeader?.startsWith("Bearer ") && process.env.NEOTOMA_BEARER_TOKEN) {
337
- const token = authHeader.slice(7).trim();
338
- if (token === process.env.NEOTOMA_BEARER_TOKEN) {
339
- req.headers["x-connection-id"] = "dev-local";
340
- connectionIdHeader = "dev-local";
341
- }
342
- }
343
- }
344
-
345
- const hasAuth = !!(authHeader?.startsWith("Bearer ") || connectionIdHeader);
346
-
347
- // When encryption is on, only the key-derived token is accepted
348
- if (config.encryption.enabled && connectionIdHeader !== "dev-local") {
349
- const wwwAuthHeader = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource"`;
350
- res.setHeader("WWW-Authenticate", wwwAuthHeader);
351
- const msg =
352
- "Encryption is enabled. Use MCP token from your private key: run 'neotoma auth mcp-token' and add Authorization: Bearer <token> to mcp.json.";
353
- if (req.method === "POST" && req.body && typeof req.body === "object") {
354
- return res.status(401).json({
355
- jsonrpc: "2.0",
356
- error: { code: -32001, message: msg },
357
- id: req.body?.id ?? null,
358
- });
359
- }
360
- return res.status(401).json({ error: "Unauthorized", error_description: msg });
361
- }
362
-
363
- // Return 401 if: no auth (regardless of session existence)
364
- // This matches oauth-repro's app.all("/mcp*", ...) pattern
365
- // After OAuth, Cursor will send Bearer token or connection_id, so we allow through
366
- // Fix: Changed from (!sessionId && !hasAuth) to (!hasAuth) to ensure 401 on first protected call
367
- // even if Cursor establishes a session first (via GET for SSE)
368
- if (!hasAuth) {
369
- const wwwAuthHeader = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource"`;
370
- res.setHeader("WWW-Authenticate", wwwAuthHeader);
371
- const unauthMessage =
372
- config.requireKeyForOauth
373
- ? "Unauthorized: Authentication required (key-authenticated OAuth or Bearer token)."
374
- : "Unauthorized: Authentication required";
375
-
376
- // For POST requests with JSON-RPC body, return JSON-RPC error format
377
- if (req.method === "POST" && req.body && typeof req.body === "object") {
378
- return res.status(401).json({
379
- jsonrpc: "2.0",
380
- error: {
381
- code: -32001,
382
- message: unauthMessage,
383
- },
384
- id: req.body?.id ?? null,
385
- });
386
- }
387
- // For GET/DELETE requests, return simple 401
388
- return res.status(401).json({
389
- error: unauthMessage,
390
- });
391
- }
392
-
393
- // Validate X-Connection-Id when that is the auth method (no Bearer). Invalid IDs return 401
394
- // so Cursor shows Connect button instead of blocking on "Loading tools".
395
- // Skip validation for dev-local and dev-local-http (no-auth defaults).
396
- if (
397
- connectionIdHeader &&
398
- connectionIdHeader !== "dev-local" &&
399
- connectionIdHeader !== "dev-local-http" &&
400
- !authHeader?.startsWith("Bearer ")
401
- ) {
402
- try {
403
- const { getAccessTokenForConnection } = await import("./services/mcp_oauth.js");
404
- await getAccessTokenForConnection(connectionIdHeader as string);
405
- } catch {
406
- logger.info(
407
- `[MCP HTTP] Invalid or expired X-Connection-Id: ${connectionIdHeader}. Returning 401 to show Connect button.`
408
- );
409
- // RFC 6750: error=invalid_token signals client to clear credentials and re-authenticate.
410
- // Use consistent error format so Cursor may show Connect prompt.
411
- const desc =
412
- "Connection invalid or expired. Remove X-Connection-Id from mcp.json and click Connect to re-authenticate.";
413
- const wwwAuthHeader = `Bearer resource_metadata="${config.apiBase}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${desc}"`;
414
- res.setHeader("WWW-Authenticate", wwwAuthHeader);
415
- if (req.method === "POST" && req.body && typeof req.body === "object") {
416
- return res.status(401).json({
417
- jsonrpc: "2.0",
418
- error: {
419
- code: -32001,
420
- message: desc,
421
- },
422
- id: req.body?.id ?? null,
423
- });
424
- }
425
- return res.status(401).json({
426
- error: "invalid_token",
427
- error_description: desc,
428
- });
429
- }
430
- }
431
-
432
- // Get or create transport
433
- let transport = sessionId ? mcpTransports.get(sessionId) : undefined;
434
-
435
- if (!transport && req.method === "POST" && isInitializeRequest(req.body)) {
436
- // Create new server instance for each session to ensure clean auth state
437
- // This ensures OAuth flow is required for each new connection
438
- const serverInstance = new NeotomaServer();
439
- const connectionIdFromReq = (req.headers["x-connection-id"] ||
440
- req.headers["X-Connection-Id"]) as string | undefined;
441
- if (connectionIdFromReq) {
442
- serverInstance.setSessionConnectionId(connectionIdFromReq);
443
- }
444
-
445
- // Create new transport for initialization
446
- transport = new StreamableHTTPServerTransport({
447
- sessionIdGenerator: () => randomUUID(),
448
- onsessioninitialized: (sid) => {
449
- if (transport) {
450
- mcpTransports.set(sid, transport);
451
- // Store server instance by session ID to preserve authentication state
452
- mcpServerInstances.set(sid, serverInstance);
453
- logger.info(`[MCP HTTP] Session initialized: ${sid}, server instance stored`);
454
- }
455
- },
456
- });
457
-
458
- transport.onclose = () => {
459
- if (transport?.sessionId) {
460
- mcpTransports.delete(transport.sessionId);
461
- mcpServerInstances.delete(transport.sessionId);
462
- logger.error(`[MCP HTTP] Session closed: ${transport.sessionId}`);
463
- }
464
- };
465
-
466
- // Connect transport to server
467
- await serverInstance.runHTTP(transport);
468
- } else if (!transport) {
469
- return res.status(400).json({
470
- jsonrpc: "2.0",
471
- error: { code: -32000, message: "Bad Request: No valid session ID provided" },
472
- id: null,
473
- });
474
- }
475
-
476
- // Handle request with the transport
477
- await transport.handleRequest(req, res, req.body);
478
- } catch (error: any) {
479
- logger.error("[MCP HTTP] Request error:", error);
480
- if (!res.headersSent) {
481
- res.status(500).json({
482
- jsonrpc: "2.0",
483
- error: { code: -32603, message: "Internal server error" },
484
- id: null,
485
- });
486
- }
487
- }
488
- });
489
-
490
- // Basic redaction helpers for safer debug logs
491
- const SENSITIVE_FIELDS = new Set([
492
- "token",
493
- "access_token",
494
- "accessToken",
495
- "public_token",
496
- "publicToken",
497
- "bearer_token",
498
- "bearerToken",
499
- "password",
500
- "secret",
501
- "api_key",
502
- "apiKey",
503
- "client_secret",
504
- "clientSecret",
505
- "authorization",
506
- "Authorization",
507
- ]);
508
-
509
- function redactHeaders(headers: Record<string, unknown>): Record<string, unknown> {
510
- const clone = { ...headers } as Record<string, unknown>;
511
- if (clone.authorization) clone.authorization = "[REDACTED]";
512
- if (clone.Authorization) clone.Authorization = "[REDACTED]";
513
- return clone;
514
- }
515
-
516
- function redactSensitiveFields(obj: unknown): unknown {
517
- if (!obj || typeof obj !== "object") {
518
- return obj;
519
- }
520
-
521
- if (Array.isArray(obj)) {
522
- return obj.map(redactSensitiveFields);
523
- }
524
-
525
- const redacted = { ...(obj as Record<string, unknown>) };
526
- for (const key in redacted) {
527
- const lowerKey = key.toLowerCase();
528
- if (SENSITIVE_FIELDS.has(key) || SENSITIVE_FIELDS.has(lowerKey)) {
529
- redacted[key] = "[REDACTED]";
530
- } else if (typeof redacted[key] === "object" && redacted[key] !== null) {
531
- redacted[key] = redactSensitiveFields(redacted[key]);
532
- }
533
- }
534
- return redacted;
535
- }
536
-
537
- function logDebug(event: string, req: express.Request, extra?: Record<string, unknown>): void {
538
- const safe = {
539
- method: req.method,
540
- path: req.path,
541
- query: redactSensitiveFields(req.query),
542
- headers: redactHeaders(req.headers as Record<string, unknown>),
543
- body: redactSensitiveFields(req.body),
544
- ...(extra ? (redactSensitiveFields(extra) as Record<string, unknown>) : {}),
545
- };
546
- // eslint-disable-next-line no-console
547
- console.debug(`[DEBUG] ${event}`, safe);
548
- }
549
-
550
- function logWarn(event: string, req: express.Request, extra?: Record<string, unknown>): void {
551
- const safe = {
552
- method: req.method,
553
- path: req.path,
554
- query: redactSensitiveFields(req.query),
555
- headers: redactHeaders(req.headers as Record<string, unknown>),
556
- body: redactSensitiveFields(req.body),
557
- ...(extra ? (redactSensitiveFields(extra) as Record<string, unknown>) : {}),
558
- };
559
- // eslint-disable-next-line no-console
560
- console.warn(`[WARN] ${event}`, safe);
561
- }
562
-
563
- function logError(
564
- event: string,
565
- req: express.Request,
566
- error: unknown,
567
- extra?: Record<string, unknown>
568
- ): void {
569
- const payload = {
570
- method: req.method,
571
- path: req.path,
572
- query: redactSensitiveFields(req.query),
573
- headers: redactHeaders(req.headers as Record<string, unknown>),
574
- body: redactSensitiveFields(req.body),
575
- error:
576
- error instanceof Error
577
- ? { name: error.name, message: error.message, stack: error.stack }
578
- : redactSensitiveFields(error),
579
- ...(extra ? (redactSensitiveFields(extra) as Record<string, unknown>) : {}),
580
- };
581
- // eslint-disable-next-line no-console
582
- console.error(`[ERROR] ${event}`, payload);
583
- }
584
-
585
- function buildErrorEnvelope(
586
- errorCode: string,
587
- message: string,
588
- details?: Record<string, unknown>,
589
- traceId?: string
590
- ): ErrorEnvelope {
591
- return {
592
- error_code: errorCode,
593
- message,
594
- details,
595
- trace_id: traceId,
596
- timestamp: new Date().toISOString(),
597
- };
598
- }
599
-
600
- function sendError(
601
- res: express.Response,
602
- status: number,
603
- errorCode: string,
604
- message: string,
605
- details?: Record<string, unknown>
606
- ): express.Response {
607
- return res.status(status).json(buildErrorEnvelope(errorCode, message, details));
608
- }
609
-
610
- function sendValidationError(res: express.Response, issues: unknown): express.Response {
611
- return res.status(400).json(
612
- buildErrorEnvelope("VALIDATION_INVALID_FORMAT", "Invalid request payload.", {
613
- issues,
614
- })
615
- );
616
- }
617
-
618
- // Public health endpoint (no auth)
619
- app.get("/health", (_req, res) => {
620
- return res.json({ ok: true });
621
- });
622
-
623
- // ============================================================================
624
- // MCP OAuth Endpoints
625
- // ============================================================================
626
-
627
- // Initiate MCP OAuth flow
628
- app.post("/mcp/oauth/initiate", oauthInitiateLimit, async (req, res) => {
629
- try {
630
- const { connection_id, client_name, redirect_uri } = req.body;
631
-
632
- if (!connection_id || typeof connection_id !== "string") {
633
- return sendError(res, 400, "VALIDATION_MISSING_FIELD", "connection_id is required");
634
- }
635
-
636
- const { initiateOAuthFlow } = await import("./services/mcp_oauth.js");
637
- const frontendBase =
638
- process.env.NEOTOMA_FRONTEND_URL ||
639
- process.env.FRONTEND_URL ||
640
- "http://localhost:5195";
641
- const finalRedirectUri =
642
- typeof redirect_uri === "string" && redirect_uri.length > 0
643
- ? redirect_uri
644
- : config.storageBackend === "local"
645
- ? `${frontendBase}/oauth`
646
- : `${config.apiBase}/mcp/oauth/callback`;
647
- const result = await initiateOAuthFlow(connection_id, client_name, finalRedirectUri);
648
-
649
- return res.json(result);
650
- } catch (error: any) {
651
- logError("MCPOAuthInitiate", req, error);
652
-
653
- // Check if it's a structured OAuthError
654
- if (error instanceof OAuthError) {
655
- const oauthError = error as {
656
- code: string;
657
- message: string;
658
- statusCode: number;
659
- retryable?: boolean;
660
- details?: Record<string, any>;
661
- };
662
-
663
- return res.status(oauthError.statusCode).json({
664
- error_code: oauthError.code,
665
- error: oauthError.message,
666
- message: oauthError.message,
667
- retryable: oauthError.retryable || false,
668
- details: oauthError.details,
669
- timestamp: new Date().toISOString(),
670
- ...(oauthError.code === "OAUTH_CLIENT_REGISTRATION_FAILED" && {
671
- hint: "Enable 'Allow Dynamic OAuth Apps' in auth server, or set NEOTOMA_OAUTH_CLIENT_ID in .env file",
672
- }),
673
- });
674
- }
675
-
676
- // Fallback for non-OAuth errors
677
- const isConfigError =
678
- error.message?.includes("client_id not configured") ||
679
- error.message?.includes("OAUTH_CLIENT_ID");
680
- const statusCode = isConfigError ? 400 : 500;
681
-
682
- return res.status(statusCode).json({
683
- error_code: isConfigError ? "OAUTH_CLIENT_REGISTRATION_FAILED" : "INTERNAL_ERROR",
684
- error: error.message,
685
- message: error.message,
686
- timestamp: new Date().toISOString(),
687
- ...(isConfigError && {
688
- hint: "Enable 'Allow Dynamic OAuth Apps' in auth server, or set NEOTOMA_OAUTH_CLIENT_ID in .env file",
689
- }),
690
- });
691
- }
692
- });
693
-
694
- // OAuth callback endpoint
695
- app.get("/mcp/oauth/callback", oauthCallbackLimit, async (req, res) => {
696
- try {
697
- if (config.storageBackend === "local") {
698
- return res
699
- .status(400)
700
- .send("Local OAuth callback is disabled. Use /mcp/oauth/local-login.");
701
- }
702
-
703
- const { code, state } = req.query;
704
-
705
- if (!code || !state || typeof code !== "string" || typeof state !== "string") {
706
- return res.status(400).send("Missing code or state parameter");
707
- }
708
-
709
- const { handleOAuthCallback } = await import("./services/mcp_oauth.js");
710
- const { connectionId, redirectUri, clientState } = await handleOAuthCallback(code, state);
711
-
712
- // If client provided a redirect_uri (Cursor, CLI, or other), send code+state there
713
- const isLocalCallback =
714
- redirectUri &&
715
- (redirectUri.startsWith("cursor://") ||
716
- redirectUri.startsWith("http://127.0.0.1") ||
717
- redirectUri.startsWith("http://localhost"));
718
- if (isLocalCallback) {
719
- const params = new URLSearchParams({ code: connectionId, state: clientState ?? state });
720
- const redirectUrl = `${redirectUri}?${params.toString()}`;
721
- return res.redirect(redirectUrl);
722
- }
723
-
724
- // Default: redirect to frontend success page
725
- const frontendBase =
726
- process.env.NEOTOMA_FRONTEND_URL ||
727
- process.env.FRONTEND_URL ||
728
- "http://localhost:5195";
729
- const successUrl = `${frontendBase}/oauth?connection_id=${encodeURIComponent(connectionId)}&status=success`;
730
- return res.redirect(successUrl);
731
- } catch (error: any) {
732
- logError("MCPOAuthCallback", req, error);
733
-
734
- // Extract structured error information
735
- let errorCode = "OAUTH_TOKEN_EXCHANGE_FAILED";
736
- let errorMessage = error.message || "OAuth callback failed";
737
- let errorDetails: string | undefined;
738
-
739
- if (error instanceof OAuthError) {
740
- errorCode = error.code;
741
- errorMessage = error.message;
742
- if (error.details) {
743
- errorDetails = JSON.stringify(error.details);
744
- }
745
- }
746
-
747
- // Build error URL with structured error information
748
- const params = new URLSearchParams({
749
- status: "error",
750
- error_code: errorCode,
751
- error: errorMessage,
752
- });
753
- if (errorDetails) {
754
- params.set("error_details", errorDetails);
755
- }
756
-
757
- const frontendBaseForError =
758
- process.env.NEOTOMA_FRONTEND_URL ||
759
- process.env.FRONTEND_URL ||
760
- "http://localhost:5195";
761
- const errorUrl = `${frontendBaseForError}/oauth?${params.toString()}`;
762
- return res.redirect(errorUrl);
763
- }
764
- });
765
-
766
- // RFC 8414 authorization endpoint (GET) for Cursor and other OAuth clients
767
- app.get("/mcp/oauth/key-auth", async (req, res) => {
768
- if (!config.requireKeyForOauth) {
769
- const nextPath = normalizeOauthNextPath((req.query.next as string | undefined) || undefined);
770
- return res.redirect(nextPath);
771
- }
772
-
773
- const nextPath = normalizeOauthNextPath((req.query.next as string | undefined) || undefined);
774
- if (hasValidOAuthKeySession(req)) {
775
- return res.redirect(nextPath);
776
- }
777
-
778
- res.setHeader("Content-Type", "text/html; charset=utf-8");
779
- return res.send(`<!DOCTYPE html>
780
- <html>
781
- <head>
782
- <meta charset="utf-8" />
783
- <title>Neotoma key authentication</title>
784
- </head>
785
- <body>
786
- <h1>Authenticate with your key</h1>
787
- <p>OAuth requires key authentication before continuing. Provide your private key hex or mnemonic.</p>
788
- <form method="post" action="/mcp/oauth/key-auth">
789
- <input type="hidden" name="next" value="${nextPath}" />
790
- <div>
791
- <label for="private_key_hex">Private key hex (optional)</label><br />
792
- <input id="private_key_hex" name="private_key_hex" type="password" autocomplete="off" />
793
- </div>
794
- <div style="margin-top: 12px;">
795
- <label for="mnemonic">Mnemonic (optional)</label><br />
796
- <textarea id="mnemonic" name="mnemonic" rows="3" cols="80"></textarea>
797
- </div>
798
- <div style="margin-top: 12px;">
799
- <label for="mnemonic_passphrase">Mnemonic passphrase (optional)</label><br />
800
- <input id="mnemonic_passphrase" name="mnemonic_passphrase" type="password" autocomplete="off" />
801
- </div>
802
- <div style="margin-top: 14px;">
803
- <button type="submit">Authenticate and continue</button>
804
- </div>
805
- </form>
806
- <p style="margin-top: 16px;">
807
- If you do not have key credentials configured for this server, OAuth is unavailable. Use
808
- <code>NEOTOMA_BEARER_TOKEN</code> and configure MCP with <code>Authorization: Bearer &lt;token&gt;</code>.
809
- </p>
810
- </body>
811
- </html>`);
812
- });
813
-
814
- app.post("/mcp/oauth/key-auth", express.urlencoded({ extended: true }), async (req, res) => {
815
- const nextPath = normalizeOauthNextPath((req.body?.next as string | undefined) || undefined);
816
-
817
- if (!config.requireKeyForOauth) {
818
- return res.redirect(nextPath);
819
- }
820
-
821
- const result = isOauthKeyCredentialValid({
822
- privateKeyHex: req.body?.private_key_hex,
823
- mnemonic: req.body?.mnemonic,
824
- mnemonicPassphrase: req.body?.mnemonic_passphrase,
825
- });
826
-
827
- if (!result.ok) {
828
- res.setHeader("Content-Type", "text/html; charset=utf-8");
829
- return res.status(401).send(`<!DOCTYPE html>
830
- <html>
831
- <head>
832
- <meta charset="utf-8" />
833
- <title>Key authentication failed</title>
834
- </head>
835
- <body>
836
- <h1>Key authentication failed</h1>
837
- <p>${result.reason || "Unable to validate key credentials."}</p>
838
- <p><a href="/mcp/oauth/key-auth?next=${encodeURIComponent(nextPath)}">Try again</a></p>
839
- </body>
840
- </html>`);
841
- }
842
-
843
- setOAuthKeySessionCookie(req, res);
844
- return res.redirect(nextPath);
845
- });
846
-
847
- // RFC 8414 authorization endpoint (GET) for Cursor and other OAuth clients
848
- app.get("/mcp/oauth/authorize", async (req, res) => {
849
- try {
850
- const redirect_uri = req.query.redirect_uri as string | undefined;
851
- const state = req.query.state as string | undefined;
852
- const code_challenge = req.query.code_challenge as string | undefined;
853
- const code_challenge_method = req.query.code_challenge_method as string | undefined;
854
- const client_id = req.query.client_id as string | undefined;
855
- const dev_stub = req.query.dev_stub as string | undefined;
856
-
857
- if (!redirect_uri) {
858
- return res.status(400).send("redirect_uri is required");
859
- }
860
- if (!state) {
861
- return res.status(400).send("state is required");
862
- }
863
- if (!code_challenge || code_challenge_method !== "S256") {
864
- return res.status(400).send("code_challenge and code_challenge_method=S256 are required");
865
- }
866
- if (config.requireKeyForOauth && !hasValidOAuthKeySession(req)) {
867
- const nextPath = normalizeOauthNextPath(req.originalUrl);
868
- return res.redirect(`/mcp/oauth/key-auth?next=${encodeURIComponent(nextPath)}`);
869
- }
870
- if (dev_stub === "1" || dev_stub === "true") {
871
- return res
872
- .status(400)
873
- .send("dev_stub is disabled. OAuth requires key authentication via /mcp/oauth/key-auth.");
874
- }
875
-
876
- if (config.storageBackend === "local") {
877
- // When reached via tunnel, only allow redirect_uri to localhost or known app schemes
878
- if (!isLocalRequest(req)) {
879
- const { isRedirectUriAllowedForTunnel } = await import("./services/mcp_oauth.js");
880
- if (!isRedirectUriAllowedForTunnel(redirect_uri)) {
881
- return res.status(400).send(
882
- "redirect_uri is not allowed when connecting via a tunnel. Use cursor://, http://localhost, or http://127.0.0.1."
883
- );
884
- }
885
- }
886
-
887
- const { randomUUID } = await import("node:crypto");
888
- const connectionId = randomUUID();
889
- const { createLocalAuthorizationRequest } = await import("./services/mcp_oauth.js");
890
-
891
- const authRequest = await createLocalAuthorizationRequest({
892
- connectionId,
893
- redirectUri: redirect_uri,
894
- clientState: state,
895
- codeChallenge: code_challenge,
896
- });
897
- // Keep local OAuth redirects on the current origin (tunnel or localhost) even if
898
- // authRequest.authUrl was built from a different absolute base URL.
899
- try {
900
- const parsed = new URL(authRequest.authUrl);
901
- return res.redirect(`${parsed.pathname}${parsed.search}`);
902
- } catch {
903
- return res.redirect(authRequest.authUrl);
904
- }
905
- }
906
-
907
- const { randomUUID } = await import("node:crypto");
908
- const connectionId = randomUUID();
909
- const { initiateOAuthFlow } = await import("./services/mcp_oauth.js");
910
- const result = await initiateOAuthFlow(
911
- connectionId,
912
- client_id ?? undefined,
913
- redirect_uri,
914
- state
915
- );
916
-
917
- return res.redirect(result.authUrl);
918
- } catch (error: any) {
919
- logError("MCPOAuthAuthorize", req, error);
920
- return res.status(500).send(error.message ?? "Authorization failed");
921
- }
922
- });
923
-
924
- // Local OAuth login page (local backend only)
925
- // With MCP-style auth: encryption off = auto dev-local, encryption on = Bearer token only (no OAuth)
926
- app.get("/mcp/oauth/local-login", async (req, res) => {
927
- if (config.storageBackend !== "local") {
928
- return res.status(404).send("Not found");
929
- }
930
-
931
- const state = (req.query.state as string | undefined)?.trim();
932
- if (!state) {
933
- return res.status(400).send("state is required");
934
- }
935
- if (config.requireKeyForOauth && !hasValidOAuthKeySession(req)) {
936
- const nextPath = normalizeOauthNextPath(req.originalUrl);
937
- return res.redirect(`/mcp/oauth/key-auth?next=${encodeURIComponent(nextPath)}`);
938
- }
939
-
940
- // When encryption is enabled: OAuth is not supported (MCP handler requires key-derived token)
941
- if (config.encryption.enabled) {
942
- res.setHeader("Content-Type", "text/html; charset=utf-8");
943
- return res.send(`<!DOCTYPE html>
944
- <html>
945
- <head>
946
- <meta charset="utf-8" />
947
- <title>OAuth not supported with encryption</title>
948
- </head>
949
- <body>
950
- <h1>OAuth not supported when encryption is enabled</h1>
951
- <p>
952
- When encryption is enabled (NEOTOMA_ENCRYPTION_ENABLED=true), MCP authentication
953
- uses a key-derived Bearer token instead of OAuth.
954
- </p>
955
- <h2>How to configure:</h2>
956
- <ol>
957
- <li>Run <code>neotoma auth mcp-token</code> to get your token</li>
958
- <li>Add to .cursor/mcp.json under neotoma server:
959
- <pre>"headers": { "Authorization": "Bearer &lt;your-token&gt;" }</pre>
960
- </li>
961
- <li>Remove <code>X-Connection-Id</code> if present</li>
962
- <li>Restart Cursor</li>
963
- </ol>
964
- <p>See <a href="https://github.com/neotoma/neotoma/blob/main/docs/developer/mcp_oauth_troubleshooting.md">MCP OAuth Troubleshooting</a> for details.</p>
965
- </body>
966
- </html>`);
967
- }
968
-
969
- // When encryption is off: local requests auto-approve; tunnel requests require explicit approval
970
- const fromTunnel = !isLocalRequest(req);
971
- if (fromTunnel && req.query.approve !== "1") {
972
- res.setHeader("Content-Type", "text/html; charset=utf-8");
973
- const base = `${req.protocol}://${req.get("host") || ""}`;
974
- const approveUrl = `${base}${req.path}?${new URLSearchParams({ state, approve: "1" }).toString()}`;
975
- return res.send(`<!DOCTYPE html>
976
- <html>
977
- <head>
978
- <meta charset="utf-8" />
979
- <title>Approve MCP connection</title>
980
- </head>
981
- <body>
982
- <h1>Approve MCP connection</h1>
983
- <p>A client is requesting access to Neotoma. Only approve if you started this connection (e.g. by clicking Connect in Cursor).</p>
984
- <p><a href="${approveUrl}">Approve this connection</a></p>
985
- <p><small>If you did not expect this, close the tab. No access will be granted.</small></p>
986
- </body>
987
- </html>`);
988
- }
989
-
990
- try {
991
- const devUser = ensureLocalDevUser();
992
- const { completeLocalAuthorization } = await import("./services/mcp_oauth.js");
993
- const { connectionId, redirectUri, clientState } = await completeLocalAuthorization(
994
- state,
995
- devUser.id
996
- );
997
- const frontendBase =
998
- process.env.NEOTOMA_FRONTEND_URL ||
999
- process.env.FRONTEND_URL ||
1000
- "http://localhost:5195";
1001
- const frontendOauth = `${frontendBase}/oauth`;
1002
- if (redirectUri) {
1003
- if (!clientState && redirectUri.startsWith(frontendOauth)) {
1004
- const successUrl = `${frontendOauth}?connection_id=${encodeURIComponent(connectionId)}&status=success`;
1005
- return res.redirect(successUrl);
1006
- }
1007
- const params = new URLSearchParams({
1008
- code: connectionId,
1009
- state: clientState ?? "",
1010
- });
1011
- return res.redirect(`${redirectUri}?${params.toString()}`);
1012
- }
1013
- return res.redirect(
1014
- `${frontendOauth}?connection_id=${encodeURIComponent(connectionId)}&status=success`
1015
- );
1016
- } catch (error: any) {
1017
- logError("MCPLocalLoginDevStub", req, error);
1018
- const status = error?.code === "OAUTH_STATE_INVALID" || error?.statusCode === 400 ? 400 : 401;
1019
- return res.status(status).send(error.message ?? "Dev account authorization failed");
1020
- }
1021
- });
1022
-
1023
- // POST handler removed: local-login now auto-uses dev account (no email/password form)
1024
-
1025
- // RFC 8414 token endpoint (POST) for Cursor and other OAuth clients
1026
- app.post(
1027
- "/mcp/oauth/token",
1028
- oauthTokenLimit,
1029
- express.urlencoded({ extended: true }),
1030
- async (req, res) => {
1031
- try {
1032
- const grant_type = req.body?.grant_type;
1033
- const code = req.body?.code;
1034
-
1035
- if (grant_type !== "authorization_code") {
1036
- return res.status(400).json({
1037
- error: "unsupported_grant_type",
1038
- error_description: "Only authorization_code is supported",
1039
- });
1040
- }
1041
- if (!code || typeof code !== "string") {
1042
- return res
1043
- .status(400)
1044
- .json({ error: "invalid_request", error_description: "code is required" });
1045
- }
1046
-
1047
- const { getTokenResponseForConnection } = await import("./services/mcp_oauth.js");
1048
- const token = await getTokenResponseForConnection(code);
1049
-
1050
- res.setHeader("Content-Type", "application/json");
1051
- return res.json(token);
1052
- } catch (error: any) {
1053
- logError("MCPOAuthToken", req, error);
1054
- return res.status(400).json({
1055
- error: "invalid_grant",
1056
- error_description: error.message ?? "Token exchange failed",
1057
- });
1058
- }
1059
- }
1060
- );
1061
-
1062
- // RFC 7591 Dynamic Client Registration (for Cursor and other MCP clients)
1063
- app.post("/mcp/oauth/register", oauthRegisterLimit, async (req, res) => {
1064
- try {
1065
- const body = req.body as { redirect_uris?: string[]; client_name?: string; scope?: string };
1066
- if (!body || typeof body !== "object") {
1067
- return res.status(400).json({
1068
- error: "invalid_request",
1069
- error_description: "Request body must be JSON object with redirect_uris",
1070
- });
1071
- }
1072
- const { handleDynamicRegistration } = await import("./services/mcp_oauth.js");
1073
- const reg = await handleDynamicRegistration({
1074
- redirect_uris: body.redirect_uris ?? [],
1075
- client_name: body.client_name,
1076
- scope: body.scope,
1077
- });
1078
- res.setHeader("Content-Type", "application/json");
1079
- return res.status(201).json(reg);
1080
- } catch (error: any) {
1081
- logError("MCPOAuthRegister", req, error);
1082
- if (error instanceof OAuthError) {
1083
- const oauth = error as { code?: string; message?: string; statusCode?: number };
1084
- const status = oauth.statusCode ?? 400;
1085
- const code =
1086
- oauth.code === "OAUTH_INVALID_REDIRECT_URI"
1087
- ? "invalid_redirect_uri"
1088
- : status >= 500
1089
- ? "server_error"
1090
- : "invalid_client_metadata";
1091
- return res.status(status).json({
1092
- error: code,
1093
- error_description: oauth.message ?? "Registration failed",
1094
- });
1095
- }
1096
- return res.status(400).json({
1097
- error: "invalid_client_metadata",
1098
- error_description: error.message ?? "Dynamic client registration failed",
1099
- });
1100
- }
1101
- });
1102
-
1103
- // Get connection status
1104
- app.get("/mcp/oauth/status", async (req, res) => {
1105
- try {
1106
- const { connection_id } = req.query;
1107
-
1108
- if (!connection_id || typeof connection_id !== "string") {
1109
- return sendError(res, 400, "VALIDATION_MISSING_FIELD", "connection_id is required");
1110
- }
1111
-
1112
- const { getConnectionStatus } = await import("./services/mcp_oauth.js");
1113
- const status = await getConnectionStatus(connection_id);
1114
-
1115
- return res.json({ status, connection_id });
1116
- } catch (error: any) {
1117
- logError("MCPOAuthStatus", req, error);
1118
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
1119
- }
1120
- });
1121
-
1122
- // List user's MCP connections (authenticated)
1123
- app.get("/mcp/oauth/connections", async (req, res) => {
1124
- try {
1125
- // Extract bearer token
1126
- const authHeader = req.headers.authorization;
1127
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
1128
- return sendError(res, 401, "AUTH_REQUIRED", "Authorization header required");
1129
- }
1130
-
1131
- const token = authHeader.substring(7);
1132
-
1133
- // Validate token and get user
1134
- const { validateSessionToken } = await import("./services/mcp_auth.js");
1135
- const { userId } = await validateSessionToken(token);
1136
-
1137
- // List connections
1138
- const { listConnections } = await import("./services/mcp_oauth.js");
1139
- const connections = await listConnections(userId);
1140
-
1141
- return res.json({ connections });
1142
- } catch (error: any) {
1143
- logError("MCPOAuthListConnections", req, error);
1144
- return sendError(res, 401, "AUTH_INVALID", "Invalid or expired token");
1145
- }
1146
- });
1147
-
1148
- // Revoke MCP connection (authenticated)
1149
- app.delete("/mcp/oauth/connections/:connection_id", async (req, res) => {
1150
- try {
1151
- // Extract bearer token
1152
- const authHeader = req.headers.authorization;
1153
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
1154
- return sendError(res, 401, "AUTH_REQUIRED", "Authorization header required");
1155
- }
1156
-
1157
- const token = authHeader.substring(7);
1158
- const { connection_id } = req.params;
1159
-
1160
- // Validate token and get user
1161
- const { validateSessionToken } = await import("./services/mcp_auth.js");
1162
- const { userId } = await validateSessionToken(token);
1163
-
1164
- // Revoke connection
1165
- const { revokeConnection } = await import("./services/mcp_oauth.js");
1166
- await revokeConnection(connection_id, userId);
1167
-
1168
- return res.json({ success: true });
1169
- } catch (error: any) {
1170
- logError("MCPOAuthRevokeConnection", req, error);
1171
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
1172
- }
1173
- });
1174
-
1175
- // Get OAuth authorization details (proxy to avoid CORS)
1176
- // This endpoint proxies requests to OAuth 2.1 Server to avoid CORS issues
1177
- app.get("/mcp/oauth/authorization-details", async (req, res) => {
1178
- logger.info(
1179
- `[MCP OAuth] Authorization details request: authorization_id=${req.query.authorization_id}`
1180
- );
1181
- try {
1182
- // Extract bearer token
1183
- const authHeader = req.headers.authorization;
1184
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
1185
- return sendError(res, 401, "AUTH_REQUIRED", "Authorization header required");
1186
- }
1187
-
1188
- const token = authHeader.substring(7);
1189
- const { authorization_id } = req.query;
1190
-
1191
- if (!authorization_id || typeof authorization_id !== "string") {
1192
- return sendError(res, 400, "VALIDATION_MISSING_FIELD", "authorization_id is required");
1193
- }
1194
-
1195
- // Validate token and get user (ensures user is authenticated)
1196
- const { validateSessionToken } = await import("./services/mcp_auth.js");
1197
- await validateSessionToken(token);
1198
-
1199
- const db = getSqliteDb();
1200
- const stateData = db
1201
- .prepare("SELECT redirect_uri FROM mcp_oauth_state WHERE state = ?")
1202
- .get(authorization_id) as { redirect_uri?: string } | undefined;
1203
-
1204
- if (!stateData?.redirect_uri) {
1205
- return res.status(404).json({
1206
- error: "Authorization not found",
1207
- error_description:
1208
- "The authorization_id has expired or is invalid. Please try the OAuth flow again.",
1209
- });
1210
- }
1211
-
1212
- return res.json({
1213
- id: authorization_id,
1214
- status: "approved",
1215
- client_id: "local_mcp_oauth_client",
1216
- redirect_uri: stateData.redirect_uri,
1217
- });
1218
- } catch (error: any) {
1219
- logError("MCPOAuthAuthorizationDetails", req, error);
1220
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
1221
- }
1222
- });
1223
-
1224
- // Dev-only endpoint to sign in as a specific user (for development/testing)
1225
- app.post("/auth/dev-signin", async (req, res) => {
1226
- // Only allow in development mode
1227
- if (config.environment !== "development") {
1228
- return sendError(res, 403, "FORBIDDEN", "Dev signin only available in development mode");
1229
- }
1230
- return sendError(res, 403, "FORBIDDEN", "Dev signin endpoint is disabled in local-only mode");
1231
- });
1232
-
1233
- // Public key-based authentication middleware
1234
- // MCP-style auth (same patterns as /mcp): encryption off = no auth or dev token; encryption on = key-derived token
1235
- app.use(async (req, res, next) => {
1236
- // Bypass auth only for truly public endpoints (no user data)
1237
- if (
1238
- req.method === "OPTIONS" ||
1239
- (req.method === "GET" && (req.path === "/openapi.yaml" || req.path === "/health")) ||
1240
- (req.method === "POST" && req.path === "/auth/dev-signin")
1241
- ) {
1242
- return next();
1243
- }
1244
-
1245
- const headerAuth = (req.headers.authorization || req.headers.Authorization || "") as string;
1246
-
1247
- // Local mode: when storage is local and request is from localhost, no Bearer → default user (00..) by default
1248
- if (
1249
- config.storageBackend === "local" &&
1250
- isLocalRequest(req) &&
1251
- !headerAuth.startsWith("Bearer ")
1252
- ) {
1253
- const devUser = ensureLocalDevUser();
1254
- (req as any).authenticatedUserId = devUser.id;
1255
- return next();
1256
- }
1257
-
1258
- // MCP-style auth (aligns CLI and REST API with MCP). Local requests can skip Bearer; tunnel requires Bearer or OAuth.
1259
- if (config.encryption.enabled) {
1260
- const expectedToken = getMcpAuthToken();
1261
- if (headerAuth.startsWith("Bearer ") && expectedToken) {
1262
- const token = headerAuth.slice(7).trim();
1263
- if (token === expectedToken) {
1264
- const devUser = ensureLocalDevUser();
1265
- (req as any).authenticatedUserId = devUser.id;
1266
- return next();
1267
- }
1268
- }
1269
- } else {
1270
- if (!headerAuth.startsWith("Bearer ")) {
1271
- if (isLocalRequest(req)) {
1272
- const devUser = ensureLocalDevUser();
1273
- (req as any).authenticatedUserId = devUser.id;
1274
- return next();
1275
- }
1276
- }
1277
- if (process.env.NEOTOMA_BEARER_TOKEN) {
1278
- const token = headerAuth.slice(7).trim();
1279
- if (token === process.env.NEOTOMA_BEARER_TOKEN) {
1280
- const devUser = ensureLocalDevUser();
1281
- (req as any).authenticatedUserId = devUser.id;
1282
- return next();
1283
- }
1284
- }
1285
- }
1286
-
1287
- // Bearer required for remaining paths (Ed25519, OAuth)
1288
- if (!headerAuth.startsWith("Bearer ")) {
1289
- logWarn("AuthMissingBearer", req);
1290
- return sendError(res, 401, "AUTH_REQUIRED", "Missing Bearer token");
1291
- }
1292
-
1293
- const bearerToken = headerAuth.slice("Bearer ".length).trim();
1294
-
1295
- // Try to validate as Ed25519 bearer token first
1296
- const registered = ensurePublicKeyRegistered(bearerToken);
1297
-
1298
- if (registered && isBearerTokenValid(bearerToken)) {
1299
- // Optional: Verify signature if provided
1300
- const { signature } = parseAuthHeader(headerAuth);
1301
- if (signature && req.body) {
1302
- const bodyString = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
1303
- const isValid = verifyRequest(bodyString, signature, bearerToken);
1304
- if (!isValid) {
1305
- logWarn("AuthInvalidSignature", req);
1306
- return sendError(res, 403, "AUTH_INVALID", "Invalid request signature");
1307
- }
1308
- }
1309
-
1310
- // Attach public key to request for encryption service
1311
- (req as any).publicKey = getPublicKey(bearerToken);
1312
- (req as any).bearerToken = bearerToken;
1313
- // If this token was registered with a userId, set it so getAuthenticatedUserId works without query param
1314
- const registeredUserId = getUserIdFromBearerToken(bearerToken);
1315
- if (registeredUserId) {
1316
- (req as any).authenticatedUserId = registeredUserId;
1317
- }
1318
- } else {
1319
- // Try to validate as session token
1320
- try {
1321
- const { validateSessionToken } = await import("./services/mcp_auth.js");
1322
- const validated = await validateSessionToken(bearerToken);
1323
- // Attach user_id and email to request for user-scoped queries and /me
1324
- (req as any).authenticatedUserId = validated.userId;
1325
- (req as any).authenticatedUserEmail = validated.email;
1326
- (req as any).bearerToken = bearerToken;
1327
- } catch (authError) {
1328
- // Not a valid token
1329
- logWarn("AuthInvalidToken", req, {
1330
- error: authError instanceof Error ? authError.message : String(authError),
1331
- });
1332
- return sendError(res, 401, "AUTH_INVALID", "Unauthorized - invalid authentication token");
1333
- }
1334
- }
1335
-
1336
- return next();
1337
- });
1338
-
1339
- // Response encryption middleware (applies to all authenticated routes)
1340
- app.use(encryptResponseMiddleware);
1341
-
1342
- // Current session (authenticated user details)
1343
- app.get("/me", async (req, res) => {
1344
- try {
1345
- const userId = (req as any).authenticatedUserId;
1346
- const email = (req as any).authenticatedUserEmail;
1347
- if (!userId) {
1348
- return sendError(res, 401, "AUTH_REQUIRED", "Not authenticated");
1349
- }
1350
- const storage =
1351
- config.storageBackend === "local"
1352
- ? {
1353
- storage_backend: "local" as const,
1354
- data_dir: config.dataDir,
1355
- sqlite_db: config.sqlitePath,
1356
- }
1357
- : undefined;
1358
- return res.json({ user_id: userId, email: email ?? undefined, storage });
1359
- } catch (error: any) {
1360
- logError("GetMe", req, error);
1361
- return sendError(res, 401, "AUTH_REQUIRED", error.message ?? "Not authenticated");
1362
- }
1363
- });
1364
-
1365
- /**
1366
- * Helper to extract authenticated user_id from request
1367
- * Supports: middleware-set user (e.g. dev-local when encryption off), session token, Ed25519 bearer
1368
- * @param req - Express request object
1369
- * @param providedUserId - Optional user_id from request body/query
1370
- * @returns Authenticated user_id
1371
- * @throws Error if not authenticated or user_id mismatch
1372
- */
1373
- async function getAuthenticatedUserId(
1374
- req: express.Request,
1375
- providedUserId?: string
1376
- ): Promise<string> {
1377
- // Use user already set by auth middleware (e.g. dev-local when encryption off and no Bearer)
1378
- const authenticatedUserId = (req as any).authenticatedUserId;
1379
- if (authenticatedUserId) {
1380
- if (providedUserId && providedUserId !== authenticatedUserId) {
1381
- // When authenticated as local dev user, allow body/query user_id override for CLI tests and dev flows.
1382
- if (authenticatedUserId === LOCAL_DEV_USER_ID) {
1383
- return providedUserId;
1384
- }
1385
- throw new Error(
1386
- `user_id parameter (${providedUserId}) does not match authenticated user (${authenticatedUserId})`
1387
- );
1388
- }
1389
- return authenticatedUserId;
1390
- }
1391
-
1392
- const headerAuth = req.headers.authorization || "";
1393
- if (!headerAuth.startsWith("Bearer ")) {
1394
- throw new Error("Not authenticated - missing Bearer token");
1395
- }
1396
-
1397
- // Ed25519 bearer token - user_id must be provided
1398
- if (!providedUserId) {
1399
- throw new Error("user_id required when using Ed25519 bearer token");
1400
- }
1401
-
1402
- // For Ed25519 tokens, we trust the provided user_id (token validation happens in middleware)
1403
- return providedUserId;
1404
- }
1405
-
1406
- /**
1407
- * Helper to handle errors in API endpoints, including authentication errors
1408
- */
1409
- function handleApiError(
1410
- req: express.Request,
1411
- res: express.Response,
1412
- error: unknown,
1413
- defaultMessage: string,
1414
- errorCode: string = "DB_QUERY_FAILED",
1415
- logContext?: string
1416
- ): express.Response {
1417
- if (error instanceof Error && error.message.includes("Not authenticated")) {
1418
- return res.status(401).json(
1419
- buildErrorEnvelope("AUTH_REQUIRED", "Authentication required.", {
1420
- detail: error.message,
1421
- })
1422
- );
1423
- }
1424
- if (error instanceof Error && error.message.includes("user_id parameter")) {
1425
- return res.status(403).json(
1426
- buildErrorEnvelope("FORBIDDEN", "user_id does not match authenticated user.", {
1427
- detail: error.message,
1428
- })
1429
- );
1430
- }
1431
- logError(logContext || "APIError", req, error);
1432
- const message = error instanceof Error ? error.message : defaultMessage;
1433
- return res.status(500).json(buildErrorEnvelope(errorCode, message));
1434
- }
1435
-
1436
- // Schemas
1437
-
1438
- // ============================================================================
1439
- // v0.2.15 Entity-Based HTTP API Endpoints
1440
- // ============================================================================
1441
-
1442
- // POST /api/entities/query - Query entities with filters
1443
- // REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
1444
- app.post("/entities/query", async (req, res) => {
1445
- const parsed = EntitiesQueryRequestSchema.safeParse(req.body);
1446
- if (!parsed.success) {
1447
- logWarn("ValidationError:entities_query", req, {
1448
- issues: parsed.error.issues,
1449
- });
1450
- return sendValidationError(res, parsed.error.issues);
1451
- }
1452
-
1453
- try {
1454
- // Get authenticated user_id (REQUIRED)
1455
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
1456
-
1457
- const { entity_type, search, limit, offset, include_merged } = parsed.data;
1458
- const { entities, total } = await queryEntitiesWithCount({
1459
- userId,
1460
- entityType: entity_type,
1461
- includeMerged: include_merged,
1462
- search,
1463
- limit,
1464
- offset,
1465
- });
1466
-
1467
- return res.json({
1468
- entities,
1469
- total,
1470
- limit,
1471
- offset,
1472
- });
1473
- } catch (error) {
1474
- return handleApiError(
1475
- req,
1476
- res,
1477
- error,
1478
- "Failed to query entities",
1479
- "DB_QUERY_FAILED",
1480
- "APIError:entities_query"
1481
- );
1482
- }
1483
- });
1484
-
1485
- // GET /api/entities/:id - Get entity detail with snapshot and provenance (FU-601)
1486
- // REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
1487
- app.get("/entities/:id", async (req, res) => {
1488
- try {
1489
- const entityId = req.params.id;
1490
-
1491
- // Get authenticated user_id (REQUIRED)
1492
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1493
-
1494
- // Verify entity exists and belongs to authenticated user
1495
- const { data: entity, error: entityError } = await db
1496
- .from("entities")
1497
- .select("id, user_id")
1498
- .eq("id", entityId)
1499
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
1500
- .single();
1501
-
1502
- if (entityError || !entity) {
1503
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
1504
- }
1505
-
1506
- const { getEntityWithProvenance } = await import("./services/entity_queries.js");
1507
- const entityWithProvenance = await getEntityWithProvenance(entityId);
1508
-
1509
- if (!entityWithProvenance) {
1510
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
1511
- }
1512
-
1513
- return res.json(entityWithProvenance);
1514
- } catch (error) {
1515
- if (error instanceof Error && error.message.includes("Not authenticated")) {
1516
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
1517
- }
1518
- logError("APIError:entity_detail", req, error);
1519
- const message = error instanceof Error ? error.message : "Failed to get entity";
1520
- return sendError(res, 500, "DB_QUERY_FAILED", message);
1521
- }
1522
- });
1523
-
1524
- // GET /api/entities/:id/observations - Get observations for entity (FU-601)
1525
- // REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
1526
- app.get("/entities/:id/observations", async (req, res) => {
1527
- try {
1528
- const entityId = req.params.id;
1529
-
1530
- // Get authenticated user_id (REQUIRED)
1531
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1532
-
1533
- // Verify entity exists and belongs to authenticated user
1534
- const { data: entity, error: entityError } = await db
1535
- .from("entities")
1536
- .select("id")
1537
- .eq("id", entityId)
1538
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
1539
- .single();
1540
-
1541
- if (entityError || !entity) {
1542
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
1543
- }
1544
-
1545
- const limit = parseInt(req.query.limit as string) || 100;
1546
- const offset = parseInt(req.query.offset as string) || 0;
1547
-
1548
- // Get observations for this entity - filter by user_id for security
1549
- const { data, error, count } = await db
1550
- .from("observations")
1551
- .select("*", { count: "exact" })
1552
- .eq("entity_id", entityId)
1553
- .eq("user_id", userId) // SECURITY: Only return observations for authenticated user
1554
- .order("observed_at", { ascending: false })
1555
- .range(offset, offset + limit - 1);
1556
-
1557
- if (error) throw error;
1558
-
1559
- return res.json({
1560
- observations: data || [],
1561
- total: count || 0,
1562
- limit,
1563
- offset,
1564
- });
1565
- } catch (error) {
1566
- if (error instanceof Error && error.message.includes("Not authenticated")) {
1567
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
1568
- }
1569
- logError("APIError:entity_observations", req, error);
1570
- const message = error instanceof Error ? error.message : "Failed to get observations";
1571
- return sendError(res, 500, "DB_QUERY_FAILED", message);
1572
- }
1573
- });
1574
-
1575
- // GET /api/entities/:id/relationships - Get relationships for entity (FU-601)
1576
- // REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
1577
- app.get("/entities/:id/relationships", async (req, res) => {
1578
- try {
1579
- const entityId = req.params.id;
1580
-
1581
- // Get authenticated user_id (REQUIRED)
1582
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1583
-
1584
- // Verify entity exists and belongs to authenticated user
1585
- const { data: entity, error: entityError } = await db
1586
- .from("entities")
1587
- .select("id")
1588
- .eq("id", entityId)
1589
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
1590
- .single();
1591
-
1592
- if (entityError || !entity) {
1593
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
1594
- }
1595
-
1596
- // Get relationships where this entity is the source - filter by user_id
1597
- const { data: outgoing, error: outgoingError } = await db
1598
- .from("relationship_snapshots")
1599
- .select("*")
1600
- .eq("source_entity_id", entityId)
1601
- .eq("user_id", userId); // SECURITY: Only return relationships for authenticated user
1602
-
1603
- if (outgoingError) throw outgoingError;
1604
-
1605
- // Get relationships where this entity is the target - filter by user_id
1606
- const { data: incoming, error: incomingError } = await db
1607
- .from("relationship_snapshots")
1608
- .select("*")
1609
- .eq("target_entity_id", entityId)
1610
- .eq("user_id", userId); // SECURITY: Only return relationships for authenticated user
1611
-
1612
- if (incomingError) throw incomingError;
1613
-
1614
- // Add id field for frontend compatibility (use relationship_key)
1615
- const formatRelationships = (rels: any[]) =>
1616
- (rels || []).map((rel: any) => ({
1617
- ...rel,
1618
- id: rel.relationship_key,
1619
- }));
1620
-
1621
- return res.json({
1622
- outgoing: formatRelationships(outgoing),
1623
- incoming: formatRelationships(incoming),
1624
- });
1625
- } catch (error) {
1626
- logError("APIError:entity_relationships", req, error);
1627
- const message = error instanceof Error ? error.message : "Failed to get relationships";
1628
- return sendError(res, 500, "DB_QUERY_FAILED", message);
1629
- }
1630
- });
1631
-
1632
- // GET /api/schemas - List all schemas (FU-XXX)
1633
- // REQUIRES AUTHENTICATION - returns global schemas + user-specific schemas for authenticated user
1634
- app.get("/schemas", async (req, res) => {
1635
- try {
1636
- // Get authenticated user_id (REQUIRED)
1637
- // For Ed25519 tokens, user_id may be in query params; for session tokens, it's extracted from token
1638
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1639
-
1640
- const keyword = req.query.keyword as string | undefined;
1641
- const filterEntityType = req.query.entity_type as string | undefined;
1642
- const limitRaw = req.query.limit as string | undefined;
1643
- const offsetRaw = req.query.offset as string | undefined;
1644
- const limit =
1645
- limitRaw && /^\d+$/.test(limitRaw) ? Math.max(0, parseInt(limitRaw, 10)) : 100;
1646
- const offset =
1647
- offsetRaw && /^\d+$/.test(offsetRaw) ? Math.max(0, parseInt(offsetRaw, 10)) : 0;
1648
-
1649
- const { SchemaRegistryService } = await import("./services/schema_registry.js");
1650
- const schemaRegistry = new SchemaRegistryService();
1651
-
1652
- // Get schemas - listEntityTypes will return global + user-specific schemas
1653
- // The service should filter by user_id, but for now we'll filter in the endpoint
1654
- const allSchemas = await schemaRegistry.listEntityTypes(keyword);
1655
-
1656
- // Filter to only show global schemas (user_id is null) or user-specific schemas for this user
1657
- // Note: listEntityTypes doesn't currently filter by user_id, so we need to query directly
1658
- // Also fetch metadata (including icons) for each schema
1659
- const { data: dbSchemas, error: dbError } = await db
1660
- .from("schema_registry")
1661
- .select("entity_type, metadata")
1662
- .eq("active", true)
1663
- .or(`user_id.is.null,user_id.eq.${userId}`); // SECURITY: Only global or user's schemas
1664
-
1665
- if (dbError) throw dbError;
1666
-
1667
- const allowedEntityTypes = new Set((dbSchemas || []).map((s: any) => s.entity_type));
1668
- let filteredSchemas = allSchemas.filter((s) => allowedEntityTypes.has(s.entity_type));
1669
-
1670
- // Filter by entity_type if provided
1671
- if (filterEntityType) {
1672
- filteredSchemas = filteredSchemas.filter((s) => s.entity_type === filterEntityType);
1673
- }
1674
-
1675
- // Enrich schemas with metadata (including icons)
1676
- const schemaMetadataMap = new Map(
1677
- (dbSchemas || []).map((s: any) => [s.entity_type, s.metadata || {}])
1678
- );
1679
-
1680
- const enrichedSchemas = filteredSchemas.map((schema) => ({
1681
- ...schema,
1682
- metadata: schemaMetadataMap.get(schema.entity_type) || {},
1683
- }));
1684
-
1685
- // Filter out test schemas (marked with metadata.test === true)
1686
- const productionSchemas = enrichedSchemas.filter((schema) => {
1687
- const metadata = (schema.metadata || {}) as Record<string, unknown>;
1688
- return metadata["test"] !== true;
1689
- });
1690
-
1691
- const sortedSchemas = productionSchemas.sort((a, b) =>
1692
- String(a.entity_type).localeCompare(String(b.entity_type))
1693
- );
1694
- const paginatedSchemas = sortedSchemas.slice(offset, offset + limit);
1695
-
1696
- return res.json({
1697
- schemas: paginatedSchemas,
1698
- total: productionSchemas.length,
1699
- limit,
1700
- offset,
1701
- });
1702
- } catch (error) {
1703
- return handleApiError(
1704
- req,
1705
- res,
1706
- error,
1707
- "Failed to list schemas",
1708
- "DB_QUERY_FAILED",
1709
- "APIError:schemas_list"
1710
- );
1711
- }
1712
- });
1713
-
1714
- // GET /api/schemas/:entity_type - Get specific schema (FU-XXX)
1715
- app.get("/schemas/:entity_type", async (req, res) => {
1716
- try {
1717
- const entityType = decodeURIComponent(req.params.entity_type);
1718
- const { SchemaRegistryService } = await import("./services/schema_registry.js");
1719
- const schemaRegistry = new SchemaRegistryService();
1720
-
1721
- // Handle authentication - support both Ed25519 and session tokens
1722
- const headerAuth = req.headers.authorization || "";
1723
- let userId: string | undefined = req.query.user_id as string | undefined;
1724
-
1725
- if (headerAuth.startsWith("Bearer ")) {
1726
- const token = headerAuth.slice("Bearer ".length).trim();
1727
-
1728
- // Try to validate as Ed25519 bearer token first
1729
- const registered = ensurePublicKeyRegistered(token);
1730
- if (!registered || !isBearerTokenValid(token)) {
1731
- // Try to validate as session token
1732
- try {
1733
- const { validateSessionToken } = await import("./services/mcp_auth.js");
1734
- const validated = await validateSessionToken(token);
1735
- // Use validated user ID if not provided in query
1736
- userId = userId || validated.userId;
1737
- } catch (authError) {
1738
- // Not a valid token - continue without user_id (will try global schema)
1739
- }
1740
- }
1741
- // If Ed25519 token is valid, use user_id from query (Ed25519 tokens don't contain user info)
1742
- }
1743
-
1744
- // Try to load active schema (global or user-specific), then fallback to code-defined schemas
1745
- let schema = await schemaRegistry.loadActiveSchema(entityType, userId);
1746
-
1747
- if (!schema) {
1748
- const { ENTITY_SCHEMAS } = await import("./services/schema_definitions.js");
1749
- const normalized = entityType.toLowerCase().trim().replace(/\s+/g, "_");
1750
- const codeSchema = ENTITY_SCHEMAS[entityType] ?? ENTITY_SCHEMAS[normalized];
1751
- if (codeSchema) {
1752
- schema = {
1753
- id: "",
1754
- entity_type: codeSchema.entity_type,
1755
- schema_version: codeSchema.schema_version,
1756
- schema_definition: codeSchema.schema_definition,
1757
- reducer_config: codeSchema.reducer_config,
1758
- active: true,
1759
- created_at: new Date().toISOString(),
1760
- };
1761
- }
1762
- }
1763
-
1764
- if (!schema) {
1765
- return sendError(res, 404, "RESOURCE_NOT_FOUND", `Schema not found: ${entityType}`);
1766
- }
1767
-
1768
- return res.json(schema);
1769
- } catch (error) {
1770
- logError("APIError:schema_detail", req, error);
1771
- const message = error instanceof Error ? error.message : "Failed to get schema";
1772
- return sendError(res, 500, "DB_QUERY_FAILED", message);
1773
- }
1774
- });
1775
-
1776
- // GET /api/relationships - List all relationships with filtering (FU-XXX)
1777
- // REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
1778
- app.get("/relationships", async (req, res) => {
1779
- try {
1780
- // Get authenticated user_id (REQUIRED)
1781
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1782
-
1783
- const relationshipType = req.query.relationship_type as string | undefined;
1784
- const sourceEntityId = req.query.source_entity_id as string | undefined;
1785
- const targetEntityId = req.query.target_entity_id as string | undefined;
1786
- const limit = parseInt(req.query.limit as string) || 100;
1787
- const offset = parseInt(req.query.offset as string) || 0;
1788
-
1789
- // Build query - ALWAYS filter by authenticated user_id
1790
- let query = db
1791
- .from("relationship_snapshots")
1792
- .select("*", { count: "exact" })
1793
- .eq("user_id", userId); // SECURITY: Always filter by authenticated user
1794
-
1795
- if (relationshipType) {
1796
- query = query.eq("relationship_type", relationshipType);
1797
- }
1798
- if (sourceEntityId) {
1799
- query = query.eq("source_entity_id", sourceEntityId);
1800
- }
1801
- if (targetEntityId) {
1802
- query = query.eq("target_entity_id", targetEntityId);
1803
- }
1804
-
1805
- const { data, error, count } = await query
1806
- .order("last_observation_at", { ascending: false })
1807
- .range(offset, offset + limit - 1);
1808
-
1809
- if (error) throw error;
1810
-
1811
- // Get unique entity IDs to fetch entity details
1812
- const entityIds = new Set<string>();
1813
- (data || []).forEach((rel: any) => {
1814
- entityIds.add(rel.source_entity_id);
1815
- entityIds.add(rel.target_entity_id);
1816
- });
1817
-
1818
- // Fetch entity details - filter by user_id for security
1819
- let entityMap = new Map<string, { canonical_name: string; entity_type: string }>();
1820
- if (entityIds.size > 0) {
1821
- const { data: entities, error: entityError } = await db
1822
- .from("entities")
1823
- .select("id, canonical_name, entity_type")
1824
- .in("id", Array.from(entityIds))
1825
- .eq("user_id", userId); // SECURITY: Only return entities for authenticated user
1826
-
1827
- if (!entityError && entities) {
1828
- entityMap = new Map(
1829
- entities.map((e: any) => [
1830
- e.id,
1831
- { canonical_name: e.canonical_name, entity_type: e.entity_type },
1832
- ])
1833
- );
1834
- }
1835
- }
1836
-
1837
- // Add id field and entity information for frontend compatibility
1838
- const relationships = (data || []).map((rel: any) => {
1839
- const sourceEntity = entityMap.get(rel.source_entity_id);
1840
- const targetEntity = entityMap.get(rel.target_entity_id);
1841
-
1842
- return {
1843
- ...rel,
1844
- id: rel.relationship_key,
1845
- source_canonical_name: sourceEntity?.canonical_name,
1846
- source_entity_type: sourceEntity?.entity_type,
1847
- target_canonical_name: targetEntity?.canonical_name,
1848
- target_entity_type: targetEntity?.entity_type,
1849
- };
1850
- });
1851
-
1852
- return res.json({
1853
- relationships,
1854
- total: count || 0,
1855
- limit,
1856
- offset,
1857
- });
1858
- } catch (error) {
1859
- return handleApiError(
1860
- req,
1861
- res,
1862
- error,
1863
- "Failed to list relationships",
1864
- "DB_QUERY_FAILED",
1865
- "APIError:relationships_list"
1866
- );
1867
- }
1868
- });
1869
-
1870
- // GET /api/relationships/:id - Get specific relationship (FU-XXX)
1871
- // REQUIRES AUTHENTICATION - verifies relationship belongs to authenticated user
1872
- // Note: id is actually relationship_key (composite key)
1873
- app.get("/relationships/:id", async (req, res) => {
1874
- try {
1875
- // Get authenticated user_id (REQUIRED)
1876
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1877
-
1878
- const relationshipKey = decodeURIComponent(req.params.id);
1879
-
1880
- // Verify relationship exists and belongs to authenticated user
1881
- const { data, error } = await db
1882
- .from("relationship_snapshots")
1883
- .select("*")
1884
- .eq("relationship_key", relationshipKey)
1885
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
1886
- .single();
1887
-
1888
- if (error) {
1889
- if (error.code === "PGRST116") {
1890
- return sendError(
1891
- res,
1892
- 404,
1893
- "RESOURCE_NOT_FOUND",
1894
- `Relationship not found: ${relationshipKey}`
1895
- );
1896
- }
1897
- throw error;
1898
- }
1899
-
1900
- // Fetch entity details - filter by user_id for security
1901
- const { data: sourceEntity } = await db
1902
- .from("entities")
1903
- .select("canonical_name, entity_type")
1904
- .eq("id", data.source_entity_id)
1905
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
1906
- .single();
1907
-
1908
- const { data: targetEntity } = await db
1909
- .from("entities")
1910
- .select("canonical_name, entity_type")
1911
- .eq("id", data.target_entity_id)
1912
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
1913
- .single();
1914
-
1915
- // Add id field and entity information for frontend compatibility
1916
- return res.json({
1917
- ...data,
1918
- id: data.relationship_key,
1919
- source_canonical_name: sourceEntity?.canonical_name,
1920
- source_entity_type: sourceEntity?.entity_type,
1921
- target_canonical_name: targetEntity?.canonical_name,
1922
- target_entity_type: targetEntity?.entity_type,
1923
- });
1924
- } catch (error) {
1925
- return handleApiError(
1926
- req,
1927
- res,
1928
- error,
1929
- "Failed to get relationship",
1930
- "DB_QUERY_FAILED",
1931
- "APIError:relationship_detail"
1932
- );
1933
- }
1934
- });
1935
-
1936
- // POST /api/relationships/snapshot - Get relationship snapshot with provenance
1937
- app.post("/relationships/snapshot", async (req, res) => {
1938
- const parsed = RelationshipSnapshotRequestSchema.safeParse(req.body);
1939
- if (!parsed.success) {
1940
- logWarn("ValidationError:get_relationship_snapshot", req, { issues: parsed.error.issues });
1941
- return sendValidationError(res, parsed.error.issues);
1942
- }
1943
-
1944
- try {
1945
- const userId = await getAuthenticatedUserId(req, undefined);
1946
- const { relationship_type, source_entity_id, target_entity_id } = parsed.data;
1947
- const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
1948
-
1949
- const { data: snapshot, error: snapshotError } = await db
1950
- .from("relationship_snapshots")
1951
- .select("*")
1952
- .eq("relationship_key", relationshipKey)
1953
- .eq("user_id", userId)
1954
- .maybeSingle();
1955
-
1956
- if (snapshotError) throw snapshotError;
1957
- if (!snapshot) {
1958
- return sendError(
1959
- res,
1960
- 404,
1961
- "RESOURCE_NOT_FOUND",
1962
- `Relationship not found: ${relationshipKey}`
1963
- );
1964
- }
1965
-
1966
- const { data: observations, error: obsError } = await db
1967
- .from("relationship_observations")
1968
- .select("id, source_id, observed_at, specificity_score, source_priority, metadata")
1969
- .eq("relationship_key", relationshipKey)
1970
- .eq("user_id", userId)
1971
- .order("observed_at", { ascending: false });
1972
-
1973
- if (obsError) throw obsError;
1974
-
1975
- return res.json({ snapshot, observations: observations ?? [] });
1976
- } catch (error) {
1977
- return handleApiError(
1978
- req,
1979
- res,
1980
- error,
1981
- "Failed to get relationship snapshot",
1982
- "DB_QUERY_FAILED",
1983
- "APIError:relationship_snapshot"
1984
- );
1985
- }
1986
- });
1987
-
1988
- // GET /api/timeline - Get timeline events with filtering (FU-303)
1989
- // REQUIRES AUTHENTICATION - filters events through sources.user_id
1990
- app.get("/timeline", async (req, res) => {
1991
- try {
1992
- // Get authenticated user_id (REQUIRED)
1993
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
1994
-
1995
- const startDate = req.query.start_date as string | undefined;
1996
- const endDate = req.query.end_date as string | undefined;
1997
- const eventType = req.query.event_type as string | undefined;
1998
- const entityId = req.query.entity_id as string | undefined;
1999
- const limit = parseInt(req.query.limit as string) || 100;
2000
- const offset = parseInt(req.query.offset as string) || 0;
2001
-
2002
- // Get source IDs for this user first (timeline_events doesn't have user_id)
2003
- const { data: userSources, error: sourcesError } = await db
2004
- .from("sources")
2005
- .select("id")
2006
- .eq("user_id", userId);
2007
-
2008
- if (sourcesError) throw sourcesError;
2009
-
2010
- const sourceIds = (userSources || []).map((s: any) => s.id);
2011
-
2012
- // Build query - filter by source_ids that belong to authenticated user
2013
- let query = db.from("timeline_events").select("*", { count: "exact" });
2014
-
2015
- // SECURITY: Only return events from sources that belong to authenticated user
2016
- if (sourceIds.length > 0) {
2017
- query = query.in("source_id", sourceIds);
2018
- } else {
2019
- // User has no sources - return empty result
2020
- return res.json({
2021
- events: [],
2022
- total: 0,
2023
- limit,
2024
- offset,
2025
- });
2026
- }
2027
-
2028
- // Filter by date range
2029
- if (startDate) {
2030
- query = query.gte("event_timestamp", startDate);
2031
- }
2032
- if (endDate) {
2033
- query = query.lte("event_timestamp", endDate);
2034
- }
2035
-
2036
- // Filter by event type
2037
- if (eventType) {
2038
- query = query.eq("event_type", eventType);
2039
- }
2040
-
2041
- // Filter by entity_id
2042
- if (entityId) {
2043
- query = query.eq("entity_id", entityId);
2044
- }
2045
-
2046
- // Sort by event timestamp (chronological)
2047
- query = query.order("event_timestamp", { ascending: false });
2048
-
2049
- // Pagination
2050
- query = query.range(offset, offset + limit - 1);
2051
-
2052
- const { data, error, count } = await query;
2053
-
2054
- if (error) {
2055
- const err = error as { code?: string; message?: string };
2056
- const errorDetails =
2057
- typeof error === "object" && error && "details" in error
2058
- ? (error as { details?: string }).details
2059
- : undefined;
2060
- const errorHint =
2061
- typeof error === "object" && error && "hint" in error
2062
- ? (error as { hint?: string }).hint
2063
- : undefined;
2064
-
2065
- logError("APIError:timeline_query", req, error, {
2066
- errorCode: err.code,
2067
- errorMessage: err.message,
2068
- errorDetails,
2069
- errorHint,
2070
- userId: userId || "none",
2071
- });
2072
-
2073
- return sendError(res, 500, "DB_QUERY_FAILED", "Failed to query timeline events", {
2074
- code: err.code,
2075
- message: err.message,
2076
- hint: errorHint,
2077
- });
2078
- }
2079
-
2080
- return res.json({
2081
- events: data || [],
2082
- total: count || 0,
2083
- limit,
2084
- offset,
2085
- });
2086
- } catch (error) {
2087
- if (error instanceof Error && error.message.includes("Not authenticated")) {
2088
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
2089
- }
2090
- logError("APIError:timeline", req, error);
2091
- const message = error instanceof Error ? error.message : "Failed to get timeline";
2092
- // Include error code if available (for db errors)
2093
- const errorCode = (error as any)?.code;
2094
- const errorDetails = errorCode ? { code: errorCode, message } : { message };
2095
- return sendError(res, 500, "DB_QUERY_FAILED", message, errorDetails);
2096
- }
2097
- });
2098
-
2099
- // GET /api/timeline/:id - Get one timeline event by ID (FU-303)
2100
- // REQUIRES AUTHENTICATION - verifies event source belongs to authenticated user
2101
- app.get("/timeline/:id", async (req, res) => {
2102
- try {
2103
- const eventId = decodeURIComponent(req.params.id);
2104
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
2105
-
2106
- // Get source IDs for this user first (timeline_events doesn't have user_id)
2107
- const { data: userSources, error: sourcesError } = await db
2108
- .from("sources")
2109
- .select("id")
2110
- .eq("user_id", userId);
2111
-
2112
- if (sourcesError) throw sourcesError;
2113
- const sourceIds = (userSources || []).map((source: { id: string }) => source.id);
2114
-
2115
- if (sourceIds.length === 0) {
2116
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Timeline event not found");
2117
- }
2118
-
2119
- const { data: event, error } = await db
2120
- .from("timeline_events")
2121
- .select("*")
2122
- .eq("id", eventId)
2123
- .in("source_id", sourceIds)
2124
- .maybeSingle();
2125
-
2126
- if (error) {
2127
- const err = error as { code?: string; message?: string };
2128
- if (err.code === "PGRST116") {
2129
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Timeline event not found");
2130
- }
2131
- throw error;
2132
- }
2133
-
2134
- if (!event) {
2135
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Timeline event not found");
2136
- }
2137
-
2138
- return res.json({ event });
2139
- } catch (error) {
2140
- if (error instanceof Error && error.message.includes("Not authenticated")) {
2141
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
2142
- }
2143
- logError("APIError:timeline_detail", req, error);
2144
- const message = error instanceof Error ? error.message : "Failed to get timeline event";
2145
- const errorCode = (error as { code?: string })?.code;
2146
- const errorDetails = errorCode ? { code: errorCode, message } : { message };
2147
- return sendError(res, 500, "DB_QUERY_FAILED", message, errorDetails);
2148
- }
2149
- });
2150
-
2151
- // GET /api/sources - Get source list (FU-301)
2152
- app.get("/sources", async (req, res) => {
2153
- try {
2154
- // Get authenticated user_id (REQUIRED)
2155
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
2156
-
2157
- const search = req.query.search as string | undefined;
2158
- const mimeType = req.query.mime_type as string | undefined;
2159
- const sourceType = req.query.source_type as string | undefined;
2160
- const limit = parseInt(req.query.limit as string) || 100;
2161
- const offset = parseInt(req.query.offset as string) || 0;
2162
-
2163
- // Build query - ALWAYS filter by authenticated user_id
2164
- let query = db.from("sources").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
2165
-
2166
- // Filter by MIME type
2167
- if (mimeType) {
2168
- query = query.eq("mime_type", mimeType);
2169
- }
2170
-
2171
- // Filter by source type
2172
- if (sourceType) {
2173
- query = query.eq("source_type", sourceType);
2174
- }
2175
-
2176
- // Search in file names and raw text
2177
- if (search) {
2178
- query = query.or(`file_name.ilike.%${search}%,original_filename.ilike.%${search}%`);
2179
- }
2180
-
2181
- // Sort by created_at descending (most recent first)
2182
- query = query.order("created_at", { ascending: false });
2183
-
2184
- // Pagination
2185
- query = query.range(offset, offset + limit - 1);
2186
-
2187
- const { data, error, count } = await query;
2188
-
2189
- if (error) throw error;
2190
-
2191
- return res.json({
2192
- sources: data || [],
2193
- total: count || 0,
2194
- limit,
2195
- offset,
2196
- });
2197
- } catch (error) {
2198
- return handleApiError(
2199
- req,
2200
- res,
2201
- error,
2202
- "Failed to get sources",
2203
- "DB_QUERY_FAILED",
2204
- "APIError:sources_list"
2205
- );
2206
- }
2207
- });
2208
-
2209
- // GET /api/sources/:id - Get source detail (FU-302)
2210
- // REQUIRES AUTHENTICATION - verifies source belongs to authenticated user
2211
- app.get("/sources/:id", async (req, res) => {
2212
- try {
2213
- // Get authenticated user_id (REQUIRED)
2214
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
2215
-
2216
- const sourceId = req.params.id;
2217
-
2218
- // Verify source exists and belongs to authenticated user
2219
- const { data: source, error } = await db
2220
- .from("sources")
2221
- .select("*")
2222
- .eq("id", sourceId)
2223
- .eq("user_id", userId) // SECURITY: Only return if belongs to authenticated user
2224
- .single();
2225
-
2226
- if (error) {
2227
- if (error.code === "PGRST116") {
2228
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
2229
- }
2230
- throw error;
2231
- }
2232
-
2233
- if (!source) {
2234
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
2235
- }
2236
-
2237
- return res.json(source);
2238
- } catch (error) {
2239
- return handleApiError(
2240
- req,
2241
- res,
2242
- error,
2243
- "Failed to get source",
2244
- "DB_QUERY_FAILED",
2245
- "APIError:source_detail"
2246
- );
2247
- }
2248
- });
2249
-
2250
- // GET /api/interpretations - Get interpretations with filters (FU-302)
2251
- // REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
2252
- app.get("/interpretations", async (req, res) => {
2253
- try {
2254
- // Get authenticated user_id (REQUIRED)
2255
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
2256
-
2257
- const sourceId = req.query.source_id as string | undefined;
2258
- const limit = parseInt(req.query.limit as string) || 100;
2259
- const offset = parseInt(req.query.offset as string) || 0;
2260
-
2261
- // Build query - ALWAYS filter by authenticated user_id
2262
- let query = db
2263
- .from("interpretations")
2264
- .select("*", { count: "exact" })
2265
- .eq("user_id", userId); // SECURITY: Always filter by authenticated user
2266
-
2267
- if (sourceId) {
2268
- query = query.eq("source_id", sourceId);
2269
- }
2270
-
2271
- query = query.order("started_at", { ascending: false });
2272
- query = query.range(offset, offset + limit - 1);
2273
-
2274
- const { data, error, count } = await query;
2275
-
2276
- if (error) throw error;
2277
-
2278
- return res.json({
2279
- interpretations: data || [],
2280
- total: count || 0,
2281
- limit,
2282
- offset,
2283
- });
2284
- } catch (error) {
2285
- logError("APIError:interpretations_list", req, error);
2286
- const message = error instanceof Error ? error.message : "Failed to get interpretations";
2287
- return sendError(res, 500, "DB_QUERY_FAILED", message);
2288
- }
2289
- });
2290
-
2291
- // GET /api/observations - Get observations with filters (FU-302, FU-601)
2292
- // REQUIRES AUTHENTICATION - all queries filtered by authenticated user_id
2293
- app.get("/observations", async (req, res) => {
2294
- try {
2295
- // Get authenticated user_id (REQUIRED)
2296
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
2297
-
2298
- const sourceId = req.query.source_id as string | undefined;
2299
- const entityId = req.query.entity_id as string | undefined;
2300
- const limit = parseInt(req.query.limit as string) || 100;
2301
- const offset = parseInt(req.query.offset as string) || 0;
2302
-
2303
- // Build query - ALWAYS filter by authenticated user_id
2304
- let query = db.from("observations").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
2305
-
2306
- if (sourceId) {
2307
- query = query.eq("source_id", sourceId);
2308
- }
2309
-
2310
- if (entityId) {
2311
- query = query.eq("entity_id", entityId);
2312
- }
2313
-
2314
- query = query.order("observed_at", { ascending: false });
2315
- query = query.range(offset, offset + limit - 1);
2316
-
2317
- const { data, error, count } = await query;
2318
-
2319
- if (error) throw error;
2320
-
2321
- return res.json({
2322
- observations: data || [],
2323
- total: count || 0,
2324
- limit,
2325
- offset,
2326
- });
2327
- } catch (error) {
2328
- return handleApiError(
2329
- req,
2330
- res,
2331
- error,
2332
- "Failed to get observations",
2333
- "DB_QUERY_FAILED",
2334
- "APIError:observations_list"
2335
- );
2336
- }
2337
- });
2338
-
2339
- // GET /api/stats - Get dashboard statistics (FU-305)
2340
- // REQUIRES AUTHENTICATION - all stats filtered by authenticated user_id
2341
- app.get("/stats", async (req, res) => {
2342
- try {
2343
- // Get authenticated user_id (REQUIRED)
2344
- const userId = await getAuthenticatedUserId(req, req.query.user_id as string | undefined);
2345
-
2346
- const { getDashboardStats } = await import("./services/dashboard_stats.js");
2347
- const stats = await getDashboardStats(userId); // SECURITY: Stats filtered by authenticated user
2348
-
2349
- return res.json(stats);
2350
- } catch (error) {
2351
- return handleApiError(
2352
- req,
2353
- res,
2354
- error,
2355
- "Failed to get dashboard stats",
2356
- "DB_QUERY_FAILED",
2357
- "APIError:dashboard_stats"
2358
- );
2359
- }
2360
- });
2361
-
2362
- // POST /api/observations/create - Create observation for entity
2363
- // REQUIRES AUTHENTICATION - validates user_id matches authenticated user
2364
- app.post("/observations/create", async (req, res) => {
2365
- const schema = z.object({
2366
- entity_type: z.string(),
2367
- entity_identifier: z.string(),
2368
- fields: z.record(z.unknown()),
2369
- source_priority: z.number().optional().default(100),
2370
- user_id: z.string().uuid(),
2371
- });
2372
-
2373
- const parsed = schema.safeParse(req.body);
2374
- if (!parsed.success) {
2375
- logWarn("ValidationError:observations_create", req, {
2376
- issues: parsed.error.issues,
2377
- });
2378
- return sendValidationError(res, parsed.error.issues);
2379
- }
2380
-
2381
- try {
2382
- // Get authenticated user_id and validate it matches provided user_id
2383
- const authenticatedUserId = await getAuthenticatedUserId(req, parsed.data.user_id);
2384
-
2385
- const { entity_type, entity_identifier, fields, source_priority, user_id } = parsed.data;
2386
-
2387
- // SECURITY: Ensure provided user_id matches authenticated user
2388
- if (user_id !== authenticatedUserId) {
2389
- return res
2390
- .status(403)
2391
- .json(buildErrorEnvelope("FORBIDDEN", "user_id does not match authenticated user."));
2392
- }
2393
-
2394
- const { resolveEntity } = await import("./services/entity_resolution.js");
2395
- const { createObservation } = await import("./services/observation_storage.js");
2396
- const { getSnapshot } = await import("./services/snapshot_computation.js");
2397
-
2398
- const entity_id = await resolveEntity({
2399
- entityType: entity_type,
2400
- fields: { name: entity_identifier },
2401
- userId: user_id,
2402
- });
2403
-
2404
- const obsData = await createObservation({
2405
- entity_id,
2406
- entity_type,
2407
- schema_version: "1.0",
2408
- source_id: null,
2409
- interpretation_id: null,
2410
- observed_at: new Date().toISOString(),
2411
- specificity_score: 1.0,
2412
- source_priority,
2413
- fields,
2414
- user_id,
2415
- });
2416
-
2417
- const snapshot = await getSnapshot(entity_id, user_id);
2418
-
2419
- return res.json({
2420
- observation_id: obsData.id,
2421
- entity_id,
2422
- snapshot: snapshot?.snapshot || {},
2423
- });
2424
- } catch (error) {
2425
- logError("APIError:observations_create", req, error);
2426
- const message = error instanceof Error ? error.message : "Failed to create observation";
2427
- return res.status(500).json(buildErrorEnvelope("DB_QUERY_FAILED", message));
2428
- }
2429
- });
2430
-
2431
- async function storeStructuredForApi(params: {
2432
- userId: string;
2433
- entities: Record<string, unknown>[];
2434
- sourcePriority: number;
2435
- idempotencyKey: string;
2436
- originalFilename?: string;
2437
- }) {
2438
- const { userId, entities, sourcePriority, idempotencyKey, originalFilename } = params;
2439
- const { resolveEntity } = await import("./services/entity_resolution.js");
2440
-
2441
- const { data: existingSource, error: existingSourceError } = await db
2442
- .from("sources")
2443
- .select("id")
2444
- .eq("user_id", userId)
2445
- .eq("idempotency_key", idempotencyKey)
2446
- .maybeSingle();
2447
-
2448
- if (existingSourceError) {
2449
- throw existingSourceError;
2450
- }
2451
-
2452
- if (existingSource) {
2453
- const { listObservationsForSource } = await import("./services/observation_storage.js");
2454
- const existingObs = await listObservationsForSource(existingSource.id, userId);
2455
-
2456
- const existingEntities = existingObs.map((obs) => ({
2457
- entity_id: obs.entity_id,
2458
- entity_type: obs.entity_type,
2459
- observation_id: obs.id,
2460
- }));
2461
-
2462
- return {
2463
- success: true,
2464
- source_id: existingSource.id,
2465
- entities_created: existingEntities.length,
2466
- observations_created: existingEntities.length,
2467
- entities: existingEntities,
2468
- };
2469
- }
2470
-
2471
- const { createObservation } = await import("./services/observation_storage.js");
2472
-
2473
- const jsonContent = JSON.stringify(entities, (key, value) => {
2474
- if (typeof value === "bigint") {
2475
- return Number(value);
2476
- }
2477
- return value;
2478
- });
2479
- const filenameForStorage = originalFilename?.trim() || undefined;
2480
- const storageResult = await storeRawContent({
2481
- userId,
2482
- fileBuffer: Buffer.from(jsonContent, "utf-8"),
2483
- mimeType: "application/json",
2484
- originalFilename: filenameForStorage,
2485
- idempotencyKey,
2486
- provenance: {
2487
- upload_method: "api_store",
2488
- client: "api",
2489
- source_priority: sourcePriority,
2490
- },
2491
- });
2492
-
2493
- const createdEntities = [];
2494
-
2495
- for (const entityData of entities) {
2496
- const entity_type = entityData.entity_type as string;
2497
- if (!entity_type) {
2498
- throw new Error("entity_type is required for each entity");
2499
- }
2500
-
2501
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- intentional omit
2502
- const { entity_type: _removed, ...fields } = entityData;
2503
- const entity_id = await resolveEntity({
2504
- entityType: entity_type,
2505
- fields,
2506
- userId,
2507
- });
2508
-
2509
- const obsData = await createObservation({
2510
- entity_id,
2511
- entity_type,
2512
- schema_version: "1.0",
2513
- source_id: storageResult.sourceId,
2514
- interpretation_id: null,
2515
- observed_at: new Date().toISOString(),
2516
- specificity_score: 1.0,
2517
- source_priority: sourcePriority,
2518
- fields,
2519
- user_id: userId,
2520
- });
2521
-
2522
- createdEntities.push({
2523
- entity_id,
2524
- entity_type,
2525
- observation_id: obsData.id,
2526
- });
2527
- }
2528
-
2529
- return {
2530
- success: true,
2531
- source_id: storageResult.sourceId,
2532
- entities_created: createdEntities.length,
2533
- observations_created: createdEntities.length,
2534
- entities: createdEntities,
2535
- };
2536
- }
2537
-
2538
- async function storeUnstructuredForApi(params: {
2539
- userId: string;
2540
- fileContent: string;
2541
- mimeType: string;
2542
- idempotencyKey?: string;
2543
- originalFilename?: string;
2544
- interpret: boolean;
2545
- interpretationConfig?: Record<string, unknown>;
2546
- }) {
2547
- const { runInterpretation } = await import("./services/interpretation.js");
2548
- const {
2549
- fileContent,
2550
- mimeType,
2551
- idempotencyKey,
2552
- originalFilename,
2553
- interpret,
2554
- userId,
2555
- interpretationConfig,
2556
- } = params;
2557
- const fileBuffer = Buffer.from(fileContent, "base64");
2558
- const resolvedIdempotencyKey =
2559
- idempotencyKey ??
2560
- (await import("node:crypto")).createHash("sha256").update(fileBuffer).digest("hex");
2561
-
2562
- const storageResult = await storeRawContent({
2563
- userId,
2564
- fileBuffer,
2565
- mimeType,
2566
- originalFilename: originalFilename?.trim() || undefined,
2567
- idempotencyKey: resolvedIdempotencyKey,
2568
- provenance: { upload_method: "api_store_unstructured", client: "api" },
2569
- });
2570
-
2571
- const response: {
2572
- source_id: string;
2573
- content_hash: string;
2574
- file_size: number;
2575
- deduplicated?: boolean;
2576
- entities_created?: number;
2577
- observations_created?: number;
2578
- interpretation_run_id?: string;
2579
- interpretation?: unknown;
2580
- interpretation_debug?: Record<string, unknown>;
2581
- entity_ids?: string[];
2582
- } = {
2583
- source_id: storageResult.sourceId,
2584
- content_hash: storageResult.contentHash,
2585
- file_size: storageResult.fileSize,
2586
- deduplicated: storageResult.deduplicated,
2587
- entities_created: 0,
2588
- observations_created: 0,
2589
- };
2590
-
2591
- if (interpret && storageResult.sourceId) {
2592
- const { extractTextFromBuffer, getPdfFirstPageImageDataUrl, getPdfWorkerDebug } = await import(
2593
- "./services/file_text_extraction.js"
2594
- );
2595
- const {
2596
- extractWithLLM,
2597
- extractWithLLMFromImage,
2598
- extractFromCSVWithChunking,
2599
- isLLMExtractionAvailable,
2600
- } = await import("./services/llm_extraction.js");
2601
-
2602
- const rawText = await extractTextFromBuffer(fileBuffer, mimeType, originalFilename || "file");
2603
-
2604
- const isCsv = mimeType?.toLowerCase() === "text/csv";
2605
-
2606
- if (!isCsv && !isLLMExtractionAvailable()) {
2607
- response.interpretation = {
2608
- skipped: true,
2609
- reason: "openai_not_configured",
2610
- message: "Set OPENAI_API_KEY in .env to enable AI interpretation",
2611
- };
2612
- return response;
2613
- }
2614
-
2615
- const isPdf =
2616
- (mimeType || "").toLowerCase().includes("pdf") ||
2617
- (originalFilename || "").toLowerCase().endsWith(".pdf");
2618
- const rawTextLength = typeof rawText === "string" ? rawText.length : 0;
2619
-
2620
- // Avoid nondeterministic LLM errors for empty non-PDF files; store the source and skip interpretation.
2621
- if (rawTextLength === 0 && !isPdf && !isCsv) {
2622
- response.interpretation = {
2623
- skipped: true,
2624
- reason: "empty_content",
2625
- message: "No extractable text found; interpretation skipped",
2626
- };
2627
- response.interpretation_debug = {
2628
- raw_text_length: 0,
2629
- };
2630
- return response;
2631
- }
2632
-
2633
- let extractionResult:
2634
- | Awaited<ReturnType<typeof extractWithLLM>>
2635
- | Awaited<ReturnType<typeof extractFromCSVWithChunking>>
2636
- | undefined = undefined;
2637
- let extractedData: Array<Record<string, unknown>> = [];
2638
- const pdfDebug = getPdfWorkerDebug();
2639
- const interpretationDebug: Record<string, unknown> = {
2640
- raw_text_length: rawTextLength,
2641
- pdf_worker_wrapper_used: pdfDebug.configured,
2642
- pdf_worker_wrapper_path_tried: pdfDebug.wrapper_path_tried,
2643
- pdf_worker_set_worker_error: pdfDebug.set_worker_error,
2644
- };
2645
- if (rawTextLength === 0 && isPdf) {
2646
- interpretationDebug.vision_fallback_attempted = true;
2647
- const imageResult = await getPdfFirstPageImageDataUrl(fileBuffer, mimeType, originalFilename || "file", {
2648
- returnError: true,
2649
- });
2650
- const imageDataUrl = typeof imageResult === "object" ? imageResult.dataUrl : imageResult;
2651
- if (typeof imageResult === "object" && imageResult.error) {
2652
- interpretationDebug.vision_fallback_image_error = imageResult.error;
2653
- }
2654
- interpretationDebug.vision_fallback_image_got = Boolean(imageDataUrl);
2655
- if (imageDataUrl) {
2656
- try {
2657
- extractionResult = await extractWithLLMFromImage(
2658
- imageDataUrl,
2659
- originalFilename || "file",
2660
- mimeType,
2661
- "gpt-4o"
2662
- );
2663
- interpretationDebug.used_vision_fallback = true;
2664
- } catch (visionErr) {
2665
- interpretationDebug.vision_fallback_error =
2666
- visionErr instanceof Error ? visionErr.message : String(visionErr);
2667
- extractionResult = await extractWithLLM(rawText, originalFilename || "file", mimeType, "gpt-4o");
2668
- }
2669
- } else {
2670
- extractionResult = await extractWithLLM(rawText, originalFilename || "file", mimeType, "gpt-4o");
2671
- }
2672
- } else if (isCsv) {
2673
- const { extractEntitiesFromCsvRows } = await import("./services/csv_row_extraction.js");
2674
- extractedData = extractEntitiesFromCsvRows(fileBuffer, originalFilename || "file");
2675
- } else {
2676
- extractionResult = await extractWithLLM(rawText, originalFilename || "file", mimeType, "gpt-4o");
2677
- }
2678
-
2679
- if (extractedData.length === 0 && extractionResult) {
2680
- if ("entities" in extractionResult) {
2681
- const multiResult = extractionResult as {
2682
- entities: Array<{ entity_type: string; fields: Record<string, unknown> }>;
2683
- };
2684
- extractedData = multiResult.entities.map((e) => ({
2685
- entity_type: e.entity_type,
2686
- ...e.fields,
2687
- }));
2688
- } else {
2689
- const { entity_type, fields } = extractionResult;
2690
- extractedData = [{ entity_type, ...fields }];
2691
- }
2692
- }
2693
-
2694
- const defaultConfig = {
2695
- provider: "openai",
2696
- model_id: "gpt-4o",
2697
- temperature: 0,
2698
- prompt_hash: "llm_extraction_v2_idempotent",
2699
- code_version: "v0.2.0",
2700
- };
2701
- interpretationDebug.extraction_field_keys = extractedData.flatMap((d) =>
2702
- Object.keys(d).filter((k) => k !== "entity_type" && k !== "type")
2703
- );
2704
- response.interpretation_debug = interpretationDebug;
2705
- try {
2706
- const interpretationResult = await runInterpretation({
2707
- userId,
2708
- sourceId: storageResult.sourceId,
2709
- extractedData,
2710
- config: {
2711
- ...defaultConfig,
2712
- ...(interpretationConfig ?? {}),
2713
- },
2714
- });
2715
- response.interpretation = interpretationResult;
2716
- if (interpretationResult.entities?.length) {
2717
- response.entity_ids = interpretationResult.entities.map((e) => e.entityId);
2718
- response.entities_created = interpretationResult.entities.length;
2719
- response.observations_created = interpretationResult.entities.length;
2720
- }
2721
- if (interpretationResult.interpretationId) {
2722
- response.interpretation_run_id = interpretationResult.interpretationId;
2723
- }
2724
- } catch (interpretError) {
2725
- response.interpretation = {
2726
- error: interpretError instanceof Error ? interpretError.message : String(interpretError),
2727
- skipped: true,
2728
- };
2729
- }
2730
- }
2731
-
2732
- return response;
2733
- }
2734
-
2735
- // POST /api/store - Unified store for structured, unstructured, or combined payloads
2736
- app.post("/store", async (req, res) => {
2737
- const parsed = StoreRequestSchema.safeParse(req.body);
2738
- if (!parsed.success) {
2739
- logWarn("ValidationError:store", req, {
2740
- issues: parsed.error.issues,
2741
- });
2742
- return sendValidationError(res, parsed.error.issues);
2743
- }
2744
-
2745
- try {
2746
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
2747
- const hasEntities = Boolean(parsed.data.entities?.length);
2748
- const hasFileContent = Boolean(parsed.data.file_content && parsed.data.mime_type);
2749
- const hasFilePath = Boolean(parsed.data.file_path);
2750
- const hasUnstructured = hasFileContent || hasFilePath;
2751
-
2752
- let structuredResult: Record<string, unknown> | undefined;
2753
- let unstructuredResult: Record<string, unknown> | undefined;
2754
-
2755
- if (hasEntities) {
2756
- if (!parsed.data.idempotency_key) {
2757
- return sendError(res, 400, "VALIDATION_ERROR", "idempotency_key is required when entities are provided");
2758
- }
2759
- structuredResult = await storeStructuredForApi({
2760
- userId,
2761
- entities: parsed.data.entities as Record<string, unknown>[],
2762
- sourcePriority: parsed.data.source_priority ?? 100,
2763
- idempotencyKey: parsed.data.idempotency_key,
2764
- originalFilename: parsed.data.original_filename,
2765
- });
2766
- }
2767
-
2768
- if (hasUnstructured) {
2769
- let fileContent = parsed.data.file_content;
2770
- let mimeType = parsed.data.mime_type;
2771
- let originalFilename = parsed.data.original_filename;
2772
-
2773
- if (hasFilePath) {
2774
- const resolvedPath = path.isAbsolute(parsed.data.file_path as string)
2775
- ? (parsed.data.file_path as string)
2776
- : path.resolve(process.cwd(), parsed.data.file_path as string);
2777
- const fileBuffer = fs.readFileSync(resolvedPath);
2778
- fileContent = fileBuffer.toString("base64");
2779
- if (!mimeType) {
2780
- const ext = path.extname(resolvedPath).toLowerCase();
2781
- mimeType = ext === ".pdf" ? "application/pdf" : ext === ".csv" ? "text/csv" : ext === ".json" ? "application/json" : "text/plain";
2782
- }
2783
- originalFilename = originalFilename || path.basename(resolvedPath);
2784
- }
2785
-
2786
- if (!fileContent || !mimeType) {
2787
- return sendError(res, 400, "VALIDATION_ERROR", "Unstructured store requires file_content+mime_type or file_path");
2788
- }
2789
-
2790
- unstructuredResult = await storeUnstructuredForApi({
2791
- userId,
2792
- fileContent,
2793
- mimeType,
2794
- idempotencyKey: parsed.data.file_idempotency_key,
2795
- originalFilename,
2796
- interpret: parsed.data.interpret ?? true,
2797
- interpretationConfig: parsed.data.interpretation_config,
2798
- });
2799
- }
2800
-
2801
- if (structuredResult && unstructuredResult) {
2802
- return res.json({
2803
- structured: structuredResult,
2804
- unstructured: unstructuredResult,
2805
- });
2806
- }
2807
- if (structuredResult) {
2808
- return res.json(structuredResult);
2809
- }
2810
- return res.status(200).json(unstructuredResult);
2811
- } catch (error) {
2812
- if (error instanceof Error && error.message.includes("Not authenticated")) {
2813
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
2814
- }
2815
- logError("APIError:store", req, error);
2816
- const message = error instanceof Error ? error.message : "Failed to store payload";
2817
- return sendError(res, 500, "DB_QUERY_FAILED", message);
2818
- }
2819
- });
2820
-
2821
- // POST /api/store/unstructured - Store raw file (base64), optional AI interpretation
2822
- app.post("/store/unstructured", async (req, res) => {
2823
- const parsed = StoreUnstructuredRequestSchema.safeParse(req.body);
2824
- if (!parsed.success) {
2825
- logWarn("ValidationError:store_unstructured", req, { issues: parsed.error.issues });
2826
- return sendValidationError(res, parsed.error.issues);
2827
- }
2828
-
2829
- try {
2830
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
2831
- const response = await storeUnstructuredForApi({
2832
- userId,
2833
- fileContent: parsed.data.file_content,
2834
- mimeType: parsed.data.mime_type,
2835
- idempotencyKey: parsed.data.idempotency_key,
2836
- originalFilename: parsed.data.original_filename,
2837
- interpret: parsed.data.interpret ?? true,
2838
- interpretationConfig: parsed.data.interpretation_config,
2839
- });
2840
- return res.status(200).json(response);
2841
- } catch (error) {
2842
- if (error instanceof Error && error.message.includes("Not authenticated")) {
2843
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
2844
- }
2845
- logError("APIError:store_unstructured", req, error);
2846
- const message = error instanceof Error ? error.message : "Failed to store unstructured file";
2847
- return sendError(res, 500, "DB_QUERY_FAILED", message);
2848
- }
2849
- });
2850
-
2851
- // POST /api/observations/query - Query observations
2852
- app.post("/observations/query", async (req, res) => {
2853
- const parsed = ObservationsQueryRequestSchema.safeParse(req.body);
2854
- if (!parsed.success) {
2855
- logWarn("ValidationError:observations_query", req, {
2856
- issues: parsed.error.issues,
2857
- });
2858
- return sendValidationError(res, parsed.error.issues);
2859
- }
2860
-
2861
- try {
2862
- // Get authenticated user_id (REQUIRED)
2863
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
2864
-
2865
- const { observation_id, entity_id, entity_type, source_id, limit, offset } = parsed.data;
2866
-
2867
- // Build query - ALWAYS filter by authenticated user_id
2868
- let query = db.from("observations").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
2869
-
2870
- if (observation_id) {
2871
- query = query.eq("id", observation_id);
2872
- }
2873
-
2874
- if (entity_id) {
2875
- query = query.eq("entity_id", entity_id);
2876
- }
2877
-
2878
- if (entity_type) {
2879
- query = query.eq("entity_type", entity_type);
2880
- }
2881
-
2882
- if (source_id) {
2883
- query = query.eq("source_id", source_id);
2884
- }
2885
-
2886
- query = query.order("observed_at", { ascending: false }).range(offset, offset + limit - 1);
2887
-
2888
- const { data, error, count } = await query;
2889
-
2890
- if (error) throw error;
2891
-
2892
- return res.json({
2893
- observations: data || [],
2894
- total: count || 0,
2895
- limit,
2896
- offset,
2897
- });
2898
- } catch (error) {
2899
- if (error instanceof Error && error.message.includes("Not authenticated")) {
2900
- return sendError(res, 401, "AUTH_REQUIRED", error.message);
2901
- }
2902
- logError("APIError:observations_query", req, error);
2903
- const message = error instanceof Error ? error.message : "Failed to query observations";
2904
- return sendError(res, 500, "DB_QUERY_FAILED", message);
2905
- }
2906
- });
2907
-
2908
- // POST /api/entities/merge - Merge duplicate entities
2909
- // REQUIRES AUTHENTICATION - validates user_id matches authenticated user and entities belong to user
2910
- app.post("/entities/merge", async (req, res) => {
2911
- const parsed = MergeEntitiesRequestSchema.safeParse(req.body);
2912
- if (!parsed.success) {
2913
- logWarn("ValidationError:entities_merge", req, {
2914
- issues: parsed.error.issues,
2915
- });
2916
- return sendValidationError(res, parsed.error.issues);
2917
- }
2918
-
2919
- try {
2920
- const authenticatedUserId = await getAuthenticatedUserId(req, parsed.data.user_id);
2921
-
2922
- const { from_entity_id, to_entity_id, merge_reason, user_id: providedUserId } = parsed.data;
2923
- const user_id = providedUserId || authenticatedUserId;
2924
-
2925
- if (providedUserId && providedUserId !== authenticatedUserId) {
2926
- return sendError(res, 403, "FORBIDDEN", "user_id does not match authenticated user.");
2927
- }
2928
-
2929
- const { mergeEntities } = await import("./services/entity_merge.js");
2930
- const result = await mergeEntities({
2931
- fromEntityId: from_entity_id,
2932
- toEntityId: to_entity_id,
2933
- userId: user_id,
2934
- mergeReason: merge_reason,
2935
- mergedBy: "http_api",
2936
- });
2937
-
2938
- return res.json(result);
2939
- } catch (error) {
2940
- const { EntityNotFoundError, EntityAlreadyMergedError } = await import(
2941
- "./services/entity_merge.js"
2942
- );
2943
- if (error instanceof EntityNotFoundError) {
2944
- return sendError(res, 404, "RESOURCE_NOT_FOUND", error.message);
2945
- }
2946
- if (error instanceof EntityAlreadyMergedError) {
2947
- return sendError(res, 400, "VALIDATION_INVALID_FORMAT", error.message);
2948
- }
2949
- logError("APIError:entities_merge", req, error);
2950
- const message = error instanceof Error ? error.message : "Failed to merge entities";
2951
- return sendError(res, 500, "DB_QUERY_FAILED", message);
2952
- }
2953
- });
2954
-
2955
- // MCP Actions for Observation Architecture (FU-061)
2956
-
2957
- // Get entity snapshot with provenance
2958
- app.post("/get_entity_snapshot", async (req, res) => {
2959
- const parsed = EntitySnapshotRequestSchema.safeParse(req.body);
2960
- if (!parsed.success) {
2961
- logWarn("ValidationError:get_entity_snapshot", req, {
2962
- issues: parsed.error.issues,
2963
- });
2964
- return sendValidationError(res, parsed.error.issues);
2965
- }
2966
-
2967
- const { entity_id } = parsed.data;
2968
-
2969
- const { data, error } = await db
2970
- .from("entity_snapshots")
2971
- .select("*")
2972
- .eq("entity_id", entity_id)
2973
- .single();
2974
-
2975
- if (error) {
2976
- if (error.code === "PGRST116") {
2977
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
2978
- }
2979
- logError("DbError:get_entity_snapshot", req, error);
2980
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
2981
- }
2982
-
2983
- logDebug("Success:get_entity_snapshot", req, { entity_id });
2984
- return res.json(data);
2985
- });
2986
-
2987
- // List observations for entity
2988
- app.post("/list_observations", async (req, res) => {
2989
- const parsed = ListObservationsRequestSchema.safeParse(req.body);
2990
- if (!parsed.success) {
2991
- logWarn("ValidationError:list_observations", req, {
2992
- issues: parsed.error.issues,
2993
- });
2994
- return sendValidationError(res, parsed.error.issues);
2995
- }
2996
-
2997
- const { entity_id, limit = 100, offset = 0 } = parsed.data;
2998
-
2999
- const query = db
3000
- .from("observations")
3001
- .select("*")
3002
- .eq("entity_id", entity_id)
3003
- .order("observed_at", { ascending: false })
3004
- .range(offset, offset + limit - 1);
3005
-
3006
- const { data, error } = await query;
3007
-
3008
- if (error) {
3009
- logError("DbError:list_observations", req, error);
3010
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
3011
- }
3012
-
3013
- logDebug("Success:list_observations", req, {
3014
- entity_id,
3015
- count: data?.length || 0,
3016
- });
3017
- return res.json({ observations: data || [] });
3018
- });
3019
-
3020
- // Get field provenance (trace field to source documents)
3021
- app.post("/get_field_provenance", async (req, res) => {
3022
- const parsed = FieldProvenanceRequestSchema.safeParse(req.body);
3023
- if (!parsed.success) {
3024
- logWarn("ValidationError:get_field_provenance", req, {
3025
- issues: parsed.error.issues,
3026
- });
3027
- return sendValidationError(res, parsed.error.issues);
3028
- }
3029
-
3030
- const { entity_id, field } = parsed.data;
3031
-
3032
- // Get snapshot to find observation ID for this field
3033
- const { data: snapshot } = await db
3034
- .from("entity_snapshots")
3035
- .select("provenance")
3036
- .eq("entity_id", entity_id)
3037
- .single();
3038
-
3039
- if (!snapshot || !snapshot.provenance) {
3040
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity or field not found");
3041
- }
3042
-
3043
- const provenance = snapshot.provenance as Record<string, string>;
3044
- const observationId = provenance[field];
3045
-
3046
- if (!observationId) {
3047
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Field not found in provenance");
3048
- }
3049
-
3050
- // Get observation(s) - may be comma-separated for merge_array
3051
- const observationIds = observationId.split(",");
3052
-
3053
- const { data: observations, error: obsError } = await db
3054
- .from("observations")
3055
- .select("*, source_id")
3056
- .in("id", observationIds);
3057
-
3058
- if (obsError) {
3059
- logError("DbError:get_field_provenance", req, obsError);
3060
- return sendError(res, 500, "DB_QUERY_FAILED", obsError.message);
3061
- }
3062
-
3063
- // Get sources for provenance
3064
- const sourceIds = (observations || [])
3065
- .map((obs: { source_id?: string }) => obs.source_id)
3066
- .filter((sourceId: string | undefined): sourceId is string => Boolean(sourceId));
3067
-
3068
- const { data: sources, error: sourceError } = await db
3069
- .from("sources")
3070
- .select("id, content_hash, mime_type, storage_url, file_name, created_at")
3071
- .in("id", sourceIds);
3072
-
3073
- if (sourceError) {
3074
- logError("DbError:get_field_provenance:sources", req, sourceError);
3075
- }
3076
-
3077
- logDebug("Success:get_field_provenance", req, { entity_id, field });
3078
- return res.json({
3079
- field,
3080
- entity_id,
3081
- observation_ids: observationIds,
3082
- observations: observations || [],
3083
- sources: sources || [],
3084
- });
3085
- });
3086
-
3087
- // Create relationship
3088
- app.post("/create_relationship", async (req, res) => {
3089
- const parsed = CreateRelationshipRequestSchema.safeParse(req.body);
3090
- if (!parsed.success) {
3091
- logWarn("ValidationError:create_relationship", req, {
3092
- issues: parsed.error.issues,
3093
- });
3094
- return sendValidationError(res, parsed.error.issues);
3095
- }
3096
-
3097
- const { relationship_type, source_entity_id, target_entity_id, source_id, metadata } =
3098
- parsed.data;
3099
-
3100
- const userId = "00000000-0000-0000-0000-000000000000"; // v0.1.0 single-user
3101
-
3102
- const { relationshipsService } = await import("./services/relationships.js");
3103
-
3104
- try {
3105
- const relationship = await relationshipsService.createRelationship({
3106
- relationship_type,
3107
- source_entity_id,
3108
- target_entity_id,
3109
- source_id: source_id || null,
3110
- metadata: metadata || {},
3111
- user_id: userId,
3112
- });
3113
-
3114
- logDebug("Success:create_relationship", req, {
3115
- relationship_key: relationship.relationship_key,
3116
- });
3117
- return res.json(relationship);
3118
- } catch (error) {
3119
- logError("RelationshipCreationError:create_relationship", req, error);
3120
- return sendError(
3121
- res,
3122
- 500,
3123
- "DB_QUERY_FAILED",
3124
- error instanceof Error ? error.message : "Failed to create relationship"
3125
- );
3126
- }
3127
- });
3128
-
3129
- // List relationships
3130
- app.post("/list_relationships", async (req, res) => {
3131
- const parsed = ListRelationshipsRequestSchema.safeParse(req.body);
3132
- if (!parsed.success) {
3133
- logWarn("ValidationError:list_relationships", req, {
3134
- issues: parsed.error.issues,
3135
- });
3136
- return sendValidationError(res, parsed.error.issues);
3137
- }
3138
-
3139
- const { entity_id, direction = "both", relationship_type } = parsed.data;
3140
- const normalizedDirection =
3141
- direction === "incoming" || direction === "inbound"
3142
- ? "incoming"
3143
- : direction === "outgoing" || direction === "outbound"
3144
- ? "outgoing"
3145
- : "both";
3146
-
3147
- const { relationshipsService } = await import("./services/relationships.js");
3148
-
3149
- try {
3150
- let relationships;
3151
- if (relationship_type) {
3152
- relationships = await relationshipsService.getRelationshipsByType(relationship_type as any);
3153
- // Filter by entity_id
3154
- relationships = relationships.filter(
3155
- (rel) => rel.source_entity_id === entity_id || rel.target_entity_id === entity_id
3156
- );
3157
- } else {
3158
- relationships = await relationshipsService.getRelationshipsForEntity(
3159
- entity_id,
3160
- normalizedDirection
3161
- );
3162
- }
3163
-
3164
- logDebug("Success:list_relationships", req, {
3165
- entity_id,
3166
- count: relationships.length,
3167
- });
3168
- return res.json({ relationships });
3169
- } catch (error) {
3170
- logError("RelationshipQueryError:list_relationships", req, error);
3171
- return sendError(
3172
- res,
3173
- 500,
3174
- "DB_QUERY_FAILED",
3175
- error instanceof Error ? error.message : "Failed to query relationships"
3176
- );
3177
- }
3178
- });
3179
-
3180
- app.get("/get_file_url", async (req, res) => {
3181
- const schema = z.object({
3182
- file_path: z.string(),
3183
- expires_in: z.coerce.number().optional(),
3184
- });
3185
- const parsed = schema.safeParse(req.query);
3186
- if (!parsed.success) {
3187
- logWarn("ValidationError:get_file_url", req, {
3188
- issues: parsed.error.issues,
3189
- });
3190
- return sendValidationError(res, parsed.error.issues);
3191
- }
3192
- const { file_path, expires_in } = parsed.data;
3193
-
3194
- const parts = file_path.split("/");
3195
- const bucket = parts[0];
3196
- const path = parts.slice(1).join("/");
3197
-
3198
- const { data, error } = await db.storage
3199
- .from(bucket)
3200
- .createSignedUrl(path, expires_in || 3600);
3201
- if (error || !data?.signedUrl) {
3202
- logError("StorageError:get_file_url", req, error, { bucket, path });
3203
- return sendError(res, 500, "DB_QUERY_FAILED", error?.message ?? "Failed to create signed URL");
3204
- }
3205
-
3206
- logDebug("Success:get_file_url", req, { path: file_path });
3207
- return res.json({ url: data.signedUrl });
3208
- });
3209
-
3210
- // ========== New HTTP REST endpoints for Phase 0 E2E test coverage ==========
3211
-
3212
- // POST /retrieve_entity_by_identifier - Search for entity by identifier
3213
- app.post("/retrieve_entity_by_identifier", async (req, res) => {
3214
- const parsed = RetrieveEntityByIdentifierSchema.safeParse(req.body);
3215
- if (!parsed.success) {
3216
- logWarn("ValidationError:retrieve_entity_by_identifier", req, {
3217
- issues: parsed.error.issues,
3218
- });
3219
- return sendValidationError(res, parsed.error.issues);
3220
- }
3221
-
3222
- try {
3223
- const { identifier, entity_type } = parsed.data;
3224
-
3225
- // Import normalization and ID generation functions
3226
- const { normalizeEntityValue, generateEntityId } =
3227
- await import("./services/entity_resolution.js");
3228
-
3229
- // Normalize the identifier
3230
- const normalized = entity_type
3231
- ? normalizeEntityValue(entity_type, identifier)
3232
- : identifier.trim().toLowerCase();
3233
-
3234
- // Search in entities table
3235
- let query = db
3236
- .from("entities")
3237
- .select("*")
3238
- .or(`canonical_name.ilike.%${normalized}%,aliases.cs.["${normalized}"]`);
3239
-
3240
- if (entity_type) {
3241
- query = query.eq("entity_type", entity_type);
3242
- }
3243
-
3244
- const { data: entities, error } = await query.limit(100);
3245
-
3246
- if (error) {
3247
- logError("DbError:retrieve_entity_by_identifier", req, error);
3248
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
3249
- }
3250
-
3251
- // Try entity ID match if no results
3252
- if ((!entities || entities.length === 0) && entity_type) {
3253
- const possibleId = generateEntityId(entity_type, identifier);
3254
- const { data: entityById, error: idError } = await db
3255
- .from("entities")
3256
- .select("*")
3257
- .eq("id", possibleId)
3258
- .single();
3259
-
3260
- if (!idError && entityById) {
3261
- return res.json({ entities: [entityById], total: 1 });
3262
- }
3263
- }
3264
-
3265
- logDebug("Success:retrieve_entity_by_identifier", req, { identifier, count: entities?.length });
3266
- return res.json({ entities: entities || [], total: entities?.length || 0 });
3267
- } catch (error) {
3268
- return handleApiError(
3269
- req,
3270
- res,
3271
- error,
3272
- "Failed to retrieve entity by identifier",
3273
- "DB_QUERY_FAILED",
3274
- "APIError:retrieve_entity_by_identifier"
3275
- );
3276
- }
3277
- });
3278
-
3279
- // POST /retrieve_related_entities - Get entities connected via relationships
3280
- app.post("/retrieve_related_entities", async (req, res) => {
3281
- const parsed = RetrieveRelatedEntitiesSchema.safeParse(req.body);
3282
- if (!parsed.success) {
3283
- logWarn("ValidationError:retrieve_related_entities", req, {
3284
- issues: parsed.error.issues,
3285
- });
3286
- return sendValidationError(res, parsed.error.issues);
3287
- }
3288
-
3289
- try {
3290
- const {
3291
- entity_id,
3292
- relationship_types,
3293
- direction = "both",
3294
- max_hops: _max_hops = 1,
3295
- include_entities = true,
3296
- } = parsed.data;
3297
-
3298
- // Simple 1-hop query implementation
3299
- const relationships: any[] = [];
3300
-
3301
- // Get outbound relationships
3302
- if (direction === "outbound" || direction === "both") {
3303
- let query = db
3304
- .from("relationship_snapshots")
3305
- .select("*")
3306
- .eq("source_entity_id", entity_id);
3307
-
3308
- if (relationship_types && relationship_types.length > 0) {
3309
- query = query.in("relationship_type", relationship_types);
3310
- }
3311
-
3312
- const { data, error } = await query;
3313
- if (!error && data) {
3314
- relationships.push(...data);
3315
- }
3316
- }
3317
-
3318
- // Get inbound relationships
3319
- if (direction === "inbound" || direction === "both") {
3320
- let query = db
3321
- .from("relationship_snapshots")
3322
- .select("*")
3323
- .eq("target_entity_id", entity_id);
3324
-
3325
- if (relationship_types && relationship_types.length > 0) {
3326
- query = query.in("relationship_type", relationship_types);
3327
- }
3328
-
3329
- const { data, error } = await query;
3330
- if (!error && data) {
3331
- relationships.push(...data);
3332
- }
3333
- }
3334
-
3335
- // Get entities if requested
3336
- let entities: any[] = [];
3337
- if (include_entities && relationships.length > 0) {
3338
- const relatedIds = new Set<string>();
3339
- relationships.forEach((rel) => {
3340
- if (rel.source_entity_id !== entity_id) relatedIds.add(rel.source_entity_id);
3341
- if (rel.target_entity_id !== entity_id) relatedIds.add(rel.target_entity_id);
3342
- });
3343
-
3344
- if (relatedIds.size > 0) {
3345
- const { data, error } = await db
3346
- .from("entities")
3347
- .select("*")
3348
- .in("id", Array.from(relatedIds));
3349
-
3350
- if (!error && data) {
3351
- entities = data;
3352
- }
3353
- }
3354
- }
3355
-
3356
- logDebug("Success:retrieve_related_entities", req, { entity_id, count: relationships.length });
3357
- return res.json({ relationships, entities });
3358
- } catch (error) {
3359
- return handleApiError(
3360
- req,
3361
- res,
3362
- error,
3363
- "Failed to retrieve related entities",
3364
- "DB_QUERY_FAILED",
3365
- "APIError:retrieve_related_entities"
3366
- );
3367
- }
3368
- });
3369
-
3370
- // POST /retrieve_graph_neighborhood - Get complete graph neighborhood
3371
- app.post("/retrieve_graph_neighborhood", async (req, res) => {
3372
- const parsed = RetrieveGraphNeighborhoodSchema.safeParse(req.body);
3373
- if (!parsed.success) {
3374
- logWarn("ValidationError:retrieve_graph_neighborhood", req, {
3375
- issues: parsed.error.issues,
3376
- });
3377
- return sendValidationError(res, parsed.error.issues);
3378
- }
3379
-
3380
- try {
3381
- const {
3382
- node_id,
3383
- node_type = "entity",
3384
- include_relationships = true,
3385
- include_sources = true,
3386
- include_events: _include_events = true,
3387
- include_observations = false,
3388
- } = parsed.data;
3389
-
3390
- const result: any = { node_id, node_type };
3391
-
3392
- if (node_type === "entity") {
3393
- // Get entity
3394
- const { data: entity, error: entityError } = await db
3395
- .from("entities")
3396
- .select("*")
3397
- .eq("id", node_id)
3398
- .single();
3399
-
3400
- if (!entityError && entity) {
3401
- result.entity = entity;
3402
- }
3403
-
3404
- // Get relationships if requested
3405
- if (include_relationships) {
3406
- const { data: relationships, error: relError } = await db
3407
- .from("relationship_snapshots")
3408
- .select("*")
3409
- .or(`source_entity_id.eq.${node_id},target_entity_id.eq.${node_id}`);
3410
-
3411
- if (!relError) {
3412
- result.relationships = relationships || [];
3413
- }
3414
- }
3415
-
3416
- // Get observations if requested
3417
- if (include_observations) {
3418
- const { data: observations, error: obsError } = await db
3419
- .from("observations")
3420
- .select("*")
3421
- .eq("entity_id", node_id);
3422
-
3423
- if (!obsError) {
3424
- result.observations = observations || [];
3425
- }
3426
- }
3427
-
3428
- // Get sources if requested
3429
- if (include_sources && include_observations) {
3430
- const sourceIds = result.observations?.map((o: any) => o.source_id).filter(Boolean) || [];
3431
- if (sourceIds.length > 0) {
3432
- const { data: sources, error: srcError } = await db
3433
- .from("source")
3434
- .select("*")
3435
- .in("id", sourceIds);
3436
-
3437
- if (!srcError) {
3438
- result.sources = sources || [];
3439
- }
3440
- }
3441
- }
3442
- } else if (node_type === "source") {
3443
- // Get source
3444
- const { data: source, error: srcError } = await db
3445
- .from("source")
3446
- .select("*")
3447
- .eq("id", node_id)
3448
- .single();
3449
-
3450
- if (!srcError && source) {
3451
- result.source = source;
3452
- }
3453
-
3454
- // Get observations from this source
3455
- if (include_observations) {
3456
- const { data: observations, error: obsError } = await db
3457
- .from("observations")
3458
- .select("*")
3459
- .eq("source_id", node_id);
3460
-
3461
- if (!obsError) {
3462
- result.observations = observations || [];
3463
- }
3464
- }
3465
- }
3466
-
3467
- logDebug("Success:retrieve_graph_neighborhood", req, { node_id });
3468
- return res.json(result);
3469
- } catch (error) {
3470
- return handleApiError(
3471
- req,
3472
- res,
3473
- error,
3474
- "Failed to retrieve graph neighborhood",
3475
- "DB_QUERY_FAILED",
3476
- "APIError:retrieve_graph_neighborhood"
3477
- );
3478
- }
3479
- });
3480
-
3481
- // POST /delete_entity - Soft delete entity
3482
- app.post("/delete_entity", async (req, res) => {
3483
- const parsed = DeleteEntityRequestSchema.safeParse(req.body);
3484
- if (!parsed.success) {
3485
- logWarn("ValidationError:delete_entity", req, {
3486
- issues: parsed.error.issues,
3487
- });
3488
- return sendValidationError(res, parsed.error.issues);
3489
- }
3490
-
3491
- try {
3492
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3493
- const { entity_id, entity_type, reason } = parsed.data;
3494
-
3495
- const { softDeleteEntity } = await import("./services/deletion.js");
3496
-
3497
- const result = await softDeleteEntity(entity_id, entity_type, userId, reason);
3498
-
3499
- if (!result.success) {
3500
- const status = result.error === "Entity not found" ? 404 : 500;
3501
- return sendError(res, status, "DELETE_FAILED", result.error || "Failed to delete entity");
3502
- }
3503
-
3504
- logDebug("Success:delete_entity", req, { entity_id });
3505
- return res.json({ success: true, entity_id, observation_id: result.observation_id });
3506
- } catch (error) {
3507
- return handleApiError(
3508
- req,
3509
- res,
3510
- error,
3511
- "Failed to delete entity",
3512
- "DB_QUERY_FAILED",
3513
- "APIError:delete_entity"
3514
- );
3515
- }
3516
- });
3517
-
3518
- // POST /restore_entity - Restore soft-deleted entity
3519
- app.post("/restore_entity", async (req, res) => {
3520
- const parsed = RestoreEntityRequestSchema.safeParse(req.body);
3521
- if (!parsed.success) {
3522
- logWarn("ValidationError:restore_entity", req, {
3523
- issues: parsed.error.issues,
3524
- });
3525
- return sendValidationError(res, parsed.error.issues);
3526
- }
3527
-
3528
- try {
3529
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3530
- const { entity_id, entity_type, reason } = parsed.data;
3531
-
3532
- const { restoreEntity } = await import("./services/deletion.js");
3533
-
3534
- const result = await restoreEntity(entity_id, entity_type, userId, reason);
3535
-
3536
- if (!result.success) {
3537
- return sendError(res, 500, "RESTORE_FAILED", result.error || "Failed to restore entity");
3538
- }
3539
-
3540
- logDebug("Success:restore_entity", req, { entity_id });
3541
- return res.json({ success: true, entity_id, observation_id: result.observation_id });
3542
- } catch (error) {
3543
- return handleApiError(
3544
- req,
3545
- res,
3546
- error,
3547
- "Failed to restore entity",
3548
- "DB_QUERY_FAILED",
3549
- "APIError:restore_entity"
3550
- );
3551
- }
3552
- });
3553
-
3554
- // POST /delete_relationship - Soft delete relationship
3555
- app.post("/delete_relationship", async (req, res) => {
3556
- const parsed = DeleteRelationshipRequestSchema.safeParse(req.body);
3557
- if (!parsed.success) {
3558
- logWarn("ValidationError:delete_relationship", req, {
3559
- issues: parsed.error.issues,
3560
- });
3561
- return sendValidationError(res, parsed.error.issues);
3562
- }
3563
-
3564
- try {
3565
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3566
- const { relationship_type, source_entity_id, target_entity_id, reason } = parsed.data;
3567
-
3568
- const { softDeleteRelationship } = await import("./services/deletion.js");
3569
-
3570
- // Construct relationship key
3571
- const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
3572
-
3573
- const result = await softDeleteRelationship(
3574
- relationshipKey,
3575
- relationship_type,
3576
- source_entity_id,
3577
- target_entity_id,
3578
- userId,
3579
- reason
3580
- );
3581
-
3582
- if (!result.success) {
3583
- return sendError(res, 500, "DELETE_FAILED", result.error || "Failed to delete relationship");
3584
- }
3585
-
3586
- logDebug("Success:delete_relationship", req, {
3587
- relationship_type,
3588
- source_entity_id,
3589
- target_entity_id,
3590
- });
3591
- return res.json({ success: true, observation_id: result.observation_id });
3592
- } catch (error) {
3593
- return handleApiError(
3594
- req,
3595
- res,
3596
- error,
3597
- "Failed to delete relationship",
3598
- "DB_QUERY_FAILED",
3599
- "APIError:delete_relationship"
3600
- );
3601
- }
3602
- });
3603
-
3604
- // POST /restore_relationship - Restore soft-deleted relationship
3605
- app.post("/restore_relationship", async (req, res) => {
3606
- const parsed = RestoreRelationshipRequestSchema.safeParse(req.body);
3607
- if (!parsed.success) {
3608
- logWarn("ValidationError:restore_relationship", req, {
3609
- issues: parsed.error.issues,
3610
- });
3611
- return sendValidationError(res, parsed.error.issues);
3612
- }
3613
-
3614
- try {
3615
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3616
- const { relationship_type, source_entity_id, target_entity_id, reason } = parsed.data;
3617
-
3618
- const { restoreRelationship } = await import("./services/deletion.js");
3619
-
3620
- // Construct relationship key
3621
- const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
3622
-
3623
- const result = await restoreRelationship(
3624
- relationshipKey,
3625
- relationship_type,
3626
- source_entity_id,
3627
- target_entity_id,
3628
- userId,
3629
- reason
3630
- );
3631
-
3632
- if (!result.success) {
3633
- return sendError(
3634
- res,
3635
- 500,
3636
- "RESTORE_FAILED",
3637
- result.error || "Failed to restore relationship"
3638
- );
3639
- }
3640
-
3641
- logDebug("Success:restore_relationship", req, {
3642
- relationship_type,
3643
- source_entity_id,
3644
- target_entity_id,
3645
- });
3646
- return res.json({ success: true, observation_id: result.observation_id });
3647
- } catch (error) {
3648
- return handleApiError(
3649
- req,
3650
- res,
3651
- error,
3652
- "Failed to restore relationship",
3653
- "DB_QUERY_FAILED",
3654
- "APIError:restore_relationship"
3655
- );
3656
- }
3657
- });
3658
-
3659
- // POST /analyze_schema_candidates - Analyze raw fragments for schema promotion
3660
- app.post("/analyze_schema_candidates", async (req, res) => {
3661
- const parsed = AnalyzeSchemaCandidatesRequestSchema.safeParse(req.body);
3662
- if (!parsed.success) {
3663
- logWarn("ValidationError:analyze_schema_candidates", req, {
3664
- issues: parsed.error.issues,
3665
- });
3666
- return sendValidationError(res, parsed.error.issues);
3667
- }
3668
-
3669
- try {
3670
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3671
- const { entity_type, min_frequency = 5, min_confidence = 0.8 } = parsed.data;
3672
-
3673
- // Query raw_fragments for field frequency analysis
3674
- let query = db
3675
- .from("raw_fragments")
3676
- .select("fragment_key, frequency_count, entity_type");
3677
-
3678
- if (entity_type) {
3679
- query = query.eq("entity_type", entity_type);
3680
- }
3681
-
3682
- if (userId) {
3683
- query = query.eq("user_id", userId);
3684
- }
3685
-
3686
- const { data: fragments, error } = await query;
3687
-
3688
- if (error) {
3689
- logError("DbError:analyze_schema_candidates", req, error);
3690
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
3691
- }
3692
-
3693
- // Analyze field frequency
3694
- const fieldFrequency = new Map<string, number>();
3695
- (fragments || []).forEach((frag: any) => {
3696
- const key = String(frag.fragment_key ?? "");
3697
- if (!key) return;
3698
- const increment =
3699
- typeof frag.frequency_count === "number" && Number.isFinite(frag.frequency_count)
3700
- ? frag.frequency_count
3701
- : 1;
3702
- const count = fieldFrequency.get(key) ?? 0;
3703
- fieldFrequency.set(key, count + increment);
3704
- });
3705
-
3706
- // Build candidates
3707
- const candidates = Array.from(fieldFrequency.entries())
3708
- .filter(([, count]) => count >= min_frequency)
3709
- .map(([field_name, frequency]) => {
3710
- const confidence = Math.min(frequency / 100, 1);
3711
- const recommendedType =
3712
- /\b(date|dated|dob)\b/i.test(field_name) || /_at$/i.test(field_name)
3713
- ? "date"
3714
- : "string";
3715
- return {
3716
- field_name,
3717
- frequency,
3718
- confidence,
3719
- recommended_type: recommendedType,
3720
- };
3721
- })
3722
- .filter((c) => c.confidence >= min_confidence)
3723
- .sort((a, b) => {
3724
- if (b.frequency !== a.frequency) return b.frequency - a.frequency;
3725
- return a.field_name.localeCompare(b.field_name);
3726
- });
3727
-
3728
- logDebug("Success:analyze_schema_candidates", req, { entity_type, count: candidates.length });
3729
- return res.json({ candidates });
3730
- } catch (error) {
3731
- return handleApiError(
3732
- req,
3733
- res,
3734
- error,
3735
- "Failed to analyze schema candidates",
3736
- "DB_QUERY_FAILED",
3737
- "APIError:analyze_schema_candidates"
3738
- );
3739
- }
3740
- });
3741
-
3742
- // POST /get_schema_recommendations - Get schema update recommendations
3743
- app.post("/get_schema_recommendations", async (req, res) => {
3744
- const parsed = GetSchemaRecommendationsRequestSchema.safeParse(req.body);
3745
- if (!parsed.success) {
3746
- logWarn("ValidationError:get_schema_recommendations", req, {
3747
- issues: parsed.error.issues,
3748
- });
3749
- return sendValidationError(res, parsed.error.issues);
3750
- }
3751
-
3752
- try {
3753
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3754
- const { entity_type, source, status } = parsed.data;
3755
-
3756
- // Query schema_recommendations table
3757
- let query = db.from("schema_recommendations").select("*").eq("entity_type", entity_type);
3758
-
3759
- if (userId) {
3760
- query = query.eq("user_id", userId);
3761
- }
3762
-
3763
- if (source && source !== "all") {
3764
- query = query.eq("source", source);
3765
- }
3766
-
3767
- if (status) {
3768
- query = query.eq("status", status);
3769
- }
3770
-
3771
- const { data: recommendations, error } = await query
3772
- .order("confidence_score", { ascending: false })
3773
- .order("created_at", { ascending: false });
3774
-
3775
- if (error) {
3776
- logError("DbError:get_schema_recommendations", req, error);
3777
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
3778
- }
3779
-
3780
- logDebug("Success:get_schema_recommendations", req, {
3781
- entity_type,
3782
- count: recommendations?.length,
3783
- });
3784
- return res.json({ recommendations: recommendations || [] });
3785
- } catch (error) {
3786
- return handleApiError(
3787
- req,
3788
- res,
3789
- error,
3790
- "Failed to get schema recommendations",
3791
- "DB_QUERY_FAILED",
3792
- "APIError:get_schema_recommendations"
3793
- );
3794
- }
3795
- });
3796
-
3797
- // POST /update_schema_incremental - Incrementally update schema with new fields
3798
- app.post("/update_schema_incremental", async (req, res) => {
3799
- const parsed = UpdateSchemaIncrementalRequestSchema.safeParse(req.body);
3800
- if (!parsed.success) {
3801
- logWarn("ValidationError:update_schema_incremental", req, {
3802
- issues: parsed.error.issues,
3803
- });
3804
- return sendValidationError(res, parsed.error.issues);
3805
- }
3806
-
3807
- try {
3808
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3809
- const {
3810
- entity_type,
3811
- fields_to_add,
3812
- schema_version,
3813
- user_specific = false,
3814
- activate = true,
3815
- } = parsed.data;
3816
-
3817
- // Get current schema
3818
- let query = db
3819
- .from("schema_registry")
3820
- .select("*")
3821
- .eq("entity_type", entity_type)
3822
- .eq("active", true);
3823
-
3824
- if (user_specific && userId) {
3825
- query = query.eq("user_id", userId);
3826
- } else {
3827
- query = query.is("user_id", null);
3828
- }
3829
-
3830
- const { data: currentSchema, error: fetchError } = await query.single();
3831
-
3832
- if (fetchError && fetchError.code !== "PGRST116") {
3833
- logError("DbError:update_schema_incremental", req, fetchError);
3834
- return sendError(res, 500, "DB_QUERY_FAILED", fetchError.message);
3835
- }
3836
-
3837
- // Build new schema definition
3838
- const baseSchema = currentSchema?.schema_definition || { fields: {} };
3839
- const newFields: Record<string, any> = {};
3840
-
3841
- fields_to_add.forEach((field: any) => {
3842
- newFields[field.field_name] = {
3843
- type: field.field_type,
3844
- required: field.required || false,
3845
- reducer_strategy: field.reducer_strategy || "highest_priority",
3846
- };
3847
- });
3848
-
3849
- const updatedSchema = {
3850
- ...baseSchema,
3851
- fields: { ...baseSchema.fields, ...newFields },
3852
- };
3853
-
3854
- // Insert new schema version
3855
- const { data: newSchemaVersion, error: insertError } = await db
3856
- .from("schema_registry")
3857
- .insert({
3858
- entity_type,
3859
- schema_version: schema_version || `${parseInt(currentSchema?.schema_version || "1") + 1}`,
3860
- schema_definition: updatedSchema,
3861
- reducer_config: currentSchema?.reducer_config || {},
3862
- user_id: user_specific ? userId : null,
3863
- active: activate,
3864
- created_at: new Date().toISOString(),
3865
- })
3866
- .select()
3867
- .single();
3868
-
3869
- if (insertError) {
3870
- logError("DbError:update_schema_incremental", req, insertError);
3871
- return sendError(res, 500, "DB_QUERY_FAILED", insertError.message);
3872
- }
3873
-
3874
- // Deactivate old schema if activating new one
3875
- if (activate && currentSchema) {
3876
- await db.from("schema_registry").update({ active: false }).eq("id", currentSchema.id);
3877
- }
3878
-
3879
- logDebug("Success:update_schema_incremental", req, {
3880
- entity_type,
3881
- fields_added: fields_to_add.length,
3882
- });
3883
- return res.json({
3884
- success: true,
3885
- schema: newSchemaVersion,
3886
- schema_version: newSchemaVersion.schema_version,
3887
- });
3888
- } catch (error) {
3889
- return handleApiError(
3890
- req,
3891
- res,
3892
- error,
3893
- "Failed to update schema incrementally",
3894
- "DB_QUERY_FAILED",
3895
- "APIError:update_schema_incremental"
3896
- );
3897
- }
3898
- });
3899
-
3900
- // POST /register_schema - Register new schema or schema version
3901
- app.post("/register_schema", async (req, res) => {
3902
- const parsed = RegisterSchemaRequestSchema.safeParse(req.body);
3903
- if (!parsed.success) {
3904
- logWarn("ValidationError:register_schema", req, {
3905
- issues: parsed.error.issues,
3906
- });
3907
- return sendValidationError(res, parsed.error.issues);
3908
- }
3909
-
3910
- try {
3911
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3912
- const {
3913
- entity_type,
3914
- schema_definition,
3915
- reducer_config,
3916
- schema_version = "1.0",
3917
- user_specific = false,
3918
- activate = false,
3919
- } = parsed.data;
3920
-
3921
- // Insert new schema
3922
- const { data: newSchema, error } = await db
3923
- .from("schema_registry")
3924
- .insert({
3925
- entity_type,
3926
- schema_version,
3927
- schema_definition,
3928
- reducer_config,
3929
- user_id: user_specific ? userId : null,
3930
- active: activate,
3931
- created_at: new Date().toISOString(),
3932
- })
3933
- .select()
3934
- .single();
3935
-
3936
- if (error) {
3937
- logError("DbError:register_schema", req, error);
3938
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
3939
- }
3940
-
3941
- // If activating, deactivate other schemas for this entity type
3942
- if (activate) {
3943
- await db
3944
- .from("schema_registry")
3945
- .update({ active: false })
3946
- .eq("entity_type", entity_type)
3947
- .not("id", "eq", newSchema.id);
3948
-
3949
- if (user_specific) {
3950
- await db.from("schema_registry").update({ active: false }).eq("user_id", userId);
3951
- }
3952
- }
3953
-
3954
- logDebug("Success:register_schema", req, { entity_type, schema_version });
3955
- return res.json({
3956
- success: true,
3957
- schema: newSchema,
3958
- schema_version: newSchema.schema_version,
3959
- });
3960
- } catch (error) {
3961
- return handleApiError(
3962
- req,
3963
- res,
3964
- error,
3965
- "Failed to register schema",
3966
- "DB_QUERY_FAILED",
3967
- "APIError:register_schema"
3968
- );
3969
- }
3970
- });
3971
-
3972
- async function runReinterpretForSource(
3973
- sourceId: string,
3974
- interpretationConfig: Record<string, unknown> | undefined,
3975
- userId?: string
3976
- ): Promise<{ interpretationId: string; observationsCreated: number; userId: string }> {
3977
- // Get source
3978
- let sourceQuery = db.from("sources").select("*").eq("id", sourceId);
3979
- if (userId) {
3980
- sourceQuery = sourceQuery.eq("user_id", userId);
3981
- }
3982
- const { data: source, error: srcError } = await sourceQuery.single();
3983
-
3984
- if (srcError || !source) {
3985
- throw new Error("Source not found");
3986
- }
3987
-
3988
- // Re-extract from source.raw_text based on mime_type
3989
- let extractedData: Array<Record<string, unknown>> = [];
3990
- if (typeof source.raw_text === "string" && source.raw_text.trim().length > 0) {
3991
- const mimeType = source.mime_type || "text/plain";
3992
- const fileName = source.original_filename;
3993
-
3994
- // Import extraction functions
3995
- const { extractWithLLM } = await import("./services/llm_extraction.js");
3996
-
3997
- // Get model ID from config or use default
3998
- const modelId =
3999
- interpretationConfig && typeof interpretationConfig.model_id === "string"
4000
- ? interpretationConfig.model_id
4001
- : "gpt-4o";
4002
-
4003
- // Handle CSV files with chunking support
4004
- if (mimeType === "text/csv" || fileName?.toLowerCase().endsWith(".csv")) {
4005
- const { extractEntitiesFromCsvRows } = await import("./services/csv_row_extraction.js");
4006
- extractedData = extractEntitiesFromCsvRows(Buffer.from(source.raw_text, "utf8"), fileName);
4007
- } else {
4008
- // Non-CSV files - use standard extraction
4009
- const extractionResult = await extractWithLLM(source.raw_text, fileName, mimeType, modelId);
4010
- const { entity_type, fields } = extractionResult;
4011
- extractedData = [
4012
- {
4013
- entity_type,
4014
- ...fields,
4015
- },
4016
- ];
4017
- }
4018
- }
4019
-
4020
- const { runInterpretation } = await import("./services/interpretation.js");
4021
- const result = await runInterpretation({
4022
- userId: source.user_id,
4023
- sourceId,
4024
- extractedData,
4025
- config: (interpretationConfig || {}) as any,
4026
- });
4027
-
4028
- return {
4029
- interpretationId: result.interpretationId,
4030
- observationsCreated: result.observationsCreated,
4031
- userId: source.user_id,
4032
- };
4033
- }
4034
-
4035
- async function listUninterpretedSourceIds(userId: string, limit: number): Promise<string[]> {
4036
- const interpretedSet = new Set<string>();
4037
- const { data: interpretedData, error: interpretedError } = await db
4038
- .from("interpretations")
4039
- .select("source_id")
4040
- .eq("user_id", userId);
4041
- if (interpretedError) {
4042
- throw interpretedError;
4043
- }
4044
- for (const row of interpretedData || []) {
4045
- if (row.source_id) interpretedSet.add(row.source_id);
4046
- }
4047
-
4048
- const pageSize = Math.max(100, limit * 3);
4049
- const sourceIds: string[] = [];
4050
- let offset = 0;
4051
-
4052
- while (sourceIds.length < limit) {
4053
- const { data: sourcePage, error: sourceError } = await db
4054
- .from("sources")
4055
- .select("id")
4056
- .eq("user_id", userId)
4057
- .order("created_at", { ascending: true })
4058
- .range(offset, offset + pageSize - 1);
4059
-
4060
- if (sourceError) {
4061
- throw sourceError;
4062
- }
4063
- if (!sourcePage || sourcePage.length === 0) {
4064
- break;
4065
- }
4066
-
4067
- for (const source of sourcePage) {
4068
- if (sourceIds.length >= limit) break;
4069
- if (interpretedSet.has(source.id)) continue;
4070
- sourceIds.push(source.id);
4071
- }
4072
-
4073
- if (sourcePage.length < pageSize) {
4074
- break;
4075
- }
4076
- offset += sourcePage.length;
4077
- }
4078
-
4079
- return sourceIds;
4080
- }
4081
-
4082
- // POST /reinterpret - Re-run AI interpretation on existing source
4083
- app.post("/reinterpret", async (req, res) => {
4084
- const parsed = ReinterpretRequestSchema.safeParse(req.body);
4085
- if (!parsed.success) {
4086
- logWarn("ValidationError:reinterpret", req, {
4087
- issues: parsed.error.issues,
4088
- });
4089
- return sendValidationError(res, parsed.error.issues);
4090
- }
4091
-
4092
- try {
4093
- let { source_id } = parsed.data;
4094
- const { interpretation_config, interpretation_id } = parsed.data;
4095
-
4096
- // Look up source_id from interpretation if not provided directly
4097
- if (!source_id && interpretation_id) {
4098
- const { data: interp, error: interpError } = await db
4099
- .from("interpretations")
4100
- .select("source_id")
4101
- .eq("id", interpretation_id)
4102
- .maybeSingle();
4103
- if (interpError || !interp) {
4104
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Interpretation not found");
4105
- }
4106
- source_id = interp.source_id;
4107
- }
4108
-
4109
- if (!source_id) {
4110
- return sendError(res, 400, "VALIDATION_ERROR", "source_id is required");
4111
- }
4112
-
4113
- const result = await runReinterpretForSource(source_id, interpretation_config);
4114
- logDebug("Success:reinterpret", req, { source_id });
4115
- return res.json({
4116
- success: true,
4117
- interpretation_id: result.interpretationId,
4118
- observations_created: result.observationsCreated,
4119
- });
4120
- } catch (error) {
4121
- if (error instanceof Error && error.message === "Source not found") {
4122
- return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
4123
- }
4124
- return handleApiError(
4125
- req,
4126
- res,
4127
- error,
4128
- "Failed to reinterpret source",
4129
- "DB_QUERY_FAILED",
4130
- "APIError:reinterpret"
4131
- );
4132
- }
4133
- });
4134
-
4135
- // POST /interpret-uninterpreted - Re-run interpretation for sources without prior interpretations
4136
- app.post("/interpret-uninterpreted", async (req, res) => {
4137
- const parsed = InterpretUninterpretedRequestSchema.safeParse(req.body);
4138
- if (!parsed.success) {
4139
- logWarn("ValidationError:interpret_uninterpreted", req, {
4140
- issues: parsed.error.issues,
4141
- });
4142
- return sendValidationError(res, parsed.error.issues);
4143
- }
4144
-
4145
- try {
4146
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
4147
- const limit = parsed.data.limit ?? 50;
4148
- const dryRun = parsed.data.dry_run ?? false;
4149
- const interpretationConfig = parsed.data.interpretation_config;
4150
- const sourceIds = await listUninterpretedSourceIds(userId, limit);
4151
-
4152
- if (dryRun) {
4153
- return res.json({
4154
- dry_run: true,
4155
- count: sourceIds.length,
4156
- would_interpret: sourceIds,
4157
- });
4158
- }
4159
-
4160
- const interpreted: Array<{
4161
- source_id: string;
4162
- interpretation_id: string;
4163
- observations_created: number;
4164
- }> = [];
4165
- const errors: Array<{ source_id: string; error: string }> = [];
4166
-
4167
- for (const sourceId of sourceIds) {
4168
- try {
4169
- const result = await runReinterpretForSource(sourceId, interpretationConfig, userId);
4170
- interpreted.push({
4171
- source_id: sourceId,
4172
- interpretation_id: result.interpretationId,
4173
- observations_created: result.observationsCreated,
4174
- });
4175
- } catch (error) {
4176
- const message =
4177
- error instanceof Error && error.message.trim().length > 0
4178
- ? error.message
4179
- : "Failed to reinterpret source";
4180
- errors.push({ source_id: sourceId, error: message });
4181
- }
4182
- }
4183
-
4184
- logDebug("Success:interpret_uninterpreted", req, {
4185
- user_id: userId,
4186
- processed: interpreted.length,
4187
- errors: errors.length,
4188
- limit,
4189
- });
4190
- return res.json({
4191
- dry_run: false,
4192
- count: interpreted.length,
4193
- interpreted,
4194
- errors,
4195
- });
4196
- } catch (error) {
4197
- return handleApiError(
4198
- req,
4199
- res,
4200
- error,
4201
- "Failed to interpret uninterpreted sources",
4202
- "DB_QUERY_FAILED",
4203
- "APIError:interpret_uninterpreted"
4204
- );
4205
- }
4206
- });
4207
-
4208
- // POST /correct - Create correction observation to override field value
4209
- app.post("/correct", async (req, res) => {
4210
- const parsed = CorrectEntityRequestSchema.safeParse(req.body);
4211
- if (!parsed.success) {
4212
- logWarn("ValidationError:correct", req, {
4213
- issues: parsed.error.issues,
4214
- });
4215
- return sendValidationError(res, parsed.error.issues);
4216
- }
4217
-
4218
- try {
4219
- const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
4220
- const { entity_id, entity_type, field, value, idempotency_key } = parsed.data;
4221
-
4222
- const { createCorrection } = await import("./services/correction.js");
4223
- const result = await createCorrection({
4224
- entity_id,
4225
- entity_type,
4226
- field,
4227
- value,
4228
- schema_version: "1.0",
4229
- user_id: userId,
4230
- idempotency_key,
4231
- });
4232
-
4233
- logDebug("Success:correct", req, { entity_id, field });
4234
- return res.json({ success: true, observation_id: result.observation_id, snapshot: result.snapshot });
4235
- } catch (error) {
4236
- return handleApiError(
4237
- req,
4238
- res,
4239
- error,
4240
- "Failed to create correction",
4241
- "DB_QUERY_FAILED",
4242
- "APIError:correct"
4243
- );
4244
- }
4245
- });
4246
-
4247
- // POST /get_authenticated_user - Get authenticated user ID
4248
- app.post("/get_authenticated_user", async (req, res) => {
4249
- try {
4250
- const userId = await getAuthenticatedUserId(req, undefined);
4251
- const storage =
4252
- config.storageBackend === "local"
4253
- ? {
4254
- storage_backend: "local" as const,
4255
- data_dir: config.dataDir,
4256
- sqlite_db: config.sqlitePath,
4257
- }
4258
- : undefined;
4259
-
4260
- logDebug("Success:get_authenticated_user", req, { user_id: userId });
4261
- return res.json({ user_id: userId, storage });
4262
- } catch (error) {
4263
- return handleApiError(
4264
- req,
4265
- res,
4266
- error,
4267
- "Failed to get authenticated user",
4268
- "AUTH_REQUIRED",
4269
- "APIError:get_authenticated_user"
4270
- );
4271
- }
4272
- });
4273
-
4274
- // POST /health_check_snapshots - Check for stale entity snapshots
4275
- app.post("/health_check_snapshots", async (req, res) => {
4276
- const schema = z.object({
4277
- auto_fix: z.boolean().optional().default(false),
4278
- });
4279
- const parsed = schema.safeParse(req.body);
4280
- if (!parsed.success) {
4281
- logWarn("ValidationError:health_check_snapshots", req, {
4282
- issues: parsed.error.issues,
4283
- });
4284
- return sendValidationError(res, parsed.error.issues);
4285
- }
4286
-
4287
- try {
4288
- const { auto_fix } = parsed.data;
4289
-
4290
- // Query for stale snapshots (observation_count=0 but observations exist)
4291
- const { data: staleSnapshots, error } = await db
4292
- .from("entity_snapshots")
4293
- .select("entity_id, entity_type, observation_count")
4294
- .eq("observation_count", 0);
4295
-
4296
- if (error) {
4297
- logError("DbError:health_check_snapshots", req, error);
4298
- return sendError(res, 500, "DB_QUERY_FAILED", error.message);
4299
- }
4300
-
4301
- // Check if these entities actually have observations
4302
- const staleEntities = [];
4303
- for (const snapshot of staleSnapshots || []) {
4304
- const { data: observations, error: obsError } = await db
4305
- .from("observations")
4306
- .select("id")
4307
- .eq("entity_id", snapshot.entity_id)
4308
- .limit(1);
4309
-
4310
- if (!obsError && observations && observations.length > 0) {
4311
- staleEntities.push(snapshot);
4312
- }
4313
- }
4314
-
4315
- let fixedCount = 0;
4316
- if (auto_fix && staleEntities.length > 0) {
4317
- // Recompute snapshots for stale entities using observationReducer
4318
- const { observationReducer } = await import("./reducers/observation_reducer.js");
4319
-
4320
- for (const entity of staleEntities) {
4321
- try {
4322
- // Get all observations for this entity
4323
- const { data: observations } = await db
4324
- .from("observations")
4325
- .select("*")
4326
- .eq("entity_id", entity.entity_id)
4327
- .order("observed_at", { ascending: false });
4328
-
4329
- if (!observations || observations.length === 0) continue;
4330
-
4331
- // Recompute snapshot
4332
- const newSnapshot = await observationReducer.computeSnapshot(
4333
- entity.entity_id,
4334
- observations as any
4335
- );
4336
- if (!newSnapshot) continue;
4337
-
4338
- const rowWithEmbedding = await prepareEntitySnapshotWithEmbedding({
4339
- entity_id: newSnapshot.entity_id,
4340
- entity_type: newSnapshot.entity_type,
4341
- schema_version: newSnapshot.schema_version,
4342
- snapshot: newSnapshot.snapshot,
4343
- computed_at: newSnapshot.computed_at,
4344
- observation_count: newSnapshot.observation_count,
4345
- last_observation_at: newSnapshot.last_observation_at,
4346
- provenance: newSnapshot.provenance,
4347
- user_id: newSnapshot.user_id,
4348
- });
4349
- await upsertEntitySnapshotWithEmbedding(rowWithEmbedding);
4350
-
4351
- fixedCount++;
4352
- } catch (error) {
4353
- console.error(`Failed to fix snapshot for ${entity.entity_id}:`, error);
4354
- }
4355
- }
4356
- }
4357
-
4358
- logDebug("Success:health_check_snapshots", req, {
4359
- stale_count: staleEntities.length,
4360
- auto_fix,
4361
- });
4362
- return res.json({
4363
- healthy: staleEntities.length === 0,
4364
- message:
4365
- staleEntities.length === 0
4366
- ? "All snapshots healthy"
4367
- : auto_fix
4368
- ? `Found ${staleEntities.length} stale snapshots, fixed ${fixedCount}`
4369
- : `Found ${staleEntities.length} stale snapshots`,
4370
- checked: staleSnapshots?.length || 0,
4371
- stale: staleEntities.length,
4372
- fixed: auto_fix ? fixedCount : undefined,
4373
- stale_snapshots: staleEntities,
4374
- });
4375
- } catch (error) {
4376
- return handleApiError(
4377
- req,
4378
- res,
4379
- error,
4380
- "Failed to check snapshot health",
4381
- "DB_QUERY_FAILED",
4382
- "APIError:health_check_snapshots"
4383
- );
4384
- }
4385
- });
4386
-
4387
- // ========== End of Phase 0 endpoints ==========
4388
-
4389
- // Chat endpoint removed - violates Application Layer constraint "MUST NOT contain conversational logic"
4390
- // Conversational interactions should be externalized to MCP-compatible agents per architecture
4391
-
4392
- app.get("/openapi.yaml", (req, res) => {
4393
- const openApiPath = path.join(process.cwd(), "openapi.yaml");
4394
- const openApiContent = fs.readFileSync(openApiPath, "utf-8");
4395
- const spec = yaml.load(openApiContent) as { servers?: Array<{ url: string; description?: string }> };
4396
- const baseUrl = (config.apiBase || "").replace(/\/$/, "");
4397
- if (spec.servers?.length && baseUrl) {
4398
- spec.servers[0] = { ...spec.servers[0], url: baseUrl };
4399
- }
4400
- res.setHeader("Content-Type", "application/yaml");
4401
- res.send(yaml.dump(spec, { lineWidth: -1 }));
4402
- });
4403
-
4404
- // Documentation routes (FU-301) - must be before SPA fallback
4405
- // setupDocumentationRoutes(app); // TODO: Re-enable after implementing routes/documentation.ts
4406
-
4407
- // SPA fallback - serve index.html for non-API routes (must be after all API routes)
4408
-
4409
- /** Try to bind on a port; resolves with server and port, or rejects on error (e.g. EADDRINUSE). */
4410
- function tryListen(
4411
- port: number
4412
- ): Promise<{ server: ReturnType<express.Express["listen"]>; port: number }> {
4413
- return new Promise((resolve, reject) => {
4414
- const server = app.listen(port, () => {
4415
- resolve({ server, port });
4416
- });
4417
- server.once("error", (err: NodeJS.ErrnoException) => {
4418
- server.close();
4419
- reject(err);
4420
- });
4421
- });
4422
- }
4423
-
4424
- // Export function to start HTTP server (called explicitly, not on import)
4425
- export async function startHTTPServer() {
4426
- // Initialize encryption service
4427
- await initServerKeys();
4428
-
4429
- const httpPortEnv = process.env.NEOTOMA_HTTP_PORT || process.env.HTTP_PORT;
4430
- const basePort = httpPortEnv
4431
- ? parseInt(httpPortEnv, 10)
4432
- : config.httpPort || 8080;
4433
- const portFile = process.env.NEOTOMA_SESSION_PORT_FILE;
4434
- const maxTries = 20;
4435
-
4436
- for (let offset = 0; offset < maxTries; offset++) {
4437
- const port = basePort + offset;
4438
- try {
4439
- const { server } = await tryListen(port);
4440
- if (portFile) {
4441
- fs.writeFileSync(portFile, String(port), "utf-8");
4442
- }
4443
- // eslint-disable-next-line no-console
4444
- console.log(`HTTP Actions listening on :${port}`);
4445
-
4446
- // Start background OAuth state cleanup job
4447
- import("./services/mcp_oauth.js").then((oauth) => {
4448
- oauth.startStateCleanupJob();
4449
- });
4450
- return { server, port };
4451
- } catch (err: unknown) {
4452
- const code = (err as NodeJS.ErrnoException)?.code;
4453
- if (code === "EADDRINUSE" && offset < maxTries - 1) {
4454
- continue;
4455
- }
4456
- throw err;
4457
- }
4458
- }
4459
- }
4460
-
4461
- // Only auto-start if not disabled AND if this is the main module
4462
- const isMainModule = import.meta.url === `file://${process.argv[1]}`;
4463
- if (process.env.NEOTOMA_ACTIONS_DISABLE_AUTOSTART !== "1" && isMainModule) {
4464
- startHTTPServer().catch((err) => {
4465
- console.error("Failed to start HTTP server:", err);
4466
- process.exit(1);
4467
- });
4468
- }