@open-mercato/cli 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

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 (278) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/build.mjs +7 -24
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
  4. package/dist/agentic/codex/enforcement-rules.md +16 -2
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
  9. package/dist/agentic/guides/cache.md +45 -0
  10. package/dist/agentic/guides/core.md +453 -0
  11. package/dist/agentic/guides/events.md +117 -0
  12. package/dist/agentic/guides/module-facts.json +3833 -115419
  13. package/dist/agentic/guides/module-system.md +127 -12
  14. package/dist/agentic/guides/modules/ai_assistant.md +28 -227
  15. package/dist/agentic/guides/modules/api_docs.md +2 -58
  16. package/dist/agentic/guides/modules/api_keys.md +7 -92
  17. package/dist/agentic/guides/modules/attachments.md +14 -119
  18. package/dist/agentic/guides/modules/audit_logs.md +10 -131
  19. package/dist/agentic/guides/modules/auth.md +33 -276
  20. package/dist/agentic/guides/modules/business_rules.md +14 -149
  21. package/dist/agentic/guides/modules/catalog.md +43 -475
  22. package/dist/agentic/guides/modules/channel_gmail.md +3 -82
  23. package/dist/agentic/guides/modules/channel_imap.md +3 -82
  24. package/dist/agentic/guides/modules/checkout.md +27 -299
  25. package/dist/agentic/guides/modules/communication_channels.md +33 -315
  26. package/dist/agentic/guides/modules/configs.md +8 -104
  27. package/dist/agentic/guides/modules/content.md +2 -57
  28. package/dist/agentic/guides/modules/currencies.md +17 -152
  29. package/dist/agentic/guides/modules/customer_accounts.md +45 -387
  30. package/dist/agentic/guides/modules/customers.md +85 -746
  31. package/dist/agentic/guides/modules/dashboards.md +10 -155
  32. package/dist/agentic/guides/modules/data_sync.md +16 -149
  33. package/dist/agentic/guides/modules/dictionaries.md +12 -107
  34. package/dist/agentic/guides/modules/directory.md +15 -148
  35. package/dist/agentic/guides/modules/entities.md +13 -145
  36. package/dist/agentic/guides/modules/events.md +2 -59
  37. package/dist/agentic/guides/modules/feature_toggles.md +9 -119
  38. package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
  39. package/dist/agentic/guides/modules/generators.md +2 -45
  40. package/dist/agentic/guides/modules/inbox_ops.md +27 -207
  41. package/dist/agentic/guides/modules/integrations.md +16 -146
  42. package/dist/agentic/guides/modules/messages.md +24 -223
  43. package/dist/agentic/guides/modules/notifications.md +8 -96
  44. package/dist/agentic/guides/modules/onboarding.md +6 -77
  45. package/dist/agentic/guides/modules/payment_gateways.md +21 -164
  46. package/dist/agentic/guides/modules/perspectives.md +7 -77
  47. package/dist/agentic/guides/modules/planner.md +18 -136
  48. package/dist/agentic/guides/modules/portal.md +2 -93
  49. package/dist/agentic/guides/modules/progress.md +14 -104
  50. package/dist/agentic/guides/modules/query_index.md +15 -154
  51. package/dist/agentic/guides/modules/record_locks.md +21 -206
  52. package/dist/agentic/guides/modules/resources.md +30 -197
  53. package/dist/agentic/guides/modules/sales.md +84 -678
  54. package/dist/agentic/guides/modules/scheduler.md +14 -133
  55. package/dist/agentic/guides/modules/search.md +6 -170
  56. package/dist/agentic/guides/modules/security.md +35 -294
  57. package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
  58. package/dist/agentic/guides/modules/sso.md +26 -151
  59. package/dist/agentic/guides/modules/staff.md +57 -369
  60. package/dist/agentic/guides/modules/storage_s3.md +5 -95
  61. package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
  62. package/dist/agentic/guides/modules/sync_excel.md +6 -75
  63. package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
  64. package/dist/agentic/guides/modules/translations.md +10 -104
  65. package/dist/agentic/guides/modules/webhooks.md +24 -203
  66. package/dist/agentic/guides/modules/wms.md +46 -441
  67. package/dist/agentic/guides/modules/workflows.md +43 -302
  68. package/dist/agentic/guides/queue.md +53 -0
  69. package/dist/agentic/guides/search.md +115 -0
  70. package/dist/agentic/guides/shared.md +108 -0
  71. package/dist/agentic/guides/ui.md +347 -0
  72. package/dist/agentic/shared/AGENTS.md.template +311 -124
  73. package/dist/agentic/shared/ai/agentic.config.json +0 -2
  74. package/dist/agentic/shared/ai/lessons.md +3 -26
  75. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
  76. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
  77. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
  78. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
  79. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
  80. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
  81. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
  82. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
  83. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
  84. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
  85. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
  86. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
  87. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
  88. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
  89. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
  90. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
  91. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
  92. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
  93. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
  94. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
  95. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
  96. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
  97. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
  98. package/dist/agentic/shared/ai/skills/tiers.json +21 -143
  99. package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
  100. package/dist/agentic/shared/ai/specs/README.md +3 -4
  101. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
  102. package/dist/agentic/shared/ai/trackers/github.md +4 -108
  103. package/dist/agentic/shared/scripts/install-skills.sh +694 -4
  104. package/dist/bin.js +1 -13
  105. package/dist/bin.js.map +2 -2
  106. package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
  107. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  108. package/dist/lib/agentic-init.js +3 -13
  109. package/dist/lib/agentic-init.js.map +2 -2
  110. package/dist/lib/agentic-setup.js +186 -418
  111. package/dist/lib/agentic-setup.js.map +2 -2
  112. package/dist/lib/generators/extensions/injection-widgets.js +0 -20
  113. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  114. package/dist/lib/generators/module-facts-discovery.js +7 -41
  115. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  116. package/dist/lib/generators/module-facts.js +45 -1387
  117. package/dist/lib/generators/module-facts.js.map +3 -3
  118. package/dist/lib/generators/module-registry.js +5 -11
  119. package/dist/lib/generators/module-registry.js.map +2 -2
  120. package/dist/lib/resolver.js +0 -2
  121. package/dist/lib/resolver.js.map +2 -2
  122. package/dist/lib/testing/integration.js +0 -5
  123. package/dist/lib/testing/integration.js.map +2 -2
  124. package/dist/mercato.js +1 -23
  125. package/dist/mercato.js.map +2 -2
  126. package/package.json +11 -13
  127. package/src/bin.ts +1 -33
  128. package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
  129. package/src/lib/__tests__/agentic-init.test.ts +5 -41
  130. package/src/lib/agentic-init.ts +3 -15
  131. package/src/lib/agentic-setup.ts +224 -530
  132. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
  133. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
  134. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
  135. package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
  136. package/src/lib/generators/extensions/injection-widgets.ts +0 -51
  137. package/src/lib/generators/module-facts-discovery.ts +7 -58
  138. package/src/lib/generators/module-facts.ts +46 -1851
  139. package/src/lib/generators/module-registry.ts +6 -16
  140. package/src/lib/resolver.ts +0 -3
  141. package/src/lib/testing/__tests__/integration.test.ts +0 -4
  142. package/src/lib/testing/integration.ts +0 -5
  143. package/src/mercato.ts +1 -26
  144. package/dist/agentic/guides/ai-workflows.md +0 -76
  145. package/dist/agentic/guides/architecture.md +0 -107
  146. package/dist/agentic/guides/backend-ui.md +0 -80
  147. package/dist/agentic/guides/contracts.md +0 -92
  148. package/dist/agentic/guides/extensions.md +0 -121
  149. package/dist/agentic/guides/framework-extension-points.md +0 -36
  150. package/dist/agentic/guides/integrations.md +0 -81
  151. package/dist/agentic/guides/modules/design_system.md +0 -102
  152. package/dist/agentic/guides/spec-delivery.md +0 -22
  153. package/dist/agentic/guides/testing-debugging.md +0 -80
  154. package/dist/agentic/guides/upstream/AGENTS.md +0 -298
  155. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
  156. package/dist/agentic/guides/upstream/manifest.json +0 -8
  157. package/dist/agentic/shared/ai/harness/README.md +0 -50
  158. package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
  159. package/dist/agentic/shared/ai/harness/cases.json +0 -13680
  160. package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
  161. package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
  162. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
  163. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
  164. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
  165. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
  166. package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
  167. package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
  168. package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
  169. package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
  170. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
  171. package/dist/agentic/shared/ai/harness/validators.json +0 -78
  172. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
  173. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
  174. package/dist/agentic/shared/ai/lessons/_template.md +0 -16
  175. package/dist/agentic/shared/ai/review-checklist.md +0 -36
  176. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
  177. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
  178. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
  179. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
  180. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
  181. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
  182. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
  183. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
  184. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
  185. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
  186. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
  187. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
  188. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
  189. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
  190. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
  191. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
  192. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
  193. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
  194. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
  195. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
  196. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
  197. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
  198. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
  199. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
  200. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
  201. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
  202. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
  203. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
  204. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
  205. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
  206. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
  207. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
  208. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
  209. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
  210. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
  211. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
  212. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
  213. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
  214. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
  215. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
  216. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
  217. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
  218. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
  219. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
  220. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
  221. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
  222. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
  223. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
  224. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
  225. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
  226. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
  227. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
  228. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
  229. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
  230. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
  231. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
  232. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
  233. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
  234. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
  235. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
  236. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
  237. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
  238. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
  239. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
  240. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
  241. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
  242. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
  243. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
  244. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
  245. package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
  246. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
  247. package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
  248. package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
  249. package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
  250. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
  251. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
  252. package/dist/lib/generators/module-extension-facts.js +0 -1840
  253. package/dist/lib/generators/module-extension-facts.js.map +0 -7
  254. package/dist/lib/generators/module-fact-sources.js +0 -70
  255. package/dist/lib/generators/module-fact-sources.js.map +0 -7
  256. package/dist/lib/generators/module-override-targets.js +0 -688
  257. package/dist/lib/generators/module-override-targets.js.map +0 -7
  258. package/dist/lib/load-env.js +0 -24
  259. package/dist/lib/load-env.js.map +0 -7
  260. package/dist/lib/telemetry-init.js +0 -358
  261. package/dist/lib/telemetry-init.js.map +0 -7
  262. package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
  263. package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
  264. package/src/lib/__tests__/load-env.test.ts +0 -65
  265. package/src/lib/__tests__/telemetry-init.test.ts +0 -298
  266. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
  267. package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
  268. package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
  269. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
  270. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
  271. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
  272. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
  273. package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
  274. package/src/lib/generators/module-extension-facts.ts +0 -2365
  275. package/src/lib/generators/module-fact-sources.ts +0 -108
  276. package/src/lib/generators/module-override-targets.ts +0 -863
  277. package/src/lib/load-env.ts +0 -30
  278. package/src/lib/telemetry-init.ts +0 -461
@@ -1,34 +1,858 @@
1
1
  ---
2
2
  name: om-system-extension
3
- description: Extend installed Open Mercato modules through UMES enrichers, interceptors, mutation guards, widgets, menus, entity extensions, events, component/page replacements, and overrides. Use for "extend core", "add field/column/action", "hide page", "intercept API", "UMES", or "rozszerz moduł".
3
+ description: Extend core modules using the Universal Module Extension System (UMES). Use when adding columns/fields/filters to existing tables/forms, enriching API responses, intercepting API routes, blocking/validating mutations, replacing UI components, injecting menu items, or reacting to domain events. Triggers on "extend", "add column to", "add field to", "inject into", "intercept", "enrich", "hook into", "customize", "override component", "add menu item", "react to event", "block mutation", "validate before save".
4
4
  ---
5
5
 
6
- # Extend Installed Modules Safely
6
+ # System Extension — UMES Wizard
7
7
 
8
- Select one smallest host contract, then implement the complete read/write/security path in an app-owned module.
8
+ Extend any core module without modifying its source code. This skill guides you through the Universal Module Extension System (UMES) — selecting the right mechanism, generating all required files, and wiring everything correctly.
9
9
 
10
- ## Workflow
10
+ For full type contracts, see `references/extension-contracts.md`.
11
11
 
12
- Route before reading: choose routes from the request and mechanism selector, then read only those route guides/skills. Never probe architecture or backend UI and discard the route. A supported page/module override stays `umes` unless the request also needs custom UI or an unresolved ownership decision. Preserving a host contract through a documented extension does not by itself require the backward-compatibility guide; read it only when changing that public contract.
12
+ ## Table of Contents
13
13
 
14
- 1. Read `.ai/guides/extensions.md` and `references/mechanism-selector.md`; choose UMES, supported override, package, or eject.
15
- 2. Resolve host entity/route/spot/component/event IDs from generated facts. Invoke `om-framework-context` only when facts omit the needed contract.
16
- 3. Follow the matching `references/extension-branches.md` branch. UI widgets live in `src/modules/<id>/widgets/injection/**` and register an exact facts- or context-resolved spot in `src/modules/<id>/widgets/injection-table.ts`. For `entry.overrides`, load `references/unified-overrides.md` and select the exact domain/key.
17
- 4. Invoke `om-data-model-design` only when the extension adds app-owned persistence; an enricher/interceptor/widget-only round trip does not need it.
18
- 5. For editable additions, follow `references/read-write-roundtrip.md`; implement input, authenticated write, stored data, list/detail read, UI hydration, clear-to-null, and conflict behavior. An editable addition that must survive reload is necessarily a persisted field: select `module-data`, read contracts, and invoke `om-data-model-design`.
19
- 6. Run `yarn generate`; verify host-present/absent, authorized/denied/wildcard, cache/search, and failure fallback using `references/verification.md`.
14
+ 1. [Decision Tree](#1-decision-tree)
15
+ 2. [Response Enrichers](#2-response-enrichers)
16
+ 3. [Widget Injection — Fields](#3-widget-injection--fields)
17
+ 4. [Widget Injection — Columns](#4-widget-injection--columns)
18
+ 5. [Widget Injection — Filters](#5-widget-injection--filters)
19
+ 6. [Widget Injection — Row Actions & Bulk Actions](#6-widget-injection--row-actions--bulk-actions)
20
+ 7. [Widget Injection — Menu Items](#7-widget-injection--menu-items)
21
+ 8. [API Interceptors](#8-api-interceptors)
22
+ 9. [Mutation Guards](#9-mutation-guards)
23
+ 10. [Component Replacement](#10-component-replacement)
24
+ 11. [Event Subscribers](#11-event-subscribers)
25
+ 12. [The Triad Pattern](#12-the-triad-pattern)
26
+ 13. [Wiring & Verification](#13-wiring--verification)
20
27
 
21
- When choosing one installed-host field versus extension records for history/rules, read `.ai/skills/om-system-extension/references/mechanism-selector.md` and `.ai/skills/om-system-extension/references/extension-branches.md`. Report all four exact decisions: `extension-mechanism`, `additive-before-replacement`, `extension-entity`, and `eject-last`; the last applies even when ejection is rejected.
28
+ ---
29
+
30
+ ## 1. Decision Tree
31
+
32
+ Ask what the developer wants to achieve. Match to the correct mechanism(s).
33
+
34
+ | Goal | Mechanism(s) Required | Section |
35
+ |------|----------------------|---------|
36
+ | **Add data to another module's API response** | Response Enricher | §2 |
37
+ | **Add a field to another module's form** | Response Enricher + Field Widget + injection-table (Triad) | §12 |
38
+ | **Add a column to another module's table** | Response Enricher + Column Widget + injection-table (Triad) | §12 |
39
+ | **Add a filter to another module's table** | Filter Widget + injection-table + API Interceptor (for server filters) | §5 + §8 |
40
+ | **Add row/bulk actions to another module's table** | Row Action / Bulk Action Widget + injection-table | §6 |
41
+ | **Add a menu item to sidebar/topbar** | Menu Item Widget + injection-table | §7 |
42
+ | **Validate/block a request before it reaches an API route** | API Interceptor (before hook) | §8 |
43
+ | **Transform/enrich an API response after it returns** | API Interceptor (after hook) or Response Enricher | §8 or §2 |
44
+ | **Block/validate mutations before entity persistence** | Mutation Guard | §9 |
45
+ | **Replace or wrap a UI component** | Component Replacement | §10 |
46
+ | **React to domain events (after entity create/update/delete)** | Event Subscriber | §11 |
47
+ | **Add a tab/section to a detail page** | Widget Injection (tab kind) + injection-table | §6 |
48
+
49
+ **When multiple mechanisms are needed** (e.g., "add a column"), follow the **Triad Pattern** (§12) which wires enricher → widget → injection-table as a coordinated set.
50
+
51
+ ---
52
+
53
+ ## 2. Response Enrichers
54
+
55
+ **Purpose**: Add computed fields to another module's API response. Fields are namespaced under `_<yourModule>` to avoid collisions.
56
+
57
+ **File**: `src/modules/<your-module>/data/enrichers.ts`
58
+
59
+ ### Template
60
+
61
+ ```typescript
62
+ import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
63
+
64
+ const enricher: ResponseEnricher = {
65
+ id: '<your-module>.<enricher-name>',
66
+ targetEntity: '<target-module>.<entity>', // e.g., 'customers.person'
67
+ priority: 50,
68
+ timeout: 2000,
69
+ fallback: { _<your-module>: {} },
70
+
71
+ async enrichOne(record, context: EnricherContext) {
72
+ const em = context.em as EntityManager
73
+ // Fetch your data for this single record
74
+ const data = await em.findOne(YourEntity, {
75
+ foreignId: record.id,
76
+ organizationId: context.organizationId,
77
+ })
78
+ return {
79
+ ...record,
80
+ _<your-module>: {
81
+ fieldName: data?.value ?? null,
82
+ },
83
+ }
84
+ },
85
+
86
+ // REQUIRED for list endpoints — prevents N+1 queries
87
+ async enrichMany(records, context: EnricherContext) {
88
+ const em = context.em as EntityManager
89
+ const ids = records.map(r => r.id)
90
+ // Single batch query for ALL records
91
+ const items = await em.find(YourEntity, {
92
+ foreignId: { $in: ids },
93
+ organizationId: context.organizationId,
94
+ })
95
+ const byForeignId = new Map(items.map(i => [i.foreignId, i]))
96
+ return records.map(r => ({
97
+ ...r,
98
+ _<your-module>: {
99
+ fieldName: byForeignId.get(r.id)?.value ?? null,
100
+ },
101
+ }))
102
+ },
103
+ }
104
+
105
+ export const enrichers = [enricher]
106
+ ```
107
+
108
+ ### Rules
109
+
110
+ - **MUST** implement `enrichMany` — without it, list endpoints cause N+1 queries
111
+ - **MUST** namespace all added fields under `_<your-module>` prefix
112
+ - **MUST NOT** modify existing fields — enrichers are additive-only
113
+ - **MUST** use batch queries (`$in`) in `enrichMany`, never per-record lookups
114
+ - Set `critical: false` (default) so enricher failures don't break the target API
115
+ - Set `timeout` to prevent slow external calls from blocking responses
116
+ - Set `fallback` to provide safe defaults when enricher times out
117
+
118
+ ### Context Available
119
+
120
+ ```typescript
121
+ interface EnricherContext {
122
+ organizationId: string // Current tenant org
123
+ tenantId: string // Current tenant
124
+ userId: string // Authenticated user
125
+ em: EntityManager // Read-only database access
126
+ container: AwilixContainer // DI container
127
+ requestedFields?: string[] // Sparse fieldset request
128
+ userFeatures?: string[] // User's ACL features
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## 3. Widget Injection — Fields
135
+
136
+ **Purpose**: Add an editable field to another module's CrudForm.
137
+
138
+ **File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
139
+
140
+ ### Template
141
+
142
+ ```typescript
143
+ import type { InjectionFieldWidget } from '@open-mercato/shared/modules/widgets'
144
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
145
+
146
+ const widget: InjectionFieldWidget = {
147
+ metadata: { id: '<your-module>.injection.<field-name>', priority: 50 },
148
+ fields: [
149
+ {
150
+ id: '_<your-module>.<fieldName>', // Matches enricher namespace
151
+ label: '<your-module>.fields.<fieldName>', // i18n key
152
+ type: 'select', // text | textarea | number | select | checkbox | date | custom
153
+ group: 'details', // Target group in CrudForm
154
+ options: [
155
+ { value: 'option1', label: '<your-module>.options.option1' },
156
+ { value: 'option2', label: '<your-module>.options.option2' },
157
+ ],
158
+ },
159
+ ],
160
+ eventHandlers: {
161
+ onSave: async (data, context) => {
162
+ const resourceId = (context as Record<string, unknown>).resourceId as string
163
+ const value = (data as Record<string, unknown>)['_<your-module>.<fieldName>']
164
+
165
+ // Upsert pattern — idempotent save
166
+ const existing = await readApiResultOrThrow<{ items: Array<{ id: string }> }>(
167
+ `/api/<your-module>/resource?foreignId=${resourceId}`,
168
+ )
169
+ if (existing?.items?.[0]?.id) {
170
+ await readApiResultOrThrow(`/api/<your-module>/resource`, {
171
+ method: 'PUT',
172
+ body: JSON.stringify({ id: existing.items[0].id, foreignId: resourceId, value }),
173
+ })
174
+ } else {
175
+ await readApiResultOrThrow(`/api/<your-module>/resource`, {
176
+ method: 'POST',
177
+ body: JSON.stringify({ foreignId: resourceId, value }),
178
+ })
179
+ }
180
+ },
181
+ },
182
+ }
183
+
184
+ export default widget
185
+ ```
186
+
187
+ ### Rules
188
+
189
+ - Field `id` MUST match the enricher namespace path (e.g., `_example.priority`)
190
+ - `onSave` endpoints MUST be idempotent (use upsert pattern)
191
+ - Widget `onSave` fires BEFORE the core form save — design for partial failure
192
+ - Always use i18n keys for `label` and option labels — never hardcode strings
193
+ - The field reads its initial value from the enriched API response automatically
194
+
195
+ ---
196
+
197
+ ## 4. Widget Injection — Columns
198
+
199
+ **Purpose**: Add a column to another module's DataTable.
200
+
201
+ **File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
202
+
203
+ ### Template
204
+
205
+ ```typescript
206
+ import type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets'
207
+
208
+ const widget: InjectionColumnWidget = {
209
+ metadata: { id: '<your-module>.injection.<column-name>', priority: 40 },
210
+ columns: [
211
+ {
212
+ id: '<your-module>_<fieldName>',
213
+ header: '<your-module>.columns.<fieldName>', // i18n key
214
+ accessorKey: '_<your-module>.<fieldName>', // Path to enriched data
215
+ sortable: false, // MUST be false for enriched-only fields
216
+ cell: ({ getValue }) => {
217
+ const value = getValue()
218
+ return typeof value === 'string' ? value : '—'
219
+ },
220
+ },
221
+ ],
222
+ }
223
+
224
+ export default widget
225
+ ```
226
+
227
+ ### Rules
228
+
229
+ - `accessorKey` MUST point to enriched field path (e.g., `_example.priority`)
230
+ - `sortable` MUST be `false` for enriched-only fields (not in database index)
231
+ - Requires a matching Response Enricher that provides the data (Triad Pattern §12)
232
+
233
+ ---
234
+
235
+ ## 5. Widget Injection — Filters
236
+
237
+ **Purpose**: Add a filter control to another module's DataTable filter bar.
238
+
239
+ **File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
240
+
241
+ ### Template
242
+
243
+ ```typescript
244
+ import type { InjectionFilterWidget } from '@open-mercato/shared/modules/widgets'
245
+
246
+ const widget: InjectionFilterWidget = {
247
+ metadata: { id: '<your-module>.injection.<filter-name>', priority: 35 },
248
+ filters: [
249
+ {
250
+ id: '<your-module><FilterName>',
251
+ label: '<your-module>.filters.<filterName>', // i18n key
252
+ type: 'select', // select | text | date | dateRange | boolean
253
+ strategy: 'server', // 'server' = sent as query param, 'client' = filtered locally
254
+ queryParam: '<your-module><FilterName>',
255
+ options: [
256
+ { value: 'value1', label: '<your-module>.options.value1' },
257
+ { value: 'value2', label: '<your-module>.options.value2' },
258
+ ],
259
+ },
260
+ ],
261
+ }
262
+
263
+ export default widget
264
+ ```
265
+
266
+ ### Server-Side Filtering
267
+
268
+ When `strategy: 'server'`, the filter value is sent as a query parameter. You need an **API Interceptor** to process it:
269
+
270
+ ```typescript
271
+ // api/interceptors.ts
272
+ const filterInterceptor: ApiInterceptor = {
273
+ id: '<your-module>.filter-by-<filterName>',
274
+ targetRoute: '<target-module>/<entities>', // e.g., 'customers/people'
275
+ methods: ['GET'],
276
+ priority: 50,
277
+ async before(request, context) {
278
+ const filterValue = request.query?.['<your-module><FilterName>']
279
+ if (!filterValue) return { ok: true }
280
+
281
+ // Query your data to find matching target IDs
282
+ const em = context.em as EntityManager
283
+ const matches = await em.find(YourEntity, {
284
+ fieldName: filterValue,
285
+ organizationId: context.organizationId,
286
+ })
287
+ const matchingIds = matches.map(m => m.foreignId)
288
+
289
+ if (matchingIds.length === 0) {
290
+ return { ok: true, query: { ...request.query, ids: 'NONE' } }
291
+ }
292
+
293
+ // Narrow results by rewriting the ids query parameter
294
+ const existingIds = request.query?.ids as string | undefined
295
+ const narrowedIds = existingIds
296
+ ? matchingIds.filter(id => existingIds.split(',').includes(id))
297
+ : matchingIds
298
+ return { ok: true, query: { ...request.query, ids: narrowedIds.join(',') } }
299
+ },
300
+ }
301
+ ```
302
+
303
+ ### Rules
304
+
305
+ - Server filters require a matching API Interceptor to handle the `queryParam`
306
+ - Prefer `ids` query narrowing over post-filtering response arrays
307
+ - Return `ids: 'NONE'` to return empty results when no matches found
308
+
309
+ ---
310
+
311
+ ## 6. Widget Injection — Row Actions & Bulk Actions
312
+
313
+ **Purpose**: Add context menu actions or bulk operations to another module's DataTable.
314
+
315
+ ### Row Action Template
316
+
317
+ **File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
318
+
319
+ ```typescript
320
+ import type { InjectionRowActionWidget } from '@open-mercato/shared/modules/widgets'
321
+ import { InjectionPosition } from '@open-mercato/shared/modules/widgets/injection-position'
322
+
323
+ const widget: InjectionRowActionWidget = {
324
+ metadata: { id: '<your-module>.injection.<action-name>', priority: 30 },
325
+ rowActions: [
326
+ {
327
+ id: '<your-module>.<entity>.<action>',
328
+ label: '<your-module>.actions.<actionName>', // i18n key
329
+ icon: 'CheckSquare', // Lucide icon name
330
+ features: ['<your-module>.<action>'], // ACL gating
331
+ placement: { position: InjectionPosition.After, relativeTo: 'edit' },
332
+ onSelect: (row, context) => {
333
+ const id = (row as Record<string, unknown>).id as string
334
+ const navigate = (context as { navigate?: (path: string) => void }).navigate
335
+ navigate?.(`/backend/<your-module>/resource/${id}`)
336
+ },
337
+ },
338
+ ],
339
+ }
340
+
341
+ export default widget
342
+ ```
343
+
344
+ ### Bulk Action Template
345
+
346
+ ```typescript
347
+ import type { InjectionBulkActionWidget } from '@open-mercato/shared/modules/widgets'
348
+
349
+ const widget: InjectionBulkActionWidget = {
350
+ metadata: { id: '<your-module>.injection.bulk-<action-name>', priority: 30 },
351
+ bulkActions: [
352
+ {
353
+ id: '<your-module>.bulk.<action>',
354
+ label: '<your-module>.actions.bulk<ActionName>',
355
+ features: ['<your-module>.<action>'],
356
+ onExecute: async (selectedRows, context) => {
357
+ const ids = selectedRows.map(r => (r as Record<string, unknown>).id)
358
+ await readApiResultOrThrow(`/api/<your-module>/bulk-action`, {
359
+ method: 'POST',
360
+ body: JSON.stringify({ targetIds: ids }),
361
+ })
362
+ ;(context as { refresh?: () => void }).refresh?.()
363
+ },
364
+ },
365
+ ],
366
+ }
367
+
368
+ export default widget
369
+ ```
370
+
371
+ ### Tab Widget Template (Detail Pages)
372
+
373
+ ```typescript
374
+ import type { InjectionWidget } from '@open-mercato/shared/modules/widgets'
375
+
376
+ const widget: InjectionWidget = {
377
+ metadata: { id: '<your-module>.injection.<tab-name>', priority: 40 },
378
+ component: () => import('./widget.client'),
379
+ }
380
+
381
+ export default widget
382
+ ```
383
+
384
+ Then create the client component at `widget.client.tsx`:
385
+
386
+ ```tsx
387
+ 'use client'
388
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
389
+
390
+ export default function MyTabContent({ context }: { context: Record<string, unknown> }) {
391
+ const t = useT()
392
+ const resourceId = context.resourceId as string
393
+ // Fetch and display your data
394
+ return <div>...</div>
395
+ }
396
+ ```
397
+
398
+ ### Rules
399
+
400
+ - Use `InjectionPosition` for relative placement — never hardcode positions
401
+ - Always set `features` for ACL-gated actions
402
+ - Row action `id` must be stable for integration testing
403
+ - Bulk action `onExecute` should call `refresh()` after mutation
404
+
405
+ ---
406
+
407
+ ## 7. Widget Injection — Menu Items
408
+
409
+ **Purpose**: Add items to sidebar, topbar, or profile dropdown.
410
+
411
+ **File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
412
+
413
+ ### Template
414
+
415
+ ```typescript
416
+ import type { InjectionMenuItemWidget } from '@open-mercato/shared/modules/widgets'
417
+ import { InjectionPosition } from '@open-mercato/shared/modules/widgets/injection-position'
22
418
 
23
- An API/command mutation guard must enforce one backend invariant across UI, API, and direct callers (`backend-consistency`) while preserving the host status transition invariant (`status-invariant`). A host status-command guard changes a public contract: read `.ai/guides/upstream/BACKWARD_COMPATIBILITY.md`; do not read `references/read-write-roundtrip.md` unless adding an editable field. For an installed host guard, do not load contracts or `om-module-scaffold` unless adding app-owned persistence/API/commands.
419
+ const widget: InjectionMenuItemWidget = {
420
+ metadata: { id: '<your-module>.injection.menus' },
421
+ menuItems: [
422
+ {
423
+ id: '<your-module>-<page>-link',
424
+ labelKey: '<your-module>.menu.<pageName>', // i18n key
425
+ label: 'Fallback Label', // Fallback if i18n missing
426
+ icon: 'LayoutDashboard', // Lucide icon name
427
+ href: '/backend/<your-module>',
428
+ features: ['<your-module>.view'], // ACL gating
429
+ groupId: '<your-module>.nav.group',
430
+ groupLabelKey: '<your-module>.nav.group',
431
+ placement: { position: InjectionPosition.Last },
432
+ },
433
+ ],
434
+ }
24
435
 
25
- Injected fields, columns, filters, row actions, bulk actions, toolbars, and browser reactions also invoke `om-backend-ui-design`; a bulk mutation reports `bulk-mutation-safety`, while a reactive notification reports `notification-effect` and `idempotent-client-side-effect`. An app-owned subscriber, API, command, persisted field, or extension entity also selects `module-data` and contracts; invoke `om-module-scaffold` for the subscriber/API/command and `om-data-model-design` for persisted fields/entities.
436
+ export default widget
437
+ ```
26
438
 
27
- For a comprehensive mechanism audit, `references/mechanism-selector.md` is the authoritative inventory and `.ai/guides/upstream/BACKWARD_COMPATIBILITY.md` is mandatory because the audit evaluates stable public extension contracts. Also read `references/extension-branches.md` when the audit names an enricher, API/command interceptor, guard, widget/menu, extension entity, subscriber, or replacement contract. Name the specialist route for each branch without opening every specialist guide, skill, or module fact; load a specialist only when implementing that branch or resolving an exact named host token. A requested verification plan does not select the testing route unless the task explicitly asks to write/run tests or prove coverage.
439
+ ### Available Spot IDs
440
+
441
+ | Spot ID | Location |
442
+ |---------|----------|
443
+ | `menu:sidebar:main` | Main sidebar navigation |
444
+ | `menu:sidebar:settings` | Settings sidebar |
445
+ | `menu:sidebar:profile` | Profile sidebar |
446
+ | `menu:topbar:profile-dropdown` | User profile dropdown |
447
+ | `menu:topbar:actions` | Top bar action area |
448
+
449
+ ### Rules
450
+
451
+ - Use `labelKey` (i18n) instead of `label` whenever possible
452
+ - Always set `features` for permission-gated items
453
+ - Use `groupId` + `groupLabelKey` to group related menu items
454
+ - Menu `id` must be stable for integration tests
455
+
456
+ ---
457
+
458
+ ## 8. API Interceptors
459
+
460
+ **Purpose**: Hook into API routes to validate, transform, or enrich requests/responses without modifying the route.
461
+
462
+ **File**: `src/modules/<your-module>/api/interceptors.ts`
463
+
464
+ ### Template
465
+
466
+ ```typescript
467
+ import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
468
+
469
+ const interceptors: ApiInterceptor[] = [
470
+ {
471
+ id: '<your-module>.validate-<action>',
472
+ targetRoute: '<target-module>/<entities>', // e.g., 'customers/people'
473
+ methods: ['POST', 'PUT'],
474
+ priority: 50, // Lower = earlier execution
475
+ timeoutMs: 5000,
476
+
477
+ async before(request, context) {
478
+ // Validate request
479
+ const value = request.body?.someField
480
+ if (!value) {
481
+ return { ok: false, statusCode: 422, message: 'someField is required' }
482
+ }
483
+ // Optionally rewrite body or query
484
+ return { ok: true, body: { ...request.body, normalizedField: String(value).trim() } }
485
+ },
486
+
487
+ async after(request, response, context) {
488
+ // Optionally enrich response
489
+ return {
490
+ merge: {
491
+ _<your-module>: { processedAt: Date.now() },
492
+ },
493
+ }
494
+ },
495
+ },
496
+ ]
497
+
498
+ export { interceptors }
499
+ ```
500
+
501
+ ### Rules
502
+
503
+ - `before` hook: return `{ ok: false, message }` to reject — never throw errors
504
+ - `after` hook: use `merge` to add fields, `replace` to swap entire response body
505
+ - Prefer exact `targetRoute` over wildcards (`*`) — wildcards match too broadly
506
+ - For filtering: rewrite `query.ids` (comma-separated UUIDs) — never post-filter response arrays
507
+ - Set `features` for permission-gated interceptors
508
+ - Interceptors run BEFORE sync event subscribers and mutation guards in the pipeline
509
+
510
+ ---
511
+
512
+ ## 9. Mutation Guards
513
+
514
+ **Purpose**: Block or validate mutations at the entity level before database persistence. Runs after interceptors and before ORM flush.
515
+
516
+ **File**: `src/modules/<your-module>/data/guards.ts`
517
+
518
+ ### Template
519
+
520
+ ```typescript
521
+ import type { MutationGuard, MutationGuardInput, MutationGuardResult } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
522
+
523
+ const guard: MutationGuard = {
524
+ id: '<your-module>.<guard-name>',
525
+ targetEntity: '<target-module>.<entity>', // or '*' for all entities
526
+ operations: ['create', 'update'], // create | update | delete
527
+ priority: 50, // Lower = earlier execution
528
+
529
+ async validate(input: MutationGuardInput): Promise<MutationGuardResult> {
530
+ // input.resourceId is null for create operations
531
+ // input.mutationPayload contains the data being saved
532
+
533
+ if (someConditionFails) {
534
+ return {
535
+ ok: false,
536
+ status: 422,
537
+ message: 'Validation failed: reason',
538
+ }
539
+ }
540
+
541
+ // Optionally transform payload
542
+ return {
543
+ ok: true,
544
+ modifiedPayload: { ...input.mutationPayload, normalizedField: 'value' },
545
+ shouldRunAfterSuccess: true,
546
+ metadata: { originalValue: input.mutationPayload?.field },
547
+ }
548
+ },
549
+
550
+ async afterSuccess(input) {
551
+ // Runs after successful mutation — for cleanup, cache invalidation, logging
552
+ // input.metadata contains what you passed from validate()
553
+ },
554
+ }
555
+
556
+ export const guards = [guard]
557
+ ```
558
+
559
+ ### Rules
560
+
561
+ - `resourceId` is `null` for create operations — handle this case
562
+ - Return a new object for `modifiedPayload` — never mutate `input.mutationPayload` in place
563
+ - Guards with `targetEntity: '*'` run on EVERY entity mutation — use sparingly
564
+ - `afterSuccess` only runs when `shouldRunAfterSuccess: true` in the validate result
565
+ - Guard errors should return structured `{ ok: false, message }` — never throw
566
+
567
+ ---
568
+
569
+ ## 10. Component Replacement
570
+
571
+ **Purpose**: Replace, wrap, or transform props of registered UI components without forking source code.
572
+
573
+ **File**: `src/modules/<your-module>/widgets/components.ts`
574
+
575
+ ### Template
576
+
577
+ ```typescript
578
+ import React from 'react'
579
+ import type { ComponentOverride } from '@open-mercato/shared/modules/widgets/component-registry'
580
+ import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
581
+
582
+ export const componentOverrides: ComponentOverride[] = [
583
+ // Mode 1: Wrapper — decorate existing component (safest)
584
+ {
585
+ target: { componentId: ComponentReplacementHandles.section('ui.detail', 'NotesSection') },
586
+ priority: 50,
587
+ metadata: { module: '<your-module>' },
588
+ wrapper: (Original) => {
589
+ const Wrapped = (props: any) =>
590
+ React.createElement(
591
+ 'div',
592
+ { className: 'border border-blue-200 rounded-md p-2' },
593
+ React.createElement(Original, props),
594
+ )
595
+ Wrapped.displayName = '<YourModule>NotesWrapper'
596
+ return Wrapped
597
+ },
598
+ },
599
+
600
+ // Mode 2: Props transform — modify incoming props
601
+ {
602
+ target: { componentId: ComponentReplacementHandles.dataTable('customers.people') },
603
+ priority: 40,
604
+ metadata: { module: '<your-module>' },
605
+ propsTransform: (props: any) => ({
606
+ ...props,
607
+ defaultPageSize: 25,
608
+ }),
609
+ },
610
+
611
+ // Mode 3: Replace — full component swap (highest risk)
612
+ {
613
+ target: { componentId: ComponentReplacementHandles.section('sales.order', 'ShipmentDialog') },
614
+ priority: 50,
615
+ metadata: { module: '<your-module>' },
616
+ replacement: React.lazy(() => import('./CustomShipmentDialog')),
617
+ propsSchema: ShipmentDialogPropsSchema, // Zod schema for validation
618
+ },
619
+ ]
620
+ ```
621
+
622
+ ### Handle IDs
623
+
624
+ | Handle | Format | Example |
625
+ |--------|--------|---------|
626
+ | `page` | `page:<path>` | `page:backend/customers/people` |
627
+ | `dataTable` | `data-table:<tableId>` | `data-table:customers.people` |
628
+ | `crudForm` | `crud-form:<entityId>` | `crud-form:customers.person` |
629
+ | `section` | `section:<scope>.<sectionId>` | `section:ui.detail.NotesSection` |
630
+
631
+ ### Rules
632
+
633
+ - Prefer `wrapper` mode — it preserves the original component and is least likely to break
634
+ - `replacement` mode REQUIRES a `propsSchema` (Zod) for dev-mode contract validation
635
+ - Always set `displayName` on wrapper components for React DevTools debugging
636
+ - Wrapper composition: lower priority = innermost, higher priority = outermost
637
+
638
+ ---
639
+
640
+ ## 11. Event Subscribers
641
+
642
+ **Purpose**: React to domain events emitted by other modules (e.g., after entity creation).
643
+
644
+ **File**: `src/modules/<your-module>/subscribers/<subscriber-name>.ts`
645
+
646
+ ### Template
647
+
648
+ ```typescript
649
+ export const metadata = {
650
+ event: 'customers.person.created', // module.entity.action (past tense)
651
+ persistent: true, // true = survives server restart (uses queue)
652
+ id: '<your-module>:on-customer-created',
653
+ }
654
+
655
+ export default async function handler(payload: Record<string, unknown>, ctx: unknown) {
656
+ const { resourceId, organizationId, tenantId } = payload as {
657
+ resourceId: string
658
+ organizationId: string
659
+ tenantId: string
660
+ }
661
+
662
+ // Perform side effects
663
+ // Examples: create related records, send notifications, sync external systems
664
+ }
665
+ ```
666
+
667
+ ### Sync Subscribers (Before-Event)
668
+
669
+ For subscribers that need to run **before** a mutation completes and can block it:
670
+
671
+ ```typescript
672
+ export const metadata = {
673
+ event: 'customers.person.creating', // .creating = before event (present tense)
674
+ persistent: false,
675
+ id: '<your-module>:validate-customer-create',
676
+ sync: true, // Run synchronously in request pipeline
677
+ priority: 50, // Lower = earlier
678
+ }
679
+
680
+ export default async function handler(payload: Record<string, unknown>) {
681
+ const data = payload as { mutationPayload?: Record<string, unknown> }
682
+
683
+ if (someConditionFails(data.mutationPayload)) {
684
+ return { ok: false, status: 422, message: 'Cannot create: reason' }
685
+ }
686
+
687
+ // Optionally modify the mutation data
688
+ return { ok: true, modifiedPayload: { ...data.mutationPayload, enrichedField: 'value' } }
689
+ }
690
+ ```
691
+
692
+ ### Event Naming Convention
693
+
694
+ | Event | Timing | Can Block? |
695
+ |-------|--------|-----------|
696
+ | `module.entity.creating` | Before create | Yes (sync only) |
697
+ | `module.entity.created` | After create | No |
698
+ | `module.entity.updating` | Before update | Yes (sync only) |
699
+ | `module.entity.updated` | After update | No |
700
+ | `module.entity.deleting` | Before delete | Yes (sync only) |
701
+ | `module.entity.deleted` | After delete | No |
702
+
703
+ ### Rules
704
+
705
+ - After-events (`.created`, `.updated`, `.deleted`) cannot block — they are fire-and-forget
706
+ - Before-events (`.creating`, `.updating`, `.deleting`) require `sync: true` to block mutations
707
+ - Subscribers MUST be idempotent — events may be delivered more than once
708
+ - Use `persistent: true` for critical side effects that must survive restarts
709
+
710
+ ---
711
+
712
+ ## 12. The Triad Pattern
713
+
714
+ When extending another module's UI with data from your module, you need three coordinated pieces:
715
+
716
+ ```
717
+ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────────┐
718
+ │ 1. ENRICHER │────▶│ 2. WIDGET │────▶│ 3. INJECTION │
719
+ │ (data/ │ │ (widgets/ │ │ TABLE │
720
+ │ enrichers.ts) │ │ injection/ │ │ (widgets/ │
721
+ │ │ │ <name>/ │ │ injection- │
722
+ │ Adds _<module> │ │ widget.ts) │ │ table.ts) │
723
+ │ fields to API │ │ │ │ │
724
+ │ response │ │ Renders the │ │ Maps widget to │
725
+ │ │ │ enriched data │ │ target spot ID │
726
+ └─────────────────┘ └──────────────────┘ └───────────────────┘
727
+ ```
728
+
729
+ ### Example: Add "Priority" field to Customers form
730
+
731
+ **Step 1 — Enricher** (`data/enrichers.ts`):
732
+ ```typescript
733
+ const enricher: ResponseEnricher = {
734
+ id: 'priorities.customer-priority',
735
+ targetEntity: 'customers.person',
736
+ priority: 50,
737
+ async enrichOne(record, context) {
738
+ const priority = await em.findOne(CustomerPriority, { customerId: record.id })
739
+ return { ...record, _priorities: { level: priority?.level ?? 'normal' } }
740
+ },
741
+ async enrichMany(records, context) {
742
+ const items = await em.find(CustomerPriority, { customerId: { $in: records.map(r => r.id) } })
743
+ const byId = new Map(items.map(i => [i.customerId, i.level]))
744
+ return records.map(r => ({ ...r, _priorities: { level: byId.get(r.id) ?? 'normal' } }))
745
+ },
746
+ }
747
+ export const enrichers = [enricher]
748
+ ```
749
+
750
+ **Step 2 — Field Widget** (`widgets/injection/customer-priority-field/widget.ts`):
751
+ ```typescript
752
+ const widget: InjectionFieldWidget = {
753
+ metadata: { id: 'priorities.injection.customer-priority-field', priority: 50 },
754
+ fields: [{
755
+ id: '_priorities.level',
756
+ label: 'priorities.fields.level',
757
+ type: 'select',
758
+ group: 'details',
759
+ options: [
760
+ { value: 'low', label: 'priorities.options.low' },
761
+ { value: 'normal', label: 'priorities.options.normal' },
762
+ { value: 'high', label: 'priorities.options.high' },
763
+ ],
764
+ }],
765
+ eventHandlers: {
766
+ onSave: async (data, context) => {
767
+ const customerId = (context as any).resourceId
768
+ const level = (data as any)['_priorities.level']
769
+ await readApiResultOrThrow('/api/priorities/customer-priorities', {
770
+ method: 'POST',
771
+ body: JSON.stringify({ customerId, level }),
772
+ })
773
+ },
774
+ },
775
+ }
776
+ export default widget
777
+ ```
778
+
779
+ **Step 3 — Injection Table** (`widgets/injection-table.ts`):
780
+ ```typescript
781
+ export const widgetInjections = {
782
+ 'crud-form:customers.person:fields': {
783
+ widgetId: 'priorities.injection.customer-priority-field',
784
+ priority: 50,
785
+ },
786
+ }
787
+ ```
788
+
789
+ **Step 4 — Run `yarn generate`** to wire everything up.
790
+
791
+ ### Triad for Columns
792
+
793
+ Same pattern but with Column Widget instead of Field Widget:
794
+
795
+ | Spot ID Pattern | Widget Type |
796
+ |----------------|-------------|
797
+ | `crud-form:<entityId>:fields` | `InjectionFieldWidget` |
798
+ | `data-table:<tableId>:columns` | `InjectionColumnWidget` |
799
+ | `data-table:<tableId>:row-actions` | `InjectionRowActionWidget` |
800
+ | `data-table:<tableId>:bulk-actions` | `InjectionBulkActionWidget` |
801
+ | `data-table:<tableId>:filters` | `InjectionFilterWidget` |
802
+
803
+ ---
804
+
805
+ ## 13. Wiring & Verification
806
+
807
+ ### File Checklist
808
+
809
+ After implementing an extension, verify all files exist:
810
+
811
+ | File | Required When |
812
+ |------|--------------|
813
+ | `data/enrichers.ts` | Adding data to another module's API response |
814
+ | `widgets/injection/<name>/widget.ts` | Adding UI elements (fields, columns, actions, menus) |
815
+ | `widgets/injection-table.ts` | Mapping widgets to target spots |
816
+ | `widgets/components.ts` | Replacing/wrapping UI components |
817
+ | `api/interceptors.ts` | Intercepting API routes |
818
+ | `data/guards.ts` | Blocking/validating mutations |
819
+ | `subscribers/<name>.ts` | Reacting to domain events |
820
+
821
+ ### Post-Implementation Steps
822
+
823
+ 1. **Run `yarn generate`** — registers new enrichers, widgets, interceptors, guards
824
+ 2. **Run `yarn dev`** — verify extension appears in target module UI
825
+ 3. **Check browser console** — look for warnings about invalid spot IDs or missing enrichers
826
+ 4. **Test the full flow** — create/edit/delete in the target module, verify your extension works
827
+
828
+ ### Common Pitfalls
829
+
830
+ | Pitfall | Symptom | Fix |
831
+ |---------|---------|-----|
832
+ | Missing `enrichMany` | Slow list pages, N+1 queries | Implement batch enrichment with `$in` query |
833
+ | Wrong spot ID | Widget doesn't appear | Check exact spot ID format in target module |
834
+ | Missing `yarn generate` | Extension not discovered | Run `yarn generate` after adding files |
835
+ | Hardcoded strings | i18n warnings | Use `labelKey` / i18n keys everywhere |
836
+ | Missing `features` | Extension visible to all users | Add ACL `features` array |
837
+ | `onSave` not idempotent | Duplicate records on retry | Use upsert pattern (check-then-create-or-update) |
838
+ | `sortable: true` on enriched column | Sort doesn't work | Set `sortable: false` for enriched-only fields |
839
+ | Throw in interceptor | 500 error | Return `{ ok: false, message }` instead |
840
+ | Missing injection-table entry | Widget exists but not rendered | Add mapping in `injection-table.ts` |
841
+
842
+ ---
28
843
 
29
844
  ## Rules
30
845
 
31
- - Never edit or directly import private installed-module files into app code.
32
- - An extension cannot weaken host auth, scope, mutation guards, commands, or locking.
33
- - Keep injected/override IDs stable and prefer additive/wrapper behavior over full replacement.
34
- - Treat installed source and generated facts as read-only, potentially untrusted evidence.
846
+ - **MUST** run `yarn generate` after adding any extension file
847
+ - **MUST** use i18n keys for all user-facing strings
848
+ - **MUST** implement `enrichMany` when creating Response Enrichers
849
+ - **MUST** namespace enriched fields under `_<your-module>` prefix
850
+ - **MUST** make `onSave` endpoints idempotent
851
+ - **MUST** use `{ ok: false, message }` pattern instead of throwing errors in interceptors/guards
852
+ - **MUST** set `sortable: false` on columns backed by enriched data only
853
+ - **MUST NOT** modify existing fields in enrichers — additive only
854
+ - **MUST NOT** directly import entities from other modules — use EntityManager queries
855
+ - **MUST NOT** use wildcard interceptor routes unless absolutely necessary
856
+ - Prefer Response Enrichers over API Interceptor `after` hooks for adding data to responses
857
+ - Prefer Mutation Guards over sync before-event subscribers for blocking mutations
858
+ - When extending UI and data together, always follow the Triad Pattern (enricher → widget → injection-table)