monkeybrain-runtime 1.0.0__py3-none-any.whl

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 (838) hide show
  1. monkeybrain_runtime-1.0.0.dist-info/METADATA +76 -0
  2. monkeybrain_runtime-1.0.0.dist-info/RECORD +838 -0
  3. monkeybrain_runtime-1.0.0.dist-info/WHEEL +5 -0
  4. monkeybrain_runtime-1.0.0.dist-info/entry_points.txt +3 -0
  5. monkeybrain_runtime-1.0.0.dist-info/top_level.txt +2 -0
  6. services/__init__.py +8 -0
  7. services/agentos/__init__.py +0 -0
  8. services/agentos/main.py +1 -0
  9. services/assets/helpers/__init__.py +12 -0
  10. services/assets/helpers/device.py +59 -0
  11. services/assets/helpers/equipment.py +179 -0
  12. services/assets/helpers/instruments.py +72 -0
  13. services/assets/helpers/machines.py +183 -0
  14. services/assets/helpers/materials.py +76 -0
  15. services/assets/helpers/parts.py +116 -0
  16. services/assets/helpers/plc.py +134 -0
  17. services/assets/helpers/tags.py +108 -0
  18. services/assets/helpers/tools.py +101 -0
  19. services/assets/main.py +75 -0
  20. services/assets/models/__init__.py +12 -0
  21. services/assets/models/device.py +79 -0
  22. services/assets/models/equipment.py +222 -0
  23. services/assets/models/instruments.py +85 -0
  24. services/assets/models/machines.py +230 -0
  25. services/assets/models/material.py +266 -0
  26. services/assets/models/parts.py +96 -0
  27. services/assets/models/plc.py +264 -0
  28. services/assets/models/tags.py +76 -0
  29. services/assets/models/tools.py +179 -0
  30. services/assets/routers/__init__.py +12 -0
  31. services/assets/routers/classes.py +65 -0
  32. services/assets/routers/device.py +86 -0
  33. services/assets/routers/equipment.py +145 -0
  34. services/assets/routers/families.py +61 -0
  35. services/assets/routers/instruments.py +70 -0
  36. services/assets/routers/machines.py +136 -0
  37. services/assets/routers/materials.py +105 -0
  38. services/assets/routers/parts.py +130 -0
  39. services/assets/routers/plc.py +94 -0
  40. services/assets/routers/subclasses.py +68 -0
  41. services/assets/routers/tags.py +138 -0
  42. services/assets/routers/tools.py +113 -0
  43. services/auth/helpers/__init__.py +13 -0
  44. services/auth/helpers/approval_decisions.py +261 -0
  45. services/auth/helpers/audit_elasticsearch_sync.py +350 -0
  46. services/auth/helpers/departments.py +53 -0
  47. services/auth/helpers/graph_store.py +848 -0
  48. services/auth/helpers/influx_store.py +280 -0
  49. services/auth/helpers/me.py +33 -0
  50. services/auth/helpers/nats_consumer.py +618 -0
  51. services/auth/helpers/nats_store.py +242 -0
  52. services/auth/helpers/permissions.py +62 -0
  53. services/auth/helpers/roles.py +87 -0
  54. services/auth/helpers/store.py +54 -0
  55. services/auth/helpers/team_members.py +155 -0
  56. services/auth/helpers/teams.py +87 -0
  57. services/auth/helpers/tokens.py +71 -0
  58. services/auth/helpers/users.py +119 -0
  59. services/auth/helpers/websocket_broadcast.py +41 -0
  60. services/auth/main.py +88 -0
  61. services/auth/models/__init__.py +12 -0
  62. services/auth/models/departments.py +55 -0
  63. services/auth/models/login.py +20 -0
  64. services/auth/models/permissions.py +61 -0
  65. services/auth/models/roles.py +53 -0
  66. services/auth/models/session.py +26 -0
  67. services/auth/models/teamMembers.py +59 -0
  68. services/auth/models/teams.py +56 -0
  69. services/auth/models/users.py +77 -0
  70. services/auth/routers/__init__.py +14 -0
  71. services/auth/routers/auth.py +3839 -0
  72. services/auth/routers/departments.py +84 -0
  73. services/auth/routers/integration_config.py +703 -0
  74. services/auth/routers/me.py +28 -0
  75. services/auth/routers/permissions.py +96 -0
  76. services/auth/routers/roles.py +139 -0
  77. services/auth/routers/session.py +224 -0
  78. services/auth/routers/team_members.py +152 -0
  79. services/auth/routers/teams.py +112 -0
  80. services/auth/routers/users.py +131 -0
  81. services/auth/routers/websocket_events.py +247 -0
  82. services/batch_execution/__init__.py +19 -0
  83. services/batch_execution/pipeline_triggers/__init__.py +53 -0
  84. services/batch_execution/pipeline_triggers/pipeline_parameter_adjuster.py +440 -0
  85. services/batch_execution/pipeline_triggers/pipeline_trigger_engine.py +445 -0
  86. services/batch_execution/pipeline_triggers/re_evaluation_queue.py +341 -0
  87. services/batch_execution/pipeline_triggers/test_phase3_implementation.py +553 -0
  88. services/batch_execution/pipeline_triggers/workflow_reevaluator.py +367 -0
  89. services/batch_execution/smoke_test_e2e_feedback_loop.py +704 -0
  90. services/batch_execution/workflow_executor.py +478 -0
  91. services/changeover/helpers/__init__.py +11 -0
  92. services/changeover/helpers/changeover.py +87 -0
  93. services/changeover/helpers/changeover_common.py +55 -0
  94. services/changeover/helpers/changeover_events.py +66 -0
  95. services/changeover/helpers/changeover_kpis.py +33 -0
  96. services/changeover/helpers/changeover_matrix.py +60 -0
  97. services/changeover/helpers/changeover_procedures.py +164 -0
  98. services/changeover/helpers/changeover_windows.py +96 -0
  99. services/changeover/main.py +52 -0
  100. services/changeover/models/__init__.py +11 -0
  101. services/changeover/models/changeover.py +73 -0
  102. services/changeover/models/changeover_events.py +142 -0
  103. services/changeover/models/changeover_kpis.py +75 -0
  104. services/changeover/models/changeover_matrix.py +63 -0
  105. services/changeover/models/changeover_procedures.py +108 -0
  106. services/changeover/models/changeover_tasks.py +87 -0
  107. services/changeover/models/changeover_windows.py +72 -0
  108. services/changeover/routers/__init__.py +9 -0
  109. services/changeover/routers/changeover_events.py +127 -0
  110. services/changeover/routers/changeover_kpis.py +80 -0
  111. services/changeover/routers/changeover_matrix.py +80 -0
  112. services/changeover/routers/changeover_procedures.py +118 -0
  113. services/changeover/routers/changeover_windows.py +98 -0
  114. services/common/__init__.py +2 -0
  115. services/common/approval_chains.py +648 -0
  116. services/common/auth.py +56 -0
  117. services/common/cdc.py +52 -0
  118. services/common/compat.py +217 -0
  119. services/common/compliance.py +562 -0
  120. services/common/config.py +134 -0
  121. services/common/cors.py +17 -0
  122. services/common/data_transformation.py +195 -0
  123. services/common/db.py +577 -0
  124. services/common/embeddings.py +97 -0
  125. services/common/event_reducers.py +194 -0
  126. services/common/event_types.py +51 -0
  127. services/common/integration_ingestion.py +169 -0
  128. services/common/logging.py +204 -0
  129. services/common/models/__init__.py +2 -0
  130. services/common/models/databricks.py +25 -0
  131. services/common/models/enums.py +64 -0
  132. services/common/module_control.py +422 -0
  133. services/common/mongo_cdc_watcher.py +106 -0
  134. services/common/n8n_auth.py +22 -0
  135. services/common/neo4j_mirror.py +1087 -0
  136. services/common/ontology_registry.py +110 -0
  137. services/common/reasoning_traces.py +52 -0
  138. services/common/supply_chain_cdc.py +555 -0
  139. services/common/tracing.py +159 -0
  140. services/common/utils.py +30 -0
  141. services/customers/helpers/__init__.py +8 -0
  142. services/customers/helpers/customer_details.py +64 -0
  143. services/customers/helpers/customer_metadata.py +64 -0
  144. services/customers/helpers/customer_order_metrics.py +67 -0
  145. services/customers/helpers/customer_payment_data.py +67 -0
  146. services/customers/main.py +50 -0
  147. services/customers/models/__init__.py +8 -0
  148. services/customers/models/customer_details.py +42 -0
  149. services/customers/models/customer_metadata.py +97 -0
  150. services/customers/models/customer_order_metrics.py +86 -0
  151. services/customers/models/customer_payment_data.py +60 -0
  152. services/customers/routers/__init__.py +8 -0
  153. services/customers/routers/customer_details.py +88 -0
  154. services/customers/routers/customer_metadata.py +88 -0
  155. services/customers/routers/customer_order_metrics.py +88 -0
  156. services/customers/routers/customer_payment_data.py +88 -0
  157. services/documents/__init__.py +1 -0
  158. services/documents/helpers/__init__.py +6 -0
  159. services/documents/helpers/document_metadata.py +569 -0
  160. services/documents/helpers/document_workflows.py +215 -0
  161. services/documents/helpers/report_templates.py +113 -0
  162. services/documents/main.py +49 -0
  163. services/documents/models/__init__.py +6 -0
  164. services/documents/models/document_metadata.py +215 -0
  165. services/documents/models/document_workflows.py +136 -0
  166. services/documents/models/report_templates.py +132 -0
  167. services/documents/routers/__init__.py +6 -0
  168. services/documents/routers/document_metadata.py +654 -0
  169. services/documents/routers/document_workflows.py +146 -0
  170. services/documents/routers/report_templates.py +86 -0
  171. services/events/helpers/__init__.py +5 -0
  172. services/events/helpers/events.py +394 -0
  173. services/events/main.py +40 -0
  174. services/events/models/__init__.py +5 -0
  175. services/events/models/events.py +50 -0
  176. services/events/routers/__init__.py +6 -0
  177. services/events/routers/count_events.py +109 -0
  178. services/events/routers/events.py +75 -0
  179. services/events/seed_events.py +196 -0
  180. services/facilities/helpers/__init__.py +8 -0
  181. services/facilities/helpers/lines.py +74 -0
  182. services/facilities/helpers/locations.py +231 -0
  183. services/facilities/helpers/plants.py +59 -0
  184. services/facilities/helpers/stages.py +110 -0
  185. services/facilities/helpers/workstation.py +213 -0
  186. services/facilities/main.py +60 -0
  187. services/facilities/models/__init__.py +10 -0
  188. services/facilities/models/industrialLine.py +72 -0
  189. services/facilities/models/industrialPlant.py +164 -0
  190. services/facilities/models/locations.py +74 -0
  191. services/facilities/models/stages.py +92 -0
  192. services/facilities/models/worker.py +73 -0
  193. services/facilities/models/workstation.py +117 -0
  194. services/facilities/models/workstation_live_state.py +59 -0
  195. services/facilities/routers/__init__.py +8 -0
  196. services/facilities/routers/bays.py +81 -0
  197. services/facilities/routers/buildings.py +92 -0
  198. services/facilities/routers/floors.py +81 -0
  199. services/facilities/routers/lines.py +154 -0
  200. services/facilities/routers/locations.py +208 -0
  201. services/facilities/routers/plant.py +203 -0
  202. services/facilities/routers/rooms.py +81 -0
  203. services/facilities/routers/stages.py +152 -0
  204. services/facilities/routers/workstation.py +173 -0
  205. services/file/backup.py +71 -0
  206. services/file/main.py +45 -0
  207. services/file/recieve.py +54 -0
  208. services/file/send.py +55 -0
  209. services/file/src/core/config.py +90 -0
  210. services/file/src/core/keycloak.py +152 -0
  211. services/file/src/core/logging_config.py +9 -0
  212. services/file/src/core/security.py +33 -0
  213. services/file/src/helpers/cad_conversion.py +331 -0
  214. services/file/src/helpers/helpers.py +825 -0
  215. services/file/src/routes/cad_conversion.py +26 -0
  216. services/file/src/routes/files.py +136 -0
  217. services/file/src/routes/presigned.py +154 -0
  218. services/file/src/services/websocket.py +293 -0
  219. services/floor_layout/helpers/__init__.py +8 -0
  220. services/floor_layout/helpers/bays.py +92 -0
  221. services/floor_layout/helpers/buildings.py +54 -0
  222. services/floor_layout/helpers/floors.py +65 -0
  223. services/floor_layout/helpers/rooms.py +76 -0
  224. services/floor_layout/main.py +52 -0
  225. services/floor_layout/models/__init__.py +8 -0
  226. services/floor_layout/models/bays.py +65 -0
  227. services/floor_layout/models/buildings.py +52 -0
  228. services/floor_layout/models/floors.py +45 -0
  229. services/floor_layout/models/rooms.py +61 -0
  230. services/floor_layout/routers/__init__.py +9 -0
  231. services/floor_layout/routers/bays.py +143 -0
  232. services/floor_layout/routers/buildings.py +116 -0
  233. services/floor_layout/routers/floors.py +89 -0
  234. services/floor_layout/routers/locations.py +80 -0
  235. services/floor_layout/routers/rooms.py +134 -0
  236. services/inventory/helpers/__init__.py +13 -0
  237. services/inventory/helpers/cycle_counts.py +124 -0
  238. services/inventory/helpers/inventory_allocations.py +134 -0
  239. services/inventory/helpers/inventory_item_counts.py +114 -0
  240. services/inventory/helpers/inventory_item_quantities.py +114 -0
  241. services/inventory/helpers/inventory_items.py +103 -0
  242. services/inventory/helpers/inventory_stage_outputs.py +134 -0
  243. services/inventory/helpers/inventory_transactions.py +112 -0
  244. services/inventory/helpers/stock_adjustment_requests.py +101 -0
  245. services/inventory/helpers/warehouse_cycle_counts.py +133 -0
  246. services/inventory/helpers/warehouse_locations.py +213 -0
  247. services/inventory/helpers/warehouse_regulated_records.py +123 -0
  248. services/inventory/main.py +62 -0
  249. services/inventory/models/__init__.py +17 -0
  250. services/inventory/models/cycle_counts.py +99 -0
  251. services/inventory/models/inventory_allocations.py +121 -0
  252. services/inventory/models/inventory_common.py +65 -0
  253. services/inventory/models/inventory_enums.py +21 -0
  254. services/inventory/models/inventory_item_count.py +65 -0
  255. services/inventory/models/inventory_item_quantity.py +82 -0
  256. services/inventory/models/inventory_items.py +168 -0
  257. services/inventory/models/inventory_responses.py +44 -0
  258. services/inventory/models/inventory_stage_outputs.py +96 -0
  259. services/inventory/models/inventory_state.py +15 -0
  260. services/inventory/models/inventory_transactions.py +80 -0
  261. services/inventory/models/stock_adjustment_requests.py +109 -0
  262. services/inventory/models/warehouse_cycle_counts.py +119 -0
  263. services/inventory/models/warehouse_location_models.py +708 -0
  264. services/inventory/models/warehouse_regulated_records.py +358 -0
  265. services/inventory/routers/__init__.py +13 -0
  266. services/inventory/routers/cycle_counts.py +106 -0
  267. services/inventory/routers/inventory_allocations.py +125 -0
  268. services/inventory/routers/inventory_item_counts.py +105 -0
  269. services/inventory/routers/inventory_item_quantities.py +105 -0
  270. services/inventory/routers/inventory_items.py +109 -0
  271. services/inventory/routers/inventory_stage_outputs.py +122 -0
  272. services/inventory/routers/inventory_transactions.py +96 -0
  273. services/inventory/routers/stock_adjustment_requests.py +124 -0
  274. services/inventory/routers/warehouse_cycle_counts.py +124 -0
  275. services/inventory/routers/warehouse_locations.py +426 -0
  276. services/inventory/routers/warehouse_regulated_records.py +273 -0
  277. services/iot/helpers/__init__.py +8 -0
  278. services/iot/helpers/ble_device.py +87 -0
  279. services/iot/helpers/mqtt_bridge.py +115 -0
  280. services/iot/helpers/sensor_readings.py +63 -0
  281. services/iot/helpers/sensors.py +77 -0
  282. services/iot/helpers/servers.py +72 -0
  283. services/iot/helpers/uwb_device.py +95 -0
  284. services/iot/main.py +53 -0
  285. services/iot/models/__init__.py +8 -0
  286. services/iot/models/ble_device.py +118 -0
  287. services/iot/models/sensors.py +256 -0
  288. services/iot/models/servers.py +206 -0
  289. services/iot/models/uwb_device.py +106 -0
  290. services/iot/routers/__init__.py +8 -0
  291. services/iot/routers/ble_device.py +110 -0
  292. services/iot/routers/sensors.py +144 -0
  293. services/iot/routers/servers.py +141 -0
  294. services/iot/routers/uwb_device.py +148 -0
  295. services/module_control/__init__.py +1 -0
  296. services/module_control/helpers/__init__.py +1 -0
  297. services/module_control/helpers/integration_config.py +243 -0
  298. services/module_control/helpers/security.py +104 -0
  299. services/module_control/main.py +44 -0
  300. services/module_control/models/__init__.py +1 -0
  301. services/module_control/models/module_control.py +65 -0
  302. services/module_control/routers/__init__.py +1 -0
  303. services/module_control/routers/module_control.py +219 -0
  304. services/orders/helpers/__init__.py +11 -0
  305. services/orders/helpers/invoices.py +123 -0
  306. services/orders/helpers/order_customer_metrics.py +61 -0
  307. services/orders/helpers/order_details.py +71 -0
  308. services/orders/helpers/order_metadata.py +61 -0
  309. services/orders/helpers/order_payment_metadata.py +74 -0
  310. services/orders/helpers/orders.py +119 -0
  311. services/orders/helpers/sales_orders.py +136 -0
  312. services/orders/main.py +56 -0
  313. services/orders/models/__init__.py +11 -0
  314. services/orders/models/invoices.py +415 -0
  315. services/orders/models/order_customer_metrics.py +78 -0
  316. services/orders/models/order_details.py +46 -0
  317. services/orders/models/order_metadata.py +60 -0
  318. services/orders/models/order_payment_metadata.py +63 -0
  319. services/orders/models/orders.py +64 -0
  320. services/orders/models/sales_orders.py +130 -0
  321. services/orders/routers/__init__.py +11 -0
  322. services/orders/routers/invoices.py +111 -0
  323. services/orders/routers/order_customer_metrics.py +87 -0
  324. services/orders/routers/order_details.py +87 -0
  325. services/orders/routers/order_metadata.py +87 -0
  326. services/orders/routers/order_payment_metadata.py +87 -0
  327. services/orders/routers/orders.py +74 -0
  328. services/orders/routers/sales_orders.py +111 -0
  329. services/pm/helpers/__init__.py +14 -0
  330. services/pm/helpers/calendar_bookings.py +114 -0
  331. services/pm/helpers/calibration_point.py +110 -0
  332. services/pm/helpers/calibrations.py +196 -0
  333. services/pm/helpers/checklists.py +318 -0
  334. services/pm/helpers/cleaning.py +333 -0
  335. services/pm/helpers/downtime.py +376 -0
  336. services/pm/helpers/kanban_boards.py +186 -0
  337. services/pm/helpers/maintainance.py +177 -0
  338. services/pm/helpers/sop.py +1155 -0
  339. services/pm/helpers/sop_cdc.py +324 -0
  340. services/pm/helpers/weekly_schedules.py +79 -0
  341. services/pm/main.py +62 -0
  342. services/pm/models/__init__.py +14 -0
  343. services/pm/models/calendar_booking.py +82 -0
  344. services/pm/models/calibration_point.py +44 -0
  345. services/pm/models/calibrations.py +167 -0
  346. services/pm/models/checklists.py +117 -0
  347. services/pm/models/cleaning.py +203 -0
  348. services/pm/models/downtime.py +109 -0
  349. services/pm/models/kanban_board.py +178 -0
  350. services/pm/models/maintainanceLog.py +148 -0
  351. services/pm/models/sop.py +152 -0
  352. services/pm/models/weekly_schedule.py +91 -0
  353. services/pm/routers/__init__.py +14 -0
  354. services/pm/routers/calendar_bookings.py +143 -0
  355. services/pm/routers/calibration_point.py +94 -0
  356. services/pm/routers/calibrations.py +232 -0
  357. services/pm/routers/checklists.py +188 -0
  358. services/pm/routers/cleaning.py +127 -0
  359. services/pm/routers/downtime.py +143 -0
  360. services/pm/routers/kanban_boards.py +283 -0
  361. services/pm/routers/maintainance.py +241 -0
  362. services/pm/routers/sop.py +437 -0
  363. services/pm/routers/weekly_schedules.py +108 -0
  364. services/process_definitions/helpers/__init__.py +11 -0
  365. services/process_definitions/helpers/cpp_cqa_registry.py +120 -0
  366. services/process_definitions/helpers/mbmr_templates.py +107 -0
  367. services/process_definitions/helpers/packing_instructions.py +113 -0
  368. services/process_definitions/helpers/process_constraints.py +495 -0
  369. services/process_definitions/helpers/process_corrections.py +279 -0
  370. services/process_definitions/helpers/process_definition.py +996 -0
  371. services/process_definitions/helpers/process_node_catalog.py +786 -0
  372. services/process_definitions/helpers/process_post_checks.py +441 -0
  373. services/process_definitions/helpers/process_pre_checks.py +351 -0
  374. services/process_definitions/helpers/process_steps.py +220 -0
  375. services/process_definitions/main.py +71 -0
  376. services/process_definitions/models/__init__.py +13 -0
  377. services/process_definitions/models/cpp_cqa_registry.py +145 -0
  378. services/process_definitions/models/gxp_change_controls.py +38 -0
  379. services/process_definitions/models/gxp_risk_assessments.py +30 -0
  380. services/process_definitions/models/gxp_validation_evidence.py +33 -0
  381. services/process_definitions/models/mbmr_templates.py +173 -0
  382. services/process_definitions/models/packing_instructions.py +176 -0
  383. services/process_definitions/models/process_constraints.py +159 -0
  384. services/process_definitions/models/process_corrections.py +118 -0
  385. services/process_definitions/models/process_definition.py +685 -0
  386. services/process_definitions/models/process_definition_common.py +48 -0
  387. services/process_definitions/models/process_node_catalog.py +25 -0
  388. services/process_definitions/models/process_post_checks.py +171 -0
  389. services/process_definitions/models/process_pre_checks.py +168 -0
  390. services/process_definitions/models/process_steps.py +170 -0
  391. services/process_definitions/node_services/__init__.py +8 -0
  392. services/process_definitions/node_services/common.py +95 -0
  393. services/process_definitions/node_services/executor.py +499 -0
  394. services/process_definitions/node_services/flow_simulator.py +733 -0
  395. services/process_definitions/node_services/functions.py +193 -0
  396. services/process_definitions/node_services/messaging.py +44 -0
  397. services/process_definitions/node_services/models.py +221 -0
  398. services/process_definitions/node_services/network.py +161 -0
  399. services/process_definitions/node_services/parsers.py +87 -0
  400. services/process_definitions/node_services/sequence.py +95 -0
  401. services/process_definitions/node_services/storage.py +50 -0
  402. services/process_definitions/node_services/webhooks.py +52 -0
  403. services/process_definitions/routers/__init__.py +10 -0
  404. services/process_definitions/routers/cpp_cqa_registry.py +86 -0
  405. services/process_definitions/routers/mbmr_templates.py +84 -0
  406. services/process_definitions/routers/packing_instructions.py +84 -0
  407. services/process_definitions/routers/process_constraints.py +564 -0
  408. services/process_definitions/routers/process_corrections.py +343 -0
  409. services/process_definitions/routers/process_definition.py +992 -0
  410. services/process_definitions/routers/process_post_checks.py +529 -0
  411. services/process_definitions/routers/process_pre_checks.py +435 -0
  412. services/process_definitions/routers/process_steps.py +274 -0
  413. services/procurement/helpers/__init__.py +9 -0
  414. services/procurement/helpers/goods_receipts.py +240 -0
  415. services/procurement/helpers/purchase_order_shipping_information.py +85 -0
  416. services/procurement/helpers/purchase_orders.py +68 -0
  417. services/procurement/helpers/quality_control.py +235 -0
  418. services/procurement/helpers/sampling.py +404 -0
  419. services/procurement/main.py +52 -0
  420. services/procurement/models/__init__.py +9 -0
  421. services/procurement/models/goods_receipts.py +165 -0
  422. services/procurement/models/purchase_orders.py +54 -0
  423. services/procurement/models/quality_control.py +464 -0
  424. services/procurement/models/reinspection_records.py +28 -0
  425. services/procurement/models/sampling.py +262 -0
  426. services/procurement/models/shipping_information.py +51 -0
  427. services/procurement/routers/__init__.py +9 -0
  428. services/procurement/routers/goods_receipts.py +201 -0
  429. services/procurement/routers/purchase_orders.py +106 -0
  430. services/procurement/routers/quality_control.py +386 -0
  431. services/procurement/routers/sampling.py +296 -0
  432. services/procurement/routers/shipping_information.py +97 -0
  433. services/production/__init__.py +1 -0
  434. services/production/agents/__init__.py +5 -0
  435. services/production/agents/batch_planning_agent.py +815 -0
  436. services/production/models/__init__.py +25 -0
  437. services/production/models/batch.py +253 -0
  438. services/products/helpers/__init__.py +10 -0
  439. services/products/helpers/boms.py +100 -0
  440. services/products/helpers/drug_research.py +644 -0
  441. services/products/helpers/product_component.py +168 -0
  442. services/products/helpers/product_inventory.py +221 -0
  443. services/products/helpers/product_pricing.py +123 -0
  444. services/products/helpers/product_utils.py +32 -0
  445. services/products/helpers/products.py +81 -0
  446. services/products/main.py +59 -0
  447. services/products/models/__init__.py +9 -0
  448. services/products/models/drug_research.py +138 -0
  449. services/products/models/product_common.py +60 -0
  450. services/products/models/product_component.py +1028 -0
  451. services/products/models/product_inventory.py +118 -0
  452. services/products/models/product_pricing.py +73 -0
  453. services/products/models/products.py +151 -0
  454. services/products/routers/__init__.py +9 -0
  455. services/products/routers/boms.py +116 -0
  456. services/products/routers/drug_research.py +115 -0
  457. services/products/routers/product_components.py +123 -0
  458. services/products/routers/product_inventory.py +185 -0
  459. services/products/routers/product_pricing.py +136 -0
  460. services/products/routers/products.py +165 -0
  461. services/replenishment/__init__.py +1 -0
  462. services/replenishment/main.py +46 -0
  463. services/replenishment/routers/__init__.py +1 -0
  464. services/replenishment/routers/replenishment.py +20 -0
  465. services/shifts/helpers/__init__.py +7 -0
  466. services/shifts/helpers/shift_templates.py +124 -0
  467. services/shifts/helpers/shifts.py +79 -0
  468. services/shifts/helpers/timesheets.py +137 -0
  469. services/shifts/main.py +48 -0
  470. services/shifts/models/__init__.py +8 -0
  471. services/shifts/models/shift.py +62 -0
  472. services/shifts/models/shift_template.py +82 -0
  473. services/shifts/models/time_range.py +31 -0
  474. services/shifts/models/timesheet.py +196 -0
  475. services/shifts/routers/__init__.py +7 -0
  476. services/shifts/routers/shift_templates.py +97 -0
  477. services/shifts/routers/shifts.py +117 -0
  478. services/shifts/routers/timesheets.py +117 -0
  479. services/shipping/helpers/__init__.py +15 -0
  480. services/shipping/helpers/carrier.py +78 -0
  481. services/shipping/helpers/customs_declaration.py +104 -0
  482. services/shipping/helpers/delivery_note.py +99 -0
  483. services/shipping/helpers/package.py +95 -0
  484. services/shipping/helpers/pallet.py +85 -0
  485. services/shipping/helpers/route.py +93 -0
  486. services/shipping/helpers/shipping_information.py +82 -0
  487. services/shipping/helpers/shipping_provider_details.py +59 -0
  488. services/shipping/helpers/shipping_provider_metadata.py +59 -0
  489. services/shipping/helpers/vehicle.py +85 -0
  490. services/shipping/helpers/waybill.py +86 -0
  491. services/shipping/main.py +64 -0
  492. services/shipping/models/__init__.py +15 -0
  493. services/shipping/models/carrier.py +97 -0
  494. services/shipping/models/customs_declaration.py +138 -0
  495. services/shipping/models/delivery_note.py +163 -0
  496. services/shipping/models/package.py +152 -0
  497. services/shipping/models/pallet.py +137 -0
  498. services/shipping/models/route.py +120 -0
  499. services/shipping/models/shipping_information.py +55 -0
  500. services/shipping/models/shipping_provider_details.py +42 -0
  501. services/shipping/models/shipping_provider_metadata.py +54 -0
  502. services/shipping/models/vehicle.py +129 -0
  503. services/shipping/models/waybill.py +189 -0
  504. services/shipping/routers/__init__.py +15 -0
  505. services/shipping/routers/carrier.py +99 -0
  506. services/shipping/routers/customs_declaration.py +132 -0
  507. services/shipping/routers/delivery_note.py +150 -0
  508. services/shipping/routers/package.py +141 -0
  509. services/shipping/routers/pallet.py +108 -0
  510. services/shipping/routers/route.py +128 -0
  511. services/shipping/routers/shipping_information.py +97 -0
  512. services/shipping/routers/shipping_provider_details.py +80 -0
  513. services/shipping/routers/shipping_provider_metadata.py +80 -0
  514. services/shipping/routers/vehicle.py +117 -0
  515. services/shipping/routers/waybill.py +119 -0
  516. services/suppliers/helpers/__init__.py +13 -0
  517. services/suppliers/helpers/supplier_capabilities.py +58 -0
  518. services/suppliers/helpers/supplier_certifications.py +67 -0
  519. services/suppliers/helpers/supplier_details.py +58 -0
  520. services/suppliers/helpers/supplier_financials.py +58 -0
  521. services/suppliers/helpers/supplier_inventory.py +74 -0
  522. services/suppliers/helpers/supplier_locations.py +60 -0
  523. services/suppliers/helpers/supplier_pricing.py +69 -0
  524. services/suppliers/helpers/supplier_quality.py +69 -0
  525. services/suppliers/helpers/supplier_shipping.py +69 -0
  526. services/suppliers/main.py +60 -0
  527. services/suppliers/models/__init__.py +13 -0
  528. services/suppliers/models/supplier_capabilities.py +70 -0
  529. services/suppliers/models/supplier_certifications.py +64 -0
  530. services/suppliers/models/supplier_details.py +75 -0
  531. services/suppliers/models/supplier_financials.py +69 -0
  532. services/suppliers/models/supplier_inventory.py +76 -0
  533. services/suppliers/models/supplier_locations.py +70 -0
  534. services/suppliers/models/supplier_pricing.py +74 -0
  535. services/suppliers/models/supplier_quality.py +74 -0
  536. services/suppliers/models/supplier_shipping.py +76 -0
  537. services/suppliers/routers/__init__.py +13 -0
  538. services/suppliers/routers/supplier_capabilities.py +88 -0
  539. services/suppliers/routers/supplier_certifications.py +87 -0
  540. services/suppliers/routers/supplier_details.py +83 -0
  541. services/suppliers/routers/supplier_financials.py +83 -0
  542. services/suppliers/routers/supplier_inventory.py +105 -0
  543. services/suppliers/routers/supplier_locations.py +89 -0
  544. services/suppliers/routers/supplier_pricing.py +96 -0
  545. services/suppliers/routers/supplier_quality.py +96 -0
  546. services/suppliers/routers/supplier_shipping.py +96 -0
  547. services/supply_allocation/main.py +46 -0
  548. services/supply_allocation/routers/__init__.py +1 -0
  549. services/supply_allocation/routers/allocation.py +20 -0
  550. services/taxonomy/helpers/__init__.py +7 -0
  551. services/taxonomy/helpers/classes.py +48 -0
  552. services/taxonomy/helpers/family.py +53 -0
  553. services/taxonomy/helpers/subclass.py +58 -0
  554. services/taxonomy/main.py +48 -0
  555. services/taxonomy/models/__init__.py +7 -0
  556. services/taxonomy/models/classes.py +52 -0
  557. services/taxonomy/models/family.py +60 -0
  558. services/taxonomy/models/subclass.py +50 -0
  559. services/taxonomy/routers/__init__.py +7 -0
  560. services/taxonomy/routers/classes.py +78 -0
  561. services/taxonomy/routers/family.py +77 -0
  562. services/taxonomy/routers/subclass.py +82 -0
  563. services/warehouse_execution/__init__.py +1 -0
  564. services/warehouse_execution/main.py +46 -0
  565. services/warehouse_execution/routers/__init__.py +1 -0
  566. services/warehouse_execution/routers/execution.py +21 -0
  567. services/work_order_agent/__init__.py +17 -0
  568. services/work_order_agent/agent/__init__.py +17 -0
  569. services/work_order_agent/agent/work_order_agent.py +658 -0
  570. services/work_order_agent/tracking/__init__.py +101 -0
  571. services/work_order_agent/tracking/event_system.py +182 -0
  572. services/work_order_agent/tracking/state_machine.py +163 -0
  573. services/work_order_agent/tracking/state_machine_integrator.py +295 -0
  574. services/work_order_agent/tracking/test_phase2_implementation.py +302 -0
  575. services/work_order_agent/tracking/time_analysis.py +301 -0
  576. services/work_order_agent/tracking/tracked_work_order.py +255 -0
  577. services/work_order_agent/tracking/work_order_adapter.py +367 -0
  578. services/work_order_agent/tracking/work_order_batch_manager.py +406 -0
  579. services/work_order_agent/tracking/work_order_repository.py +431 -0
  580. services/workorders/helpers/__init__.py +5 -0
  581. services/workorders/helpers/area_room_usage_ledger.py +139 -0
  582. services/workorders/helpers/batch_execution_records.py +265 -0
  583. services/workorders/helpers/batch_release_workflows.py +158 -0
  584. services/workorders/helpers/batch_step_executions.py +145 -0
  585. services/workorders/helpers/equipment_usage_ledger.py +209 -0
  586. services/workorders/helpers/executed_bmr_records.py +170 -0
  587. services/workorders/helpers/executed_bpr_records.py +170 -0
  588. services/workorders/helpers/executed_instruction_evidence.py +155 -0
  589. services/workorders/helpers/ipc_result_records.py +134 -0
  590. services/workorders/helpers/production_batches.py +117 -0
  591. services/workorders/helpers/work_orders.py +367 -0
  592. services/workorders/helpers/yield_reconciliation_records.py +158 -0
  593. services/workorders/main.py +110 -0
  594. services/workorders/models/__init__.py +5 -0
  595. services/workorders/models/area_room_usage_ledger.py +154 -0
  596. services/workorders/models/batch_execution_records.py +575 -0
  597. services/workorders/models/batch_release_workflows.py +190 -0
  598. services/workorders/models/batch_step_executions.py +142 -0
  599. services/workorders/models/equipment_usage_ledger.py +144 -0
  600. services/workorders/models/executed_bmr_records.py +220 -0
  601. services/workorders/models/executed_bpr_records.py +220 -0
  602. services/workorders/models/executed_instruction_evidence.py +128 -0
  603. services/workorders/models/ipc_result_records.py +164 -0
  604. services/workorders/models/production_batches.py +181 -0
  605. services/workorders/models/work_orders.py +255 -0
  606. services/workorders/models/yield_reconciliation_records.py +175 -0
  607. services/workorders/routers/__init__.py +5 -0
  608. services/workorders/routers/area_room_usage_ledger.py +117 -0
  609. services/workorders/routers/batch_execution_records.py +103 -0
  610. services/workorders/routers/batch_release_workflows.py +86 -0
  611. services/workorders/routers/batch_step_executions.py +88 -0
  612. services/workorders/routers/equipment_usage_ledger.py +115 -0
  613. services/workorders/routers/executed_bmr_records.py +86 -0
  614. services/workorders/routers/executed_bpr_records.py +86 -0
  615. services/workorders/routers/executed_instruction_evidence.py +86 -0
  616. services/workorders/routers/ipc_result_records.py +86 -0
  617. services/workorders/routers/production_batches.py +86 -0
  618. services/workorders/routers/work_orders.py +257 -0
  619. services/workorders/routers/yield_reconciliation_records.py +86 -0
  620. src/broca/__init__.py +5 -0
  621. src/broca/agent.py +201 -0
  622. src/cerebellum/__init__.py +0 -0
  623. src/cerebellum/adapter.py +84 -0
  624. src/cerebellum/capabilities/__init__.py +0 -0
  625. src/cerebellum/capabilities/agent/__init__.py +0 -0
  626. src/cerebellum/capabilities/agent/agents.py +65 -0
  627. src/cerebellum/capabilities/ai/__init__.py +0 -0
  628. src/cerebellum/capabilities/ai/providers.py +106 -0
  629. src/cerebellum/capabilities/api/__init__.py +0 -0
  630. src/cerebellum/capabilities/api/graphql.py +35 -0
  631. src/cerebellum/capabilities/api/rest_api.py +45 -0
  632. src/cerebellum/capabilities/api/webhook.py +30 -0
  633. src/cerebellum/capabilities/browser/__init__.py +0 -0
  634. src/cerebellum/capabilities/browser/browsers.py +27 -0
  635. src/cerebellum/capabilities/cloud/__init__.py +0 -0
  636. src/cerebellum/capabilities/cloud/cloud.py +46 -0
  637. src/cerebellum/capabilities/communication/__init__.py +0 -0
  638. src/cerebellum/capabilities/communication/communication.py +62 -0
  639. src/cerebellum/capabilities/database/__init__.py +0 -0
  640. src/cerebellum/capabilities/database/elasticsearch.py +40 -0
  641. src/cerebellum/capabilities/database/influxdb.py +37 -0
  642. src/cerebellum/capabilities/database/mongodb.py +50 -0
  643. src/cerebellum/capabilities/database/neo4j.py +32 -0
  644. src/cerebellum/capabilities/database/redis.py +44 -0
  645. src/cerebellum/capabilities/enterprise/__init__.py +0 -0
  646. src/cerebellum/capabilities/enterprise/opensource.py +180 -0
  647. src/cerebellum/capabilities/enterprise/proprietary.py +313 -0
  648. src/cerebellum/capabilities/event_streaming/__init__.py +0 -0
  649. src/cerebellum/capabilities/event_streaming/streaming.py +38 -0
  650. src/cerebellum/capabilities/infrastructure/__init__.py +0 -0
  651. src/cerebellum/capabilities/infrastructure/infrastructure.py +30 -0
  652. src/cerebellum/capabilities/productivity/__init__.py +0 -0
  653. src/cerebellum/capabilities/productivity/productivity.py +158 -0
  654. src/cerebellum/capabilities/robotics/__init__.py +0 -0
  655. src/cerebellum/capabilities/robotics/robotics.py +124 -0
  656. src/cerebellum/capabilities/runtime/__init__.py +0 -0
  657. src/cerebellum/capabilities/runtime/runtimes.py +92 -0
  658. src/cerebellum/capabilities/search/__init__.py +0 -0
  659. src/cerebellum/capabilities/search/search.py +63 -0
  660. src/cerebellum/capabilities/source_control/__init__.py +0 -0
  661. src/cerebellum/capabilities/source_control/source_control.py +113 -0
  662. src/cerebellum/capabilities/storage/__init__.py +0 -0
  663. src/cerebellum/capabilities/storage/storage.py +94 -0
  664. src/cerebellum/capabilities/workflow/__init__.py +0 -0
  665. src/cerebellum/capabilities/workflow/workflows.py +49 -0
  666. src/cerebellum/capability.py +108 -0
  667. src/cerebellum/config.py +157 -0
  668. src/cerebellum/fallback.py +147 -0
  669. src/cerebellum/fallback_engine.py +121 -0
  670. src/cerebellum/key_manager.py +129 -0
  671. src/cerebellum/keystore.py +179 -0
  672. src/cerebellum/lifecycle.py +54 -0
  673. src/cerebellum/metadata.py +61 -0
  674. src/cerebellum/operator/base.py +25 -0
  675. src/cerebellum/peripheral.py +92 -0
  676. src/cerebellum/registry.py +98 -0
  677. src/cerebellum/resolve_entity_capability.py +259 -0
  678. src/cingulate/benchmark/__init__.py +23 -0
  679. src/cingulate/benchmark/reporter.py +102 -0
  680. src/cingulate/benchmark/runner.py +159 -0
  681. src/cingulate/benchmark/scenario_runner.py +150 -0
  682. src/cingulate/benchmark/validator.py +102 -0
  683. src/cingulate/governance/__init__.py +21 -0
  684. src/cingulate/governance/architecture_validator.py +194 -0
  685. src/cingulate/governance/compliance.py +104 -0
  686. src/cingulate/governance/governance.py +77 -0
  687. src/cingulate/governance/policy_registry.py +91 -0
  688. src/cortex/__init__.py +33 -0
  689. src/cortex/cost.py +71 -0
  690. src/cortex/counterfactual.py +162 -0
  691. src/cortex/digital_twin.py +90 -0
  692. src/cortex/experience.py +83 -0
  693. src/cortex/feedback.py +144 -0
  694. src/cortex/loss.py +116 -0
  695. src/cortex/prediction.py +142 -0
  696. src/cortex/replay.py +130 -0
  697. src/cortex/reward.py +113 -0
  698. src/cortex/simulator.py +102 -0
  699. src/cortex/world_model.py +180 -0
  700. src/cortex/world_model_simulation.py +1591 -0
  701. src/cortex/world_state.py +121 -0
  702. src/cortex/xavier.py +250 -0
  703. src/deepdive/__init__.py +29 -0
  704. src/deepdive/aggregation.py +113 -0
  705. src/deepdive/digital_twin_aggregator.py +128 -0
  706. src/deepdive/elasticsearch_adapter.py +110 -0
  707. src/deepdive/fleet_analytics.py +131 -0
  708. src/deepdive/knowledge_aggregator.py +130 -0
  709. src/homeostasis/__init__.py +19 -0
  710. src/homeostasis/control_plane.py +159 -0
  711. src/introspection/__init__.py +38 -0
  712. src/introspection/alerting.py +142 -0
  713. src/introspection/health.py +101 -0
  714. src/introspection/lemon.py +243 -0
  715. src/introspection/logging.py +147 -0
  716. src/introspection/metrics.py +106 -0
  717. src/introspection/tracing.py +162 -0
  718. src/monkey_brain/__init__.py +1 -0
  719. src/monkey_brain/api/main.py +148 -0
  720. src/monkey_brain/api/models.py +81 -0
  721. src/monkey_brain/api/routes/routes/keys.py +106 -0
  722. src/monkey_brain/api/routes/routes/run.py +169 -0
  723. src/monkey_brain/api/routes/routes/simulate.py +485 -0
  724. src/monkey_brain/dlm/__init__.py +44 -0
  725. src/monkey_brain/dlm/dlm.py +139 -0
  726. src/monkey_brain/dlm/gc.py +115 -0
  727. src/monkey_brain/dlm/lifecycle.py +149 -0
  728. src/monkey_brain/dlm/orphans.py +99 -0
  729. src/monkey_brain/dlm/storage.py +149 -0
  730. src/monkey_brain/dlm/ttl.py +140 -0
  731. src/monkey_brain/documents/__init__.py +0 -0
  732. src/monkey_brain/documents/document_ocr.py +6 -0
  733. src/monkey_brain/kernel/__init__.py +53 -0
  734. src/monkey_brain/kernel/capability_interface.py +144 -0
  735. src/monkey_brain/kernel/classifier/__init__.py +1 -0
  736. src/monkey_brain/kernel/classifier/embed_classifier.py +125 -0
  737. src/monkey_brain/kernel/classifier/intent_examples.py +106 -0
  738. src/monkey_brain/kernel/dag.py +23 -0
  739. src/monkey_brain/kernel/execution_state.py +257 -0
  740. src/monkey_brain/kernel/goal_planner.py +85 -0
  741. src/monkey_brain/kernel/goal_router.py +20 -0
  742. src/monkey_brain/kernel/goals/__init__.py +1 -0
  743. src/monkey_brain/kernel/goals/goal.py +130 -0
  744. src/monkey_brain/kernel/goals/goal_bootstrap.py +38 -0
  745. src/monkey_brain/kernel/goals/goal_classifier.py +132 -0
  746. src/monkey_brain/kernel/goals/goal_registry.py +75 -0
  747. src/monkey_brain/kernel/intents/__init__.py +1 -0
  748. src/monkey_brain/kernel/intents/event_adapter.py +246 -0
  749. src/monkey_brain/kernel/intents/helpers.py +13 -0
  750. src/monkey_brain/kernel/intents/intent_registry.py +705 -0
  751. src/monkey_brain/kernel/intents/intent_router.py +102 -0
  752. src/monkey_brain/kernel/intents/predicates/approval_create.py +9 -0
  753. src/monkey_brain/kernel/intents/predicates/approval_decision.py +9 -0
  754. src/monkey_brain/kernel/intents/predicates/approval_hold.py +9 -0
  755. src/monkey_brain/kernel/intents/predicates/approval_query.py +9 -0
  756. src/monkey_brain/kernel/intents/predicates/batch_close.py +9 -0
  757. src/monkey_brain/kernel/intents/predicates/batch_creation.py +9 -0
  758. src/monkey_brain/kernel/intents/predicates/batch_delete.py +9 -0
  759. src/monkey_brain/kernel/intents/predicates/batch_hold.py +9 -0
  760. src/monkey_brain/kernel/intents/predicates/batch_record.py +9 -0
  761. src/monkey_brain/kernel/intents/predicates/batch_update.py +9 -0
  762. src/monkey_brain/kernel/intents/predicates/change_control.py +49 -0
  763. src/monkey_brain/kernel/intents/predicates/compliance_audit.py +14 -0
  764. src/monkey_brain/kernel/intents/predicates/decision_intelligence.py +9 -0
  765. src/monkey_brain/kernel/intents/predicates/drug_research.py +9 -0
  766. src/monkey_brain/kernel/intents/predicates/fuzzy_match.py +19 -0
  767. src/monkey_brain/kernel/intents/predicates/production_kpi.py +9 -0
  768. src/monkey_brain/kernel/intents/predicates/sop_create.py +9 -0
  769. src/monkey_brain/kernel/intents/predicates/sop_query.py +9 -0
  770. src/monkey_brain/kernel/intents/predicates/sop_update.py +9 -0
  771. src/monkey_brain/kernel/intents/predicates/warehouse_shipping.py +9 -0
  772. src/monkey_brain/kernel/intents/predicates/work_order_create.py +9 -0
  773. src/monkey_brain/kernel/intents/predicates/work_order_delete.py +9 -0
  774. src/monkey_brain/kernel/intents/predicates/work_order_hold.py +9 -0
  775. src/monkey_brain/kernel/intents/predicates/work_order_query.py +9 -0
  776. src/monkey_brain/kernel/intents/predicates/work_order_status.py +9 -0
  777. src/monkey_brain/kernel/intents/predicates/work_order_update.py +9 -0
  778. src/monkey_brain/kernel/intents/predicates/worker.py +9 -0
  779. src/monkey_brain/kernel/intents/telemetry_adapter.py +274 -0
  780. src/monkey_brain/kernel/intents/utils.py +68 -0
  781. src/monkey_brain/kernel/learning.py +98 -0
  782. src/monkey_brain/kernel/llm_explorer.py +188 -0
  783. src/monkey_brain/kernel/loss.py +81 -0
  784. src/monkey_brain/kernel/nlp/__init__.py +1 -0
  785. src/monkey_brain/kernel/nlp/compat.py +23 -0
  786. src/monkey_brain/kernel/nlp/models.py +10 -0
  787. src/monkey_brain/kernel/nlp/question_analyzer.py +203 -0
  788. src/monkey_brain/kernel/nlp/spacy_parser.py +53 -0
  789. src/monkey_brain/kernel/observer.py +97 -0
  790. src/monkey_brain/kernel/parser/__init__.py +3 -0
  791. src/monkey_brain/kernel/parser/ast.py +28 -0
  792. src/monkey_brain/kernel/parser/extractors/__init__.py +11 -0
  793. src/monkey_brain/kernel/parser/extractors/entities.py +21 -0
  794. src/monkey_brain/kernel/parser/extractors/filters.py +16 -0
  795. src/monkey_brain/kernel/parser/extractors/projections.py +36 -0
  796. src/monkey_brain/kernel/parser/extractors/verbs.py +31 -0
  797. src/monkey_brain/kernel/parser/parser.py +57 -0
  798. src/monkey_brain/kernel/parser/rules.py +75 -0
  799. src/monkey_brain/kernel/pipeline.py +44 -0
  800. src/monkey_brain/kernel/planner.py +57 -0
  801. src/monkey_brain/kernel/rl/__init__.py +33 -0
  802. src/monkey_brain/kernel/rl/learner.py +98 -0
  803. src/monkey_brain/kernel/rl/policy.py +254 -0
  804. src/monkey_brain/kernel/rl/reward.py +117 -0
  805. src/monkey_brain/kernel/rl/transition.py +112 -0
  806. src/monkey_brain/persistence/__init__.py +47 -0
  807. src/monkey_brain/persistence/adapters.py +49 -0
  808. src/monkey_brain/persistence/events.py +105 -0
  809. src/monkey_brain/persistence/manager.py +124 -0
  810. src/monkey_brain/persistence/mongodb_adapter.py +91 -0
  811. src/monkey_brain/persistence/redis_adapter.py +93 -0
  812. src/monkey_brain/persistence/reducer.py +111 -0
  813. src/monkey_brain/runtime/__init__.py +49 -0
  814. src/monkey_brain/runtime/depedencies.py +8 -0
  815. src/monkey_brain/runtime/engine.py +183 -0
  816. src/monkey_brain/runtime/message_bus.py +82 -0
  817. src/monkey_brain/runtime/process.py +144 -0
  818. src/monkey_brain/runtime/resource_manager.py +100 -0
  819. src/monkey_brain/runtime/routers.py +8 -0
  820. src/monkey_brain/runtime/runtime.py +199 -0
  821. src/monkey_brain/runtime/scheduler.py +165 -0
  822. src/monkey_brain/runtime/supervisor.py +133 -0
  823. src/monkey_brain/runtime/worker_pool.py +111 -0
  824. src/plasticity/seed/__init__.py +30 -0
  825. src/plasticity/seed/benchmark_generator.py +105 -0
  826. src/plasticity/seed/event_generator.py +122 -0
  827. src/plasticity/seed/scenario_builder.py +134 -0
  828. src/plasticity/seed/seed_data.py +206 -0
  829. src/plasticity/seed/seeder.py +122 -0
  830. src/plasticity/testing/__init__.py +28 -0
  831. src/plasticity/testing/performance.py +131 -0
  832. src/plasticity/testing/profiler.py +255 -0
  833. src/plasticity/testing/reporter.py +84 -0
  834. src/plasticity/testing/runner.py +209 -0
  835. src/sync/__init__.py +12 -0
  836. src/sync/cloud_aggregator.py +63 -0
  837. src/sync/edge_node.py +51 -0
  838. src/sync/sync_manager.py +74 -0
@@ -0,0 +1,1155 @@
1
+ """
2
+ helpers/sop_helper.py
3
+ ---------------------
4
+ MongoDB CRUD helper for the SOP (Standard Operating Procedure) aggregate model.
5
+ Mirrors the pattern established in the devices helper.
6
+ """
7
+
8
+ import re
9
+ from typing import Any, Optional
10
+ from uuid import uuid4
11
+ from motor.motor_asyncio import AsyncIOMotorDatabase
12
+
13
+ from services.pm.models.sop import SOPCreate, SOPResponse, SOPUpdate
14
+ from services.common.compliance import CHANGE_CONTROL_COLLECTION, utc_now
15
+
16
+ COLLECTION = "sops"
17
+ MATERIAL_FLOW_COLLECTION = "material_flows"
18
+
19
+
20
+ def _serialize(doc: dict) -> dict:
21
+ """Strip MongoDB's internal _id before returning a document."""
22
+ doc.pop("_id", None)
23
+ return doc
24
+
25
+
26
+ def _sop_process_definition_id(record: dict) -> str:
27
+ process_definition = record.get("process_definition")
28
+ if isinstance(process_definition, dict) and process_definition.get("process_definition_id"):
29
+ return str(process_definition["process_definition_id"])
30
+ for section_name in ("prechecks", "postchecks", "constraints", "corrective_actions"):
31
+ section = record.get(section_name)
32
+ if isinstance(section, dict):
33
+ value = section.get("process_definition_id") or section.get("workflow_id")
34
+ if value:
35
+ return str(value)
36
+ return f"PROC-{record.get('id') or 'SOP'}"
37
+
38
+
39
+ def _normalize_section(section: object, process_definition_id: str, section_id: str) -> dict:
40
+ normalized = dict(section) if isinstance(section, dict) else {}
41
+ normalized.setdefault("id", section_id)
42
+ normalized["process_definition_id"] = str(normalized.get("process_definition_id") or normalized.get("workflow_id") or process_definition_id)
43
+ return normalized
44
+
45
+
46
+ def _normalize_sop_record(doc: Optional[dict]) -> Optional[dict]:
47
+ if not doc:
48
+ return None
49
+
50
+ record = _serialize(doc)
51
+ sop_id = str(
52
+ record.get("id")
53
+ or record.get("sop_id")
54
+ or record.get("document_id")
55
+ or record.get("name")
56
+ or "SOP"
57
+ )
58
+ record["id"] = sop_id
59
+ process_definition_id = _sop_process_definition_id(record)
60
+ process_step_id = str(
61
+ record.get("process_step_id")
62
+ or (record.get("corrective_actions") or {}).get("process_step_id")
63
+ or (record.get("corrective_actions") or {}).get("workflow_step_id")
64
+ or f"{process_definition_id}-step-001"
65
+ )
66
+
67
+ record.setdefault("title", sop_id)
68
+ record.setdefault("purpose", record.get("title") or sop_id)
69
+ record.setdefault("scope", record.get("purpose") or record.get("title") or sop_id)
70
+ record.setdefault("version", "1.0.0")
71
+ record.setdefault("references", [])
72
+ record.setdefault("tags", [])
73
+ record.setdefault("document_ids", [])
74
+
75
+ process_definition = record.get("process_definition")
76
+ if not isinstance(process_definition, dict):
77
+ process_definition = record.get("processDefinition") if isinstance(record.get("processDefinition"), dict) else {}
78
+ process_definition = dict(process_definition)
79
+ process_definition.setdefault("process_definition_id", process_definition_id)
80
+ process_definition.setdefault("name", record.get("title") or process_definition_id)
81
+ process_definition.setdefault("description", record.get("purpose") or record.get("scope") or "")
82
+ process_definition.setdefault("version", record.get("version") or "1.0.0")
83
+ process_definition.setdefault("owner", record.get("authored_by"))
84
+ process_definition.setdefault("tags", record.get("tags") or [])
85
+ process_definition.setdefault("status", "pending")
86
+ if process_definition.get("status") == "active":
87
+ process_definition["status"] = "completed"
88
+ elif process_definition.get("status") not in {"pending", "in_progress", "completed", "failed", "skipped", "blocked"}:
89
+ process_definition["status"] = "pending"
90
+ process_definition.setdefault("approval_status", "draft")
91
+ process_definition.setdefault("ipc_checkpoints", [])
92
+ process_definition.setdefault("cleaning_requirements", [])
93
+ process_definition.setdefault("instruction_templates", [])
94
+ process_definition.setdefault(
95
+ "steps",
96
+ {
97
+ "id": f"{process_definition_id}-steps",
98
+ "process_definition_id": process_definition_id,
99
+ "description": record.get("scope") or record.get("purpose"),
100
+ "steps": [],
101
+ },
102
+ )
103
+ record["process_definition"] = process_definition
104
+
105
+ record["prechecks"] = _normalize_section(record.get("prechecks"), process_definition_id, f"PRE-{sop_id}")
106
+ record["postchecks"] = _normalize_section(record.get("postchecks"), process_definition_id, f"POST-{sop_id}")
107
+ record["constraints"] = _normalize_section(record.get("constraints"), process_definition_id, f"CON-{sop_id}")
108
+ constraints = record["constraints"].get("constraints")
109
+ if isinstance(constraints, list):
110
+ record["constraints"]["constraints"] = [
111
+ {**item, "process_definition_id": str(item.get("process_definition_id") or item.get("workflow_id") or process_definition_id)}
112
+ if isinstance(item, dict) else item
113
+ for item in constraints
114
+ ]
115
+
116
+ record["corrective_actions"] = _normalize_section(record.get("corrective_actions"), process_definition_id, f"CA-{sop_id}")
117
+ record["corrective_actions"]["process_step_id"] = str(
118
+ record["corrective_actions"].get("process_step_id")
119
+ or record["corrective_actions"].get("workflow_step_id")
120
+ or process_step_id
121
+ )
122
+
123
+ return SOPResponse(**record).model_dump()
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Read
128
+ # ---------------------------------------------------------------------------
129
+
130
+ async def get_all(
131
+ db: AsyncIOMotorDatabase,
132
+ page: int = 1,
133
+ page_size: int = 20,
134
+ ) -> tuple[list[dict], int]:
135
+ """Return a paginated list of all SOPs and the total count."""
136
+ query: dict = {}
137
+ total = await db[COLLECTION].count_documents(query)
138
+ cursor = (
139
+ db[COLLECTION]
140
+ .find(query)
141
+ .skip((page - 1) * page_size)
142
+ .limit(page_size)
143
+ )
144
+ return [_normalize_sop_record(d) async for d in cursor], total
145
+
146
+
147
+ async def get_by_id(
148
+ db: AsyncIOMotorDatabase,
149
+ sop_id: str,
150
+ ) -> Optional[dict]:
151
+ """Fetch a single SOP by its id."""
152
+ doc = await db[COLLECTION].find_one({"id": sop_id})
153
+ return _normalize_sop_record(doc) if doc else None
154
+
155
+
156
+ async def get_by_process_definition_id(
157
+ db: AsyncIOMotorDatabase,
158
+ process_definition_id: str,
159
+ ) -> Optional[dict]:
160
+ """Fetch the SOP that wraps a given process_definition."""
161
+ if not process_definition_id:
162
+ return None
163
+ doc = await db[COLLECTION].find_one({"process_definition.process_definition_id": process_definition_id})
164
+ return _normalize_sop_record(doc) if doc else None
165
+
166
+
167
+ async def get_by_author(
168
+ db: AsyncIOMotorDatabase,
169
+ authored_by: str,
170
+ ) -> list[dict]:
171
+ """Return all SOPs created by a given author."""
172
+ if not authored_by:
173
+ return []
174
+ cursor = db[COLLECTION].find({"authored_by": authored_by})
175
+ return [_normalize_sop_record(d) async for d in cursor]
176
+
177
+
178
+ async def get_by_approver(
179
+ db: AsyncIOMotorDatabase,
180
+ approved_by: str,
181
+ ) -> list[dict]:
182
+ """Return all SOPs approved by a given approver."""
183
+ if not approved_by:
184
+ return []
185
+ cursor = db[COLLECTION].find({"approved_by": approved_by})
186
+ return [_normalize_sop_record(d) async for d in cursor]
187
+
188
+
189
+ async def get_by_tag(
190
+ db: AsyncIOMotorDatabase,
191
+ tag: str,
192
+ ) -> list[dict]:
193
+ """Return all SOPs that carry the given tag."""
194
+ if not tag:
195
+ return []
196
+ cursor = db[COLLECTION].find({"tags": tag})
197
+ return [_normalize_sop_record(d) async for d in cursor]
198
+
199
+
200
+ async def get_by_version(
201
+ db: AsyncIOMotorDatabase,
202
+ version: str,
203
+ page: int = 1,
204
+ page_size: int = 20,
205
+ ) -> tuple[list[dict], int]:
206
+ """Return paginated SOPs that match a specific version string."""
207
+ query = {"version": version}
208
+ total = await db[COLLECTION].count_documents(query)
209
+ cursor = (
210
+ db[COLLECTION]
211
+ .find(query)
212
+ .skip((page - 1) * page_size)
213
+ .limit(page_size)
214
+ )
215
+ return [_normalize_sop_record(d) async for d in cursor], total
216
+
217
+
218
+ def _search_text(record: dict) -> str:
219
+ values: list[str] = []
220
+
221
+ def collect(value):
222
+ if value is None:
223
+ return
224
+ if isinstance(value, (str, int, float, bool)):
225
+ values.append(str(value))
226
+ return
227
+ if isinstance(value, dict):
228
+ for item in value.values():
229
+ collect(item)
230
+ return
231
+ if isinstance(value, list):
232
+ for item in value:
233
+ collect(item)
234
+
235
+ for key in (
236
+ "id",
237
+ "sop_id",
238
+ "document_id",
239
+ "title",
240
+ "name",
241
+ "purpose",
242
+ "scope",
243
+ "version",
244
+ "entity_name",
245
+ "entity_type",
246
+ "plant_id",
247
+ "line_id",
248
+ "stage_id",
249
+ "workstation_id",
250
+ "machine_id",
251
+ "equipment_id",
252
+ "tags",
253
+ "process_definition",
254
+ "prechecks",
255
+ "postchecks",
256
+ "constraints",
257
+ "corrective_actions",
258
+ "notes",
259
+ "remarks",
260
+ "references",
261
+ ):
262
+ collect(record.get(key))
263
+ return " ".join(values).lower()
264
+
265
+
266
+ def _search_score(query: str, record: dict) -> float:
267
+ terms = [term for term in re.findall(r"[a-z0-9]+", query.lower()) if len(term) >= 3]
268
+ if not terms:
269
+ return 0.0
270
+ haystack = _search_text(record)
271
+ return sum(1 for term in terms if term in haystack) / len(terms)
272
+
273
+
274
+ async def search(
275
+ db: AsyncIOMotorDatabase,
276
+ query: str,
277
+ limit: int = 20,
278
+ ) -> list[dict]:
279
+ """Search SOP records by free text across SOP and embedded process sections."""
280
+ terms = [term for term in re.findall(r"[A-Za-z0-9_-]+", query or "") if len(term) >= 3][:10]
281
+ if not terms:
282
+ return []
283
+ regex = "|".join(re.escape(term) for term in terms)
284
+ text_match = {"$regex": regex, "$options": "i"}
285
+ mongo_query = {
286
+ "$or": [
287
+ {"id": text_match},
288
+ {"sop_id": text_match},
289
+ {"document_id": text_match},
290
+ {"title": text_match},
291
+ {"name": text_match},
292
+ {"purpose": text_match},
293
+ {"scope": text_match},
294
+ {"entity_name": text_match},
295
+ {"entity_type": text_match},
296
+ {"workstation_id": text_match},
297
+ {"machine_id": text_match},
298
+ {"equipment_id": text_match},
299
+ {"tags": text_match},
300
+ {"process_definition.name": text_match},
301
+ {"process_definition.description": text_match},
302
+ {"prechecks.description": text_match},
303
+ {"postchecks.description": text_match},
304
+ {"constraints.description": text_match},
305
+ {"corrective_actions.description": text_match},
306
+ {"notes": text_match},
307
+ {"remarks": text_match},
308
+ ]
309
+ }
310
+ cursor = db[COLLECTION].find(mongo_query).limit(max(limit * 4, limit))
311
+ scored: list[tuple[float, dict]] = []
312
+ async for document in cursor:
313
+ normalized = _normalize_sop_record(document)
314
+ if normalized:
315
+ scored.append((_search_score(query, normalized), normalized))
316
+ scored.sort(key=lambda item: item[0], reverse=True)
317
+ return [record for score, record in scored if score > 0][:limit]
318
+
319
+
320
+ def _text_value(value: Any) -> str | None:
321
+ if value is None:
322
+ return None
323
+ text = str(value).strip()
324
+ return text or None
325
+
326
+
327
+ def _dedupe(values: list[str | None]) -> list[str]:
328
+ seen: set[str] = set()
329
+ result: list[str] = []
330
+ for value in values:
331
+ if value and value not in seen:
332
+ seen.add(value)
333
+ result.append(value)
334
+ return result
335
+
336
+
337
+ def _record_id(record: dict) -> str | None:
338
+ for key in ("id", "sop_id", "document_id", "entity_id", "stage_id", "workstation_id", "machine_id", "equipment_id"):
339
+ value = _text_value(record.get(key))
340
+ if value:
341
+ return value
342
+ return None
343
+
344
+
345
+ async def _lookup_stage_ids_for_sop(db: AsyncIOMotorDatabase, sop: dict) -> list[str]:
346
+ process_definition = sop.get("process_definition") if isinstance(sop.get("process_definition"), dict) else {}
347
+ stage_ids = [
348
+ _text_value(sop.get("stage_id")),
349
+ _text_value(process_definition.get("stage_id")),
350
+ ]
351
+
352
+ workstation_id = _text_value(sop.get("workstation_id") or process_definition.get("workstation_id"))
353
+ if workstation_id:
354
+ workstation = await db["workstations"].find_one(
355
+ {
356
+ "$or": [
357
+ {"id": workstation_id},
358
+ {"workstation_id": workstation_id},
359
+ {"entity_id": workstation_id},
360
+ ]
361
+ },
362
+ {"_id": 0, "stage_id": 1},
363
+ )
364
+ if workstation:
365
+ stage_ids.append(_text_value(workstation.get("stage_id")))
366
+
367
+ for collection, key in (
368
+ ("pharmaceutical_machines", "machine_id"),
369
+ ("machines", "machine_id"),
370
+ ("pharmaceutical_equipment", "equipment_id"),
371
+ ("equipment", "equipment_id"),
372
+ ):
373
+ entity_id = _text_value(sop.get(key) or process_definition.get(key))
374
+ if not entity_id:
375
+ continue
376
+ entity = await db[collection].find_one(
377
+ {
378
+ "$or": [
379
+ {"id": entity_id},
380
+ {key: entity_id},
381
+ {"entity_id": entity_id},
382
+ ]
383
+ },
384
+ {"_id": 0, "stage_id": 1},
385
+ )
386
+ if entity:
387
+ stage_ids.append(_text_value(entity.get("stage_id")))
388
+
389
+ return _dedupe(stage_ids)
390
+
391
+
392
+ async def _material_flow_graph(
393
+ db: AsyncIOMotorDatabase,
394
+ stage_ids: list[str],
395
+ *,
396
+ include_upstream: bool,
397
+ include_downstream: bool,
398
+ max_depth: int,
399
+ ) -> tuple[list[dict], list[str]]:
400
+ if not stage_ids:
401
+ return [], []
402
+ visited_stages = set(stage_ids)
403
+ frontier = set(stage_ids)
404
+ flows: list[dict] = []
405
+ flow_keys: set[tuple[str, str, str]] = set()
406
+
407
+ for depth in range(1, max_depth + 1):
408
+ clauses = []
409
+ if include_downstream:
410
+ clauses.append({"source_stage_id": {"$in": list(frontier)}})
411
+ if include_upstream:
412
+ clauses.append({"target_stage_id": {"$in": list(frontier)}})
413
+ if not clauses:
414
+ break
415
+
416
+ cursor = db[MATERIAL_FLOW_COLLECTION].find({"$or": clauses}, {"_id": 0})
417
+ next_frontier: set[str] = set()
418
+ async for flow in cursor:
419
+ source_stage_id = _text_value(flow.get("source_stage_id"))
420
+ target_stage_id = _text_value(flow.get("target_stage_id"))
421
+ if not source_stage_id or not target_stage_id:
422
+ continue
423
+ direction = "downstream" if source_stage_id in frontier else "upstream"
424
+ connected_stage_id = target_stage_id if direction == "downstream" else source_stage_id
425
+ if direction == "downstream" and not include_downstream:
426
+ continue
427
+ if direction == "upstream" and not include_upstream:
428
+ continue
429
+
430
+ key = (
431
+ _text_value(flow.get("flow_id") or flow.get("id")) or f"{source_stage_id}->{target_stage_id}",
432
+ source_stage_id,
433
+ target_stage_id,
434
+ )
435
+ if key not in flow_keys:
436
+ flow_keys.add(key)
437
+ flows.append(
438
+ {
439
+ **flow,
440
+ "flow_id": key[0],
441
+ "source_stage_id": source_stage_id,
442
+ "target_stage_id": target_stage_id,
443
+ "direction_from_source": direction,
444
+ "depth": depth,
445
+ }
446
+ )
447
+ if connected_stage_id not in visited_stages:
448
+ visited_stages.add(connected_stage_id)
449
+ next_frontier.add(connected_stage_id)
450
+ if not next_frontier:
451
+ break
452
+ frontier = next_frontier
453
+
454
+ return flows, sorted(visited_stages - set(stage_ids))
455
+
456
+
457
+ async def _linked_sops_for_stages(
458
+ db: AsyncIOMotorDatabase,
459
+ stage_ids: list[str],
460
+ *,
461
+ source_sop_id: str,
462
+ ) -> list[dict]:
463
+ if not stage_ids:
464
+ return []
465
+
466
+ workstations = await db["workstations"].find(
467
+ {"stage_id": {"$in": stage_ids}},
468
+ {"_id": 0, "id": 1, "workstation_id": 1},
469
+ ).to_list(length=500)
470
+ workstation_ids = _dedupe([_text_value(item.get("id") or item.get("workstation_id")) for item in workstations])
471
+
472
+ machines = await db["pharmaceutical_machines"].find(
473
+ {"stage_id": {"$in": stage_ids}},
474
+ {"_id": 0, "id": 1, "machine_id": 1},
475
+ ).to_list(length=500)
476
+ machine_ids = _dedupe([_text_value(item.get("id") or item.get("machine_id")) for item in machines])
477
+
478
+ equipment = await db["pharmaceutical_equipment"].find(
479
+ {"stage_id": {"$in": stage_ids}},
480
+ {"_id": 0, "id": 1, "equipment_id": 1},
481
+ ).to_list(length=500)
482
+ equipment_ids = _dedupe([_text_value(item.get("id") or item.get("equipment_id")) for item in equipment])
483
+
484
+ clauses: list[dict] = [
485
+ {"stage_id": {"$in": stage_ids}},
486
+ {"process_definition.stage_id": {"$in": stage_ids}},
487
+ ]
488
+ if workstation_ids:
489
+ clauses.extend([{"workstation_id": {"$in": workstation_ids}}, {"process_definition.workstation_id": {"$in": workstation_ids}}])
490
+ if machine_ids:
491
+ clauses.extend([{"machine_id": {"$in": machine_ids}}, {"process_definition.machine_id": {"$in": machine_ids}}])
492
+ if equipment_ids:
493
+ clauses.extend([{"equipment_id": {"$in": equipment_ids}}, {"process_definition.equipment_id": {"$in": equipment_ids}}])
494
+
495
+ cursor = db[COLLECTION].find({"$or": clauses}, {"_id": 0, "embedding": 0})
496
+ linked: list[dict] = []
497
+ seen: set[str] = set()
498
+ async for document in cursor:
499
+ normalized = _normalize_sop_record(document)
500
+ if not normalized:
501
+ continue
502
+ sop_id = _record_id(normalized)
503
+ if not sop_id or sop_id == source_sop_id or sop_id in seen:
504
+ continue
505
+ seen.add(sop_id)
506
+ linked.append(normalized)
507
+ return linked
508
+
509
+
510
+ def _changed_fields(proposed_values: dict) -> list[str]:
511
+ return sorted(str(key) for key in proposed_values.keys())
512
+
513
+
514
+ def _relationship_path(sop: dict, flows: list[dict]) -> list[dict]:
515
+ sop_stage_id = _text_value(sop.get("stage_id") or (sop.get("process_definition") or {}).get("stage_id"))
516
+ if not sop_stage_id:
517
+ return []
518
+ return [
519
+ {
520
+ "flow_id": flow.get("flow_id"),
521
+ "source_stage_id": flow.get("source_stage_id"),
522
+ "target_stage_id": flow.get("target_stage_id"),
523
+ "direction": flow.get("direction_from_source"),
524
+ "depth": flow.get("depth"),
525
+ }
526
+ for flow in flows
527
+ if flow.get("source_stage_id") == sop_stage_id or flow.get("target_stage_id") == sop_stage_id
528
+ ]
529
+
530
+
531
+ def _impact_review_values(source_sop: dict, impacted_sop: dict, proposed_values: dict, flows: list[dict]) -> dict:
532
+ now = utc_now()
533
+ source_sop_id = _record_id(source_sop)
534
+ impacted_sop_id = _record_id(impacted_sop)
535
+ review = {
536
+ "required": True,
537
+ "status": "Pending Approval",
538
+ "source_sop_id": source_sop_id,
539
+ "source_sop_title": source_sop.get("title") or source_sop_id,
540
+ "impacted_sop_id": impacted_sop_id,
541
+ "changed_fields": _changed_fields(proposed_values),
542
+ "relationship_path": _relationship_path(impacted_sop, flows),
543
+ "analyzed_at": now,
544
+ }
545
+ return {
546
+ "linked_sop_change_required": True,
547
+ "requires_review": True,
548
+ "change_impact_review": review,
549
+ "updated_at": now,
550
+ }
551
+
552
+
553
+ def _effect_stage_names(flows: list[dict], direction: str) -> list[str]:
554
+ names: list[str] = []
555
+ for flow in flows:
556
+ if flow.get("direction_from_source") != direction:
557
+ continue
558
+ key = "target_stage_name" if direction == "downstream" else "source_stage_name"
559
+ value = _text_value(flow.get(key))
560
+ if value and value not in names:
561
+ names.append(value)
562
+ return names
563
+
564
+
565
+ def _stage_ids_for_direction(flows: list[dict], direction: str) -> list[str]:
566
+ ids: list[str] = []
567
+ for flow in flows:
568
+ if flow.get("direction_from_source") != direction:
569
+ continue
570
+ key = "target_stage_id" if direction == "downstream" else "source_stage_id"
571
+ value = _text_value(flow.get(key))
572
+ if value and value not in ids:
573
+ ids.append(value)
574
+ return ids
575
+
576
+
577
+ def _proposed_change_text(proposed_values: dict) -> str:
578
+ fragments: list[str] = []
579
+ for section, value in dict(proposed_values or {}).items():
580
+ if isinstance(value, dict):
581
+ for key, nested_value in value.items():
582
+ if isinstance(nested_value, (str, int, float, bool)):
583
+ fragments.append(f"{section}.{key}={nested_value}")
584
+ elif isinstance(value, (str, int, float, bool)):
585
+ fragments.append(f"{section}={value}")
586
+ return "; ".join(fragments[:8])
587
+
588
+
589
+ def _change_effects(
590
+ source_sop: dict,
591
+ proposed_values: dict,
592
+ flows: list[dict],
593
+ linked_sops: list[dict],
594
+ required_changes: list[dict],
595
+ ) -> dict:
596
+ changed_fields = _changed_fields(proposed_values)
597
+ downstream_stage_names = _effect_stage_names(flows, "downstream")
598
+ upstream_stage_names = _effect_stage_names(flows, "upstream")
599
+ downstream_stage_ids = _stage_ids_for_direction(flows, "downstream")
600
+ upstream_stage_ids = _stage_ids_for_direction(flows, "upstream")
601
+ proposed_text = _proposed_change_text(proposed_values).lower()
602
+ proposed = dict(proposed_values or {})
603
+ constraints = proposed.get("constraints") if isinstance(proposed.get("constraints"), dict) else {}
604
+ postchecks = proposed.get("postchecks") if isinstance(proposed.get("postchecks"), dict) else {}
605
+
606
+ release_gate = bool(
607
+ constraints.get("quality_hold_required")
608
+ or constraints.get("downstream_release_requires_qa_disposition")
609
+ or "qa" in proposed_text
610
+ or "hold" in proposed_text
611
+ or "release" in proposed_text
612
+ )
613
+ deviation_required = bool("deviation" in proposed_text or "capa" in proposed_text)
614
+ postcheck_tightened = bool(postchecks or "postchecks" in changed_fields)
615
+ downstream_start_blocked = release_gate and bool(downstream_stage_names)
616
+
617
+ operational_effects: list[dict[str, Any]] = []
618
+ if release_gate:
619
+ operational_effects.append(
620
+ {
621
+ "effect": "Adds a QA release gate at the source SOP stage.",
622
+ "what_changes": "Material cannot be released from the changed SOP stage until the new postcheck/constraint is satisfied.",
623
+ "scope": source_sop.get("stage_id"),
624
+ }
625
+ )
626
+ if downstream_start_blocked:
627
+ operational_effects.append(
628
+ {
629
+ "effect": "Blocks downstream stage start when the gate is not satisfied.",
630
+ "what_changes": "Downstream execution must wait for QA disposition before consuming the released material.",
631
+ "affected_stages": downstream_stage_names,
632
+ }
633
+ )
634
+ if deviation_required:
635
+ operational_effects.append(
636
+ {
637
+ "effect": "Turns exceptions into formal quality events.",
638
+ "what_changes": "Alarms, quantity mismatches, or identity exceptions require deviation/CAPA review instead of simple operator acknowledgement.",
639
+ "affected_records": ["deviation_records", "capa_records", "quality_refs"],
640
+ }
641
+ )
642
+ if postcheck_tightened:
643
+ operational_effects.append(
644
+ {
645
+ "effect": "Increases closeout evidence requirements.",
646
+ "what_changes": "Operators and supervisors must capture stronger postcheck evidence before handoff.",
647
+ "affected_records": ["document_metadata", "work_orders", "part11_audit_trail"],
648
+ }
649
+ )
650
+
651
+ if not operational_effects:
652
+ operational_effects.append(
653
+ {
654
+ "effect": "No material-flow effect detected.",
655
+ "what_changes": "The proposed change stays local to the source SOP unless manually linked to additional stages.",
656
+ "scope": source_sop.get("stage_id"),
657
+ }
658
+ )
659
+
660
+ risk_effects = [
661
+ {
662
+ "risk": "Quality escape risk",
663
+ "direction": "decreases" if release_gate or deviation_required else "unchanged",
664
+ "reason": "The changed SOP requires QA disposition before downstream material movement." if release_gate else "No new QA gate was detected.",
665
+ },
666
+ {
667
+ "risk": "Cycle time / queue time",
668
+ "direction": "increases" if release_gate else "unchanged",
669
+ "reason": "Downstream work may wait for QA review and deviation/CAPA closure.",
670
+ },
671
+ {
672
+ "risk": "Documentation burden",
673
+ "direction": "increases" if postcheck_tightened or deviation_required else "unchanged",
674
+ "reason": "More evidence, signatures, and quality-event links are required.",
675
+ },
676
+ ]
677
+
678
+ changes_needed: list[dict[str, Any]] = [
679
+ {
680
+ "area": "Source SOP",
681
+ "change": "Revise the source SOP text, postchecks, constraints, and corrective-action trigger.",
682
+ "why": "The desired result has to be encoded in the controlled instruction, not only described in the impact analysis.",
683
+ "target_records": [_record_id(source_sop)],
684
+ "approval_required": True,
685
+ },
686
+ {
687
+ "area": "ProcessDefinition",
688
+ "change": "Add or update the workflow gate that evaluates the new postcheck before material release.",
689
+ "why": "The simulator and live workflow need an executable gate, otherwise downstream stages can still start from the old workflow.",
690
+ "target_records": ["process-definitions", "process_definition_postchecks", "process_constraints"],
691
+ "approval_required": True,
692
+ },
693
+ {
694
+ "area": "Audit and evidence",
695
+ "change": "Require evidence capture, e-signature, and audit entries for the new decision point.",
696
+ "why": "The desired result depends on proving who reviewed the exception, when, and on what evidence.",
697
+ "target_records": ["document_metadata", "node_reference_links", "part11_audit_trail"],
698
+ "approval_required": False,
699
+ },
700
+ ]
701
+ if downstream_stage_names:
702
+ changes_needed.append(
703
+ {
704
+ "area": "Downstream SOPs",
705
+ "change": "Update downstream start prechecks to require QA disposition/release from the source stage.",
706
+ "why": "This is what prevents Tablet Compression, Capsule Filling, or later stages from consuming unreleased material.",
707
+ "target_stages": downstream_stage_names,
708
+ "target_stage_ids": downstream_stage_ids,
709
+ "target_sop_count": len(
710
+ {
711
+ _record_id(sop)
712
+ for sop in linked_sops
713
+ if _text_value(sop.get("stage_id") or (sop.get("process_definition") or {}).get("stage_id")) in downstream_stage_ids
714
+ }
715
+ ),
716
+ "approval_required": True,
717
+ }
718
+ )
719
+ if upstream_stage_names:
720
+ changes_needed.append(
721
+ {
722
+ "area": "Upstream SOPs",
723
+ "change": "Review upstream handoff and material identity checks for compatibility with the stricter downstream release gate.",
724
+ "why": "Upstream stages may need clearer lot/quantity/identity evidence so the new gate can be satisfied without rework.",
725
+ "target_stages": upstream_stage_names,
726
+ "target_stage_ids": upstream_stage_ids,
727
+ "target_sop_count": len(
728
+ {
729
+ _record_id(sop)
730
+ for sop in linked_sops
731
+ if _text_value(sop.get("stage_id") or (sop.get("process_definition") or {}).get("stage_id")) in upstream_stage_ids
732
+ }
733
+ ),
734
+ "approval_required": True,
735
+ }
736
+ )
737
+ if deviation_required:
738
+ changes_needed.append(
739
+ {
740
+ "area": "Quality workflows",
741
+ "change": "Link deviation/CAPA creation, QA disposition, and effectiveness checks to the SOP exception path.",
742
+ "why": "This turns the desired quality behavior into enforceable records and follow-up actions.",
743
+ "target_records": ["deviation_records", "capa_records", "quality_refs", "corrective_actions"],
744
+ "approval_required": True,
745
+ }
746
+ )
747
+ if release_gate:
748
+ changes_needed.append(
749
+ {
750
+ "area": "Execution scheduling",
751
+ "change": "Make downstream work orders/bookings wait for the QA release signal when the source gate is open.",
752
+ "why": "Without this scheduling change, operators may still see downstream work as executable.",
753
+ "target_records": ["work_orders", "calendar_bookings", "timesheets"],
754
+ "approval_required": False,
755
+ }
756
+ )
757
+ changes_needed.append(
758
+ {
759
+ "area": "Simulation",
760
+ "change": "Rerun positive and negative simulations after the SOP/process changes are staged.",
761
+ "why": "Positive run proves compliant material can still flow; negative run proves exceptions stop downstream release.",
762
+ "target_records": ["world_model_simulation_runs", "world_model_simulation_audits"],
763
+ "approval_required": False,
764
+ }
765
+ )
766
+
767
+ return {
768
+ "summary": (
769
+ "This change adds a controlled release gate and can delay downstream execution until QA disposition is complete."
770
+ if release_gate
771
+ else "This change is traceable, but no downstream release gate was detected from the proposed values."
772
+ ),
773
+ "changed_fields": changed_fields,
774
+ "detected_controls": {
775
+ "release_gate": release_gate,
776
+ "deviation_or_capa_required": deviation_required,
777
+ "postcheck_tightened": postcheck_tightened,
778
+ "downstream_start_blocked_until_disposition": downstream_start_blocked,
779
+ },
780
+ "material_flow_effect": {
781
+ "upstream_context_stages": upstream_stage_names,
782
+ "upstream_context_stage_ids": upstream_stage_ids,
783
+ "downstream_affected_stages": downstream_stage_names,
784
+ "downstream_affected_stage_ids": downstream_stage_ids,
785
+ "connections_analyzed": len(flows),
786
+ },
787
+ "operational_effects": operational_effects,
788
+ "risk_effects": risk_effects,
789
+ "changes_needed_to_get_desired_result": changes_needed,
790
+ "records_and_workflows_affected": {
791
+ "linked_sops_requiring_review": len(linked_sops),
792
+ "required_change_actions": len(required_changes),
793
+ "approval_required": bool(required_changes or release_gate or deviation_required),
794
+ "audit_required": True,
795
+ "collections": [
796
+ "sops",
797
+ "process-definitions",
798
+ "process_definition_postchecks",
799
+ "process_constraints",
800
+ "corrective_actions",
801
+ "deviation_records",
802
+ "capa_records",
803
+ "node_reference_links",
804
+ ],
805
+ },
806
+ "expected_operator_experience": (
807
+ "The operator can complete the SOP only after recording the stricter postcheck evidence. "
808
+ "If an alarm, mismatch, or identity exception exists, the downstream handoff pauses until QA disposition is recorded."
809
+ if release_gate
810
+ else "The operator sees the revised SOP text/checks, but downstream execution is not automatically paused by material-flow logic."
811
+ ),
812
+ }
813
+
814
+
815
+ def _sop_graph(source_sop: dict, source_stage_ids: list[str], flows: list[dict], linked_sops: list[dict]) -> dict:
816
+ nodes: list[dict] = []
817
+ edges: list[dict] = []
818
+ seen_nodes: set[tuple[str, str]] = set()
819
+
820
+ def add_node(node_type: str, node_id: str | None, **properties: Any) -> None:
821
+ if not node_id:
822
+ return
823
+ key = (node_type, node_id)
824
+ if key in seen_nodes:
825
+ return
826
+ seen_nodes.add(key)
827
+ nodes.append({"node_id": node_id, "node_type": node_type, **properties})
828
+
829
+ source_sop_id = _record_id(source_sop)
830
+ add_node("sop", source_sop_id, title=source_sop.get("title"), role="source")
831
+ for stage_id in source_stage_ids:
832
+ add_node("stage", stage_id, role="source_stage")
833
+ edges.append({"type": "HAS_SOP", "source_type": "stage", "source_id": stage_id, "target_type": "sop", "target_id": source_sop_id})
834
+
835
+ for flow in flows:
836
+ source_stage_id = flow.get("source_stage_id")
837
+ target_stage_id = flow.get("target_stage_id")
838
+ add_node("stage", source_stage_id, name=flow.get("source_stage_name"))
839
+ add_node("stage", target_stage_id, name=flow.get("target_stage_name"))
840
+ edges.append(
841
+ {
842
+ "type": "MATERIAL_FLOWS_TO",
843
+ "source_type": "stage",
844
+ "source_id": source_stage_id,
845
+ "target_type": "stage",
846
+ "target_id": target_stage_id,
847
+ "flow_id": flow.get("flow_id"),
848
+ "depth": flow.get("depth"),
849
+ }
850
+ )
851
+
852
+ for sop in linked_sops:
853
+ sop_id = _record_id(sop)
854
+ stage_id = _text_value(sop.get("stage_id") or (sop.get("process_definition") or {}).get("stage_id"))
855
+ add_node("sop", sop_id, title=sop.get("title"), role="impacted")
856
+ if stage_id:
857
+ add_node("stage", stage_id)
858
+ edges.append({"type": "HAS_SOP", "source_type": "stage", "source_id": stage_id, "target_type": "sop", "target_id": sop_id})
859
+ edges.append({"type": "SOP_CHANGE_IMPACTS", "source_type": "sop", "source_id": source_sop_id, "target_type": "sop", "target_id": sop_id})
860
+ edges.append({"type": "REQUIRES_REVIEW", "source_type": "sop", "source_id": sop_id, "target_type": "change", "target_id": source_sop_id})
861
+
862
+ return {"nodes": nodes, "edges": edges}
863
+
864
+
865
+ async def analyze_sop_change_impact(
866
+ db: AsyncIOMotorDatabase,
867
+ sop_id: str,
868
+ proposed_values: dict,
869
+ *,
870
+ change_reason: str = "SOP revision impact analysis",
871
+ include_upstream: bool = True,
872
+ include_downstream: bool = True,
873
+ max_depth: int = 2,
874
+ ) -> dict:
875
+ source_sop = await get_by_id(db, sop_id)
876
+ if not source_sop:
877
+ return {}
878
+
879
+ source_sop_id = _record_id(source_sop) or sop_id
880
+ source_stage_ids = await _lookup_stage_ids_for_sop(db, source_sop)
881
+ production_connections, connected_stage_ids = await _material_flow_graph(
882
+ db,
883
+ source_stage_ids,
884
+ include_upstream=include_upstream,
885
+ include_downstream=include_downstream,
886
+ max_depth=max_depth,
887
+ )
888
+ linked_sops = await _linked_sops_for_stages(db, connected_stage_ids, source_sop_id=source_sop_id)
889
+ required_changes = [
890
+ {
891
+ "collection": COLLECTION,
892
+ "entity_id": _record_id(sop),
893
+ "entity_name": sop.get("title") or _record_id(sop),
894
+ "reason": change_reason,
895
+ "impact_type": "material_flow_linked_sop_review",
896
+ "proposed_values": _impact_review_values(source_sop, sop, proposed_values, production_connections),
897
+ }
898
+ for sop in linked_sops
899
+ if _record_id(sop)
900
+ ]
901
+ effects = _change_effects(source_sop, proposed_values, production_connections, linked_sops, required_changes)
902
+ return {
903
+ "source_sop": source_sop,
904
+ "source_change": {
905
+ "collection": COLLECTION,
906
+ "entity_id": source_sop_id,
907
+ "entity_name": source_sop.get("title") or source_sop_id,
908
+ "reason": change_reason,
909
+ "proposed_values": {**dict(proposed_values or {}), "updated_at": utc_now()},
910
+ },
911
+ "effects": effects,
912
+ "production_connections": production_connections,
913
+ "linked_sops": [
914
+ {
915
+ "id": _record_id(sop),
916
+ "title": sop.get("title"),
917
+ "stage_id": sop.get("stage_id") or (sop.get("process_definition") or {}).get("stage_id"),
918
+ "workstation_id": sop.get("workstation_id") or (sop.get("process_definition") or {}).get("workstation_id"),
919
+ "relationship_path": _relationship_path(sop, production_connections),
920
+ }
921
+ for sop in linked_sops
922
+ ],
923
+ "required_changes": required_changes,
924
+ "sop_graph": _sop_graph(source_sop, source_stage_ids, production_connections, linked_sops),
925
+ }
926
+
927
+
928
+ async def create_sop_change_control(
929
+ db: AsyncIOMotorDatabase,
930
+ impact: dict,
931
+ *,
932
+ current_user: dict,
933
+ change_type: str = "Major",
934
+ risk_score: int | None = 45,
935
+ approval_chain: list[dict] | None = None,
936
+ ) -> dict:
937
+ from services.auth.routers.auth import _build_change_control_record, _create_proposed_change_nodes
938
+ from services.common.approval_chains import create_approval_tracking_and_notifications
939
+
940
+ source_change = impact.get("source_change") or {}
941
+ required_changes = impact.get("required_changes") or []
942
+ proposed_changes = [source_change, *required_changes]
943
+ affected_entities = [
944
+ {
945
+ "type": "sop",
946
+ "collection": COLLECTION,
947
+ "entity_id": change.get("entity_id"),
948
+ "name": change.get("entity_name"),
949
+ }
950
+ for change in proposed_changes
951
+ if change.get("entity_id")
952
+ ]
953
+ payload = {
954
+ "title": f"SOP material-flow impact change: {source_change.get('entity_name') or source_change.get('entity_id')}",
955
+ "description": "Source SOP revision with graph-derived downstream/upstream SOP review requirements.",
956
+ "change_type": change_type,
957
+ "reason": source_change.get("reason") or "SOP revision impact analysis",
958
+ "justification": "Production connection and material-flow graph identified linked SOPs requiring review.",
959
+ "affected_entity_class": "sops",
960
+ "affected_entities": affected_entities,
961
+ "affected_collections": [COLLECTION],
962
+ "graph_impact_summary": {
963
+ "source_sop": impact.get("source_sop"),
964
+ "sop_graph": impact.get("sop_graph"),
965
+ "production_connections": impact.get("production_connections"),
966
+ "linked_sops": impact.get("linked_sops"),
967
+ },
968
+ "impact_assessment": "Linked SOP impact generated from production connections and material flow.",
969
+ "proposed_changes": proposed_changes,
970
+ "risk_score": risk_score,
971
+ "validation_required": True,
972
+ "sop_revision_required": True,
973
+ "retraining_required": True,
974
+ "status": "Open",
975
+ "approval_chain": approval_chain or [],
976
+ }
977
+ record = await _build_change_control_record(db, payload, current_user)
978
+ graph_relationships = list(record.get("graph_relationships") or [])
979
+ graph_relationships.extend(impact.get("sop_graph", {}).get("edges") or [])
980
+ record["graph_relationships"] = graph_relationships
981
+ record["sop_change_impact_graph_id"] = f"sop-impact-graph-{uuid4().hex}"
982
+ from services.common.compliance import sha256_hex
983
+
984
+ record["record_hash"] = sha256_hex(record)
985
+ await db[CHANGE_CONTROL_COLLECTION].insert_one(record)
986
+
987
+ proposed_nodes = await _create_proposed_change_nodes(db, record, current_user)
988
+ if proposed_nodes:
989
+ await db[CHANGE_CONTROL_COLLECTION].update_one(
990
+ {"change_control_id": record["change_control_id"]},
991
+ {
992
+ "$set": {
993
+ "proposed_change_nodes": proposed_nodes,
994
+ "proposed_change_node_ids": [node.get("proposed_change_id") for node in proposed_nodes],
995
+ "live_node_touched": False,
996
+ "updated_at": utc_now(),
997
+ }
998
+ },
999
+ )
1000
+ record["proposed_change_nodes"] = proposed_nodes
1001
+ record["proposed_change_node_ids"] = [node.get("proposed_change_id") for node in proposed_nodes]
1002
+ record["live_node_touched"] = False
1003
+
1004
+ approval_tokens = await create_approval_tracking_and_notifications(
1005
+ db,
1006
+ source_collection=CHANGE_CONTROL_COLLECTION,
1007
+ source_id=record["change_control_id"],
1008
+ source_title=record.get("title") or record.get("change_control_number") or record["change_control_id"],
1009
+ chain=record["approval_chain"],
1010
+ current_user_id=current_user.get("user_id") or current_user.get("sub"),
1011
+ )
1012
+ record["approval_token_records"] = approval_tokens
1013
+ return record
1014
+
1015
+
1016
+ # ---------------------------------------------------------------------------
1017
+ # Write
1018
+ # ---------------------------------------------------------------------------
1019
+
1020
+ async def create(
1021
+ db: AsyncIOMotorDatabase,
1022
+ data: SOPCreate,
1023
+ ) -> dict:
1024
+ """Insert a new SOP document and return it."""
1025
+ doc = data.model_dump()
1026
+ await db[COLLECTION].insert_one(doc)
1027
+ return _normalize_sop_record(doc)
1028
+
1029
+
1030
+ async def update(
1031
+ db: AsyncIOMotorDatabase,
1032
+ sop_id: str,
1033
+ data: SOPUpdate,
1034
+ ) -> Optional[dict]:
1035
+ """
1036
+ Partially update a SOP.
1037
+ Only non-None fields are written; returns the updated doc
1038
+ or None if no SOP matched.
1039
+ """
1040
+ fields = {k: v for k, v in data.model_dump().items() if v is not None}
1041
+ if not fields:
1042
+ return await get_by_id(db, sop_id)
1043
+ result = await db[COLLECTION].find_one_and_update(
1044
+ {"id": sop_id},
1045
+ {"$set": fields},
1046
+ return_document=True,
1047
+ )
1048
+ return _normalize_sop_record(result) if result else None
1049
+
1050
+
1051
+ async def update_process_definition(
1052
+ db: AsyncIOMotorDatabase,
1053
+ sop_id: str,
1054
+ process_definition: dict,
1055
+ ) -> Optional[dict]:
1056
+ """Replace the embedded process_definition sub-document."""
1057
+ result = await db[COLLECTION].find_one_and_update(
1058
+ {"id": sop_id},
1059
+ {"$set": {"process_definition": process_definition}},
1060
+ return_document=True,
1061
+ )
1062
+ return _normalize_sop_record(result) if result else None
1063
+
1064
+
1065
+ async def update_prechecks(
1066
+ db: AsyncIOMotorDatabase,
1067
+ sop_id: str,
1068
+ prechecks: dict,
1069
+ ) -> Optional[dict]:
1070
+ """Replace the embedded prechecks sub-document."""
1071
+ result = await db[COLLECTION].find_one_and_update(
1072
+ {"id": sop_id},
1073
+ {"$set": {"prechecks": prechecks}},
1074
+ return_document=True,
1075
+ )
1076
+ return _normalize_sop_record(result) if result else None
1077
+
1078
+
1079
+ async def update_postchecks(
1080
+ db: AsyncIOMotorDatabase,
1081
+ sop_id: str,
1082
+ postchecks: dict,
1083
+ ) -> Optional[dict]:
1084
+ """Replace the embedded postchecks sub-document."""
1085
+ result = await db[COLLECTION].find_one_and_update(
1086
+ {"id": sop_id},
1087
+ {"$set": {"postchecks": postchecks}},
1088
+ return_document=True,
1089
+ )
1090
+ return _normalize_sop_record(result) if result else None
1091
+
1092
+
1093
+ async def update_constraints(
1094
+ db: AsyncIOMotorDatabase,
1095
+ sop_id: str,
1096
+ constraints: dict,
1097
+ ) -> Optional[dict]:
1098
+ """Replace the embedded constraints sub-document."""
1099
+ result = await db[COLLECTION].find_one_and_update(
1100
+ {"id": sop_id},
1101
+ {"$set": {"constraints": constraints}},
1102
+ return_document=True,
1103
+ )
1104
+ return _normalize_sop_record(result) if result else None
1105
+
1106
+
1107
+ async def update_corrective_actions(
1108
+ db: AsyncIOMotorDatabase,
1109
+ sop_id: str,
1110
+ corrective_actions: dict,
1111
+ ) -> Optional[dict]:
1112
+ """Replace the embedded corrective_actions sub-document."""
1113
+ result = await db[COLLECTION].find_one_and_update(
1114
+ {"id": sop_id},
1115
+ {"$set": {"corrective_actions": corrective_actions}},
1116
+ return_document=True,
1117
+ )
1118
+ return _normalize_sop_record(result) if result else None
1119
+
1120
+
1121
+ async def add_reference(
1122
+ db: AsyncIOMotorDatabase,
1123
+ sop_id: str,
1124
+ reference: str,
1125
+ ) -> Optional[dict]:
1126
+ """Append a new URL/document reference to the references array."""
1127
+ result = await db[COLLECTION].find_one_and_update(
1128
+ {"id": sop_id},
1129
+ {"$addToSet": {"references": reference}},
1130
+ return_document=True,
1131
+ )
1132
+ return _normalize_sop_record(result) if result else None
1133
+
1134
+
1135
+ async def remove_reference(
1136
+ db: AsyncIOMotorDatabase,
1137
+ sop_id: str,
1138
+ reference: str,
1139
+ ) -> Optional[dict]:
1140
+ """Remove a specific reference from the references array."""
1141
+ result = await db[COLLECTION].find_one_and_update(
1142
+ {"id": sop_id},
1143
+ {"$pull": {"references": reference}},
1144
+ return_document=True,
1145
+ )
1146
+ return _normalize_sop_record(result) if result else None
1147
+
1148
+
1149
+ async def delete(
1150
+ db: AsyncIOMotorDatabase,
1151
+ sop_id: str,
1152
+ ) -> bool:
1153
+ """Delete a SOP by ID. Returns True if a document was removed."""
1154
+ result = await db[COLLECTION].delete_one({"id": sop_id})
1155
+ return result.deleted_count == 1