@open-mercato/shared 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Unified `modules.ts` override surface — one place for downstream apps to
3
+ * replace or disable any contract a module presents.
4
+ *
5
+ * Spec: `.ai/specs/2026-05-04-modules-ts-unified-overrides.md`
6
+ *
7
+ * Each `ModuleEntry` in `apps/<app>/src/modules.ts` may carry an
8
+ * `overrides` field whose sub-keys address one domain at a time:
9
+ *
10
+ * {
11
+ * id: 'example',
12
+ * from: '@app',
13
+ * overrides: {
14
+ * ai: { agents: {...}, tools: {...} }, // Phase 1 — wired
15
+ * routes: { api: {...}, pages: {...} }, // Phase 2/3 — stub
16
+ * events: { subscribers: {...} }, // Phase 4 — stub
17
+ * workers: {...}, // Phase 5 — stub
18
+ * ...
19
+ * },
20
+ * }
21
+ *
22
+ * The umbrella shape is the union of every per-domain sub-shape. Per-
23
+ * domain runtime hooks ("wired" domains) own their composers and apply
24
+ * the override map against their registry. Until a phase ships, the
25
+ * dispatcher emits a one-shot structured warning when it sees an
26
+ * override targeting that unwired domain — the runtime never throws on
27
+ * unwired domains so app boot stays unaffected during the rollout.
28
+ *
29
+ * Resolution order across all domains (highest precedence first):
30
+ *
31
+ * 1. Programmatic — direct calls into the per-domain `apply*Overrides()` API.
32
+ * 2. `modules.ts` inline — `entry.overrides.<domain>` here.
33
+ * 3. File-based — overrides exported from a contributing module's own files.
34
+ * 4. Base — the module's own registrations.
35
+ *
36
+ * `null` always means "disable"; a definition replaces.
37
+ */
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Domain sub-shapes
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * AI domain — agents and tools. Re-exports the canonical maps from
45
+ * `@open-mercato/ai-assistant` so consumers do not need to import that
46
+ * package directly when they only want to declare overrides.
47
+ *
48
+ * Imported lazily as `unknown` here because `@open-mercato/shared` must
49
+ * NOT take a runtime dependency on `@open-mercato/ai-assistant` (the
50
+ * dependency direction is the other way around). Apps that author
51
+ * `entry.overrides.ai` should import the strongly-typed
52
+ * `AiAgentOverridesMap` / `AiToolOverridesMap` from `@open-mercato/ai-assistant`
53
+ * directly — TypeScript structurally compatible types make the loose
54
+ * shape here a no-op annotation cost.
55
+ */
56
+ export interface AiOverridesShape {
57
+ agents?: Record<string, unknown>
58
+ tools?: Record<string, unknown>
59
+ extensions?: unknown[]
60
+ }
61
+
62
+ /** Phase 2/3 — routes (api + pages). Stubbed until wired. */
63
+ export interface RoutesOverridesShape {
64
+ api?: Record<string, unknown>
65
+ pages?: Record<string, unknown>
66
+ }
67
+
68
+ /** Phase 4 — event subscribers. Stubbed until wired. */
69
+ export interface EventsOverridesShape {
70
+ subscribers?: Record<string, unknown>
71
+ }
72
+
73
+ /** Phase 6/7/8 — widget injection, component handles, dashboard widgets. */
74
+ export interface WidgetsOverridesShape {
75
+ injection?: Record<string, unknown>
76
+ components?: Record<string, unknown>
77
+ dashboard?: Record<string, unknown>
78
+ }
79
+
80
+ /** Phase 9 — notification types + handlers. */
81
+ export interface NotificationsOverridesShape {
82
+ types?: Record<string, unknown>
83
+ handlers?: Record<string, unknown>
84
+ }
85
+
86
+ /** Phase 15 — setup lifecycle hooks. */
87
+ export interface SetupOverridesShape {
88
+ defaultRoleFeatures?: Record<string, readonly string[]>
89
+ seedDefaults?: false
90
+ seedExamples?: false
91
+ onTenantCreated?: false
92
+ }
93
+
94
+ /** Phase 16 — ACL features (per-feature override). */
95
+ export interface AclOverridesShape {
96
+ features?: Record<string, unknown>
97
+ }
98
+
99
+ /** Phase 18 — encryption maps per entity id. */
100
+ export interface EncryptionOverridesShape {
101
+ maps?: Record<string, unknown>
102
+ }
103
+
104
+ /**
105
+ * Umbrella shape for `entry.overrides`. Every key is optional; a
106
+ * downstream app sets only the domains it cares about.
107
+ */
108
+ export interface ModuleOverrides {
109
+ ai?: AiOverridesShape
110
+ routes?: RoutesOverridesShape
111
+ events?: EventsOverridesShape
112
+ workers?: Record<string, unknown>
113
+ widgets?: WidgetsOverridesShape
114
+ notifications?: NotificationsOverridesShape
115
+ interceptors?: Record<string, unknown>
116
+ commandInterceptors?: Record<string, unknown>
117
+ enrichers?: Record<string, unknown>
118
+ guards?: Record<string, unknown>
119
+ cli?: Record<string, unknown>
120
+ setup?: SetupOverridesShape
121
+ acl?: AclOverridesShape
122
+ di?: Record<string, unknown>
123
+ encryption?: EncryptionOverridesShape
124
+ }
125
+
126
+ /**
127
+ * Public shape consumed by the dispatcher. Mirrors the `ModuleEntry`
128
+ * defined in each app's `modules.ts` — the dispatcher only needs `id`
129
+ * and `overrides`.
130
+ */
131
+ export interface ModuleEntryWithOverrides {
132
+ id: string
133
+ from?: string
134
+ overrides?: ModuleOverrides
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Per-domain runtime hook registry
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Each wired domain registers an applier that receives the list of
143
+ * `(moduleId, overrides)` pairs in module-load order and forwards them
144
+ * to its own runtime hook. Unwired domains do not register an applier
145
+ * and instead trigger the dispatcher's one-shot warning.
146
+ */
147
+ export type ModuleOverrideDomain =
148
+ | 'ai'
149
+ | 'routes'
150
+ | 'events'
151
+ | 'workers'
152
+ | 'widgets'
153
+ | 'notifications'
154
+ | 'interceptors'
155
+ | 'commandInterceptors'
156
+ | 'enrichers'
157
+ | 'guards'
158
+ | 'cli'
159
+ | 'setup'
160
+ | 'acl'
161
+ | 'di'
162
+ | 'encryption'
163
+
164
+ export interface ModuleOverrideEntry<TShape> {
165
+ moduleId: string
166
+ overrides: TShape
167
+ }
168
+
169
+ export type ModuleOverrideApplier<TShape> = (
170
+ entries: ReadonlyArray<ModuleOverrideEntry<TShape>>,
171
+ ) => void
172
+
173
+ const appliers = new Map<ModuleOverrideDomain, ModuleOverrideApplier<unknown>>()
174
+ const warnedUnwiredDomains = new Set<ModuleOverrideDomain>()
175
+
176
+ /**
177
+ * Register a per-domain runtime hook. Called once at module-load time
178
+ * by each wired domain (e.g. the AI subsystem registers `'ai'` from
179
+ * `@open-mercato/ai-assistant`).
180
+ */
181
+ export function registerModuleOverrideApplier<TShape>(
182
+ domain: ModuleOverrideDomain,
183
+ applier: ModuleOverrideApplier<TShape>,
184
+ ): void {
185
+ appliers.set(domain, applier as ModuleOverrideApplier<unknown>)
186
+ }
187
+
188
+ /** @__internal Test-only hook — clear all registered appliers + warnings. */
189
+ export function resetModuleOverrideAppliersForTests(): void {
190
+ appliers.clear()
191
+ warnedUnwiredDomains.clear()
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Dispatcher
196
+ // ---------------------------------------------------------------------------
197
+
198
+ const DOMAIN_KEYS: ModuleOverrideDomain[] = [
199
+ 'ai',
200
+ 'routes',
201
+ 'events',
202
+ 'workers',
203
+ 'widgets',
204
+ 'notifications',
205
+ 'interceptors',
206
+ 'commandInterceptors',
207
+ 'enrichers',
208
+ 'guards',
209
+ 'cli',
210
+ 'setup',
211
+ 'acl',
212
+ 'di',
213
+ 'encryption',
214
+ ]
215
+
216
+ const TRACKING_ISSUE_HINT =
217
+ 'See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and tracking issue https://github.com/open-mercato/open-mercato/issues/1787.'
218
+
219
+ /**
220
+ * Walk every `ModuleEntry` and dispatch its `overrides.<domain>` shape
221
+ * to the matching wired applier. Unwired domains emit a one-shot
222
+ * structured warning.
223
+ *
224
+ * Call this exactly once from `apps/<app>/src/bootstrap.ts` BEFORE any
225
+ * registry first-loads. Calling it more than once is safe but
226
+ * accumulates per-domain entries each time.
227
+ */
228
+ export function applyModuleOverridesFromEnabledModules(
229
+ modules: ReadonlyArray<ModuleEntryWithOverrides>,
230
+ ): void {
231
+ if (!Array.isArray(modules) || modules.length === 0) return
232
+
233
+ // Bucket entries by domain in module-load order.
234
+ const buckets = new Map<ModuleOverrideDomain, Array<ModuleOverrideEntry<unknown>>>()
235
+
236
+ for (const entry of modules) {
237
+ if (!entry || typeof entry.id !== 'string' || !entry.id) continue
238
+ const overrides = entry.overrides
239
+ if (!overrides || typeof overrides !== 'object') continue
240
+
241
+ for (const domain of DOMAIN_KEYS) {
242
+ const value = (overrides as Record<string, unknown>)[domain]
243
+ if (value === undefined || value === null) continue
244
+ if (typeof value !== 'object') continue
245
+ const list = buckets.get(domain) ?? []
246
+ list.push({ moduleId: entry.id, overrides: value })
247
+ buckets.set(domain, list)
248
+ }
249
+ }
250
+
251
+ // Dispatch each domain to its wired applier; warn on unwired domains.
252
+ for (const [domain, entries] of buckets) {
253
+ const applier = appliers.get(domain)
254
+ if (!applier) {
255
+ if (!warnedUnwiredDomains.has(domain)) {
256
+ warnedUnwiredDomains.add(domain)
257
+ const moduleIds = Array.from(new Set(entries.map((e) => e.moduleId))).join(', ')
258
+ console.warn(
259
+ `[Module Overrides] Domain "${domain}" not yet wired — entry.overrides.${domain} for module(s) [${moduleIds}] was ignored. ${TRACKING_ISSUE_HINT}`,
260
+ )
261
+ }
262
+ continue
263
+ }
264
+ applier(entries)
265
+ }
266
+ }
@@ -373,6 +373,16 @@ export function getFrontendRouteManifests(): FrontendRouteManifestEntry[] {
373
373
  return _frontendRouteManifests ?? []
374
374
  }
375
375
 
376
+ let _apiRouteManifests: ApiRouteManifestEntry[] | null = null
377
+
378
+ export function registerApiRouteManifests(routes: ApiRouteManifestEntry[]) {
379
+ _apiRouteManifests = routes
380
+ }
381
+
382
+ export function getApiRouteManifests(): ApiRouteManifestEntry[] {
383
+ return _apiRouteManifests ?? []
384
+ }
385
+
376
386
  // CLI modules registry - shared between CLI and module workers
377
387
  let _cliModules: Module[] | null = null
378
388