@open-mercato/cli 0.6.7 → 0.6.8-develop.6875.1.871a4afc94
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.
- package/.turbo/turbo-build.log +4 -4
- package/build.mjs +24 -7
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
- package/dist/agentic/codex/enforcement-rules.md +2 -16
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
- package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
- package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
- package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
- package/dist/agentic/guides/ai-workflows.md +76 -0
- package/dist/agentic/guides/architecture.md +107 -0
- package/dist/agentic/guides/backend-ui.md +80 -0
- package/dist/agentic/guides/contracts.md +92 -0
- package/dist/agentic/guides/extensions.md +121 -0
- package/dist/agentic/guides/framework-extension-points.md +36 -0
- package/dist/agentic/guides/integrations.md +81 -0
- package/dist/agentic/guides/module-facts.json +115537 -3951
- package/dist/agentic/guides/module-system.md +12 -127
- package/dist/agentic/guides/modules/ai_assistant.md +227 -28
- package/dist/agentic/guides/modules/api_docs.md +58 -2
- package/dist/agentic/guides/modules/api_keys.md +92 -7
- package/dist/agentic/guides/modules/attachments.md +119 -14
- package/dist/agentic/guides/modules/audit_logs.md +131 -10
- package/dist/agentic/guides/modules/auth.md +276 -33
- package/dist/agentic/guides/modules/business_rules.md +149 -14
- package/dist/agentic/guides/modules/catalog.md +475 -43
- package/dist/agentic/guides/modules/channel_gmail.md +82 -3
- package/dist/agentic/guides/modules/channel_imap.md +82 -3
- package/dist/agentic/guides/modules/checkout.md +299 -27
- package/dist/agentic/guides/modules/communication_channels.md +315 -33
- package/dist/agentic/guides/modules/configs.md +104 -8
- package/dist/agentic/guides/modules/content.md +57 -2
- package/dist/agentic/guides/modules/currencies.md +152 -17
- package/dist/agentic/guides/modules/customer_accounts.md +387 -45
- package/dist/agentic/guides/modules/customers.md +746 -85
- package/dist/agentic/guides/modules/dashboards.md +155 -10
- package/dist/agentic/guides/modules/data_sync.md +149 -16
- package/dist/agentic/guides/modules/design_system.md +102 -0
- package/dist/agentic/guides/modules/dictionaries.md +107 -12
- package/dist/agentic/guides/modules/directory.md +148 -15
- package/dist/agentic/guides/modules/entities.md +145 -13
- package/dist/agentic/guides/modules/events.md +59 -2
- package/dist/agentic/guides/modules/feature_toggles.md +119 -9
- package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
- package/dist/agentic/guides/modules/generators.md +45 -2
- package/dist/agentic/guides/modules/inbox_ops.md +207 -27
- package/dist/agentic/guides/modules/integrations.md +146 -16
- package/dist/agentic/guides/modules/messages.md +223 -24
- package/dist/agentic/guides/modules/notifications.md +96 -8
- package/dist/agentic/guides/modules/onboarding.md +77 -6
- package/dist/agentic/guides/modules/payment_gateways.md +164 -21
- package/dist/agentic/guides/modules/perspectives.md +77 -7
- package/dist/agentic/guides/modules/planner.md +136 -18
- package/dist/agentic/guides/modules/portal.md +93 -2
- package/dist/agentic/guides/modules/progress.md +104 -14
- package/dist/agentic/guides/modules/query_index.md +154 -15
- package/dist/agentic/guides/modules/record_locks.md +206 -21
- package/dist/agentic/guides/modules/resources.md +197 -30
- package/dist/agentic/guides/modules/sales.md +678 -84
- package/dist/agentic/guides/modules/scheduler.md +133 -14
- package/dist/agentic/guides/modules/search.md +170 -6
- package/dist/agentic/guides/modules/security.md +294 -35
- package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
- package/dist/agentic/guides/modules/sso.md +151 -26
- package/dist/agentic/guides/modules/staff.md +369 -57
- package/dist/agentic/guides/modules/storage_s3.md +95 -5
- package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
- package/dist/agentic/guides/modules/sync_excel.md +75 -6
- package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
- package/dist/agentic/guides/modules/translations.md +104 -10
- package/dist/agentic/guides/modules/webhooks.md +203 -24
- package/dist/agentic/guides/modules/wms.md +441 -46
- package/dist/agentic/guides/modules/workflows.md +302 -43
- package/dist/agentic/guides/spec-delivery.md +22 -0
- package/dist/agentic/guides/testing-debugging.md +80 -0
- package/dist/agentic/guides/upstream/AGENTS.md +299 -0
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
- package/dist/agentic/guides/upstream/manifest.json +8 -0
- package/dist/agentic/shared/AGENTS.md.template +124 -311
- package/dist/agentic/shared/ai/agentic.config.json +2 -0
- package/dist/agentic/shared/ai/harness/README.md +50 -0
- package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
- package/dist/agentic/shared/ai/harness/cases.json +13680 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +133 -0
- package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +158 -0
- package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
- package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
- package/dist/agentic/shared/ai/harness/validators.json +78 -0
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
- package/dist/agentic/shared/ai/lessons/_template.md +16 -0
- package/dist/agentic/shared/ai/lessons.md +26 -3
- package/dist/agentic/shared/ai/review-checklist.md +36 -0
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +29 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +41 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
- package/dist/agentic/shared/ai/skills/tiers.json +143 -21
- package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
- package/dist/agentic/shared/ai/specs/README.md +4 -3
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
- package/dist/agentic/shared/ai/trackers/github.md +108 -4
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +187 -0
- package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2958 -0
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
- package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
- package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
- package/dist/agentic/shared/scripts/install-skills.sh +4 -694
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
- package/dist/bin.js +13 -1
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +13 -3
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +434 -186
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +20 -0
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-extension-facts.js +1840 -0
- package/dist/lib/generators/module-extension-facts.js.map +7 -0
- package/dist/lib/generators/module-fact-sources.js +70 -0
- package/dist/lib/generators/module-fact-sources.js.map +7 -0
- package/dist/lib/generators/module-facts-discovery.js +41 -7
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +1387 -45
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-override-targets.js +688 -0
- package/dist/lib/generators/module-override-targets.js.map +7 -0
- package/dist/lib/generators/module-registry.js +11 -5
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/load-env.js +24 -0
- package/dist/lib/load-env.js.map +7 -0
- package/dist/lib/resolver.js +2 -0
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/telemetry-init.js +358 -0
- package/dist/lib/telemetry-init.js.map +7 -0
- package/dist/lib/testing/integration.js +5 -0
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +23 -1
- package/dist/mercato.js.map +2 -2
- package/package.json +13 -11
- package/src/bin.ts +33 -1
- package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
- package/src/lib/__tests__/agentic-init.test.ts +41 -5
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
- package/src/lib/__tests__/load-env.test.ts +65 -0
- package/src/lib/__tests__/telemetry-init.test.ts +298 -0
- package/src/lib/agentic-init.ts +15 -3
- package/src/lib/agentic-setup.ts +573 -222
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +404 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +88 -2
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +119 -0
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
- package/src/lib/generators/__tests__/module-override-targets.test.ts +304 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
- package/src/lib/generators/extensions/injection-widgets.ts +51 -0
- package/src/lib/generators/module-extension-facts.ts +2365 -0
- package/src/lib/generators/module-fact-sources.ts +108 -0
- package/src/lib/generators/module-facts-discovery.ts +58 -7
- package/src/lib/generators/module-facts.ts +1851 -46
- package/src/lib/generators/module-override-targets.ts +863 -0
- package/src/lib/generators/module-registry.ts +16 -6
- package/src/lib/load-env.ts +30 -0
- package/src/lib/resolver.ts +3 -0
- package/src/lib/telemetry-init.ts +461 -0
- package/src/lib/testing/__tests__/integration.test.ts +4 -0
- package/src/lib/testing/integration.ts +5 -0
- package/src/mercato.ts +26 -1
- package/dist/agentic/guides/cache.md +0 -45
- package/dist/agentic/guides/core.md +0 -453
- package/dist/agentic/guides/events.md +0 -117
- package/dist/agentic/guides/queue.md +0 -53
- package/dist/agentic/guides/search.md +0 -115
- package/dist/agentic/guides/shared.md +0 -108
- package/dist/agentic/guides/ui.md +0 -347
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import ts from 'typescript-js'
|
|
4
|
+
import type {
|
|
5
|
+
ModuleExtensionContributionFact,
|
|
6
|
+
ModuleExtensionHostFact,
|
|
7
|
+
ModuleExtensionSurfaceFacts,
|
|
8
|
+
ModuleExtensionTargetRef,
|
|
9
|
+
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
4
10
|
import { toSnake } from '../utils'
|
|
11
|
+
import { extractCommandIdsFromSource } from './module-registry'
|
|
12
|
+
import { MODULE_CODE_EXTENSIONS } from './scanner'
|
|
13
|
+
import type { ApiRouteInterceptorBridge } from './module-extension-facts'
|
|
14
|
+
import {
|
|
15
|
+
assertNoUnresolvedExtensionTargets,
|
|
16
|
+
correlateIncomingExtensions,
|
|
17
|
+
correlateModuleExtensionFacts,
|
|
18
|
+
extractActivationTargetOwners,
|
|
19
|
+
extractKnownApiRouteIds,
|
|
20
|
+
extractKnownCommandIds,
|
|
21
|
+
extractModuleExtensionFacts,
|
|
22
|
+
renderFrameworkExtensionPointsMarkdown,
|
|
23
|
+
withModuleExtensionFactExtractionCache,
|
|
24
|
+
} from './module-extension-facts'
|
|
25
|
+
import {
|
|
26
|
+
collectModuleOverrideTargets,
|
|
27
|
+
type ModuleOverrideTarget,
|
|
28
|
+
type ModuleOverrideTargetDiagnostic,
|
|
29
|
+
} from './module-override-targets'
|
|
30
|
+
import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
|
|
5
31
|
|
|
6
32
|
export interface ModuleEntityFact {
|
|
7
33
|
id: string
|
|
@@ -21,6 +47,49 @@ export interface ModuleApiRouteFact {
|
|
|
21
47
|
path: string
|
|
22
48
|
methods: string[]
|
|
23
49
|
auth: Record<string, ApiRouteAuthRule>
|
|
50
|
+
sourcePath: string | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Statically declared page metadata that affects extensibility or access, using the
|
|
55
|
+
* runtime `PageMetadata` contract keys and the same alias precedence as
|
|
56
|
+
* `resolvePageRouteMetadata` (`pageGroup ?? group`, `pageGroupKey ?? groupKey`,
|
|
57
|
+
* `pageOrder ?? order`). Executable guards, icons, titles and translation results are
|
|
58
|
+
* never serialized.
|
|
59
|
+
*/
|
|
60
|
+
export interface ModulePageMetadata {
|
|
61
|
+
requireAuth?: boolean
|
|
62
|
+
requireCustomerAuth?: boolean
|
|
63
|
+
requireFeatures?: string[]
|
|
64
|
+
requireCustomerFeatures?: string[]
|
|
65
|
+
navHidden?: boolean
|
|
66
|
+
navGroup?: string
|
|
67
|
+
navGroupKey?: string
|
|
68
|
+
navOrder?: number
|
|
69
|
+
pageContext?: 'main' | 'admin' | 'settings' | 'profile'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ModulePageFact {
|
|
73
|
+
path: string
|
|
74
|
+
sourcePath: string
|
|
75
|
+
/** Companion metadata file (`page.meta.*` / `meta.*`) when runtime reads metadata from one. */
|
|
76
|
+
metadataSourcePath?: string
|
|
77
|
+
metadata?: ModulePageMetadata
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ModuleCliCommandFact {
|
|
81
|
+
command: string
|
|
82
|
+
sourcePath: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ModuleAiToolFact {
|
|
86
|
+
name: string
|
|
87
|
+
sourcePath: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ModuleAiAgentFact {
|
|
91
|
+
id: string
|
|
92
|
+
sourcePath: string
|
|
24
93
|
}
|
|
25
94
|
|
|
26
95
|
export interface ModuleEventFact {
|
|
@@ -28,6 +97,8 @@ export interface ModuleEventFact {
|
|
|
28
97
|
label?: string
|
|
29
98
|
category: string | null
|
|
30
99
|
entity: string | null
|
|
100
|
+
clientBroadcast?: boolean
|
|
101
|
+
portalBroadcast?: boolean
|
|
31
102
|
}
|
|
32
103
|
|
|
33
104
|
export interface ModuleHostTokens {
|
|
@@ -35,11 +106,116 @@ export interface ModuleHostTokens {
|
|
|
35
106
|
tableIds: string[]
|
|
36
107
|
}
|
|
37
108
|
|
|
109
|
+
export type ModuleFactSourceRef = {
|
|
110
|
+
sourcePath: string
|
|
111
|
+
exportName?: string
|
|
112
|
+
line?: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ModuleFactRef = {
|
|
116
|
+
factSection: string
|
|
117
|
+
factKey: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type ModuleFactSourceKind =
|
|
121
|
+
| 'module-metadata'
|
|
122
|
+
| 'entity'
|
|
123
|
+
| 'event'
|
|
124
|
+
| 'acl-feature'
|
|
125
|
+
| 'api-route'
|
|
126
|
+
| 'di-registration'
|
|
127
|
+
| 'search'
|
|
128
|
+
| 'vector'
|
|
129
|
+
| 'notification'
|
|
130
|
+
| 'cli-command'
|
|
131
|
+
| 'backend-page'
|
|
132
|
+
| 'frontend-page'
|
|
133
|
+
| 'ai-tool'
|
|
134
|
+
| 'ai-agent'
|
|
135
|
+
| 'ai-extension'
|
|
136
|
+
| 'command'
|
|
137
|
+
| 'subscriber'
|
|
138
|
+
| 'worker'
|
|
139
|
+
| 'page-middleware'
|
|
140
|
+
| 'setup'
|
|
141
|
+
| 'encryption'
|
|
142
|
+
| 'custom-entity'
|
|
143
|
+
| 'integration'
|
|
144
|
+
| 'generator-plugin'
|
|
145
|
+
| 'extension-host'
|
|
146
|
+
| 'extension-contribution'
|
|
147
|
+
|
|
148
|
+
export type ModuleOwnedContractKind =
|
|
149
|
+
| 'module-metadata'
|
|
150
|
+
| 'command'
|
|
151
|
+
| 'worker'
|
|
152
|
+
| 'page-middleware'
|
|
153
|
+
| 'setup'
|
|
154
|
+
| 'encryption'
|
|
155
|
+
| 'di-registration'
|
|
156
|
+
| 'custom-entity'
|
|
157
|
+
| 'ai-extension'
|
|
158
|
+
| 'generator-plugin'
|
|
159
|
+
|
|
160
|
+
export type ModuleFactSafeScalar = string | number | boolean | null
|
|
161
|
+
|
|
162
|
+
export type ModuleOwnedContractFact = {
|
|
163
|
+
kind: ModuleOwnedContractKind
|
|
164
|
+
id: string
|
|
165
|
+
source: ModuleFactSourceRef
|
|
166
|
+
metadata?: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ModuleOwnedContracts = Partial<
|
|
170
|
+
Record<ModuleOwnedContractKind, ModuleOwnedContractFact[]>
|
|
171
|
+
>
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Provenance pointer into a fact section that already serializes the declaration site.
|
|
175
|
+
* `factKey` is omitted when it equals the owning entry's `id`, which is the common case.
|
|
176
|
+
*/
|
|
177
|
+
export type ModuleFactIndexRef = {
|
|
178
|
+
factSection: string
|
|
179
|
+
factKey?: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* One provenance entry per proven `(kind, id)` in the module. Exactly one of `source`
|
|
184
|
+
* (the declaration site, for facts that have no other home in the JSON) or `factRef`
|
|
185
|
+
* (a deterministic pointer into a fact section that already carries the source inline)
|
|
186
|
+
* is present, so a consumer resolves any fact's origin through this single index.
|
|
187
|
+
*/
|
|
188
|
+
export type ModuleFactSourceEntry = {
|
|
189
|
+
kind: ModuleFactSourceKind
|
|
190
|
+
id: string
|
|
191
|
+
source?: ModuleFactSourceRef
|
|
192
|
+
factRef?: ModuleFactIndexRef
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type ModuleFactDiagnostic = {
|
|
196
|
+
code: 'duplicate-source' | 'unresolved-static-contract'
|
|
197
|
+
kind: ModuleFactSourceKind
|
|
198
|
+
id: string
|
|
199
|
+
source?: ModuleFactSourceRef
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ModuleDiRegistrationFact = {
|
|
203
|
+
token: string
|
|
204
|
+
registrationKind: 'function' | 'class' | 'value' | 'alias'
|
|
205
|
+
providerSymbol?: string
|
|
206
|
+
lifetime?: 'singleton' | 'scoped' | 'transient'
|
|
207
|
+
injectionMode?: 'classic' | 'proxy'
|
|
208
|
+
source: ModuleFactSourceRef
|
|
209
|
+
}
|
|
210
|
+
|
|
38
211
|
export interface ModuleFacts {
|
|
39
212
|
module: string
|
|
40
213
|
title: string | null
|
|
41
214
|
description: string | null
|
|
42
215
|
coreVersion: string | null
|
|
216
|
+
sourcePackage: string | null
|
|
217
|
+
sourceVersion: string | null
|
|
218
|
+
sourceRoot: string
|
|
43
219
|
entities: ModuleEntityFact[]
|
|
44
220
|
events: ModuleEventFact[]
|
|
45
221
|
aclFeatures: string[]
|
|
@@ -49,6 +225,17 @@ export interface ModuleFacts {
|
|
|
49
225
|
hostTokens: ModuleHostTokens
|
|
50
226
|
notifications: string[]
|
|
51
227
|
cli: string[]
|
|
228
|
+
backendPages: ModulePageFact[]
|
|
229
|
+
frontendPages: ModulePageFact[]
|
|
230
|
+
cliCommands: ModuleCliCommandFact[]
|
|
231
|
+
aiTools: ModuleAiToolFact[]
|
|
232
|
+
aiAgents: ModuleAiAgentFact[]
|
|
233
|
+
extensionSurfaces?: ModuleExtensionSurfaceFacts
|
|
234
|
+
factSources?: ModuleFactSourceEntry[]
|
|
235
|
+
ownedContracts?: ModuleOwnedContracts
|
|
236
|
+
factDiagnostics?: ModuleFactDiagnostic[]
|
|
237
|
+
overrideTargets?: ModuleOverrideTarget[]
|
|
238
|
+
overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
|
|
52
239
|
warnings: string[]
|
|
53
240
|
}
|
|
54
241
|
|
|
@@ -64,6 +251,8 @@ export interface ExtractModuleFactsOptions {
|
|
|
64
251
|
/** Explicit module source directory. When set it overrides `coreSrcRoot + moduleId`. */
|
|
65
252
|
moduleRoot?: string
|
|
66
253
|
coreVersion?: string | null
|
|
254
|
+
sourcePackage?: string | null
|
|
255
|
+
sourceVersion?: string | null
|
|
67
256
|
registryPath?: string | null
|
|
68
257
|
registrySource?: string | null
|
|
69
258
|
}
|
|
@@ -73,6 +262,7 @@ export interface ModuleFactSource {
|
|
|
73
262
|
moduleId: string
|
|
74
263
|
moduleRoot: string
|
|
75
264
|
from?: string
|
|
265
|
+
packageVersion?: string | null
|
|
76
266
|
}
|
|
77
267
|
|
|
78
268
|
function readSourceFile(filePath: string): ts.SourceFile | null {
|
|
@@ -159,10 +349,41 @@ function collectCustomFieldEntityIds(ceFilePath: string | null): Set<string> {
|
|
|
159
349
|
const sourceFile = readSourceFile(ceFilePath)
|
|
160
350
|
if (!sourceFile) return result
|
|
161
351
|
|
|
352
|
+
const readEntityId = (expression: ts.Expression): string | undefined => {
|
|
353
|
+
const current = unwrapExpression(expression)
|
|
354
|
+
if (ts.isStringLiteralLike(current)) {
|
|
355
|
+
return current.text.includes(':') ? current.text : undefined
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const readAccessPath = (candidate: ts.Expression): string[] | null => {
|
|
359
|
+
const access = unwrapExpression(candidate)
|
|
360
|
+
if (ts.isIdentifier(access)) return [access.text]
|
|
361
|
+
if (ts.isPropertyAccessExpression(access)) {
|
|
362
|
+
const parent = readAccessPath(access.expression)
|
|
363
|
+
return parent ? [...parent, access.name.text] : null
|
|
364
|
+
}
|
|
365
|
+
if (ts.isElementAccessExpression(access) && access.argumentExpression) {
|
|
366
|
+
const parent = readAccessPath(access.expression)
|
|
367
|
+
const key = unwrapExpression(access.argumentExpression)
|
|
368
|
+
return parent && ts.isStringLiteralLike(key) ? [...parent, key.text] : null
|
|
369
|
+
}
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const accessPath = readAccessPath(current)
|
|
374
|
+
if (accessPath?.length === 3 && accessPath[0] === 'E') {
|
|
375
|
+
return `${accessPath[1]}:${accessPath[2]}`
|
|
376
|
+
}
|
|
377
|
+
return undefined
|
|
378
|
+
}
|
|
379
|
+
|
|
162
380
|
const visit = (node: ts.Node): void => {
|
|
163
381
|
if (ts.isObjectLiteralExpression(node)) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
382
|
+
const initializer = getObjectPropertyInitializer(node, 'id')
|
|
383
|
+
if (initializer) {
|
|
384
|
+
const id = readEntityId(initializer)
|
|
385
|
+
if (id) result.add(id)
|
|
386
|
+
}
|
|
166
387
|
}
|
|
167
388
|
node.forEachChild(visit)
|
|
168
389
|
}
|
|
@@ -351,7 +572,11 @@ function extractEvents(eventsFilePath: string | null): ModuleEventFact[] {
|
|
|
351
572
|
category: category ?? null,
|
|
352
573
|
entity: entity ?? null,
|
|
353
574
|
}
|
|
575
|
+
const clientBroadcast = readBooleanPropertyInitializer(element, 'clientBroadcast')
|
|
576
|
+
const portalBroadcast = readBooleanPropertyInitializer(element, 'portalBroadcast')
|
|
354
577
|
if (label !== undefined) fact.label = label
|
|
578
|
+
if (clientBroadcast !== undefined) fact.clientBroadcast = clientBroadcast
|
|
579
|
+
if (portalBroadcast !== undefined) fact.portalBroadcast = portalBroadcast
|
|
355
580
|
facts.push(fact)
|
|
356
581
|
}
|
|
357
582
|
|
|
@@ -459,13 +684,14 @@ function parseApiRouteEntry(entryLiteral: ts.ObjectLiteralExpression): ModuleApi
|
|
|
459
684
|
}
|
|
460
685
|
}
|
|
461
686
|
|
|
462
|
-
return { path: routePath, methods, auth }
|
|
687
|
+
return { path: routePath, methods, auth, sourcePath: null }
|
|
463
688
|
}
|
|
464
689
|
|
|
465
690
|
function extractApiRoutes(
|
|
466
691
|
moduleId: string,
|
|
467
692
|
registrySource: string | null,
|
|
468
693
|
registryDescription: string,
|
|
694
|
+
routeSourcePaths: ReadonlyMap<string, string>,
|
|
469
695
|
warnings: string[],
|
|
470
696
|
): ModuleApiRouteFact[] {
|
|
471
697
|
if (registrySource == null) {
|
|
@@ -493,7 +719,7 @@ function extractApiRoutes(
|
|
|
493
719
|
const route = parseApiRouteEntry(element)
|
|
494
720
|
if (route && !seenPaths.has(route.path)) {
|
|
495
721
|
seenPaths.add(route.path)
|
|
496
|
-
routes.push(route)
|
|
722
|
+
routes.push({ ...route, sourcePath: routeSourcePaths.get(route.path) ?? null })
|
|
497
723
|
}
|
|
498
724
|
}
|
|
499
725
|
}
|
|
@@ -666,36 +892,241 @@ function extractCli(cliFilePath: string | null, warnings: string[]): string[] {
|
|
|
666
892
|
function listSourceFilesRecursive(directory: string): string[] {
|
|
667
893
|
const files: string[] = []
|
|
668
894
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
669
|
-
if (entry.name === '__tests__' || entry.name === 'node_modules') continue
|
|
895
|
+
if (entry.name === '__tests__' || entry.name === '__mocks__' || entry.name === 'node_modules') continue
|
|
670
896
|
const fullPath = path.join(directory, entry.name)
|
|
671
897
|
if (entry.isDirectory()) {
|
|
672
898
|
files.push(...listSourceFilesRecursive(fullPath))
|
|
673
|
-
} else if (
|
|
899
|
+
} else if (
|
|
900
|
+
/\.tsx?$/.test(entry.name)
|
|
901
|
+
&& !entry.name.endsWith('.d.ts')
|
|
902
|
+
&& !/\.(?:test|spec)\.tsx?$/.test(entry.name)
|
|
903
|
+
) {
|
|
674
904
|
files.push(fullPath)
|
|
675
905
|
}
|
|
676
906
|
}
|
|
677
907
|
return files
|
|
678
908
|
}
|
|
679
909
|
|
|
680
|
-
function
|
|
681
|
-
|
|
910
|
+
function toPortableSourceRoot(moduleId: string, sourcePackage: string | null): string {
|
|
911
|
+
return path.posix.join('node_modules', sourcePackage ?? '@open-mercato/core', 'src', 'modules', moduleId)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function toPortableSourcePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
|
|
915
|
+
const relativePath = path.relative(moduleRoot, filePath).split(path.sep).join('/')
|
|
916
|
+
return path.posix.join(sourceRoot, relativePath)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function sourceHasDefaultExport(sourceFile: ts.SourceFile): boolean {
|
|
920
|
+
return sourceFile.statements.some((statement) => {
|
|
921
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) return true
|
|
922
|
+
if (!ts.isFunctionDeclaration(statement) && !ts.isClassDeclaration(statement)) return false
|
|
923
|
+
return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function readRouteMetadataPath(sourceFile: ts.SourceFile): string | null {
|
|
928
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
929
|
+
return metadata ? readStringPropertyInitializer(metadata, 'path') ?? null : null
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function extractApiRouteSourcePaths(
|
|
933
|
+
moduleId: string,
|
|
934
|
+
moduleRoot: string,
|
|
935
|
+
sourceRoot: string,
|
|
936
|
+
): Map<string, string> {
|
|
937
|
+
const apiRoot = path.join(moduleRoot, 'api')
|
|
938
|
+
const sources = new Map<string, string>()
|
|
939
|
+
if (!fs.existsSync(apiRoot)) return sources
|
|
940
|
+
const methodDirectories = new Set(['get', 'post', 'put', 'patch', 'delete'])
|
|
941
|
+
|
|
942
|
+
for (const filePath of listSourceFilesRecursive(apiRoot)) {
|
|
943
|
+
const relativePath = path.relative(apiRoot, filePath).split(path.sep).join('/')
|
|
944
|
+
const segments = relativePath.split('/')
|
|
945
|
+
const fileName = segments.pop() as string
|
|
946
|
+
const fileStem = fileName.replace(/\.tsx?$/, '')
|
|
947
|
+
let routeSegments: string[]
|
|
948
|
+
if (fileStem === 'route') {
|
|
949
|
+
routeSegments = segments
|
|
950
|
+
} else if (segments[0] && methodDirectories.has(segments[0].toLowerCase())) {
|
|
951
|
+
routeSegments = [...segments.slice(1), fileStem]
|
|
952
|
+
} else {
|
|
953
|
+
routeSegments = [...segments, fileStem]
|
|
954
|
+
}
|
|
955
|
+
const defaultPath = `/${[moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
956
|
+
const sourceFile = readSourceFile(filePath)
|
|
957
|
+
const routePath = sourceFile ? readRouteMetadataPath(sourceFile) ?? defaultPath : defaultPath
|
|
958
|
+
sources.set(routePath, toPortableSourcePath(moduleRoot, sourceRoot, filePath))
|
|
959
|
+
}
|
|
960
|
+
return sources
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function extractModulePages(
|
|
964
|
+
moduleId: string,
|
|
965
|
+
moduleRoot: string,
|
|
966
|
+
sourceRoot: string,
|
|
967
|
+
surface: 'backend' | 'frontend',
|
|
968
|
+
): ModulePageFact[] {
|
|
969
|
+
const pageRoot = path.join(moduleRoot, surface)
|
|
970
|
+
if (!fs.existsSync(pageRoot)) return []
|
|
971
|
+
const pages: ModulePageFact[] = []
|
|
972
|
+
const seen = new Set<string>()
|
|
973
|
+
|
|
974
|
+
for (const filePath of listSourceFilesRecursive(pageRoot)) {
|
|
975
|
+
if (!filePath.endsWith('.tsx')) continue
|
|
976
|
+
const relativePath = path.relative(pageRoot, filePath).split(path.sep).join('/')
|
|
977
|
+
const segments = relativePath.split('/')
|
|
978
|
+
const fileName = segments.pop() as string
|
|
979
|
+
const fileStem = fileName.replace(/\.tsx$/, '')
|
|
980
|
+
const isModernPage = fileStem === 'page'
|
|
981
|
+
if (!isModernPage && (fileStem === 'meta' || fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
|
|
982
|
+
const sourceFile = readSourceFile(filePath)
|
|
983
|
+
if (!sourceFile || !sourceHasDefaultExport(sourceFile)) continue
|
|
984
|
+
const routeSegments = isModernPage ? segments : [...segments, fileStem]
|
|
985
|
+
const routePath = surface === 'frontend'
|
|
986
|
+
? `/${routeSegments.filter(Boolean).join('/')}`
|
|
987
|
+
: isModernPage
|
|
988
|
+
? `/backend/${routeSegments.join('/') || moduleId}`
|
|
989
|
+
: `/backend/${routeSegments[0] === moduleId
|
|
990
|
+
? routeSegments.filter(Boolean).join('/')
|
|
991
|
+
: [moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
992
|
+
if (seen.has(routePath)) continue
|
|
993
|
+
seen.add(routePath)
|
|
994
|
+
const metadataFilePath = resolvePageMetadataFile(path.dirname(filePath))
|
|
995
|
+
const metadataSourceFile = metadataFilePath ? readSourceFile(metadataFilePath) : sourceFile
|
|
996
|
+
const metadata = metadataSourceFile ? readPageMetadata(metadataSourceFile) : undefined
|
|
997
|
+
pages.push({
|
|
998
|
+
path: routePath,
|
|
999
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
1000
|
+
...(metadataFilePath
|
|
1001
|
+
? { metadataSourcePath: toPortableSourcePath(moduleRoot, sourceRoot, metadataFilePath) }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(metadata ? { metadata } : {}),
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
return pages.sort((left, right) => left.path.localeCompare(right.path))
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function findAncestorVariableDeclaration(node: ts.Node): ts.VariableDeclaration | null {
|
|
1010
|
+
let current: ts.Node | undefined = node.parent
|
|
1011
|
+
while (current) {
|
|
1012
|
+
if (ts.isVariableDeclaration(current)) return current
|
|
1013
|
+
current = current.parent
|
|
1014
|
+
}
|
|
1015
|
+
return null
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function findAncestorCallExpression(node: ts.Node): ts.CallExpression | null {
|
|
1019
|
+
let current: ts.Node | undefined = node.parent
|
|
1020
|
+
while (current) {
|
|
1021
|
+
if (ts.isCallExpression(current)) return current
|
|
1022
|
+
if (ts.isVariableDeclaration(current) || ts.isStatement(current)) return null
|
|
1023
|
+
current = current.parent
|
|
1024
|
+
}
|
|
1025
|
+
return null
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function extractNamedAiFacts(
|
|
1029
|
+
filePaths: readonly string[],
|
|
1030
|
+
moduleRoot: string,
|
|
1031
|
+
sourceRoot: string,
|
|
1032
|
+
propertyName: 'name' | 'id',
|
|
1033
|
+
): Array<{ value: string; sourcePath: string }> {
|
|
1034
|
+
const facts: Array<{ value: string; sourcePath: string }> = []
|
|
1035
|
+
const seen = new Set<string>()
|
|
1036
|
+
for (const filePath of filePaths) {
|
|
1037
|
+
const sourceFile = readSourceFile(filePath)
|
|
1038
|
+
if (!sourceFile) continue
|
|
1039
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1040
|
+
const readDefinitionValue = (objectLiteral: ts.ObjectLiteralExpression): string | undefined => {
|
|
1041
|
+
const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
|
|
1042
|
+
if (!initializer) return undefined
|
|
1043
|
+
const resolved = unwrapExpression(initializer)
|
|
1044
|
+
if (ts.isStringLiteralLike(resolved)) return resolved.text
|
|
1045
|
+
if (ts.isIdentifier(resolved)) {
|
|
1046
|
+
const declarationInitializer = initializers.get(resolved.text)
|
|
1047
|
+
if (declarationInitializer) {
|
|
1048
|
+
const declarationValue = unwrapExpression(declarationInitializer)
|
|
1049
|
+
if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return undefined
|
|
1053
|
+
}
|
|
1054
|
+
const visit = (node: ts.Node): void => {
|
|
1055
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1056
|
+
const value = readDefinitionValue(node)
|
|
1057
|
+
if (value && !seen.has(value)) {
|
|
1058
|
+
const declaration = findAncestorVariableDeclaration(node)
|
|
1059
|
+
const declarationName = declaration && ts.isIdentifier(declaration.name) ? declaration.name.text : ''
|
|
1060
|
+
const declarationType = declaration?.type?.getText(sourceFile) ?? ''
|
|
1061
|
+
const call = findAncestorCallExpression(node)
|
|
1062
|
+
const callee = call?.expression.getText(sourceFile) ?? ''
|
|
1063
|
+
const isDefinition = propertyName === 'name'
|
|
1064
|
+
? /AiTool/.test(declarationType) || /tool$/i.test(declarationName) || /AiTool/.test(callee)
|
|
1065
|
+
: /AiAgentDefinition/.test(declarationType) || /agent$/i.test(declarationName) || /defineAiAgent/.test(callee)
|
|
1066
|
+
if (isDefinition) {
|
|
1067
|
+
seen.add(value)
|
|
1068
|
+
facts.push({
|
|
1069
|
+
value,
|
|
1070
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
1071
|
+
})
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
node.forEachChild(visit)
|
|
1076
|
+
}
|
|
1077
|
+
sourceFile.forEachChild(visit)
|
|
1078
|
+
}
|
|
1079
|
+
return facts.sort((left, right) => left.value.localeCompare(right.value))
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function extractAiTools(moduleRoot: string, sourceRoot: string): ModuleAiToolFact[] {
|
|
1083
|
+
const files = new Set<string>()
|
|
1084
|
+
const rootFile = resolveConventionFile(moduleRoot, 'ai-tools')
|
|
1085
|
+
if (rootFile) files.add(rootFile)
|
|
1086
|
+
const toolsDirectory = path.join(moduleRoot, 'ai-tools')
|
|
1087
|
+
if (fs.existsSync(toolsDirectory)) {
|
|
1088
|
+
for (const filePath of listSourceFilesRecursive(toolsDirectory)) files.add(filePath)
|
|
1089
|
+
}
|
|
1090
|
+
return extractNamedAiFacts([...files], moduleRoot, sourceRoot, 'name')
|
|
1091
|
+
.map((fact) => ({ name: fact.value, sourcePath: fact.sourcePath }))
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function extractAiAgents(moduleRoot: string, sourceRoot: string): ModuleAiAgentFact[] {
|
|
1095
|
+
const agentsFile = resolveConventionFile(moduleRoot, 'ai-agents')
|
|
1096
|
+
if (!agentsFile) return []
|
|
1097
|
+
return extractNamedAiFacts([agentsFile], moduleRoot, sourceRoot, 'id')
|
|
1098
|
+
.map((fact) => ({ id: fact.value, sourcePath: fact.sourcePath }))
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function extractTableIds(moduleRoot: string): string[] {
|
|
1102
|
+
if (!fs.existsSync(moduleRoot)) return []
|
|
682
1103
|
|
|
683
1104
|
const tableIds: string[] = []
|
|
684
1105
|
const seen = new Set<string>()
|
|
685
|
-
|
|
1106
|
+
const collectLiteralValues = (expression: ts.Expression): string[] => {
|
|
1107
|
+
const current = unwrapExpression(expression)
|
|
1108
|
+
if (ts.isStringLiteralLike(current)) return [current.text]
|
|
1109
|
+
if (ts.isConditionalExpression(current)) {
|
|
1110
|
+
return [
|
|
1111
|
+
...collectLiteralValues(current.whenTrue),
|
|
1112
|
+
...collectLiteralValues(current.whenFalse),
|
|
1113
|
+
]
|
|
1114
|
+
}
|
|
1115
|
+
return []
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
for (const filePath of listSourceFilesRecursive(moduleRoot)) {
|
|
686
1119
|
const sourceFile = readSourceFile(filePath)
|
|
687
1120
|
if (!sourceFile) continue
|
|
688
1121
|
const visit = (node: ts.Node): void => {
|
|
689
1122
|
if (ts.isPropertyAssignment(node)) {
|
|
690
1123
|
const propertyName = getPropertyName(node)
|
|
691
|
-
if (
|
|
692
|
-
(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
seen.add(value)
|
|
698
|
-
tableIds.push(value)
|
|
1124
|
+
if (propertyName === 'tableId' || propertyName === 'extensionTableId') {
|
|
1125
|
+
for (const value of collectLiteralValues(node.initializer)) {
|
|
1126
|
+
if (value && !seen.has(value)) {
|
|
1127
|
+
seen.add(value)
|
|
1128
|
+
tableIds.push(value)
|
|
1129
|
+
}
|
|
699
1130
|
}
|
|
700
1131
|
}
|
|
701
1132
|
}
|
|
@@ -723,13 +1154,750 @@ function extractModuleMeta(indexFilePath: string | null): { title: string | null
|
|
|
723
1154
|
}
|
|
724
1155
|
}
|
|
725
1156
|
|
|
1157
|
+
function nodeLine(sourceFile: ts.SourceFile, node: ts.Node): number {
|
|
1158
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function readPropertyNameText(name: ts.PropertyName): string | undefined {
|
|
1162
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text
|
|
1163
|
+
return undefined
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function readMethodBearingPropertyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
|
|
1167
|
+
const names: string[] = []
|
|
1168
|
+
for (const property of objectLiteral.properties) {
|
|
1169
|
+
if (ts.isMethodDeclaration(property)) {
|
|
1170
|
+
const name = readPropertyNameText(property.name)
|
|
1171
|
+
if (name) names.push(name)
|
|
1172
|
+
continue
|
|
1173
|
+
}
|
|
1174
|
+
if (ts.isPropertyAssignment(property)) {
|
|
1175
|
+
const name = readPropertyNameText(property.name)
|
|
1176
|
+
if (!name) continue
|
|
1177
|
+
const initializer = unwrapExpression(property.initializer)
|
|
1178
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) names.push(name)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return names.sort((left, right) => left.localeCompare(right))
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function readObjectKeyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
|
|
1185
|
+
const names: string[] = []
|
|
1186
|
+
for (const property of objectLiteral.properties) {
|
|
1187
|
+
const name = getPropertyName(property)
|
|
1188
|
+
if (name) names.push(name)
|
|
1189
|
+
}
|
|
1190
|
+
return names.sort((left, right) => left.localeCompare(right))
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function mapDiRegistrationKind(base: string | null): ModuleDiRegistrationFact['registrationKind'] | null {
|
|
1194
|
+
switch (base) {
|
|
1195
|
+
case 'asFunction':
|
|
1196
|
+
return 'function'
|
|
1197
|
+
case 'asClass':
|
|
1198
|
+
return 'class'
|
|
1199
|
+
case 'asValue':
|
|
1200
|
+
return 'value'
|
|
1201
|
+
case 'aliasTo':
|
|
1202
|
+
return 'alias'
|
|
1203
|
+
default:
|
|
1204
|
+
return null
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function readDiChainMetadata(initializer: ts.Expression): {
|
|
1209
|
+
lifetime?: ModuleDiRegistrationFact['lifetime']
|
|
1210
|
+
injectionMode?: ModuleDiRegistrationFact['injectionMode']
|
|
1211
|
+
} {
|
|
1212
|
+
const result: {
|
|
1213
|
+
lifetime?: ModuleDiRegistrationFact['lifetime']
|
|
1214
|
+
injectionMode?: ModuleDiRegistrationFact['injectionMode']
|
|
1215
|
+
} = {}
|
|
1216
|
+
let current: ts.Expression = unwrapExpression(initializer)
|
|
1217
|
+
while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
1218
|
+
const method = current.expression.name.text
|
|
1219
|
+
if ((method === 'singleton' || method === 'scoped' || method === 'transient') && !result.lifetime) {
|
|
1220
|
+
result.lifetime = method
|
|
1221
|
+
} else if (method === 'proxy' && !result.injectionMode) {
|
|
1222
|
+
result.injectionMode = 'proxy'
|
|
1223
|
+
} else if (method === 'classic' && !result.injectionMode) {
|
|
1224
|
+
result.injectionMode = 'classic'
|
|
1225
|
+
}
|
|
1226
|
+
current = unwrapExpression(current.expression.expression)
|
|
1227
|
+
}
|
|
1228
|
+
return result
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function readDiProviderSymbol(
|
|
1232
|
+
initializer: ts.Expression,
|
|
1233
|
+
kind: ModuleDiRegistrationFact['registrationKind'],
|
|
1234
|
+
): string | undefined {
|
|
1235
|
+
let current: ts.Expression = unwrapExpression(initializer)
|
|
1236
|
+
while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
1237
|
+
current = unwrapExpression(current.expression.expression)
|
|
1238
|
+
}
|
|
1239
|
+
if (!ts.isCallExpression(current)) return undefined
|
|
1240
|
+
const argument = current.arguments[0]
|
|
1241
|
+
if (!argument) return undefined
|
|
1242
|
+
if (kind === 'value') return undefined
|
|
1243
|
+
if (kind === 'alias' && ts.isStringLiteralLike(argument)) return argument.text
|
|
1244
|
+
if ((kind === 'function' || kind === 'class') && ts.isIdentifier(argument)) return argument.text
|
|
1245
|
+
return undefined
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function extractDiRegistrationFacts(
|
|
1249
|
+
diFilePath: string | null,
|
|
1250
|
+
sourcePath: string,
|
|
1251
|
+
): { facts: ModuleDiRegistrationFact[]; unresolvedTokens: string[] } {
|
|
1252
|
+
if (!diFilePath) return { facts: [], unresolvedTokens: [] }
|
|
1253
|
+
const sourceFile = readSourceFile(diFilePath)
|
|
1254
|
+
if (!sourceFile) return { facts: [], unresolvedTokens: [] }
|
|
1255
|
+
|
|
1256
|
+
const facts: ModuleDiRegistrationFact[] = []
|
|
1257
|
+
const unresolvedTokens: string[] = []
|
|
1258
|
+
const visit = (node: ts.Node): void => {
|
|
1259
|
+
if (
|
|
1260
|
+
ts.isCallExpression(node) &&
|
|
1261
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
1262
|
+
node.expression.name.text === 'register'
|
|
1263
|
+
) {
|
|
1264
|
+
const argument = node.arguments[0]
|
|
1265
|
+
if (argument && ts.isObjectLiteralExpression(argument)) {
|
|
1266
|
+
for (const property of argument.properties) {
|
|
1267
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
1268
|
+
const token = getPropertyName(property)
|
|
1269
|
+
if (!token) continue
|
|
1270
|
+
const kind = mapDiRegistrationKind(detectAwilixRegistrationKind(property.initializer))
|
|
1271
|
+
if (!kind) {
|
|
1272
|
+
if (!unresolvedTokens.includes(token)) unresolvedTokens.push(token)
|
|
1273
|
+
continue
|
|
1274
|
+
}
|
|
1275
|
+
const chain = readDiChainMetadata(property.initializer)
|
|
1276
|
+
const providerSymbol = readDiProviderSymbol(property.initializer, kind)
|
|
1277
|
+
facts.push({
|
|
1278
|
+
token,
|
|
1279
|
+
registrationKind: kind,
|
|
1280
|
+
...(providerSymbol ? { providerSymbol } : {}),
|
|
1281
|
+
...(chain.lifetime ? { lifetime: chain.lifetime } : {}),
|
|
1282
|
+
...(chain.injectionMode ? { injectionMode: chain.injectionMode } : {}),
|
|
1283
|
+
source: { sourcePath, exportName: token, line: nodeLine(sourceFile, property) },
|
|
1284
|
+
})
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
node.forEachChild(visit)
|
|
1289
|
+
}
|
|
1290
|
+
sourceFile.forEachChild(visit)
|
|
1291
|
+
return { facts, unresolvedTokens }
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function deriveLegacyDiTokens(facts: readonly ModuleDiRegistrationFact[]): string[] {
|
|
1295
|
+
const tokens: string[] = []
|
|
1296
|
+
const seen = new Set<string>()
|
|
1297
|
+
for (const fact of facts) {
|
|
1298
|
+
if (fact.registrationKind !== 'function' && fact.registrationKind !== 'class') continue
|
|
1299
|
+
if (seen.has(fact.token)) continue
|
|
1300
|
+
seen.add(fact.token)
|
|
1301
|
+
tokens.push(fact.token)
|
|
1302
|
+
}
|
|
1303
|
+
return tokens
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function dedupeOwnedContractFacts(facts: readonly ModuleOwnedContractFact[]): ModuleOwnedContractFact[] {
|
|
1307
|
+
const byId = new Map<string, ModuleOwnedContractFact>()
|
|
1308
|
+
for (const fact of facts) {
|
|
1309
|
+
const existing = byId.get(fact.id)
|
|
1310
|
+
if (!existing) {
|
|
1311
|
+
byId.set(fact.id, fact)
|
|
1312
|
+
continue
|
|
1313
|
+
}
|
|
1314
|
+
if (fact.source.sourcePath.localeCompare(existing.source.sourcePath) < 0) byId.set(fact.id, fact)
|
|
1315
|
+
}
|
|
1316
|
+
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id))
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const PAGE_CONTEXT_VALUES = new Set(['main', 'admin', 'settings', 'profile'])
|
|
1320
|
+
|
|
1321
|
+
function readAliasedStringProperty(
|
|
1322
|
+
metadata: ts.ObjectLiteralExpression,
|
|
1323
|
+
preferred: string,
|
|
1324
|
+
fallback: string,
|
|
1325
|
+
): string | undefined {
|
|
1326
|
+
return readStringPropertyInitializer(metadata, preferred) ?? readStringPropertyInitializer(metadata, fallback)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function readNumericProperty(metadata: ts.ObjectLiteralExpression, propertyName: string): number | undefined {
|
|
1330
|
+
const initializer = getObjectPropertyInitializer(metadata, propertyName)
|
|
1331
|
+
if (!initializer || !ts.isNumericLiteral(initializer)) return undefined
|
|
1332
|
+
return Number(initializer.text)
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function readPageMetadata(sourceFile: ts.SourceFile): ModulePageMetadata | undefined {
|
|
1336
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1337
|
+
if (!metadata) return undefined
|
|
1338
|
+
const result: ModulePageMetadata = {}
|
|
1339
|
+
const requireAuth = readBooleanPropertyInitializer(metadata, 'requireAuth')
|
|
1340
|
+
if (requireAuth !== undefined) result.requireAuth = requireAuth
|
|
1341
|
+
const requireCustomerAuth = readBooleanPropertyInitializer(metadata, 'requireCustomerAuth')
|
|
1342
|
+
if (requireCustomerAuth !== undefined) result.requireCustomerAuth = requireCustomerAuth
|
|
1343
|
+
const requireFeatures = readStringArrayPropertyInitializer(metadata, 'requireFeatures')
|
|
1344
|
+
if (requireFeatures && requireFeatures.length > 0) result.requireFeatures = requireFeatures
|
|
1345
|
+
const requireCustomerFeatures = readStringArrayPropertyInitializer(metadata, 'requireCustomerFeatures')
|
|
1346
|
+
if (requireCustomerFeatures && requireCustomerFeatures.length > 0) {
|
|
1347
|
+
result.requireCustomerFeatures = requireCustomerFeatures
|
|
1348
|
+
}
|
|
1349
|
+
const navHidden = readBooleanPropertyInitializer(metadata, 'navHidden')
|
|
1350
|
+
if (navHidden !== undefined) result.navHidden = navHidden
|
|
1351
|
+
const navGroup = readAliasedStringProperty(metadata, 'pageGroup', 'group')
|
|
1352
|
+
if (navGroup) result.navGroup = navGroup
|
|
1353
|
+
const navGroupKey = readAliasedStringProperty(metadata, 'pageGroupKey', 'groupKey')
|
|
1354
|
+
if (navGroupKey) result.navGroupKey = navGroupKey
|
|
1355
|
+
const navOrder = readNumericProperty(metadata, 'pageOrder') ?? readNumericProperty(metadata, 'order')
|
|
1356
|
+
if (navOrder !== undefined) result.navOrder = navOrder
|
|
1357
|
+
const pageContext = readStringPropertyInitializer(metadata, 'pageContext')
|
|
1358
|
+
if (pageContext && PAGE_CONTEXT_VALUES.has(pageContext)) {
|
|
1359
|
+
result.pageContext = pageContext as ModulePageMetadata['pageContext']
|
|
1360
|
+
}
|
|
1361
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Mirrors the runtime companion-file convention: a page's metadata comes from
|
|
1366
|
+
* `page.meta.*` (or `meta.*`) in the page directory when one exists, and only
|
|
1367
|
+
* otherwise from the page component itself.
|
|
1368
|
+
*/
|
|
1369
|
+
function resolvePageMetadataFile(pageDirectory: string): string | null {
|
|
1370
|
+
for (const baseName of ['page.meta', 'meta']) {
|
|
1371
|
+
for (const extension of MODULE_CODE_EXTENSIONS) {
|
|
1372
|
+
const candidate = path.join(pageDirectory, `${baseName}${extension}`)
|
|
1373
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return null
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function extractModuleMetadataContract(
|
|
1380
|
+
indexFilePath: string | null,
|
|
1381
|
+
moduleId: string,
|
|
1382
|
+
sourcePath: string | null,
|
|
1383
|
+
): ModuleOwnedContractFact | null {
|
|
1384
|
+
if (!indexFilePath || !sourcePath) return null
|
|
1385
|
+
const sourceFile = readSourceFile(indexFilePath)
|
|
1386
|
+
if (!sourceFile) return null
|
|
1387
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1388
|
+
if (!metadata) return null
|
|
1389
|
+
const facts: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1390
|
+
const name = readStringPropertyInitializer(metadata, 'name')
|
|
1391
|
+
if (name) facts.name = name
|
|
1392
|
+
const version = readStringPropertyInitializer(metadata, 'version')
|
|
1393
|
+
if (version) facts.version = version
|
|
1394
|
+
const requires = readStringArrayPropertyInitializer(metadata, 'requires')
|
|
1395
|
+
if (requires && requires.length > 0) facts.requires = requires
|
|
1396
|
+
const ejectable = readBooleanPropertyInitializer(metadata, 'ejectable')
|
|
1397
|
+
if (ejectable !== undefined) facts.ejectable = ejectable
|
|
1398
|
+
return {
|
|
1399
|
+
kind: 'module-metadata',
|
|
1400
|
+
id: moduleId,
|
|
1401
|
+
source: { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) },
|
|
1402
|
+
...(Object.keys(facts).length > 0 ? { metadata: facts } : {}),
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function extractCommandContractCandidates(
|
|
1407
|
+
moduleId: string,
|
|
1408
|
+
moduleRoot: string,
|
|
1409
|
+
sourceRoot: string,
|
|
1410
|
+
): Array<{ id: string; sourcePath: string }> {
|
|
1411
|
+
const commandsRoot = path.join(moduleRoot, 'commands')
|
|
1412
|
+
if (!fs.existsSync(commandsRoot)) return []
|
|
1413
|
+
const candidates: Array<{ id: string; sourcePath: string }> = []
|
|
1414
|
+
for (const filePath of listSourceFilesRecursive(commandsRoot)) {
|
|
1415
|
+
if (path.basename(filePath).replace(/\.tsx?$/, '') === 'interceptors') continue
|
|
1416
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1417
|
+
for (const id of extractCommandIdsFromSource(filePath)) candidates.push({ id, sourcePath })
|
|
1418
|
+
}
|
|
1419
|
+
return candidates
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Runtime worker id: `metadata.id` when declared, otherwise the path-derived
|
|
1424
|
+
* `<moduleId>:workers:<...segments>:<fileStem>` that `discoverWorkers` builds.
|
|
1425
|
+
*/
|
|
1426
|
+
function deriveWorkerId(moduleId: string, workersRoot: string, filePath: string): string {
|
|
1427
|
+
const relativePath = path.relative(workersRoot, filePath).split(path.sep).join('/')
|
|
1428
|
+
const segments = relativePath.split('/')
|
|
1429
|
+
const fileName = segments.pop() as string
|
|
1430
|
+
const fileStem = fileName.replace(/\.tsx?$/, '')
|
|
1431
|
+
return [moduleId, 'workers', ...segments, fileStem].filter(Boolean).join(':')
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/** Resolves a string property directly or through a local `const` in the same file. */
|
|
1435
|
+
function readResolvedStringProperty(
|
|
1436
|
+
objectLiteral: ts.ObjectLiteralExpression,
|
|
1437
|
+
propertyName: string,
|
|
1438
|
+
initializers: Map<string, ts.Expression>,
|
|
1439
|
+
): string | undefined {
|
|
1440
|
+
const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
|
|
1441
|
+
if (!initializer) return undefined
|
|
1442
|
+
const resolved = unwrapExpression(initializer)
|
|
1443
|
+
if (ts.isStringLiteralLike(resolved)) return resolved.text
|
|
1444
|
+
if (ts.isIdentifier(resolved)) {
|
|
1445
|
+
const declaration = initializers.get(resolved.text)
|
|
1446
|
+
if (declaration) {
|
|
1447
|
+
const declarationValue = unwrapExpression(declaration)
|
|
1448
|
+
if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return undefined
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function extractWorkerContracts(
|
|
1455
|
+
moduleId: string,
|
|
1456
|
+
moduleRoot: string,
|
|
1457
|
+
sourceRoot: string,
|
|
1458
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1459
|
+
const workersRoot = path.join(moduleRoot, 'workers')
|
|
1460
|
+
if (!fs.existsSync(workersRoot)) return { facts: [], unresolved: [] }
|
|
1461
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1462
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1463
|
+
for (const filePath of listSourceFilesRecursive(workersRoot)) {
|
|
1464
|
+
const sourceFile = readSourceFile(filePath)
|
|
1465
|
+
if (!sourceFile) continue
|
|
1466
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1467
|
+
if (!metadata) continue
|
|
1468
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1469
|
+
const source = { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) }
|
|
1470
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1471
|
+
const workerId = readResolvedStringProperty(metadata, 'id', initializers)
|
|
1472
|
+
?? deriveWorkerId(moduleId, workersRoot, filePath)
|
|
1473
|
+
|
|
1474
|
+
// Runtime skips any worker without a queue, so a worker missing the required
|
|
1475
|
+
// property is not a contract; a present-but-dynamic queue is reported instead.
|
|
1476
|
+
const hasQueueProperty = getObjectPropertyInitializer(metadata, 'queue') !== undefined
|
|
1477
|
+
const queue = readResolvedStringProperty(metadata, 'queue', initializers)
|
|
1478
|
+
if (!hasQueueProperty) {
|
|
1479
|
+
unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1480
|
+
continue
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const workerMetadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1484
|
+
if (queue) workerMetadata.queue = queue
|
|
1485
|
+
else unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1486
|
+
const name = readResolvedStringProperty(metadata, 'name', initializers)
|
|
1487
|
+
if (name) workerMetadata.name = name
|
|
1488
|
+
const concurrencyInitializer = getObjectPropertyInitializer(metadata, 'concurrency')
|
|
1489
|
+
if (concurrencyInitializer && ts.isNumericLiteral(concurrencyInitializer)) {
|
|
1490
|
+
workerMetadata.concurrency = Number(concurrencyInitializer.text)
|
|
1491
|
+
}
|
|
1492
|
+
facts.push({
|
|
1493
|
+
kind: 'worker',
|
|
1494
|
+
id: workerId,
|
|
1495
|
+
source,
|
|
1496
|
+
...(Object.keys(workerMetadata).length > 0 ? { metadata: workerMetadata } : {}),
|
|
1497
|
+
})
|
|
1498
|
+
}
|
|
1499
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function extractPageMiddlewareContracts(
|
|
1503
|
+
moduleRoot: string,
|
|
1504
|
+
sourceRoot: string,
|
|
1505
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1506
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1507
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1508
|
+
for (const surface of ['backend', 'frontend'] as const) {
|
|
1509
|
+
const filePath = resolveConventionFile(path.join(moduleRoot, surface), 'middleware')
|
|
1510
|
+
if (!filePath) continue
|
|
1511
|
+
const sourceFile = readSourceFile(filePath)
|
|
1512
|
+
if (!sourceFile) continue
|
|
1513
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1514
|
+
const middlewareArray =
|
|
1515
|
+
findArrayLiteralDeclaration(sourceFile, 'middleware') ??
|
|
1516
|
+
(() => {
|
|
1517
|
+
const defaultExport = findDefaultExportExpression(sourceFile)
|
|
1518
|
+
return defaultExport ? unwrapArrayLiteral(defaultExport) : null
|
|
1519
|
+
})()
|
|
1520
|
+
if (!middlewareArray) continue
|
|
1521
|
+
let index = 0
|
|
1522
|
+
for (const element of middlewareArray.elements) {
|
|
1523
|
+
if (!ts.isObjectLiteralExpression(element)) continue
|
|
1524
|
+
const middlewareId = readStringPropertyInitializer(element, 'id')
|
|
1525
|
+
if (!middlewareId) {
|
|
1526
|
+
unresolved.push({
|
|
1527
|
+
code: 'unresolved-static-contract',
|
|
1528
|
+
kind: 'page-middleware',
|
|
1529
|
+
id: `${sourcePath}#${index}`,
|
|
1530
|
+
source: { sourcePath, line: nodeLine(sourceFile, element) },
|
|
1531
|
+
})
|
|
1532
|
+
index += 1
|
|
1533
|
+
continue
|
|
1534
|
+
}
|
|
1535
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = { surface }
|
|
1536
|
+
const mode = readStringPropertyInitializer(element, 'mode')
|
|
1537
|
+
if (mode) metadata.mode = mode
|
|
1538
|
+
const priorityInitializer = getObjectPropertyInitializer(element, 'priority')
|
|
1539
|
+
if (priorityInitializer && ts.isNumericLiteral(priorityInitializer)) {
|
|
1540
|
+
metadata.priority = Number(priorityInitializer.text)
|
|
1541
|
+
}
|
|
1542
|
+
facts.push({
|
|
1543
|
+
kind: 'page-middleware',
|
|
1544
|
+
id: middlewareId,
|
|
1545
|
+
source: { sourcePath, exportName: 'middleware', line: nodeLine(sourceFile, element) },
|
|
1546
|
+
metadata,
|
|
1547
|
+
})
|
|
1548
|
+
index += 1
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function extractSetupContract(
|
|
1555
|
+
moduleId: string,
|
|
1556
|
+
setupFilePath: string | null,
|
|
1557
|
+
sourcePath: string | null,
|
|
1558
|
+
): ModuleOwnedContractFact | null {
|
|
1559
|
+
if (!setupFilePath || !sourcePath) return null
|
|
1560
|
+
const sourceFile = readSourceFile(setupFilePath)
|
|
1561
|
+
if (!sourceFile) return null
|
|
1562
|
+
const setupObject =
|
|
1563
|
+
findObjectLiteralDeclaration(sourceFile, 'setup') ??
|
|
1564
|
+
(() => {
|
|
1565
|
+
const defaultExport = findDefaultExportExpression(sourceFile)
|
|
1566
|
+
if (!defaultExport) return null
|
|
1567
|
+
const unwrapped = unwrapExpression(defaultExport)
|
|
1568
|
+
return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : null
|
|
1569
|
+
})()
|
|
1570
|
+
if (!setupObject) return null
|
|
1571
|
+
const hooks = readMethodBearingPropertyNames(setupObject)
|
|
1572
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1573
|
+
if (hooks.length > 0) metadata.hooks = hooks
|
|
1574
|
+
const roleFeatures = getObjectPropertyInitializer(setupObject, 'defaultRoleFeatures')
|
|
1575
|
+
if (roleFeatures && ts.isObjectLiteralExpression(roleFeatures)) {
|
|
1576
|
+
const roles = readObjectKeyNames(roleFeatures)
|
|
1577
|
+
if (roles.length > 0) metadata.roles = roles
|
|
1578
|
+
}
|
|
1579
|
+
const customerRoleFeatures = getObjectPropertyInitializer(setupObject, 'defaultCustomerRoleFeatures')
|
|
1580
|
+
if (customerRoleFeatures && ts.isObjectLiteralExpression(customerRoleFeatures)) {
|
|
1581
|
+
const customerRoles = readObjectKeyNames(customerRoleFeatures)
|
|
1582
|
+
if (customerRoles.length > 0) metadata.customerRoles = customerRoles
|
|
1583
|
+
}
|
|
1584
|
+
return {
|
|
1585
|
+
kind: 'setup',
|
|
1586
|
+
id: `${moduleId}:setup`,
|
|
1587
|
+
source: { sourcePath, exportName: 'setup', line: nodeLine(sourceFile, setupObject) },
|
|
1588
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function extractEncryptionContracts(
|
|
1593
|
+
encryptionFilePath: string | null,
|
|
1594
|
+
sourcePath: string | null,
|
|
1595
|
+
): ModuleOwnedContractFact[] {
|
|
1596
|
+
if (!encryptionFilePath || !sourcePath) return []
|
|
1597
|
+
const sourceFile = readSourceFile(encryptionFilePath)
|
|
1598
|
+
if (!sourceFile) return []
|
|
1599
|
+
const mapsArray =
|
|
1600
|
+
findArrayLiteralDeclaration(sourceFile, 'defaultEncryptionMaps') ??
|
|
1601
|
+
findArrayLiteralDeclaration(sourceFile, 'encryptionMaps')
|
|
1602
|
+
if (!mapsArray) return []
|
|
1603
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1604
|
+
for (const element of mapsArray.elements) {
|
|
1605
|
+
if (!ts.isObjectLiteralExpression(element)) continue
|
|
1606
|
+
const entityId = readStringPropertyInitializer(element, 'entityId')
|
|
1607
|
+
if (!entityId) continue
|
|
1608
|
+
const fieldsInitializer = getObjectPropertyInitializer(element, 'fields')
|
|
1609
|
+
const fields: string[] = []
|
|
1610
|
+
if (fieldsInitializer) {
|
|
1611
|
+
const fieldsArray = unwrapArrayLiteral(fieldsInitializer)
|
|
1612
|
+
if (fieldsArray) {
|
|
1613
|
+
for (const fieldElement of fieldsArray.elements) {
|
|
1614
|
+
if (!ts.isObjectLiteralExpression(fieldElement)) continue
|
|
1615
|
+
const field = readStringPropertyInitializer(fieldElement, 'field')
|
|
1616
|
+
if (field) fields.push(field)
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
facts.push({
|
|
1621
|
+
kind: 'encryption',
|
|
1622
|
+
id: entityId,
|
|
1623
|
+
source: { sourcePath, exportName: 'defaultEncryptionMaps', line: nodeLine(sourceFile, element) },
|
|
1624
|
+
...(fields.length > 0 ? { metadata: { fields: fields.sort((left, right) => left.localeCompare(right)) } } : {}),
|
|
1625
|
+
})
|
|
1626
|
+
}
|
|
1627
|
+
return dedupeOwnedContractFacts(facts)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function extractCustomEntityContracts(
|
|
1631
|
+
ceFilePath: string | null,
|
|
1632
|
+
sourcePath: string | null,
|
|
1633
|
+
): ModuleOwnedContractFact[] {
|
|
1634
|
+
if (!ceFilePath || !sourcePath) return []
|
|
1635
|
+
const entityIds = [...collectCustomFieldEntityIds(ceFilePath)].sort((left, right) => left.localeCompare(right))
|
|
1636
|
+
return entityIds.map((id) => ({
|
|
1637
|
+
kind: 'custom-entity' as const,
|
|
1638
|
+
id,
|
|
1639
|
+
source: { sourcePath, exportName: 'default' },
|
|
1640
|
+
}))
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Keyed AI override exports. Agent *extensions* (`aiAgentExtensions`) patch an agent
|
|
1645
|
+
* additively and belong to the `ai.extensions` array; these two replace or disable an
|
|
1646
|
+
* entry by key and map to exact `ai.agents.<id>` / `ai.tools.<id>` override targets.
|
|
1647
|
+
*/
|
|
1648
|
+
const AI_OVERRIDE_EXPORTS = [
|
|
1649
|
+
{ exportName: 'aiAgentOverrides', target: 'agent' },
|
|
1650
|
+
{ exportName: 'aiToolOverrides', target: 'tool' },
|
|
1651
|
+
] as const
|
|
1652
|
+
|
|
1653
|
+
function extractAiExtensionContracts(
|
|
1654
|
+
moduleRoot: string,
|
|
1655
|
+
sourceRoot: string,
|
|
1656
|
+
): ModuleOwnedContractFact[] {
|
|
1657
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1658
|
+
const sources: Array<{ basename: string; relativeDir: string }> = [
|
|
1659
|
+
{ basename: 'ai-agents', relativeDir: '.' },
|
|
1660
|
+
{ basename: 'ai-tools', relativeDir: '.' },
|
|
1661
|
+
{ basename: 'ai-agents.extensions', relativeDir: '.' },
|
|
1662
|
+
{ basename: 'ai-tools.extensions', relativeDir: '.' },
|
|
1663
|
+
]
|
|
1664
|
+
for (const entry of sources) {
|
|
1665
|
+
const filePath = resolveConventionFile(path.join(moduleRoot, entry.relativeDir), entry.basename)
|
|
1666
|
+
if (!filePath) continue
|
|
1667
|
+
const sourceFile = readSourceFile(filePath)
|
|
1668
|
+
if (!sourceFile) continue
|
|
1669
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1670
|
+
const extensionsArray = findArrayLiteralDeclaration(sourceFile, 'aiAgentExtensions')
|
|
1671
|
+
if (extensionsArray) {
|
|
1672
|
+
for (const element of extensionsArray.elements) {
|
|
1673
|
+
const objectLiteral = ts.isObjectLiteralExpression(element)
|
|
1674
|
+
? element
|
|
1675
|
+
: ts.isCallExpression(element) && element.arguments[0] && ts.isObjectLiteralExpression(element.arguments[0])
|
|
1676
|
+
? element.arguments[0]
|
|
1677
|
+
: null
|
|
1678
|
+
if (!objectLiteral) continue
|
|
1679
|
+
const targetAgentId = readStringPropertyInitializer(objectLiteral, 'targetAgentId')
|
|
1680
|
+
if (!targetAgentId) continue
|
|
1681
|
+
facts.push({
|
|
1682
|
+
kind: 'ai-extension',
|
|
1683
|
+
id: `agent-extension:${targetAgentId}`,
|
|
1684
|
+
source: { sourcePath, exportName: 'aiAgentExtensions', line: nodeLine(sourceFile, objectLiteral) },
|
|
1685
|
+
metadata: { target: 'agent', mode: 'extension', targetAgentId },
|
|
1686
|
+
})
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
// `aiAgentOverrides` / `aiToolOverrides` are keyed replace-or-disable maps; only
|
|
1690
|
+
// their keys are contract facts. Values (agent/tool definitions, `null` disables)
|
|
1691
|
+
// are never read or serialized.
|
|
1692
|
+
for (const { exportName, target } of AI_OVERRIDE_EXPORTS) {
|
|
1693
|
+
const overridesObject = findObjectLiteralDeclaration(sourceFile, exportName)
|
|
1694
|
+
if (!overridesObject) continue
|
|
1695
|
+
for (const property of overridesObject.properties) {
|
|
1696
|
+
const overrideId = getPropertyName(property)
|
|
1697
|
+
if (!overrideId) continue
|
|
1698
|
+
facts.push({
|
|
1699
|
+
kind: 'ai-extension',
|
|
1700
|
+
id: `${target}-override:${overrideId}`,
|
|
1701
|
+
source: { sourcePath, exportName, line: nodeLine(sourceFile, property) },
|
|
1702
|
+
metadata: { target, mode: 'override', overrideKey: overrideId },
|
|
1703
|
+
})
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return dedupeOwnedContractFacts(facts)
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function extractGeneratorPluginContracts(
|
|
1711
|
+
moduleRoot: string,
|
|
1712
|
+
sourceRoot: string,
|
|
1713
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1714
|
+
const filePath = resolveConventionFile(moduleRoot, 'generators')
|
|
1715
|
+
if (!filePath) return { facts: [], unresolved: [] }
|
|
1716
|
+
const sourceFile = readSourceFile(filePath)
|
|
1717
|
+
if (!sourceFile) return { facts: [], unresolved: [] }
|
|
1718
|
+
const pluginsArray = findArrayLiteralDeclaration(sourceFile, 'generatorPlugins')
|
|
1719
|
+
if (!pluginsArray) return { facts: [], unresolved: [] }
|
|
1720
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1721
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1722
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1723
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1724
|
+
let index = 0
|
|
1725
|
+
for (const element of pluginsArray.elements) {
|
|
1726
|
+
const objectLiteral = resolveToObjectLiteral(element, initializers)
|
|
1727
|
+
if (!objectLiteral) {
|
|
1728
|
+
index += 1
|
|
1729
|
+
continue
|
|
1730
|
+
}
|
|
1731
|
+
const pluginId = readStringPropertyInitializer(objectLiteral, 'id')
|
|
1732
|
+
if (!pluginId) {
|
|
1733
|
+
unresolved.push({
|
|
1734
|
+
code: 'unresolved-static-contract',
|
|
1735
|
+
kind: 'generator-plugin',
|
|
1736
|
+
id: `${sourcePath}#${index}`,
|
|
1737
|
+
source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
|
|
1738
|
+
})
|
|
1739
|
+
index += 1
|
|
1740
|
+
continue
|
|
1741
|
+
}
|
|
1742
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1743
|
+
const conventionFile = readStringPropertyInitializer(objectLiteral, 'conventionFile')
|
|
1744
|
+
if (conventionFile) metadata.conventionFile = conventionFile
|
|
1745
|
+
const outputFileName = readStringPropertyInitializer(objectLiteral, 'outputFileName')
|
|
1746
|
+
if (outputFileName) metadata.outputFileName = outputFileName
|
|
1747
|
+
facts.push({
|
|
1748
|
+
kind: 'generator-plugin',
|
|
1749
|
+
id: pluginId,
|
|
1750
|
+
source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
|
|
1751
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
1752
|
+
})
|
|
1753
|
+
index += 1
|
|
1754
|
+
}
|
|
1755
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function compareSourceRefs(left: ModuleFactSourceRef, right: ModuleFactSourceRef): number {
|
|
1759
|
+
return (
|
|
1760
|
+
left.sourcePath.localeCompare(right.sourcePath) ||
|
|
1761
|
+
(left.line ?? 0) - (right.line ?? 0) ||
|
|
1762
|
+
(left.exportName ?? '').localeCompare(right.exportName ?? '')
|
|
1763
|
+
)
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function sameSourceRef(left: ModuleFactSourceRef, right: ModuleFactSourceRef): boolean {
|
|
1767
|
+
return (
|
|
1768
|
+
left.sourcePath === right.sourcePath &&
|
|
1769
|
+
(left.line ?? null) === (right.line ?? null) &&
|
|
1770
|
+
(left.exportName ?? null) === (right.exportName ?? null)
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Every proven `(kind, id)` reaches `factSources`. Facts whose declaration site is already
|
|
1775
|
+
// serialized inline (pages, CLI commands, routes, AI tools/agents, reused contributions) and
|
|
1776
|
+
// the owned families (which carry `source` inside `ownedContracts`) are emitted as a typed
|
|
1777
|
+
// `factRef` pointer instead of a duplicated source ref, so the index stays uniform without
|
|
1778
|
+
// copying provenance payloads past the determinism byte guard. Duplicate detection runs over
|
|
1779
|
+
// ALL candidates regardless of which projection they take.
|
|
1780
|
+
type ProvenanceCandidate = {
|
|
1781
|
+
kind: ModuleFactSourceKind
|
|
1782
|
+
id: string
|
|
1783
|
+
source?: ModuleFactSourceRef
|
|
1784
|
+
factRef?: ModuleFactRef
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function provenanceCandidateOrderKey(candidate: ProvenanceCandidate): string {
|
|
1788
|
+
if (candidate.source) {
|
|
1789
|
+
return JSON.stringify([
|
|
1790
|
+
0,
|
|
1791
|
+
candidate.source.sourcePath,
|
|
1792
|
+
candidate.source.line ?? 0,
|
|
1793
|
+
candidate.source.exportName ?? '',
|
|
1794
|
+
])
|
|
1795
|
+
}
|
|
1796
|
+
return JSON.stringify([1, candidate.factRef?.factSection ?? '', candidate.factRef?.factKey ?? ''])
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function buildProvenanceIndex(candidates: readonly ProvenanceCandidate[]): {
|
|
1800
|
+
factSources: ModuleFactSourceEntry[]
|
|
1801
|
+
diagnostics: ModuleFactDiagnostic[]
|
|
1802
|
+
} {
|
|
1803
|
+
const grouped = new Map<string, ProvenanceCandidate[]>()
|
|
1804
|
+
for (const candidate of candidates) {
|
|
1805
|
+
const key = JSON.stringify([candidate.kind, candidate.id])
|
|
1806
|
+
const group = grouped.get(key) ?? []
|
|
1807
|
+
group.push(candidate)
|
|
1808
|
+
grouped.set(key, group)
|
|
1809
|
+
}
|
|
1810
|
+
// Keep the provenance index compact: sourcePath is the provenance and (kind,id) the
|
|
1811
|
+
// stable identity, so exportName and line (recoverable from source) are dropped here.
|
|
1812
|
+
// Owned families retain the richer source ref inside `ownedContracts`.
|
|
1813
|
+
const factSources: ModuleFactSourceEntry[] = []
|
|
1814
|
+
const diagnostics: ModuleFactDiagnostic[] = []
|
|
1815
|
+
for (const group of grouped.values()) {
|
|
1816
|
+
const sorted = [...group].sort(
|
|
1817
|
+
(left, right) => provenanceCandidateOrderKey(left).localeCompare(provenanceCandidateOrderKey(right)),
|
|
1818
|
+
)
|
|
1819
|
+
const canonical = sorted[0]
|
|
1820
|
+
factSources.push(
|
|
1821
|
+
canonical.factRef
|
|
1822
|
+
? {
|
|
1823
|
+
kind: canonical.kind,
|
|
1824
|
+
id: canonical.id,
|
|
1825
|
+
factRef: {
|
|
1826
|
+
factSection: canonical.factRef.factSection,
|
|
1827
|
+
...(canonical.factRef.factKey === canonical.id
|
|
1828
|
+
? {}
|
|
1829
|
+
: { factKey: canonical.factRef.factKey }),
|
|
1830
|
+
},
|
|
1831
|
+
}
|
|
1832
|
+
: {
|
|
1833
|
+
kind: canonical.kind,
|
|
1834
|
+
id: canonical.id,
|
|
1835
|
+
source: { sourcePath: (canonical.source as ModuleFactSourceRef).sourcePath },
|
|
1836
|
+
},
|
|
1837
|
+
)
|
|
1838
|
+
const canonicalKey = provenanceCandidateOrderKey(canonical)
|
|
1839
|
+
for (const rejected of sorted.slice(1)) {
|
|
1840
|
+
if (provenanceCandidateOrderKey(rejected) === canonicalKey) continue
|
|
1841
|
+
diagnostics.push({
|
|
1842
|
+
code: 'duplicate-source',
|
|
1843
|
+
kind: rejected.kind,
|
|
1844
|
+
id: rejected.id,
|
|
1845
|
+
...(rejected.source ? { source: rejected.source } : {}),
|
|
1846
|
+
})
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
factSources.sort(
|
|
1850
|
+
(left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id),
|
|
1851
|
+
)
|
|
1852
|
+
diagnostics.sort(
|
|
1853
|
+
(left, right) =>
|
|
1854
|
+
left.code.localeCompare(right.code) ||
|
|
1855
|
+
left.kind.localeCompare(right.kind) ||
|
|
1856
|
+
left.id.localeCompare(right.id) ||
|
|
1857
|
+
compareSourceRefs(left.source ?? { sourcePath: '' }, right.source ?? { sourcePath: '' }),
|
|
1858
|
+
)
|
|
1859
|
+
return { factSources, diagnostics }
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function buildOwnedContracts(
|
|
1863
|
+
families: ReadonlyArray<[ModuleOwnedContractKind, ModuleOwnedContractFact[]]>,
|
|
1864
|
+
): ModuleOwnedContracts | undefined {
|
|
1865
|
+
const result: ModuleOwnedContracts = {}
|
|
1866
|
+
for (const [kind, facts] of families) {
|
|
1867
|
+
if (facts.length === 0) continue
|
|
1868
|
+
// Drop the (constant/redundant) exportName from the emitted source ref; the line
|
|
1869
|
+
// anchor plus sourcePath already pin the declaration and keep the JSON compact.
|
|
1870
|
+
result[kind] = facts.map((fact) => ({
|
|
1871
|
+
...fact,
|
|
1872
|
+
source: {
|
|
1873
|
+
sourcePath: fact.source.sourcePath,
|
|
1874
|
+
...(fact.source.line ? { line: fact.source.line } : {}),
|
|
1875
|
+
},
|
|
1876
|
+
}))
|
|
1877
|
+
}
|
|
1878
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const CONTRIBUTION_KIND_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
|
|
1882
|
+
subscriber: 'subscriber',
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const REGISTRY_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
|
|
1886
|
+
vector: 'vector',
|
|
1887
|
+
integration: 'integration',
|
|
1888
|
+
payment: 'integration',
|
|
1889
|
+
shipping: 'integration',
|
|
1890
|
+
currency: 'integration',
|
|
1891
|
+
}
|
|
1892
|
+
|
|
726
1893
|
export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFacts {
|
|
727
|
-
const { moduleId, coreVersion = null } = options
|
|
1894
|
+
const { moduleId, coreVersion = null, sourcePackage = null, sourceVersion = null } = options
|
|
728
1895
|
const moduleRoot = options.moduleRoot
|
|
729
1896
|
?? (options.coreSrcRoot ? path.join(options.coreSrcRoot, moduleId) : null)
|
|
730
1897
|
if (!moduleRoot) {
|
|
731
1898
|
throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
|
|
732
1899
|
}
|
|
1900
|
+
const sourceRoot = toPortableSourceRoot(moduleId, sourcePackage)
|
|
733
1901
|
|
|
734
1902
|
const entitiesFilePath =
|
|
735
1903
|
resolveConventionFile(path.join(moduleRoot, 'data'), 'entities') ??
|
|
@@ -743,7 +1911,11 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
743
1911
|
const notificationsFilePath = resolveConventionFile(moduleRoot, 'notifications')
|
|
744
1912
|
const cliFilePath = resolveConventionFile(moduleRoot, 'cli')
|
|
745
1913
|
const indexFilePath = resolveConventionFile(moduleRoot, 'index')
|
|
746
|
-
const
|
|
1914
|
+
const setupFilePath = resolveConventionFile(moduleRoot, 'setup')
|
|
1915
|
+
const encryptionFilePath = resolveConventionFile(moduleRoot, 'encryption')
|
|
1916
|
+
|
|
1917
|
+
const portableOf = (filePath: string | null): string | null =>
|
|
1918
|
+
filePath ? toPortableSourcePath(moduleRoot, sourceRoot, filePath) : null
|
|
747
1919
|
|
|
748
1920
|
const warnings: string[] = []
|
|
749
1921
|
const { title, description } = extractModuleMeta(indexFilePath)
|
|
@@ -753,21 +1925,247 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
753
1925
|
const aclFeatures = extractAclFeatures(aclFilePath)
|
|
754
1926
|
|
|
755
1927
|
const { source: registrySource, description: registryDescription } = resolveRegistrySource(options)
|
|
756
|
-
const
|
|
757
|
-
const
|
|
1928
|
+
const apiRouteSourcePaths = extractApiRouteSourcePaths(moduleId, moduleRoot, sourceRoot)
|
|
1929
|
+
const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, apiRouteSourcePaths, warnings)
|
|
1930
|
+
const diRegistrations = extractDiRegistrationFacts(diFilePath, portableOf(diFilePath) ?? '')
|
|
1931
|
+
const diTokens = deriveLegacyDiTokens(diRegistrations.facts)
|
|
1932
|
+
for (const legacyToken of extractDiTokens(diFilePath)) {
|
|
1933
|
+
if (!diTokens.includes(legacyToken)) diTokens.push(legacyToken)
|
|
1934
|
+
}
|
|
758
1935
|
const searchEntities = extractSearchEntities(searchFilePath, warnings)
|
|
759
1936
|
const notifications = extractNotifications(notificationsFilePath, warnings)
|
|
760
1937
|
const cli = extractCli(cliFilePath, warnings)
|
|
1938
|
+
const cliCommands = cliFilePath
|
|
1939
|
+
? cli.map((command) => ({
|
|
1940
|
+
command,
|
|
1941
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, cliFilePath),
|
|
1942
|
+
}))
|
|
1943
|
+
: []
|
|
1944
|
+
const backendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'backend')
|
|
1945
|
+
const frontendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'frontend')
|
|
1946
|
+
const aiTools = extractAiTools(moduleRoot, sourceRoot)
|
|
1947
|
+
const aiAgents = extractAiAgents(moduleRoot, sourceRoot)
|
|
761
1948
|
const hostTokens: ModuleHostTokens = {
|
|
762
1949
|
entityIds: extractHostEntityIds(entities),
|
|
763
|
-
tableIds: extractTableIds(
|
|
1950
|
+
tableIds: extractTableIds(moduleRoot),
|
|
764
1951
|
}
|
|
1952
|
+
const extensionSurfaces = extractModuleExtensionFacts({
|
|
1953
|
+
moduleId,
|
|
1954
|
+
moduleRoot,
|
|
1955
|
+
sourceRoot,
|
|
1956
|
+
entities,
|
|
1957
|
+
events,
|
|
1958
|
+
apiRoutes,
|
|
1959
|
+
searchEntities,
|
|
1960
|
+
notifications,
|
|
1961
|
+
aiTools,
|
|
1962
|
+
aiAgents,
|
|
1963
|
+
})
|
|
765
1964
|
|
|
766
|
-
|
|
1965
|
+
const indexSourcePath = portableOf(indexFilePath)
|
|
1966
|
+
const diSourcePath = portableOf(diFilePath)
|
|
1967
|
+
const moduleMetadataFact = extractModuleMetadataContract(indexFilePath, moduleId, indexSourcePath)
|
|
1968
|
+
const commandCandidates = extractCommandContractCandidates(moduleId, moduleRoot, sourceRoot)
|
|
1969
|
+
const commandFacts = dedupeOwnedContractFacts(
|
|
1970
|
+
commandCandidates.map((candidate) => ({
|
|
1971
|
+
kind: 'command' as const,
|
|
1972
|
+
id: candidate.id,
|
|
1973
|
+
source: { sourcePath: candidate.sourcePath },
|
|
1974
|
+
})),
|
|
1975
|
+
)
|
|
1976
|
+
const workers = extractWorkerContracts(moduleId, moduleRoot, sourceRoot)
|
|
1977
|
+
const pageMiddleware = extractPageMiddlewareContracts(moduleRoot, sourceRoot)
|
|
1978
|
+
const setupFact = extractSetupContract(moduleId, setupFilePath, portableOf(setupFilePath))
|
|
1979
|
+
const encryptionFacts = extractEncryptionContracts(encryptionFilePath, portableOf(encryptionFilePath))
|
|
1980
|
+
const customEntityFacts = extractCustomEntityContracts(ceFilePath, portableOf(ceFilePath))
|
|
1981
|
+
const aiExtensionFacts = extractAiExtensionContracts(moduleRoot, sourceRoot)
|
|
1982
|
+
const generatorPlugins = extractGeneratorPluginContracts(moduleRoot, sourceRoot)
|
|
1983
|
+
const diContractFacts = dedupeOwnedContractFacts(
|
|
1984
|
+
diRegistrations.facts.map((fact) => ({
|
|
1985
|
+
kind: 'di-registration' as const,
|
|
1986
|
+
id: fact.token,
|
|
1987
|
+
source: fact.source,
|
|
1988
|
+
metadata: {
|
|
1989
|
+
registrationKind: fact.registrationKind,
|
|
1990
|
+
...(fact.providerSymbol ? { providerSymbol: fact.providerSymbol } : {}),
|
|
1991
|
+
...(fact.lifetime ? { lifetime: fact.lifetime } : {}),
|
|
1992
|
+
...(fact.injectionMode ? { injectionMode: fact.injectionMode } : {}),
|
|
1993
|
+
},
|
|
1994
|
+
})),
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
const ownedContracts = buildOwnedContracts([
|
|
1998
|
+
['module-metadata', moduleMetadataFact ? [moduleMetadataFact] : []],
|
|
1999
|
+
['command', commandFacts],
|
|
2000
|
+
['worker', workers.facts],
|
|
2001
|
+
['page-middleware', pageMiddleware.facts],
|
|
2002
|
+
['setup', setupFact ? [setupFact] : []],
|
|
2003
|
+
['encryption', encryptionFacts],
|
|
2004
|
+
['di-registration', diContractFacts],
|
|
2005
|
+
['custom-entity', customEntityFacts],
|
|
2006
|
+
['ai-extension', aiExtensionFacts],
|
|
2007
|
+
['generator-plugin', generatorPlugins.facts],
|
|
2008
|
+
])
|
|
2009
|
+
|
|
2010
|
+
const provenanceCandidates: ProvenanceCandidate[] = []
|
|
2011
|
+
const pushSource = (kind: ModuleFactSourceKind, id: string, source: ModuleFactSourceRef): void => {
|
|
2012
|
+
provenanceCandidates.push({ kind, id, source })
|
|
2013
|
+
}
|
|
2014
|
+
const pushRef = (
|
|
2015
|
+
kind: ModuleFactSourceKind,
|
|
2016
|
+
id: string,
|
|
2017
|
+
source: ModuleFactSourceRef,
|
|
2018
|
+
factSection: string,
|
|
2019
|
+
factKey: string,
|
|
2020
|
+
): void => {
|
|
2021
|
+
provenanceCandidates.push({ kind, id, source, factRef: { factSection, factKey } })
|
|
2022
|
+
}
|
|
2023
|
+
const pushOwnedRefs = (kind: ModuleOwnedContractKind, facts: readonly ModuleOwnedContractFact[]): void => {
|
|
2024
|
+
for (const fact of facts) pushRef(kind, fact.id, fact.source, `ownedContracts.${kind}`, fact.id)
|
|
2025
|
+
}
|
|
2026
|
+
if (moduleMetadataFact) {
|
|
2027
|
+
pushRef(
|
|
2028
|
+
'module-metadata',
|
|
2029
|
+
moduleId,
|
|
2030
|
+
moduleMetadataFact.source,
|
|
2031
|
+
'ownedContracts.module-metadata',
|
|
2032
|
+
moduleMetadataFact.id,
|
|
2033
|
+
)
|
|
2034
|
+
}
|
|
2035
|
+
if (entitiesFilePath) {
|
|
2036
|
+
const entitiesSourcePath = portableOf(entitiesFilePath) as string
|
|
2037
|
+
for (const entity of entities) {
|
|
2038
|
+
pushSource('entity', entity.id, { sourcePath: entitiesSourcePath, exportName: entity.class })
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (eventsFilePath) {
|
|
2042
|
+
const eventsSourcePath = portableOf(eventsFilePath) as string
|
|
2043
|
+
for (const event of events) pushSource('event', event.id, { sourcePath: eventsSourcePath, exportName: 'events' })
|
|
2044
|
+
}
|
|
2045
|
+
if (aclFilePath) {
|
|
2046
|
+
const aclSourcePath = portableOf(aclFilePath) as string
|
|
2047
|
+
for (const feature of aclFeatures) pushSource('acl-feature', feature, { sourcePath: aclSourcePath, exportName: 'features' })
|
|
2048
|
+
}
|
|
2049
|
+
for (const route of apiRoutes) {
|
|
2050
|
+
if (route.sourcePath) {
|
|
2051
|
+
pushRef('api-route', route.path, { sourcePath: route.sourcePath }, 'apiRoutes', route.path)
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (searchFilePath) {
|
|
2055
|
+
const searchSourcePath = portableOf(searchFilePath) as string
|
|
2056
|
+
for (const entityId of searchEntities) pushSource('search', entityId, { sourcePath: searchSourcePath, exportName: 'searchConfig' })
|
|
2057
|
+
}
|
|
2058
|
+
if (notificationsFilePath) {
|
|
2059
|
+
const notificationsSourcePath = portableOf(notificationsFilePath) as string
|
|
2060
|
+
for (const id of notifications) pushSource('notification', id, { sourcePath: notificationsSourcePath })
|
|
2061
|
+
}
|
|
2062
|
+
for (const command of cliCommands) {
|
|
2063
|
+
pushRef('cli-command', command.command, { sourcePath: command.sourcePath }, 'cliCommands', command.command)
|
|
2064
|
+
}
|
|
2065
|
+
for (const page of backendPages) {
|
|
2066
|
+
pushRef('backend-page', page.path, { sourcePath: page.sourcePath }, 'backendPages', page.path)
|
|
2067
|
+
}
|
|
2068
|
+
for (const page of frontendPages) {
|
|
2069
|
+
pushRef('frontend-page', page.path, { sourcePath: page.sourcePath }, 'frontendPages', page.path)
|
|
2070
|
+
}
|
|
2071
|
+
for (const tool of aiTools) pushRef('ai-tool', tool.name, { sourcePath: tool.sourcePath }, 'aiTools', tool.name)
|
|
2072
|
+
for (const agent of aiAgents) pushRef('ai-agent', agent.id, { sourcePath: agent.sourcePath }, 'aiAgents', agent.id)
|
|
2073
|
+
for (const candidate of commandCandidates) {
|
|
2074
|
+
pushRef('command', candidate.id, { sourcePath: candidate.sourcePath }, 'ownedContracts.command', candidate.id)
|
|
2075
|
+
}
|
|
2076
|
+
pushOwnedRefs('worker', workers.facts)
|
|
2077
|
+
pushOwnedRefs('page-middleware', pageMiddleware.facts)
|
|
2078
|
+
if (setupFact) pushOwnedRefs('setup', [setupFact])
|
|
2079
|
+
pushOwnedRefs('encryption', encryptionFacts)
|
|
2080
|
+
for (const fact of diRegistrations.facts) {
|
|
2081
|
+
pushRef('di-registration', fact.token, fact.source, 'ownedContracts.di-registration', fact.token)
|
|
2082
|
+
}
|
|
2083
|
+
pushOwnedRefs('custom-entity', customEntityFacts)
|
|
2084
|
+
pushOwnedRefs('ai-extension', aiExtensionFacts)
|
|
2085
|
+
pushOwnedRefs('generator-plugin', generatorPlugins.facts)
|
|
2086
|
+
const contributionRef = (contributionId: string): { factSection: string; factKey: string } => ({
|
|
2087
|
+
factSection: 'extensionSurfaces.contributions',
|
|
2088
|
+
factKey: contributionId,
|
|
2089
|
+
})
|
|
2090
|
+
for (const contribution of extensionSurfaces.contributions) {
|
|
2091
|
+
const contributionSource: ModuleFactSourceRef = {
|
|
2092
|
+
sourcePath: contribution.source.path,
|
|
2093
|
+
...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
|
|
2094
|
+
}
|
|
2095
|
+
const ref = contributionRef(contribution.id)
|
|
2096
|
+
if (contribution.kind === 'subscriber') {
|
|
2097
|
+
pushRef('subscriber', contribution.id, contributionSource, ref.factSection, ref.factKey)
|
|
2098
|
+
continue
|
|
2099
|
+
}
|
|
2100
|
+
if (contribution.kind === 'specialized-registry') {
|
|
2101
|
+
const details = contribution.details as { registry?: string; registryId?: string }
|
|
2102
|
+
const sourceKind = details.registry ? REGISTRY_TO_SOURCE_KIND[details.registry] : undefined
|
|
2103
|
+
if (sourceKind) {
|
|
2104
|
+
pushRef(
|
|
2105
|
+
sourceKind,
|
|
2106
|
+
details.registryId ?? contribution.id,
|
|
2107
|
+
contributionSource,
|
|
2108
|
+
ref.factSection,
|
|
2109
|
+
ref.factKey,
|
|
2110
|
+
)
|
|
2111
|
+
}
|
|
2112
|
+
continue
|
|
2113
|
+
}
|
|
2114
|
+
const mapped = CONTRIBUTION_KIND_TO_SOURCE_KIND[contribution.kind]
|
|
2115
|
+
if (mapped) pushRef(mapped, contribution.id, contributionSource, ref.factSection, ref.factKey)
|
|
2116
|
+
}
|
|
2117
|
+
// extension-host / extension-contribution provenance is emitted as typed `factRef` pointers
|
|
2118
|
+
// rather than duplicated source refs: those facts already carry their own `source` inside
|
|
2119
|
+
// `extensionSurfaces.{hosts,contributions}`, and copying every declared host spot (hundreds
|
|
2120
|
+
// per large module) would push the shared JSON past the determinism byte guard asserted in
|
|
2121
|
+
// `module-facts.bc-guard.test.ts`.
|
|
2122
|
+
// Hosts are indexed by their registry `key`: `id` repeats across families (an entity id is
|
|
2123
|
+
// also an api-entity id), while `key` is unique inside a module.
|
|
2124
|
+
for (const host of extensionSurfaces.hosts) {
|
|
2125
|
+
provenanceCandidates.push({
|
|
2126
|
+
kind: 'extension-host',
|
|
2127
|
+
id: host.key,
|
|
2128
|
+
...(host.source.kind === 'fact-ref' ? {} : { source: { sourcePath: host.source.path } }),
|
|
2129
|
+
factRef: { factSection: 'extensionSurfaces.hosts', factKey: host.key },
|
|
2130
|
+
})
|
|
2131
|
+
}
|
|
2132
|
+
for (const contribution of extensionSurfaces.contributions) {
|
|
2133
|
+
pushRef(
|
|
2134
|
+
'extension-contribution',
|
|
2135
|
+
contribution.id,
|
|
2136
|
+
{ sourcePath: contribution.source.path },
|
|
2137
|
+
'extensionSurfaces.contributions',
|
|
2138
|
+
contribution.id,
|
|
2139
|
+
)
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
const provenance = buildProvenanceIndex(provenanceCandidates)
|
|
2143
|
+
const factDiagnostics = [
|
|
2144
|
+
...provenance.diagnostics,
|
|
2145
|
+
...workers.unresolved,
|
|
2146
|
+
...pageMiddleware.unresolved,
|
|
2147
|
+
...generatorPlugins.unresolved,
|
|
2148
|
+
...diRegistrations.unresolvedTokens.map((token): ModuleFactDiagnostic => ({
|
|
2149
|
+
code: 'unresolved-static-contract',
|
|
2150
|
+
kind: 'di-registration',
|
|
2151
|
+
id: token,
|
|
2152
|
+
...(diSourcePath ? { source: { sourcePath: diSourcePath, exportName: token } } : {}),
|
|
2153
|
+
})),
|
|
2154
|
+
].sort(
|
|
2155
|
+
(left, right) =>
|
|
2156
|
+
left.code.localeCompare(right.code) ||
|
|
2157
|
+
left.kind.localeCompare(right.kind) ||
|
|
2158
|
+
left.id.localeCompare(right.id),
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
const facts: ModuleFacts = {
|
|
767
2162
|
module: moduleId,
|
|
768
2163
|
title,
|
|
769
2164
|
description,
|
|
770
2165
|
coreVersion,
|
|
2166
|
+
sourcePackage,
|
|
2167
|
+
sourceVersion,
|
|
2168
|
+
sourceRoot,
|
|
771
2169
|
entities,
|
|
772
2170
|
events,
|
|
773
2171
|
aclFeatures,
|
|
@@ -777,20 +2175,43 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
777
2175
|
hostTokens,
|
|
778
2176
|
notifications,
|
|
779
2177
|
cli,
|
|
2178
|
+
backendPages,
|
|
2179
|
+
frontendPages,
|
|
2180
|
+
cliCommands,
|
|
2181
|
+
aiTools,
|
|
2182
|
+
aiAgents,
|
|
2183
|
+
extensionSurfaces,
|
|
2184
|
+
...(provenance.factSources.length > 0 ? { factSources: provenance.factSources } : {}),
|
|
2185
|
+
...(ownedContracts ? { ownedContracts } : {}),
|
|
2186
|
+
...(factDiagnostics.length > 0 ? { factDiagnostics } : {}),
|
|
780
2187
|
warnings,
|
|
781
2188
|
}
|
|
2189
|
+
|
|
2190
|
+
// Exact unified override targets (spec 2026-08-02-module-facts-exact-override-targets).
|
|
2191
|
+
// Adapters consume the fully-populated owned facts + contributions above.
|
|
2192
|
+
const overrides = collectModuleOverrideTargets(facts, buildFactSourceLookup(facts))
|
|
2193
|
+
if (overrides.overrideTargets.length > 0) facts.overrideTargets = overrides.overrideTargets
|
|
2194
|
+
if (overrides.overrideTargetDiagnostics.length > 0) {
|
|
2195
|
+
facts.overrideTargetDiagnostics = overrides.overrideTargetDiagnostics
|
|
2196
|
+
}
|
|
2197
|
+
return facts
|
|
782
2198
|
}
|
|
783
2199
|
|
|
784
2200
|
export interface ModuleFactsJsonEvent {
|
|
785
2201
|
id: string
|
|
786
2202
|
category: string | null
|
|
787
2203
|
entity: string | null
|
|
2204
|
+
clientBroadcast?: boolean
|
|
2205
|
+
portalBroadcast?: boolean
|
|
788
2206
|
}
|
|
789
2207
|
|
|
790
2208
|
export interface ModuleFactsJsonEntry {
|
|
791
2209
|
title: string | null
|
|
792
2210
|
description: string | null
|
|
793
2211
|
coreVersion: string | null
|
|
2212
|
+
sourcePackage: string | null
|
|
2213
|
+
sourceVersion: string | null
|
|
2214
|
+
sourceRoot: string
|
|
794
2215
|
entities: ModuleEntityFact[]
|
|
795
2216
|
events: ModuleFactsJsonEvent[]
|
|
796
2217
|
aclFeatures: string[]
|
|
@@ -800,32 +2221,54 @@ export interface ModuleFactsJsonEntry {
|
|
|
800
2221
|
hostTokens: ModuleHostTokens
|
|
801
2222
|
notifications: string[]
|
|
802
2223
|
cli: string[]
|
|
2224
|
+
backendPages: ModulePageFact[]
|
|
2225
|
+
frontendPages: ModulePageFact[]
|
|
2226
|
+
cliCommands: ModuleCliCommandFact[]
|
|
2227
|
+
aiTools: ModuleAiToolFact[]
|
|
2228
|
+
aiAgents: ModuleAiAgentFact[]
|
|
2229
|
+
extensionSurfaces?: ModuleExtensionSurfaceFacts
|
|
2230
|
+
factSources?: ModuleFactSourceEntry[]
|
|
2231
|
+
ownedContracts?: ModuleOwnedContracts
|
|
2232
|
+
factDiagnostics?: ModuleFactDiagnostic[]
|
|
2233
|
+
overrideTargets?: ModuleOverrideTarget[]
|
|
2234
|
+
overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
|
|
803
2235
|
}
|
|
804
2236
|
|
|
805
2237
|
const EMPTY_SECTION_MARKER = '_none_'
|
|
806
2238
|
|
|
807
|
-
function renderVersionStamp(
|
|
2239
|
+
function renderVersionStamp(
|
|
2240
|
+
coreVersion: string | null,
|
|
2241
|
+
sourcePackage: string | null,
|
|
2242
|
+
sourceVersion: string | null,
|
|
2243
|
+
): string {
|
|
2244
|
+
if (sourcePackage) {
|
|
2245
|
+
return `<!-- generated from ${sourcePackage} ${sourceVersion || '<unknown>'}; core ${coreVersion || '<unknown>'} — R1 staleness stamp -->`
|
|
2246
|
+
}
|
|
808
2247
|
const version = coreVersion && coreVersion.length > 0 ? coreVersion : '<unknown>'
|
|
809
2248
|
return `<!-- generated from @open-mercato/core ${version} — R1 staleness stamp -->`
|
|
810
2249
|
}
|
|
811
2250
|
|
|
812
|
-
function renderEntitiesSection(entities: ModuleEntityFact[]): string {
|
|
2251
|
+
function renderEntitiesSection(entities: ModuleEntityFact[], lookup: FactSourceLookup): string {
|
|
813
2252
|
if (entities.length === 0) return `## Entities\n\n${EMPTY_SECTION_MARKER}`
|
|
814
|
-
const header = '| Entity ID | Class | Table | Editable | CustomFields |'
|
|
815
|
-
const divider = '
|
|
2253
|
+
const header = '| Entity ID | Class | Table | Editable | CustomFields | Source |'
|
|
2254
|
+
const divider = '|---|---|---|---|---|---|'
|
|
816
2255
|
const rows = entities.map(
|
|
817
2256
|
(entity) =>
|
|
818
|
-
`| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} |`,
|
|
2257
|
+
`| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} | ${renderLookedUpSource(lookup, 'entity', entity.id)} |`,
|
|
819
2258
|
)
|
|
820
2259
|
return ['## Entities', '', header, divider, ...rows].join('\n')
|
|
821
2260
|
}
|
|
822
2261
|
|
|
823
|
-
function renderEventsSection(events: ModuleEventFact[]): string {
|
|
2262
|
+
function renderEventsSection(events: ModuleEventFact[], lookup: FactSourceLookup): string {
|
|
824
2263
|
const heading = `## Events (${events.length})`
|
|
825
2264
|
if (events.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
826
|
-
const header = '| ID | Category | Entity |'
|
|
827
|
-
const divider = '
|
|
828
|
-
const rows = events.map((event) =>
|
|
2265
|
+
const header = '| ID | Category | Entity | Browser transport | Source |'
|
|
2266
|
+
const divider = '|---|---|---|---|---|'
|
|
2267
|
+
const rows = events.map((event) => {
|
|
2268
|
+
const transports = [event.clientBroadcast ? 'client' : null, event.portalBroadcast ? 'portal' : null]
|
|
2269
|
+
.filter((value): value is string => value !== null)
|
|
2270
|
+
return `| ${event.id} | ${event.category ?? '—'} | ${event.entity ?? '—'} | ${transports.join(', ') || '—'} | ${renderLookedUpSource(lookup, 'event', event.id)} |`
|
|
2271
|
+
})
|
|
829
2272
|
return [heading, '', header, divider, ...rows].join('\n')
|
|
830
2273
|
}
|
|
831
2274
|
|
|
@@ -834,6 +2277,73 @@ function renderInlineListSection(heading: string, values: string[]): string {
|
|
|
834
2277
|
return `${heading}\n\n${values.join(' · ')}`
|
|
835
2278
|
}
|
|
836
2279
|
|
|
2280
|
+
function renderSourceLinkedListSection(
|
|
2281
|
+
heading: string,
|
|
2282
|
+
values: readonly string[],
|
|
2283
|
+
lookup: FactSourceLookup,
|
|
2284
|
+
kind: ModuleFactSourceKind,
|
|
2285
|
+
idColumnLabel: string,
|
|
2286
|
+
): string {
|
|
2287
|
+
if (values.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2288
|
+
const rows = values.map((value) => `| ${value} | ${renderLookedUpSource(lookup, kind, value)} |`)
|
|
2289
|
+
return [heading, '', `| ${idColumnLabel} | Source |`, '|---|---|', ...rows].join('\n')
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function renderSourceLink(sourcePath: string): string {
|
|
2293
|
+
return `[${sourcePath}](../../../${sourcePath})`
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function renderSourceRefLink(source: ModuleFactSourceRef): string {
|
|
2297
|
+
const anchor = source.line ? `#L${source.line}` : ''
|
|
2298
|
+
const label = source.line ? `${source.sourcePath}:${source.line}` : source.sourcePath
|
|
2299
|
+
return `[${label}](../../../${source.sourcePath}${anchor})`
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
function renderLookedUpSource(lookup: FactSourceLookup, kind: ModuleFactSourceKind, id: string): string {
|
|
2304
|
+
const source = lookup(kind, id)
|
|
2305
|
+
return source ? renderSourceRefLink(source) : '—'
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function renderSafeScalar(value: ModuleFactSafeScalar | ModuleFactSafeScalar[] | undefined): string {
|
|
2309
|
+
if (value === undefined || value === null) return '—'
|
|
2310
|
+
if (Array.isArray(value)) return value.length > 0 ? value.map((entry) => String(entry)).join(', ') : '—'
|
|
2311
|
+
return String(value)
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function renderOwnedContractMetadata(metadata: ModuleOwnedContractFact['metadata']): string {
|
|
2315
|
+
if (!metadata) return '—'
|
|
2316
|
+
const keys = Object.keys(metadata).sort((left, right) => left.localeCompare(right))
|
|
2317
|
+
if (keys.length === 0) return '—'
|
|
2318
|
+
return keys.map((key) => `${key}=${renderSafeScalar(metadata[key])}`).join('; ')
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
function renderOwnedContractSection(
|
|
2322
|
+
heading: string,
|
|
2323
|
+
facts: readonly ModuleOwnedContractFact[] | undefined,
|
|
2324
|
+
): string {
|
|
2325
|
+
if (!facts || facts.length === 0) return ''
|
|
2326
|
+
const rows = facts.map(
|
|
2327
|
+
(fact) => `| ${fact.id} | ${renderOwnedContractMetadata(fact.metadata)} | ${renderSourceRefLink(fact.source)} |`,
|
|
2328
|
+
)
|
|
2329
|
+
return [heading, '', '| ID | Metadata | Source |', '|---|---|---|', ...rows].join('\n')
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
function renderDiRegistrationsSection(facts: readonly ModuleOwnedContractFact[] | undefined): string {
|
|
2333
|
+
if (!facts || facts.length === 0) return ''
|
|
2334
|
+
const rows = facts.map((fact) => {
|
|
2335
|
+
const metadata = fact.metadata ?? {}
|
|
2336
|
+
return `| ${fact.id} | ${renderSafeScalar(metadata.registrationKind)} | ${renderSafeScalar(metadata.lifetime)} | ${renderSafeScalar(metadata.injectionMode)} | ${renderSafeScalar(metadata.providerSymbol)} | ${renderSourceRefLink(fact.source)} |`
|
|
2337
|
+
})
|
|
2338
|
+
return [
|
|
2339
|
+
'## DI registrations (rich)',
|
|
2340
|
+
'',
|
|
2341
|
+
'| Token | Kind | Lifetime | Injection | Provider | Source |',
|
|
2342
|
+
'|---|---|---|---|---|---|',
|
|
2343
|
+
...rows,
|
|
2344
|
+
].join('\n')
|
|
2345
|
+
}
|
|
2346
|
+
|
|
837
2347
|
function describeAuthRule(rule: ApiRouteAuthRule | undefined): string {
|
|
838
2348
|
if (!rule) return 'public'
|
|
839
2349
|
if (rule.requireFeatures && rule.requireFeatures.length > 0) return rule.requireFeatures.join(', ')
|
|
@@ -856,44 +2366,253 @@ function renderApiRouteAuthCell(route: ModuleApiRouteFact): string {
|
|
|
856
2366
|
|
|
857
2367
|
function renderApiRoutesSection(routes: ModuleApiRouteFact[]): string {
|
|
858
2368
|
if (routes.length === 0) return `## API routes\n\n${EMPTY_SECTION_MARKER}`
|
|
859
|
-
const header = '| Path | Methods | Auth (per-method requireFeatures) |'
|
|
860
|
-
const divider = '
|
|
2369
|
+
const header = '| Path | Methods | Auth (per-method requireFeatures) | Source |'
|
|
2370
|
+
const divider = '|---|---|---|---|'
|
|
861
2371
|
const rows = routes.map(
|
|
862
|
-
(route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} |`,
|
|
2372
|
+
(route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} | ${route.sourcePath ? renderSourceLink(route.sourcePath) : '—'} |`,
|
|
863
2373
|
)
|
|
864
2374
|
return ['## API routes', '', header, divider, ...rows].join('\n')
|
|
865
2375
|
}
|
|
866
2376
|
|
|
2377
|
+
function renderLinkedFactsSection(
|
|
2378
|
+
heading: string,
|
|
2379
|
+
facts: ReadonlyArray<{ label: string; sourcePath: string }>,
|
|
2380
|
+
): string {
|
|
2381
|
+
if (facts.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2382
|
+
const rows = facts.map((fact) => `| ${fact.label} | ${renderSourceLink(fact.sourcePath)} |`)
|
|
2383
|
+
return [heading, '', '| ID / path | Source |', '|---|---|', ...rows].join('\n')
|
|
2384
|
+
}
|
|
2385
|
+
|
|
867
2386
|
function renderHostTokensSection(hostTokens: ModuleHostTokens): string {
|
|
868
2387
|
const entityIdsLine = hostTokens.entityIds.length > 0 ? hostTokens.entityIds.join(' · ') : EMPTY_SECTION_MARKER
|
|
869
2388
|
const tableIdsLine = hostTokens.tableIds.length > 0 ? hostTokens.tableIds.join(' · ') : EMPTY_SECTION_MARKER
|
|
870
2389
|
return ['## Host extension points', '', `- Entity IDs: ${entityIdsLine}`, `- Table IDs: ${tableIdsLine}`].join('\n')
|
|
871
2390
|
}
|
|
872
2391
|
|
|
2392
|
+
function renderExtensionHostContext(host: ModuleExtensionSurfaceFacts['hosts'][number]): string {
|
|
2393
|
+
return host.contextContract ?? host.runtimeContract ?? host.scopeContract ?? '—'
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function renderExtensionHostSource(host: ModuleExtensionHostFact): string {
|
|
2397
|
+
if (host.source.kind === 'fact-ref') return `${host.source.factSection}:${host.source.factKey}`
|
|
2398
|
+
return renderSourceRefLink({ sourcePath: host.source.path })
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function renderExtensionHostsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2402
|
+
const boundHosts = extensionSurfaces.hosts.filter((host) => host.bound)
|
|
2403
|
+
if (boundHosts.length === 0) return `## UMES hosts\n\n${EMPTY_SECTION_MARKER}`
|
|
2404
|
+
const rows = boundHosts.map((host) =>
|
|
2405
|
+
`| ${host.id} | ${host.family} | ${host.capabilities.join(', ') || '—'} | ${renderExtensionHostContext(host)} | ${host.stability.toUpperCase()} | ${renderExtensionHostSource(host)} |`,
|
|
2406
|
+
)
|
|
2407
|
+
return [
|
|
2408
|
+
'## UMES hosts',
|
|
2409
|
+
'',
|
|
2410
|
+
'| ID / pattern | Family | Supports | Context | Stability | Source |',
|
|
2411
|
+
'|---|---|---|---|---|---|',
|
|
2412
|
+
...rows,
|
|
2413
|
+
].join('\n')
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function compactContributionDetails(contribution: ModuleExtensionContributionFact): string {
|
|
2417
|
+
const details = contribution.details as unknown as Record<string, unknown>
|
|
2418
|
+
return Object.keys(details).sort((left, right) => left.localeCompare(right)).flatMap((key) => {
|
|
2419
|
+
const value = details[key]
|
|
2420
|
+
if (value === undefined) return []
|
|
2421
|
+
if (Array.isArray(value)) return [`${key}=${value.join(',') || 'none'}`]
|
|
2422
|
+
if (value && typeof value === 'object') {
|
|
2423
|
+
const nested = Object.keys(value as Record<string, unknown>).sort((left, right) => left.localeCompare(right)).map((nestedKey) => {
|
|
2424
|
+
const nestedValue = (value as Record<string, unknown>)[nestedKey]
|
|
2425
|
+
return `${nestedKey}:${Array.isArray(nestedValue) ? nestedValue.join(',') : String(nestedValue)}`
|
|
2426
|
+
})
|
|
2427
|
+
return [`${key}={${nested.join(';')}}`]
|
|
2428
|
+
}
|
|
2429
|
+
return [`${key}=${String(value)}`]
|
|
2430
|
+
}).join('; ')
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
function renderExtensionContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2434
|
+
if (extensionSurfaces.contributions.length === 0) return `## UMES contributions\n\n${EMPTY_SECTION_MARKER}`
|
|
2435
|
+
const rows = extensionSurfaces.contributions.map((contribution) => {
|
|
2436
|
+
const targets = contribution.targets.map((entry) => entry.id).join(', ')
|
|
2437
|
+
const resolution = contribution.targets.map((entry) => entry.resolution).join(', ')
|
|
2438
|
+
const phases = [...(contribution.phases ?? []), ...(contribution.operations ?? [])].join(', ') || '—'
|
|
2439
|
+
const source = renderSourceRefLink({ sourcePath: contribution.source.path })
|
|
2440
|
+
return `| ${contribution.id} | ${contribution.kind} | ${targets || '—'} | ${phases} | ${compactContributionDetails(contribution)} | ${resolution || '—'} | ${source} |`
|
|
2441
|
+
})
|
|
2442
|
+
return [
|
|
2443
|
+
'## UMES contributions',
|
|
2444
|
+
'',
|
|
2445
|
+
'| ID | Kind | Target | Phase / operations | Contract | Resolution | Source |',
|
|
2446
|
+
'|---|---|---|---|---|---|---|',
|
|
2447
|
+
...rows,
|
|
2448
|
+
].join('\n')
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function renderExtensionDiagnosticsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2452
|
+
const unbound = extensionSurfaces.hosts.filter((host) => !host.bound)
|
|
2453
|
+
if (unbound.length === 0 && extensionSurfaces.unresolved.length === 0) return ''
|
|
2454
|
+
const diagnostics = [
|
|
2455
|
+
...unbound.map((host) => `- unbound-helper: ${host.id}`),
|
|
2456
|
+
...extensionSurfaces.unresolved.map((entry) => `- ${entry.reason}: ${entry.key} (${entry.source.path})`),
|
|
2457
|
+
]
|
|
2458
|
+
return ['## UMES diagnostics', '', ...diagnostics].join('\n')
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function renderExtensionTargetRef(target: ModuleExtensionTargetRef): string {
|
|
2462
|
+
const method = target.method ? ` ${target.method}` : ''
|
|
2463
|
+
const owner = target.moduleId ? ` @${target.moduleId}` : ''
|
|
2464
|
+
return `${target.kind}:${target.id}${method}${owner}`
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function renderActivationBindingsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2468
|
+
const activations = extensionSurfaces.activations ?? []
|
|
2469
|
+
if (activations.length === 0) return `## Active extension bindings\n\n${EMPTY_SECTION_MARKER}`
|
|
2470
|
+
const rows = activations.map((activation) => {
|
|
2471
|
+
const phases = activation.phases && activation.phases.length > 0 ? activation.phases.join(', ') : '—'
|
|
2472
|
+
const source = activation.source ? renderSourceRefLink(activation.source) : '—'
|
|
2473
|
+
const bridge = activation.bridge ? `${activation.bridge.factSection}:${activation.bridge.factKey}` : '—'
|
|
2474
|
+
return `| ${activation.id} | ${activation.kind} | ${renderExtensionTargetRef(activation.host)} | ${activation.contributionKinds.join(', ') || '—'} | ${phases} | ${bridge} | ${source} |`
|
|
2475
|
+
})
|
|
2476
|
+
return [
|
|
2477
|
+
'## Active extension bindings',
|
|
2478
|
+
'',
|
|
2479
|
+
'| Activation | Kind | Host | Contribution kinds | Phases | Bridge | Source |',
|
|
2480
|
+
'|---|---|---|---|---|---|---|',
|
|
2481
|
+
...rows,
|
|
2482
|
+
].join('\n')
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function renderIncomingContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2486
|
+
const incoming = extensionSurfaces.incoming ?? []
|
|
2487
|
+
if (incoming.length === 0) return `## Incoming installed contributions\n\n${EMPTY_SECTION_MARKER}`
|
|
2488
|
+
const rows = incoming.map((entry) => {
|
|
2489
|
+
const activation = entry.activationId ?? '—'
|
|
2490
|
+
const source = entry.source ? renderSourceRefLink(entry.source) : '—'
|
|
2491
|
+
return `| ${entry.contributorModuleId} | ${entry.contributionKind} | ${renderExtensionTargetRef(entry.target)} | ${entry.resolution} | ${activation} | ${entry.contributionId} · ${source} |`
|
|
2492
|
+
})
|
|
2493
|
+
return [
|
|
2494
|
+
'## Incoming installed contributions',
|
|
2495
|
+
'',
|
|
2496
|
+
'| Contributor | Kind | Target | Resolution | Activation | Contribution · Source |',
|
|
2497
|
+
'|---|---|---|---|---|---|',
|
|
2498
|
+
...rows,
|
|
2499
|
+
].join('\n')
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function renderContributionResolutionsSection(
|
|
2503
|
+
extensionSurfaces: ModuleExtensionSurfaceFacts,
|
|
2504
|
+
lookup: FactSourceLookup,
|
|
2505
|
+
): string {
|
|
2506
|
+
const resolutions = extensionSurfaces.contributionResolutions ?? []
|
|
2507
|
+
if (resolutions.length === 0) return `## Contribution resolutions\n\n${EMPTY_SECTION_MARKER}`
|
|
2508
|
+
const rows = resolutions.map((resolution) => {
|
|
2509
|
+
const activations = resolution.activationIds.length > 0 ? resolution.activationIds.join(', ') : '—'
|
|
2510
|
+
const source = renderLookedUpSource(lookup, 'extension-contribution', resolution.contributionId)
|
|
2511
|
+
return `| ${resolution.contributionId} | ${renderExtensionTargetRef(resolution.target)} | ${resolution.resolution} | ${activations} | ${source} |`
|
|
2512
|
+
})
|
|
2513
|
+
return [
|
|
2514
|
+
'## Contribution resolutions',
|
|
2515
|
+
'',
|
|
2516
|
+
'| Contribution | Target | Resolution | Activations | Source |',
|
|
2517
|
+
'|---|---|---|---|---|',
|
|
2518
|
+
...rows,
|
|
2519
|
+
].join('\n')
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
function renderOverrideTargetPath(target: ModuleOverrideTarget): string {
|
|
2523
|
+
let display = 'overrides'
|
|
2524
|
+
for (let index = 0; index < target.path.length; index += 1) {
|
|
2525
|
+
const segment = target.path[index]
|
|
2526
|
+
const isKeyedTerminal = target.key !== undefined && index === target.path.length - 1
|
|
2527
|
+
display += isKeyedTerminal ? `[${JSON.stringify(segment)}]` : `.${segment}`
|
|
2528
|
+
}
|
|
2529
|
+
return display
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function renderOverrideTargetsSection(targets: readonly ModuleOverrideTarget[] | undefined): string {
|
|
2533
|
+
if (!targets || targets.length === 0) return `## Exact override targets\n\n${EMPTY_SECTION_MARKER}`
|
|
2534
|
+
const header = '| Domain | Path / key | Supported modes | Referenced fact | Source |'
|
|
2535
|
+
const divider = '|---|---|---|---|---|'
|
|
2536
|
+
const rows = targets.map((target) => {
|
|
2537
|
+
const path = `\`${renderOverrideTargetPath(target)}\``
|
|
2538
|
+
const modes = target.modes.join(', ') || '—'
|
|
2539
|
+
const fact = `${target.factRef.factSection}:${target.factRef.factKey}`
|
|
2540
|
+
return `| ${target.domain} | ${path} | ${modes} | ${fact} | ${renderSourceRefLink(target.source)} |`
|
|
2541
|
+
})
|
|
2542
|
+
return ['## Exact override targets', '', header, divider, ...rows].join('\n')
|
|
2543
|
+
}
|
|
2544
|
+
|
|
873
2545
|
export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
2546
|
+
const extensionSurfaces = facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] }
|
|
2547
|
+
const lookup = buildFactSourceLookup(facts)
|
|
874
2548
|
const sections = [
|
|
875
2549
|
`# ${facts.module} — module facts (generated, do not edit)`,
|
|
876
|
-
renderVersionStamp(facts.coreVersion),
|
|
2550
|
+
renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
|
|
2551
|
+
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
877
2552
|
'',
|
|
878
|
-
renderEntitiesSection(facts.entities),
|
|
2553
|
+
renderEntitiesSection(facts.entities, lookup),
|
|
879
2554
|
'',
|
|
880
|
-
renderEventsSection(facts.events),
|
|
2555
|
+
renderEventsSection(facts.events, lookup),
|
|
881
2556
|
'',
|
|
882
|
-
|
|
2557
|
+
renderSourceLinkedListSection(
|
|
2558
|
+
`## ACL features (${facts.aclFeatures.length})`,
|
|
2559
|
+
facts.aclFeatures,
|
|
2560
|
+
lookup,
|
|
2561
|
+
'acl-feature',
|
|
2562
|
+
'Feature',
|
|
2563
|
+
),
|
|
883
2564
|
'',
|
|
884
2565
|
renderApiRoutesSection(facts.apiRoutes),
|
|
885
2566
|
'',
|
|
886
|
-
|
|
2567
|
+
renderLinkedFactsSection('## Backend pages', facts.backendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
2568
|
+
'',
|
|
2569
|
+
renderLinkedFactsSection('## Frontend pages', facts.frontendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
887
2570
|
'',
|
|
888
|
-
|
|
2571
|
+
renderSourceLinkedListSection('## DI service tokens', facts.diTokens, lookup, 'di-registration', 'Token'),
|
|
2572
|
+
'',
|
|
2573
|
+
renderSourceLinkedListSection('## Search entities', facts.searchEntities, lookup, 'search', 'Entity ID'),
|
|
889
2574
|
'',
|
|
890
2575
|
renderHostTokensSection(facts.hostTokens),
|
|
891
2576
|
'',
|
|
892
|
-
|
|
2577
|
+
renderExtensionHostsSection(extensionSurfaces),
|
|
2578
|
+
'',
|
|
2579
|
+
renderExtensionContributionsSection(extensionSurfaces),
|
|
2580
|
+
'',
|
|
2581
|
+
renderActivationBindingsSection(extensionSurfaces),
|
|
2582
|
+
'',
|
|
2583
|
+
renderIncomingContributionsSection(extensionSurfaces),
|
|
2584
|
+
'',
|
|
2585
|
+
renderContributionResolutionsSection(extensionSurfaces, lookup),
|
|
893
2586
|
'',
|
|
894
|
-
|
|
2587
|
+
renderExtensionDiagnosticsSection(extensionSurfaces),
|
|
2588
|
+
'',
|
|
2589
|
+
renderSourceLinkedListSection('## Notifications', facts.notifications, lookup, 'notification', 'Notification ID'),
|
|
2590
|
+
'',
|
|
2591
|
+
renderLinkedFactsSection('## CLI commands', facts.cliCommands.map((command) => ({ label: command.command, sourcePath: command.sourcePath }))),
|
|
2592
|
+
'',
|
|
2593
|
+
renderLinkedFactsSection('## AI tools / MCP capabilities', facts.aiTools.map((tool) => ({ label: tool.name, sourcePath: tool.sourcePath }))),
|
|
2594
|
+
'',
|
|
2595
|
+
renderLinkedFactsSection('## AI agents', facts.aiAgents.map((agent) => ({ label: agent.id, sourcePath: agent.sourcePath }))),
|
|
2596
|
+
'',
|
|
2597
|
+
renderOverrideTargetsSection(facts.overrideTargets),
|
|
895
2598
|
'',
|
|
896
2599
|
]
|
|
2600
|
+
const owned = facts.ownedContracts ?? {}
|
|
2601
|
+
const ownedSections = [
|
|
2602
|
+
renderOwnedContractSection('## Owned contract — module metadata', owned['module-metadata']),
|
|
2603
|
+
renderOwnedContractSection('## Domain commands', owned.command),
|
|
2604
|
+
renderOwnedContractSection('## Workers', owned.worker),
|
|
2605
|
+
renderOwnedContractSection('## Page middleware', owned['page-middleware']),
|
|
2606
|
+
renderOwnedContractSection('## Setup', owned.setup),
|
|
2607
|
+
renderOwnedContractSection('## Encryption', owned.encryption),
|
|
2608
|
+
renderDiRegistrationsSection(owned['di-registration']),
|
|
2609
|
+
renderOwnedContractSection('## Custom entities', owned['custom-entity']),
|
|
2610
|
+
renderOwnedContractSection('## AI extensions', owned['ai-extension']),
|
|
2611
|
+
renderOwnedContractSection('## Generator plugins', owned['generator-plugin']),
|
|
2612
|
+
].filter((section) => section.length > 0)
|
|
2613
|
+
for (const section of ownedSections) {
|
|
2614
|
+
sections.push(section, '')
|
|
2615
|
+
}
|
|
897
2616
|
return sections.join('\n')
|
|
898
2617
|
}
|
|
899
2618
|
|
|
@@ -902,8 +2621,17 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
902
2621
|
title: facts.title,
|
|
903
2622
|
description: facts.description,
|
|
904
2623
|
coreVersion: facts.coreVersion,
|
|
2624
|
+
sourcePackage: facts.sourcePackage,
|
|
2625
|
+
sourceVersion: facts.sourceVersion,
|
|
2626
|
+
sourceRoot: facts.sourceRoot,
|
|
905
2627
|
entities: facts.entities,
|
|
906
|
-
events: facts.events.map((event) => ({
|
|
2628
|
+
events: facts.events.map((event) => ({
|
|
2629
|
+
id: event.id,
|
|
2630
|
+
category: event.category,
|
|
2631
|
+
entity: event.entity,
|
|
2632
|
+
...(event.clientBroadcast !== undefined ? { clientBroadcast: event.clientBroadcast } : {}),
|
|
2633
|
+
...(event.portalBroadcast !== undefined ? { portalBroadcast: event.portalBroadcast } : {}),
|
|
2634
|
+
})),
|
|
907
2635
|
aclFeatures: facts.aclFeatures,
|
|
908
2636
|
apiRoutes: facts.apiRoutes,
|
|
909
2637
|
diTokens: facts.diTokens,
|
|
@@ -911,6 +2639,25 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
911
2639
|
hostTokens: facts.hostTokens,
|
|
912
2640
|
notifications: facts.notifications,
|
|
913
2641
|
cli: facts.cli,
|
|
2642
|
+
backendPages: facts.backendPages,
|
|
2643
|
+
frontendPages: facts.frontendPages,
|
|
2644
|
+
cliCommands: facts.cliCommands,
|
|
2645
|
+
aiTools: facts.aiTools,
|
|
2646
|
+
aiAgents: facts.aiAgents,
|
|
2647
|
+
...(facts.extensionSurfaces ? { extensionSurfaces: facts.extensionSurfaces } : {}),
|
|
2648
|
+
...(facts.factSources && facts.factSources.length > 0 ? { factSources: facts.factSources } : {}),
|
|
2649
|
+
...(facts.ownedContracts && Object.keys(facts.ownedContracts).length > 0
|
|
2650
|
+
? { ownedContracts: facts.ownedContracts }
|
|
2651
|
+
: {}),
|
|
2652
|
+
...(facts.factDiagnostics && facts.factDiagnostics.length > 0
|
|
2653
|
+
? { factDiagnostics: facts.factDiagnostics }
|
|
2654
|
+
: {}),
|
|
2655
|
+
...(facts.overrideTargets && facts.overrideTargets.length > 0
|
|
2656
|
+
? { overrideTargets: facts.overrideTargets }
|
|
2657
|
+
: {}),
|
|
2658
|
+
...(facts.overrideTargetDiagnostics && facts.overrideTargetDiagnostics.length > 0
|
|
2659
|
+
? { overrideTargetDiagnostics: facts.overrideTargetDiagnostics }
|
|
2660
|
+
: {}),
|
|
914
2661
|
}
|
|
915
2662
|
}
|
|
916
2663
|
|
|
@@ -947,9 +2694,14 @@ export interface ExtractAllModuleFactsResult {
|
|
|
947
2694
|
factsByModule: Record<string, ModuleFacts>
|
|
948
2695
|
markdownByModule: Record<string, string>
|
|
949
2696
|
warnings: string[]
|
|
2697
|
+
frameworkMarkdown: string
|
|
950
2698
|
}
|
|
951
2699
|
|
|
952
2700
|
export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
|
|
2701
|
+
return withModuleExtensionFactExtractionCache(() => extractAllModuleFactsWithCache(options))
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
|
|
953
2705
|
const sources: ModuleFactSource[] = options.sources
|
|
954
2706
|
? [...options.sources]
|
|
955
2707
|
: (options.coreSrcRoot
|
|
@@ -967,12 +2719,65 @@ export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): Ex
|
|
|
967
2719
|
moduleId: source.moduleId,
|
|
968
2720
|
moduleRoot: source.moduleRoot,
|
|
969
2721
|
coreVersion: options.coreVersion ?? null,
|
|
2722
|
+
sourcePackage: source.from ?? null,
|
|
2723
|
+
sourceVersion: source.packageVersion ?? null,
|
|
970
2724
|
registryPath: options.registryPath ?? null,
|
|
971
2725
|
registrySource: options.registrySource ?? null,
|
|
972
2726
|
})
|
|
973
2727
|
factsByModule[source.moduleId] = facts
|
|
974
|
-
markdownByModule[source.moduleId] = renderModuleFactsMarkdown(facts)
|
|
975
2728
|
warnings.push(...facts.warnings)
|
|
976
2729
|
}
|
|
977
|
-
|
|
2730
|
+
const surfacesByModule = Object.fromEntries(
|
|
2731
|
+
Object.entries(factsByModule).map(([moduleId, facts]) => [
|
|
2732
|
+
moduleId,
|
|
2733
|
+
facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] },
|
|
2734
|
+
]),
|
|
2735
|
+
)
|
|
2736
|
+
const correlated = correlateModuleExtensionFacts({
|
|
2737
|
+
surfacesByModule,
|
|
2738
|
+
entityIds: new Set(Object.values(factsByModule).flatMap((facts) => facts.entities.map((entity) => entity.id))),
|
|
2739
|
+
eventIds: new Set(Object.values(factsByModule).flatMap((facts) => facts.events.map((event) => event.id))),
|
|
2740
|
+
apiRoutes: new Set([
|
|
2741
|
+
...Object.values(factsByModule).flatMap((facts) => facts.apiRoutes.map((route) => route.path)),
|
|
2742
|
+
...sources.flatMap((source) => extractKnownApiRouteIds(source.moduleId, source.moduleRoot)),
|
|
2743
|
+
]),
|
|
2744
|
+
commandIds: new Set(sources.flatMap((source) => extractKnownCommandIds(source.moduleId, source.moduleRoot))),
|
|
2745
|
+
})
|
|
2746
|
+
assertNoUnresolvedExtensionTargets(correlated)
|
|
2747
|
+
const apiRouteOwners = new Map<string, {
|
|
2748
|
+
moduleId: string
|
|
2749
|
+
source: ModuleFactSourceRef
|
|
2750
|
+
bridges: ApiRouteInterceptorBridge[]
|
|
2751
|
+
}>()
|
|
2752
|
+
const commandOwners = new Map<string, { moduleId: string; source: ModuleFactSourceRef }>()
|
|
2753
|
+
for (const source of [...sources].sort((left, right) => left.moduleId.localeCompare(right.moduleId))) {
|
|
2754
|
+
const facts = factsByModule[source.moduleId]
|
|
2755
|
+
const sourceRoot = facts ? facts.sourceRoot : source.moduleId
|
|
2756
|
+
const owners = extractActivationTargetOwners({
|
|
2757
|
+
moduleId: source.moduleId,
|
|
2758
|
+
moduleRoot: source.moduleRoot,
|
|
2759
|
+
sourceRoot,
|
|
2760
|
+
})
|
|
2761
|
+
for (const route of owners.apiRoutes) {
|
|
2762
|
+
if (!apiRouteOwners.has(route.id)) {
|
|
2763
|
+
apiRouteOwners.set(route.id, { moduleId: source.moduleId, source: route.source, bridges: route.bridges })
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
for (const command of owners.commands) {
|
|
2767
|
+
if (!commandOwners.has(command.id)) commandOwners.set(command.id, { moduleId: source.moduleId, source: command.source })
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
const withIncoming = correlateIncomingExtensions({
|
|
2771
|
+
surfacesByModule: correlated,
|
|
2772
|
+
apiRouteOwners,
|
|
2773
|
+
commandOwners,
|
|
2774
|
+
})
|
|
2775
|
+
for (const moduleId of Object.keys(factsByModule).sort((left, right) => left.localeCompare(right))) {
|
|
2776
|
+
factsByModule[moduleId].extensionSurfaces = withIncoming[moduleId]
|
|
2777
|
+
markdownByModule[moduleId] = renderModuleFactsMarkdown(factsByModule[moduleId])
|
|
2778
|
+
warnings.push(...correlated[moduleId].unresolved.map((entry) =>
|
|
2779
|
+
`[module-facts] ${moduleId} ${entry.reason}: ${entry.key} (${entry.source.path})`,
|
|
2780
|
+
))
|
|
2781
|
+
}
|
|
2782
|
+
return { factsByModule, markdownByModule, warnings, frameworkMarkdown: renderFrameworkExtensionPointsMarkdown() }
|
|
978
2783
|
}
|