@open-mercato/cli 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/build.mjs +7 -24
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
- package/dist/agentic/codex/enforcement-rules.md +16 -2
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
- package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
- package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
- package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
- package/dist/agentic/guides/cache.md +45 -0
- package/dist/agentic/guides/core.md +453 -0
- package/dist/agentic/guides/events.md +117 -0
- package/dist/agentic/guides/module-facts.json +3833 -115419
- package/dist/agentic/guides/module-system.md +127 -12
- package/dist/agentic/guides/modules/ai_assistant.md +28 -227
- package/dist/agentic/guides/modules/api_docs.md +2 -58
- package/dist/agentic/guides/modules/api_keys.md +7 -92
- package/dist/agentic/guides/modules/attachments.md +14 -119
- package/dist/agentic/guides/modules/audit_logs.md +10 -131
- package/dist/agentic/guides/modules/auth.md +33 -276
- package/dist/agentic/guides/modules/business_rules.md +14 -149
- package/dist/agentic/guides/modules/catalog.md +43 -475
- package/dist/agentic/guides/modules/channel_gmail.md +3 -82
- package/dist/agentic/guides/modules/channel_imap.md +3 -82
- package/dist/agentic/guides/modules/checkout.md +27 -299
- package/dist/agentic/guides/modules/communication_channels.md +33 -315
- package/dist/agentic/guides/modules/configs.md +8 -104
- package/dist/agentic/guides/modules/content.md +2 -57
- package/dist/agentic/guides/modules/currencies.md +17 -152
- package/dist/agentic/guides/modules/customer_accounts.md +45 -387
- package/dist/agentic/guides/modules/customers.md +85 -746
- package/dist/agentic/guides/modules/dashboards.md +10 -155
- package/dist/agentic/guides/modules/data_sync.md +16 -149
- package/dist/agentic/guides/modules/dictionaries.md +12 -107
- package/dist/agentic/guides/modules/directory.md +15 -148
- package/dist/agentic/guides/modules/entities.md +13 -145
- package/dist/agentic/guides/modules/events.md +2 -59
- package/dist/agentic/guides/modules/feature_toggles.md +9 -119
- package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
- package/dist/agentic/guides/modules/generators.md +2 -45
- package/dist/agentic/guides/modules/inbox_ops.md +27 -207
- package/dist/agentic/guides/modules/integrations.md +16 -146
- package/dist/agentic/guides/modules/messages.md +24 -223
- package/dist/agentic/guides/modules/notifications.md +8 -96
- package/dist/agentic/guides/modules/onboarding.md +6 -77
- package/dist/agentic/guides/modules/payment_gateways.md +21 -164
- package/dist/agentic/guides/modules/perspectives.md +7 -77
- package/dist/agentic/guides/modules/planner.md +18 -136
- package/dist/agentic/guides/modules/portal.md +2 -93
- package/dist/agentic/guides/modules/progress.md +14 -104
- package/dist/agentic/guides/modules/query_index.md +15 -154
- package/dist/agentic/guides/modules/record_locks.md +21 -206
- package/dist/agentic/guides/modules/resources.md +30 -197
- package/dist/agentic/guides/modules/sales.md +84 -678
- package/dist/agentic/guides/modules/scheduler.md +14 -133
- package/dist/agentic/guides/modules/search.md +6 -170
- package/dist/agentic/guides/modules/security.md +35 -294
- package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
- package/dist/agentic/guides/modules/sso.md +26 -151
- package/dist/agentic/guides/modules/staff.md +57 -369
- package/dist/agentic/guides/modules/storage_s3.md +5 -95
- package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
- package/dist/agentic/guides/modules/sync_excel.md +6 -75
- package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
- package/dist/agentic/guides/modules/translations.md +10 -104
- package/dist/agentic/guides/modules/webhooks.md +24 -203
- package/dist/agentic/guides/modules/wms.md +46 -441
- package/dist/agentic/guides/modules/workflows.md +43 -302
- package/dist/agentic/guides/queue.md +53 -0
- package/dist/agentic/guides/search.md +115 -0
- package/dist/agentic/guides/shared.md +108 -0
- package/dist/agentic/guides/ui.md +347 -0
- package/dist/agentic/shared/AGENTS.md.template +311 -124
- package/dist/agentic/shared/ai/agentic.config.json +0 -2
- package/dist/agentic/shared/ai/lessons.md +3 -26
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
- package/dist/agentic/shared/ai/skills/tiers.json +21 -143
- package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
- package/dist/agentic/shared/ai/specs/README.md +3 -4
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
- package/dist/agentic/shared/ai/trackers/github.md +4 -108
- package/dist/agentic/shared/scripts/install-skills.sh +694 -4
- package/dist/bin.js +1 -13
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +3 -13
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +186 -418
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +0 -20
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-facts-discovery.js +7 -41
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +45 -1387
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-registry.js +5 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/resolver.js +0 -2
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/testing/integration.js +0 -5
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +1 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +11 -13
- package/src/bin.ts +1 -33
- package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
- package/src/lib/__tests__/agentic-init.test.ts +5 -41
- package/src/lib/agentic-init.ts +3 -15
- package/src/lib/agentic-setup.ts +224 -530
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
- package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
- package/src/lib/generators/extensions/injection-widgets.ts +0 -51
- package/src/lib/generators/module-facts-discovery.ts +7 -58
- package/src/lib/generators/module-facts.ts +46 -1851
- package/src/lib/generators/module-registry.ts +6 -16
- package/src/lib/resolver.ts +0 -3
- package/src/lib/testing/__tests__/integration.test.ts +0 -4
- package/src/lib/testing/integration.ts +0 -5
- package/src/mercato.ts +1 -26
- package/dist/agentic/guides/ai-workflows.md +0 -76
- package/dist/agentic/guides/architecture.md +0 -107
- package/dist/agentic/guides/backend-ui.md +0 -80
- package/dist/agentic/guides/contracts.md +0 -92
- package/dist/agentic/guides/extensions.md +0 -121
- package/dist/agentic/guides/framework-extension-points.md +0 -36
- package/dist/agentic/guides/integrations.md +0 -81
- package/dist/agentic/guides/modules/design_system.md +0 -102
- package/dist/agentic/guides/spec-delivery.md +0 -22
- package/dist/agentic/guides/testing-debugging.md +0 -80
- package/dist/agentic/guides/upstream/AGENTS.md +0 -298
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
- package/dist/agentic/guides/upstream/manifest.json +0 -8
- package/dist/agentic/shared/ai/harness/README.md +0 -50
- package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
- package/dist/agentic/shared/ai/harness/cases.json +0 -13680
- package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
- package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
- package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
- package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
- package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
- package/dist/agentic/shared/ai/harness/validators.json +0 -78
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
- package/dist/agentic/shared/ai/lessons/_template.md +0 -16
- package/dist/agentic/shared/ai/review-checklist.md +0 -36
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
- package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
- package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
- package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
- package/dist/lib/generators/module-extension-facts.js +0 -1840
- package/dist/lib/generators/module-extension-facts.js.map +0 -7
- package/dist/lib/generators/module-fact-sources.js +0 -70
- package/dist/lib/generators/module-fact-sources.js.map +0 -7
- package/dist/lib/generators/module-override-targets.js +0 -688
- package/dist/lib/generators/module-override-targets.js.map +0 -7
- package/dist/lib/load-env.js +0 -24
- package/dist/lib/load-env.js.map +0 -7
- package/dist/lib/telemetry-init.js +0 -358
- package/dist/lib/telemetry-init.js.map +0 -7
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
- package/src/lib/__tests__/load-env.test.ts +0 -65
- package/src/lib/__tests__/telemetry-init.test.ts +0 -298
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
- package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
- package/src/lib/generators/module-extension-facts.ts +0 -2365
- package/src/lib/generators/module-fact-sources.ts +0 -108
- package/src/lib/generators/module-override-targets.ts +0 -863
- package/src/lib/load-env.ts +0 -30
- package/src/lib/telemetry-init.ts +0 -461
|
@@ -1,762 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import {
|
|
5
|
-
assertNoUnresolvedExtensionTargets,
|
|
6
|
-
correlateIncomingExtensions,
|
|
7
|
-
correlateModuleExtensionFacts,
|
|
8
|
-
extractActivationTargetOwners,
|
|
9
|
-
extractModuleExtensionFacts,
|
|
10
|
-
withModuleExtensionFactExtractionCache,
|
|
11
|
-
ALL_ACTIVATION_KINDS,
|
|
12
|
-
type ApiRouteInterceptorBridge,
|
|
13
|
-
} from '../module-extension-facts'
|
|
14
|
-
import { renderModuleFactsMarkdown, type ModuleFacts } from '../module-facts'
|
|
15
|
-
import type {
|
|
16
|
-
ModuleExtensionSurfaceFacts,
|
|
17
|
-
ModuleExtensionContributionFact,
|
|
18
|
-
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
19
|
-
|
|
20
|
-
function write(root: string, relativePath: string, source: string): void {
|
|
21
|
-
const filePath = path.join(root, relativePath)
|
|
22
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
23
|
-
fs.writeFileSync(filePath, source)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type ModuleSpec = {
|
|
27
|
-
id: string
|
|
28
|
-
entities?: Array<{ id: string }>
|
|
29
|
-
events?: Array<{ id: string; clientBroadcast?: boolean; portalBroadcast?: boolean }>
|
|
30
|
-
apiRoutes?: Array<{ path: string; methods: string[] }>
|
|
31
|
-
searchEntities?: string[]
|
|
32
|
-
notifications?: string[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function runPipeline(
|
|
36
|
-
roots: Record<string, string>,
|
|
37
|
-
modules: ModuleSpec[],
|
|
38
|
-
): Record<string, ModuleExtensionSurfaceFacts> {
|
|
39
|
-
return withModuleExtensionFactExtractionCache(() => {
|
|
40
|
-
const surfacesByModule: Record<string, ModuleExtensionSurfaceFacts> = {}
|
|
41
|
-
const entityIds = new Set<string>()
|
|
42
|
-
const eventIds = new Set<string>()
|
|
43
|
-
const apiRoutes = new Set<string>()
|
|
44
|
-
const commandIds = new Set<string>()
|
|
45
|
-
for (const module of modules) {
|
|
46
|
-
const sourceRoot = `node_modules/pkg/src/modules/${module.id}`
|
|
47
|
-
surfacesByModule[module.id] = extractModuleExtensionFacts({
|
|
48
|
-
moduleId: module.id,
|
|
49
|
-
moduleRoot: roots[module.id],
|
|
50
|
-
sourceRoot,
|
|
51
|
-
entities: module.entities ?? [],
|
|
52
|
-
events: module.events ?? [],
|
|
53
|
-
apiRoutes: module.apiRoutes ?? [],
|
|
54
|
-
searchEntities: module.searchEntities ?? [],
|
|
55
|
-
notifications: module.notifications,
|
|
56
|
-
})
|
|
57
|
-
for (const entity of module.entities ?? []) entityIds.add(entity.id)
|
|
58
|
-
for (const event of module.events ?? []) eventIds.add(event.id)
|
|
59
|
-
for (const route of module.apiRoutes ?? []) apiRoutes.add(route.path)
|
|
60
|
-
}
|
|
61
|
-
const apiRouteOwners = new Map<string, {
|
|
62
|
-
moduleId: string
|
|
63
|
-
source: { sourcePath: string }
|
|
64
|
-
bridges: ApiRouteInterceptorBridge[]
|
|
65
|
-
}>()
|
|
66
|
-
const commandOwners = new Map<string, { moduleId: string; source: { sourcePath: string } }>()
|
|
67
|
-
for (const module of modules) {
|
|
68
|
-
const sourceRoot = `node_modules/pkg/src/modules/${module.id}`
|
|
69
|
-
const owners = extractActivationTargetOwners({ moduleId: module.id, moduleRoot: roots[module.id], sourceRoot })
|
|
70
|
-
for (const route of owners.apiRoutes) {
|
|
71
|
-
apiRoutes.add(`/${route.id}`)
|
|
72
|
-
if (!apiRouteOwners.has(route.id)) {
|
|
73
|
-
apiRouteOwners.set(route.id, { moduleId: module.id, source: route.source, bridges: route.bridges })
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
for (const command of owners.commands) {
|
|
77
|
-
commandIds.add(command.id)
|
|
78
|
-
if (!commandOwners.has(command.id)) commandOwners.set(command.id, { moduleId: module.id, source: command.source })
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const correlated = correlateModuleExtensionFacts({ surfacesByModule, entityIds, eventIds, apiRoutes, commandIds })
|
|
82
|
-
assertNoUnresolvedExtensionTargets(correlated)
|
|
83
|
-
return correlateIncomingExtensions({ surfacesByModule: correlated, apiRouteOwners, commandOwners })
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe('module extension activations and incoming index', () => {
|
|
88
|
-
let workspace: string
|
|
89
|
-
const roots: Record<string, string> = {}
|
|
90
|
-
|
|
91
|
-
const moduleRoot = (id: string): string => {
|
|
92
|
-
if (!roots[id]) {
|
|
93
|
-
roots[id] = path.join(workspace, id)
|
|
94
|
-
fs.mkdirSync(roots[id], { recursive: true })
|
|
95
|
-
}
|
|
96
|
-
return roots[id]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
beforeEach(() => {
|
|
100
|
-
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'module-activations-'))
|
|
101
|
-
for (const key of Object.keys(roots)) delete roots[key]
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
afterEach(() => {
|
|
105
|
-
fs.rmSync(workspace, { recursive: true, force: true })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('binds only the CRUD response enricher from the factory option, never the query stage', () => {
|
|
109
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
110
|
-
import { makeCrudRoute } from 'x'
|
|
111
|
-
export const crud = makeCrudRoute({
|
|
112
|
-
orm: { entity: Rec },
|
|
113
|
-
enrichers: { entityId: 'host:record' },
|
|
114
|
-
})
|
|
115
|
-
export const GET = crud.GET
|
|
116
|
-
`)
|
|
117
|
-
write(moduleRoot('ext'), 'data/enrichers.ts', `
|
|
118
|
-
export const enrichers = [{ id: 'ext.record-badge', targetEntity: 'host:record', async enrichOne(r) { return r } }]
|
|
119
|
-
`)
|
|
120
|
-
|
|
121
|
-
const result = runPipeline(roots, [
|
|
122
|
-
{ id: 'host', entities: [{ id: 'host:record' }], apiRoutes: [{ path: '/host/records', methods: ['GET'] }] },
|
|
123
|
-
{ id: 'ext' },
|
|
124
|
-
])
|
|
125
|
-
|
|
126
|
-
// `enrichers: { entityId }` runs `applyResponseEnrichers` only; the query-engine
|
|
127
|
-
// enricher stage needs a query call that passes `extensions`.
|
|
128
|
-
const enricherActivations = (result.host.activations ?? []).filter((a) => a.host.id === 'host:record')
|
|
129
|
-
expect(enricherActivations.map((a) => a.kind)).toEqual(['crud-response-enricher'])
|
|
130
|
-
expect(enricherActivations[0].source.sourcePath).toBe('node_modules/pkg/src/modules/host/api/records/route.ts')
|
|
131
|
-
expect(enricherActivations[0].source.line).toBeGreaterThan(0)
|
|
132
|
-
expect(enricherActivations[0].host).toEqual({ kind: 'entity', id: 'host:record', moduleId: 'host' })
|
|
133
|
-
|
|
134
|
-
const incoming = (result.host.incoming ?? []).filter((entry) => entry.contributionId === 'ext.record-badge')
|
|
135
|
-
expect(incoming).toHaveLength(1)
|
|
136
|
-
expect(incoming[0].resolution).toBe('bound')
|
|
137
|
-
expect(incoming[0].contributorModuleId).toBe('ext')
|
|
138
|
-
expect(incoming[0].activationId).toBe('entity:host:record:crud-response-enricher')
|
|
139
|
-
|
|
140
|
-
const resolutions = (result.ext.contributionResolutions ?? []).filter((r) => r.contributionId === 'ext.record-badge')
|
|
141
|
-
expect(resolutions).toHaveLength(1)
|
|
142
|
-
expect(resolutions[0].resolution).toBe('bound')
|
|
143
|
-
expect(resolutions[0].activationIds).toEqual(['entity:host:record:crud-response-enricher'])
|
|
144
|
-
expect(resolutions[0].target).toEqual({ kind: 'entity', id: 'host:record', moduleId: 'host' })
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('binds the query-enricher stage from a query call site that passes extensions', () => {
|
|
148
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
149
|
-
import { makeCrudRoute } from 'x'
|
|
150
|
-
export const crud = makeCrudRoute({
|
|
151
|
-
orm: { entity: Rec },
|
|
152
|
-
enrichers: { entityId: 'host:record' },
|
|
153
|
-
})
|
|
154
|
-
export async function GET(req) {
|
|
155
|
-
const queryEngine = resolve('queryEngine')
|
|
156
|
-
return queryEngine.query('host:record', { tenantId: 't', extensions: { userId: 'u' } })
|
|
157
|
-
}
|
|
158
|
-
`)
|
|
159
|
-
write(moduleRoot('ext'), 'data/enrichers.ts', `
|
|
160
|
-
export const enrichers = [{ id: 'ext.record-badge', targetEntity: 'host:record', queryEngine: { enabled: true }, async enrichOne(r) { return r } }]
|
|
161
|
-
`)
|
|
162
|
-
|
|
163
|
-
const result = runPipeline(roots, [
|
|
164
|
-
{ id: 'host', entities: [{ id: 'host:record' }], apiRoutes: [{ path: '/host/records', methods: ['GET'] }] },
|
|
165
|
-
{ id: 'ext' },
|
|
166
|
-
])
|
|
167
|
-
|
|
168
|
-
const kinds = (result.host.activations ?? [])
|
|
169
|
-
.filter((a) => a.host.id === 'host:record')
|
|
170
|
-
.map((a) => a.kind)
|
|
171
|
-
.sort()
|
|
172
|
-
expect(kinds).toEqual(['crud-response-enricher', 'query-enricher'])
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('treats an enricher with queryEngine.enabled false as response-only', () => {
|
|
176
|
-
write(moduleRoot('ext'), 'data/enrichers.ts', `
|
|
177
|
-
export const enrichers = [
|
|
178
|
-
{ id: 'ext.disabled', targetEntity: 'host:record', queryEngine: { enabled: false, engines: ['json'] }, async enrichOne(r) { return r } },
|
|
179
|
-
{ id: 'ext.enabled', targetEntity: 'host:record', queryEngine: { enabled: true, engines: ['json'] }, async enrichOne(r) { return r } },
|
|
180
|
-
{ id: 'ext.plain', targetEntity: 'host:record', async enrichOne(r) { return r } },
|
|
181
|
-
]
|
|
182
|
-
`)
|
|
183
|
-
|
|
184
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
185
|
-
import { makeCrudRoute } from 'x'
|
|
186
|
-
export const crud = makeCrudRoute({ orm: { entity: Rec } })
|
|
187
|
-
export const GET = crud.GET
|
|
188
|
-
`)
|
|
189
|
-
|
|
190
|
-
const result = runPipeline(roots, [
|
|
191
|
-
{ id: 'host', entities: [{ id: 'host:record' }] },
|
|
192
|
-
{ id: 'ext' },
|
|
193
|
-
])
|
|
194
|
-
|
|
195
|
-
const byId = new Map((result.ext.contributions ?? []).map((contribution) => [contribution.id, contribution]))
|
|
196
|
-
const disabled = byId.get('ext.disabled')
|
|
197
|
-
const enabled = byId.get('ext.enabled')
|
|
198
|
-
const plain = byId.get('ext.plain')
|
|
199
|
-
expect(disabled?.details).not.toHaveProperty('queryEngine')
|
|
200
|
-
expect(disabled?.activation).toBe('host-opt-in')
|
|
201
|
-
expect(plain?.details).not.toHaveProperty('queryEngine')
|
|
202
|
-
expect(enabled?.details).toHaveProperty('queryEngine')
|
|
203
|
-
expect(enabled?.activation).toBe('caller-opt-in')
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('classifies an enricher targeting an entity WITHOUT the factory option as capability-only, never bound', () => {
|
|
207
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
208
|
-
import { makeCrudRoute } from 'x'
|
|
209
|
-
export const crud = makeCrudRoute({ orm: { entity: Rec } })
|
|
210
|
-
export const GET = crud.GET
|
|
211
|
-
`)
|
|
212
|
-
write(moduleRoot('ext'), 'data/enrichers.ts', `
|
|
213
|
-
export const enrichers = [{ id: 'ext.record-badge', targetEntity: 'host:record', async enrichOne(r) { return r } }]
|
|
214
|
-
`)
|
|
215
|
-
|
|
216
|
-
const result = runPipeline(roots, [
|
|
217
|
-
{ id: 'host', entities: [{ id: 'host:record' }], apiRoutes: [{ path: '/host/records', methods: ['GET'] }] },
|
|
218
|
-
{ id: 'ext' },
|
|
219
|
-
])
|
|
220
|
-
|
|
221
|
-
expect((result.host.activations ?? []).some((a) => a.kind === 'crud-response-enricher')).toBe(false)
|
|
222
|
-
const incoming = (result.host.incoming ?? []).filter((entry) => entry.contributionId === 'ext.record-badge')
|
|
223
|
-
expect(incoming).toHaveLength(1)
|
|
224
|
-
expect(incoming[0].resolution).toBe('capability-only')
|
|
225
|
-
expect(incoming[0].activationId).toBeUndefined()
|
|
226
|
-
|
|
227
|
-
const resolution = (result.ext.contributionResolutions ?? []).find((r) => r.contributionId === 'ext.record-badge')
|
|
228
|
-
expect(resolution?.resolution).toBe('capability-only')
|
|
229
|
-
expect(resolution?.activationIds).toEqual([])
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('binds a mutation guard only when the bridge is called, not from data/guards.ts alone', () => {
|
|
233
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
234
|
-
import { validateCrudMutationGuard } from 'x'
|
|
235
|
-
export async function POST(req, ctx) {
|
|
236
|
-
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
237
|
-
resourceKind: 'host:record', operation: 'update', requestMethod: 'POST',
|
|
238
|
-
})
|
|
239
|
-
return guardResult
|
|
240
|
-
}
|
|
241
|
-
`)
|
|
242
|
-
write(moduleRoot('guard-only'), 'data/guards.ts', `
|
|
243
|
-
export const guards = [{ id: 'guard-only.rec', targetEntity: 'guard-only:record', operations: ['update'], async validate() {} }]
|
|
244
|
-
`)
|
|
245
|
-
write(moduleRoot('ext'), 'data/guards.ts', `
|
|
246
|
-
export const guards = [{ id: 'ext.rec-guard', targetEntity: 'host:record', operations: ['update'], async validate() {} }]
|
|
247
|
-
`)
|
|
248
|
-
|
|
249
|
-
const result = runPipeline(roots, [
|
|
250
|
-
{ id: 'host', entities: [{ id: 'host:record' }], apiRoutes: [{ path: '/host/records', methods: ['POST'] }] },
|
|
251
|
-
{ id: 'guard-only', entities: [{ id: 'guard-only:record' }] },
|
|
252
|
-
{ id: 'ext' },
|
|
253
|
-
])
|
|
254
|
-
|
|
255
|
-
// Bridge call site produced a mutation-guard activation on the host.
|
|
256
|
-
expect((result.host.activations ?? []).some((a) => a.kind === 'mutation-guard' && a.host.id === 'host:record')).toBe(true)
|
|
257
|
-
// data/guards.ts existence alone does NOT create an activation for its own entity.
|
|
258
|
-
expect((result['guard-only'].activations ?? [])).toEqual([])
|
|
259
|
-
|
|
260
|
-
const boundGuard = (result.host.incoming ?? []).find((entry) => entry.contributionId === 'ext.rec-guard')
|
|
261
|
-
expect(boundGuard?.resolution).toBe('bound')
|
|
262
|
-
expect(boundGuard?.activationId).toBe('entity:host:record:mutation-guard')
|
|
263
|
-
|
|
264
|
-
const guardOnlyResolution = (result['guard-only'].contributionResolutions ?? []).find((r) => r.contributionId === 'guard-only.rec')
|
|
265
|
-
// Same-module guard whose entity has no bridge activation is capability-only.
|
|
266
|
-
expect(guardOnlyResolution?.resolution).toBe('capability-only')
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('binds api and command interceptor contributions to exact host identities across modules', () => {
|
|
270
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
271
|
-
import { makeCrudRoute } from 'x'
|
|
272
|
-
export const { POST } = makeCrudRoute({ orm: { entity: Rec } })
|
|
273
|
-
`)
|
|
274
|
-
write(moduleRoot('host'), 'commands/update.ts', `
|
|
275
|
-
const updateCommand = { id: 'host.records.update', async execute() {} }
|
|
276
|
-
registerCommand(updateCommand)
|
|
277
|
-
`)
|
|
278
|
-
write(moduleRoot('ext'), 'api/interceptors.ts', `
|
|
279
|
-
export const interceptors = [{ id: 'ext.records.interceptor', targetRoute: 'host/records', methods: ['POST'], async before() {}, async after() {} }]
|
|
280
|
-
`)
|
|
281
|
-
write(moduleRoot('ext'), 'commands/interceptors.ts', `
|
|
282
|
-
export const interceptors = [{ id: 'ext.records.command', targetCommand: 'host.records.update', async beforeExecute() {} }]
|
|
283
|
-
`)
|
|
284
|
-
|
|
285
|
-
const result = runPipeline(roots, [
|
|
286
|
-
{ id: 'host', apiRoutes: [{ path: '/host/records', methods: ['POST'] }] },
|
|
287
|
-
{ id: 'ext' },
|
|
288
|
-
])
|
|
289
|
-
|
|
290
|
-
const apiIncoming = (result.host.incoming ?? []).find((entry) => entry.contributionId === 'ext.records.interceptor')
|
|
291
|
-
expect(apiIncoming?.resolution).toBe('bound')
|
|
292
|
-
expect(apiIncoming?.target).toEqual(expect.objectContaining({ kind: 'api-route', id: 'host/records', moduleId: 'host' }))
|
|
293
|
-
// The activation identity carries the bridged method the interceptor matched.
|
|
294
|
-
expect(apiIncoming?.activationId).toBe('api-route:host/records:POST:api-interceptor-bridge')
|
|
295
|
-
const apiActivation = (result.host.activations ?? []).find((a) => a.id === 'api-route:host/records:POST:api-interceptor-bridge')
|
|
296
|
-
expect(apiActivation?.phases).toEqual(['after', 'before'])
|
|
297
|
-
expect(apiActivation?.host.method).toBe('POST')
|
|
298
|
-
expect(apiActivation?.source.sourcePath).toContain('host/api/records/route.ts')
|
|
299
|
-
|
|
300
|
-
const commandIncoming = (result.host.incoming ?? []).find((entry) => entry.contributionId === 'ext.records.command')
|
|
301
|
-
expect(commandIncoming?.resolution).toBe('bound')
|
|
302
|
-
expect(commandIncoming?.target).toEqual(expect.objectContaining({ kind: 'command', id: 'host.records.update', moduleId: 'host' }))
|
|
303
|
-
expect(commandIncoming?.activationId).toBe('command:host.records.update:command-interceptor-bridge')
|
|
304
|
-
const commandActivation = (result.host.activations ?? [])
|
|
305
|
-
.find((activation) => activation.id === 'command:host.records.update:command-interceptor-bridge')
|
|
306
|
-
expect(commandActivation?.bridge).toEqual({
|
|
307
|
-
factSection: 'ownedContracts.command',
|
|
308
|
-
factKey: 'host.records.update',
|
|
309
|
-
})
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
it('binds widget-spot and replaceable-component consumption to the declared host', () => {
|
|
313
|
-
write(moduleRoot('host'), 'extension-points.ts', `
|
|
314
|
-
import { defineModuleExtensionPoints, dataTableExtensionHost, componentExtensionHost } from 'x'
|
|
315
|
-
export const extensionPoints = defineModuleExtensionPoints({
|
|
316
|
-
moduleId: 'host',
|
|
317
|
-
hosts: {
|
|
318
|
-
records: dataTableExtensionHost({ tableId: 'host.records', source: 'Records.tsx' }),
|
|
319
|
-
detail: componentExtensionHost({ componentId: 'page:host.detail', source: 'Detail.tsx' }),
|
|
320
|
-
},
|
|
321
|
-
})
|
|
322
|
-
`)
|
|
323
|
-
write(moduleRoot('host'), 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
|
|
324
|
-
write(moduleRoot('host'), 'Detail.tsx', 'export const id = extensionPoints.hosts.detail.componentId')
|
|
325
|
-
write(moduleRoot('ext'), 'widgets/injection-table.ts', `
|
|
326
|
-
export const injectionTable = {
|
|
327
|
-
'data-table:host.records:columns': [{ widgetId: 'ext.column' }],
|
|
328
|
-
}
|
|
329
|
-
`)
|
|
330
|
-
write(moduleRoot('ext'), 'widgets/components.ts', `
|
|
331
|
-
export const componentOverrides = [{ target: { componentId: 'page:host.detail' }, wrapper: true }]
|
|
332
|
-
`)
|
|
333
|
-
|
|
334
|
-
const result = runPipeline(roots, [
|
|
335
|
-
{ id: 'host', entities: [{ id: 'host:record' }] },
|
|
336
|
-
{ id: 'ext' },
|
|
337
|
-
])
|
|
338
|
-
|
|
339
|
-
const columnIncoming = (result.host.incoming ?? []).find((entry) => entry.contributionKind === 'data-table')
|
|
340
|
-
expect(columnIncoming?.resolution).toBe('bound')
|
|
341
|
-
expect(columnIncoming?.target).toEqual(expect.objectContaining({ kind: 'widget-spot', id: 'data-table:host.records:columns', moduleId: 'host' }))
|
|
342
|
-
const columnActivation = (result.host.activations ?? []).find((a) => a.kind === 'widget-injection-consumer')
|
|
343
|
-
expect(columnActivation?.source.sourcePath).toContain('host/extension-points.ts')
|
|
344
|
-
|
|
345
|
-
const componentIncoming = (result.host.incoming ?? []).find((entry) => entry.contributionKind === 'component-override')
|
|
346
|
-
expect(componentIncoming?.resolution).toBe('bound')
|
|
347
|
-
expect(componentIncoming?.target).toEqual(expect.objectContaining({ kind: 'component', id: 'page:host.detail', moduleId: 'host' }))
|
|
348
|
-
expect((result.host.activations ?? []).some((a) => a.kind === 'component-extension-consumer')).toBe(true)
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
it('records optional-missing and wildcard resolutions without incoming rows and without failing the build', () => {
|
|
352
|
-
write(moduleRoot('ext'), 'data/enrichers.ts', `
|
|
353
|
-
export const enrichers = [{ id: 'ext.optional', targetEntity: 'absent:thing', async enrichOne(r) { return r } }]
|
|
354
|
-
`)
|
|
355
|
-
write(moduleRoot('ext'), 'api/interceptors.ts', `
|
|
356
|
-
export const interceptors = [{ id: 'ext.wildcard', targetRoute: '*', methods: ['POST'], async before() {} }]
|
|
357
|
-
`)
|
|
358
|
-
|
|
359
|
-
const result = runPipeline(roots, [{ id: 'ext' }])
|
|
360
|
-
|
|
361
|
-
const optional = (result.ext.contributionResolutions ?? []).find((r) => r.contributionId === 'ext.optional')
|
|
362
|
-
expect(optional?.resolution).toBe('optional-target-missing')
|
|
363
|
-
expect(optional?.activationIds).toEqual([])
|
|
364
|
-
|
|
365
|
-
const wildcard = (result.ext.contributionResolutions ?? []).find((r) => r.contributionId === 'ext.wildcard')
|
|
366
|
-
expect(wildcard?.resolution).toBe('wildcard')
|
|
367
|
-
expect(wildcard?.target.kind).toBe('wildcard')
|
|
368
|
-
|
|
369
|
-
// Neither optional-missing nor wildcard owns a concrete target, so no incoming rows exist anywhere.
|
|
370
|
-
for (const surface of Object.values(result)) {
|
|
371
|
-
expect((surface.incoming ?? []).length).toBe(0)
|
|
372
|
-
}
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it('emits one incoming row per duplicate provider targeting the same host activation', () => {
|
|
376
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
377
|
-
import { makeCrudRoute } from 'x'
|
|
378
|
-
export const crud = makeCrudRoute({ orm: { entity: Rec }, enrichers: { entityId: 'host:record' } })
|
|
379
|
-
export const GET = crud.GET
|
|
380
|
-
`)
|
|
381
|
-
write(moduleRoot('ext-a'), 'data/enrichers.ts', `
|
|
382
|
-
export const enrichers = [{ id: 'ext-a.badge', targetEntity: 'host:record', async enrichOne(r) { return r } }]
|
|
383
|
-
`)
|
|
384
|
-
write(moduleRoot('ext-b'), 'data/enrichers.ts', `
|
|
385
|
-
export const enrichers = [{ id: 'ext-b.badge', targetEntity: 'host:record', async enrichOne(r) { return r } }]
|
|
386
|
-
`)
|
|
387
|
-
|
|
388
|
-
const result = runPipeline(roots, [
|
|
389
|
-
{ id: 'host', entities: [{ id: 'host:record' }], apiRoutes: [{ path: '/host/records', methods: ['GET'] }] },
|
|
390
|
-
{ id: 'ext-a' },
|
|
391
|
-
{ id: 'ext-b' },
|
|
392
|
-
])
|
|
393
|
-
|
|
394
|
-
const contributors = new Set((result.host.incoming ?? []).map((entry) => entry.contributorModuleId))
|
|
395
|
-
expect(contributors).toEqual(new Set(['ext-a', 'ext-b']))
|
|
396
|
-
// The route activates response enrichment only, so each contributor matches one activation.
|
|
397
|
-
expect((result.host.incoming ?? []).filter((e) => e.contributorModuleId === 'ext-a')).toHaveLength(1)
|
|
398
|
-
expect((result.host.incoming ?? []).filter((e) => e.contributorModuleId === 'ext-b')).toHaveLength(1)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
it('marks a genuinely unresolved concrete target distinctly from optional-missing', () => {
|
|
402
|
-
const contribution: ModuleExtensionContributionFact = {
|
|
403
|
-
id: 'ext.broken',
|
|
404
|
-
kind: 'response-enricher',
|
|
405
|
-
targets: [{ id: 'host:missing', resolution: 'unresolved' }],
|
|
406
|
-
scopeContract: 'tenant-and-organization',
|
|
407
|
-
source: { path: 'data/enrichers.ts', symbol: 'ext.broken' },
|
|
408
|
-
details: {
|
|
409
|
-
targetEntity: 'host:missing', surfaces: ['detail'], timeoutMs: 2000,
|
|
410
|
-
fallback: 'none', critical: false, cachePosture: 'rerun-on-list-cache-hit',
|
|
411
|
-
},
|
|
412
|
-
}
|
|
413
|
-
const result = correlateIncomingExtensions({
|
|
414
|
-
surfacesByModule: {
|
|
415
|
-
ext: { hosts: [], contributions: [contribution], unresolved: [], activations: [] },
|
|
416
|
-
},
|
|
417
|
-
})
|
|
418
|
-
const resolution = (result.ext.contributionResolutions ?? [])[0]
|
|
419
|
-
expect(resolution.resolution).toBe('unresolved')
|
|
420
|
-
expect((result.ext.incoming ?? [])).toEqual([])
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
it('leaves the existing hosts and contributions arrays untouched by the additive topology fields', () => {
|
|
424
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
425
|
-
import { makeCrudRoute } from 'x'
|
|
426
|
-
export const crud = makeCrudRoute({ orm: { entity: Rec }, enrichers: { entityId: 'host:record' } })
|
|
427
|
-
export const GET = crud.GET
|
|
428
|
-
`)
|
|
429
|
-
const surface = withModuleExtensionFactExtractionCache(() => extractModuleExtensionFacts({
|
|
430
|
-
moduleId: 'host',
|
|
431
|
-
moduleRoot: roots.host = moduleRoot('host'),
|
|
432
|
-
sourceRoot: 'node_modules/pkg/src/modules/host',
|
|
433
|
-
entities: [{ id: 'host:record' }],
|
|
434
|
-
events: [],
|
|
435
|
-
apiRoutes: [{ path: '/host/records', methods: ['GET'] }],
|
|
436
|
-
searchEntities: [],
|
|
437
|
-
}))
|
|
438
|
-
const correlated = correlateModuleExtensionFacts({
|
|
439
|
-
surfacesByModule: { host: surface },
|
|
440
|
-
entityIds: new Set(['host:record']),
|
|
441
|
-
eventIds: new Set(),
|
|
442
|
-
apiRoutes: new Set(['/host/records']),
|
|
443
|
-
commandIds: new Set(),
|
|
444
|
-
})
|
|
445
|
-
const withIncoming = correlateIncomingExtensions({ surfacesByModule: correlated }).host
|
|
446
|
-
expect(withIncoming.hosts).toEqual(correlated.host.hosts)
|
|
447
|
-
expect(withIncoming.contributions).toEqual(correlated.host.contributions)
|
|
448
|
-
expect(Array.isArray(withIncoming.activations)).toBe(true)
|
|
449
|
-
expect(Array.isArray(withIncoming.incoming)).toBe(true)
|
|
450
|
-
expect(Array.isArray(withIncoming.contributionResolutions)).toBe(true)
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
it('renders three distinct markdown sections without merging capability and bound rows', () => {
|
|
454
|
-
const surface: ModuleExtensionSurfaceFacts = {
|
|
455
|
-
hosts: [],
|
|
456
|
-
contributions: [],
|
|
457
|
-
unresolved: [],
|
|
458
|
-
activations: [{
|
|
459
|
-
id: 'entity:host:record:crud-response-enricher',
|
|
460
|
-
kind: 'crud-response-enricher',
|
|
461
|
-
host: { kind: 'entity', id: 'host:record', moduleId: 'host' },
|
|
462
|
-
contributionKinds: ['response-enricher'],
|
|
463
|
-
source: { sourcePath: 'node_modules/pkg/src/modules/host/api/records/route.ts', line: 5 },
|
|
464
|
-
bridge: { factSection: 'apiRoutes', factKey: '/host/records' },
|
|
465
|
-
}],
|
|
466
|
-
incoming: [{
|
|
467
|
-
contributionId: 'ext.badge',
|
|
468
|
-
contributionKind: 'response-enricher',
|
|
469
|
-
contributorModuleId: 'ext',
|
|
470
|
-
target: { kind: 'entity', id: 'host:record', moduleId: 'host' },
|
|
471
|
-
activationId: 'entity:host:record:crud-response-enricher',
|
|
472
|
-
resolution: 'bound',
|
|
473
|
-
source: { sourcePath: 'node_modules/pkg/src/modules/ext/data/enrichers.ts', exportName: 'ext.badge' },
|
|
474
|
-
}],
|
|
475
|
-
contributionResolutions: [{
|
|
476
|
-
contributionId: 'ext.badge',
|
|
477
|
-
target: { kind: 'entity', id: 'host:record', moduleId: 'host' },
|
|
478
|
-
resolution: 'bound',
|
|
479
|
-
activationIds: ['entity:host:record:crud-response-enricher'],
|
|
480
|
-
}],
|
|
481
|
-
}
|
|
482
|
-
const facts: ModuleFacts = {
|
|
483
|
-
module: 'host', title: null, description: null, coreVersion: null, sourcePackage: null,
|
|
484
|
-
sourceVersion: null, sourceRoot: 'node_modules/pkg/src/modules/host', entities: [], events: [],
|
|
485
|
-
aclFeatures: [], apiRoutes: [], diTokens: [], searchEntities: [],
|
|
486
|
-
hostTokens: { entityIds: [], tableIds: [] }, notifications: [], cli: [],
|
|
487
|
-
backendPages: [], frontendPages: [], cliCommands: [], aiTools: [], aiAgents: [],
|
|
488
|
-
extensionSurfaces: surface, warnings: [],
|
|
489
|
-
}
|
|
490
|
-
const markdown = renderModuleFactsMarkdown(facts)
|
|
491
|
-
expect(markdown).toContain('## UMES hosts')
|
|
492
|
-
expect(markdown).toContain('## Active extension bindings')
|
|
493
|
-
expect(markdown).toContain('## Incoming installed contributions')
|
|
494
|
-
expect(markdown).toContain('crud-response-enricher')
|
|
495
|
-
expect(markdown).toContain('entity:host:record @host')
|
|
496
|
-
expect(markdown.indexOf('## Active extension bindings')).toBeLessThan(markdown.indexOf('## Incoming installed contributions'))
|
|
497
|
-
|
|
498
|
-
// Activation rows are identified by the referenced activation id and bridge, not by kind alone.
|
|
499
|
-
expect(markdown).toContain('| Activation | Kind | Host | Contribution kinds | Phases | Bridge | Source |')
|
|
500
|
-
expect(markdown).toContain('| entity:host:record:crud-response-enricher | crud-response-enricher |')
|
|
501
|
-
expect(markdown).toContain('apiRoutes:/host/records')
|
|
502
|
-
|
|
503
|
-
// Contribution resolutions render as their own section.
|
|
504
|
-
expect(markdown).toContain('## Contribution resolutions')
|
|
505
|
-
expect(markdown).toContain('| Contribution | Target | Resolution | Activations | Source |')
|
|
506
|
-
expect(markdown).toContain('| ext.badge | entity:host:record @host | bound | entity:host:record:crud-response-enricher |')
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
it('never binds an api interceptor to a route with no interceptor bridge', () => {
|
|
510
|
-
write(moduleRoot('host'), 'api/custom/route.ts', `
|
|
511
|
-
export async function POST(req) { return new Response('ok') }
|
|
512
|
-
`)
|
|
513
|
-
write(moduleRoot('ext'), 'api/interceptors.ts', `
|
|
514
|
-
export const interceptors = [{ id: 'ext.custom.interceptor', targetRoute: 'host/custom', methods: ['POST'], async before() {} }]
|
|
515
|
-
`)
|
|
516
|
-
|
|
517
|
-
const result = runPipeline(roots, [
|
|
518
|
-
{ id: 'host', apiRoutes: [{ path: '/host/custom', methods: ['POST'] }] },
|
|
519
|
-
{ id: 'ext' },
|
|
520
|
-
])
|
|
521
|
-
|
|
522
|
-
const resolution = (result.ext.contributionResolutions ?? [])
|
|
523
|
-
.find((entry) => entry.contributionId === 'ext.custom.interceptor')
|
|
524
|
-
expect(resolution?.resolution).toBe('capability-only')
|
|
525
|
-
expect(resolution?.activationIds).toEqual([])
|
|
526
|
-
expect((result.host.activations ?? []).filter((a) => a.kind === 'api-interceptor-bridge')).toEqual([])
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('binds only the phase a hand-written route actually runs', () => {
|
|
530
|
-
write(moduleRoot('host'), 'api/custom/route.ts', `
|
|
531
|
-
import { runApiInterceptorsAfter } from 'x'
|
|
532
|
-
export async function POST(req) {
|
|
533
|
-
const response = await handle(req)
|
|
534
|
-
return runApiInterceptorsAfter({ routePath: '/host/custom', method: 'POST', response, context: ctx })
|
|
535
|
-
}
|
|
536
|
-
`)
|
|
537
|
-
write(moduleRoot('ext'), 'api/interceptors.ts', `
|
|
538
|
-
export const interceptors = [
|
|
539
|
-
{ id: 'ext.after-only', targetRoute: 'host/custom', methods: ['POST'], async after() {} },
|
|
540
|
-
{ id: 'ext.before-only', targetRoute: 'host/custom', methods: ['POST'], async before() {} },
|
|
541
|
-
]
|
|
542
|
-
`)
|
|
543
|
-
|
|
544
|
-
const result = runPipeline(roots, [
|
|
545
|
-
{ id: 'host', apiRoutes: [{ path: '/host/custom', methods: ['POST'] }] },
|
|
546
|
-
{ id: 'ext' },
|
|
547
|
-
])
|
|
548
|
-
|
|
549
|
-
const byId = new Map((result.ext.contributionResolutions ?? []).map((entry) => [entry.contributionId, entry]))
|
|
550
|
-
expect(byId.get('ext.after-only')?.resolution).toBe('bound')
|
|
551
|
-
expect(byId.get('ext.after-only')?.activationIds).toEqual(['api-route:host/custom:POST:api-interceptor-bridge'])
|
|
552
|
-
// The route never runs the before phase, so a before-only interceptor is not active.
|
|
553
|
-
expect(byId.get('ext.before-only')?.resolution).toBe('capability-only')
|
|
554
|
-
expect(byId.get('ext.before-only')?.activationIds).toEqual([])
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
it('never binds an api interceptor whose method the route does not handle', () => {
|
|
558
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
559
|
-
import { makeCrudRoute } from 'x'
|
|
560
|
-
export const crud = makeCrudRoute({ orm: { entity: Rec } })
|
|
561
|
-
export const GET = crud.GET
|
|
562
|
-
`)
|
|
563
|
-
write(moduleRoot('ext'), 'api/interceptors.ts', `
|
|
564
|
-
export const interceptors = [{ id: 'ext.delete-only', targetRoute: 'host/records', methods: ['DELETE'], async before() {} }]
|
|
565
|
-
`)
|
|
566
|
-
|
|
567
|
-
const result = runPipeline(roots, [
|
|
568
|
-
{ id: 'host', apiRoutes: [{ path: '/host/records', methods: ['GET'] }] },
|
|
569
|
-
{ id: 'ext' },
|
|
570
|
-
])
|
|
571
|
-
|
|
572
|
-
const resolution = (result.ext.contributionResolutions ?? [])
|
|
573
|
-
.find((entry) => entry.contributionId === 'ext.delete-only')
|
|
574
|
-
expect(resolution?.resolution).toBe('capability-only')
|
|
575
|
-
expect(resolution?.activationIds).toEqual([])
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
it('binds the canonical runRouteMutationGuards shape declared in a lib wrapper', () => {
|
|
579
|
-
write(moduleRoot('host'), 'lib/route-mutation-guard.ts', `
|
|
580
|
-
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
581
|
-
export async function guardRecordMutation(container, req, auth) {
|
|
582
|
-
return runRouteMutationGuards({
|
|
583
|
-
container,
|
|
584
|
-
req,
|
|
585
|
-
auth,
|
|
586
|
-
input: { resourceKind: 'host:record', operation: 'delete' },
|
|
587
|
-
})
|
|
588
|
-
}
|
|
589
|
-
`)
|
|
590
|
-
write(moduleRoot('ext'), 'data/guards.ts', `
|
|
591
|
-
export const guards = [{ id: 'ext.rec-delete', targetEntity: 'host:record', operations: ['delete'], async validate() {} }]
|
|
592
|
-
`)
|
|
593
|
-
|
|
594
|
-
const result = runPipeline(roots, [
|
|
595
|
-
{ id: 'host', entities: [{ id: 'host:record' }] },
|
|
596
|
-
{ id: 'ext' },
|
|
597
|
-
])
|
|
598
|
-
|
|
599
|
-
const activation = (result.host.activations ?? []).find((entry) => entry.kind === 'mutation-guard')
|
|
600
|
-
expect(activation?.host).toEqual({ kind: 'entity', id: 'host:record', moduleId: 'host' })
|
|
601
|
-
expect(activation?.operations).toEqual(['delete'])
|
|
602
|
-
expect(activation?.source.sourcePath).toContain('host/lib/route-mutation-guard.ts')
|
|
603
|
-
|
|
604
|
-
const resolution = (result.ext.contributionResolutions ?? [])
|
|
605
|
-
.find((entry) => entry.contributionId === 'ext.rec-delete')
|
|
606
|
-
expect(resolution?.resolution).toBe('bound')
|
|
607
|
-
expect(resolution?.activationIds).toEqual(['entity:host:record:mutation-guard'])
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
it('does not bind a guard whose operations the call site never guards', () => {
|
|
611
|
-
write(moduleRoot('host'), 'lib/route-mutation-guard.ts', `
|
|
612
|
-
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
613
|
-
export async function guardRecordDelete(container, req, auth) {
|
|
614
|
-
return runRouteMutationGuards({ container, req, auth, input: { resourceKind: 'host:record', operation: 'delete' } })
|
|
615
|
-
}
|
|
616
|
-
`)
|
|
617
|
-
write(moduleRoot('ext'), 'data/guards.ts', `
|
|
618
|
-
export const guards = [{ id: 'ext.rec-create', targetEntity: 'host:record', operations: ['create'], async validate() {} }]
|
|
619
|
-
`)
|
|
620
|
-
|
|
621
|
-
const result = runPipeline(roots, [
|
|
622
|
-
{ id: 'host', entities: [{ id: 'host:record' }] },
|
|
623
|
-
{ id: 'ext' },
|
|
624
|
-
])
|
|
625
|
-
|
|
626
|
-
const resolution = (result.ext.contributionResolutions ?? [])
|
|
627
|
-
.find((entry) => entry.contributionId === 'ext.rec-create')
|
|
628
|
-
expect(resolution?.resolution).toBe('capability-only')
|
|
629
|
-
expect(resolution?.activationIds).toEqual([])
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
it('unions operations from multiple mutation-guard bridges for the same entity', () => {
|
|
633
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
634
|
-
import { validateCrudMutationGuard } from 'x'
|
|
635
|
-
export async function POST(container) {
|
|
636
|
-
return validateCrudMutationGuard(container, { resourceKind: 'host:record', operation: 'create' })
|
|
637
|
-
}
|
|
638
|
-
export async function DELETE(container) {
|
|
639
|
-
return validateCrudMutationGuard(container, { resourceKind: 'host:record', operation: 'delete' })
|
|
640
|
-
}
|
|
641
|
-
`)
|
|
642
|
-
write(moduleRoot('ext'), 'data/guards.ts', `
|
|
643
|
-
export const guards = [
|
|
644
|
-
{ id: 'ext.rec-create', targetEntity: 'host:record', operations: ['create'], async validate() {} },
|
|
645
|
-
{ id: 'ext.rec-delete', targetEntity: 'host:record', operations: ['delete'], async validate() {} },
|
|
646
|
-
]
|
|
647
|
-
`)
|
|
648
|
-
|
|
649
|
-
const result = runPipeline(roots, [
|
|
650
|
-
{ id: 'host', entities: [{ id: 'host:record' }] },
|
|
651
|
-
{ id: 'ext' },
|
|
652
|
-
])
|
|
653
|
-
|
|
654
|
-
const activation = (result.host.activations ?? []).find((entry) => entry.kind === 'mutation-guard')
|
|
655
|
-
expect(activation?.operations).toEqual(['create', 'delete'])
|
|
656
|
-
const resolutions = new Map(
|
|
657
|
-
(result.ext.contributionResolutions ?? []).map((entry) => [entry.contributionId, entry]),
|
|
658
|
-
)
|
|
659
|
-
expect(resolutions.get('ext.rec-create')?.resolution).toBe('bound')
|
|
660
|
-
expect(resolutions.get('ext.rec-delete')?.resolution).toBe('bound')
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
it('maps a custom action endpoint onto the update registry operation', () => {
|
|
664
|
-
write(moduleRoot('host'), 'api/records/archive/route.ts', `
|
|
665
|
-
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
666
|
-
export async function POST(req) {
|
|
667
|
-
return runRouteMutationGuards({ container, req, auth, input: { resourceKind: 'host:record', operation: 'custom' } })
|
|
668
|
-
}
|
|
669
|
-
`)
|
|
670
|
-
write(moduleRoot('ext'), 'data/guards.ts', `
|
|
671
|
-
export const guards = [{ id: 'ext.rec-update', targetEntity: 'host:record', operations: ['update'], async validate() {} }]
|
|
672
|
-
`)
|
|
673
|
-
|
|
674
|
-
const result = runPipeline(roots, [
|
|
675
|
-
{ id: 'host', entities: [{ id: 'host:record' }] },
|
|
676
|
-
{ id: 'ext' },
|
|
677
|
-
])
|
|
678
|
-
|
|
679
|
-
const activation = (result.host.activations ?? []).find((entry) => entry.kind === 'mutation-guard')
|
|
680
|
-
expect(activation?.operations).toEqual(['update'])
|
|
681
|
-
const resolution = (result.ext.contributionResolutions ?? [])
|
|
682
|
-
.find((entry) => entry.contributionId === 'ext.rec-update')
|
|
683
|
-
expect(resolution?.resolution).toBe('bound')
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
it('executes every activation adapter: each kind produces a bound activation', () => {
|
|
687
|
-
write(moduleRoot('host'), 'extension-points.ts', `
|
|
688
|
-
import { defineModuleExtensionPoints, dataTableExtensionHost, componentExtensionHost } from 'x'
|
|
689
|
-
export const extensionPoints = defineModuleExtensionPoints({
|
|
690
|
-
moduleId: 'host',
|
|
691
|
-
hosts: {
|
|
692
|
-
records: dataTableExtensionHost({ tableId: 'host.records', source: 'Records.tsx' }),
|
|
693
|
-
detail: componentExtensionHost({ componentId: 'page:host.detail', source: 'Detail.tsx' }),
|
|
694
|
-
},
|
|
695
|
-
})
|
|
696
|
-
`)
|
|
697
|
-
write(moduleRoot('host'), 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
|
|
698
|
-
write(moduleRoot('host'), 'Detail.tsx', 'export const id = extensionPoints.hosts.detail.componentId')
|
|
699
|
-
write(moduleRoot('host'), 'api/records/route.ts', `
|
|
700
|
-
import { makeCrudRoute } from 'x'
|
|
701
|
-
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
702
|
-
export const crud = makeCrudRoute({ orm: { entity: Rec }, enrichers: { entityId: 'host:record' } })
|
|
703
|
-
export const POST = crud.POST
|
|
704
|
-
export async function GET(req) {
|
|
705
|
-
await runRouteMutationGuards({ container, req, auth, input: { resourceKind: 'host:record' } })
|
|
706
|
-
return queryEngine.query('host:record', { tenantId: 't', extensions: { userId: 'u' } })
|
|
707
|
-
}
|
|
708
|
-
`)
|
|
709
|
-
write(moduleRoot('host'), 'commands/update.ts', `
|
|
710
|
-
const updateCommand = { id: 'host.records.update', async execute() {} }
|
|
711
|
-
registerCommand(updateCommand)
|
|
712
|
-
`)
|
|
713
|
-
write(moduleRoot('ext'), 'data/enrichers.ts', `
|
|
714
|
-
export const enrichers = [{ id: 'ext.badge', targetEntity: 'host:record', async enrichOne(r) { return r } }]
|
|
715
|
-
`)
|
|
716
|
-
write(moduleRoot('ext'), 'data/guards.ts', `
|
|
717
|
-
export const guards = [{ id: 'ext.guard', targetEntity: 'host:record', operations: ['update'], async validate() {} }]
|
|
718
|
-
`)
|
|
719
|
-
write(moduleRoot('ext'), 'api/interceptors.ts', `
|
|
720
|
-
export const interceptors = [{ id: 'ext.interceptor', targetRoute: 'host/records', methods: ['POST'], async before() {} }]
|
|
721
|
-
`)
|
|
722
|
-
write(moduleRoot('ext'), 'commands/interceptors.ts', `
|
|
723
|
-
export const interceptors = [{ id: 'ext.command', targetCommand: 'host.records.update', async beforeExecute() {} }]
|
|
724
|
-
`)
|
|
725
|
-
write(moduleRoot('ext'), 'widgets/injection-table.ts', `
|
|
726
|
-
export const injectionTable = {
|
|
727
|
-
'data-table:host.records:columns': [{ widgetId: 'ext.column' }],
|
|
728
|
-
}
|
|
729
|
-
`)
|
|
730
|
-
write(moduleRoot('ext'), 'widgets/components.ts', `
|
|
731
|
-
export const componentOverrides = [{ target: { componentId: 'page:host.detail' }, wrapper: true }]
|
|
732
|
-
`)
|
|
733
|
-
write(moduleRoot('ext'), 'widgets/dashboard/stats/widget.ts', `
|
|
734
|
-
export const widget = { metadata: { id: 'ext.stats', title: 'Stats' } }
|
|
735
|
-
export default widget
|
|
736
|
-
`)
|
|
737
|
-
|
|
738
|
-
const result = runPipeline(roots, [
|
|
739
|
-
{ id: 'host', entities: [{ id: 'host:record' }], apiRoutes: [{ path: '/host/records', methods: ['GET', 'POST'] }] },
|
|
740
|
-
{ id: 'ext' },
|
|
741
|
-
])
|
|
742
|
-
|
|
743
|
-
const activationKinds = new Set(
|
|
744
|
-
Object.values(result).flatMap((surface) => (surface.activations ?? []).map((activation) => activation.kind)),
|
|
745
|
-
)
|
|
746
|
-
expect([...activationKinds].sort()).toEqual([...ALL_ACTIVATION_KINDS].sort())
|
|
747
|
-
|
|
748
|
-
// Dashboard contributions bind to the framework `dashboard:*` host, which owns no
|
|
749
|
-
// module surface — the activation is recorded on the contributor with a traceable
|
|
750
|
-
// framework source, and no incoming row is emitted.
|
|
751
|
-
const dashboardActivation = (result.ext.activations ?? [])
|
|
752
|
-
.find((activation) => activation.kind === 'dashboard-host-consumer')
|
|
753
|
-
expect(dashboardActivation?.source.sourcePath).toBe('packages/ui/src')
|
|
754
|
-
expect(dashboardActivation?.bridge).toEqual({ factSection: 'hosts', factKey: 'framework.dashboard' })
|
|
755
|
-
expect((result.ext.incoming ?? []).some((entry) => entry.contributionKind === 'widget'
|
|
756
|
-
&& entry.target.id.startsWith('dashboard:'))).toBe(false)
|
|
757
|
-
|
|
758
|
-
const dashboardResolution = (result.ext.contributionResolutions ?? [])
|
|
759
|
-
.find((entry) => entry.target.id.startsWith('dashboard:'))
|
|
760
|
-
expect(dashboardResolution?.resolution).toBe('bound')
|
|
761
|
-
})
|
|
762
|
-
})
|