@open-mercato/cli 0.6.7 → 0.6.8-canary.320.1.bf889f49e4
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 +77 -4
- package/AGENTS.md +6 -1
- package/build.mjs +68 -11
- 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 +86 -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 +116757 -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 +482 -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 +305 -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 +149 -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 +168 -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 +687 -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 +372 -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 +469 -47
- package/dist/agentic/guides/modules/workflows.md +302 -43
- package/dist/agentic/guides/reference-module-facts.json +6942 -0
- package/dist/agentic/guides/reference-modules/example.md +462 -0
- package/dist/agentic/guides/spec-delivery.md +56 -0
- package/dist/agentic/guides/testing-debugging.md +80 -0
- package/dist/agentic/guides/upstream/AGENTS.md +300 -0
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +334 -0
- package/dist/agentic/guides/upstream/manifest.json +8 -0
- package/dist/agentic/shared/AGENTS.md.template +120 -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 +59 -0
- package/dist/agentic/shared/ai/harness/cases.json +20596 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +196 -0
- package/dist/agentic/shared/ai/harness/design-system-inventory.json +6326 -0
- package/dist/agentic/shared/ai/harness/fixtures/index.json +51 -0
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +166 -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/knowledge-change.schema.json +228 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +163 -0
- package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
- package/dist/agentic/shared/ai/harness/result.schema.json +136 -0
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +22 -0
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +1706 -0
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
- package/dist/agentic/shared/ai/harness/validators.json +82 -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/harness/writable-spec-oracles.mjs +602 -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 +21 -241
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +31 -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 +15 -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 +17 -601
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +21 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +15 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +16 -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 +15 -310
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +19 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +31 -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/knowledge-change.md +133 -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 +25 -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 +25 -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 +32 -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 +14 -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 +40 -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 +20 -843
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +42 -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 +43 -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 +17 -467
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +29 -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/2026-08-06-reference-module-activation.md +248 -0
- 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 +224 -0
- package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +3812 -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/agentic/shared/scripts/validate-knowledge-change.mjs +1172 -0
- package/dist/bin.js +32 -34
- 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/cli-bootstrap-mode.js +32 -0
- package/dist/lib/cli-bootstrap-mode.js.map +7 -0
- package/dist/lib/dev-supervisor-manifest.js +105 -0
- package/dist/lib/dev-supervisor-manifest.js.map +7 -0
- package/dist/lib/generate-watch-events.js +15 -0
- package/dist/lib/generate-watch-events.js.map +2 -2
- package/dist/lib/generators/entity-ids.js +11 -24
- package/dist/lib/generators/entity-ids.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-di.js +10 -23
- package/dist/lib/generators/module-di.js.map +2 -2
- package/dist/lib/generators/module-entities.js +10 -23
- package/dist/lib/generators/module-entities.js.map +2 -2
- package/dist/lib/generators/module-extension-facts.js +1882 -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 +64 -7
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +1913 -52
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-override-targets.js +712 -0
- package/dist/lib/generators/module-override-targets.js.map +7 -0
- package/dist/lib/generators/module-registry.js +405 -36
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-manifest-shards.js +170 -0
- package/dist/lib/generators/route-manifest-shards.js.map +7 -0
- package/dist/lib/in-process-generate-watcher.js +12 -3
- package/dist/lib/in-process-generate-watcher.js.map +2 -2
- package/dist/lib/load-env.js +24 -0
- package/dist/lib/load-env.js.map +7 -0
- package/dist/lib/queue-worker-supervisor.js.map +2 -2
- package/dist/lib/resolver.js +28 -3
- 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 -2
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/lib/utils.js +15 -8
- package/dist/lib/utils.js.map +2 -2
- package/dist/mercato.js +35 -9
- package/dist/mercato.js.map +2 -2
- package/package.json +13 -11
- package/src/__tests__/mercato.test.ts +144 -0
- package/src/bin.ts +60 -42
- 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__/cli-bootstrap-mode.test.ts +22 -0
- package/src/lib/__tests__/content-stable-writes.test.ts +74 -0
- package/src/lib/__tests__/dev-supervisor-manifest.test.ts +131 -0
- package/src/lib/__tests__/generate-watch-events.test.ts +76 -0
- package/src/lib/__tests__/in-process-generate-watcher.test.ts +52 -1
- 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__/queue-worker-supervisor.test.ts +33 -0
- package/src/lib/__tests__/resolver.enterprise.test.ts +50 -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/cli-bootstrap-mode.ts +31 -0
- package/src/lib/dev-supervisor-manifest.ts +142 -0
- package/src/lib/generate-watch-events.ts +14 -0
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
- package/src/lib/generators/__tests__/entity-ids.test.ts +32 -0
- package/src/lib/generators/__tests__/helpers/override-target-diagnostic-fixtures.ts +150 -0
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +603 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +139 -2
- package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
- 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.example-fact-coverage.test.ts +414 -0
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
- package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +393 -0
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
- package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +160 -0
- package/src/lib/generators/__tests__/module-override-targets.diagnostic-wiring.test.ts +92 -0
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
- package/src/lib/generators/__tests__/module-override-targets.test.ts +328 -0
- package/src/lib/generators/__tests__/module-override-targets.unknown-mode.test.ts +117 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +266 -0
- package/src/lib/generators/__tests__/output-snapshots.test.ts +3 -3
- package/src/lib/generators/__tests__/route-manifest-shards.test.ts +62 -0
- package/src/lib/generators/entity-ids.ts +10 -26
- package/src/lib/generators/extensions/injection-widgets.ts +51 -0
- package/src/lib/generators/module-di.ts +9 -25
- package/src/lib/generators/module-entities.ts +9 -25
- package/src/lib/generators/module-extension-facts.ts +2472 -0
- package/src/lib/generators/module-fact-sources.ts +108 -0
- package/src/lib/generators/module-facts-discovery.ts +104 -7
- package/src/lib/generators/module-facts.ts +2747 -138
- package/src/lib/generators/module-override-targets.ts +907 -0
- package/src/lib/generators/module-registry.ts +448 -35
- package/src/lib/generators/route-manifest-shards.ts +146 -0
- package/src/lib/in-process-generate-watcher.ts +13 -4
- package/src/lib/load-env.ts +30 -0
- package/src/lib/queue-worker-supervisor.ts +7 -2
- package/src/lib/resolver.ts +34 -3
- 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 -2
- package/src/lib/utils.ts +24 -16
- package/src/mercato.ts +46 -9
- 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,39 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
import fs from 'node:fs'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import ts from 'typescript-js'
|
|
5
|
+
import type {
|
|
6
|
+
ModuleExtensionContributionFact,
|
|
7
|
+
ModuleExtensionHostFact,
|
|
8
|
+
ModuleExtensionSurfaceFacts,
|
|
9
|
+
ModuleExtensionTargetRef,
|
|
10
|
+
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
4
11
|
import { toSnake } from '../utils'
|
|
12
|
+
import { extractCommandIdsFromSource } from './module-registry'
|
|
13
|
+
import { MODULE_CODE_EXTENSIONS } from './scanner'
|
|
14
|
+
import type { ApiRouteInterceptorBridge } from './module-extension-facts'
|
|
15
|
+
import {
|
|
16
|
+
assertNoUnresolvedExtensionTargets,
|
|
17
|
+
correlateIncomingExtensions,
|
|
18
|
+
correlateModuleExtensionFacts,
|
|
19
|
+
extractActivationTargetOwners,
|
|
20
|
+
extractKnownApiRouteIds,
|
|
21
|
+
extractKnownCommandIds,
|
|
22
|
+
extractModuleExtensionFacts,
|
|
23
|
+
renderFrameworkExtensionPointsMarkdown,
|
|
24
|
+
withModuleExtensionFactExtractionCache,
|
|
25
|
+
} from './module-extension-facts'
|
|
26
|
+
import {
|
|
27
|
+
ALL_OVERRIDE_DOMAINS,
|
|
28
|
+
MODULE_OVERRIDE_MODES,
|
|
29
|
+
MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES,
|
|
30
|
+
MODULE_OVERRIDE_TARGET_NOTES,
|
|
31
|
+
collectModuleOverrideTargets,
|
|
32
|
+
type ModuleOverrideTarget,
|
|
33
|
+
type ModuleOverrideTargetDiagnostic,
|
|
34
|
+
} from './module-override-targets'
|
|
35
|
+
import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
|
|
36
|
+
import { appendLocalReferenceModuleSource } from './module-facts-discovery'
|
|
5
37
|
|
|
6
38
|
export interface ModuleEntityFact {
|
|
7
39
|
id: string
|
|
@@ -21,6 +53,49 @@ export interface ModuleApiRouteFact {
|
|
|
21
53
|
path: string
|
|
22
54
|
methods: string[]
|
|
23
55
|
auth: Record<string, ApiRouteAuthRule>
|
|
56
|
+
sourcePath: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Statically declared page metadata that affects extensibility or access, using the
|
|
61
|
+
* runtime `PageMetadata` contract keys and the same alias precedence as
|
|
62
|
+
* `resolvePageRouteMetadata` (`pageGroup ?? group`, `pageGroupKey ?? groupKey`,
|
|
63
|
+
* `pageOrder ?? order`). Executable guards, icons, titles and translation results are
|
|
64
|
+
* never serialized.
|
|
65
|
+
*/
|
|
66
|
+
export interface ModulePageMetadata {
|
|
67
|
+
requireAuth?: boolean
|
|
68
|
+
requireCustomerAuth?: boolean
|
|
69
|
+
requireFeatures?: string[]
|
|
70
|
+
requireCustomerFeatures?: string[]
|
|
71
|
+
navHidden?: boolean
|
|
72
|
+
navGroup?: string
|
|
73
|
+
navGroupKey?: string
|
|
74
|
+
navOrder?: number
|
|
75
|
+
pageContext?: 'main' | 'admin' | 'settings' | 'profile'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ModulePageFact {
|
|
79
|
+
path: string
|
|
80
|
+
sourcePath: string
|
|
81
|
+
/** Companion metadata file (`page.meta.*` / `meta.*`) when runtime reads metadata from one. */
|
|
82
|
+
metadataSourcePath?: string
|
|
83
|
+
metadata?: ModulePageMetadata
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ModuleCliCommandFact {
|
|
87
|
+
command: string
|
|
88
|
+
sourcePath: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ModuleAiToolFact {
|
|
92
|
+
name: string
|
|
93
|
+
sourcePath: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ModuleAiAgentFact {
|
|
97
|
+
id: string
|
|
98
|
+
sourcePath: string
|
|
24
99
|
}
|
|
25
100
|
|
|
26
101
|
export interface ModuleEventFact {
|
|
@@ -28,6 +103,8 @@ export interface ModuleEventFact {
|
|
|
28
103
|
label?: string
|
|
29
104
|
category: string | null
|
|
30
105
|
entity: string | null
|
|
106
|
+
clientBroadcast?: boolean
|
|
107
|
+
portalBroadcast?: boolean
|
|
31
108
|
}
|
|
32
109
|
|
|
33
110
|
export interface ModuleHostTokens {
|
|
@@ -35,11 +112,139 @@ export interface ModuleHostTokens {
|
|
|
35
112
|
tableIds: string[]
|
|
36
113
|
}
|
|
37
114
|
|
|
115
|
+
export type ModuleFactSourceRef = {
|
|
116
|
+
sourcePath: string
|
|
117
|
+
exportName?: string
|
|
118
|
+
line?: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ModuleFactRef = {
|
|
122
|
+
factSection: string
|
|
123
|
+
factKey: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Closed provenance/contract sets, published as `as const` ledgers whose types
|
|
128
|
+
* derive from them. The `factCoverage` ledger enumerates these at runtime, so a
|
|
129
|
+
* kind added here without a ledger row fails the coverage contract test rather
|
|
130
|
+
* than silently joining the public surface unclassified.
|
|
131
|
+
*/
|
|
132
|
+
export const MODULE_FACT_SOURCE_KINDS = [
|
|
133
|
+
'module-metadata',
|
|
134
|
+
'entity',
|
|
135
|
+
'event',
|
|
136
|
+
'acl-feature',
|
|
137
|
+
'api-route',
|
|
138
|
+
'di-registration',
|
|
139
|
+
'search',
|
|
140
|
+
'vector',
|
|
141
|
+
'notification',
|
|
142
|
+
'cli-command',
|
|
143
|
+
'backend-page',
|
|
144
|
+
'frontend-page',
|
|
145
|
+
'ai-tool',
|
|
146
|
+
'ai-agent',
|
|
147
|
+
'ai-extension',
|
|
148
|
+
'command',
|
|
149
|
+
'subscriber',
|
|
150
|
+
'worker',
|
|
151
|
+
'page-middleware',
|
|
152
|
+
'setup',
|
|
153
|
+
'encryption',
|
|
154
|
+
'custom-entity',
|
|
155
|
+
'integration',
|
|
156
|
+
'generator-plugin',
|
|
157
|
+
'extension-host',
|
|
158
|
+
'extension-contribution',
|
|
159
|
+
] as const
|
|
160
|
+
|
|
161
|
+
export type ModuleFactSourceKind = (typeof MODULE_FACT_SOURCE_KINDS)[number]
|
|
162
|
+
|
|
163
|
+
export const MODULE_OWNED_CONTRACT_KINDS = [
|
|
164
|
+
'module-metadata',
|
|
165
|
+
'command',
|
|
166
|
+
'worker',
|
|
167
|
+
'page-middleware',
|
|
168
|
+
'setup',
|
|
169
|
+
'encryption',
|
|
170
|
+
'di-registration',
|
|
171
|
+
'custom-entity',
|
|
172
|
+
'ai-extension',
|
|
173
|
+
'generator-plugin',
|
|
174
|
+
] as const
|
|
175
|
+
|
|
176
|
+
export type ModuleOwnedContractKind = (typeof MODULE_OWNED_CONTRACT_KINDS)[number]
|
|
177
|
+
|
|
178
|
+
export type ModuleFactSafeScalar = string | number | boolean | null
|
|
179
|
+
|
|
180
|
+
export type ModuleOwnedContractFact = {
|
|
181
|
+
kind: ModuleOwnedContractKind
|
|
182
|
+
id: string
|
|
183
|
+
source: ModuleFactSourceRef
|
|
184
|
+
metadata?: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type ModuleOwnedContracts = Partial<
|
|
188
|
+
Record<ModuleOwnedContractKind, ModuleOwnedContractFact[]>
|
|
189
|
+
>
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Provenance pointer into a fact section that already serializes the declaration site.
|
|
193
|
+
* `factKey` is omitted when it equals the owning entry's `id`, which is the common case.
|
|
194
|
+
*/
|
|
195
|
+
export type ModuleFactIndexRef = {
|
|
196
|
+
factSection: string
|
|
197
|
+
factKey?: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One provenance entry per proven `(kind, id)` in the module. Exactly one of `source`
|
|
202
|
+
* (the declaration site, for facts that have no other home in the JSON) or `factRef`
|
|
203
|
+
* (a deterministic pointer into a fact section that already carries the source inline)
|
|
204
|
+
* is present, so a consumer resolves any fact's origin through this single index.
|
|
205
|
+
*/
|
|
206
|
+
export type ModuleFactSourceEntry = {
|
|
207
|
+
kind: ModuleFactSourceKind
|
|
208
|
+
id: string
|
|
209
|
+
source?: ModuleFactSourceRef
|
|
210
|
+
factRef?: ModuleFactIndexRef
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type ModuleFactDiagnostic = {
|
|
214
|
+
code: 'duplicate-source' | 'unresolved-static-contract'
|
|
215
|
+
kind: ModuleFactSourceKind
|
|
216
|
+
id: string
|
|
217
|
+
source?: ModuleFactSourceRef
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type ModuleDiRegistrationFact = {
|
|
221
|
+
token: string
|
|
222
|
+
registrationKind: 'function' | 'class' | 'value' | 'alias'
|
|
223
|
+
providerSymbol?: string
|
|
224
|
+
lifetime?: 'singleton' | 'scoped' | 'transient'
|
|
225
|
+
injectionMode?: 'classic' | 'proxy'
|
|
226
|
+
source: ModuleFactSourceRef
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* How the extracted module reached the batch. `package` is the implicit default for
|
|
231
|
+
* every installed `@open-mercato/*` module and never appears in emitted output, so the
|
|
232
|
+
* package sidecar contract stays byte-identical. `local-reference` marks an app-local
|
|
233
|
+
* module projected into the explicit reference bundle; it is an independent discriminator
|
|
234
|
+
* from `sourcePackage`, which keeps its legacy "null means core package" meaning.
|
|
235
|
+
*/
|
|
236
|
+
export type ModuleFactProjectionSourceKind = 'package' | 'local-reference'
|
|
237
|
+
|
|
38
238
|
export interface ModuleFacts {
|
|
39
239
|
module: string
|
|
40
240
|
title: string | null
|
|
41
241
|
description: string | null
|
|
42
242
|
coreVersion: string | null
|
|
243
|
+
sourcePackage: string | null
|
|
244
|
+
sourceVersion: string | null
|
|
245
|
+
sourceRoot: string
|
|
246
|
+
/** Present only for `local-reference` projections; package modules omit it entirely. */
|
|
247
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
43
248
|
entities: ModuleEntityFact[]
|
|
44
249
|
events: ModuleEventFact[]
|
|
45
250
|
aclFeatures: string[]
|
|
@@ -49,6 +254,17 @@ export interface ModuleFacts {
|
|
|
49
254
|
hostTokens: ModuleHostTokens
|
|
50
255
|
notifications: string[]
|
|
51
256
|
cli: string[]
|
|
257
|
+
backendPages: ModulePageFact[]
|
|
258
|
+
frontendPages: ModulePageFact[]
|
|
259
|
+
cliCommands: ModuleCliCommandFact[]
|
|
260
|
+
aiTools: ModuleAiToolFact[]
|
|
261
|
+
aiAgents: ModuleAiAgentFact[]
|
|
262
|
+
extensionSurfaces?: ModuleExtensionSurfaceFacts
|
|
263
|
+
factSources?: ModuleFactSourceEntry[]
|
|
264
|
+
ownedContracts?: ModuleOwnedContracts
|
|
265
|
+
factDiagnostics?: ModuleFactDiagnostic[]
|
|
266
|
+
overrideTargets?: ModuleOverrideTarget[]
|
|
267
|
+
overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
|
|
52
268
|
warnings: string[]
|
|
53
269
|
}
|
|
54
270
|
|
|
@@ -64,8 +280,17 @@ export interface ExtractModuleFactsOptions {
|
|
|
64
280
|
/** Explicit module source directory. When set it overrides `coreSrcRoot + moduleId`. */
|
|
65
281
|
moduleRoot?: string
|
|
66
282
|
coreVersion?: string | null
|
|
283
|
+
sourcePackage?: string | null
|
|
284
|
+
sourceVersion?: string | null
|
|
67
285
|
registryPath?: string | null
|
|
68
286
|
registrySource?: string | null
|
|
287
|
+
/**
|
|
288
|
+
* App-root-relative POSIX root written into every emitted source path. Overrides the
|
|
289
|
+
* default `node_modules/<package>/src/modules/<moduleId>` layout so an app-local module
|
|
290
|
+
* emits `src/modules/<moduleId>` instead of a package path it does not live in.
|
|
291
|
+
*/
|
|
292
|
+
portableSourceRoot?: string
|
|
293
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
69
294
|
}
|
|
70
295
|
|
|
71
296
|
/** A discovered module and the source directory its facts are extracted from. */
|
|
@@ -73,6 +298,9 @@ export interface ModuleFactSource {
|
|
|
73
298
|
moduleId: string
|
|
74
299
|
moduleRoot: string
|
|
75
300
|
from?: string
|
|
301
|
+
packageVersion?: string | null
|
|
302
|
+
portableSourceRoot?: string
|
|
303
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
76
304
|
}
|
|
77
305
|
|
|
78
306
|
function readSourceFile(filePath: string): ts.SourceFile | null {
|
|
@@ -153,6 +381,49 @@ function classHasUpdatedAtColumn(node: ts.ClassDeclaration): boolean {
|
|
|
153
381
|
return false
|
|
154
382
|
}
|
|
155
383
|
|
|
384
|
+
function readStaticAccessPath(candidate: ts.Expression): string[] | null {
|
|
385
|
+
const access = unwrapExpression(candidate)
|
|
386
|
+
if (ts.isIdentifier(access)) return [access.text]
|
|
387
|
+
if (ts.isPropertyAccessExpression(access)) {
|
|
388
|
+
const parent = readStaticAccessPath(access.expression)
|
|
389
|
+
return parent ? [...parent, access.name.text] : null
|
|
390
|
+
}
|
|
391
|
+
if (ts.isElementAccessExpression(access) && access.argumentExpression) {
|
|
392
|
+
const parent = readStaticAccessPath(access.expression)
|
|
393
|
+
const key = unwrapExpression(access.argumentExpression)
|
|
394
|
+
return parent && ts.isStringLiteralLike(key) ? [...parent, key.text] : null
|
|
395
|
+
}
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Resolves an entity-id expression to its literal `module:entity` value.
|
|
401
|
+
*
|
|
402
|
+
* Modules reference entity ids three ways and all three are statically knowable:
|
|
403
|
+
* a string literal, `E.<module>.<entity>` from the generated entity-id registry
|
|
404
|
+
* (`entity-ids.ts` emits `E[module][entity] = '<module>:<entity>'`), and — when
|
|
405
|
+
* `initializers` is supplied — a same-file `const ENTITY_ID = 'module:entity'`.
|
|
406
|
+
* Anything else stays undefined: a guessed id is worse than a missing one.
|
|
407
|
+
*/
|
|
408
|
+
function resolveEntityIdReference(
|
|
409
|
+
expression: ts.Expression,
|
|
410
|
+
initializers?: Map<string, ts.Expression>,
|
|
411
|
+
visitedIdentifiers: Set<string> = new Set(),
|
|
412
|
+
): string | undefined {
|
|
413
|
+
const current = unwrapExpression(expression)
|
|
414
|
+
if (ts.isStringLiteralLike(current)) return current.text
|
|
415
|
+
const accessPath = readStaticAccessPath(current)
|
|
416
|
+
if (accessPath?.length === 3 && accessPath[0] === 'E') {
|
|
417
|
+
return `${accessPath[1]}:${accessPath[2]}`
|
|
418
|
+
}
|
|
419
|
+
if (initializers && ts.isIdentifier(current) && !visitedIdentifiers.has(current.text)) {
|
|
420
|
+
visitedIdentifiers.add(current.text)
|
|
421
|
+
const declaration = initializers.get(current.text)
|
|
422
|
+
if (declaration) return resolveEntityIdReference(declaration, initializers, visitedIdentifiers)
|
|
423
|
+
}
|
|
424
|
+
return undefined
|
|
425
|
+
}
|
|
426
|
+
|
|
156
427
|
function collectCustomFieldEntityIds(ceFilePath: string | null): Set<string> {
|
|
157
428
|
const result = new Set<string>()
|
|
158
429
|
if (!ceFilePath) return result
|
|
@@ -161,8 +432,11 @@ function collectCustomFieldEntityIds(ceFilePath: string | null): Set<string> {
|
|
|
161
432
|
|
|
162
433
|
const visit = (node: ts.Node): void => {
|
|
163
434
|
if (ts.isObjectLiteralExpression(node)) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
435
|
+
const initializer = getObjectPropertyInitializer(node, 'id')
|
|
436
|
+
if (initializer) {
|
|
437
|
+
const id = resolveEntityIdReference(initializer)
|
|
438
|
+
if (id && id.includes(':')) result.add(id)
|
|
439
|
+
}
|
|
166
440
|
}
|
|
167
441
|
node.forEachChild(visit)
|
|
168
442
|
}
|
|
@@ -222,9 +496,38 @@ function getPropertyName(property: ts.ObjectLiteralElementLike): string | undefi
|
|
|
222
496
|
if (!name) return undefined
|
|
223
497
|
if (ts.isIdentifier(name)) return name.text
|
|
224
498
|
if (ts.isStringLiteralLike(name)) return name.text
|
|
499
|
+
// A computed key whose expression is a string literal or a same-file `const NAME = 'token'`
|
|
500
|
+
// is still a statically known token. Without this, `{ [SERVICE_TOKEN]: asFunction(...) }` —
|
|
501
|
+
// the idiomatic, type-safe way to register a DI token from an exported const — yielded
|
|
502
|
+
// undefined and the registration vanished from module facts with NO diagnostic, because the
|
|
503
|
+
// unresolved-token path is only reached for a NAMED token whose Awilix kind is unrecognised.
|
|
504
|
+
if (ts.isComputedPropertyName(name)) return resolveComputedPropertyKey(name.expression)
|
|
225
505
|
return undefined
|
|
226
506
|
}
|
|
227
507
|
|
|
508
|
+
/** Resolves a computed key to its literal token when the value is statically knowable. */
|
|
509
|
+
function resolveComputedPropertyKey(expression: ts.Expression): string | undefined {
|
|
510
|
+
if (ts.isStringLiteralLike(expression)) return expression.text
|
|
511
|
+
// `X as const` / `X satisfies Y` wrappers.
|
|
512
|
+
if (ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) {
|
|
513
|
+
return resolveComputedPropertyKey(expression.expression)
|
|
514
|
+
}
|
|
515
|
+
if (ts.isParenthesizedExpression(expression)) return resolveComputedPropertyKey(expression.expression)
|
|
516
|
+
if (!ts.isIdentifier(expression)) return undefined
|
|
517
|
+
const sourceFile = expression.getSourceFile()
|
|
518
|
+
if (!sourceFile) return undefined
|
|
519
|
+
let resolved: string | undefined
|
|
520
|
+
const visit = (node: ts.Node): void => {
|
|
521
|
+
if (resolved) return
|
|
522
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === expression.text) {
|
|
523
|
+
if (node.initializer) resolved = resolveComputedPropertyKey(node.initializer)
|
|
524
|
+
}
|
|
525
|
+
node.forEachChild(visit)
|
|
526
|
+
}
|
|
527
|
+
sourceFile.forEachChild(visit)
|
|
528
|
+
return resolved
|
|
529
|
+
}
|
|
530
|
+
|
|
228
531
|
function getObjectPropertyInitializer(
|
|
229
532
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
230
533
|
propertyName: string,
|
|
@@ -351,7 +654,11 @@ function extractEvents(eventsFilePath: string | null): ModuleEventFact[] {
|
|
|
351
654
|
category: category ?? null,
|
|
352
655
|
entity: entity ?? null,
|
|
353
656
|
}
|
|
657
|
+
const clientBroadcast = readBooleanPropertyInitializer(element, 'clientBroadcast')
|
|
658
|
+
const portalBroadcast = readBooleanPropertyInitializer(element, 'portalBroadcast')
|
|
354
659
|
if (label !== undefined) fact.label = label
|
|
660
|
+
if (clientBroadcast !== undefined) fact.clientBroadcast = clientBroadcast
|
|
661
|
+
if (portalBroadcast !== undefined) fact.portalBroadcast = portalBroadcast
|
|
355
662
|
facts.push(fact)
|
|
356
663
|
}
|
|
357
664
|
|
|
@@ -459,13 +766,14 @@ function parseApiRouteEntry(entryLiteral: ts.ObjectLiteralExpression): ModuleApi
|
|
|
459
766
|
}
|
|
460
767
|
}
|
|
461
768
|
|
|
462
|
-
return { path: routePath, methods, auth }
|
|
769
|
+
return { path: routePath, methods, auth, sourcePath: null }
|
|
463
770
|
}
|
|
464
771
|
|
|
465
772
|
function extractApiRoutes(
|
|
466
773
|
moduleId: string,
|
|
467
774
|
registrySource: string | null,
|
|
468
775
|
registryDescription: string,
|
|
776
|
+
routeSourcePaths: ReadonlyMap<string, string>,
|
|
469
777
|
warnings: string[],
|
|
470
778
|
): ModuleApiRouteFact[] {
|
|
471
779
|
if (registrySource == null) {
|
|
@@ -493,7 +801,7 @@ function extractApiRoutes(
|
|
|
493
801
|
const route = parseApiRouteEntry(element)
|
|
494
802
|
if (route && !seenPaths.has(route.path)) {
|
|
495
803
|
seenPaths.add(route.path)
|
|
496
|
-
routes.push(route)
|
|
804
|
+
routes.push({ ...route, sourcePath: routeSourcePaths.get(route.path) ?? null })
|
|
497
805
|
}
|
|
498
806
|
}
|
|
499
807
|
}
|
|
@@ -581,15 +889,29 @@ function extractSearchEntities(searchFilePath: string | null, warnings: string[]
|
|
|
581
889
|
return []
|
|
582
890
|
}
|
|
583
891
|
|
|
892
|
+
// `entityId` is rarely a bare string literal: modules reference the entity through
|
|
893
|
+
// `E.<module>.<entity>` or a same-file `const ENTITY_ID`. Reading only string literals
|
|
894
|
+
// dropped every such entry with NO diagnostic, so a module shipping `search.ts` scored
|
|
895
|
+
// zero search facts while looking healthy. Unresolvable entries now warn instead.
|
|
896
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
584
897
|
const entityIds: string[] = []
|
|
585
898
|
const seen = new Set<string>()
|
|
586
899
|
for (const element of entitiesArray.elements) {
|
|
587
900
|
if (!ts.isObjectLiteralExpression(element)) continue
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
901
|
+
const initializer = getObjectPropertyInitializer(element, 'entityId')
|
|
902
|
+
const location = `${searchFilePath}:${nodeLine(sourceFile, element)}`
|
|
903
|
+
if (!initializer) {
|
|
904
|
+
warnings.push(`[module-facts] searchConfig entity declares no entityId: ${location}`)
|
|
905
|
+
continue
|
|
906
|
+
}
|
|
907
|
+
const entityId = resolveEntityIdReference(initializer, initializers)
|
|
908
|
+
if (!entityId) {
|
|
909
|
+
warnings.push(`[module-facts] searchConfig entityId is not statically resolvable: ${location}`)
|
|
910
|
+
continue
|
|
592
911
|
}
|
|
912
|
+
if (seen.has(entityId)) continue
|
|
913
|
+
seen.add(entityId)
|
|
914
|
+
entityIds.push(entityId)
|
|
593
915
|
}
|
|
594
916
|
return entityIds
|
|
595
917
|
}
|
|
@@ -666,36 +988,261 @@ function extractCli(cliFilePath: string | null, warnings: string[]): string[] {
|
|
|
666
988
|
function listSourceFilesRecursive(directory: string): string[] {
|
|
667
989
|
const files: string[] = []
|
|
668
990
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
669
|
-
if (entry.name === '__tests__' || entry.name === 'node_modules') continue
|
|
991
|
+
if (entry.name === '__tests__' || entry.name === '__mocks__' || entry.name === 'node_modules') continue
|
|
670
992
|
const fullPath = path.join(directory, entry.name)
|
|
671
993
|
if (entry.isDirectory()) {
|
|
672
994
|
files.push(...listSourceFilesRecursive(fullPath))
|
|
673
|
-
} else if (
|
|
995
|
+
} else if (
|
|
996
|
+
/\.tsx?$/.test(entry.name)
|
|
997
|
+
&& !entry.name.endsWith('.d.ts')
|
|
998
|
+
&& !/\.(?:test|spec)\.tsx?$/.test(entry.name)
|
|
999
|
+
) {
|
|
674
1000
|
files.push(fullPath)
|
|
675
1001
|
}
|
|
676
1002
|
}
|
|
677
1003
|
return files
|
|
678
1004
|
}
|
|
679
1005
|
|
|
680
|
-
function
|
|
681
|
-
|
|
1006
|
+
function toPortableSourceRoot(moduleId: string, sourcePackage: string | null): string {
|
|
1007
|
+
return path.posix.join('node_modules', sourcePackage ?? '@open-mercato/core', 'src', 'modules', moduleId)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* An explicit portable root must stay app-root-relative and POSIX-shaped: every emitted
|
|
1012
|
+
* fact path and markdown link is resolved against the generated app root, so an absolute
|
|
1013
|
+
* path, a Windows separator, or a traversal segment would emit an unclickable or
|
|
1014
|
+
* machine-specific reference into a distributed artifact.
|
|
1015
|
+
*/
|
|
1016
|
+
export function assertPortableSourceRoot(moduleId: string, portableSourceRoot: string): string {
|
|
1017
|
+
const invalid = portableSourceRoot.length === 0
|
|
1018
|
+
|| path.posix.isAbsolute(portableSourceRoot)
|
|
1019
|
+
|| /^[A-Za-z]:/.test(portableSourceRoot)
|
|
1020
|
+
|| portableSourceRoot.includes('\\')
|
|
1021
|
+
|| portableSourceRoot.split('/').includes('..')
|
|
1022
|
+
if (invalid) {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
`[module-facts] portableSourceRoot for module "${moduleId}" must be a relative POSIX app path, received "${portableSourceRoot}"`,
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
return portableSourceRoot
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function toPortableSourcePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
|
|
1031
|
+
const relativePath = path.relative(moduleRoot, filePath).split(path.sep).join('/')
|
|
1032
|
+
return path.posix.join(sourceRoot, relativePath)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function sourceHasDefaultExport(sourceFile: ts.SourceFile): boolean {
|
|
1036
|
+
return sourceFile.statements.some((statement) => {
|
|
1037
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) return true
|
|
1038
|
+
if (!ts.isFunctionDeclaration(statement) && !ts.isClassDeclaration(statement)) return false
|
|
1039
|
+
return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
1040
|
+
})
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function readRouteMetadataPath(sourceFile: ts.SourceFile): string | null {
|
|
1044
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1045
|
+
return metadata ? readStringPropertyInitializer(metadata, 'path') ?? null : null
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function extractApiRouteSourcePaths(
|
|
1049
|
+
moduleId: string,
|
|
1050
|
+
moduleRoot: string,
|
|
1051
|
+
sourceRoot: string,
|
|
1052
|
+
): Map<string, string> {
|
|
1053
|
+
const apiRoot = path.join(moduleRoot, 'api')
|
|
1054
|
+
const sources = new Map<string, string>()
|
|
1055
|
+
if (!fs.existsSync(apiRoot)) return sources
|
|
1056
|
+
const methodDirectories = new Set(['get', 'post', 'put', 'patch', 'delete'])
|
|
1057
|
+
|
|
1058
|
+
for (const filePath of listSourceFilesRecursive(apiRoot)) {
|
|
1059
|
+
const relativePath = path.relative(apiRoot, filePath).split(path.sep).join('/')
|
|
1060
|
+
const segments = relativePath.split('/')
|
|
1061
|
+
const fileName = segments.pop() as string
|
|
1062
|
+
const fileStem = fileName.replace(/\.tsx?$/, '')
|
|
1063
|
+
let routeSegments: string[]
|
|
1064
|
+
if (fileStem === 'route') {
|
|
1065
|
+
routeSegments = segments
|
|
1066
|
+
} else if (segments[0] && methodDirectories.has(segments[0].toLowerCase())) {
|
|
1067
|
+
routeSegments = [...segments.slice(1), fileStem]
|
|
1068
|
+
} else {
|
|
1069
|
+
routeSegments = [...segments, fileStem]
|
|
1070
|
+
}
|
|
1071
|
+
const defaultPath = `/${[moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
1072
|
+
const sourceFile = readSourceFile(filePath)
|
|
1073
|
+
const routePath = sourceFile ? readRouteMetadataPath(sourceFile) ?? defaultPath : defaultPath
|
|
1074
|
+
sources.set(routePath, toPortableSourcePath(moduleRoot, sourceRoot, filePath))
|
|
1075
|
+
}
|
|
1076
|
+
return sources
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function extractModulePages(
|
|
1080
|
+
moduleId: string,
|
|
1081
|
+
moduleRoot: string,
|
|
1082
|
+
sourceRoot: string,
|
|
1083
|
+
surface: 'backend' | 'frontend',
|
|
1084
|
+
): ModulePageFact[] {
|
|
1085
|
+
const pageRoot = path.join(moduleRoot, surface)
|
|
1086
|
+
if (!fs.existsSync(pageRoot)) return []
|
|
1087
|
+
const pages: ModulePageFact[] = []
|
|
1088
|
+
const seen = new Set<string>()
|
|
1089
|
+
|
|
1090
|
+
for (const filePath of listSourceFilesRecursive(pageRoot)) {
|
|
1091
|
+
if (!filePath.endsWith('.tsx')) continue
|
|
1092
|
+
const relativePath = path.relative(pageRoot, filePath).split(path.sep).join('/')
|
|
1093
|
+
const segments = relativePath.split('/')
|
|
1094
|
+
const fileName = segments.pop() as string
|
|
1095
|
+
const fileStem = fileName.replace(/\.tsx$/, '')
|
|
1096
|
+
const isModernPage = fileStem === 'page'
|
|
1097
|
+
if (!isModernPage && (fileStem === 'meta' || fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
|
|
1098
|
+
const sourceFile = readSourceFile(filePath)
|
|
1099
|
+
if (!sourceFile || !sourceHasDefaultExport(sourceFile)) continue
|
|
1100
|
+
const routeSegments = isModernPage ? segments : [...segments, fileStem]
|
|
1101
|
+
const routePath = surface === 'frontend'
|
|
1102
|
+
? `/${routeSegments.filter(Boolean).join('/')}`
|
|
1103
|
+
: isModernPage
|
|
1104
|
+
? `/backend/${routeSegments.join('/') || moduleId}`
|
|
1105
|
+
: `/backend/${routeSegments[0] === moduleId
|
|
1106
|
+
? routeSegments.filter(Boolean).join('/')
|
|
1107
|
+
: [moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
1108
|
+
if (seen.has(routePath)) continue
|
|
1109
|
+
seen.add(routePath)
|
|
1110
|
+
const metadataFilePath = resolvePageMetadataFile(path.dirname(filePath))
|
|
1111
|
+
const metadataSourceFile = metadataFilePath ? readSourceFile(metadataFilePath) : sourceFile
|
|
1112
|
+
const metadata = metadataSourceFile ? readPageMetadata(metadataSourceFile) : undefined
|
|
1113
|
+
pages.push({
|
|
1114
|
+
path: routePath,
|
|
1115
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
1116
|
+
...(metadataFilePath
|
|
1117
|
+
? { metadataSourcePath: toPortableSourcePath(moduleRoot, sourceRoot, metadataFilePath) }
|
|
1118
|
+
: {}),
|
|
1119
|
+
...(metadata ? { metadata } : {}),
|
|
1120
|
+
})
|
|
1121
|
+
}
|
|
1122
|
+
return pages.sort((left, right) => left.path.localeCompare(right.path))
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function findAncestorVariableDeclaration(node: ts.Node): ts.VariableDeclaration | null {
|
|
1126
|
+
let current: ts.Node | undefined = node.parent
|
|
1127
|
+
while (current) {
|
|
1128
|
+
if (ts.isVariableDeclaration(current)) return current
|
|
1129
|
+
current = current.parent
|
|
1130
|
+
}
|
|
1131
|
+
return null
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function findAncestorCallExpression(node: ts.Node): ts.CallExpression | null {
|
|
1135
|
+
let current: ts.Node | undefined = node.parent
|
|
1136
|
+
while (current) {
|
|
1137
|
+
if (ts.isCallExpression(current)) return current
|
|
1138
|
+
if (ts.isVariableDeclaration(current) || ts.isStatement(current)) return null
|
|
1139
|
+
current = current.parent
|
|
1140
|
+
}
|
|
1141
|
+
return null
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function extractNamedAiFacts(
|
|
1145
|
+
filePaths: readonly string[],
|
|
1146
|
+
moduleRoot: string,
|
|
1147
|
+
sourceRoot: string,
|
|
1148
|
+
propertyName: 'name' | 'id',
|
|
1149
|
+
): Array<{ value: string; sourcePath: string }> {
|
|
1150
|
+
const facts: Array<{ value: string; sourcePath: string }> = []
|
|
1151
|
+
const seen = new Set<string>()
|
|
1152
|
+
for (const filePath of filePaths) {
|
|
1153
|
+
const sourceFile = readSourceFile(filePath)
|
|
1154
|
+
if (!sourceFile) continue
|
|
1155
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1156
|
+
const readDefinitionValue = (objectLiteral: ts.ObjectLiteralExpression): string | undefined => {
|
|
1157
|
+
const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
|
|
1158
|
+
if (!initializer) return undefined
|
|
1159
|
+
const resolved = unwrapExpression(initializer)
|
|
1160
|
+
if (ts.isStringLiteralLike(resolved)) return resolved.text
|
|
1161
|
+
if (ts.isIdentifier(resolved)) {
|
|
1162
|
+
const declarationInitializer = initializers.get(resolved.text)
|
|
1163
|
+
if (declarationInitializer) {
|
|
1164
|
+
const declarationValue = unwrapExpression(declarationInitializer)
|
|
1165
|
+
if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return undefined
|
|
1169
|
+
}
|
|
1170
|
+
const visit = (node: ts.Node): void => {
|
|
1171
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1172
|
+
const value = readDefinitionValue(node)
|
|
1173
|
+
if (value && !seen.has(value)) {
|
|
1174
|
+
const declaration = findAncestorVariableDeclaration(node)
|
|
1175
|
+
const declarationName = declaration && ts.isIdentifier(declaration.name) ? declaration.name.text : ''
|
|
1176
|
+
const declarationType = declaration?.type?.getText(sourceFile) ?? ''
|
|
1177
|
+
const call = findAncestorCallExpression(node)
|
|
1178
|
+
const callee = call?.expression.getText(sourceFile) ?? ''
|
|
1179
|
+
const isDefinition = propertyName === 'name'
|
|
1180
|
+
? /AiTool/.test(declarationType) || /tool$/i.test(declarationName) || /AiTool/.test(callee)
|
|
1181
|
+
: /AiAgentDefinition/.test(declarationType) || /agent$/i.test(declarationName) || /defineAiAgent/.test(callee)
|
|
1182
|
+
if (isDefinition) {
|
|
1183
|
+
seen.add(value)
|
|
1184
|
+
facts.push({
|
|
1185
|
+
value,
|
|
1186
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
1187
|
+
})
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
node.forEachChild(visit)
|
|
1192
|
+
}
|
|
1193
|
+
sourceFile.forEachChild(visit)
|
|
1194
|
+
}
|
|
1195
|
+
return facts.sort((left, right) => left.value.localeCompare(right.value))
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function extractAiTools(moduleRoot: string, sourceRoot: string): ModuleAiToolFact[] {
|
|
1199
|
+
const files = new Set<string>()
|
|
1200
|
+
const rootFile = resolveConventionFile(moduleRoot, 'ai-tools')
|
|
1201
|
+
if (rootFile) files.add(rootFile)
|
|
1202
|
+
const toolsDirectory = path.join(moduleRoot, 'ai-tools')
|
|
1203
|
+
if (fs.existsSync(toolsDirectory)) {
|
|
1204
|
+
for (const filePath of listSourceFilesRecursive(toolsDirectory)) files.add(filePath)
|
|
1205
|
+
}
|
|
1206
|
+
return extractNamedAiFacts([...files], moduleRoot, sourceRoot, 'name')
|
|
1207
|
+
.map((fact) => ({ name: fact.value, sourcePath: fact.sourcePath }))
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function extractAiAgents(moduleRoot: string, sourceRoot: string): ModuleAiAgentFact[] {
|
|
1211
|
+
const agentsFile = resolveConventionFile(moduleRoot, 'ai-agents')
|
|
1212
|
+
if (!agentsFile) return []
|
|
1213
|
+
return extractNamedAiFacts([agentsFile], moduleRoot, sourceRoot, 'id')
|
|
1214
|
+
.map((fact) => ({ id: fact.value, sourcePath: fact.sourcePath }))
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function extractTableIds(moduleRoot: string): string[] {
|
|
1218
|
+
if (!fs.existsSync(moduleRoot)) return []
|
|
682
1219
|
|
|
683
1220
|
const tableIds: string[] = []
|
|
684
1221
|
const seen = new Set<string>()
|
|
685
|
-
|
|
1222
|
+
const collectLiteralValues = (expression: ts.Expression): string[] => {
|
|
1223
|
+
const current = unwrapExpression(expression)
|
|
1224
|
+
if (ts.isStringLiteralLike(current)) return [current.text]
|
|
1225
|
+
if (ts.isConditionalExpression(current)) {
|
|
1226
|
+
return [
|
|
1227
|
+
...collectLiteralValues(current.whenTrue),
|
|
1228
|
+
...collectLiteralValues(current.whenFalse),
|
|
1229
|
+
]
|
|
1230
|
+
}
|
|
1231
|
+
return []
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
for (const filePath of listSourceFilesRecursive(moduleRoot)) {
|
|
686
1235
|
const sourceFile = readSourceFile(filePath)
|
|
687
1236
|
if (!sourceFile) continue
|
|
688
1237
|
const visit = (node: ts.Node): void => {
|
|
689
1238
|
if (ts.isPropertyAssignment(node)) {
|
|
690
1239
|
const propertyName = getPropertyName(node)
|
|
691
|
-
if (
|
|
692
|
-
(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
seen.add(value)
|
|
698
|
-
tableIds.push(value)
|
|
1240
|
+
if (propertyName === 'tableId' || propertyName === 'extensionTableId') {
|
|
1241
|
+
for (const value of collectLiteralValues(node.initializer)) {
|
|
1242
|
+
if (value && !seen.has(value)) {
|
|
1243
|
+
seen.add(value)
|
|
1244
|
+
tableIds.push(value)
|
|
1245
|
+
}
|
|
699
1246
|
}
|
|
700
1247
|
}
|
|
701
1248
|
}
|
|
@@ -723,130 +1270,1227 @@ function extractModuleMeta(indexFilePath: string | null): { title: string | null
|
|
|
723
1270
|
}
|
|
724
1271
|
}
|
|
725
1272
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
?? (options.coreSrcRoot ? path.join(options.coreSrcRoot, moduleId) : null)
|
|
730
|
-
if (!moduleRoot) {
|
|
731
|
-
throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
|
|
732
|
-
}
|
|
1273
|
+
function nodeLine(sourceFile: ts.SourceFile, node: ts.Node): number {
|
|
1274
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
|
|
1275
|
+
}
|
|
733
1276
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const ceFilePath = resolveConventionFile(moduleRoot, 'ce')
|
|
739
|
-
const eventsFilePath = resolveConventionFile(moduleRoot, 'events')
|
|
740
|
-
const aclFilePath = resolveConventionFile(moduleRoot, 'acl')
|
|
741
|
-
const diFilePath = resolveConventionFile(moduleRoot, 'di')
|
|
742
|
-
const searchFilePath = resolveConventionFile(moduleRoot, 'search')
|
|
743
|
-
const notificationsFilePath = resolveConventionFile(moduleRoot, 'notifications')
|
|
744
|
-
const cliFilePath = resolveConventionFile(moduleRoot, 'cli')
|
|
745
|
-
const indexFilePath = resolveConventionFile(moduleRoot, 'index')
|
|
746
|
-
const backendDir = path.join(moduleRoot, 'backend')
|
|
1277
|
+
function readPropertyNameText(name: ts.PropertyName): string | undefined {
|
|
1278
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text
|
|
1279
|
+
return undefined
|
|
1280
|
+
}
|
|
747
1281
|
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1282
|
+
function readMethodBearingPropertyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
|
|
1283
|
+
const names: string[] = []
|
|
1284
|
+
for (const property of objectLiteral.properties) {
|
|
1285
|
+
if (ts.isMethodDeclaration(property)) {
|
|
1286
|
+
const name = readPropertyNameText(property.name)
|
|
1287
|
+
if (name) names.push(name)
|
|
1288
|
+
continue
|
|
1289
|
+
}
|
|
1290
|
+
if (ts.isPropertyAssignment(property)) {
|
|
1291
|
+
const name = readPropertyNameText(property.name)
|
|
1292
|
+
if (!name) continue
|
|
1293
|
+
const initializer = unwrapExpression(property.initializer)
|
|
1294
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) names.push(name)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return names.sort((left, right) => left.localeCompare(right))
|
|
1298
|
+
}
|
|
754
1299
|
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const cli = extractCli(cliFilePath, warnings)
|
|
761
|
-
const hostTokens: ModuleHostTokens = {
|
|
762
|
-
entityIds: extractHostEntityIds(entities),
|
|
763
|
-
tableIds: extractTableIds(backendDir),
|
|
1300
|
+
function readObjectKeyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
|
|
1301
|
+
const names: string[] = []
|
|
1302
|
+
for (const property of objectLiteral.properties) {
|
|
1303
|
+
const name = getPropertyName(property)
|
|
1304
|
+
if (name) names.push(name)
|
|
764
1305
|
}
|
|
1306
|
+
return names.sort((left, right) => left.localeCompare(right))
|
|
1307
|
+
}
|
|
765
1308
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
notifications,
|
|
779
|
-
cli,
|
|
780
|
-
warnings,
|
|
1309
|
+
function mapDiRegistrationKind(base: string | null): ModuleDiRegistrationFact['registrationKind'] | null {
|
|
1310
|
+
switch (base) {
|
|
1311
|
+
case 'asFunction':
|
|
1312
|
+
return 'function'
|
|
1313
|
+
case 'asClass':
|
|
1314
|
+
return 'class'
|
|
1315
|
+
case 'asValue':
|
|
1316
|
+
return 'value'
|
|
1317
|
+
case 'aliasTo':
|
|
1318
|
+
return 'alias'
|
|
1319
|
+
default:
|
|
1320
|
+
return null
|
|
781
1321
|
}
|
|
782
1322
|
}
|
|
783
1323
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1324
|
+
function readDiChainMetadata(initializer: ts.Expression): {
|
|
1325
|
+
lifetime?: ModuleDiRegistrationFact['lifetime']
|
|
1326
|
+
injectionMode?: ModuleDiRegistrationFact['injectionMode']
|
|
1327
|
+
} {
|
|
1328
|
+
const result: {
|
|
1329
|
+
lifetime?: ModuleDiRegistrationFact['lifetime']
|
|
1330
|
+
injectionMode?: ModuleDiRegistrationFact['injectionMode']
|
|
1331
|
+
} = {}
|
|
1332
|
+
let current: ts.Expression = unwrapExpression(initializer)
|
|
1333
|
+
while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
1334
|
+
const method = current.expression.name.text
|
|
1335
|
+
if ((method === 'singleton' || method === 'scoped' || method === 'transient') && !result.lifetime) {
|
|
1336
|
+
result.lifetime = method
|
|
1337
|
+
} else if (method === 'proxy' && !result.injectionMode) {
|
|
1338
|
+
result.injectionMode = 'proxy'
|
|
1339
|
+
} else if (method === 'classic' && !result.injectionMode) {
|
|
1340
|
+
result.injectionMode = 'classic'
|
|
1341
|
+
}
|
|
1342
|
+
current = unwrapExpression(current.expression.expression)
|
|
1343
|
+
}
|
|
1344
|
+
return result
|
|
788
1345
|
}
|
|
789
1346
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1347
|
+
function readDiProviderSymbol(
|
|
1348
|
+
initializer: ts.Expression,
|
|
1349
|
+
kind: ModuleDiRegistrationFact['registrationKind'],
|
|
1350
|
+
): string | undefined {
|
|
1351
|
+
let current: ts.Expression = unwrapExpression(initializer)
|
|
1352
|
+
while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
1353
|
+
current = unwrapExpression(current.expression.expression)
|
|
1354
|
+
}
|
|
1355
|
+
if (!ts.isCallExpression(current)) return undefined
|
|
1356
|
+
const argument = current.arguments[0]
|
|
1357
|
+
if (!argument) return undefined
|
|
1358
|
+
if (kind === 'value') return undefined
|
|
1359
|
+
if (kind === 'alias' && ts.isStringLiteralLike(argument)) return argument.text
|
|
1360
|
+
if ((kind === 'function' || kind === 'class') && ts.isIdentifier(argument)) return argument.text
|
|
1361
|
+
return undefined
|
|
803
1362
|
}
|
|
804
1363
|
|
|
805
|
-
|
|
1364
|
+
function extractDiRegistrationFacts(
|
|
1365
|
+
diFilePath: string | null,
|
|
1366
|
+
sourcePath: string,
|
|
1367
|
+
): { facts: ModuleDiRegistrationFact[]; unresolvedTokens: string[] } {
|
|
1368
|
+
if (!diFilePath) return { facts: [], unresolvedTokens: [] }
|
|
1369
|
+
const sourceFile = readSourceFile(diFilePath)
|
|
1370
|
+
if (!sourceFile) return { facts: [], unresolvedTokens: [] }
|
|
806
1371
|
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
1372
|
+
const facts: ModuleDiRegistrationFact[] = []
|
|
1373
|
+
const unresolvedTokens: string[] = []
|
|
1374
|
+
const visit = (node: ts.Node): void => {
|
|
1375
|
+
if (
|
|
1376
|
+
ts.isCallExpression(node) &&
|
|
1377
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
1378
|
+
node.expression.name.text === 'register'
|
|
1379
|
+
) {
|
|
1380
|
+
const argument = node.arguments[0]
|
|
1381
|
+
if (argument && ts.isObjectLiteralExpression(argument)) {
|
|
1382
|
+
for (const property of argument.properties) {
|
|
1383
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
1384
|
+
const token = getPropertyName(property)
|
|
1385
|
+
if (!token) continue
|
|
1386
|
+
const kind = mapDiRegistrationKind(detectAwilixRegistrationKind(property.initializer))
|
|
1387
|
+
if (!kind) {
|
|
1388
|
+
if (!unresolvedTokens.includes(token)) unresolvedTokens.push(token)
|
|
1389
|
+
continue
|
|
1390
|
+
}
|
|
1391
|
+
const chain = readDiChainMetadata(property.initializer)
|
|
1392
|
+
const providerSymbol = readDiProviderSymbol(property.initializer, kind)
|
|
1393
|
+
facts.push({
|
|
1394
|
+
token,
|
|
1395
|
+
registrationKind: kind,
|
|
1396
|
+
...(providerSymbol ? { providerSymbol } : {}),
|
|
1397
|
+
...(chain.lifetime ? { lifetime: chain.lifetime } : {}),
|
|
1398
|
+
...(chain.injectionMode ? { injectionMode: chain.injectionMode } : {}),
|
|
1399
|
+
source: { sourcePath, exportName: token, line: nodeLine(sourceFile, property) },
|
|
1400
|
+
})
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
node.forEachChild(visit)
|
|
1405
|
+
}
|
|
1406
|
+
sourceFile.forEachChild(visit)
|
|
1407
|
+
return { facts, unresolvedTokens }
|
|
810
1408
|
}
|
|
811
1409
|
|
|
812
|
-
function
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
(
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1410
|
+
function deriveLegacyDiTokens(facts: readonly ModuleDiRegistrationFact[]): string[] {
|
|
1411
|
+
const tokens: string[] = []
|
|
1412
|
+
const seen = new Set<string>()
|
|
1413
|
+
for (const fact of facts) {
|
|
1414
|
+
if (fact.registrationKind !== 'function' && fact.registrationKind !== 'class') continue
|
|
1415
|
+
if (seen.has(fact.token)) continue
|
|
1416
|
+
seen.add(fact.token)
|
|
1417
|
+
tokens.push(fact.token)
|
|
1418
|
+
}
|
|
1419
|
+
return tokens
|
|
821
1420
|
}
|
|
822
1421
|
|
|
823
|
-
function
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1422
|
+
function dedupeOwnedContractFacts(facts: readonly ModuleOwnedContractFact[]): ModuleOwnedContractFact[] {
|
|
1423
|
+
const byId = new Map<string, ModuleOwnedContractFact>()
|
|
1424
|
+
for (const fact of facts) {
|
|
1425
|
+
const existing = byId.get(fact.id)
|
|
1426
|
+
if (!existing) {
|
|
1427
|
+
byId.set(fact.id, fact)
|
|
1428
|
+
continue
|
|
1429
|
+
}
|
|
1430
|
+
if (fact.source.sourcePath.localeCompare(existing.source.sourcePath) < 0) byId.set(fact.id, fact)
|
|
1431
|
+
}
|
|
1432
|
+
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id))
|
|
830
1433
|
}
|
|
831
1434
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1435
|
+
const PAGE_CONTEXT_VALUES = new Set(['main', 'admin', 'settings', 'profile'])
|
|
1436
|
+
|
|
1437
|
+
function readAliasedStringProperty(
|
|
1438
|
+
metadata: ts.ObjectLiteralExpression,
|
|
1439
|
+
preferred: string,
|
|
1440
|
+
fallback: string,
|
|
1441
|
+
): string | undefined {
|
|
1442
|
+
return readStringPropertyInitializer(metadata, preferred) ?? readStringPropertyInitializer(metadata, fallback)
|
|
835
1443
|
}
|
|
836
1444
|
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
if (
|
|
840
|
-
|
|
841
|
-
if (rule.requireAuth) return 'auth'
|
|
842
|
-
return 'public'
|
|
1445
|
+
function readNumericProperty(metadata: ts.ObjectLiteralExpression, propertyName: string): number | undefined {
|
|
1446
|
+
const initializer = getObjectPropertyInitializer(metadata, propertyName)
|
|
1447
|
+
if (!initializer || !ts.isNumericLiteral(initializer)) return undefined
|
|
1448
|
+
return Number(initializer.text)
|
|
843
1449
|
}
|
|
844
1450
|
|
|
845
|
-
function
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1451
|
+
function readPageMetadata(sourceFile: ts.SourceFile): ModulePageMetadata | undefined {
|
|
1452
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1453
|
+
if (!metadata) return undefined
|
|
1454
|
+
const result: ModulePageMetadata = {}
|
|
1455
|
+
const requireAuth = readBooleanPropertyInitializer(metadata, 'requireAuth')
|
|
1456
|
+
if (requireAuth !== undefined) result.requireAuth = requireAuth
|
|
1457
|
+
const requireCustomerAuth = readBooleanPropertyInitializer(metadata, 'requireCustomerAuth')
|
|
1458
|
+
if (requireCustomerAuth !== undefined) result.requireCustomerAuth = requireCustomerAuth
|
|
1459
|
+
const requireFeatures = readStringArrayPropertyInitializer(metadata, 'requireFeatures')
|
|
1460
|
+
if (requireFeatures && requireFeatures.length > 0) result.requireFeatures = requireFeatures
|
|
1461
|
+
const requireCustomerFeatures = readStringArrayPropertyInitializer(metadata, 'requireCustomerFeatures')
|
|
1462
|
+
if (requireCustomerFeatures && requireCustomerFeatures.length > 0) {
|
|
1463
|
+
result.requireCustomerFeatures = requireCustomerFeatures
|
|
1464
|
+
}
|
|
1465
|
+
const navHidden = readBooleanPropertyInitializer(metadata, 'navHidden')
|
|
1466
|
+
if (navHidden !== undefined) result.navHidden = navHidden
|
|
1467
|
+
const navGroup = readAliasedStringProperty(metadata, 'pageGroup', 'group')
|
|
1468
|
+
if (navGroup) result.navGroup = navGroup
|
|
1469
|
+
const navGroupKey = readAliasedStringProperty(metadata, 'pageGroupKey', 'groupKey')
|
|
1470
|
+
if (navGroupKey) result.navGroupKey = navGroupKey
|
|
1471
|
+
const navOrder = readNumericProperty(metadata, 'pageOrder') ?? readNumericProperty(metadata, 'order')
|
|
1472
|
+
if (navOrder !== undefined) result.navOrder = navOrder
|
|
1473
|
+
const pageContext = readStringPropertyInitializer(metadata, 'pageContext')
|
|
1474
|
+
if (pageContext && PAGE_CONTEXT_VALUES.has(pageContext)) {
|
|
1475
|
+
result.pageContext = pageContext as ModulePageMetadata['pageContext']
|
|
1476
|
+
}
|
|
1477
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Mirrors the runtime companion-file convention: a page's metadata comes from
|
|
1482
|
+
* `page.meta.*` (or `meta.*`) in the page directory when one exists, and only
|
|
1483
|
+
* otherwise from the page component itself.
|
|
1484
|
+
*/
|
|
1485
|
+
function resolvePageMetadataFile(pageDirectory: string): string | null {
|
|
1486
|
+
for (const baseName of ['page.meta', 'meta']) {
|
|
1487
|
+
for (const extension of MODULE_CODE_EXTENSIONS) {
|
|
1488
|
+
const candidate = path.join(pageDirectory, `${baseName}${extension}`)
|
|
1489
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return null
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function extractModuleMetadataContract(
|
|
1496
|
+
indexFilePath: string | null,
|
|
1497
|
+
moduleId: string,
|
|
1498
|
+
sourcePath: string | null,
|
|
1499
|
+
): ModuleOwnedContractFact | null {
|
|
1500
|
+
if (!indexFilePath || !sourcePath) return null
|
|
1501
|
+
const sourceFile = readSourceFile(indexFilePath)
|
|
1502
|
+
if (!sourceFile) return null
|
|
1503
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1504
|
+
if (!metadata) return null
|
|
1505
|
+
const facts: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1506
|
+
const name = readStringPropertyInitializer(metadata, 'name')
|
|
1507
|
+
if (name) facts.name = name
|
|
1508
|
+
const version = readStringPropertyInitializer(metadata, 'version')
|
|
1509
|
+
if (version) facts.version = version
|
|
1510
|
+
const requires = readStringArrayPropertyInitializer(metadata, 'requires')
|
|
1511
|
+
if (requires && requires.length > 0) facts.requires = requires
|
|
1512
|
+
const ejectable = readBooleanPropertyInitializer(metadata, 'ejectable')
|
|
1513
|
+
if (ejectable !== undefined) facts.ejectable = ejectable
|
|
1514
|
+
return {
|
|
1515
|
+
kind: 'module-metadata',
|
|
1516
|
+
id: moduleId,
|
|
1517
|
+
source: { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) },
|
|
1518
|
+
...(Object.keys(facts).length > 0 ? { metadata: facts } : {}),
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function extractCommandContractCandidates(
|
|
1523
|
+
moduleId: string,
|
|
1524
|
+
moduleRoot: string,
|
|
1525
|
+
sourceRoot: string,
|
|
1526
|
+
): Array<{ id: string; sourcePath: string }> {
|
|
1527
|
+
const commandsRoot = path.join(moduleRoot, 'commands')
|
|
1528
|
+
if (!fs.existsSync(commandsRoot)) return []
|
|
1529
|
+
const candidates: Array<{ id: string; sourcePath: string }> = []
|
|
1530
|
+
for (const filePath of listSourceFilesRecursive(commandsRoot)) {
|
|
1531
|
+
if (path.basename(filePath).replace(/\.tsx?$/, '') === 'interceptors') continue
|
|
1532
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1533
|
+
for (const id of extractCommandIdsFromSource(filePath)) candidates.push({ id, sourcePath })
|
|
1534
|
+
}
|
|
1535
|
+
return candidates
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* Runtime worker id: `metadata.id` when declared, otherwise the path-derived
|
|
1540
|
+
* `<moduleId>:workers:<...segments>:<fileStem>` that `discoverWorkers` builds.
|
|
1541
|
+
*/
|
|
1542
|
+
function deriveWorkerId(moduleId: string, workersRoot: string, filePath: string): string {
|
|
1543
|
+
const relativePath = path.relative(workersRoot, filePath).split(path.sep).join('/')
|
|
1544
|
+
const segments = relativePath.split('/')
|
|
1545
|
+
const fileName = segments.pop() as string
|
|
1546
|
+
const fileStem = fileName.replace(/\.tsx?$/, '')
|
|
1547
|
+
return [moduleId, 'workers', ...segments, fileStem].filter(Boolean).join(':')
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** Resolves a string property directly or through a local `const` in the same file. */
|
|
1551
|
+
function readResolvedStringProperty(
|
|
1552
|
+
objectLiteral: ts.ObjectLiteralExpression,
|
|
1553
|
+
propertyName: string,
|
|
1554
|
+
initializers: Map<string, ts.Expression>,
|
|
1555
|
+
): string | undefined {
|
|
1556
|
+
const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
|
|
1557
|
+
if (!initializer) return undefined
|
|
1558
|
+
const resolved = unwrapExpression(initializer)
|
|
1559
|
+
if (ts.isStringLiteralLike(resolved)) return resolved.text
|
|
1560
|
+
if (ts.isIdentifier(resolved)) {
|
|
1561
|
+
const declaration = initializers.get(resolved.text)
|
|
1562
|
+
if (declaration) {
|
|
1563
|
+
const declarationValue = unwrapExpression(declaration)
|
|
1564
|
+
if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return undefined
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function extractWorkerContracts(
|
|
1571
|
+
moduleId: string,
|
|
1572
|
+
moduleRoot: string,
|
|
1573
|
+
sourceRoot: string,
|
|
1574
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1575
|
+
const workersRoot = path.join(moduleRoot, 'workers')
|
|
1576
|
+
if (!fs.existsSync(workersRoot)) return { facts: [], unresolved: [] }
|
|
1577
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1578
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1579
|
+
for (const filePath of listSourceFilesRecursive(workersRoot)) {
|
|
1580
|
+
const sourceFile = readSourceFile(filePath)
|
|
1581
|
+
if (!sourceFile) continue
|
|
1582
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1583
|
+
if (!metadata) continue
|
|
1584
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1585
|
+
const source = { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) }
|
|
1586
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1587
|
+
const workerId = readResolvedStringProperty(metadata, 'id', initializers)
|
|
1588
|
+
?? deriveWorkerId(moduleId, workersRoot, filePath)
|
|
1589
|
+
|
|
1590
|
+
// Runtime skips any worker without a queue, so a worker missing the required
|
|
1591
|
+
// property is not a contract; a present-but-dynamic queue is reported instead.
|
|
1592
|
+
const hasQueueProperty = getObjectPropertyInitializer(metadata, 'queue') !== undefined
|
|
1593
|
+
const queue = readResolvedStringProperty(metadata, 'queue', initializers)
|
|
1594
|
+
if (!hasQueueProperty) {
|
|
1595
|
+
unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1596
|
+
continue
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const workerMetadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1600
|
+
if (queue) workerMetadata.queue = queue
|
|
1601
|
+
else unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1602
|
+
const name = readResolvedStringProperty(metadata, 'name', initializers)
|
|
1603
|
+
if (name) workerMetadata.name = name
|
|
1604
|
+
const concurrencyInitializer = getObjectPropertyInitializer(metadata, 'concurrency')
|
|
1605
|
+
if (concurrencyInitializer && ts.isNumericLiteral(concurrencyInitializer)) {
|
|
1606
|
+
workerMetadata.concurrency = Number(concurrencyInitializer.text)
|
|
1607
|
+
}
|
|
1608
|
+
facts.push({
|
|
1609
|
+
kind: 'worker',
|
|
1610
|
+
id: workerId,
|
|
1611
|
+
source,
|
|
1612
|
+
...(Object.keys(workerMetadata).length > 0 ? { metadata: workerMetadata } : {}),
|
|
1613
|
+
})
|
|
1614
|
+
}
|
|
1615
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function extractPageMiddlewareContracts(
|
|
1619
|
+
moduleRoot: string,
|
|
1620
|
+
sourceRoot: string,
|
|
1621
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1622
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1623
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1624
|
+
for (const surface of ['backend', 'frontend'] as const) {
|
|
1625
|
+
const filePath = resolveConventionFile(path.join(moduleRoot, surface), 'middleware')
|
|
1626
|
+
if (!filePath) continue
|
|
1627
|
+
const sourceFile = readSourceFile(filePath)
|
|
1628
|
+
if (!sourceFile) continue
|
|
1629
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1630
|
+
const middlewareArray =
|
|
1631
|
+
findArrayLiteralDeclaration(sourceFile, 'middleware') ??
|
|
1632
|
+
(() => {
|
|
1633
|
+
const defaultExport = findDefaultExportExpression(sourceFile)
|
|
1634
|
+
return defaultExport ? unwrapArrayLiteral(defaultExport) : null
|
|
1635
|
+
})()
|
|
1636
|
+
if (!middlewareArray) continue
|
|
1637
|
+
let index = 0
|
|
1638
|
+
for (const element of middlewareArray.elements) {
|
|
1639
|
+
if (!ts.isObjectLiteralExpression(element)) continue
|
|
1640
|
+
const middlewareId = readStringPropertyInitializer(element, 'id')
|
|
1641
|
+
if (!middlewareId) {
|
|
1642
|
+
unresolved.push({
|
|
1643
|
+
code: 'unresolved-static-contract',
|
|
1644
|
+
kind: 'page-middleware',
|
|
1645
|
+
id: `${sourcePath}#${index}`,
|
|
1646
|
+
source: { sourcePath, line: nodeLine(sourceFile, element) },
|
|
1647
|
+
})
|
|
1648
|
+
index += 1
|
|
1649
|
+
continue
|
|
1650
|
+
}
|
|
1651
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = { surface }
|
|
1652
|
+
const mode = readStringPropertyInitializer(element, 'mode')
|
|
1653
|
+
if (mode) metadata.mode = mode
|
|
1654
|
+
const priorityInitializer = getObjectPropertyInitializer(element, 'priority')
|
|
1655
|
+
if (priorityInitializer && ts.isNumericLiteral(priorityInitializer)) {
|
|
1656
|
+
metadata.priority = Number(priorityInitializer.text)
|
|
1657
|
+
}
|
|
1658
|
+
facts.push({
|
|
1659
|
+
kind: 'page-middleware',
|
|
1660
|
+
id: middlewareId,
|
|
1661
|
+
source: { sourcePath, exportName: 'middleware', line: nodeLine(sourceFile, element) },
|
|
1662
|
+
metadata,
|
|
1663
|
+
})
|
|
1664
|
+
index += 1
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function extractSetupContract(
|
|
1671
|
+
moduleId: string,
|
|
1672
|
+
setupFilePath: string | null,
|
|
1673
|
+
sourcePath: string | null,
|
|
1674
|
+
): ModuleOwnedContractFact | null {
|
|
1675
|
+
if (!setupFilePath || !sourcePath) return null
|
|
1676
|
+
const sourceFile = readSourceFile(setupFilePath)
|
|
1677
|
+
if (!sourceFile) return null
|
|
1678
|
+
const setupObject =
|
|
1679
|
+
findObjectLiteralDeclaration(sourceFile, 'setup') ??
|
|
1680
|
+
(() => {
|
|
1681
|
+
const defaultExport = findDefaultExportExpression(sourceFile)
|
|
1682
|
+
if (!defaultExport) return null
|
|
1683
|
+
const unwrapped = unwrapExpression(defaultExport)
|
|
1684
|
+
return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : null
|
|
1685
|
+
})()
|
|
1686
|
+
if (!setupObject) return null
|
|
1687
|
+
const hooks = readMethodBearingPropertyNames(setupObject)
|
|
1688
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1689
|
+
if (hooks.length > 0) metadata.hooks = hooks
|
|
1690
|
+
const roleFeatures = getObjectPropertyInitializer(setupObject, 'defaultRoleFeatures')
|
|
1691
|
+
if (roleFeatures && ts.isObjectLiteralExpression(roleFeatures)) {
|
|
1692
|
+
const roles = readObjectKeyNames(roleFeatures)
|
|
1693
|
+
if (roles.length > 0) metadata.roles = roles
|
|
1694
|
+
}
|
|
1695
|
+
const customerRoleFeatures = getObjectPropertyInitializer(setupObject, 'defaultCustomerRoleFeatures')
|
|
1696
|
+
if (customerRoleFeatures && ts.isObjectLiteralExpression(customerRoleFeatures)) {
|
|
1697
|
+
const customerRoles = readObjectKeyNames(customerRoleFeatures)
|
|
1698
|
+
if (customerRoles.length > 0) metadata.customerRoles = customerRoles
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1701
|
+
kind: 'setup',
|
|
1702
|
+
id: `${moduleId}:setup`,
|
|
1703
|
+
source: { sourcePath, exportName: 'setup', line: nodeLine(sourceFile, setupObject) },
|
|
1704
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function extractEncryptionContracts(
|
|
1709
|
+
encryptionFilePath: string | null,
|
|
1710
|
+
sourcePath: string | null,
|
|
1711
|
+
): ModuleOwnedContractFact[] {
|
|
1712
|
+
if (!encryptionFilePath || !sourcePath) return []
|
|
1713
|
+
const sourceFile = readSourceFile(encryptionFilePath)
|
|
1714
|
+
if (!sourceFile) return []
|
|
1715
|
+
const mapsArray =
|
|
1716
|
+
findArrayLiteralDeclaration(sourceFile, 'defaultEncryptionMaps') ??
|
|
1717
|
+
findArrayLiteralDeclaration(sourceFile, 'encryptionMaps')
|
|
1718
|
+
if (!mapsArray) return []
|
|
1719
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1720
|
+
for (const element of mapsArray.elements) {
|
|
1721
|
+
if (!ts.isObjectLiteralExpression(element)) continue
|
|
1722
|
+
const entityId = readStringPropertyInitializer(element, 'entityId')
|
|
1723
|
+
if (!entityId) continue
|
|
1724
|
+
const fieldsInitializer = getObjectPropertyInitializer(element, 'fields')
|
|
1725
|
+
const fields: string[] = []
|
|
1726
|
+
if (fieldsInitializer) {
|
|
1727
|
+
const fieldsArray = unwrapArrayLiteral(fieldsInitializer)
|
|
1728
|
+
if (fieldsArray) {
|
|
1729
|
+
for (const fieldElement of fieldsArray.elements) {
|
|
1730
|
+
if (!ts.isObjectLiteralExpression(fieldElement)) continue
|
|
1731
|
+
const field = readStringPropertyInitializer(fieldElement, 'field')
|
|
1732
|
+
if (field) fields.push(field)
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
facts.push({
|
|
1737
|
+
kind: 'encryption',
|
|
1738
|
+
id: entityId,
|
|
1739
|
+
source: { sourcePath, exportName: 'defaultEncryptionMaps', line: nodeLine(sourceFile, element) },
|
|
1740
|
+
...(fields.length > 0 ? { metadata: { fields: fields.sort((left, right) => left.localeCompare(right)) } } : {}),
|
|
1741
|
+
})
|
|
1742
|
+
}
|
|
1743
|
+
return dedupeOwnedContractFacts(facts)
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function extractCustomEntityContracts(
|
|
1747
|
+
ceFilePath: string | null,
|
|
1748
|
+
sourcePath: string | null,
|
|
1749
|
+
): ModuleOwnedContractFact[] {
|
|
1750
|
+
if (!ceFilePath || !sourcePath) return []
|
|
1751
|
+
const entityIds = [...collectCustomFieldEntityIds(ceFilePath)].sort((left, right) => left.localeCompare(right))
|
|
1752
|
+
return entityIds.map((id) => ({
|
|
1753
|
+
kind: 'custom-entity' as const,
|
|
1754
|
+
id,
|
|
1755
|
+
source: { sourcePath, exportName: 'default' },
|
|
1756
|
+
}))
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Keyed AI override exports. Agent *extensions* (`aiAgentExtensions`) patch an agent
|
|
1761
|
+
* additively and belong to the `ai.extensions` array; these two replace or disable an
|
|
1762
|
+
* entry by key and map to exact `ai.agents.<id>` / `ai.tools.<id>` override targets.
|
|
1763
|
+
*/
|
|
1764
|
+
const AI_OVERRIDE_EXPORTS = [
|
|
1765
|
+
{ exportName: 'aiAgentOverrides', target: 'agent' },
|
|
1766
|
+
{ exportName: 'aiToolOverrides', target: 'tool' },
|
|
1767
|
+
] as const
|
|
1768
|
+
|
|
1769
|
+
function extractAiExtensionContracts(
|
|
1770
|
+
moduleRoot: string,
|
|
1771
|
+
sourceRoot: string,
|
|
1772
|
+
): ModuleOwnedContractFact[] {
|
|
1773
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1774
|
+
const sources: Array<{ basename: string; relativeDir: string }> = [
|
|
1775
|
+
{ basename: 'ai-agents', relativeDir: '.' },
|
|
1776
|
+
{ basename: 'ai-tools', relativeDir: '.' },
|
|
1777
|
+
{ basename: 'ai-agents.extensions', relativeDir: '.' },
|
|
1778
|
+
{ basename: 'ai-tools.extensions', relativeDir: '.' },
|
|
1779
|
+
]
|
|
1780
|
+
for (const entry of sources) {
|
|
1781
|
+
const filePath = resolveConventionFile(path.join(moduleRoot, entry.relativeDir), entry.basename)
|
|
1782
|
+
if (!filePath) continue
|
|
1783
|
+
const sourceFile = readSourceFile(filePath)
|
|
1784
|
+
if (!sourceFile) continue
|
|
1785
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1786
|
+
const extensionsArray = findArrayLiteralDeclaration(sourceFile, 'aiAgentExtensions')
|
|
1787
|
+
if (extensionsArray) {
|
|
1788
|
+
for (const element of extensionsArray.elements) {
|
|
1789
|
+
const objectLiteral = ts.isObjectLiteralExpression(element)
|
|
1790
|
+
? element
|
|
1791
|
+
: ts.isCallExpression(element) && element.arguments[0] && ts.isObjectLiteralExpression(element.arguments[0])
|
|
1792
|
+
? element.arguments[0]
|
|
1793
|
+
: null
|
|
1794
|
+
if (!objectLiteral) continue
|
|
1795
|
+
const targetAgentId = readStringPropertyInitializer(objectLiteral, 'targetAgentId')
|
|
1796
|
+
if (!targetAgentId) continue
|
|
1797
|
+
facts.push({
|
|
1798
|
+
kind: 'ai-extension',
|
|
1799
|
+
id: `agent-extension:${targetAgentId}`,
|
|
1800
|
+
source: { sourcePath, exportName: 'aiAgentExtensions', line: nodeLine(sourceFile, objectLiteral) },
|
|
1801
|
+
metadata: { target: 'agent', mode: 'extension', targetAgentId },
|
|
1802
|
+
})
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// `aiAgentOverrides` / `aiToolOverrides` are keyed replace-or-disable maps; only
|
|
1806
|
+
// their keys are contract facts. Values (agent/tool definitions, `null` disables)
|
|
1807
|
+
// are never read or serialized.
|
|
1808
|
+
for (const { exportName, target } of AI_OVERRIDE_EXPORTS) {
|
|
1809
|
+
const overridesObject = findObjectLiteralDeclaration(sourceFile, exportName)
|
|
1810
|
+
if (!overridesObject) continue
|
|
1811
|
+
for (const property of overridesObject.properties) {
|
|
1812
|
+
const overrideId = getPropertyName(property)
|
|
1813
|
+
if (!overrideId) continue
|
|
1814
|
+
facts.push({
|
|
1815
|
+
kind: 'ai-extension',
|
|
1816
|
+
id: `${target}-override:${overrideId}`,
|
|
1817
|
+
source: { sourcePath, exportName, line: nodeLine(sourceFile, property) },
|
|
1818
|
+
metadata: { target, mode: 'override', overrideKey: overrideId },
|
|
1819
|
+
})
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return dedupeOwnedContractFacts(facts)
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function extractGeneratorPluginContracts(
|
|
1827
|
+
moduleRoot: string,
|
|
1828
|
+
sourceRoot: string,
|
|
1829
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1830
|
+
const filePath = resolveConventionFile(moduleRoot, 'generators')
|
|
1831
|
+
if (!filePath) return { facts: [], unresolved: [] }
|
|
1832
|
+
const sourceFile = readSourceFile(filePath)
|
|
1833
|
+
if (!sourceFile) return { facts: [], unresolved: [] }
|
|
1834
|
+
const pluginsArray = findArrayLiteralDeclaration(sourceFile, 'generatorPlugins')
|
|
1835
|
+
if (!pluginsArray) return { facts: [], unresolved: [] }
|
|
1836
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1837
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1838
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1839
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1840
|
+
let index = 0
|
|
1841
|
+
for (const element of pluginsArray.elements) {
|
|
1842
|
+
const objectLiteral = resolveToObjectLiteral(element, initializers)
|
|
1843
|
+
if (!objectLiteral) {
|
|
1844
|
+
index += 1
|
|
1845
|
+
continue
|
|
1846
|
+
}
|
|
1847
|
+
const pluginId = readStringPropertyInitializer(objectLiteral, 'id')
|
|
1848
|
+
if (!pluginId) {
|
|
1849
|
+
unresolved.push({
|
|
1850
|
+
code: 'unresolved-static-contract',
|
|
1851
|
+
kind: 'generator-plugin',
|
|
1852
|
+
id: `${sourcePath}#${index}`,
|
|
1853
|
+
source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
|
|
1854
|
+
})
|
|
1855
|
+
index += 1
|
|
1856
|
+
continue
|
|
1857
|
+
}
|
|
1858
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1859
|
+
const conventionFile = readStringPropertyInitializer(objectLiteral, 'conventionFile')
|
|
1860
|
+
if (conventionFile) metadata.conventionFile = conventionFile
|
|
1861
|
+
const outputFileName = readStringPropertyInitializer(objectLiteral, 'outputFileName')
|
|
1862
|
+
if (outputFileName) metadata.outputFileName = outputFileName
|
|
1863
|
+
facts.push({
|
|
1864
|
+
kind: 'generator-plugin',
|
|
1865
|
+
id: pluginId,
|
|
1866
|
+
source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
|
|
1867
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
1868
|
+
})
|
|
1869
|
+
index += 1
|
|
1870
|
+
}
|
|
1871
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function compareSourceRefs(left: ModuleFactSourceRef, right: ModuleFactSourceRef): number {
|
|
1875
|
+
return (
|
|
1876
|
+
left.sourcePath.localeCompare(right.sourcePath) ||
|
|
1877
|
+
(left.line ?? 0) - (right.line ?? 0) ||
|
|
1878
|
+
(left.exportName ?? '').localeCompare(right.exportName ?? '')
|
|
1879
|
+
)
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function sameSourceRef(left: ModuleFactSourceRef, right: ModuleFactSourceRef): boolean {
|
|
1883
|
+
return (
|
|
1884
|
+
left.sourcePath === right.sourcePath &&
|
|
1885
|
+
(left.line ?? null) === (right.line ?? null) &&
|
|
1886
|
+
(left.exportName ?? null) === (right.exportName ?? null)
|
|
1887
|
+
)
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Every proven `(kind, id)` reaches `factSources`. Facts whose declaration site is already
|
|
1891
|
+
// serialized inline (pages, CLI commands, routes, AI tools/agents, reused contributions) and
|
|
1892
|
+
// the owned families (which carry `source` inside `ownedContracts`) are emitted as a typed
|
|
1893
|
+
// `factRef` pointer instead of a duplicated source ref, so the index stays uniform without
|
|
1894
|
+
// copying provenance payloads past the determinism byte guard. Duplicate detection runs over
|
|
1895
|
+
// ALL candidates regardless of which projection they take.
|
|
1896
|
+
type ProvenanceCandidate = {
|
|
1897
|
+
kind: ModuleFactSourceKind
|
|
1898
|
+
id: string
|
|
1899
|
+
source?: ModuleFactSourceRef
|
|
1900
|
+
factRef?: ModuleFactRef
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function provenanceCandidateOrderKey(candidate: ProvenanceCandidate): string {
|
|
1904
|
+
if (candidate.source) {
|
|
1905
|
+
return JSON.stringify([
|
|
1906
|
+
0,
|
|
1907
|
+
candidate.source.sourcePath,
|
|
1908
|
+
candidate.source.line ?? 0,
|
|
1909
|
+
candidate.source.exportName ?? '',
|
|
1910
|
+
])
|
|
1911
|
+
}
|
|
1912
|
+
return JSON.stringify([1, candidate.factRef?.factSection ?? '', candidate.factRef?.factKey ?? ''])
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function buildProvenanceIndex(candidates: readonly ProvenanceCandidate[]): {
|
|
1916
|
+
factSources: ModuleFactSourceEntry[]
|
|
1917
|
+
diagnostics: ModuleFactDiagnostic[]
|
|
1918
|
+
} {
|
|
1919
|
+
const grouped = new Map<string, ProvenanceCandidate[]>()
|
|
1920
|
+
for (const candidate of candidates) {
|
|
1921
|
+
const key = JSON.stringify([candidate.kind, candidate.id])
|
|
1922
|
+
const group = grouped.get(key) ?? []
|
|
1923
|
+
group.push(candidate)
|
|
1924
|
+
grouped.set(key, group)
|
|
1925
|
+
}
|
|
1926
|
+
// Keep the provenance index compact: sourcePath is the provenance and (kind,id) the
|
|
1927
|
+
// stable identity, so exportName and line (recoverable from source) are dropped here.
|
|
1928
|
+
// Owned families retain the richer source ref inside `ownedContracts`.
|
|
1929
|
+
const factSources: ModuleFactSourceEntry[] = []
|
|
1930
|
+
const diagnostics: ModuleFactDiagnostic[] = []
|
|
1931
|
+
for (const group of grouped.values()) {
|
|
1932
|
+
const sorted = [...group].sort(
|
|
1933
|
+
(left, right) => provenanceCandidateOrderKey(left).localeCompare(provenanceCandidateOrderKey(right)),
|
|
1934
|
+
)
|
|
1935
|
+
const canonical = sorted[0]
|
|
1936
|
+
factSources.push(
|
|
1937
|
+
canonical.factRef
|
|
1938
|
+
? {
|
|
1939
|
+
kind: canonical.kind,
|
|
1940
|
+
id: canonical.id,
|
|
1941
|
+
factRef: {
|
|
1942
|
+
factSection: canonical.factRef.factSection,
|
|
1943
|
+
...(canonical.factRef.factKey === canonical.id
|
|
1944
|
+
? {}
|
|
1945
|
+
: { factKey: canonical.factRef.factKey }),
|
|
1946
|
+
},
|
|
1947
|
+
}
|
|
1948
|
+
: {
|
|
1949
|
+
kind: canonical.kind,
|
|
1950
|
+
id: canonical.id,
|
|
1951
|
+
source: { sourcePath: (canonical.source as ModuleFactSourceRef).sourcePath },
|
|
1952
|
+
},
|
|
1953
|
+
)
|
|
1954
|
+
const canonicalKey = provenanceCandidateOrderKey(canonical)
|
|
1955
|
+
for (const rejected of sorted.slice(1)) {
|
|
1956
|
+
if (provenanceCandidateOrderKey(rejected) === canonicalKey) continue
|
|
1957
|
+
diagnostics.push({
|
|
1958
|
+
code: 'duplicate-source',
|
|
1959
|
+
kind: rejected.kind,
|
|
1960
|
+
id: rejected.id,
|
|
1961
|
+
...(rejected.source ? { source: rejected.source } : {}),
|
|
1962
|
+
})
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
factSources.sort(
|
|
1966
|
+
(left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id),
|
|
1967
|
+
)
|
|
1968
|
+
diagnostics.sort(
|
|
1969
|
+
(left, right) =>
|
|
1970
|
+
left.code.localeCompare(right.code) ||
|
|
1971
|
+
left.kind.localeCompare(right.kind) ||
|
|
1972
|
+
left.id.localeCompare(right.id) ||
|
|
1973
|
+
compareSourceRefs(left.source ?? { sourcePath: '' }, right.source ?? { sourcePath: '' }),
|
|
1974
|
+
)
|
|
1975
|
+
return { factSources, diagnostics }
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function buildOwnedContracts(
|
|
1979
|
+
families: ReadonlyArray<[ModuleOwnedContractKind, ModuleOwnedContractFact[]]>,
|
|
1980
|
+
): ModuleOwnedContracts | undefined {
|
|
1981
|
+
const result: ModuleOwnedContracts = {}
|
|
1982
|
+
for (const [kind, facts] of families) {
|
|
1983
|
+
if (facts.length === 0) continue
|
|
1984
|
+
// Drop the (constant/redundant) exportName from the emitted source ref; the line
|
|
1985
|
+
// anchor plus sourcePath already pin the declaration and keep the JSON compact.
|
|
1986
|
+
result[kind] = facts.map((fact) => ({
|
|
1987
|
+
...fact,
|
|
1988
|
+
source: {
|
|
1989
|
+
sourcePath: fact.source.sourcePath,
|
|
1990
|
+
...(fact.source.line ? { line: fact.source.line } : {}),
|
|
1991
|
+
},
|
|
1992
|
+
}))
|
|
1993
|
+
}
|
|
1994
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const CONTRIBUTION_KIND_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
|
|
1998
|
+
subscriber: 'subscriber',
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const REGISTRY_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
|
|
2002
|
+
vector: 'vector',
|
|
2003
|
+
integration: 'integration',
|
|
2004
|
+
payment: 'integration',
|
|
2005
|
+
shipping: 'integration',
|
|
2006
|
+
currency: 'integration',
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFacts {
|
|
2010
|
+
const { moduleId, coreVersion = null, sourcePackage = null, sourceVersion = null } = options
|
|
2011
|
+
const moduleRoot = options.moduleRoot
|
|
2012
|
+
?? (options.coreSrcRoot ? path.join(options.coreSrcRoot, moduleId) : null)
|
|
2013
|
+
if (!moduleRoot) {
|
|
2014
|
+
throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
|
|
2015
|
+
}
|
|
2016
|
+
const sourceRoot = options.portableSourceRoot
|
|
2017
|
+
? assertPortableSourceRoot(moduleId, options.portableSourceRoot)
|
|
2018
|
+
: toPortableSourceRoot(moduleId, sourcePackage)
|
|
2019
|
+
|
|
2020
|
+
const entitiesFilePath =
|
|
2021
|
+
resolveConventionFile(path.join(moduleRoot, 'data'), 'entities') ??
|
|
2022
|
+
resolveConventionFile(path.join(moduleRoot, 'db'), 'entities') ??
|
|
2023
|
+
resolveConventionFile(path.join(moduleRoot, 'data'), 'schema')
|
|
2024
|
+
const ceFilePath = resolveConventionFile(moduleRoot, 'ce')
|
|
2025
|
+
const eventsFilePath = resolveConventionFile(moduleRoot, 'events')
|
|
2026
|
+
const aclFilePath = resolveConventionFile(moduleRoot, 'acl')
|
|
2027
|
+
const diFilePath = resolveConventionFile(moduleRoot, 'di')
|
|
2028
|
+
const searchFilePath = resolveConventionFile(moduleRoot, 'search')
|
|
2029
|
+
const notificationsFilePath = resolveConventionFile(moduleRoot, 'notifications')
|
|
2030
|
+
const cliFilePath = resolveConventionFile(moduleRoot, 'cli')
|
|
2031
|
+
const indexFilePath = resolveConventionFile(moduleRoot, 'index')
|
|
2032
|
+
const setupFilePath = resolveConventionFile(moduleRoot, 'setup')
|
|
2033
|
+
const encryptionFilePath = resolveConventionFile(moduleRoot, 'encryption')
|
|
2034
|
+
|
|
2035
|
+
const portableOf = (filePath: string | null): string | null =>
|
|
2036
|
+
filePath ? toPortableSourcePath(moduleRoot, sourceRoot, filePath) : null
|
|
2037
|
+
|
|
2038
|
+
const warnings: string[] = []
|
|
2039
|
+
const { title, description } = extractModuleMeta(indexFilePath)
|
|
2040
|
+
const customFieldEntityIds = collectCustomFieldEntityIds(ceFilePath)
|
|
2041
|
+
const entities = extractEntities(moduleId, entitiesFilePath, customFieldEntityIds)
|
|
2042
|
+
const events = extractEvents(eventsFilePath)
|
|
2043
|
+
const aclFeatures = extractAclFeatures(aclFilePath)
|
|
2044
|
+
|
|
2045
|
+
const { source: registrySource, description: registryDescription } = resolveRegistrySource(options)
|
|
2046
|
+
const apiRouteSourcePaths = extractApiRouteSourcePaths(moduleId, moduleRoot, sourceRoot)
|
|
2047
|
+
const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, apiRouteSourcePaths, warnings)
|
|
2048
|
+
const diRegistrations = extractDiRegistrationFacts(diFilePath, portableOf(diFilePath) ?? '')
|
|
2049
|
+
const diTokens = deriveLegacyDiTokens(diRegistrations.facts)
|
|
2050
|
+
for (const legacyToken of extractDiTokens(diFilePath)) {
|
|
2051
|
+
if (!diTokens.includes(legacyToken)) diTokens.push(legacyToken)
|
|
2052
|
+
}
|
|
2053
|
+
const searchEntities = extractSearchEntities(searchFilePath, warnings)
|
|
2054
|
+
const notifications = extractNotifications(notificationsFilePath, warnings)
|
|
2055
|
+
const cli = extractCli(cliFilePath, warnings)
|
|
2056
|
+
const cliCommands = cliFilePath
|
|
2057
|
+
? cli.map((command) => ({
|
|
2058
|
+
command,
|
|
2059
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, cliFilePath),
|
|
2060
|
+
}))
|
|
2061
|
+
: []
|
|
2062
|
+
const backendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'backend')
|
|
2063
|
+
const frontendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'frontend')
|
|
2064
|
+
const aiTools = extractAiTools(moduleRoot, sourceRoot)
|
|
2065
|
+
const aiAgents = extractAiAgents(moduleRoot, sourceRoot)
|
|
2066
|
+
const hostTokens: ModuleHostTokens = {
|
|
2067
|
+
entityIds: extractHostEntityIds(entities),
|
|
2068
|
+
tableIds: extractTableIds(moduleRoot),
|
|
2069
|
+
}
|
|
2070
|
+
const extensionSurfaces = extractModuleExtensionFacts({
|
|
2071
|
+
moduleId,
|
|
2072
|
+
moduleRoot,
|
|
2073
|
+
sourceRoot,
|
|
2074
|
+
entities,
|
|
2075
|
+
events,
|
|
2076
|
+
apiRoutes,
|
|
2077
|
+
searchEntities,
|
|
2078
|
+
notifications,
|
|
2079
|
+
aiTools,
|
|
2080
|
+
aiAgents,
|
|
2081
|
+
})
|
|
2082
|
+
|
|
2083
|
+
const indexSourcePath = portableOf(indexFilePath)
|
|
2084
|
+
const diSourcePath = portableOf(diFilePath)
|
|
2085
|
+
const moduleMetadataFact = extractModuleMetadataContract(indexFilePath, moduleId, indexSourcePath)
|
|
2086
|
+
const commandCandidates = extractCommandContractCandidates(moduleId, moduleRoot, sourceRoot)
|
|
2087
|
+
const commandFacts = dedupeOwnedContractFacts(
|
|
2088
|
+
commandCandidates.map((candidate) => ({
|
|
2089
|
+
kind: 'command' as const,
|
|
2090
|
+
id: candidate.id,
|
|
2091
|
+
source: { sourcePath: candidate.sourcePath },
|
|
2092
|
+
})),
|
|
2093
|
+
)
|
|
2094
|
+
const workers = extractWorkerContracts(moduleId, moduleRoot, sourceRoot)
|
|
2095
|
+
const pageMiddleware = extractPageMiddlewareContracts(moduleRoot, sourceRoot)
|
|
2096
|
+
const setupFact = extractSetupContract(moduleId, setupFilePath, portableOf(setupFilePath))
|
|
2097
|
+
const encryptionFacts = extractEncryptionContracts(encryptionFilePath, portableOf(encryptionFilePath))
|
|
2098
|
+
const customEntityFacts = extractCustomEntityContracts(ceFilePath, portableOf(ceFilePath))
|
|
2099
|
+
const aiExtensionFacts = extractAiExtensionContracts(moduleRoot, sourceRoot)
|
|
2100
|
+
const generatorPlugins = extractGeneratorPluginContracts(moduleRoot, sourceRoot)
|
|
2101
|
+
const diContractFacts = dedupeOwnedContractFacts(
|
|
2102
|
+
diRegistrations.facts.map((fact) => ({
|
|
2103
|
+
kind: 'di-registration' as const,
|
|
2104
|
+
id: fact.token,
|
|
2105
|
+
source: fact.source,
|
|
2106
|
+
metadata: {
|
|
2107
|
+
registrationKind: fact.registrationKind,
|
|
2108
|
+
...(fact.providerSymbol ? { providerSymbol: fact.providerSymbol } : {}),
|
|
2109
|
+
...(fact.lifetime ? { lifetime: fact.lifetime } : {}),
|
|
2110
|
+
...(fact.injectionMode ? { injectionMode: fact.injectionMode } : {}),
|
|
2111
|
+
},
|
|
2112
|
+
})),
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
const ownedContracts = buildOwnedContracts([
|
|
2116
|
+
['module-metadata', moduleMetadataFact ? [moduleMetadataFact] : []],
|
|
2117
|
+
['command', commandFacts],
|
|
2118
|
+
['worker', workers.facts],
|
|
2119
|
+
['page-middleware', pageMiddleware.facts],
|
|
2120
|
+
['setup', setupFact ? [setupFact] : []],
|
|
2121
|
+
['encryption', encryptionFacts],
|
|
2122
|
+
['di-registration', diContractFacts],
|
|
2123
|
+
['custom-entity', customEntityFacts],
|
|
2124
|
+
['ai-extension', aiExtensionFacts],
|
|
2125
|
+
['generator-plugin', generatorPlugins.facts],
|
|
2126
|
+
])
|
|
2127
|
+
|
|
2128
|
+
const provenanceCandidates: ProvenanceCandidate[] = []
|
|
2129
|
+
const pushSource = (kind: ModuleFactSourceKind, id: string, source: ModuleFactSourceRef): void => {
|
|
2130
|
+
provenanceCandidates.push({ kind, id, source })
|
|
2131
|
+
}
|
|
2132
|
+
const pushRef = (
|
|
2133
|
+
kind: ModuleFactSourceKind,
|
|
2134
|
+
id: string,
|
|
2135
|
+
source: ModuleFactSourceRef,
|
|
2136
|
+
factSection: string,
|
|
2137
|
+
factKey: string,
|
|
2138
|
+
): void => {
|
|
2139
|
+
provenanceCandidates.push({ kind, id, source, factRef: { factSection, factKey } })
|
|
2140
|
+
}
|
|
2141
|
+
const pushOwnedRefs = (kind: ModuleOwnedContractKind, facts: readonly ModuleOwnedContractFact[]): void => {
|
|
2142
|
+
for (const fact of facts) pushRef(kind, fact.id, fact.source, `ownedContracts.${kind}`, fact.id)
|
|
2143
|
+
}
|
|
2144
|
+
if (moduleMetadataFact) {
|
|
2145
|
+
pushRef(
|
|
2146
|
+
'module-metadata',
|
|
2147
|
+
moduleId,
|
|
2148
|
+
moduleMetadataFact.source,
|
|
2149
|
+
'ownedContracts.module-metadata',
|
|
2150
|
+
moduleMetadataFact.id,
|
|
2151
|
+
)
|
|
2152
|
+
}
|
|
2153
|
+
if (entitiesFilePath) {
|
|
2154
|
+
const entitiesSourcePath = portableOf(entitiesFilePath) as string
|
|
2155
|
+
for (const entity of entities) {
|
|
2156
|
+
pushSource('entity', entity.id, { sourcePath: entitiesSourcePath, exportName: entity.class })
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (eventsFilePath) {
|
|
2160
|
+
const eventsSourcePath = portableOf(eventsFilePath) as string
|
|
2161
|
+
for (const event of events) pushSource('event', event.id, { sourcePath: eventsSourcePath, exportName: 'events' })
|
|
2162
|
+
}
|
|
2163
|
+
if (aclFilePath) {
|
|
2164
|
+
const aclSourcePath = portableOf(aclFilePath) as string
|
|
2165
|
+
for (const feature of aclFeatures) pushSource('acl-feature', feature, { sourcePath: aclSourcePath, exportName: 'features' })
|
|
2166
|
+
}
|
|
2167
|
+
for (const route of apiRoutes) {
|
|
2168
|
+
if (route.sourcePath) {
|
|
2169
|
+
pushRef('api-route', route.path, { sourcePath: route.sourcePath }, 'apiRoutes', route.path)
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (searchFilePath) {
|
|
2173
|
+
const searchSourcePath = portableOf(searchFilePath) as string
|
|
2174
|
+
for (const entityId of searchEntities) pushSource('search', entityId, { sourcePath: searchSourcePath, exportName: 'searchConfig' })
|
|
2175
|
+
}
|
|
2176
|
+
if (notificationsFilePath) {
|
|
2177
|
+
const notificationsSourcePath = portableOf(notificationsFilePath) as string
|
|
2178
|
+
for (const id of notifications) pushSource('notification', id, { sourcePath: notificationsSourcePath })
|
|
2179
|
+
}
|
|
2180
|
+
for (const command of cliCommands) {
|
|
2181
|
+
pushRef('cli-command', command.command, { sourcePath: command.sourcePath }, 'cliCommands', command.command)
|
|
2182
|
+
}
|
|
2183
|
+
for (const page of backendPages) {
|
|
2184
|
+
pushRef('backend-page', page.path, { sourcePath: page.sourcePath }, 'backendPages', page.path)
|
|
2185
|
+
}
|
|
2186
|
+
for (const page of frontendPages) {
|
|
2187
|
+
pushRef('frontend-page', page.path, { sourcePath: page.sourcePath }, 'frontendPages', page.path)
|
|
2188
|
+
}
|
|
2189
|
+
for (const tool of aiTools) pushRef('ai-tool', tool.name, { sourcePath: tool.sourcePath }, 'aiTools', tool.name)
|
|
2190
|
+
for (const agent of aiAgents) pushRef('ai-agent', agent.id, { sourcePath: agent.sourcePath }, 'aiAgents', agent.id)
|
|
2191
|
+
for (const candidate of commandCandidates) {
|
|
2192
|
+
pushRef('command', candidate.id, { sourcePath: candidate.sourcePath }, 'ownedContracts.command', candidate.id)
|
|
2193
|
+
}
|
|
2194
|
+
pushOwnedRefs('worker', workers.facts)
|
|
2195
|
+
pushOwnedRefs('page-middleware', pageMiddleware.facts)
|
|
2196
|
+
if (setupFact) pushOwnedRefs('setup', [setupFact])
|
|
2197
|
+
pushOwnedRefs('encryption', encryptionFacts)
|
|
2198
|
+
for (const fact of diRegistrations.facts) {
|
|
2199
|
+
pushRef('di-registration', fact.token, fact.source, 'ownedContracts.di-registration', fact.token)
|
|
2200
|
+
}
|
|
2201
|
+
pushOwnedRefs('custom-entity', customEntityFacts)
|
|
2202
|
+
pushOwnedRefs('ai-extension', aiExtensionFacts)
|
|
2203
|
+
pushOwnedRefs('generator-plugin', generatorPlugins.facts)
|
|
2204
|
+
const contributionRef = (contributionId: string): { factSection: string; factKey: string } => ({
|
|
2205
|
+
factSection: 'extensionSurfaces.contributions',
|
|
2206
|
+
factKey: contributionId,
|
|
2207
|
+
})
|
|
2208
|
+
for (const contribution of extensionSurfaces.contributions) {
|
|
2209
|
+
const contributionSource: ModuleFactSourceRef = {
|
|
2210
|
+
sourcePath: contribution.source.path,
|
|
2211
|
+
...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
|
|
2212
|
+
}
|
|
2213
|
+
const ref = contributionRef(contribution.id)
|
|
2214
|
+
if (contribution.kind === 'subscriber') {
|
|
2215
|
+
pushRef('subscriber', contribution.id, contributionSource, ref.factSection, ref.factKey)
|
|
2216
|
+
continue
|
|
2217
|
+
}
|
|
2218
|
+
if (contribution.kind === 'specialized-registry') {
|
|
2219
|
+
const details = contribution.details as { registry?: string; registryId?: string }
|
|
2220
|
+
const sourceKind = details.registry ? REGISTRY_TO_SOURCE_KIND[details.registry] : undefined
|
|
2221
|
+
if (sourceKind) {
|
|
2222
|
+
pushRef(
|
|
2223
|
+
sourceKind,
|
|
2224
|
+
details.registryId ?? contribution.id,
|
|
2225
|
+
contributionSource,
|
|
2226
|
+
ref.factSection,
|
|
2227
|
+
ref.factKey,
|
|
2228
|
+
)
|
|
2229
|
+
}
|
|
2230
|
+
continue
|
|
2231
|
+
}
|
|
2232
|
+
const mapped = CONTRIBUTION_KIND_TO_SOURCE_KIND[contribution.kind]
|
|
2233
|
+
if (mapped) pushRef(mapped, contribution.id, contributionSource, ref.factSection, ref.factKey)
|
|
2234
|
+
}
|
|
2235
|
+
// extension-host / extension-contribution provenance is emitted as typed `factRef` pointers
|
|
2236
|
+
// rather than duplicated source refs: those facts already carry their own `source` inside
|
|
2237
|
+
// `extensionSurfaces.{hosts,contributions}`, and copying every declared host spot (hundreds
|
|
2238
|
+
// per large module) would push the shared JSON past the determinism byte guard asserted in
|
|
2239
|
+
// `module-facts.bc-guard.test.ts`.
|
|
2240
|
+
// Hosts are indexed by their registry `key`: `id` repeats across families (an entity id is
|
|
2241
|
+
// also an api-entity id), while `key` is unique inside a module.
|
|
2242
|
+
for (const host of extensionSurfaces.hosts) {
|
|
2243
|
+
provenanceCandidates.push({
|
|
2244
|
+
kind: 'extension-host',
|
|
2245
|
+
id: host.key,
|
|
2246
|
+
...(host.source.kind === 'fact-ref' ? {} : { source: { sourcePath: host.source.path } }),
|
|
2247
|
+
factRef: { factSection: 'extensionSurfaces.hosts', factKey: host.key },
|
|
2248
|
+
})
|
|
2249
|
+
}
|
|
2250
|
+
for (const contribution of extensionSurfaces.contributions) {
|
|
2251
|
+
pushRef(
|
|
2252
|
+
'extension-contribution',
|
|
2253
|
+
contribution.id,
|
|
2254
|
+
{ sourcePath: contribution.source.path },
|
|
2255
|
+
'extensionSurfaces.contributions',
|
|
2256
|
+
contribution.id,
|
|
2257
|
+
)
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const provenance = buildProvenanceIndex(provenanceCandidates)
|
|
2261
|
+
const factDiagnostics = [
|
|
2262
|
+
...provenance.diagnostics,
|
|
2263
|
+
...workers.unresolved,
|
|
2264
|
+
...pageMiddleware.unresolved,
|
|
2265
|
+
...generatorPlugins.unresolved,
|
|
2266
|
+
...diRegistrations.unresolvedTokens.map((token): ModuleFactDiagnostic => ({
|
|
2267
|
+
code: 'unresolved-static-contract',
|
|
2268
|
+
kind: 'di-registration',
|
|
2269
|
+
id: token,
|
|
2270
|
+
...(diSourcePath ? { source: { sourcePath: diSourcePath, exportName: token } } : {}),
|
|
2271
|
+
})),
|
|
2272
|
+
].sort(
|
|
2273
|
+
(left, right) =>
|
|
2274
|
+
left.code.localeCompare(right.code) ||
|
|
2275
|
+
left.kind.localeCompare(right.kind) ||
|
|
2276
|
+
left.id.localeCompare(right.id),
|
|
2277
|
+
)
|
|
2278
|
+
|
|
2279
|
+
const facts: ModuleFacts = {
|
|
2280
|
+
module: moduleId,
|
|
2281
|
+
title,
|
|
2282
|
+
description,
|
|
2283
|
+
coreVersion,
|
|
2284
|
+
sourcePackage,
|
|
2285
|
+
sourceVersion,
|
|
2286
|
+
sourceRoot,
|
|
2287
|
+
...(options.sourceKind === 'local-reference' ? { sourceKind: options.sourceKind } : {}),
|
|
2288
|
+
entities,
|
|
2289
|
+
events,
|
|
2290
|
+
aclFeatures,
|
|
2291
|
+
apiRoutes,
|
|
2292
|
+
diTokens,
|
|
2293
|
+
searchEntities,
|
|
2294
|
+
hostTokens,
|
|
2295
|
+
notifications,
|
|
2296
|
+
cli,
|
|
2297
|
+
backendPages,
|
|
2298
|
+
frontendPages,
|
|
2299
|
+
cliCommands,
|
|
2300
|
+
aiTools,
|
|
2301
|
+
aiAgents,
|
|
2302
|
+
extensionSurfaces,
|
|
2303
|
+
...(provenance.factSources.length > 0 ? { factSources: provenance.factSources } : {}),
|
|
2304
|
+
...(ownedContracts ? { ownedContracts } : {}),
|
|
2305
|
+
...(factDiagnostics.length > 0 ? { factDiagnostics } : {}),
|
|
2306
|
+
warnings,
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// Exact unified override targets (spec 2026-08-02-module-facts-exact-override-targets).
|
|
2310
|
+
// Adapters consume the fully-populated owned facts + contributions above.
|
|
2311
|
+
const overrides = collectModuleOverrideTargets(facts, buildFactSourceLookup(facts))
|
|
2312
|
+
if (overrides.overrideTargets.length > 0) facts.overrideTargets = overrides.overrideTargets
|
|
2313
|
+
if (overrides.overrideTargetDiagnostics.length > 0) {
|
|
2314
|
+
facts.overrideTargetDiagnostics = overrides.overrideTargetDiagnostics
|
|
2315
|
+
}
|
|
2316
|
+
return facts
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
export interface ModuleFactsJsonEvent {
|
|
2320
|
+
id: string
|
|
2321
|
+
category: string | null
|
|
2322
|
+
entity: string | null
|
|
2323
|
+
clientBroadcast?: boolean
|
|
2324
|
+
portalBroadcast?: boolean
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
export interface ModuleFactsJsonEntry {
|
|
2328
|
+
title: string | null
|
|
2329
|
+
description: string | null
|
|
2330
|
+
coreVersion: string | null
|
|
2331
|
+
sourcePackage: string | null
|
|
2332
|
+
sourceVersion: string | null
|
|
2333
|
+
sourceRoot: string
|
|
2334
|
+
/** Present only for `local-reference` projections; package sidecar entries omit it. */
|
|
2335
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
2336
|
+
entities: ModuleEntityFact[]
|
|
2337
|
+
events: ModuleFactsJsonEvent[]
|
|
2338
|
+
aclFeatures: string[]
|
|
2339
|
+
apiRoutes: ModuleApiRouteFact[]
|
|
2340
|
+
diTokens: string[]
|
|
2341
|
+
searchEntities: string[]
|
|
2342
|
+
hostTokens: ModuleHostTokens
|
|
2343
|
+
notifications: string[]
|
|
2344
|
+
cli: string[]
|
|
2345
|
+
backendPages: ModulePageFact[]
|
|
2346
|
+
frontendPages: ModulePageFact[]
|
|
2347
|
+
cliCommands: ModuleCliCommandFact[]
|
|
2348
|
+
aiTools: ModuleAiToolFact[]
|
|
2349
|
+
aiAgents: ModuleAiAgentFact[]
|
|
2350
|
+
extensionSurfaces?: ModuleExtensionSurfaceFacts
|
|
2351
|
+
factSources?: ModuleFactSourceEntry[]
|
|
2352
|
+
ownedContracts?: ModuleOwnedContracts
|
|
2353
|
+
factDiagnostics?: ModuleFactDiagnostic[]
|
|
2354
|
+
overrideTargets?: ModuleOverrideTarget[]
|
|
2355
|
+
overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const EMPTY_SECTION_MARKER = '_none_'
|
|
2359
|
+
|
|
2360
|
+
function renderVersionStamp(
|
|
2361
|
+
coreVersion: string | null,
|
|
2362
|
+
sourcePackage: string | null,
|
|
2363
|
+
sourceVersion: string | null,
|
|
2364
|
+
): string {
|
|
2365
|
+
if (sourcePackage) {
|
|
2366
|
+
return `<!-- generated from ${sourcePackage} ${sourceVersion || '<unknown>'}; core ${coreVersion || '<unknown>'} — R1 staleness stamp -->`
|
|
2367
|
+
}
|
|
2368
|
+
const version = coreVersion && coreVersion.length > 0 ? coreVersion : '<unknown>'
|
|
2369
|
+
return `<!-- generated from @open-mercato/core ${version} — R1 staleness stamp -->`
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function renderEntitiesSection(entities: ModuleEntityFact[], lookup: FactSourceLookup): string {
|
|
2373
|
+
if (entities.length === 0) return `## Entities\n\n${EMPTY_SECTION_MARKER}`
|
|
2374
|
+
const header = '| Entity ID | Class | Table | Editable | CustomFields | Source |'
|
|
2375
|
+
const divider = '|---|---|---|---|---|---|'
|
|
2376
|
+
const rows = entities.map(
|
|
2377
|
+
(entity) =>
|
|
2378
|
+
`| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} | ${renderLookedUpSource(lookup, 'entity', entity.id)} |`,
|
|
2379
|
+
)
|
|
2380
|
+
return ['## Entities', '', header, divider, ...rows].join('\n')
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function renderEventsSection(events: ModuleEventFact[], lookup: FactSourceLookup): string {
|
|
2384
|
+
const heading = `## Events (${events.length})`
|
|
2385
|
+
if (events.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2386
|
+
const header = '| ID | Category | Entity | Browser transport | Source |'
|
|
2387
|
+
const divider = '|---|---|---|---|---|'
|
|
2388
|
+
const rows = events.map((event) => {
|
|
2389
|
+
const transports = [event.clientBroadcast ? 'client' : null, event.portalBroadcast ? 'portal' : null]
|
|
2390
|
+
.filter((value): value is string => value !== null)
|
|
2391
|
+
return `| ${event.id} | ${event.category ?? '—'} | ${event.entity ?? '—'} | ${transports.join(', ') || '—'} | ${renderLookedUpSource(lookup, 'event', event.id)} |`
|
|
2392
|
+
})
|
|
2393
|
+
return [heading, '', header, divider, ...rows].join('\n')
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function renderInlineListSection(heading: string, values: string[]): string {
|
|
2397
|
+
if (values.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2398
|
+
return `${heading}\n\n${values.join(' · ')}`
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function renderSourceLinkedListSection(
|
|
2402
|
+
heading: string,
|
|
2403
|
+
values: readonly string[],
|
|
2404
|
+
lookup: FactSourceLookup,
|
|
2405
|
+
kind: ModuleFactSourceKind,
|
|
2406
|
+
idColumnLabel: string,
|
|
2407
|
+
): string {
|
|
2408
|
+
if (values.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2409
|
+
const rows = values.map((value) => `| ${value} | ${renderLookedUpSource(lookup, kind, value)} |`)
|
|
2410
|
+
return [heading, '', `| ${idColumnLabel} | Source |`, '|---|---|', ...rows].join('\n')
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* A generated fact only earns a markdown link when it names an exact module code
|
|
2415
|
+
* file. Directory-valued provenance (the module source root, framework hosts that
|
|
2416
|
+
* only know their owning package folder) renders as plain text: a link to a folder
|
|
2417
|
+
* resolves to nothing in an editor or on the repository browser, so emitting one
|
|
2418
|
+
* costs a consumer a dead click instead of taking them to the declaration site.
|
|
2419
|
+
*/
|
|
2420
|
+
export function isExactSourceFilePath(sourcePath: string): boolean {
|
|
2421
|
+
return MODULE_CODE_EXTENSIONS.some((extension) => sourcePath.endsWith(extension))
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function renderSourceLink(sourcePath: string): string {
|
|
2425
|
+
if (!isExactSourceFilePath(sourcePath)) return sourcePath
|
|
2426
|
+
return `[${sourcePath}](../../../${sourcePath})`
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function renderSourceRefLink(source: ModuleFactSourceRef): string {
|
|
2430
|
+
const anchor = source.line ? `#L${source.line}` : ''
|
|
2431
|
+
const label = source.line ? `${source.sourcePath}:${source.line}` : source.sourcePath
|
|
2432
|
+
if (!isExactSourceFilePath(source.sourcePath)) return label
|
|
2433
|
+
return `[${label}](../../../${source.sourcePath}${anchor})`
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
|
|
2437
|
+
function renderLookedUpSource(lookup: FactSourceLookup, kind: ModuleFactSourceKind, id: string): string {
|
|
2438
|
+
const source = lookup(kind, id)
|
|
2439
|
+
return source ? renderSourceRefLink(source) : '—'
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function renderSafeScalar(value: ModuleFactSafeScalar | ModuleFactSafeScalar[] | undefined): string {
|
|
2443
|
+
if (value === undefined || value === null) return '—'
|
|
2444
|
+
if (Array.isArray(value)) return value.length > 0 ? value.map((entry) => String(entry)).join(', ') : '—'
|
|
2445
|
+
return String(value)
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function renderOwnedContractMetadata(metadata: ModuleOwnedContractFact['metadata']): string {
|
|
2449
|
+
if (!metadata) return '—'
|
|
2450
|
+
const keys = Object.keys(metadata).sort((left, right) => left.localeCompare(right))
|
|
2451
|
+
if (keys.length === 0) return '—'
|
|
2452
|
+
return keys.map((key) => `${key}=${renderSafeScalar(metadata[key])}`).join('; ')
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function renderOwnedContractSection(
|
|
2456
|
+
heading: string,
|
|
2457
|
+
facts: readonly ModuleOwnedContractFact[] | undefined,
|
|
2458
|
+
): string {
|
|
2459
|
+
if (!facts || facts.length === 0) return ''
|
|
2460
|
+
const rows = facts.map(
|
|
2461
|
+
(fact) => `| ${fact.id} | ${renderOwnedContractMetadata(fact.metadata)} | ${renderSourceRefLink(fact.source)} |`,
|
|
2462
|
+
)
|
|
2463
|
+
return [heading, '', '| ID | Metadata | Source |', '|---|---|---|', ...rows].join('\n')
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function renderDiRegistrationsSection(facts: readonly ModuleOwnedContractFact[] | undefined): string {
|
|
2467
|
+
if (!facts || facts.length === 0) return ''
|
|
2468
|
+
const rows = facts.map((fact) => {
|
|
2469
|
+
const metadata = fact.metadata ?? {}
|
|
2470
|
+
return `| ${fact.id} | ${renderSafeScalar(metadata.registrationKind)} | ${renderSafeScalar(metadata.lifetime)} | ${renderSafeScalar(metadata.injectionMode)} | ${renderSafeScalar(metadata.providerSymbol)} | ${renderSourceRefLink(fact.source)} |`
|
|
2471
|
+
})
|
|
2472
|
+
return [
|
|
2473
|
+
'## DI registrations (rich)',
|
|
2474
|
+
'',
|
|
2475
|
+
'| Token | Kind | Lifetime | Injection | Provider | Source |',
|
|
2476
|
+
'|---|---|---|---|---|---|',
|
|
2477
|
+
...rows,
|
|
2478
|
+
].join('\n')
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function describeAuthRule(rule: ApiRouteAuthRule | undefined): string {
|
|
2482
|
+
if (!rule) return 'public'
|
|
2483
|
+
if (rule.requireFeatures && rule.requireFeatures.length > 0) return rule.requireFeatures.join(', ')
|
|
2484
|
+
if (rule.requireRoles && rule.requireRoles.length > 0) return rule.requireRoles.join(', ')
|
|
2485
|
+
if (rule.requireAuth) return 'auth'
|
|
2486
|
+
return 'public'
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
function renderApiRouteAuthCell(route: ModuleApiRouteFact): string {
|
|
2490
|
+
if (route.methods.length === 0) return '—'
|
|
2491
|
+
const groups: Array<{ label: string; methods: string[] }> = []
|
|
2492
|
+
for (const method of route.methods) {
|
|
2493
|
+
const label = describeAuthRule(route.auth[method])
|
|
850
2494
|
const existing = groups.find((group) => group.label === label)
|
|
851
2495
|
if (existing) existing.methods.push(method)
|
|
852
2496
|
else groups.push({ label, methods: [method] })
|
|
@@ -856,44 +2500,284 @@ function renderApiRouteAuthCell(route: ModuleApiRouteFact): string {
|
|
|
856
2500
|
|
|
857
2501
|
function renderApiRoutesSection(routes: ModuleApiRouteFact[]): string {
|
|
858
2502
|
if (routes.length === 0) return `## API routes\n\n${EMPTY_SECTION_MARKER}`
|
|
859
|
-
const header = '| Path | Methods | Auth (per-method requireFeatures) |'
|
|
860
|
-
const divider = '
|
|
2503
|
+
const header = '| Path | Methods | Auth (per-method requireFeatures) | Source |'
|
|
2504
|
+
const divider = '|---|---|---|---|'
|
|
861
2505
|
const rows = routes.map(
|
|
862
|
-
(route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} |`,
|
|
2506
|
+
(route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} | ${route.sourcePath ? renderSourceLink(route.sourcePath) : '—'} |`,
|
|
863
2507
|
)
|
|
864
2508
|
return ['## API routes', '', header, divider, ...rows].join('\n')
|
|
865
2509
|
}
|
|
866
2510
|
|
|
2511
|
+
function renderLinkedFactsSection(
|
|
2512
|
+
heading: string,
|
|
2513
|
+
facts: ReadonlyArray<{ label: string; sourcePath: string }>,
|
|
2514
|
+
): string {
|
|
2515
|
+
if (facts.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2516
|
+
const rows = facts.map((fact) => `| ${fact.label} | ${renderSourceLink(fact.sourcePath)} |`)
|
|
2517
|
+
return [heading, '', '| ID / path | Source |', '|---|---|', ...rows].join('\n')
|
|
2518
|
+
}
|
|
2519
|
+
|
|
867
2520
|
function renderHostTokensSection(hostTokens: ModuleHostTokens): string {
|
|
868
2521
|
const entityIdsLine = hostTokens.entityIds.length > 0 ? hostTokens.entityIds.join(' · ') : EMPTY_SECTION_MARKER
|
|
869
2522
|
const tableIdsLine = hostTokens.tableIds.length > 0 ? hostTokens.tableIds.join(' · ') : EMPTY_SECTION_MARKER
|
|
870
2523
|
return ['## Host extension points', '', `- Entity IDs: ${entityIdsLine}`, `- Table IDs: ${tableIdsLine}`].join('\n')
|
|
871
2524
|
}
|
|
872
2525
|
|
|
873
|
-
|
|
2526
|
+
function renderExtensionHostContext(host: ModuleExtensionSurfaceFacts['hosts'][number]): string {
|
|
2527
|
+
return host.contextContract ?? host.runtimeContract ?? host.scopeContract ?? '—'
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function renderExtensionHostSource(host: ModuleExtensionHostFact): string {
|
|
2531
|
+
if (host.source.kind === 'fact-ref') return `${host.source.factSection}:${host.source.factKey}`
|
|
2532
|
+
return renderSourceRefLink({ sourcePath: host.source.path })
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function renderExtensionHostsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2536
|
+
const boundHosts = extensionSurfaces.hosts.filter((host) => host.bound)
|
|
2537
|
+
if (boundHosts.length === 0) return `## UMES hosts\n\n${EMPTY_SECTION_MARKER}`
|
|
2538
|
+
const rows = boundHosts.map((host) =>
|
|
2539
|
+
`| ${host.id} | ${host.family} | ${host.capabilities.join(', ') || '—'} | ${renderExtensionHostContext(host)} | ${host.stability.toUpperCase()} | ${renderExtensionHostSource(host)} |`,
|
|
2540
|
+
)
|
|
2541
|
+
return [
|
|
2542
|
+
'## UMES hosts',
|
|
2543
|
+
'',
|
|
2544
|
+
'| ID / pattern | Family | Supports | Context | Stability | Source |',
|
|
2545
|
+
'|---|---|---|---|---|---|',
|
|
2546
|
+
...rows,
|
|
2547
|
+
].join('\n')
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function compactContributionDetails(contribution: ModuleExtensionContributionFact): string {
|
|
2551
|
+
const details = contribution.details as unknown as Record<string, unknown>
|
|
2552
|
+
return Object.keys(details).sort((left, right) => left.localeCompare(right)).flatMap((key) => {
|
|
2553
|
+
const value = details[key]
|
|
2554
|
+
if (value === undefined) return []
|
|
2555
|
+
if (Array.isArray(value)) return [`${key}=${value.join(',') || 'none'}`]
|
|
2556
|
+
if (value && typeof value === 'object') {
|
|
2557
|
+
const nested = Object.keys(value as Record<string, unknown>).sort((left, right) => left.localeCompare(right)).map((nestedKey) => {
|
|
2558
|
+
const nestedValue = (value as Record<string, unknown>)[nestedKey]
|
|
2559
|
+
return `${nestedKey}:${Array.isArray(nestedValue) ? nestedValue.join(',') : String(nestedValue)}`
|
|
2560
|
+
})
|
|
2561
|
+
return [`${key}={${nested.join(';')}}`]
|
|
2562
|
+
}
|
|
2563
|
+
return [`${key}=${String(value)}`]
|
|
2564
|
+
}).join('; ')
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function renderExtensionContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2568
|
+
if (extensionSurfaces.contributions.length === 0) return `## UMES contributions\n\n${EMPTY_SECTION_MARKER}`
|
|
2569
|
+
const rows = extensionSurfaces.contributions.map((contribution) => {
|
|
2570
|
+
const targets = contribution.targets.map((entry) => entry.id).join(', ')
|
|
2571
|
+
const resolution = contribution.targets.map((entry) => entry.resolution).join(', ')
|
|
2572
|
+
const phases = [...(contribution.phases ?? []), ...(contribution.operations ?? [])].join(', ') || '—'
|
|
2573
|
+
const source = renderSourceRefLink({ sourcePath: contribution.source.path })
|
|
2574
|
+
return `| ${contribution.id} | ${contribution.kind} | ${targets || '—'} | ${phases} | ${compactContributionDetails(contribution)} | ${resolution || '—'} | ${source} |`
|
|
2575
|
+
})
|
|
2576
|
+
return [
|
|
2577
|
+
'## UMES contributions',
|
|
2578
|
+
'',
|
|
2579
|
+
'| ID | Kind | Target | Phase / operations | Contract | Resolution | Source |',
|
|
2580
|
+
'|---|---|---|---|---|---|---|',
|
|
2581
|
+
...rows,
|
|
2582
|
+
].join('\n')
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
function renderExtensionDiagnosticsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2586
|
+
const unbound = extensionSurfaces.hosts.filter((host) => !host.bound)
|
|
2587
|
+
if (unbound.length === 0 && extensionSurfaces.unresolved.length === 0) return ''
|
|
2588
|
+
const diagnostics = [
|
|
2589
|
+
...unbound.map((host) => `- unbound-helper: ${host.id}`),
|
|
2590
|
+
...extensionSurfaces.unresolved.map((entry) => `- ${entry.reason}: ${entry.key} (${entry.source.path})`),
|
|
2591
|
+
]
|
|
2592
|
+
return ['## UMES diagnostics', '', ...diagnostics].join('\n')
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
function renderExtensionTargetRef(target: ModuleExtensionTargetRef): string {
|
|
2596
|
+
const method = target.method ? ` ${target.method}` : ''
|
|
2597
|
+
const owner = target.moduleId ? ` @${target.moduleId}` : ''
|
|
2598
|
+
return `${target.kind}:${target.id}${method}${owner}`
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
function renderActivationBindingsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2602
|
+
const activations = extensionSurfaces.activations ?? []
|
|
2603
|
+
if (activations.length === 0) return `## Active extension bindings\n\n${EMPTY_SECTION_MARKER}`
|
|
2604
|
+
const rows = activations.map((activation) => {
|
|
2605
|
+
const phases = activation.phases && activation.phases.length > 0 ? activation.phases.join(', ') : '—'
|
|
2606
|
+
const source = activation.source ? renderSourceRefLink(activation.source) : '—'
|
|
2607
|
+
const bridge = activation.bridge ? `${activation.bridge.factSection}:${activation.bridge.factKey}` : '—'
|
|
2608
|
+
return `| ${activation.id} | ${activation.kind} | ${renderExtensionTargetRef(activation.host)} | ${activation.contributionKinds.join(', ') || '—'} | ${phases} | ${bridge} | ${source} |`
|
|
2609
|
+
})
|
|
2610
|
+
return [
|
|
2611
|
+
'## Active extension bindings',
|
|
2612
|
+
'',
|
|
2613
|
+
'| Activation | Kind | Host | Contribution kinds | Phases | Bridge | Source |',
|
|
2614
|
+
'|---|---|---|---|---|---|---|',
|
|
2615
|
+
...rows,
|
|
2616
|
+
].join('\n')
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
function renderIncomingContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2620
|
+
const incoming = extensionSurfaces.incoming ?? []
|
|
2621
|
+
if (incoming.length === 0) return `## Incoming installed contributions\n\n${EMPTY_SECTION_MARKER}`
|
|
2622
|
+
const rows = incoming.map((entry) => {
|
|
2623
|
+
const activation = entry.activationId ?? '—'
|
|
2624
|
+
const source = entry.source ? renderSourceRefLink(entry.source) : '—'
|
|
2625
|
+
return `| ${entry.contributorModuleId} | ${entry.contributionKind} | ${renderExtensionTargetRef(entry.target)} | ${entry.resolution} | ${activation} | ${entry.contributionId} · ${source} |`
|
|
2626
|
+
})
|
|
2627
|
+
return [
|
|
2628
|
+
'## Incoming installed contributions',
|
|
2629
|
+
'',
|
|
2630
|
+
'| Contributor | Kind | Target | Resolution | Activation | Contribution · Source |',
|
|
2631
|
+
'|---|---|---|---|---|---|',
|
|
2632
|
+
...rows,
|
|
2633
|
+
].join('\n')
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function renderContributionResolutionsSection(
|
|
2637
|
+
extensionSurfaces: ModuleExtensionSurfaceFacts,
|
|
2638
|
+
lookup: FactSourceLookup,
|
|
2639
|
+
): string {
|
|
2640
|
+
const resolutions = extensionSurfaces.contributionResolutions ?? []
|
|
2641
|
+
if (resolutions.length === 0) return `## Contribution resolutions\n\n${EMPTY_SECTION_MARKER}`
|
|
2642
|
+
const rows = resolutions.map((resolution) => {
|
|
2643
|
+
const activations = resolution.activationIds.length > 0 ? resolution.activationIds.join(', ') : '—'
|
|
2644
|
+
const source = renderLookedUpSource(lookup, 'extension-contribution', resolution.contributionId)
|
|
2645
|
+
return `| ${resolution.contributionId} | ${renderExtensionTargetRef(resolution.target)} | ${resolution.resolution} | ${activations} | ${source} |`
|
|
2646
|
+
})
|
|
2647
|
+
return [
|
|
2648
|
+
'## Contribution resolutions',
|
|
2649
|
+
'',
|
|
2650
|
+
'| Contribution | Target | Resolution | Activations | Source |',
|
|
2651
|
+
'|---|---|---|---|---|',
|
|
2652
|
+
...rows,
|
|
2653
|
+
].join('\n')
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
function renderOverrideTargetPath(target: ModuleOverrideTarget): string {
|
|
2657
|
+
let display = 'overrides'
|
|
2658
|
+
for (let index = 0; index < target.path.length; index += 1) {
|
|
2659
|
+
const segment = target.path[index]
|
|
2660
|
+
const isKeyedTerminal = target.key !== undefined && index === target.path.length - 1
|
|
2661
|
+
display += isKeyedTerminal ? `[${JSON.stringify(segment)}]` : `.${segment}`
|
|
2662
|
+
}
|
|
2663
|
+
return display
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
function renderOverrideTargetsSection(targets: readonly ModuleOverrideTarget[] | undefined): string {
|
|
2667
|
+
if (!targets || targets.length === 0) return `## Exact override targets\n\n${EMPTY_SECTION_MARKER}`
|
|
2668
|
+
const header = '| Domain | Path / key | Supported modes | Referenced fact | Source |'
|
|
2669
|
+
const divider = '|---|---|---|---|---|'
|
|
2670
|
+
const rows = targets.map((target) => {
|
|
2671
|
+
const path = `\`${renderOverrideTargetPath(target)}\``
|
|
2672
|
+
const modes = target.modes.join(', ') || '—'
|
|
2673
|
+
const fact = `${target.factRef.factSection}:${target.factRef.factKey}`
|
|
2674
|
+
return `| ${target.domain} | ${path} | ${modes} | ${fact} | ${renderSourceRefLink(target.source)} |`
|
|
2675
|
+
})
|
|
2676
|
+
return ['## Exact override targets', '', header, divider, ...rows].join('\n')
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
export interface ReferenceProjectionFingerprints {
|
|
2680
|
+
sourceFingerprint: string
|
|
2681
|
+
taxonomyFingerprint: string
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/**
|
|
2685
|
+
* Header for a `local-reference` projection. It never claims a package version — the
|
|
2686
|
+
* module is app-local — and it states in the document itself that the activations and
|
|
2687
|
+
* exact override targets below describe behavior only after the module is opted in,
|
|
2688
|
+
* because the artifact is generated while the module is disabled.
|
|
2689
|
+
*/
|
|
2690
|
+
function renderReferenceProjectionHeader(
|
|
2691
|
+
facts: ModuleFacts,
|
|
2692
|
+
fingerprints: ReferenceProjectionFingerprints,
|
|
2693
|
+
): string[] {
|
|
2694
|
+
return [
|
|
2695
|
+
`# ${facts.module} — module facts (generated reference projection, do not edit)`,
|
|
2696
|
+
`<!-- generated from local reference ${facts.sourceRoot}; source-fingerprint ${fingerprints.sourceFingerprint}; taxonomy-fingerprint ${fingerprints.taxonomyFingerprint} — R1 staleness stamp -->`,
|
|
2697
|
+
`> Reference projection: \`${facts.module}\` is not selected by the current runtime. Its activations and exact override targets describe behavior only after opt-in in \`src/modules.ts\`.`,
|
|
2698
|
+
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
2699
|
+
]
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
export function renderModuleFactsMarkdown(
|
|
2703
|
+
facts: ModuleFacts,
|
|
2704
|
+
referenceProjection?: ReferenceProjectionFingerprints,
|
|
2705
|
+
): string {
|
|
2706
|
+
const extensionSurfaces = facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] }
|
|
2707
|
+
const lookup = buildFactSourceLookup(facts)
|
|
2708
|
+
const header = facts.sourceKind === 'local-reference' && referenceProjection
|
|
2709
|
+
? renderReferenceProjectionHeader(facts, referenceProjection)
|
|
2710
|
+
: [
|
|
2711
|
+
`# ${facts.module} — module facts (generated, do not edit)`,
|
|
2712
|
+
renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
|
|
2713
|
+
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
2714
|
+
]
|
|
874
2715
|
const sections = [
|
|
875
|
-
|
|
876
|
-
renderVersionStamp(facts.coreVersion),
|
|
2716
|
+
...header,
|
|
877
2717
|
'',
|
|
878
|
-
renderEntitiesSection(facts.entities),
|
|
2718
|
+
renderEntitiesSection(facts.entities, lookup),
|
|
879
2719
|
'',
|
|
880
|
-
renderEventsSection(facts.events),
|
|
2720
|
+
renderEventsSection(facts.events, lookup),
|
|
881
2721
|
'',
|
|
882
|
-
|
|
2722
|
+
renderSourceLinkedListSection(
|
|
2723
|
+
`## ACL features (${facts.aclFeatures.length})`,
|
|
2724
|
+
facts.aclFeatures,
|
|
2725
|
+
lookup,
|
|
2726
|
+
'acl-feature',
|
|
2727
|
+
'Feature',
|
|
2728
|
+
),
|
|
883
2729
|
'',
|
|
884
2730
|
renderApiRoutesSection(facts.apiRoutes),
|
|
885
2731
|
'',
|
|
886
|
-
|
|
2732
|
+
renderLinkedFactsSection('## Backend pages', facts.backendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
887
2733
|
'',
|
|
888
|
-
|
|
2734
|
+
renderLinkedFactsSection('## Frontend pages', facts.frontendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
2735
|
+
'',
|
|
2736
|
+
renderSourceLinkedListSection('## DI service tokens', facts.diTokens, lookup, 'di-registration', 'Token'),
|
|
2737
|
+
'',
|
|
2738
|
+
renderSourceLinkedListSection('## Search entities', facts.searchEntities, lookup, 'search', 'Entity ID'),
|
|
889
2739
|
'',
|
|
890
2740
|
renderHostTokensSection(facts.hostTokens),
|
|
891
2741
|
'',
|
|
892
|
-
|
|
2742
|
+
renderExtensionHostsSection(extensionSurfaces),
|
|
2743
|
+
'',
|
|
2744
|
+
renderExtensionContributionsSection(extensionSurfaces),
|
|
2745
|
+
'',
|
|
2746
|
+
renderActivationBindingsSection(extensionSurfaces),
|
|
2747
|
+
'',
|
|
2748
|
+
renderIncomingContributionsSection(extensionSurfaces),
|
|
2749
|
+
'',
|
|
2750
|
+
renderContributionResolutionsSection(extensionSurfaces, lookup),
|
|
2751
|
+
'',
|
|
2752
|
+
renderExtensionDiagnosticsSection(extensionSurfaces),
|
|
2753
|
+
'',
|
|
2754
|
+
renderSourceLinkedListSection('## Notifications', facts.notifications, lookup, 'notification', 'Notification ID'),
|
|
2755
|
+
'',
|
|
2756
|
+
renderLinkedFactsSection('## CLI commands', facts.cliCommands.map((command) => ({ label: command.command, sourcePath: command.sourcePath }))),
|
|
2757
|
+
'',
|
|
2758
|
+
renderLinkedFactsSection('## AI tools / MCP capabilities', facts.aiTools.map((tool) => ({ label: tool.name, sourcePath: tool.sourcePath }))),
|
|
893
2759
|
'',
|
|
894
|
-
|
|
2760
|
+
renderLinkedFactsSection('## AI agents', facts.aiAgents.map((agent) => ({ label: agent.id, sourcePath: agent.sourcePath }))),
|
|
2761
|
+
'',
|
|
2762
|
+
renderOverrideTargetsSection(facts.overrideTargets),
|
|
895
2763
|
'',
|
|
896
2764
|
]
|
|
2765
|
+
const owned = facts.ownedContracts ?? {}
|
|
2766
|
+
const ownedSections = [
|
|
2767
|
+
renderOwnedContractSection('## Owned contract — module metadata', owned['module-metadata']),
|
|
2768
|
+
renderOwnedContractSection('## Domain commands', owned.command),
|
|
2769
|
+
renderOwnedContractSection('## Workers', owned.worker),
|
|
2770
|
+
renderOwnedContractSection('## Page middleware', owned['page-middleware']),
|
|
2771
|
+
renderOwnedContractSection('## Setup', owned.setup),
|
|
2772
|
+
renderOwnedContractSection('## Encryption', owned.encryption),
|
|
2773
|
+
renderDiRegistrationsSection(owned['di-registration']),
|
|
2774
|
+
renderOwnedContractSection('## Custom entities', owned['custom-entity']),
|
|
2775
|
+
renderOwnedContractSection('## AI extensions', owned['ai-extension']),
|
|
2776
|
+
renderOwnedContractSection('## Generator plugins', owned['generator-plugin']),
|
|
2777
|
+
].filter((section) => section.length > 0)
|
|
2778
|
+
for (const section of ownedSections) {
|
|
2779
|
+
sections.push(section, '')
|
|
2780
|
+
}
|
|
897
2781
|
return sections.join('\n')
|
|
898
2782
|
}
|
|
899
2783
|
|
|
@@ -902,8 +2786,18 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
902
2786
|
title: facts.title,
|
|
903
2787
|
description: facts.description,
|
|
904
2788
|
coreVersion: facts.coreVersion,
|
|
2789
|
+
sourcePackage: facts.sourcePackage,
|
|
2790
|
+
sourceVersion: facts.sourceVersion,
|
|
2791
|
+
sourceRoot: facts.sourceRoot,
|
|
2792
|
+
...(facts.sourceKind === 'local-reference' ? { sourceKind: facts.sourceKind } : {}),
|
|
905
2793
|
entities: facts.entities,
|
|
906
|
-
events: facts.events.map((event) => ({
|
|
2794
|
+
events: facts.events.map((event) => ({
|
|
2795
|
+
id: event.id,
|
|
2796
|
+
category: event.category,
|
|
2797
|
+
entity: event.entity,
|
|
2798
|
+
...(event.clientBroadcast !== undefined ? { clientBroadcast: event.clientBroadcast } : {}),
|
|
2799
|
+
...(event.portalBroadcast !== undefined ? { portalBroadcast: event.portalBroadcast } : {}),
|
|
2800
|
+
})),
|
|
907
2801
|
aclFeatures: facts.aclFeatures,
|
|
908
2802
|
apiRoutes: facts.apiRoutes,
|
|
909
2803
|
diTokens: facts.diTokens,
|
|
@@ -911,6 +2805,25 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
911
2805
|
hostTokens: facts.hostTokens,
|
|
912
2806
|
notifications: facts.notifications,
|
|
913
2807
|
cli: facts.cli,
|
|
2808
|
+
backendPages: facts.backendPages,
|
|
2809
|
+
frontendPages: facts.frontendPages,
|
|
2810
|
+
cliCommands: facts.cliCommands,
|
|
2811
|
+
aiTools: facts.aiTools,
|
|
2812
|
+
aiAgents: facts.aiAgents,
|
|
2813
|
+
...(facts.extensionSurfaces ? { extensionSurfaces: facts.extensionSurfaces } : {}),
|
|
2814
|
+
...(facts.factSources && facts.factSources.length > 0 ? { factSources: facts.factSources } : {}),
|
|
2815
|
+
...(facts.ownedContracts && Object.keys(facts.ownedContracts).length > 0
|
|
2816
|
+
? { ownedContracts: facts.ownedContracts }
|
|
2817
|
+
: {}),
|
|
2818
|
+
...(facts.factDiagnostics && facts.factDiagnostics.length > 0
|
|
2819
|
+
? { factDiagnostics: facts.factDiagnostics }
|
|
2820
|
+
: {}),
|
|
2821
|
+
...(facts.overrideTargets && facts.overrideTargets.length > 0
|
|
2822
|
+
? { overrideTargets: facts.overrideTargets }
|
|
2823
|
+
: {}),
|
|
2824
|
+
...(facts.overrideTargetDiagnostics && facts.overrideTargetDiagnostics.length > 0
|
|
2825
|
+
? { overrideTargetDiagnostics: facts.overrideTargetDiagnostics }
|
|
2826
|
+
: {}),
|
|
914
2827
|
}
|
|
915
2828
|
}
|
|
916
2829
|
|
|
@@ -928,6 +2841,273 @@ export function renderModuleFactsJson(factsByModule: Record<string, ModuleFacts>
|
|
|
928
2841
|
return `${JSON.stringify(buildModuleFactsJsonObject(factsByModule), null, 2)}\n`
|
|
929
2842
|
}
|
|
930
2843
|
|
|
2844
|
+
/**
|
|
2845
|
+
* The normal package sidecar (`module-facts.json`) and the enabled-filtered package
|
|
2846
|
+
* markdown subset carry package-provided modules only. An app-local module reaching
|
|
2847
|
+
* either output would publish `bound` activations and exact override targets for code
|
|
2848
|
+
* the runtime does not select, so it fails the build instead of shipping.
|
|
2849
|
+
*/
|
|
2850
|
+
export function assertPackageModuleFactsOnly(factsByModule: Record<string, ModuleFacts>): void {
|
|
2851
|
+
const offenders = Object.values(factsByModule)
|
|
2852
|
+
.filter((facts) => facts.sourceKind === 'local-reference' || !facts.sourceRoot.startsWith('node_modules/'))
|
|
2853
|
+
.map((facts) => `${facts.module} (${facts.sourceRoot})`)
|
|
2854
|
+
.sort((left, right) => left.localeCompare(right))
|
|
2855
|
+
if (offenders.length > 0) {
|
|
2856
|
+
throw new Error(
|
|
2857
|
+
`[module-facts] normal package output must not contain app-local modules: ${offenders.join(', ')}`,
|
|
2858
|
+
)
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
function sha256Hex(value: string | Buffer): string {
|
|
2863
|
+
return createHash('sha256').update(value).digest('hex')
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
/**
|
|
2867
|
+
* Canonical form used by both reference fingerprints: object keys are recursively sorted
|
|
2868
|
+
* and arrays are sorted by their own canonical text. Every array a fact entry emits is
|
|
2869
|
+
* set-valued (capabilities, phases, operations, modes, per-section fact lists), so a
|
|
2870
|
+
* single ordering rule keeps the fingerprint stable against extractor iteration order
|
|
2871
|
+
* without inventing per-field exceptions.
|
|
2872
|
+
*/
|
|
2873
|
+
function canonicalizeFingerprintValue(value: unknown): unknown {
|
|
2874
|
+
if (Array.isArray(value)) {
|
|
2875
|
+
return value
|
|
2876
|
+
.map((item) => {
|
|
2877
|
+
const canonical = canonicalizeFingerprintValue(item)
|
|
2878
|
+
return { canonical, text: JSON.stringify(canonical) ?? '' }
|
|
2879
|
+
})
|
|
2880
|
+
.sort((left, right) => (left.text < right.text ? -1 : left.text > right.text ? 1 : 0))
|
|
2881
|
+
.map((item) => item.canonical)
|
|
2882
|
+
}
|
|
2883
|
+
if (value !== null && typeof value === 'object') {
|
|
2884
|
+
const record = value as Record<string, unknown>
|
|
2885
|
+
const result: Record<string, unknown> = {}
|
|
2886
|
+
for (const key of Object.keys(record).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))) {
|
|
2887
|
+
if (record[key] === undefined) continue
|
|
2888
|
+
result[key] = canonicalizeFingerprintValue(record[key])
|
|
2889
|
+
}
|
|
2890
|
+
return result
|
|
2891
|
+
}
|
|
2892
|
+
return value
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function canonicalFingerprintJson(value: unknown): string {
|
|
2896
|
+
return JSON.stringify(canonicalizeFingerprintValue(value))
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
const REFERENCE_FINGERPRINT_SKIPPED_DIRECTORIES = new Set(['__tests__', '__integration__', 'node_modules'])
|
|
2900
|
+
|
|
2901
|
+
/**
|
|
2902
|
+
* SHA-256 over the canonical JSON of `{ path, sha256 }` records sorted by path, where
|
|
2903
|
+
* `path` is the portable app-relative path so the value is identical across the
|
|
2904
|
+
* authoring, template, and emitted roots. Test-only directories are excluded because the
|
|
2905
|
+
* scaffold does not emit them, so including them would make the emitted app's recomputed
|
|
2906
|
+
* fingerprint disagree with the one built from the template.
|
|
2907
|
+
*/
|
|
2908
|
+
export function computeModuleSourceFingerprint(moduleRoot: string, portableSourceRoot: string): string {
|
|
2909
|
+
assertPortableSourceRoot(path.basename(portableSourceRoot), portableSourceRoot)
|
|
2910
|
+
const records: { path: string; sha256: string }[] = []
|
|
2911
|
+
const walk = (directory: string): void => {
|
|
2912
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
2913
|
+
for (const entry of entries) {
|
|
2914
|
+
const fullPath = path.join(directory, entry.name)
|
|
2915
|
+
if (entry.isDirectory()) {
|
|
2916
|
+
if (REFERENCE_FINGERPRINT_SKIPPED_DIRECTORIES.has(entry.name)) continue
|
|
2917
|
+
walk(fullPath)
|
|
2918
|
+
continue
|
|
2919
|
+
}
|
|
2920
|
+
if (!entry.isFile()) continue
|
|
2921
|
+
records.push({
|
|
2922
|
+
path: toPortableSourcePath(moduleRoot, portableSourceRoot, fullPath),
|
|
2923
|
+
sha256: sha256Hex(fs.readFileSync(fullPath)),
|
|
2924
|
+
})
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
walk(moduleRoot)
|
|
2928
|
+
records.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
2929
|
+
return sha256Hex(canonicalFingerprintJson(records))
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
/**
|
|
2933
|
+
* Provenance and release metadata are not taxonomy: a version bump must not invalidate
|
|
2934
|
+
* the classification fingerprint. Every remaining section of the emitted entry is a
|
|
2935
|
+
* classified set and contributes.
|
|
2936
|
+
*/
|
|
2937
|
+
const TAXONOMY_EXCLUDED_SECTIONS = new Set([
|
|
2938
|
+
'title',
|
|
2939
|
+
'description',
|
|
2940
|
+
'coreVersion',
|
|
2941
|
+
'sourcePackage',
|
|
2942
|
+
'sourceVersion',
|
|
2943
|
+
'sourceRoot',
|
|
2944
|
+
'sourceKind',
|
|
2945
|
+
])
|
|
2946
|
+
|
|
2947
|
+
function collectTaxonomyRecords(
|
|
2948
|
+
setName: string,
|
|
2949
|
+
value: unknown,
|
|
2950
|
+
records: { setName: string; value: unknown }[],
|
|
2951
|
+
): void {
|
|
2952
|
+
if (value === undefined) return
|
|
2953
|
+
if (Array.isArray(value)) {
|
|
2954
|
+
for (const item of value) records.push({ setName, value: canonicalizeFingerprintValue(item) })
|
|
2955
|
+
return
|
|
2956
|
+
}
|
|
2957
|
+
if (value !== null && typeof value === 'object') {
|
|
2958
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
2959
|
+
collectTaxonomyRecords(`${setName}.${key}`, child, records)
|
|
2960
|
+
}
|
|
2961
|
+
return
|
|
2962
|
+
}
|
|
2963
|
+
records.push({ setName, value })
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
/**
|
|
2967
|
+
* SHA-256 over the canonical JSON of `{ setName, value }` records sorted by set name and
|
|
2968
|
+
* canonicalized value. Each record carries a complete literal or structured entry — key,
|
|
2969
|
+
* suffix, capabilities, `bound`, phases, operations included — so no delimiter-joined
|
|
2970
|
+
* tuple can collapse two distinct classifications into one fingerprint input.
|
|
2971
|
+
*/
|
|
2972
|
+
export function computeModuleTaxonomyFingerprint(entry: ModuleFactsJsonEntry): string {
|
|
2973
|
+
const records: { setName: string; value: unknown }[] = []
|
|
2974
|
+
for (const [setName, value] of Object.entries(entry as unknown as Record<string, unknown>)) {
|
|
2975
|
+
if (TAXONOMY_EXCLUDED_SECTIONS.has(setName)) continue
|
|
2976
|
+
collectTaxonomyRecords(setName, value, records)
|
|
2977
|
+
}
|
|
2978
|
+
const decorated = records
|
|
2979
|
+
.map((record) => ({ record, text: JSON.stringify(record.value) ?? '' }))
|
|
2980
|
+
.sort((left, right) =>
|
|
2981
|
+
left.record.setName < right.record.setName
|
|
2982
|
+
? -1
|
|
2983
|
+
: left.record.setName > right.record.setName
|
|
2984
|
+
? 1
|
|
2985
|
+
: left.text < right.text
|
|
2986
|
+
? -1
|
|
2987
|
+
: left.text > right.text
|
|
2988
|
+
? 1
|
|
2989
|
+
: 0,
|
|
2990
|
+
)
|
|
2991
|
+
return sha256Hex(JSON.stringify(decorated.map((item) => item.record)))
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
export interface ReferenceModuleFactsEntry {
|
|
2995
|
+
moduleId: string
|
|
2996
|
+
projectionKind: 'activated-reference'
|
|
2997
|
+
sourceKind: 'local-reference'
|
|
2998
|
+
runtimeSelected: false
|
|
2999
|
+
sourceFingerprint: string
|
|
3000
|
+
taxonomyFingerprint: string
|
|
3001
|
+
/**
|
|
3002
|
+
* The enum-derived classification of every taxonomy `facts` is expressed in, built
|
|
3003
|
+
* by the same generation run. It travels with the projection so a reader of the
|
|
3004
|
+
* emitted sheet can tell a value the example really emits from one that is only
|
|
3005
|
+
* reserved, pending, or reachable through a negative fixture.
|
|
3006
|
+
*/
|
|
3007
|
+
factCoverage: ModuleFactCoverageFamily[]
|
|
3008
|
+
facts: ModuleFactsJsonEntry
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
export type ReferenceModuleFactsJson = Record<string, ReferenceModuleFactsEntry>
|
|
3012
|
+
|
|
3013
|
+
export function renderReferenceModuleFactsJson(bundle: ReferenceModuleFactsJson): string {
|
|
3014
|
+
const ordered: ReferenceModuleFactsJson = {}
|
|
3015
|
+
for (const moduleId of Object.keys(bundle).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))) {
|
|
3016
|
+
ordered[moduleId] = bundle[moduleId]
|
|
3017
|
+
}
|
|
3018
|
+
return `${JSON.stringify(ordered, null, 2)}\n`
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
export interface ExtractLocalReferenceModuleFactsOptions {
|
|
3022
|
+
/** Package-provided sources, exactly as the normal batch receives them. */
|
|
3023
|
+
packageSources: readonly ModuleFactSource[]
|
|
3024
|
+
/** The app-local module projected into the reference bundle. */
|
|
3025
|
+
reference: ModuleFactSource
|
|
3026
|
+
registryPath?: string | null
|
|
3027
|
+
registrySource?: string | null
|
|
3028
|
+
coreVersion?: string | null
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
export interface LocalReferenceModuleFactsResult {
|
|
3032
|
+
entry: ReferenceModuleFactsEntry
|
|
3033
|
+
markdown: string
|
|
3034
|
+
warnings: string[]
|
|
3035
|
+
/**
|
|
3036
|
+
* First-party targets the activated projection could not correlate. They also remain in
|
|
3037
|
+
* the emitted `facts.extensionSurfaces.unresolved`, so the artifact states its own gaps
|
|
3038
|
+
* rather than presenting a partial surface as complete.
|
|
3039
|
+
*/
|
|
3040
|
+
unresolvedTargets: string[]
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
/**
|
|
3044
|
+
* Produces the reference projection of an app-local module from an explicitly activated,
|
|
3045
|
+
* disposable selection context: the local source is appended to the package sources and
|
|
3046
|
+
* the whole set runs through the same `extractAllModuleFacts` selection and correlation
|
|
3047
|
+
* batch, so incoming contributions and optional targets resolve exactly as they would
|
|
3048
|
+
* after opt-in. Merging an independently extracted object into a finished batch is
|
|
3049
|
+
* forbidden — it would silently drop both resolutions. The batch is disposable: the
|
|
3050
|
+
* caller keeps only this module's projection and never writes the rest, which is why the
|
|
3051
|
+
* normal package outputs stay byte-identical.
|
|
3052
|
+
*/
|
|
3053
|
+
export function extractLocalReferenceModuleFacts(
|
|
3054
|
+
options: ExtractLocalReferenceModuleFactsOptions,
|
|
3055
|
+
): LocalReferenceModuleFactsResult {
|
|
3056
|
+
const { reference } = options
|
|
3057
|
+
if (reference.sourceKind !== 'local-reference') {
|
|
3058
|
+
throw new Error(
|
|
3059
|
+
`[module-facts] reference source "${reference.moduleId}" must declare sourceKind "local-reference"`,
|
|
3060
|
+
)
|
|
3061
|
+
}
|
|
3062
|
+
const portableSourceRoot = reference.portableSourceRoot
|
|
3063
|
+
if (!portableSourceRoot) {
|
|
3064
|
+
throw new Error(`[module-facts] reference source "${reference.moduleId}" must declare portableSourceRoot`)
|
|
3065
|
+
}
|
|
3066
|
+
assertPortableSourceRoot(reference.moduleId, portableSourceRoot)
|
|
3067
|
+
|
|
3068
|
+
const sources = appendLocalReferenceModuleSource(options.packageSources, reference)
|
|
3069
|
+
const activated = extractAllModuleFacts({
|
|
3070
|
+
sources,
|
|
3071
|
+
registryPath: options.registryPath ?? null,
|
|
3072
|
+
registrySource: options.registrySource ?? null,
|
|
3073
|
+
coreVersion: options.coreVersion ?? null,
|
|
3074
|
+
// The disposable projection collects rather than throws: an app-local module can name
|
|
3075
|
+
// a target whose classification is still missing from the shared extension reader, and
|
|
3076
|
+
// aborting there would delete the whole reference bundle instead of publishing the
|
|
3077
|
+
// exact gap. Normal package output keeps the strict default.
|
|
3078
|
+
unresolvedFirstPartyTargets: 'collect',
|
|
3079
|
+
})
|
|
3080
|
+
const facts = activated.factsByModule[reference.moduleId]
|
|
3081
|
+
if (!facts) {
|
|
3082
|
+
throw new Error(`[module-facts] reference module "${reference.moduleId}" produced no facts`)
|
|
3083
|
+
}
|
|
3084
|
+
if (facts.sourceRoot !== portableSourceRoot) {
|
|
3085
|
+
throw new Error(
|
|
3086
|
+
`[module-facts] reference module "${reference.moduleId}" emitted non-portable source root "${facts.sourceRoot}"`,
|
|
3087
|
+
)
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
const jsonEntry = toModuleFactsJsonEntry(facts)
|
|
3091
|
+
const fingerprints: ReferenceProjectionFingerprints = {
|
|
3092
|
+
sourceFingerprint: computeModuleSourceFingerprint(reference.moduleRoot, portableSourceRoot),
|
|
3093
|
+
taxonomyFingerprint: computeModuleTaxonomyFingerprint(jsonEntry),
|
|
3094
|
+
}
|
|
3095
|
+
return {
|
|
3096
|
+
entry: {
|
|
3097
|
+
moduleId: reference.moduleId,
|
|
3098
|
+
projectionKind: 'activated-reference',
|
|
3099
|
+
sourceKind: 'local-reference',
|
|
3100
|
+
runtimeSelected: false,
|
|
3101
|
+
...fingerprints,
|
|
3102
|
+
factCoverage: activated.factCoverage,
|
|
3103
|
+
facts: jsonEntry,
|
|
3104
|
+
},
|
|
3105
|
+
markdown: renderModuleFactsMarkdown(facts, fingerprints),
|
|
3106
|
+
warnings: activated.warnings,
|
|
3107
|
+
unresolvedTargets: activated.unresolvedFirstPartyTargets,
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
931
3111
|
export interface ExtractAllModuleFactsOptions {
|
|
932
3112
|
/**
|
|
933
3113
|
* Discovered module sources (auto-discovery path). When provided, each entry's
|
|
@@ -941,15 +3121,53 @@ export interface ExtractAllModuleFactsOptions {
|
|
|
941
3121
|
coreVersion?: string | null
|
|
942
3122
|
/** @deprecated Legacy explicit module-id list; only consulted when `sources` is absent. */
|
|
943
3123
|
moduleIds?: readonly string[]
|
|
3124
|
+
/**
|
|
3125
|
+
* `throw` (the default, and the only value any normal package output uses) aborts the
|
|
3126
|
+
* batch when a first-party extension target does not correlate. `collect` keeps the
|
|
3127
|
+
* unresolved entries in `extensionSurfaces.unresolved` and reports them on the result
|
|
3128
|
+
* so a disposable projection can publish the gap instead of silently dropping it.
|
|
3129
|
+
*/
|
|
3130
|
+
unresolvedFirstPartyTargets?: 'throw' | 'collect'
|
|
944
3131
|
}
|
|
945
3132
|
|
|
946
3133
|
export interface ExtractAllModuleFactsResult {
|
|
947
3134
|
factsByModule: Record<string, ModuleFacts>
|
|
948
3135
|
markdownByModule: Record<string, string>
|
|
949
3136
|
warnings: string[]
|
|
3137
|
+
frameworkMarkdown: string
|
|
3138
|
+
/** Always empty under the default `throw` policy, which aborts before returning. */
|
|
3139
|
+
unresolvedFirstPartyTargets: string[]
|
|
3140
|
+
/**
|
|
3141
|
+
* The enum-derived coverage ledger for the taxonomies these facts are expressed
|
|
3142
|
+
* in, built during generation so an enum value with no classification aborts the
|
|
3143
|
+
* run instead of shipping an unclassified public value.
|
|
3144
|
+
*/
|
|
3145
|
+
factCoverage: ModuleFactCoverageFamily[]
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
/**
|
|
3149
|
+
* `<moduleId>:<contributionId>:<targetId>` for every first-party contribution target that
|
|
3150
|
+
* did not correlate — the same identity {@link assertNoUnresolvedExtensionTargets} throws
|
|
3151
|
+
* with, so a collected batch reports exactly what a strict batch would have refused.
|
|
3152
|
+
*/
|
|
3153
|
+
export function collectUnresolvedFirstPartyTargets(
|
|
3154
|
+
surfacesByModule: Readonly<Record<string, ModuleExtensionSurfaceFacts>>,
|
|
3155
|
+
): string[] {
|
|
3156
|
+
return Object.entries(surfacesByModule)
|
|
3157
|
+
.flatMap(([moduleId, surface]) =>
|
|
3158
|
+
surface.unresolved
|
|
3159
|
+
.filter((entry) => entry.reason === 'unresolved-first-party-target')
|
|
3160
|
+
.map((entry) => `${moduleId}:${entry.key}`),
|
|
3161
|
+
)
|
|
3162
|
+
.sort((left, right) => left.localeCompare(right))
|
|
950
3163
|
}
|
|
951
3164
|
|
|
952
3165
|
export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
|
|
3166
|
+
return withModuleExtensionFactExtractionCache(() => extractAllModuleFactsWithCache(options))
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
|
|
3170
|
+
const factCoverage = buildModuleFactCoverageLedger()
|
|
953
3171
|
const sources: ModuleFactSource[] = options.sources
|
|
954
3172
|
? [...options.sources]
|
|
955
3173
|
: (options.coreSrcRoot
|
|
@@ -967,12 +3185,403 @@ export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): Ex
|
|
|
967
3185
|
moduleId: source.moduleId,
|
|
968
3186
|
moduleRoot: source.moduleRoot,
|
|
969
3187
|
coreVersion: options.coreVersion ?? null,
|
|
3188
|
+
sourcePackage: source.from ?? null,
|
|
3189
|
+
sourceVersion: source.packageVersion ?? null,
|
|
970
3190
|
registryPath: options.registryPath ?? null,
|
|
971
3191
|
registrySource: options.registrySource ?? null,
|
|
3192
|
+
...(source.portableSourceRoot ? { portableSourceRoot: source.portableSourceRoot } : {}),
|
|
3193
|
+
...(source.sourceKind ? { sourceKind: source.sourceKind } : {}),
|
|
972
3194
|
})
|
|
973
3195
|
factsByModule[source.moduleId] = facts
|
|
974
|
-
markdownByModule[source.moduleId] = renderModuleFactsMarkdown(facts)
|
|
975
3196
|
warnings.push(...facts.warnings)
|
|
976
3197
|
}
|
|
977
|
-
|
|
3198
|
+
const surfacesByModule = Object.fromEntries(
|
|
3199
|
+
Object.entries(factsByModule).map(([moduleId, facts]) => [
|
|
3200
|
+
moduleId,
|
|
3201
|
+
facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] },
|
|
3202
|
+
]),
|
|
3203
|
+
)
|
|
3204
|
+
const correlated = correlateModuleExtensionFacts({
|
|
3205
|
+
surfacesByModule,
|
|
3206
|
+
entityIds: new Set(Object.values(factsByModule).flatMap((facts) => facts.entities.map((entity) => entity.id))),
|
|
3207
|
+
eventIds: new Set(Object.values(factsByModule).flatMap((facts) => facts.events.map((event) => event.id))),
|
|
3208
|
+
apiRoutes: new Set([
|
|
3209
|
+
...Object.values(factsByModule).flatMap((facts) => facts.apiRoutes.map((route) => route.path)),
|
|
3210
|
+
...sources.flatMap((source) => extractKnownApiRouteIds(source.moduleId, source.moduleRoot)),
|
|
3211
|
+
]),
|
|
3212
|
+
commandIds: new Set(sources.flatMap((source) => extractKnownCommandIds(source.moduleId, source.moduleRoot))),
|
|
3213
|
+
})
|
|
3214
|
+
const unresolvedFirstPartyTargets = collectUnresolvedFirstPartyTargets(correlated)
|
|
3215
|
+
if ((options.unresolvedFirstPartyTargets ?? 'throw') === 'throw') assertNoUnresolvedExtensionTargets(correlated)
|
|
3216
|
+
const apiRouteOwners = new Map<string, {
|
|
3217
|
+
moduleId: string
|
|
3218
|
+
source: ModuleFactSourceRef
|
|
3219
|
+
bridges: ApiRouteInterceptorBridge[]
|
|
3220
|
+
}>()
|
|
3221
|
+
const commandOwners = new Map<string, { moduleId: string; source: ModuleFactSourceRef }>()
|
|
3222
|
+
for (const source of [...sources].sort((left, right) => left.moduleId.localeCompare(right.moduleId))) {
|
|
3223
|
+
const facts = factsByModule[source.moduleId]
|
|
3224
|
+
const sourceRoot = facts ? facts.sourceRoot : source.moduleId
|
|
3225
|
+
const owners = extractActivationTargetOwners({
|
|
3226
|
+
moduleId: source.moduleId,
|
|
3227
|
+
moduleRoot: source.moduleRoot,
|
|
3228
|
+
sourceRoot,
|
|
3229
|
+
})
|
|
3230
|
+
for (const route of owners.apiRoutes) {
|
|
3231
|
+
if (!apiRouteOwners.has(route.id)) {
|
|
3232
|
+
apiRouteOwners.set(route.id, { moduleId: source.moduleId, source: route.source, bridges: route.bridges })
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
for (const command of owners.commands) {
|
|
3236
|
+
if (!commandOwners.has(command.id)) commandOwners.set(command.id, { moduleId: source.moduleId, source: command.source })
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
const withIncoming = correlateIncomingExtensions({
|
|
3240
|
+
surfacesByModule: correlated,
|
|
3241
|
+
apiRouteOwners,
|
|
3242
|
+
commandOwners,
|
|
3243
|
+
})
|
|
3244
|
+
for (const moduleId of Object.keys(factsByModule).sort((left, right) => left.localeCompare(right))) {
|
|
3245
|
+
factsByModule[moduleId].extensionSurfaces = withIncoming[moduleId]
|
|
3246
|
+
markdownByModule[moduleId] = renderModuleFactsMarkdown(factsByModule[moduleId])
|
|
3247
|
+
warnings.push(...correlated[moduleId].unresolved.map((entry) =>
|
|
3248
|
+
`[module-facts] ${moduleId} ${entry.reason}: ${entry.key} (${entry.source.path})`,
|
|
3249
|
+
))
|
|
3250
|
+
}
|
|
3251
|
+
return {
|
|
3252
|
+
factsByModule,
|
|
3253
|
+
markdownByModule,
|
|
3254
|
+
warnings,
|
|
3255
|
+
frameworkMarkdown: renderFrameworkExtensionPointsMarkdown(),
|
|
3256
|
+
unresolvedFirstPartyTargets,
|
|
3257
|
+
factCoverage,
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
/**
|
|
3262
|
+
* Enum-derived `factCoverage` ledger — spec
|
|
3263
|
+
* `.ai/specs/2026-07-31-standalone-canonical-example-module.md`,
|
|
3264
|
+
* § PR #4883 Module-Fact and Extension-Topology Contract.
|
|
3265
|
+
*
|
|
3266
|
+
* Every finite public set is enumerated from the `as const` ledger that also derives
|
|
3267
|
+
* its type, so an added enum value has no ledger row and fails
|
|
3268
|
+
* {@link buildModuleFactCoverageLedger} instead of joining the public surface
|
|
3269
|
+
* unclassified. The purpose of the ledger is to make a *silent zero* impossible: a
|
|
3270
|
+
* family cannot claim canonical coverage while the real extractor publishes no
|
|
3271
|
+
* contribution for it.
|
|
3272
|
+
*/
|
|
3273
|
+
export type ModuleFactCoverageStatus =
|
|
3274
|
+
/** The canonical example really emits it; a real extraction proves a non-zero count. */
|
|
3275
|
+
| 'emitted-example'
|
|
3276
|
+
/** A framework/app-level setting that never becomes a module contribution. */
|
|
3277
|
+
| 'framework-only'
|
|
3278
|
+
/** Described by the framework catalog only; the example contributes nothing for it. */
|
|
3279
|
+
| 'catalog-only'
|
|
3280
|
+
/** In the closed public set, but no generator code path emits it today. */
|
|
3281
|
+
| 'currently-unbound'
|
|
3282
|
+
/**
|
|
3283
|
+
* Required to be `emitted-example` by the spec but NOT emitted yet. The status
|
|
3284
|
+
* itself states the gap, so a machine reader keying on `status` alone is never
|
|
3285
|
+
* told the example emits something it does not.
|
|
3286
|
+
*/
|
|
3287
|
+
| 'pending-emission'
|
|
3288
|
+
/** Only a deliberately broken fixture produces it; canonical output emits none. */
|
|
3289
|
+
| 'negative-fixture'
|
|
3290
|
+
|
|
3291
|
+
export type ModuleFactCoverageRow = {
|
|
3292
|
+
value: string
|
|
3293
|
+
status: ModuleFactCoverageStatus
|
|
3294
|
+
note: string
|
|
3295
|
+
/**
|
|
3296
|
+
* Names the slice that owns closing a `pending-emission` gap. A pending row is
|
|
3297
|
+
* asserted to have a *zero* real count, so it can never be mistaken for proven
|
|
3298
|
+
* coverage and cannot go stale once the gap closes.
|
|
3299
|
+
*/
|
|
3300
|
+
pendingEmission?: string
|
|
3301
|
+
/**
|
|
3302
|
+
* Set when the value is extractable only from the generated module registry rather
|
|
3303
|
+
* than from module source, so a source-only extraction legitimately reports zero.
|
|
3304
|
+
*
|
|
3305
|
+
* This is the only way a row skips the non-zero proof, so the flag is proven rather
|
|
3306
|
+
* than trusted: `module-facts.example-fact-coverage.test.ts` asserts every row
|
|
3307
|
+
* carrying it counts zero from a source-only extraction of the example AND non-zero
|
|
3308
|
+
* once the same extraction is handed a module registry.
|
|
3309
|
+
*/
|
|
3310
|
+
requiresGeneratedRegistry?: true
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
export type ModuleFactCoverageFamily = {
|
|
3314
|
+
family: string
|
|
3315
|
+
enumSource: string
|
|
3316
|
+
rows: ModuleFactCoverageRow[]
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
const FACT_SOURCE_KIND_COVERAGE: Record<ModuleFactSourceKind, ModuleFactCoverageRow> = {
|
|
3320
|
+
'module-metadata': { value: 'module-metadata', status: 'emitted-example', note: 'example/index.ts ModuleInfo export' },
|
|
3321
|
+
entity: { value: 'entity', status: 'emitted-example', note: 'example/data/entities.ts ORM entities' },
|
|
3322
|
+
event: { value: 'event', status: 'emitted-example', note: 'example/events.ts typed event definitions' },
|
|
3323
|
+
'acl-feature': { value: 'acl-feature', status: 'emitted-example', note: 'example/acl.ts RBAC features' },
|
|
3324
|
+
'api-route': {
|
|
3325
|
+
value: 'api-route',
|
|
3326
|
+
status: 'emitted-example',
|
|
3327
|
+
note: 'example/api/**/route.ts; route facts are read from the generated module registry, not from module source',
|
|
3328
|
+
requiresGeneratedRegistry: true,
|
|
3329
|
+
},
|
|
3330
|
+
'di-registration': { value: 'di-registration', status: 'emitted-example', note: 'example/di.ts Awilix container.register' },
|
|
3331
|
+
search: { value: 'search', status: 'emitted-example', note: 'example/search.ts indexed entity declaration' },
|
|
3332
|
+
vector: {
|
|
3333
|
+
value: 'vector',
|
|
3334
|
+
status: 'pending-emission',
|
|
3335
|
+
note: 'REQUIRED: a vector-search identity for the Todo entity; the example declares none today',
|
|
3336
|
+
pendingEmission: 'slice-H-missing-example-surfaces',
|
|
3337
|
+
},
|
|
3338
|
+
notification: { value: 'notification', status: 'emitted-example', note: 'example/notifications.ts notification types' },
|
|
3339
|
+
'cli-command': { value: 'cli-command', status: 'emitted-example', note: 'example/cli.ts module commands' },
|
|
3340
|
+
'backend-page': { value: 'backend-page', status: 'emitted-example', note: 'example/backend/**/page.tsx admin pages' },
|
|
3341
|
+
'frontend-page': { value: 'frontend-page', status: 'emitted-example', note: 'example/frontend/**/page.tsx portal pages' },
|
|
3342
|
+
'ai-tool': { value: 'ai-tool', status: 'emitted-example', note: 'example/ai-tools.ts defineAiTool declarations' },
|
|
3343
|
+
'ai-agent': { value: 'ai-agent', status: 'emitted-example', note: 'example/ai-agents.ts defineAiAgent declarations' },
|
|
3344
|
+
'ai-extension': { value: 'ai-extension', status: 'emitted-example', note: 'example AI agent/tool override and extension records' },
|
|
3345
|
+
command: { value: 'command', status: 'emitted-example', note: 'example/commands/todos.ts command handlers' },
|
|
3346
|
+
subscriber: { value: 'subscriber', status: 'emitted-example', note: 'example/subscribers/*.ts event subscribers' },
|
|
3347
|
+
worker: { value: 'worker', status: 'emitted-example', note: 'example/workers/*.ts queue workers' },
|
|
3348
|
+
'page-middleware': { value: 'page-middleware', status: 'emitted-example', note: 'example/backend/middleware.ts page guard' },
|
|
3349
|
+
setup: { value: 'setup', status: 'emitted-example', note: 'example/setup.ts tenant setup hooks' },
|
|
3350
|
+
encryption: { value: 'encryption', status: 'emitted-example', note: 'example/encryption.ts field encryption map' },
|
|
3351
|
+
'custom-entity': { value: 'custom-entity', status: 'emitted-example', note: 'example/ce.ts custom entity and field DSL' },
|
|
3352
|
+
integration: { value: 'integration', status: 'emitted-example', note: 'example/lib/mock-*-adapter.ts integration registrations' },
|
|
3353
|
+
'generator-plugin': {
|
|
3354
|
+
value: 'generator-plugin',
|
|
3355
|
+
status: 'pending-emission',
|
|
3356
|
+
note: 'REQUIRED: an example/generators.ts GeneratorPlugin declaration; the file does not exist yet',
|
|
3357
|
+
pendingEmission: 'slice-H-missing-example-surfaces',
|
|
3358
|
+
},
|
|
3359
|
+
'extension-host': { value: 'extension-host', status: 'emitted-example', note: 'example/extension-points.ts declared hosts' },
|
|
3360
|
+
'extension-contribution': {
|
|
3361
|
+
value: 'extension-contribution',
|
|
3362
|
+
status: 'emitted-example',
|
|
3363
|
+
note: 'example widget, DataTable, CrudForm, interceptor and enricher contributions',
|
|
3364
|
+
},
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
const OWNED_CONTRACT_KIND_COVERAGE: Record<ModuleOwnedContractKind, ModuleFactCoverageRow> = {
|
|
3368
|
+
'module-metadata': { value: 'module-metadata', status: 'emitted-example', note: 'ModuleInfo owned contract' },
|
|
3369
|
+
command: { value: 'command', status: 'emitted-example', note: 'example/commands/todos.ts' },
|
|
3370
|
+
worker: { value: 'worker', status: 'emitted-example', note: 'example/workers/*.ts' },
|
|
3371
|
+
'page-middleware': { value: 'page-middleware', status: 'emitted-example', note: 'example/backend/middleware.ts' },
|
|
3372
|
+
setup: { value: 'setup', status: 'emitted-example', note: 'example/setup.ts' },
|
|
3373
|
+
encryption: { value: 'encryption', status: 'emitted-example', note: 'example/encryption.ts' },
|
|
3374
|
+
'di-registration': {
|
|
3375
|
+
value: 'di-registration',
|
|
3376
|
+
status: 'emitted-example',
|
|
3377
|
+
note: 'example/di.ts scoped Todo summary cache service consumed by a route',
|
|
3378
|
+
},
|
|
3379
|
+
'custom-entity': { value: 'custom-entity', status: 'emitted-example', note: 'example/ce.ts' },
|
|
3380
|
+
'ai-extension': { value: 'ai-extension', status: 'emitted-example', note: 'example AI override/extension record' },
|
|
3381
|
+
'generator-plugin': {
|
|
3382
|
+
value: 'generator-plugin',
|
|
3383
|
+
status: 'pending-emission',
|
|
3384
|
+
note: 'REQUIRED: an example/generators.ts GeneratorPlugin declaration; the file does not exist yet',
|
|
3385
|
+
pendingEmission: 'slice-H-missing-example-surfaces',
|
|
3386
|
+
},
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
const OVERRIDE_DOMAIN_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3390
|
+
ai: { value: 'ai', status: 'emitted-example', note: 'ai.agents, ai.tools and ai.extensions targets' },
|
|
3391
|
+
routes: { value: 'routes', status: 'emitted-example', note: 'routes.api and routes.pages targets' },
|
|
3392
|
+
events: { value: 'events', status: 'emitted-example', note: 'events.subscribers targets' },
|
|
3393
|
+
workers: { value: 'workers', status: 'emitted-example', note: 'workers targets' },
|
|
3394
|
+
widgets: { value: 'widgets', status: 'emitted-example', note: 'widgets.injection, .components and .dashboard targets' },
|
|
3395
|
+
notifications: { value: 'notifications', status: 'emitted-example', note: 'notifications.types and notifications.handlers targets' },
|
|
3396
|
+
interceptors: { value: 'interceptors', status: 'emitted-example', note: 'API interceptor targets' },
|
|
3397
|
+
commandInterceptors: { value: 'commandInterceptors', status: 'emitted-example', note: 'command interceptor targets' },
|
|
3398
|
+
enrichers: { value: 'enrichers', status: 'emitted-example', note: 'response enricher targets' },
|
|
3399
|
+
guards: { value: 'guards', status: 'emitted-example', note: 'page-middleware guard targets' },
|
|
3400
|
+
cli: { value: 'cli', status: 'emitted-example', note: 'CLI command targets' },
|
|
3401
|
+
setup: { value: 'setup', status: 'emitted-example', note: 'setup hook and default-role targets' },
|
|
3402
|
+
acl: { value: 'acl', status: 'emitted-example', note: 'acl.features targets' },
|
|
3403
|
+
di: { value: 'di', status: 'emitted-example', note: 'DI token targets' },
|
|
3404
|
+
encryption: { value: 'encryption', status: 'emitted-example', note: 'encryption.maps targets' },
|
|
3405
|
+
nav: {
|
|
3406
|
+
value: 'nav',
|
|
3407
|
+
status: 'framework-only',
|
|
3408
|
+
note: 'nav.groupOrder is an app-level framework setting; no module target is ever emitted for it',
|
|
3409
|
+
},
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
const OVERRIDE_MODE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3413
|
+
'disable-replace': {
|
|
3414
|
+
value: 'disable-replace',
|
|
3415
|
+
status: 'emitted-example',
|
|
3416
|
+
note: 'default mode for most example override hosts',
|
|
3417
|
+
},
|
|
3418
|
+
replace: { value: 'replace', status: 'emitted-example', note: 'the setup host replaces wholesale' },
|
|
3419
|
+
additive: { value: 'additive', status: 'emitted-example', note: 'the ai.extensions host merges additively' },
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
const OVERRIDE_NOTE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3423
|
+
'safe-metadata-only': {
|
|
3424
|
+
value: 'safe-metadata-only',
|
|
3425
|
+
status: 'emitted-example',
|
|
3426
|
+
note: 'emitted on the metadata-only override target',
|
|
3427
|
+
},
|
|
3428
|
+
'page-middleware-not-mutation-guard': {
|
|
3429
|
+
value: 'page-middleware-not-mutation-guard',
|
|
3430
|
+
status: 'emitted-example',
|
|
3431
|
+
note: 'emitted on the guards target so a page guard is not read as a mutation guard',
|
|
3432
|
+
},
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
/**
|
|
3436
|
+
* Every `negative-fixture` row here names a code that a real fixture in
|
|
3437
|
+
* `module-override-targets.diagnostic-wiring.test.ts` drives out of
|
|
3438
|
+
* `collectModuleOverrideTargets`; the coverage test asserts that fixture set matches
|
|
3439
|
+
* these rows exactly, so a row can never claim a fixture that does not exist.
|
|
3440
|
+
*/
|
|
3441
|
+
const OVERRIDE_DIAGNOSTIC_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3442
|
+
'missing-owned-fact': {
|
|
3443
|
+
value: 'missing-owned-fact',
|
|
3444
|
+
status: 'currently-unbound',
|
|
3445
|
+
note: 'reserved by the spec\'s closed diagnostic set; no adapter emits it today, so not even a broken fixture can produce it',
|
|
3446
|
+
},
|
|
3447
|
+
'missing-source': {
|
|
3448
|
+
value: 'missing-source',
|
|
3449
|
+
status: 'negative-fixture',
|
|
3450
|
+
note: 'a fact whose provenance cannot be proven yields this and no target; activated canonical output emits none',
|
|
3451
|
+
},
|
|
3452
|
+
'unsupported-dynamic-key': {
|
|
3453
|
+
value: 'unsupported-dynamic-key',
|
|
3454
|
+
status: 'negative-fixture',
|
|
3455
|
+
note: 'a key the runtime cannot address yields this and no target; activated canonical output emits none',
|
|
3456
|
+
},
|
|
3457
|
+
'unknown-framework-domain': {
|
|
3458
|
+
value: 'unknown-framework-domain',
|
|
3459
|
+
status: 'negative-fixture',
|
|
3460
|
+
note: 'the framework catalog does not describe the dotted host at all',
|
|
3461
|
+
},
|
|
3462
|
+
'unknown-framework-mode': {
|
|
3463
|
+
value: 'unknown-framework-mode',
|
|
3464
|
+
status: 'negative-fixture',
|
|
3465
|
+
note: 'the framework catalog describes the host but names an operation that is not a public mode',
|
|
3466
|
+
},
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
/**
|
|
3470
|
+
* One family of the ledger. Both directions are enforced: an enumerated value with
|
|
3471
|
+
* no row, and a row classifying a value the enum no longer carries, each throw — so
|
|
3472
|
+
* neither an unclassified new value nor a stale row for a removed one can ship.
|
|
3473
|
+
*/
|
|
3474
|
+
export function buildCoverageFamily(
|
|
3475
|
+
family: string,
|
|
3476
|
+
enumSource: string,
|
|
3477
|
+
values: readonly string[],
|
|
3478
|
+
coverage: Readonly<Record<string, ModuleFactCoverageRow>>,
|
|
3479
|
+
): ModuleFactCoverageFamily {
|
|
3480
|
+
const rows: ModuleFactCoverageRow[] = []
|
|
3481
|
+
const missing: string[] = []
|
|
3482
|
+
for (const value of values) {
|
|
3483
|
+
const row = coverage[value]
|
|
3484
|
+
if (!row) {
|
|
3485
|
+
missing.push(value)
|
|
3486
|
+
continue
|
|
3487
|
+
}
|
|
3488
|
+
rows.push(row)
|
|
3489
|
+
}
|
|
3490
|
+
if (missing.length > 0) {
|
|
3491
|
+
throw new Error(
|
|
3492
|
+
`[module-facts] factCoverage family "${family}" has no row for: ${missing.join(', ')}. ` +
|
|
3493
|
+
'Add an explicit classification instead of leaving the value unclassified.',
|
|
3494
|
+
)
|
|
3495
|
+
}
|
|
3496
|
+
const extra = Object.keys(coverage).filter((value) => !values.includes(value))
|
|
3497
|
+
if (extra.length > 0) {
|
|
3498
|
+
throw new Error(
|
|
3499
|
+
`[module-facts] factCoverage family "${family}" classifies values absent from the enum: ${extra.join(', ')}.`,
|
|
3500
|
+
)
|
|
3501
|
+
}
|
|
3502
|
+
return { family, enumSource, rows }
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
/**
|
|
3506
|
+
* Builds the whole ledger, throwing when an enumerated value lacks a row or a row
|
|
3507
|
+
* classifies a value the enum no longer carries. {@link extractAllModuleFacts} calls
|
|
3508
|
+
* it on every run, so either violation fails fact generation — not only the unit
|
|
3509
|
+
* test — and the built ledger is returned on the generation result so the published
|
|
3510
|
+
* mirrors have a single generated authority to be checked against.
|
|
3511
|
+
*/
|
|
3512
|
+
export function buildModuleFactCoverageLedger(): ModuleFactCoverageFamily[] {
|
|
3513
|
+
return [
|
|
3514
|
+
buildCoverageFamily(
|
|
3515
|
+
'ModuleFactSourceKind',
|
|
3516
|
+
'packages/cli/src/lib/generators/module-facts.ts#MODULE_FACT_SOURCE_KINDS',
|
|
3517
|
+
MODULE_FACT_SOURCE_KINDS,
|
|
3518
|
+
FACT_SOURCE_KIND_COVERAGE,
|
|
3519
|
+
),
|
|
3520
|
+
buildCoverageFamily(
|
|
3521
|
+
'ModuleOwnedContractKind',
|
|
3522
|
+
'packages/cli/src/lib/generators/module-facts.ts#MODULE_OWNED_CONTRACT_KINDS',
|
|
3523
|
+
MODULE_OWNED_CONTRACT_KINDS,
|
|
3524
|
+
OWNED_CONTRACT_KIND_COVERAGE,
|
|
3525
|
+
),
|
|
3526
|
+
buildCoverageFamily(
|
|
3527
|
+
'ModuleOverrideDomain',
|
|
3528
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#ALL_OVERRIDE_DOMAINS',
|
|
3529
|
+
ALL_OVERRIDE_DOMAINS,
|
|
3530
|
+
OVERRIDE_DOMAIN_COVERAGE,
|
|
3531
|
+
),
|
|
3532
|
+
buildCoverageFamily(
|
|
3533
|
+
'ModuleOverrideMode',
|
|
3534
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#MODULE_OVERRIDE_MODES',
|
|
3535
|
+
MODULE_OVERRIDE_MODES,
|
|
3536
|
+
OVERRIDE_MODE_COVERAGE,
|
|
3537
|
+
),
|
|
3538
|
+
buildCoverageFamily(
|
|
3539
|
+
'ModuleOverrideTargetNote',
|
|
3540
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#MODULE_OVERRIDE_TARGET_NOTES',
|
|
3541
|
+
MODULE_OVERRIDE_TARGET_NOTES,
|
|
3542
|
+
OVERRIDE_NOTE_COVERAGE,
|
|
3543
|
+
),
|
|
3544
|
+
buildCoverageFamily(
|
|
3545
|
+
'ModuleOverrideTargetDiagnosticCode',
|
|
3546
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES',
|
|
3547
|
+
MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES,
|
|
3548
|
+
OVERRIDE_DIAGNOSTIC_COVERAGE,
|
|
3549
|
+
),
|
|
3550
|
+
]
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
/**
|
|
3554
|
+
* Real per-value counts for every ledger family, read from an extracted module's
|
|
3555
|
+
* facts. This is the anti-silent-zero oracle: a ledger claim is checked against what
|
|
3556
|
+
* the extractor actually published, never against a hand-maintained inventory row.
|
|
3557
|
+
*/
|
|
3558
|
+
export function countModuleFactCoverage(facts: ModuleFacts): Record<string, Record<string, number>> {
|
|
3559
|
+
const factSourceKind: Record<string, number> = {}
|
|
3560
|
+
for (const entry of facts.factSources ?? []) {
|
|
3561
|
+
factSourceKind[entry.kind] = (factSourceKind[entry.kind] ?? 0) + 1
|
|
3562
|
+
}
|
|
3563
|
+
const ownedContractKind: Record<string, number> = {}
|
|
3564
|
+
for (const [kind, list] of Object.entries(facts.ownedContracts ?? {})) {
|
|
3565
|
+
ownedContractKind[kind] = (list ?? []).length
|
|
3566
|
+
}
|
|
3567
|
+
const overrideDomain: Record<string, number> = {}
|
|
3568
|
+
const overrideMode: Record<string, number> = {}
|
|
3569
|
+
const overrideNote: Record<string, number> = {}
|
|
3570
|
+
for (const target of facts.overrideTargets ?? []) {
|
|
3571
|
+
overrideDomain[target.domain] = (overrideDomain[target.domain] ?? 0) + 1
|
|
3572
|
+
for (const mode of target.modes) overrideMode[mode] = (overrideMode[mode] ?? 0) + 1
|
|
3573
|
+
for (const note of target.notes ?? []) overrideNote[note] = (overrideNote[note] ?? 0) + 1
|
|
3574
|
+
}
|
|
3575
|
+
const overrideDiagnosticCode: Record<string, number> = {}
|
|
3576
|
+
for (const diagnostic of facts.overrideTargetDiagnostics ?? []) {
|
|
3577
|
+
overrideDiagnosticCode[diagnostic.code] = (overrideDiagnosticCode[diagnostic.code] ?? 0) + 1
|
|
3578
|
+
}
|
|
3579
|
+
return {
|
|
3580
|
+
ModuleFactSourceKind: factSourceKind,
|
|
3581
|
+
ModuleOwnedContractKind: ownedContractKind,
|
|
3582
|
+
ModuleOverrideDomain: overrideDomain,
|
|
3583
|
+
ModuleOverrideMode: overrideMode,
|
|
3584
|
+
ModuleOverrideTargetNote: overrideNote,
|
|
3585
|
+
ModuleOverrideTargetDiagnosticCode: overrideDiagnosticCode,
|
|
3586
|
+
}
|
|
978
3587
|
}
|