@open-mercato/cli 0.6.8-develop.6931.1.4a1e1c786b → 0.6.8-develop.6941.1.3c26284542

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 (168) hide show
  1. package/.turbo/turbo-build.log +64 -0
  2. package/build.mjs +57 -7
  3. package/dist/agentic/guides/backend-ui.md +7 -1
  4. package/dist/agentic/guides/module-facts.json +565 -224
  5. package/dist/agentic/guides/module-facts.v2.json +122933 -0
  6. package/dist/agentic/guides/modules/ai_assistant.md +2 -2
  7. package/dist/agentic/guides/modules/api_docs.md +2 -2
  8. package/dist/agentic/guides/modules/api_keys.md +2 -2
  9. package/dist/agentic/guides/modules/attachments.md +2 -2
  10. package/dist/agentic/guides/modules/audit_logs.md +2 -2
  11. package/dist/agentic/guides/modules/auth.md +2 -2
  12. package/dist/agentic/guides/modules/business_rules.md +2 -2
  13. package/dist/agentic/guides/modules/catalog.md +11 -4
  14. package/dist/agentic/guides/modules/channel_gmail.md +2 -2
  15. package/dist/agentic/guides/modules/channel_imap.md +2 -2
  16. package/dist/agentic/guides/modules/checkout.md +8 -2
  17. package/dist/agentic/guides/modules/communication_channels.md +6 -6
  18. package/dist/agentic/guides/modules/configs.md +2 -2
  19. package/dist/agentic/guides/modules/content.md +2 -2
  20. package/dist/agentic/guides/modules/currencies.md +2 -2
  21. package/dist/agentic/guides/modules/customer_accounts.md +2 -2
  22. package/dist/agentic/guides/modules/customers.md +8 -8
  23. package/dist/agentic/guides/modules/dashboards.md +12 -12
  24. package/dist/agentic/guides/modules/data_sync.md +4 -4
  25. package/dist/agentic/guides/modules/design_system.md +2 -2
  26. package/dist/agentic/guides/modules/dictionaries.md +2 -2
  27. package/dist/agentic/guides/modules/directory.md +2 -2
  28. package/dist/agentic/guides/modules/entities.md +3 -3
  29. package/dist/agentic/guides/modules/eudr.md +3 -3
  30. package/dist/agentic/guides/modules/events.md +2 -2
  31. package/dist/agentic/guides/modules/feature_toggles.md +2 -2
  32. package/dist/agentic/guides/modules/gateway_stripe.md +2 -2
  33. package/dist/agentic/guides/modules/generators.md +2 -2
  34. package/dist/agentic/guides/modules/inbox_ops.md +2 -2
  35. package/dist/agentic/guides/modules/integrations.md +5 -2
  36. package/dist/agentic/guides/modules/messages.md +2 -2
  37. package/dist/agentic/guides/modules/notifications.md +2 -2
  38. package/dist/agentic/guides/modules/onboarding.md +2 -2
  39. package/dist/agentic/guides/modules/payment_gateways.md +7 -3
  40. package/dist/agentic/guides/modules/perspectives.md +2 -2
  41. package/dist/agentic/guides/modules/planner.md +2 -2
  42. package/dist/agentic/guides/modules/portal.md +2 -2
  43. package/dist/agentic/guides/modules/progress.md +2 -2
  44. package/dist/agentic/guides/modules/query_index.md +2 -2
  45. package/dist/agentic/guides/modules/record_locks.md +3 -3
  46. package/dist/agentic/guides/modules/resources.md +2 -2
  47. package/dist/agentic/guides/modules/sales.md +13 -4
  48. package/dist/agentic/guides/modules/scheduler.md +2 -2
  49. package/dist/agentic/guides/modules/search.md +4 -4
  50. package/dist/agentic/guides/modules/security.md +3 -3
  51. package/dist/agentic/guides/modules/shipping_carriers.md +2 -2
  52. package/dist/agentic/guides/modules/sso.md +2 -2
  53. package/dist/agentic/guides/modules/staff.md +7 -4
  54. package/dist/agentic/guides/modules/storage_s3.md +2 -2
  55. package/dist/agentic/guides/modules/sync_akeneo.md +4 -4
  56. package/dist/agentic/guides/modules/sync_excel.md +2 -2
  57. package/dist/agentic/guides/modules/system_status_overlays.md +2 -2
  58. package/dist/agentic/guides/modules/translations.md +2 -2
  59. package/dist/agentic/guides/modules/webhooks.md +2 -2
  60. package/dist/agentic/guides/modules/wms.md +31 -4
  61. package/dist/agentic/guides/modules/workflows.md +2 -2
  62. package/dist/agentic/guides/reference-module-facts.json +8335 -0
  63. package/dist/agentic/guides/reference-modules/example.md +489 -0
  64. package/dist/agentic/guides/spec-delivery.md +34 -0
  65. package/dist/agentic/guides/testing-debugging.md +2 -1
  66. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +3 -2
  67. package/dist/agentic/guides/upstream/manifest.json +2 -2
  68. package/dist/agentic/shared/AGENTS.md.template +10 -14
  69. package/dist/agentic/shared/ai/harness/README.md +4 -4
  70. package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
  71. package/dist/agentic/shared/ai/harness/cases.json +11350 -5088
  72. package/dist/agentic/shared/ai/harness/cases.schema.json +70 -7
  73. package/dist/agentic/shared/ai/harness/design-system-inventory.json +6697 -0
  74. package/dist/agentic/shared/ai/harness/fixtures/index.json +3 -1
  75. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +4 -0
  76. package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
  77. package/dist/agentic/shared/ai/harness/release-matrix.json +9 -4
  78. package/dist/agentic/shared/ai/harness/result.schema.json +60 -0
  79. package/dist/agentic/shared/ai/harness/routing-response.schema.json +10 -0
  80. package/dist/agentic/shared/ai/harness/source-link-inventory.json +3231 -0
  81. package/dist/agentic/shared/ai/harness/validators.json +8 -4
  82. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +5 -2
  83. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +3 -0
  84. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +879 -0
  85. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +102 -0
  86. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +1 -0
  87. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +15 -0
  88. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +6 -0
  89. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +4 -0
  90. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +1 -0
  91. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +2 -0
  92. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +2 -0
  93. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +3 -1
  94. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +1 -0
  95. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +2 -0
  96. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +2 -0
  97. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +3 -3
  98. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
  99. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +133 -0
  100. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +1 -0
  101. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +11 -0
  102. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +1 -1
  103. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +2 -2
  104. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +14 -1
  105. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +2 -0
  106. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +19 -0
  107. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +1 -4
  108. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +1 -0
  109. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +24 -0
  110. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +3 -1
  111. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +1 -0
  112. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +13 -0
  113. package/dist/agentic/shared/ai/skills/tiers.json +2 -1
  114. package/dist/agentic/shared/ai/specs/2026-08-06-reference-module-activation.md +248 -0
  115. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +39 -2
  116. package/dist/agentic/shared/scripts/design-source-contract.mjs +108 -0
  117. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +935 -28
  118. package/dist/agentic/shared/scripts/framework-context.mjs +4 -1
  119. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1 -1
  120. package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +1511 -0
  121. package/dist/lib/__integration__/TC-INT-008.spec.js +1 -0
  122. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  123. package/dist/lib/agentic-setup.js +9 -1
  124. package/dist/lib/agentic-setup.js.map +2 -2
  125. package/dist/lib/generators/module-extension-facts.js +93 -26
  126. package/dist/lib/generators/module-extension-facts.js.map +3 -3
  127. package/dist/lib/generators/module-facts-discovery.js +23 -0
  128. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  129. package/dist/lib/generators/module-facts.js +969 -41
  130. package/dist/lib/generators/module-facts.js.map +2 -2
  131. package/dist/lib/generators/module-override-targets.js +35 -11
  132. package/dist/lib/generators/module-override-targets.js.map +2 -2
  133. package/dist/lib/generators/module-registry.js +105 -2
  134. package/dist/lib/generators/module-registry.js.map +2 -2
  135. package/dist/lib/resolver.js +5 -0
  136. package/dist/lib/resolver.js.map +2 -2
  137. package/dist/lib/testing/integration-discovery.js +34 -0
  138. package/dist/lib/testing/integration-discovery.js.map +2 -2
  139. package/dist/lib/testing/integration.js +0 -2
  140. package/dist/lib/testing/integration.js.map +2 -2
  141. package/dist/mercato.js +4 -4
  142. package/dist/mercato.js.map +2 -2
  143. package/package.json +6 -6
  144. package/src/lib/__integration__/TC-INT-008.spec.ts +4 -2
  145. package/src/lib/__tests__/resolve-environment.test.ts +18 -0
  146. package/src/lib/agentic-setup.ts +10 -2
  147. package/src/lib/generators/__tests__/helpers/override-target-diagnostic-fixtures.ts +150 -0
  148. package/src/lib/generators/__tests__/module-extension-facts.test.ts +290 -0
  149. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +72 -7
  150. package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
  151. package/src/lib/generators/__tests__/module-facts.example-fact-coverage.test.ts +502 -0
  152. package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +414 -0
  153. package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
  154. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +42 -1
  155. package/src/lib/generators/__tests__/module-override-targets.diagnostic-wiring.test.ts +92 -0
  156. package/src/lib/generators/__tests__/module-override-targets.test.ts +24 -0
  157. package/src/lib/generators/__tests__/module-override-targets.unknown-mode.test.ts +117 -0
  158. package/src/lib/generators/__tests__/module-subset.test.ts +60 -0
  159. package/src/lib/generators/module-extension-facts.ts +181 -25
  160. package/src/lib/generators/module-facts-discovery.ts +46 -0
  161. package/src/lib/generators/module-facts.ts +1330 -86
  162. package/src/lib/generators/module-override-targets.ts +70 -26
  163. package/src/lib/generators/module-registry.ts +135 -2
  164. package/src/lib/resolver.ts +6 -1
  165. package/src/lib/testing/__tests__/integration-discovery.test.ts +35 -0
  166. package/src/lib/testing/integration-discovery.ts +50 -0
  167. package/src/lib/testing/integration.ts +0 -2
  168. package/src/mercato.ts +3 -3
@@ -60,6 +60,8 @@ export interface ExtractModuleExtensionFactsOptions {
60
60
  notifications?: readonly string[]
61
61
  aiTools?: ReadonlyArray<{ name: string; sourcePath: string }>
62
62
  aiAgents?: ReadonlyArray<{ id: string; sourcePath: string }>
63
+ /** Generated-facts compatibility projection. Omitted means the corrected v2 contract. */
64
+ factsContractVersion?: 1 | 2
63
65
  }
64
66
 
65
67
  export interface CorrelateExtensionFactsOptions {
@@ -69,12 +71,15 @@ export interface CorrelateExtensionFactsOptions {
69
71
  apiRoutes: ReadonlySet<string>
70
72
  commandIds?: ReadonlySet<string>
71
73
  contributingModuleId?: string
74
+ /** Generated-facts compatibility projection. Omitted means the corrected v2 contract. */
75
+ factsContractVersion?: 1 | 2
72
76
  }
73
77
 
74
78
  type StaticContext = {
75
79
  initializers: Map<string, ts.Expression | ts.FunctionDeclaration>
76
80
  sourceFile: ts.SourceFile
77
81
  resolving: Set<string>
82
+ factsContractVersion: 1 | 2
78
83
  }
79
84
 
80
85
  type ApiExtensionHostIds = {
@@ -229,7 +234,7 @@ function propertyName(node: ts.PropertyName): string | null {
229
234
  return null
230
235
  }
231
236
 
232
- function buildStaticContext(file: ts.SourceFile): StaticContext {
237
+ function buildStaticContext(file: ts.SourceFile, factsContractVersion: 1 | 2 = 2): StaticContext {
233
238
  const initializers = new Map<string, ts.Expression | ts.FunctionDeclaration>()
234
239
  const visit = (node: ts.Node): void => {
235
240
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
@@ -241,7 +246,7 @@ function buildStaticContext(file: ts.SourceFile): StaticContext {
241
246
  node.forEachChild(visit)
242
247
  }
243
248
  file.forEachChild(visit)
244
- return { initializers, sourceFile: file, resolving: new Set() }
249
+ return { initializers, sourceFile: file, resolving: new Set(), factsContractVersion }
245
250
  }
246
251
 
247
252
  function entityRegistryValue(expression: ts.PropertyAccessExpression): string | null {
@@ -266,6 +271,26 @@ function staticTemplate(expression: ts.TemplateExpression, context: StaticContex
266
271
  return value
267
272
  }
268
273
 
274
+ /**
275
+ * A function-valued property carries no statically readable value, but its
276
+ * PRESENCE is the discriminant several conventions are keyed on (a
277
+ * `ComponentOverride` is a wrapper/props-transform/replacement depending on
278
+ * which callable it declares; an enricher declares `enrichOne`/`enrichMany`).
279
+ * Method shorthand (`enrichOne() {}`) is already recorded as `true`, so an
280
+ * arrow/function-expression property — or an identifier bound to one — records
281
+ * the same marker instead of vanishing from the object.
282
+ */
283
+ function isFunctionLikeInitializer(expression: ts.Expression, context: StaticContext): boolean {
284
+ const current = unwrap(expression)
285
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) return true
286
+ if (!ts.isIdentifier(current)) return false
287
+ const initializer = context.initializers.get(current.text)
288
+ if (!initializer) return false
289
+ if (ts.isFunctionDeclaration(initializer)) return true
290
+ const resolved = unwrap(initializer)
291
+ return ts.isArrowFunction(resolved) || ts.isFunctionExpression(resolved)
292
+ }
293
+
269
294
  function staticObject(expression: ts.ObjectLiteralExpression, context: StaticContext): StaticObject {
270
295
  const result: StaticObject = {}
271
296
  for (const property of expression.properties) {
@@ -274,6 +299,7 @@ function staticObject(expression: ts.ObjectLiteralExpression, context: StaticCon
274
299
  if (!name) continue
275
300
  const value = staticValue(property.initializer, context)
276
301
  if (value !== undefined) result[name] = value
302
+ else if (context.factsContractVersion === 2 && isFunctionLikeInitializer(property.initializer, context)) result[name] = true
277
303
  continue
278
304
  }
279
305
  if (ts.isShorthandPropertyAssignment(property)) {
@@ -326,6 +352,12 @@ function staticValue(expression: ts.Expression, context: StaticContext): StaticV
326
352
  return value
327
353
  }
328
354
  if (ts.isCallExpression(current)) {
355
+ const handle = componentReplacementHandle(current, context)
356
+ if (handle) {
357
+ if (context.factsContractVersion === 2) return handle.resolved
358
+ const firstArgument = current.arguments[0]
359
+ return firstArgument ? staticValue(firstArgument, context) : undefined
360
+ }
329
361
  if (ts.isIdentifier(current.expression)) {
330
362
  const callable = context.initializers.get(current.expression.text)
331
363
  if (callable && (ts.isArrowFunction(callable) || ts.isFunctionExpression(callable) || ts.isFunctionDeclaration(callable))) {
@@ -339,14 +371,19 @@ function staticValue(expression: ts.Expression, context: StaticContext): StaticV
339
371
  initializers,
340
372
  sourceFile: context.sourceFile,
341
373
  resolving: new Set(context.resolving),
374
+ factsContractVersion: context.factsContractVersion,
342
375
  }
343
376
  if (callable.body && ts.isExpression(callable.body)) return staticValue(callable.body, childContext)
344
377
  const returnStatement = callable.body?.statements.find(ts.isReturnStatement)
345
378
  if (returnStatement?.expression) return staticValue(returnStatement.expression, childContext)
346
379
  }
347
380
  }
381
+ // Factory-style `defineThing({ … })` calls forward their configuration object,
382
+ // but a member call (`Handles.section('a', 'b')`) computes a value from its
383
+ // arguments — forwarding the first one there invents a wrong id, so it stays
384
+ // unresolved unless a formula above knows the builder.
348
385
  const firstArgument = current.arguments[0]
349
- if (firstArgument) {
386
+ if (firstArgument && (context.factsContractVersion === 1 || ts.isIdentifier(current.expression))) {
350
387
  const value = staticValue(firstArgument, context)
351
388
  if (!isStaticObject(value) || !ts.isIdentifier(current.expression)) return value
352
389
  if (current.expression.text === 'dataTableExtensionHost') return { family: 'data-table', ...value }
@@ -363,6 +400,33 @@ function staticValue(expression: ts.Expression, context: StaticContext): StaticV
363
400
  return undefined
364
401
  }
365
402
 
403
+ /**
404
+ * `ComponentReplacementHandles` (packages/shared/src/modules/widgets/component-registry.ts)
405
+ * is the framework-owned builder every `widgets/components.ts` uses to name a
406
+ * replacement handle. Without these formulas the generic call fallback would read
407
+ * `ComponentReplacementHandles.section('ui.detail', 'NotesSection')` as its first
408
+ * argument and publish `ui.detail` as the handle — a target id that exists nowhere.
409
+ */
410
+ const COMPONENT_REPLACEMENT_HANDLE_BUILDERS: Record<string, (args: Array<string | undefined>) => string | undefined> = {
411
+ page: ([routePath]) => routePath ? `page:${routePath}` : undefined,
412
+ dataTable: ([tableId]) => tableId ? `data-table:${tableId}` : undefined,
413
+ crudForm: ([entityId]) => entityId ? `crud-form:${entityId}` : undefined,
414
+ section: ([scope, sectionId]) => scope && sectionId ? `section:${scope}.${sectionId}` : undefined,
415
+ }
416
+
417
+ function componentReplacementHandle(
418
+ expression: ts.CallExpression,
419
+ context: StaticContext,
420
+ ): { resolved: string | undefined } | null {
421
+ if (!ts.isPropertyAccessExpression(expression.expression)) return null
422
+ const callee = expression.expression
423
+ if (!ts.isIdentifier(callee.expression) || callee.expression.text !== 'ComponentReplacementHandles') return null
424
+ const builder = COMPONENT_REPLACEMENT_HANDLE_BUILDERS[callee.name.text]
425
+ if (!builder) return { resolved: undefined }
426
+ const args = expression.arguments.map((argument) => stringValue(staticValue(argument, context)))
427
+ return { resolved: builder(args) }
428
+ }
429
+
366
430
  function isStaticObject(value: StaticValue | undefined): value is StaticObject {
367
431
  return !!value && typeof value === 'object' && !Array.isArray(value)
368
432
  }
@@ -399,20 +463,32 @@ function conventionPath(moduleRoot: string, relativePath: string): string | null
399
463
  return null
400
464
  }
401
465
 
402
- export function readConventionObjectArray(filePath: string, exportName: string): StaticObject[] {
466
+ function readConventionObjectArrayForContract(
467
+ filePath: string,
468
+ exportName: string,
469
+ factsContractVersion: 1 | 2,
470
+ ): StaticObject[] {
403
471
  const file = sourceFile(filePath)
404
472
  if (!file) return []
405
- const context = buildStaticContext(file)
473
+ const context = buildStaticContext(file, factsContractVersion)
406
474
  const initializer = context.initializers.get(exportName)
407
475
  if (!initializer || ts.isFunctionDeclaration(initializer)) return []
408
476
  const value = staticValue(initializer, context)
409
477
  return Array.isArray(value) ? value.filter(isStaticObject) : []
410
478
  }
411
479
 
412
- function readRootObject(filePath: string, variableName: string): StaticObject | null {
480
+ export function readConventionObjectArray(filePath: string, exportName: string): StaticObject[] {
481
+ return readConventionObjectArrayForContract(filePath, exportName, 2)
482
+ }
483
+
484
+ function readRootObject(
485
+ filePath: string,
486
+ variableName: string,
487
+ factsContractVersion: 1 | 2 = 2,
488
+ ): StaticObject | null {
413
489
  const file = sourceFile(filePath)
414
490
  if (!file) return null
415
- const context = buildStaticContext(file)
491
+ const context = buildStaticContext(file, factsContractVersion)
416
492
  const initializer = context.initializers.get(variableName)
417
493
  if (!initializer || ts.isFunctionDeclaration(initializer)) return null
418
494
  const value = staticValue(initializer, context)
@@ -1019,20 +1095,36 @@ function contributionBase(
1019
1095
  }
1020
1096
  }
1021
1097
 
1098
+ /**
1099
+ * `ModuleInjectionTable` maps a spot to `ModuleInjectionSlot | ModuleInjectionSlot[]`,
1100
+ * and a slot is either a bare widget-id string or a placement object. The runtime
1101
+ * loader normalizes all three shapes (`injection-loader.ts` → `loadInjectionTable`);
1102
+ * reading only the array form here silently dropped every string and single-object
1103
+ * slot from the generated contribution facts.
1104
+ */
1105
+ function injectionTableSlots(value: StaticValue | undefined): StaticValue[] {
1106
+ if (value === undefined) return []
1107
+ return Array.isArray(value) ? value : [value]
1108
+ }
1109
+
1022
1110
  function extractInjectionTable(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
1023
1111
  const filePath = conventionPath(options.moduleRoot, 'widgets/injection-table.ts')
1024
1112
  if (!filePath) return []
1025
- const table = readRootObject(filePath, 'injectionTable')
1113
+ const table = readRootObject(filePath, 'injectionTable', options.factsContractVersion ?? 2)
1026
1114
  if (!table) return []
1027
1115
  const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, filePath)
1028
1116
  const facts: ModuleExtensionContributionFact[] = []
1029
1117
  for (const targetId of Object.keys(table).sort((left, right) => left.localeCompare(right))) {
1030
- const entries = table[targetId]
1031
- if (!Array.isArray(entries)) continue
1118
+ const entries = options.factsContractVersion === 1
1119
+ ? (Array.isArray(table[targetId]) ? table[targetId] as StaticValue[] : [])
1120
+ : injectionTableSlots(table[targetId])
1032
1121
  for (const entry of entries) {
1033
- if (!isStaticObject(entry)) continue
1034
- const widgetId = stringValue(entry.widgetId)
1122
+ if (options.factsContractVersion === 1 && !isStaticObject(entry)) continue
1123
+ const slot = isStaticObject(entry) ? entry : null
1124
+ const widgetId = typeof entry === 'string' ? entry : slot ? stringValue(slot.widgetId) : undefined
1035
1125
  if (!widgetId) continue
1126
+ const features = slot ? strings(slot.features) : []
1127
+ const priority = slot ? numberValue(slot.priority) : undefined
1036
1128
  const payload = targetId.endsWith(':columns') ? 'column'
1037
1129
  : targetId.endsWith(':row-actions') ? 'row-action'
1038
1130
  : targetId.endsWith(':bulk-actions') ? 'bulk-action'
@@ -1043,8 +1135,8 @@ function extractInjectionTable(options: ExtractModuleExtensionFactsOptions): Mod
1043
1135
  const shared = {
1044
1136
  ...base,
1045
1137
  targets: [target(targetId)],
1046
- features: strings(entry.features),
1047
- placement: numberValue(entry.priority) !== undefined ? { priority: numberValue(entry.priority) } : undefined,
1138
+ features,
1139
+ placement: priority !== undefined ? { priority } : undefined,
1048
1140
  }
1049
1141
  if (targetId.startsWith('data-table:')) {
1050
1142
  facts.push({
@@ -1053,7 +1145,7 @@ function extractInjectionTable(options: ExtractModuleExtensionFactsOptions): Mod
1053
1145
  details: {
1054
1146
  payload: payload === 'field' ? 'render' : payload,
1055
1147
  tableId: targetId.replace(/^data-table:/, '').replace(/:(?:columns|row-actions|bulk-actions|filters|toolbar|header|footer|search-trailing)$/, ''),
1056
- executionGuard: strings(entry.features).length > 0 ? 'both' : 'host',
1148
+ executionGuard: features.length > 0 ? 'both' : 'host',
1057
1149
  },
1058
1150
  })
1059
1151
  } else if (targetId.startsWith('crud-form:')) {
@@ -1073,7 +1165,7 @@ function extractInjectionTable(options: ExtractModuleExtensionFactsOptions): Mod
1073
1165
  details: {
1074
1166
  payload: 'render',
1075
1167
  registryKey: widgetId,
1076
- executionGuard: strings(entry.features).length > 0 ? 'both' : 'host',
1168
+ executionGuard: features.length > 0 ? 'both' : 'host',
1077
1169
  },
1078
1170
  })
1079
1171
  }
@@ -1093,7 +1185,9 @@ function extractObjectConvention(options: {
1093
1185
  if (!filePath) return []
1094
1186
  const exportNames = [options.exportName, ...(options.exportAliases ?? [])]
1095
1187
  const entries = exportNames.reduce<StaticObject[]>((found, exportName) =>
1096
- found.length > 0 ? found : readConventionObjectArray(filePath, exportName), [])
1188
+ found.length > 0
1189
+ ? found
1190
+ : readConventionObjectArrayForContract(filePath, exportName, options.module.factsContractVersion ?? 2), [])
1097
1191
  const sourcePath = portablePath(options.module.moduleRoot, options.module.sourceRoot, filePath)
1098
1192
  return entries.flatMap((entry, index) => options.build(entry, sourcePath, index) ?? [])
1099
1193
  }
@@ -1353,7 +1447,11 @@ function extractComponentOverrides(options: ExtractModuleExtensionFactsOptions):
1353
1447
  const targetDefinition = isStaticObject(entry.target) ? entry.target : null
1354
1448
  const handle = targetDefinition ? stringValue(targetDefinition.componentId) : undefined
1355
1449
  if (!handle) return null
1356
- const mode = entry.wrapper !== undefined ? 'wrapper' : entry.props !== undefined ? 'props' : 'replace'
1450
+ // `ComponentOverride` discriminates on `wrapper` / `propsTransform` /
1451
+ // `replacement` — never on a `props` property, which the union has no member for.
1452
+ const mode = options.factsContractVersion === 1
1453
+ ? (entry.wrapper !== undefined ? 'wrapper' : entry.props !== undefined ? 'props' : 'replace')
1454
+ : (entry.wrapper !== undefined ? 'wrapper' : entry.propsTransform !== undefined ? 'props' : 'replace')
1357
1455
  const id = `${options.moduleId}.component-override.${index}:${handle}`
1358
1456
  const contribution = contributionBase(id, sourcePath, 'componentOverrides')
1359
1457
  return {
@@ -1780,6 +1878,22 @@ function targetModuleId(targetId: string): string | null {
1780
1878
  return match?.[1] ?? null
1781
1879
  }
1782
1880
 
1881
+ function normalizedEntityId(value: string): string {
1882
+ return value.replace(/^([a-z][a-z0-9_]*):/, '$1.')
1883
+ }
1884
+
1885
+ function entityFactKeyForTarget(targetId: string, entityIds: ReadonlySet<string>): string | null {
1886
+ const normalizedTarget = normalizedEntityId(targetId)
1887
+ for (const entityId of entityIds) {
1888
+ const normalizedEntity = normalizedEntityId(entityId)
1889
+ if (normalizedTarget === normalizedEntity) return entityId
1890
+ if (normalizedTarget.startsWith(`${normalizedEntity}.`) && /\.(?:creating|created|updating|updated|deleting|deleted)$/.test(normalizedTarget)) {
1891
+ return entityId
1892
+ }
1893
+ }
1894
+ return null
1895
+ }
1896
+
1783
1897
  export function correlateExtensionTarget(
1784
1898
  targetFact: ModuleExtensionTargetFact,
1785
1899
  options: CorrelateExtensionFactsOptions,
@@ -1813,8 +1927,11 @@ export function correlateExtensionTarget(
1813
1927
  const framework = allFrameworkHosts().find((host) => patternMatches(host.id, targetFact.id))
1814
1928
  ?? (FRAMEWORK_PREFIXES.some((prefix) => targetFact.id.startsWith(prefix)) ? FRAMEWORK_HOSTS[0] : undefined)
1815
1929
  if (framework) return { id: targetFact.id, resolution: 'framework' }
1816
- if (options.entityIds.has(targetFact.id)) {
1817
- return { id: targetFact.id, resolution: 'fact-ref', factRef: { factSection: 'entities', factKey: targetFact.id } }
1930
+ const entityFactKey = options.factsContractVersion === 1
1931
+ ? (options.entityIds.has(targetFact.id) ? targetFact.id : null)
1932
+ : entityFactKeyForTarget(targetFact.id, options.entityIds)
1933
+ if (entityFactKey) {
1934
+ return { id: targetFact.id, resolution: 'fact-ref', factRef: { factSection: 'entities', factKey: entityFactKey } }
1818
1935
  }
1819
1936
  if (options.eventIds.has(targetFact.id)) {
1820
1937
  return { id: targetFact.id, resolution: 'fact-ref', factRef: { factSection: 'events', factKey: targetFact.id } }
@@ -2331,17 +2448,56 @@ export function assertNoUnresolvedExtensionTargets(
2331
2448
  * — spec 2026-08-02-module-facts-exact-override-targets). Keyed by the dotted
2332
2449
  * host id (e.g. `routes.api`, `ai.extensions`, `nav.groupOrder`) so the module
2333
2450
  * override-target adapters share one mode source of truth with the catalog.
2451
+ *
2452
+ * @deprecated Use {@link getFrameworkOverrideHostOperations}. This helper drops any host whose
2453
+ * declared operation is not one of the three recognized modes, which collapses "the catalog does
2454
+ * not describe this host" into "the catalog describes it with a mode this generator has fallen
2455
+ * behind on" — the distinction the `unknown-framework-domain` / `unknown-framework-mode`
2456
+ * diagnostics exist to keep apart. Every in-tree consumer has moved; the export is retained
2457
+ * because `BACKWARD_COMPATIBILITY.md` classifies exported generator helpers as removable only
2458
+ * through the deprecation protocol, and it stays behaviour-identical for as long as it exists.
2334
2459
  */
2335
- export function getFrameworkOverrideModes(): Record<string, 'disable-replace' | 'replace' | 'additive'> {
2460
+ export function getFrameworkOverrideModes(
2461
+ hosts: readonly ModuleExtensionHostFact[] = FRAMEWORK_OVERRIDE_HOSTS,
2462
+ ): Record<string, 'disable-replace' | 'replace' | 'additive'> {
2336
2463
  const modes: Record<string, 'disable-replace' | 'replace' | 'additive'> = {}
2337
- for (const host of FRAMEWORK_OVERRIDE_HOSTS) {
2338
- const dotted = host.key.replace(/^framework\.module-override\./, '')
2339
- const mode = host.operations?.[0]
2340
- if (mode === 'disable-replace' || mode === 'replace' || mode === 'additive') modes[dotted] = mode
2464
+ for (const [dotted, operation] of Object.entries(getFrameworkOverrideHostOperations(hosts))) {
2465
+ if (operation === 'disable-replace' || operation === 'replace' || operation === 'additive') {
2466
+ modes[dotted] = operation
2467
+ }
2341
2468
  }
2342
2469
  return modes
2343
2470
  }
2344
2471
 
2472
+ /**
2473
+ * The catalog's declared first operation for every dotted unified-override host,
2474
+ * **without** validating it against the known mode set.
2475
+ *
2476
+ * {@link getFrameworkOverrideModes} silently drops a host whose operation is not a
2477
+ * recognized mode, which makes "the catalog does not describe this host at all"
2478
+ * indistinguishable from "the catalog describes it with a mode this generator does
2479
+ * not understand". Consumers that must tell those apart (the override-target
2480
+ * adapters, which emit different diagnostics for each) read the raw operations
2481
+ * here instead — spec `2026-07-31-standalone-canonical-example-module.md`,
2482
+ * § PR #4883 Module-Fact and Extension-Topology Contract.
2483
+ *
2484
+ * `hosts` defaults to the framework catalog. Every catalog host declares a valid
2485
+ * mode today, so the pass-through is unobservable against the real catalog; passing
2486
+ * an explicit host list is how `module-override-targets.unknown-mode.test.ts` proves
2487
+ * that an operation this generator does not recognize really does survive here.
2488
+ */
2489
+ export function getFrameworkOverrideHostOperations(
2490
+ hosts: readonly ModuleExtensionHostFact[] = FRAMEWORK_OVERRIDE_HOSTS,
2491
+ ): Record<string, string> {
2492
+ const operations: Record<string, string> = {}
2493
+ for (const host of hosts) {
2494
+ const dotted = host.key.replace(/^framework\.module-override\./, '')
2495
+ const operation = host.operations?.[0]
2496
+ if (typeof operation === 'string') operations[dotted] = operation
2497
+ }
2498
+ return operations
2499
+ }
2500
+
2345
2501
  export function getFrameworkExtensionHosts(): ModuleExtensionHostFact[] {
2346
2502
  return sortHosts(allFrameworkHosts().map((host) => ({
2347
2503
  ...host,
@@ -107,3 +107,49 @@ export function discoverPackageModuleSources(resolver: PackageResolver): ModuleF
107
107
  }
108
108
  return resolveDuplicateModuleIds(sources, resolver)
109
109
  }
110
+
111
+ /** App-local modules always live under this app-root-relative directory. */
112
+ export const LOCAL_MODULES_DIRECTORY = path.posix.join('src', 'modules')
113
+
114
+ /**
115
+ * Locates an app-local module that is projected into the reference bundle rather than the
116
+ * normal package outputs. `appRoot` is the directory containing `src/modules` — the
117
+ * create-app template root at build time, the generated app root at scaffold and
118
+ * `agentic:init` time — so the same module yields the same portable `src/modules/<id>`
119
+ * provenance from every root. Returns `null` when the module is absent or is not readable
120
+ * TypeScript source, because a reference bundle built from an unreadable root would claim
121
+ * an empty surface rather than fail.
122
+ */
123
+ export function discoverLocalReferenceModuleSource(options: {
124
+ appRoot: string
125
+ moduleId: string
126
+ }): ModuleFactSource | null {
127
+ const moduleRoot = path.join(options.appRoot, 'src', 'modules', options.moduleId)
128
+ if (!hasReadableModuleSource(moduleRoot)) return null
129
+ return {
130
+ moduleId: options.moduleId,
131
+ moduleRoot,
132
+ portableSourceRoot: path.posix.join(LOCAL_MODULES_DIRECTORY, options.moduleId),
133
+ sourceKind: 'local-reference',
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Appends the local reference source to the package batch, rejecting a duplicate module
139
+ * id *before* assignment. A package that already provides the same id would otherwise be
140
+ * silently replaced by (or silently replace) the app-local projection, and the emitted
141
+ * bundle would attribute one module's surfaces to the other's source tree.
142
+ */
143
+ export function appendLocalReferenceModuleSource(
144
+ packageSources: readonly ModuleFactSource[],
145
+ reference: ModuleFactSource,
146
+ ): ModuleFactSource[] {
147
+ const conflict = packageSources.find((source) => source.moduleId === reference.moduleId)
148
+ if (conflict) {
149
+ throw new Error(
150
+ `[module-facts] local reference module "${reference.moduleId}" collides with the module provided by `
151
+ + `${conflict.from ?? '<unknown package>'}. Rename the app-local module or remove the package providing it.`,
152
+ )
153
+ }
154
+ return [...packageSources, reference]
155
+ }