@open-mercato/cli 0.6.8-develop.6931.1.4a1e1c786b → 0.6.8-develop.6940.1.177ea30c6e
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 +65 -1
- package/build.mjs +57 -7
- package/dist/agentic/guides/backend-ui.md +7 -1
- package/dist/agentic/guides/module-facts.json +565 -224
- package/dist/agentic/guides/module-facts.v2.json +122933 -0
- package/dist/agentic/guides/modules/ai_assistant.md +2 -2
- package/dist/agentic/guides/modules/api_docs.md +2 -2
- package/dist/agentic/guides/modules/api_keys.md +2 -2
- package/dist/agentic/guides/modules/attachments.md +2 -2
- package/dist/agentic/guides/modules/audit_logs.md +2 -2
- package/dist/agentic/guides/modules/auth.md +2 -2
- package/dist/agentic/guides/modules/business_rules.md +2 -2
- package/dist/agentic/guides/modules/catalog.md +11 -4
- package/dist/agentic/guides/modules/channel_gmail.md +2 -2
- package/dist/agentic/guides/modules/channel_imap.md +2 -2
- package/dist/agentic/guides/modules/checkout.md +8 -2
- package/dist/agentic/guides/modules/communication_channels.md +6 -6
- package/dist/agentic/guides/modules/configs.md +2 -2
- package/dist/agentic/guides/modules/content.md +2 -2
- package/dist/agentic/guides/modules/currencies.md +2 -2
- package/dist/agentic/guides/modules/customer_accounts.md +2 -2
- package/dist/agentic/guides/modules/customers.md +8 -8
- package/dist/agentic/guides/modules/dashboards.md +12 -12
- package/dist/agentic/guides/modules/data_sync.md +4 -4
- package/dist/agentic/guides/modules/design_system.md +2 -2
- package/dist/agentic/guides/modules/dictionaries.md +2 -2
- package/dist/agentic/guides/modules/directory.md +2 -2
- package/dist/agentic/guides/modules/entities.md +3 -3
- package/dist/agentic/guides/modules/eudr.md +3 -3
- package/dist/agentic/guides/modules/events.md +2 -2
- package/dist/agentic/guides/modules/feature_toggles.md +2 -2
- package/dist/agentic/guides/modules/gateway_stripe.md +2 -2
- package/dist/agentic/guides/modules/generators.md +2 -2
- package/dist/agentic/guides/modules/inbox_ops.md +2 -2
- package/dist/agentic/guides/modules/integrations.md +5 -2
- package/dist/agentic/guides/modules/messages.md +2 -2
- package/dist/agentic/guides/modules/notifications.md +2 -2
- package/dist/agentic/guides/modules/onboarding.md +2 -2
- package/dist/agentic/guides/modules/payment_gateways.md +7 -3
- package/dist/agentic/guides/modules/perspectives.md +2 -2
- package/dist/agentic/guides/modules/planner.md +2 -2
- package/dist/agentic/guides/modules/portal.md +2 -2
- package/dist/agentic/guides/modules/progress.md +2 -2
- package/dist/agentic/guides/modules/query_index.md +2 -2
- package/dist/agentic/guides/modules/record_locks.md +3 -3
- package/dist/agentic/guides/modules/resources.md +2 -2
- package/dist/agentic/guides/modules/sales.md +13 -4
- package/dist/agentic/guides/modules/scheduler.md +2 -2
- package/dist/agentic/guides/modules/search.md +4 -4
- package/dist/agentic/guides/modules/security.md +3 -3
- package/dist/agentic/guides/modules/shipping_carriers.md +2 -2
- package/dist/agentic/guides/modules/sso.md +2 -2
- package/dist/agentic/guides/modules/staff.md +7 -4
- package/dist/agentic/guides/modules/storage_s3.md +2 -2
- package/dist/agentic/guides/modules/sync_akeneo.md +4 -4
- package/dist/agentic/guides/modules/sync_excel.md +2 -2
- package/dist/agentic/guides/modules/system_status_overlays.md +2 -2
- package/dist/agentic/guides/modules/translations.md +2 -2
- package/dist/agentic/guides/modules/webhooks.md +2 -2
- package/dist/agentic/guides/modules/wms.md +31 -4
- package/dist/agentic/guides/modules/workflows.md +2 -2
- package/dist/agentic/guides/reference-module-facts.json +8335 -0
- package/dist/agentic/guides/reference-modules/example.md +489 -0
- package/dist/agentic/guides/spec-delivery.md +34 -0
- package/dist/agentic/guides/testing-debugging.md +2 -1
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +3 -2
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/AGENTS.md.template +10 -14
- package/dist/agentic/shared/ai/harness/README.md +4 -4
- package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
- package/dist/agentic/shared/ai/harness/cases.json +11350 -5088
- package/dist/agentic/shared/ai/harness/cases.schema.json +70 -7
- package/dist/agentic/shared/ai/harness/design-system-inventory.json +6697 -0
- package/dist/agentic/shared/ai/harness/fixtures/index.json +3 -1
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +4 -0
- package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +9 -4
- package/dist/agentic/shared/ai/harness/result.schema.json +60 -0
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +10 -0
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +3231 -0
- package/dist/agentic/shared/ai/harness/validators.json +8 -4
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +5 -2
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +3 -0
- package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +879 -0
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +102 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +1 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +15 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +6 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +4 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +1 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +2 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +2 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +3 -1
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +1 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +2 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +2 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +3 -3
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +133 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +1 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +11 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +1 -1
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +2 -2
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +14 -1
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +2 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +19 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +1 -4
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +1 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +24 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +3 -1
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +1 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +13 -0
- package/dist/agentic/shared/ai/skills/tiers.json +2 -1
- package/dist/agentic/shared/ai/specs/2026-08-06-reference-module-activation.md +248 -0
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +39 -2
- package/dist/agentic/shared/scripts/design-source-contract.mjs +108 -0
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +935 -28
- package/dist/agentic/shared/scripts/framework-context.mjs +4 -1
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1 -1
- package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +1511 -0
- package/dist/lib/__integration__/TC-INT-008.spec.js +1 -0
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-setup.js +9 -1
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/module-extension-facts.js +93 -26
- package/dist/lib/generators/module-extension-facts.js.map +3 -3
- package/dist/lib/generators/module-facts-discovery.js +23 -0
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +969 -41
- package/dist/lib/generators/module-facts.js.map +2 -2
- package/dist/lib/generators/module-override-targets.js +35 -11
- package/dist/lib/generators/module-override-targets.js.map +2 -2
- package/dist/lib/generators/module-registry.js +105 -2
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/resolver.js +5 -0
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/testing/integration-discovery.js +34 -0
- package/dist/lib/testing/integration-discovery.js.map +2 -2
- package/dist/lib/testing/integration.js +0 -2
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +4 -4
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/lib/__integration__/TC-INT-008.spec.ts +4 -2
- package/src/lib/__tests__/resolve-environment.test.ts +18 -0
- package/src/lib/agentic-setup.ts +10 -2
- package/src/lib/generators/__tests__/helpers/override-target-diagnostic-fixtures.ts +150 -0
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +290 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +72 -7
- package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
- package/src/lib/generators/__tests__/module-facts.example-fact-coverage.test.ts +502 -0
- package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +414 -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 +42 -1
- package/src/lib/generators/__tests__/module-override-targets.diagnostic-wiring.test.ts +92 -0
- package/src/lib/generators/__tests__/module-override-targets.test.ts +24 -0
- package/src/lib/generators/__tests__/module-override-targets.unknown-mode.test.ts +117 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +60 -0
- package/src/lib/generators/module-extension-facts.ts +181 -25
- package/src/lib/generators/module-facts-discovery.ts +46 -0
- package/src/lib/generators/module-facts.ts +1330 -86
- package/src/lib/generators/module-override-targets.ts +70 -26
- package/src/lib/generators/module-registry.ts +135 -2
- package/src/lib/resolver.ts +6 -1
- package/src/lib/testing/__tests__/integration-discovery.test.ts +35 -0
- package/src/lib/testing/integration-discovery.ts +50 -0
- package/src/lib/testing/integration.ts +0 -2
- package/src/mercato.ts +3 -3
|
@@ -1,11 +1,28 @@
|
|
|
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'
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import {
|
|
6
|
+
COMPONENT_OVERRIDE_MODES,
|
|
7
|
+
CRUD_FORM_EXTENSION_SURFACE_KEYS,
|
|
8
|
+
CRUD_FORM_LIFECYCLE_PHASES,
|
|
9
|
+
CRUD_FORM_OPERATIONS,
|
|
10
|
+
DATA_TABLE_EXTENSION_SURFACE_KEYS,
|
|
11
|
+
EXTENSION_HOST_ACTIVATIONS,
|
|
12
|
+
EXTENSION_HOST_CAPABILITIES,
|
|
13
|
+
EXTENSION_HOST_FAMILIES,
|
|
14
|
+
MODULE_EXTENSION_ACTIVATION_KINDS,
|
|
15
|
+
MODULE_EXTENSION_CONTRIBUTION_KINDS,
|
|
16
|
+
MODULE_EXTENSION_HOST_RESOLUTIONS,
|
|
17
|
+
MODULE_EXTENSION_RESOLUTIONS,
|
|
18
|
+
MODULE_EXTENSION_TARGET_KINDS,
|
|
19
|
+
MODULE_EXTENSION_TARGET_RESOLUTIONS,
|
|
20
|
+
MODULE_EXTENSION_UNRESOLVED_REASONS,
|
|
21
|
+
MODULE_SPECIALIZED_REGISTRIES,
|
|
22
|
+
type ModuleExtensionContributionFact,
|
|
23
|
+
type ModuleExtensionHostFact,
|
|
24
|
+
type ModuleExtensionSurfaceFacts,
|
|
25
|
+
type ModuleExtensionTargetRef,
|
|
9
26
|
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
10
27
|
import { toSnake } from '../utils'
|
|
11
28
|
import { extractCommandIdsFromSource } from './module-registry'
|
|
@@ -23,11 +40,16 @@ import {
|
|
|
23
40
|
withModuleExtensionFactExtractionCache,
|
|
24
41
|
} from './module-extension-facts'
|
|
25
42
|
import {
|
|
43
|
+
ALL_OVERRIDE_DOMAINS,
|
|
44
|
+
MODULE_OVERRIDE_MODES,
|
|
45
|
+
MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES,
|
|
46
|
+
MODULE_OVERRIDE_TARGET_NOTES,
|
|
26
47
|
collectModuleOverrideTargets,
|
|
27
48
|
type ModuleOverrideTarget,
|
|
28
49
|
type ModuleOverrideTargetDiagnostic,
|
|
29
50
|
} from './module-override-targets'
|
|
30
51
|
import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
|
|
52
|
+
import { appendLocalReferenceModuleSource } from './module-facts-discovery'
|
|
31
53
|
|
|
32
54
|
export interface ModuleEntityFact {
|
|
33
55
|
id: string
|
|
@@ -117,45 +139,57 @@ export type ModuleFactRef = {
|
|
|
117
139
|
factKey: string
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Closed provenance/contract sets, published as `as const` ledgers whose types
|
|
144
|
+
* derive from them. The `factCoverage` ledger enumerates these at runtime, so a
|
|
145
|
+
* kind added here without a ledger row fails the coverage contract test rather
|
|
146
|
+
* than silently joining the public surface unclassified.
|
|
147
|
+
*/
|
|
148
|
+
export const MODULE_FACT_SOURCE_KINDS = [
|
|
149
|
+
'module-metadata',
|
|
150
|
+
'entity',
|
|
151
|
+
'event',
|
|
152
|
+
'acl-feature',
|
|
153
|
+
'api-route',
|
|
154
|
+
'di-registration',
|
|
155
|
+
'search',
|
|
156
|
+
'vector',
|
|
157
|
+
'notification',
|
|
158
|
+
'cli-command',
|
|
159
|
+
'backend-page',
|
|
160
|
+
'frontend-page',
|
|
161
|
+
'ai-tool',
|
|
162
|
+
'ai-agent',
|
|
163
|
+
'ai-extension',
|
|
164
|
+
'command',
|
|
165
|
+
'subscriber',
|
|
166
|
+
'worker',
|
|
167
|
+
'page-middleware',
|
|
168
|
+
'setup',
|
|
169
|
+
'encryption',
|
|
170
|
+
'custom-entity',
|
|
171
|
+
'integration',
|
|
172
|
+
'generator-plugin',
|
|
173
|
+
'extension-host',
|
|
174
|
+
'extension-contribution',
|
|
175
|
+
] as const
|
|
176
|
+
|
|
177
|
+
export type ModuleFactSourceKind = (typeof MODULE_FACT_SOURCE_KINDS)[number]
|
|
178
|
+
|
|
179
|
+
export const MODULE_OWNED_CONTRACT_KINDS = [
|
|
180
|
+
'module-metadata',
|
|
181
|
+
'command',
|
|
182
|
+
'worker',
|
|
183
|
+
'page-middleware',
|
|
184
|
+
'setup',
|
|
185
|
+
'encryption',
|
|
186
|
+
'di-registration',
|
|
187
|
+
'custom-entity',
|
|
188
|
+
'ai-extension',
|
|
189
|
+
'generator-plugin',
|
|
190
|
+
] as const
|
|
191
|
+
|
|
192
|
+
export type ModuleOwnedContractKind = (typeof MODULE_OWNED_CONTRACT_KINDS)[number]
|
|
159
193
|
|
|
160
194
|
export type ModuleFactSafeScalar = string | number | boolean | null
|
|
161
195
|
|
|
@@ -192,8 +226,12 @@ export type ModuleFactSourceEntry = {
|
|
|
192
226
|
factRef?: ModuleFactIndexRef
|
|
193
227
|
}
|
|
194
228
|
|
|
229
|
+
export const MODULE_FACT_DIAGNOSTIC_CODES = ['duplicate-source', 'unresolved-static-contract'] as const
|
|
230
|
+
|
|
231
|
+
export type ModuleFactDiagnosticCode = (typeof MODULE_FACT_DIAGNOSTIC_CODES)[number]
|
|
232
|
+
|
|
195
233
|
export type ModuleFactDiagnostic = {
|
|
196
|
-
code:
|
|
234
|
+
code: ModuleFactDiagnosticCode
|
|
197
235
|
kind: ModuleFactSourceKind
|
|
198
236
|
id: string
|
|
199
237
|
source?: ModuleFactSourceRef
|
|
@@ -208,6 +246,15 @@ export type ModuleDiRegistrationFact = {
|
|
|
208
246
|
source: ModuleFactSourceRef
|
|
209
247
|
}
|
|
210
248
|
|
|
249
|
+
/**
|
|
250
|
+
* How the extracted module reached the batch. `package` is the implicit default for
|
|
251
|
+
* every installed `@open-mercato/*` module and never appears in emitted output, so the
|
|
252
|
+
* package sidecar contract stays byte-identical. `local-reference` marks an app-local
|
|
253
|
+
* module projected into the explicit reference bundle; it is an independent discriminator
|
|
254
|
+
* from `sourcePackage`, which keeps its legacy "null means core package" meaning.
|
|
255
|
+
*/
|
|
256
|
+
export type ModuleFactProjectionSourceKind = 'package' | 'local-reference'
|
|
257
|
+
|
|
211
258
|
export interface ModuleFacts {
|
|
212
259
|
module: string
|
|
213
260
|
title: string | null
|
|
@@ -216,6 +263,8 @@ export interface ModuleFacts {
|
|
|
216
263
|
sourcePackage: string | null
|
|
217
264
|
sourceVersion: string | null
|
|
218
265
|
sourceRoot: string
|
|
266
|
+
/** Present only for `local-reference` projections; package modules omit it entirely. */
|
|
267
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
219
268
|
entities: ModuleEntityFact[]
|
|
220
269
|
events: ModuleEventFact[]
|
|
221
270
|
aclFeatures: string[]
|
|
@@ -255,6 +304,15 @@ export interface ExtractModuleFactsOptions {
|
|
|
255
304
|
sourceVersion?: string | null
|
|
256
305
|
registryPath?: string | null
|
|
257
306
|
registrySource?: string | null
|
|
307
|
+
/**
|
|
308
|
+
* App-root-relative POSIX root written into every emitted source path. Overrides the
|
|
309
|
+
* default `node_modules/<package>/src/modules/<moduleId>` layout so an app-local module
|
|
310
|
+
* emits `src/modules/<moduleId>` instead of a package path it does not live in.
|
|
311
|
+
*/
|
|
312
|
+
portableSourceRoot?: string
|
|
313
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
314
|
+
/** Generated-facts compatibility projection. Omitted means the corrected v2 contract. */
|
|
315
|
+
factsContractVersion?: 1 | 2
|
|
258
316
|
}
|
|
259
317
|
|
|
260
318
|
/** A discovered module and the source directory its facts are extracted from. */
|
|
@@ -263,6 +321,8 @@ export interface ModuleFactSource {
|
|
|
263
321
|
moduleRoot: string
|
|
264
322
|
from?: string
|
|
265
323
|
packageVersion?: string | null
|
|
324
|
+
portableSourceRoot?: string
|
|
325
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
266
326
|
}
|
|
267
327
|
|
|
268
328
|
function readSourceFile(filePath: string): ts.SourceFile | null {
|
|
@@ -343,46 +403,61 @@ function classHasUpdatedAtColumn(node: ts.ClassDeclaration): boolean {
|
|
|
343
403
|
return false
|
|
344
404
|
}
|
|
345
405
|
|
|
406
|
+
function readStaticAccessPath(candidate: ts.Expression): string[] | null {
|
|
407
|
+
const access = unwrapExpression(candidate)
|
|
408
|
+
if (ts.isIdentifier(access)) return [access.text]
|
|
409
|
+
if (ts.isPropertyAccessExpression(access)) {
|
|
410
|
+
const parent = readStaticAccessPath(access.expression)
|
|
411
|
+
return parent ? [...parent, access.name.text] : null
|
|
412
|
+
}
|
|
413
|
+
if (ts.isElementAccessExpression(access) && access.argumentExpression) {
|
|
414
|
+
const parent = readStaticAccessPath(access.expression)
|
|
415
|
+
const key = unwrapExpression(access.argumentExpression)
|
|
416
|
+
return parent && ts.isStringLiteralLike(key) ? [...parent, key.text] : null
|
|
417
|
+
}
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Resolves an entity-id expression to its literal `module:entity` value.
|
|
423
|
+
*
|
|
424
|
+
* Modules reference entity ids three ways and all three are statically knowable:
|
|
425
|
+
* a string literal, `E.<module>.<entity>` from the generated entity-id registry
|
|
426
|
+
* (`entity-ids.ts` emits `E[module][entity] = '<module>:<entity>'`), and — when
|
|
427
|
+
* `initializers` is supplied — a same-file `const ENTITY_ID = 'module:entity'`.
|
|
428
|
+
* Anything else stays undefined: a guessed id is worse than a missing one.
|
|
429
|
+
*/
|
|
430
|
+
function resolveEntityIdReference(
|
|
431
|
+
expression: ts.Expression,
|
|
432
|
+
initializers?: Map<string, ts.Expression>,
|
|
433
|
+
visitedIdentifiers: Set<string> = new Set(),
|
|
434
|
+
): string | undefined {
|
|
435
|
+
const current = unwrapExpression(expression)
|
|
436
|
+
if (ts.isStringLiteralLike(current)) return current.text
|
|
437
|
+
const accessPath = readStaticAccessPath(current)
|
|
438
|
+
if (accessPath?.length === 3 && accessPath[0] === 'E') {
|
|
439
|
+
return `${accessPath[1]}:${accessPath[2]}`
|
|
440
|
+
}
|
|
441
|
+
if (initializers && ts.isIdentifier(current) && !visitedIdentifiers.has(current.text)) {
|
|
442
|
+
visitedIdentifiers.add(current.text)
|
|
443
|
+
const declaration = initializers.get(current.text)
|
|
444
|
+
if (declaration) return resolveEntityIdReference(declaration, initializers, visitedIdentifiers)
|
|
445
|
+
}
|
|
446
|
+
return undefined
|
|
447
|
+
}
|
|
448
|
+
|
|
346
449
|
function collectCustomFieldEntityIds(ceFilePath: string | null): Set<string> {
|
|
347
450
|
const result = new Set<string>()
|
|
348
451
|
if (!ceFilePath) return result
|
|
349
452
|
const sourceFile = readSourceFile(ceFilePath)
|
|
350
453
|
if (!sourceFile) return result
|
|
351
454
|
|
|
352
|
-
const readEntityId = (expression: ts.Expression): string | undefined => {
|
|
353
|
-
const current = unwrapExpression(expression)
|
|
354
|
-
if (ts.isStringLiteralLike(current)) {
|
|
355
|
-
return current.text.includes(':') ? current.text : undefined
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const readAccessPath = (candidate: ts.Expression): string[] | null => {
|
|
359
|
-
const access = unwrapExpression(candidate)
|
|
360
|
-
if (ts.isIdentifier(access)) return [access.text]
|
|
361
|
-
if (ts.isPropertyAccessExpression(access)) {
|
|
362
|
-
const parent = readAccessPath(access.expression)
|
|
363
|
-
return parent ? [...parent, access.name.text] : null
|
|
364
|
-
}
|
|
365
|
-
if (ts.isElementAccessExpression(access) && access.argumentExpression) {
|
|
366
|
-
const parent = readAccessPath(access.expression)
|
|
367
|
-
const key = unwrapExpression(access.argumentExpression)
|
|
368
|
-
return parent && ts.isStringLiteralLike(key) ? [...parent, key.text] : null
|
|
369
|
-
}
|
|
370
|
-
return null
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const accessPath = readAccessPath(current)
|
|
374
|
-
if (accessPath?.length === 3 && accessPath[0] === 'E') {
|
|
375
|
-
return `${accessPath[1]}:${accessPath[2]}`
|
|
376
|
-
}
|
|
377
|
-
return undefined
|
|
378
|
-
}
|
|
379
|
-
|
|
380
455
|
const visit = (node: ts.Node): void => {
|
|
381
456
|
if (ts.isObjectLiteralExpression(node)) {
|
|
382
457
|
const initializer = getObjectPropertyInitializer(node, 'id')
|
|
383
458
|
if (initializer) {
|
|
384
|
-
const id =
|
|
385
|
-
if (id) result.add(id)
|
|
459
|
+
const id = resolveEntityIdReference(initializer)
|
|
460
|
+
if (id && id.includes(':')) result.add(id)
|
|
386
461
|
}
|
|
387
462
|
}
|
|
388
463
|
node.forEachChild(visit)
|
|
@@ -443,9 +518,38 @@ function getPropertyName(property: ts.ObjectLiteralElementLike): string | undefi
|
|
|
443
518
|
if (!name) return undefined
|
|
444
519
|
if (ts.isIdentifier(name)) return name.text
|
|
445
520
|
if (ts.isStringLiteralLike(name)) return name.text
|
|
521
|
+
// A computed key whose expression is a string literal or a same-file `const NAME = 'token'`
|
|
522
|
+
// is still a statically known token. Without this, `{ [SERVICE_TOKEN]: asFunction(...) }` —
|
|
523
|
+
// the idiomatic, type-safe way to register a DI token from an exported const — yielded
|
|
524
|
+
// undefined and the registration vanished from module facts with NO diagnostic, because the
|
|
525
|
+
// unresolved-token path is only reached for a NAMED token whose Awilix kind is unrecognised.
|
|
526
|
+
if (ts.isComputedPropertyName(name)) return resolveComputedPropertyKey(name.expression)
|
|
446
527
|
return undefined
|
|
447
528
|
}
|
|
448
529
|
|
|
530
|
+
/** Resolves a computed key to its literal token when the value is statically knowable. */
|
|
531
|
+
function resolveComputedPropertyKey(expression: ts.Expression): string | undefined {
|
|
532
|
+
if (ts.isStringLiteralLike(expression)) return expression.text
|
|
533
|
+
// `X as const` / `X satisfies Y` wrappers.
|
|
534
|
+
if (ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) {
|
|
535
|
+
return resolveComputedPropertyKey(expression.expression)
|
|
536
|
+
}
|
|
537
|
+
if (ts.isParenthesizedExpression(expression)) return resolveComputedPropertyKey(expression.expression)
|
|
538
|
+
if (!ts.isIdentifier(expression)) return undefined
|
|
539
|
+
const sourceFile = expression.getSourceFile()
|
|
540
|
+
if (!sourceFile) return undefined
|
|
541
|
+
let resolved: string | undefined
|
|
542
|
+
const visit = (node: ts.Node): void => {
|
|
543
|
+
if (resolved) return
|
|
544
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === expression.text) {
|
|
545
|
+
if (node.initializer) resolved = resolveComputedPropertyKey(node.initializer)
|
|
546
|
+
}
|
|
547
|
+
node.forEachChild(visit)
|
|
548
|
+
}
|
|
549
|
+
sourceFile.forEachChild(visit)
|
|
550
|
+
return resolved
|
|
551
|
+
}
|
|
552
|
+
|
|
449
553
|
function getObjectPropertyInitializer(
|
|
450
554
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
451
555
|
propertyName: string,
|
|
@@ -807,15 +911,29 @@ function extractSearchEntities(searchFilePath: string | null, warnings: string[]
|
|
|
807
911
|
return []
|
|
808
912
|
}
|
|
809
913
|
|
|
914
|
+
// `entityId` is rarely a bare string literal: modules reference the entity through
|
|
915
|
+
// `E.<module>.<entity>` or a same-file `const ENTITY_ID`. Reading only string literals
|
|
916
|
+
// dropped every such entry with NO diagnostic, so a module shipping `search.ts` scored
|
|
917
|
+
// zero search facts while looking healthy. Unresolvable entries now warn instead.
|
|
918
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
810
919
|
const entityIds: string[] = []
|
|
811
920
|
const seen = new Set<string>()
|
|
812
921
|
for (const element of entitiesArray.elements) {
|
|
813
922
|
if (!ts.isObjectLiteralExpression(element)) continue
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
923
|
+
const initializer = getObjectPropertyInitializer(element, 'entityId')
|
|
924
|
+
const location = `${searchFilePath}:${nodeLine(sourceFile, element)}`
|
|
925
|
+
if (!initializer) {
|
|
926
|
+
warnings.push(`[module-facts] searchConfig entity declares no entityId: ${location}`)
|
|
927
|
+
continue
|
|
928
|
+
}
|
|
929
|
+
const entityId = resolveEntityIdReference(initializer, initializers)
|
|
930
|
+
if (!entityId) {
|
|
931
|
+
warnings.push(`[module-facts] searchConfig entityId is not statically resolvable: ${location}`)
|
|
932
|
+
continue
|
|
818
933
|
}
|
|
934
|
+
if (seen.has(entityId)) continue
|
|
935
|
+
seen.add(entityId)
|
|
936
|
+
entityIds.push(entityId)
|
|
819
937
|
}
|
|
820
938
|
return entityIds
|
|
821
939
|
}
|
|
@@ -911,6 +1029,26 @@ function toPortableSourceRoot(moduleId: string, sourcePackage: string | null): s
|
|
|
911
1029
|
return path.posix.join('node_modules', sourcePackage ?? '@open-mercato/core', 'src', 'modules', moduleId)
|
|
912
1030
|
}
|
|
913
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* An explicit portable root must stay app-root-relative and POSIX-shaped: every emitted
|
|
1034
|
+
* fact path and markdown link is resolved against the generated app root, so an absolute
|
|
1035
|
+
* path, a Windows separator, or a traversal segment would emit an unclickable or
|
|
1036
|
+
* machine-specific reference into a distributed artifact.
|
|
1037
|
+
*/
|
|
1038
|
+
export function assertPortableSourceRoot(moduleId: string, portableSourceRoot: string): string {
|
|
1039
|
+
const invalid = portableSourceRoot.length === 0
|
|
1040
|
+
|| path.posix.isAbsolute(portableSourceRoot)
|
|
1041
|
+
|| /^[A-Za-z]:/.test(portableSourceRoot)
|
|
1042
|
+
|| portableSourceRoot.includes('\\')
|
|
1043
|
+
|| portableSourceRoot.split('/').includes('..')
|
|
1044
|
+
if (invalid) {
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
`[module-facts] portableSourceRoot for module "${moduleId}" must be a relative POSIX app path, received "${portableSourceRoot}"`,
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
return portableSourceRoot
|
|
1050
|
+
}
|
|
1051
|
+
|
|
914
1052
|
function toPortableSourcePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
|
|
915
1053
|
const relativePath = path.relative(moduleRoot, filePath).split(path.sep).join('/')
|
|
916
1054
|
return path.posix.join(sourceRoot, relativePath)
|
|
@@ -1451,6 +1589,42 @@ function readResolvedStringProperty(
|
|
|
1451
1589
|
return undefined
|
|
1452
1590
|
}
|
|
1453
1591
|
|
|
1592
|
+
function readImportedStringIdentifier(
|
|
1593
|
+
sourceFile: ts.SourceFile,
|
|
1594
|
+
sourcePath: string,
|
|
1595
|
+
identifier: string,
|
|
1596
|
+
): string | undefined {
|
|
1597
|
+
for (const statement of sourceFile.statements) {
|
|
1598
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue
|
|
1599
|
+
const importClause = statement.importClause
|
|
1600
|
+
const bindings = importClause?.namedBindings
|
|
1601
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue
|
|
1602
|
+
const imported = bindings.elements.find((element) => element.name.text === identifier)
|
|
1603
|
+
if (!imported) continue
|
|
1604
|
+
const request = statement.moduleSpecifier.text
|
|
1605
|
+
if (!request.startsWith('.')) return undefined
|
|
1606
|
+
const basePath = path.resolve(path.dirname(sourcePath), request)
|
|
1607
|
+
const candidates = [
|
|
1608
|
+
`${basePath}.ts`,
|
|
1609
|
+
`${basePath}.tsx`,
|
|
1610
|
+
path.join(basePath, 'index.ts'),
|
|
1611
|
+
path.join(basePath, 'index.tsx'),
|
|
1612
|
+
]
|
|
1613
|
+
const importedPath = candidates.find((candidate) => fs.existsSync(candidate))
|
|
1614
|
+
if (!importedPath) return undefined
|
|
1615
|
+
const importedSource = readSourceFile(importedPath)
|
|
1616
|
+
if (!importedSource) return undefined
|
|
1617
|
+
const exportName = imported.propertyName?.text ?? imported.name.text
|
|
1618
|
+
const declaration = importedSource.statements
|
|
1619
|
+
.filter(ts.isVariableStatement)
|
|
1620
|
+
.flatMap((variable) => [...variable.declarationList.declarations])
|
|
1621
|
+
.find((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === exportName)
|
|
1622
|
+
const initializer = declaration?.initializer ? unwrapExpression(declaration.initializer) : null
|
|
1623
|
+
return initializer && ts.isStringLiteralLike(initializer) ? initializer.text : undefined
|
|
1624
|
+
}
|
|
1625
|
+
return undefined
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1454
1628
|
function extractWorkerContracts(
|
|
1455
1629
|
moduleId: string,
|
|
1456
1630
|
moduleRoot: string,
|
|
@@ -1474,7 +1648,11 @@ function extractWorkerContracts(
|
|
|
1474
1648
|
// Runtime skips any worker without a queue, so a worker missing the required
|
|
1475
1649
|
// property is not a contract; a present-but-dynamic queue is reported instead.
|
|
1476
1650
|
const hasQueueProperty = getObjectPropertyInitializer(metadata, 'queue') !== undefined
|
|
1651
|
+
const queueInitializer = getObjectPropertyInitializer(metadata, 'queue')
|
|
1477
1652
|
const queue = readResolvedStringProperty(metadata, 'queue', initializers)
|
|
1653
|
+
?? (queueInitializer && ts.isIdentifier(unwrapExpression(queueInitializer))
|
|
1654
|
+
? readImportedStringIdentifier(sourceFile, filePath, (unwrapExpression(queueInitializer) as ts.Identifier).text)
|
|
1655
|
+
: undefined)
|
|
1478
1656
|
if (!hasQueueProperty) {
|
|
1479
1657
|
unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1480
1658
|
continue
|
|
@@ -1897,7 +2075,9 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
1897
2075
|
if (!moduleRoot) {
|
|
1898
2076
|
throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
|
|
1899
2077
|
}
|
|
1900
|
-
const sourceRoot =
|
|
2078
|
+
const sourceRoot = options.portableSourceRoot
|
|
2079
|
+
? assertPortableSourceRoot(moduleId, options.portableSourceRoot)
|
|
2080
|
+
: toPortableSourceRoot(moduleId, sourcePackage)
|
|
1901
2081
|
|
|
1902
2082
|
const entitiesFilePath =
|
|
1903
2083
|
resolveConventionFile(path.join(moduleRoot, 'data'), 'entities') ??
|
|
@@ -1960,6 +2140,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
1960
2140
|
notifications,
|
|
1961
2141
|
aiTools,
|
|
1962
2142
|
aiAgents,
|
|
2143
|
+
factsContractVersion: options.factsContractVersion ?? 2,
|
|
1963
2144
|
})
|
|
1964
2145
|
|
|
1965
2146
|
const indexSourcePath = portableOf(indexFilePath)
|
|
@@ -2166,6 +2347,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
2166
2347
|
sourcePackage,
|
|
2167
2348
|
sourceVersion,
|
|
2168
2349
|
sourceRoot,
|
|
2350
|
+
...(options.sourceKind === 'local-reference' ? { sourceKind: options.sourceKind } : {}),
|
|
2169
2351
|
entities,
|
|
2170
2352
|
events,
|
|
2171
2353
|
aclFeatures,
|
|
@@ -2212,6 +2394,8 @@ export interface ModuleFactsJsonEntry {
|
|
|
2212
2394
|
sourcePackage: string | null
|
|
2213
2395
|
sourceVersion: string | null
|
|
2214
2396
|
sourceRoot: string
|
|
2397
|
+
/** Present only for `local-reference` projections; package sidecar entries omit it. */
|
|
2398
|
+
sourceKind?: ModuleFactProjectionSourceKind
|
|
2215
2399
|
entities: ModuleEntityFact[]
|
|
2216
2400
|
events: ModuleFactsJsonEvent[]
|
|
2217
2401
|
aclFeatures: string[]
|
|
@@ -2289,13 +2473,26 @@ function renderSourceLinkedListSection(
|
|
|
2289
2473
|
return [heading, '', `| ${idColumnLabel} | Source |`, '|---|---|', ...rows].join('\n')
|
|
2290
2474
|
}
|
|
2291
2475
|
|
|
2476
|
+
/**
|
|
2477
|
+
* A generated fact only earns a markdown link when it names an exact module code
|
|
2478
|
+
* file. Directory-valued provenance (the module source root, framework hosts that
|
|
2479
|
+
* only know their owning package folder) renders as plain text: a link to a folder
|
|
2480
|
+
* resolves to nothing in an editor or on the repository browser, so emitting one
|
|
2481
|
+
* costs a consumer a dead click instead of taking them to the declaration site.
|
|
2482
|
+
*/
|
|
2483
|
+
export function isExactSourceFilePath(sourcePath: string): boolean {
|
|
2484
|
+
return MODULE_CODE_EXTENSIONS.some((extension) => sourcePath.endsWith(extension))
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2292
2487
|
function renderSourceLink(sourcePath: string): string {
|
|
2488
|
+
if (!isExactSourceFilePath(sourcePath)) return sourcePath
|
|
2293
2489
|
return `[${sourcePath}](../../../${sourcePath})`
|
|
2294
2490
|
}
|
|
2295
2491
|
|
|
2296
2492
|
function renderSourceRefLink(source: ModuleFactSourceRef): string {
|
|
2297
2493
|
const anchor = source.line ? `#L${source.line}` : ''
|
|
2298
2494
|
const label = source.line ? `${source.sourcePath}:${source.line}` : source.sourcePath
|
|
2495
|
+
if (!isExactSourceFilePath(source.sourcePath)) return label
|
|
2299
2496
|
return `[${label}](../../../${source.sourcePath}${anchor})`
|
|
2300
2497
|
}
|
|
2301
2498
|
|
|
@@ -2542,13 +2739,44 @@ function renderOverrideTargetsSection(targets: readonly ModuleOverrideTarget[] |
|
|
|
2542
2739
|
return ['## Exact override targets', '', header, divider, ...rows].join('\n')
|
|
2543
2740
|
}
|
|
2544
2741
|
|
|
2545
|
-
export
|
|
2742
|
+
export interface ReferenceProjectionFingerprints {
|
|
2743
|
+
sourceFingerprint: string
|
|
2744
|
+
taxonomyFingerprint: string
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
/**
|
|
2748
|
+
* Header for a `local-reference` projection. It never claims a package version — the
|
|
2749
|
+
* module is app-local — and it states in the document itself that the activations and
|
|
2750
|
+
* exact override targets below describe behavior only after the module is opted in,
|
|
2751
|
+
* because the artifact is generated while the module is disabled.
|
|
2752
|
+
*/
|
|
2753
|
+
function renderReferenceProjectionHeader(
|
|
2754
|
+
facts: ModuleFacts,
|
|
2755
|
+
fingerprints: ReferenceProjectionFingerprints,
|
|
2756
|
+
): string[] {
|
|
2757
|
+
return [
|
|
2758
|
+
`# ${facts.module} — module facts (generated reference projection, do not edit)`,
|
|
2759
|
+
`<!-- generated from local reference ${facts.sourceRoot}; source-fingerprint ${fingerprints.sourceFingerprint}; taxonomy-fingerprint ${fingerprints.taxonomyFingerprint} — R1 staleness stamp -->`,
|
|
2760
|
+
`> 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\`.`,
|
|
2761
|
+
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
2762
|
+
]
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
export function renderModuleFactsMarkdown(
|
|
2766
|
+
facts: ModuleFacts,
|
|
2767
|
+
referenceProjection?: ReferenceProjectionFingerprints,
|
|
2768
|
+
): string {
|
|
2546
2769
|
const extensionSurfaces = facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] }
|
|
2547
2770
|
const lookup = buildFactSourceLookup(facts)
|
|
2771
|
+
const header = facts.sourceKind === 'local-reference' && referenceProjection
|
|
2772
|
+
? renderReferenceProjectionHeader(facts, referenceProjection)
|
|
2773
|
+
: [
|
|
2774
|
+
`# ${facts.module} — module facts (generated, do not edit)`,
|
|
2775
|
+
renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
|
|
2776
|
+
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
2777
|
+
]
|
|
2548
2778
|
const sections = [
|
|
2549
|
-
|
|
2550
|
-
renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
|
|
2551
|
-
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
2779
|
+
...header,
|
|
2552
2780
|
'',
|
|
2553
2781
|
renderEntitiesSection(facts.entities, lookup),
|
|
2554
2782
|
'',
|
|
@@ -2624,6 +2852,7 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
2624
2852
|
sourcePackage: facts.sourcePackage,
|
|
2625
2853
|
sourceVersion: facts.sourceVersion,
|
|
2626
2854
|
sourceRoot: facts.sourceRoot,
|
|
2855
|
+
...(facts.sourceKind === 'local-reference' ? { sourceKind: facts.sourceKind } : {}),
|
|
2627
2856
|
entities: facts.entities,
|
|
2628
2857
|
events: facts.events.map((event) => ({
|
|
2629
2858
|
id: event.id,
|
|
@@ -2675,6 +2904,274 @@ export function renderModuleFactsJson(factsByModule: Record<string, ModuleFacts>
|
|
|
2675
2904
|
return `${JSON.stringify(buildModuleFactsJsonObject(factsByModule), null, 2)}\n`
|
|
2676
2905
|
}
|
|
2677
2906
|
|
|
2907
|
+
/**
|
|
2908
|
+
* The normal package sidecar (`module-facts.json`) and the enabled-filtered package
|
|
2909
|
+
* markdown subset carry package-provided modules only. An app-local module reaching
|
|
2910
|
+
* either output would publish `bound` activations and exact override targets for code
|
|
2911
|
+
* the runtime does not select, so it fails the build instead of shipping.
|
|
2912
|
+
*/
|
|
2913
|
+
export function assertPackageModuleFactsOnly(factsByModule: Record<string, ModuleFacts>): void {
|
|
2914
|
+
const offenders = Object.values(factsByModule)
|
|
2915
|
+
.filter((facts) => facts.sourceKind === 'local-reference' || !facts.sourceRoot.startsWith('node_modules/'))
|
|
2916
|
+
.map((facts) => `${facts.module} (${facts.sourceRoot})`)
|
|
2917
|
+
.sort((left, right) => left.localeCompare(right))
|
|
2918
|
+
if (offenders.length > 0) {
|
|
2919
|
+
throw new Error(
|
|
2920
|
+
`[module-facts] normal package output must not contain app-local modules: ${offenders.join(', ')}`,
|
|
2921
|
+
)
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
function sha256Hex(value: string | Buffer): string {
|
|
2926
|
+
return createHash('sha256').update(value).digest('hex')
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
/**
|
|
2930
|
+
* Canonical form used by both reference fingerprints: object keys are recursively sorted
|
|
2931
|
+
* and set-valued arrays are sorted by their own canonical text. Structured `path` arrays
|
|
2932
|
+
* are ordered tuples whose segment order is part of the public target identity, so they
|
|
2933
|
+
* retain their declared order.
|
|
2934
|
+
*/
|
|
2935
|
+
function canonicalizeFingerprintValue(value: unknown, propertyName?: string): unknown {
|
|
2936
|
+
if (Array.isArray(value)) {
|
|
2937
|
+
const items = value
|
|
2938
|
+
.map((item) => {
|
|
2939
|
+
const canonical = canonicalizeFingerprintValue(item)
|
|
2940
|
+
return { canonical, text: JSON.stringify(canonical) ?? '' }
|
|
2941
|
+
})
|
|
2942
|
+
if (propertyName === 'path') return items.map((item) => item.canonical)
|
|
2943
|
+
return items
|
|
2944
|
+
.sort((left, right) => (left.text < right.text ? -1 : left.text > right.text ? 1 : 0))
|
|
2945
|
+
.map((item) => item.canonical)
|
|
2946
|
+
}
|
|
2947
|
+
if (value !== null && typeof value === 'object') {
|
|
2948
|
+
const record = value as Record<string, unknown>
|
|
2949
|
+
const result: Record<string, unknown> = {}
|
|
2950
|
+
for (const key of Object.keys(record).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))) {
|
|
2951
|
+
if (record[key] === undefined) continue
|
|
2952
|
+
result[key] = canonicalizeFingerprintValue(record[key], key)
|
|
2953
|
+
}
|
|
2954
|
+
return result
|
|
2955
|
+
}
|
|
2956
|
+
return value
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function canonicalFingerprintJson(value: unknown): string {
|
|
2960
|
+
return JSON.stringify(canonicalizeFingerprintValue(value))
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
const REFERENCE_FINGERPRINT_SKIPPED_DIRECTORIES = new Set(['__tests__', '__integration__', 'node_modules'])
|
|
2964
|
+
|
|
2965
|
+
/**
|
|
2966
|
+
* SHA-256 over the canonical JSON of `{ path, sha256 }` records sorted by path, where
|
|
2967
|
+
* `path` is the portable app-relative path so the value is identical across the
|
|
2968
|
+
* authoring, template, and emitted roots. Test-only directories are excluded because the
|
|
2969
|
+
* scaffold does not emit them, so including them would make the emitted app's recomputed
|
|
2970
|
+
* fingerprint disagree with the one built from the template.
|
|
2971
|
+
*/
|
|
2972
|
+
export function computeModuleSourceFingerprint(moduleRoot: string, portableSourceRoot: string): string {
|
|
2973
|
+
assertPortableSourceRoot(path.basename(portableSourceRoot), portableSourceRoot)
|
|
2974
|
+
const records: { path: string; sha256: string }[] = []
|
|
2975
|
+
const walk = (directory: string): void => {
|
|
2976
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
2977
|
+
for (const entry of entries) {
|
|
2978
|
+
const fullPath = path.join(directory, entry.name)
|
|
2979
|
+
if (entry.isDirectory()) {
|
|
2980
|
+
if (REFERENCE_FINGERPRINT_SKIPPED_DIRECTORIES.has(entry.name)) continue
|
|
2981
|
+
walk(fullPath)
|
|
2982
|
+
continue
|
|
2983
|
+
}
|
|
2984
|
+
if (!entry.isFile()) continue
|
|
2985
|
+
records.push({
|
|
2986
|
+
path: toPortableSourcePath(moduleRoot, portableSourceRoot, fullPath),
|
|
2987
|
+
sha256: sha256Hex(fs.readFileSync(fullPath)),
|
|
2988
|
+
})
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
walk(moduleRoot)
|
|
2992
|
+
records.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
2993
|
+
return sha256Hex(canonicalFingerprintJson(records))
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
/**
|
|
2997
|
+
* Provenance and release metadata are not taxonomy: a version bump must not invalidate
|
|
2998
|
+
* the classification fingerprint. Every remaining section of the emitted entry is a
|
|
2999
|
+
* classified set and contributes.
|
|
3000
|
+
*/
|
|
3001
|
+
const TAXONOMY_EXCLUDED_SECTIONS = new Set([
|
|
3002
|
+
'title',
|
|
3003
|
+
'description',
|
|
3004
|
+
'coreVersion',
|
|
3005
|
+
'sourcePackage',
|
|
3006
|
+
'sourceVersion',
|
|
3007
|
+
'sourceRoot',
|
|
3008
|
+
'sourceKind',
|
|
3009
|
+
])
|
|
3010
|
+
|
|
3011
|
+
function collectTaxonomyRecords(
|
|
3012
|
+
setName: string,
|
|
3013
|
+
value: unknown,
|
|
3014
|
+
records: { setName: string; value: unknown }[],
|
|
3015
|
+
): void {
|
|
3016
|
+
if (value === undefined) return
|
|
3017
|
+
if (Array.isArray(value)) {
|
|
3018
|
+
for (const item of value) records.push({ setName, value: canonicalizeFingerprintValue(item) })
|
|
3019
|
+
return
|
|
3020
|
+
}
|
|
3021
|
+
if (value !== null && typeof value === 'object') {
|
|
3022
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
3023
|
+
collectTaxonomyRecords(`${setName}.${key}`, child, records)
|
|
3024
|
+
}
|
|
3025
|
+
return
|
|
3026
|
+
}
|
|
3027
|
+
records.push({ setName, value })
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
/**
|
|
3031
|
+
* SHA-256 over the canonical JSON of `{ setName, value }` records sorted by set name and
|
|
3032
|
+
* canonicalized value. Each record carries a complete literal or structured entry — key,
|
|
3033
|
+
* suffix, capabilities, `bound`, phases, operations included — so no delimiter-joined
|
|
3034
|
+
* tuple can collapse two distinct classifications into one fingerprint input.
|
|
3035
|
+
*/
|
|
3036
|
+
export function computeModuleTaxonomyFingerprint(entry: ModuleFactsJsonEntry): string {
|
|
3037
|
+
const records: { setName: string; value: unknown }[] = []
|
|
3038
|
+
for (const [setName, value] of Object.entries(entry as unknown as Record<string, unknown>)) {
|
|
3039
|
+
if (TAXONOMY_EXCLUDED_SECTIONS.has(setName)) continue
|
|
3040
|
+
collectTaxonomyRecords(setName, value, records)
|
|
3041
|
+
}
|
|
3042
|
+
const decorated = records
|
|
3043
|
+
.map((record) => ({ record, text: JSON.stringify(record.value) ?? '' }))
|
|
3044
|
+
.sort((left, right) =>
|
|
3045
|
+
left.record.setName < right.record.setName
|
|
3046
|
+
? -1
|
|
3047
|
+
: left.record.setName > right.record.setName
|
|
3048
|
+
? 1
|
|
3049
|
+
: left.text < right.text
|
|
3050
|
+
? -1
|
|
3051
|
+
: left.text > right.text
|
|
3052
|
+
? 1
|
|
3053
|
+
: 0,
|
|
3054
|
+
)
|
|
3055
|
+
return sha256Hex(JSON.stringify(decorated.map((item) => item.record)))
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
export interface ReferenceModuleFactsEntry {
|
|
3059
|
+
moduleId: string
|
|
3060
|
+
projectionKind: 'activated-reference'
|
|
3061
|
+
sourceKind: 'local-reference'
|
|
3062
|
+
runtimeSelected: false
|
|
3063
|
+
sourceFingerprint: string
|
|
3064
|
+
taxonomyFingerprint: string
|
|
3065
|
+
/**
|
|
3066
|
+
* The enum-derived classification of every taxonomy `facts` is expressed in, built
|
|
3067
|
+
* by the same generation run. It travels with the projection so a reader of the
|
|
3068
|
+
* emitted sheet can tell a value the example really emits from one that is only
|
|
3069
|
+
* reserved, pending, or reachable through a negative fixture.
|
|
3070
|
+
*/
|
|
3071
|
+
factCoverage: ModuleFactCoverageFamily[]
|
|
3072
|
+
facts: ModuleFactsJsonEntry
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
export type ReferenceModuleFactsJson = Record<string, ReferenceModuleFactsEntry>
|
|
3076
|
+
|
|
3077
|
+
export function renderReferenceModuleFactsJson(bundle: ReferenceModuleFactsJson): string {
|
|
3078
|
+
const ordered: ReferenceModuleFactsJson = {}
|
|
3079
|
+
for (const moduleId of Object.keys(bundle).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))) {
|
|
3080
|
+
ordered[moduleId] = bundle[moduleId]
|
|
3081
|
+
}
|
|
3082
|
+
return `${JSON.stringify(ordered, null, 2)}\n`
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
export interface ExtractLocalReferenceModuleFactsOptions {
|
|
3086
|
+
/** Package-provided sources, exactly as the normal batch receives them. */
|
|
3087
|
+
packageSources: readonly ModuleFactSource[]
|
|
3088
|
+
/** The app-local module projected into the reference bundle. */
|
|
3089
|
+
reference: ModuleFactSource
|
|
3090
|
+
registryPath?: string | null
|
|
3091
|
+
registrySource?: string | null
|
|
3092
|
+
coreVersion?: string | null
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
export interface LocalReferenceModuleFactsResult {
|
|
3096
|
+
entry: ReferenceModuleFactsEntry
|
|
3097
|
+
markdown: string
|
|
3098
|
+
warnings: string[]
|
|
3099
|
+
/**
|
|
3100
|
+
* First-party targets the activated projection could not correlate. They also remain in
|
|
3101
|
+
* the emitted `facts.extensionSurfaces.unresolved`, so the artifact states its own gaps
|
|
3102
|
+
* rather than presenting a partial surface as complete.
|
|
3103
|
+
*/
|
|
3104
|
+
unresolvedTargets: string[]
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Produces the reference projection of an app-local module from an explicitly activated,
|
|
3109
|
+
* disposable selection context: the local source is appended to the package sources and
|
|
3110
|
+
* the whole set runs through the same `extractAllModuleFacts` selection and correlation
|
|
3111
|
+
* batch, so incoming contributions and optional targets resolve exactly as they would
|
|
3112
|
+
* after opt-in. Merging an independently extracted object into a finished batch is
|
|
3113
|
+
* forbidden — it would silently drop both resolutions. The batch is disposable: the
|
|
3114
|
+
* caller keeps only this module's projection and never writes the rest, which is why the
|
|
3115
|
+
* normal package outputs stay byte-identical.
|
|
3116
|
+
*/
|
|
3117
|
+
export function extractLocalReferenceModuleFacts(
|
|
3118
|
+
options: ExtractLocalReferenceModuleFactsOptions,
|
|
3119
|
+
): LocalReferenceModuleFactsResult {
|
|
3120
|
+
const { reference } = options
|
|
3121
|
+
if (reference.sourceKind !== 'local-reference') {
|
|
3122
|
+
throw new Error(
|
|
3123
|
+
`[module-facts] reference source "${reference.moduleId}" must declare sourceKind "local-reference"`,
|
|
3124
|
+
)
|
|
3125
|
+
}
|
|
3126
|
+
const portableSourceRoot = reference.portableSourceRoot
|
|
3127
|
+
if (!portableSourceRoot) {
|
|
3128
|
+
throw new Error(`[module-facts] reference source "${reference.moduleId}" must declare portableSourceRoot`)
|
|
3129
|
+
}
|
|
3130
|
+
assertPortableSourceRoot(reference.moduleId, portableSourceRoot)
|
|
3131
|
+
|
|
3132
|
+
const sources = appendLocalReferenceModuleSource(options.packageSources, reference)
|
|
3133
|
+
const activated = extractAllModuleFacts({
|
|
3134
|
+
sources,
|
|
3135
|
+
registryPath: options.registryPath ?? null,
|
|
3136
|
+
registrySource: options.registrySource ?? null,
|
|
3137
|
+
coreVersion: options.coreVersion ?? null,
|
|
3138
|
+
// The disposable projection collects rather than throws: an app-local module can name
|
|
3139
|
+
// a target whose classification is still missing from the shared extension reader, and
|
|
3140
|
+
// aborting there would delete the whole reference bundle instead of publishing the
|
|
3141
|
+
// exact gap. Normal package output keeps the strict default.
|
|
3142
|
+
unresolvedFirstPartyTargets: 'collect',
|
|
3143
|
+
})
|
|
3144
|
+
const facts = activated.factsByModule[reference.moduleId]
|
|
3145
|
+
if (!facts) {
|
|
3146
|
+
throw new Error(`[module-facts] reference module "${reference.moduleId}" produced no facts`)
|
|
3147
|
+
}
|
|
3148
|
+
if (facts.sourceRoot !== portableSourceRoot) {
|
|
3149
|
+
throw new Error(
|
|
3150
|
+
`[module-facts] reference module "${reference.moduleId}" emitted non-portable source root "${facts.sourceRoot}"`,
|
|
3151
|
+
)
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
const jsonEntry = toModuleFactsJsonEntry(facts)
|
|
3155
|
+
const fingerprints: ReferenceProjectionFingerprints = {
|
|
3156
|
+
sourceFingerprint: computeModuleSourceFingerprint(reference.moduleRoot, portableSourceRoot),
|
|
3157
|
+
taxonomyFingerprint: computeModuleTaxonomyFingerprint(jsonEntry),
|
|
3158
|
+
}
|
|
3159
|
+
return {
|
|
3160
|
+
entry: {
|
|
3161
|
+
moduleId: reference.moduleId,
|
|
3162
|
+
projectionKind: 'activated-reference',
|
|
3163
|
+
sourceKind: 'local-reference',
|
|
3164
|
+
runtimeSelected: false,
|
|
3165
|
+
...fingerprints,
|
|
3166
|
+
factCoverage: activated.factCoverage ?? buildModuleFactCoverageLedger(),
|
|
3167
|
+
facts: jsonEntry,
|
|
3168
|
+
},
|
|
3169
|
+
markdown: renderModuleFactsMarkdown(facts, fingerprints),
|
|
3170
|
+
warnings: activated.warnings,
|
|
3171
|
+
unresolvedTargets: activated.unresolvedFirstPartyTargets ?? [],
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
|
|
2678
3175
|
export interface ExtractAllModuleFactsOptions {
|
|
2679
3176
|
/**
|
|
2680
3177
|
* Discovered module sources (auto-discovery path). When provided, each entry's
|
|
@@ -2688,6 +3185,15 @@ export interface ExtractAllModuleFactsOptions {
|
|
|
2688
3185
|
coreVersion?: string | null
|
|
2689
3186
|
/** @deprecated Legacy explicit module-id list; only consulted when `sources` is absent. */
|
|
2690
3187
|
moduleIds?: readonly string[]
|
|
3188
|
+
/**
|
|
3189
|
+
* `throw` (the default, and the only value any normal package output uses) aborts the
|
|
3190
|
+
* batch when a first-party extension target does not correlate. `collect` keeps the
|
|
3191
|
+
* unresolved entries in `extensionSurfaces.unresolved` and reports them on the result
|
|
3192
|
+
* so a disposable projection can publish the gap instead of silently dropping it.
|
|
3193
|
+
*/
|
|
3194
|
+
unresolvedFirstPartyTargets?: 'throw' | 'collect'
|
|
3195
|
+
/** Generated-facts compatibility projection. Omitted means the corrected v2 contract. */
|
|
3196
|
+
factsContractVersion?: 1 | 2
|
|
2691
3197
|
}
|
|
2692
3198
|
|
|
2693
3199
|
export interface ExtractAllModuleFactsResult {
|
|
@@ -2695,6 +3201,31 @@ export interface ExtractAllModuleFactsResult {
|
|
|
2695
3201
|
markdownByModule: Record<string, string>
|
|
2696
3202
|
warnings: string[]
|
|
2697
3203
|
frameworkMarkdown: string
|
|
3204
|
+
/** Always empty under the default `throw` policy, which aborts before returning. */
|
|
3205
|
+
unresolvedFirstPartyTargets?: string[]
|
|
3206
|
+
/**
|
|
3207
|
+
* The enum-derived coverage ledger for the taxonomies these facts are expressed
|
|
3208
|
+
* in, built during generation so an enum value with no classification aborts the
|
|
3209
|
+
* run instead of shipping an unclassified public value.
|
|
3210
|
+
*/
|
|
3211
|
+
factCoverage?: ModuleFactCoverageFamily[]
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/**
|
|
3215
|
+
* `<moduleId>:<contributionId>:<targetId>` for every first-party contribution target that
|
|
3216
|
+
* did not correlate — the same identity {@link assertNoUnresolvedExtensionTargets} throws
|
|
3217
|
+
* with, so a collected batch reports exactly what a strict batch would have refused.
|
|
3218
|
+
*/
|
|
3219
|
+
export function collectUnresolvedFirstPartyTargets(
|
|
3220
|
+
surfacesByModule: Readonly<Record<string, ModuleExtensionSurfaceFacts>>,
|
|
3221
|
+
): string[] {
|
|
3222
|
+
return Object.entries(surfacesByModule)
|
|
3223
|
+
.flatMap(([moduleId, surface]) =>
|
|
3224
|
+
surface.unresolved
|
|
3225
|
+
.filter((entry) => entry.reason === 'unresolved-first-party-target')
|
|
3226
|
+
.map((entry) => `${moduleId}:${entry.key}`),
|
|
3227
|
+
)
|
|
3228
|
+
.sort((left, right) => left.localeCompare(right))
|
|
2698
3229
|
}
|
|
2699
3230
|
|
|
2700
3231
|
export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
|
|
@@ -2702,6 +3233,7 @@ export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): Ex
|
|
|
2702
3233
|
}
|
|
2703
3234
|
|
|
2704
3235
|
function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
|
|
3236
|
+
const factCoverage = buildModuleFactCoverageLedger()
|
|
2705
3237
|
const sources: ModuleFactSource[] = options.sources
|
|
2706
3238
|
? [...options.sources]
|
|
2707
3239
|
: (options.coreSrcRoot
|
|
@@ -2723,6 +3255,9 @@ function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions):
|
|
|
2723
3255
|
sourceVersion: source.packageVersion ?? null,
|
|
2724
3256
|
registryPath: options.registryPath ?? null,
|
|
2725
3257
|
registrySource: options.registrySource ?? null,
|
|
3258
|
+
...(source.portableSourceRoot ? { portableSourceRoot: source.portableSourceRoot } : {}),
|
|
3259
|
+
...(source.sourceKind ? { sourceKind: source.sourceKind } : {}),
|
|
3260
|
+
factsContractVersion: options.factsContractVersion ?? 2,
|
|
2726
3261
|
})
|
|
2727
3262
|
factsByModule[source.moduleId] = facts
|
|
2728
3263
|
warnings.push(...facts.warnings)
|
|
@@ -2742,8 +3277,10 @@ function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions):
|
|
|
2742
3277
|
...sources.flatMap((source) => extractKnownApiRouteIds(source.moduleId, source.moduleRoot)),
|
|
2743
3278
|
]),
|
|
2744
3279
|
commandIds: new Set(sources.flatMap((source) => extractKnownCommandIds(source.moduleId, source.moduleRoot))),
|
|
3280
|
+
factsContractVersion: options.factsContractVersion ?? 2,
|
|
2745
3281
|
})
|
|
2746
|
-
|
|
3282
|
+
const unresolvedFirstPartyTargets = collectUnresolvedFirstPartyTargets(correlated)
|
|
3283
|
+
if ((options.unresolvedFirstPartyTargets ?? 'throw') === 'throw') assertNoUnresolvedExtensionTargets(correlated)
|
|
2747
3284
|
const apiRouteOwners = new Map<string, {
|
|
2748
3285
|
moduleId: string
|
|
2749
3286
|
source: ModuleFactSourceRef
|
|
@@ -2779,5 +3316,712 @@ function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions):
|
|
|
2779
3316
|
`[module-facts] ${moduleId} ${entry.reason}: ${entry.key} (${entry.source.path})`,
|
|
2780
3317
|
))
|
|
2781
3318
|
}
|
|
2782
|
-
return {
|
|
3319
|
+
return {
|
|
3320
|
+
factsByModule,
|
|
3321
|
+
markdownByModule,
|
|
3322
|
+
warnings,
|
|
3323
|
+
frameworkMarkdown: renderFrameworkExtensionPointsMarkdown(),
|
|
3324
|
+
unresolvedFirstPartyTargets,
|
|
3325
|
+
factCoverage,
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* Enum-derived `factCoverage` ledger — spec
|
|
3331
|
+
* `.ai/specs/2026-07-31-standalone-canonical-example-module.md`,
|
|
3332
|
+
* § PR #4883 Module-Fact and Extension-Topology Contract.
|
|
3333
|
+
*
|
|
3334
|
+
* Every finite public set is enumerated from the `as const` ledger that also derives
|
|
3335
|
+
* its type, so an added enum value has no ledger row and fails
|
|
3336
|
+
* {@link buildModuleFactCoverageLedger} instead of joining the public surface
|
|
3337
|
+
* unclassified. The purpose of the ledger is to make a *silent zero* impossible: a
|
|
3338
|
+
* family cannot claim canonical coverage while the real extractor publishes no
|
|
3339
|
+
* contribution for it.
|
|
3340
|
+
*/
|
|
3341
|
+
export type ModuleFactCoverageStatus =
|
|
3342
|
+
/** The canonical example really emits it; a real extraction proves a non-zero count. */
|
|
3343
|
+
| 'emitted-example'
|
|
3344
|
+
/** A framework/app-level setting that never becomes a module contribution. */
|
|
3345
|
+
| 'framework-only'
|
|
3346
|
+
/** Described by the framework catalog only; the example contributes nothing for it. */
|
|
3347
|
+
| 'catalog-only'
|
|
3348
|
+
/** In the closed public set, but no generator code path emits it today. */
|
|
3349
|
+
| 'currently-unbound'
|
|
3350
|
+
/**
|
|
3351
|
+
* Required to be `emitted-example` by the spec but NOT emitted yet. The status
|
|
3352
|
+
* itself states the gap, so a machine reader keying on `status` alone is never
|
|
3353
|
+
* told the example emits something it does not.
|
|
3354
|
+
*/
|
|
3355
|
+
| 'pending-emission'
|
|
3356
|
+
/** Only a deliberately broken fixture produces it; canonical output emits none. */
|
|
3357
|
+
| 'negative-fixture'
|
|
3358
|
+
|
|
3359
|
+
export type ModuleFactCoverageRow = {
|
|
3360
|
+
value: string
|
|
3361
|
+
status: ModuleFactCoverageStatus
|
|
3362
|
+
note: string
|
|
3363
|
+
/**
|
|
3364
|
+
* Names the slice that owns closing a `pending-emission` gap. A pending row is
|
|
3365
|
+
* asserted to have a *zero* real count, so it can never be mistaken for proven
|
|
3366
|
+
* coverage and cannot go stale once the gap closes.
|
|
3367
|
+
*/
|
|
3368
|
+
pendingEmission?: string
|
|
3369
|
+
/**
|
|
3370
|
+
* Set when the value is extractable only from the generated module registry rather
|
|
3371
|
+
* than from module source, so a source-only extraction legitimately reports zero.
|
|
3372
|
+
*
|
|
3373
|
+
* This is the only way a row skips the non-zero proof, so the flag is proven rather
|
|
3374
|
+
* than trusted: `module-facts.example-fact-coverage.test.ts` asserts every row
|
|
3375
|
+
* carrying it counts zero from a source-only extraction of the example AND non-zero
|
|
3376
|
+
* once the same extraction is handed a module registry.
|
|
3377
|
+
*/
|
|
3378
|
+
requiresGeneratedRegistry?: true
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
export type ModuleFactCoverageFamily = {
|
|
3382
|
+
family: string
|
|
3383
|
+
enumSource: string
|
|
3384
|
+
rows: ModuleFactCoverageRow[]
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
const FACT_SOURCE_KIND_COVERAGE: Record<ModuleFactSourceKind, ModuleFactCoverageRow> = {
|
|
3388
|
+
'module-metadata': { value: 'module-metadata', status: 'emitted-example', note: 'example/index.ts ModuleInfo export' },
|
|
3389
|
+
entity: { value: 'entity', status: 'emitted-example', note: 'example/data/entities.ts ORM entities' },
|
|
3390
|
+
event: { value: 'event', status: 'emitted-example', note: 'example/events.ts typed event definitions' },
|
|
3391
|
+
'acl-feature': { value: 'acl-feature', status: 'emitted-example', note: 'example/acl.ts RBAC features' },
|
|
3392
|
+
'api-route': {
|
|
3393
|
+
value: 'api-route',
|
|
3394
|
+
status: 'emitted-example',
|
|
3395
|
+
note: 'example/api/**/route.ts; route facts are read from the generated module registry, not from module source',
|
|
3396
|
+
requiresGeneratedRegistry: true,
|
|
3397
|
+
},
|
|
3398
|
+
'di-registration': { value: 'di-registration', status: 'emitted-example', note: 'example/di.ts Awilix container.register' },
|
|
3399
|
+
search: { value: 'search', status: 'emitted-example', note: 'example/search.ts indexed entity declaration' },
|
|
3400
|
+
vector: {
|
|
3401
|
+
value: 'vector',
|
|
3402
|
+
status: 'emitted-example',
|
|
3403
|
+
note: 'example/vector.ts deterministic Todo vector declaration',
|
|
3404
|
+
},
|
|
3405
|
+
notification: { value: 'notification', status: 'emitted-example', note: 'example/notifications.ts notification types' },
|
|
3406
|
+
'cli-command': { value: 'cli-command', status: 'emitted-example', note: 'example/cli.ts module commands' },
|
|
3407
|
+
'backend-page': { value: 'backend-page', status: 'emitted-example', note: 'example/backend/**/page.tsx admin pages' },
|
|
3408
|
+
'frontend-page': { value: 'frontend-page', status: 'emitted-example', note: 'example/frontend/**/page.tsx portal pages' },
|
|
3409
|
+
'ai-tool': { value: 'ai-tool', status: 'emitted-example', note: 'example/ai-tools.ts defineAiTool declarations' },
|
|
3410
|
+
'ai-agent': { value: 'ai-agent', status: 'emitted-example', note: 'example/ai-agents.ts defineAiAgent declarations' },
|
|
3411
|
+
'ai-extension': { value: 'ai-extension', status: 'emitted-example', note: 'example AI agent/tool override and extension records' },
|
|
3412
|
+
command: { value: 'command', status: 'emitted-example', note: 'example/commands/todos.ts command handlers' },
|
|
3413
|
+
subscriber: { value: 'subscriber', status: 'emitted-example', note: 'example/subscribers/*.ts event subscribers' },
|
|
3414
|
+
worker: { value: 'worker', status: 'emitted-example', note: 'example/workers/*.ts queue workers' },
|
|
3415
|
+
'page-middleware': { value: 'page-middleware', status: 'emitted-example', note: 'example/backend/middleware.ts page guard' },
|
|
3416
|
+
setup: { value: 'setup', status: 'emitted-example', note: 'example/setup.ts tenant setup hooks' },
|
|
3417
|
+
encryption: { value: 'encryption', status: 'emitted-example', note: 'example/encryption.ts field encryption map' },
|
|
3418
|
+
'custom-entity': { value: 'custom-entity', status: 'emitted-example', note: 'example/ce.ts custom entity and field DSL' },
|
|
3419
|
+
integration: { value: 'integration', status: 'emitted-example', note: 'example/lib/mock-*-adapter.ts integration registrations' },
|
|
3420
|
+
'generator-plugin': {
|
|
3421
|
+
value: 'generator-plugin',
|
|
3422
|
+
status: 'emitted-example',
|
|
3423
|
+
note: 'example/generators.ts deterministic reference-index generator plugin',
|
|
3424
|
+
},
|
|
3425
|
+
'extension-host': { value: 'extension-host', status: 'emitted-example', note: 'example/extension-points.ts declared hosts' },
|
|
3426
|
+
'extension-contribution': {
|
|
3427
|
+
value: 'extension-contribution',
|
|
3428
|
+
status: 'emitted-example',
|
|
3429
|
+
note: 'example widget, DataTable, CrudForm, interceptor and enricher contributions',
|
|
3430
|
+
},
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
const OWNED_CONTRACT_KIND_COVERAGE: Record<ModuleOwnedContractKind, ModuleFactCoverageRow> = {
|
|
3434
|
+
'module-metadata': { value: 'module-metadata', status: 'emitted-example', note: 'ModuleInfo owned contract' },
|
|
3435
|
+
command: { value: 'command', status: 'emitted-example', note: 'example/commands/todos.ts' },
|
|
3436
|
+
worker: { value: 'worker', status: 'emitted-example', note: 'example/workers/*.ts' },
|
|
3437
|
+
'page-middleware': { value: 'page-middleware', status: 'emitted-example', note: 'example/backend/middleware.ts' },
|
|
3438
|
+
setup: { value: 'setup', status: 'emitted-example', note: 'example/setup.ts' },
|
|
3439
|
+
encryption: { value: 'encryption', status: 'emitted-example', note: 'example/encryption.ts' },
|
|
3440
|
+
'di-registration': {
|
|
3441
|
+
value: 'di-registration',
|
|
3442
|
+
status: 'emitted-example',
|
|
3443
|
+
note: 'example/di.ts scoped Todo summary cache service consumed by a route',
|
|
3444
|
+
},
|
|
3445
|
+
'custom-entity': { value: 'custom-entity', status: 'emitted-example', note: 'example/ce.ts' },
|
|
3446
|
+
'ai-extension': { value: 'ai-extension', status: 'emitted-example', note: 'example AI override/extension record' },
|
|
3447
|
+
'generator-plugin': {
|
|
3448
|
+
value: 'generator-plugin',
|
|
3449
|
+
status: 'emitted-example',
|
|
3450
|
+
note: 'example/generators.ts deterministic reference-index generator plugin',
|
|
3451
|
+
},
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
const OVERRIDE_DOMAIN_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3455
|
+
ai: { value: 'ai', status: 'emitted-example', note: 'ai.agents, ai.tools and ai.extensions targets' },
|
|
3456
|
+
routes: { value: 'routes', status: 'emitted-example', note: 'routes.api and routes.pages targets' },
|
|
3457
|
+
events: { value: 'events', status: 'emitted-example', note: 'events.subscribers targets' },
|
|
3458
|
+
workers: { value: 'workers', status: 'emitted-example', note: 'workers targets' },
|
|
3459
|
+
widgets: { value: 'widgets', status: 'emitted-example', note: 'widgets.injection, .components and .dashboard targets' },
|
|
3460
|
+
notifications: { value: 'notifications', status: 'emitted-example', note: 'notifications.types and notifications.handlers targets' },
|
|
3461
|
+
interceptors: { value: 'interceptors', status: 'emitted-example', note: 'API interceptor targets' },
|
|
3462
|
+
commandInterceptors: { value: 'commandInterceptors', status: 'emitted-example', note: 'command interceptor targets' },
|
|
3463
|
+
enrichers: { value: 'enrichers', status: 'emitted-example', note: 'response enricher targets' },
|
|
3464
|
+
guards: { value: 'guards', status: 'emitted-example', note: 'page-middleware guard targets' },
|
|
3465
|
+
cli: { value: 'cli', status: 'emitted-example', note: 'CLI command targets' },
|
|
3466
|
+
setup: { value: 'setup', status: 'emitted-example', note: 'setup hook and default-role targets' },
|
|
3467
|
+
acl: { value: 'acl', status: 'emitted-example', note: 'acl.features targets' },
|
|
3468
|
+
di: { value: 'di', status: 'emitted-example', note: 'DI token targets' },
|
|
3469
|
+
encryption: { value: 'encryption', status: 'emitted-example', note: 'encryption.maps targets' },
|
|
3470
|
+
nav: {
|
|
3471
|
+
value: 'nav',
|
|
3472
|
+
status: 'framework-only',
|
|
3473
|
+
note: 'nav.groupOrder is an app-level framework setting; no module target is ever emitted for it',
|
|
3474
|
+
},
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
const OVERRIDE_MODE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3478
|
+
'disable-replace': {
|
|
3479
|
+
value: 'disable-replace',
|
|
3480
|
+
status: 'emitted-example',
|
|
3481
|
+
note: 'default mode for most example override hosts',
|
|
3482
|
+
},
|
|
3483
|
+
replace: { value: 'replace', status: 'emitted-example', note: 'the setup host replaces wholesale' },
|
|
3484
|
+
additive: { value: 'additive', status: 'emitted-example', note: 'the ai.extensions host merges additively' },
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
const OVERRIDE_NOTE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3488
|
+
'safe-metadata-only': {
|
|
3489
|
+
value: 'safe-metadata-only',
|
|
3490
|
+
status: 'emitted-example',
|
|
3491
|
+
note: 'emitted on the metadata-only override target',
|
|
3492
|
+
},
|
|
3493
|
+
'page-middleware-not-mutation-guard': {
|
|
3494
|
+
value: 'page-middleware-not-mutation-guard',
|
|
3495
|
+
status: 'emitted-example',
|
|
3496
|
+
note: 'emitted on the guards target so a page guard is not read as a mutation guard',
|
|
3497
|
+
},
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
/**
|
|
3501
|
+
* Every `negative-fixture` row here names a code that a real fixture in
|
|
3502
|
+
* `module-override-targets.diagnostic-wiring.test.ts` drives out of
|
|
3503
|
+
* `collectModuleOverrideTargets`; the coverage test asserts that fixture set matches
|
|
3504
|
+
* these rows exactly, so a row can never claim a fixture that does not exist.
|
|
3505
|
+
*/
|
|
3506
|
+
const OVERRIDE_DIAGNOSTIC_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3507
|
+
'missing-owned-fact': {
|
|
3508
|
+
value: 'missing-owned-fact',
|
|
3509
|
+
status: 'currently-unbound',
|
|
3510
|
+
note: 'reserved by the spec\'s closed diagnostic set; no adapter emits it today, so not even a broken fixture can produce it',
|
|
3511
|
+
},
|
|
3512
|
+
'missing-source': {
|
|
3513
|
+
value: 'missing-source',
|
|
3514
|
+
status: 'negative-fixture',
|
|
3515
|
+
note: 'a fact whose provenance cannot be proven yields this and no target; activated canonical output emits none',
|
|
3516
|
+
},
|
|
3517
|
+
'unsupported-dynamic-key': {
|
|
3518
|
+
value: 'unsupported-dynamic-key',
|
|
3519
|
+
status: 'negative-fixture',
|
|
3520
|
+
note: 'a key the runtime cannot address yields this and no target; activated canonical output emits none',
|
|
3521
|
+
},
|
|
3522
|
+
'unknown-framework-domain': {
|
|
3523
|
+
value: 'unknown-framework-domain',
|
|
3524
|
+
status: 'negative-fixture',
|
|
3525
|
+
note: 'the framework catalog does not describe the dotted host at all',
|
|
3526
|
+
},
|
|
3527
|
+
'unknown-framework-mode': {
|
|
3528
|
+
value: 'unknown-framework-mode',
|
|
3529
|
+
status: 'negative-fixture',
|
|
3530
|
+
note: 'the framework catalog describes the host but names an operation that is not a public mode',
|
|
3531
|
+
},
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
const CONTRIBUTION_KIND_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3535
|
+
widget: { value: 'widget', status: 'emitted-example', note: 'example widget injection and dashboard registrations' },
|
|
3536
|
+
'data-table': { value: 'data-table', status: 'emitted-example', note: 'example DataTable extension entries' },
|
|
3537
|
+
'crud-form': { value: 'crud-form', status: 'emitted-example', note: 'example CrudForm field and lifecycle entries' },
|
|
3538
|
+
'component-override': { value: 'component-override', status: 'emitted-example', note: 'example replace, wrapper and props component entries' },
|
|
3539
|
+
'response-enricher': { value: 'response-enricher', status: 'emitted-example', note: 'example customer priority response enricher' },
|
|
3540
|
+
'api-interceptor': { value: 'api-interceptor', status: 'emitted-example', note: 'example CRUD API interceptor entries' },
|
|
3541
|
+
'command-interceptor': { value: 'command-interceptor', status: 'emitted-example', note: 'example command interceptor entries' },
|
|
3542
|
+
'mutation-guard': { value: 'mutation-guard', status: 'emitted-example', note: 'example Todo mutation guard' },
|
|
3543
|
+
'entity-extension': { value: 'entity-extension', status: 'emitted-example', note: 'example customer entity extension' },
|
|
3544
|
+
subscriber: { value: 'subscriber', status: 'emitted-example', note: 'example synchronous and asynchronous subscribers' },
|
|
3545
|
+
'browser-reaction': { value: 'browser-reaction', status: 'emitted-example', note: 'example client, portal and notification reactions' },
|
|
3546
|
+
'specialized-registry': { value: 'specialized-registry', status: 'emitted-example', note: 'example notification, search, AI and mock adapter registries' },
|
|
3547
|
+
'module-override': { value: 'module-override', status: 'catalog-only', note: 'app-owned configuration is not an outgoing module contribution' },
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
const ACTIVATION_KIND_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3551
|
+
'crud-response-enricher': { value: 'crud-response-enricher', status: 'currently-unbound', note: 'example contributes a response enricher but owns no CRUD response call site for its customer target' },
|
|
3552
|
+
'query-enricher': { value: 'query-enricher', status: 'currently-unbound', note: 'example contributes a query enricher but owns no query-engine call site for its customer target' },
|
|
3553
|
+
'mutation-guard': { value: 'mutation-guard', status: 'emitted-example', note: 'Todo mutation-guard route bridge' },
|
|
3554
|
+
'api-interceptor-bridge': { value: 'api-interceptor-bridge', status: 'emitted-example', note: 'CRUD route interceptor pipeline' },
|
|
3555
|
+
'command-interceptor-bridge': { value: 'command-interceptor-bridge', status: 'emitted-example', note: 'exact Todo command bus target' },
|
|
3556
|
+
'widget-injection-consumer': { value: 'widget-injection-consumer', status: 'emitted-example', note: 'bound injection host consumer' },
|
|
3557
|
+
'component-extension-consumer': { value: 'component-extension-consumer', status: 'emitted-example', note: 'bound component handle consumer' },
|
|
3558
|
+
'dashboard-host-consumer': { value: 'dashboard-host-consumer', status: 'emitted-example', note: 'framework dashboard registry consumer' },
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
const HOST_FAMILY_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3562
|
+
generic: { value: 'generic', status: 'emitted-example', note: 'example headless injection host' },
|
|
3563
|
+
menu: { value: 'menu', status: 'framework-only', note: 'framework menu hosts consumed by example widgets' },
|
|
3564
|
+
'data-table': { value: 'data-table', status: 'emitted-example', note: 'example Todo DataTable host' },
|
|
3565
|
+
'crud-form': { value: 'crud-form', status: 'emitted-example', note: 'example Todo CrudForm host' },
|
|
3566
|
+
detail: { value: 'detail', status: 'framework-only', note: 'framework detail hosts consumed by example widgets' },
|
|
3567
|
+
'portal-page': { value: 'portal-page', status: 'framework-only', note: 'framework portal host consumed by example widgets' },
|
|
3568
|
+
'component-handle': { value: 'component-handle', status: 'emitted-example', note: 'example replaceable component host' },
|
|
3569
|
+
entity: { value: 'entity', status: 'emitted-example', note: 'example entity fact-reference hosts' },
|
|
3570
|
+
'api-route': { value: 'api-route', status: 'framework-only', note: 'API route hosts are owned by the target module and consumed by example interceptors' },
|
|
3571
|
+
command: { value: 'command', status: 'framework-only', note: 'command hosts are owned by the target module and consumed by example interceptors' },
|
|
3572
|
+
event: { value: 'event', status: 'emitted-example', note: 'example event fact-reference hosts' },
|
|
3573
|
+
'query-lifecycle': { value: 'query-lifecycle', status: 'emitted-example', note: 'example Todo query lifecycle hosts' },
|
|
3574
|
+
dashboard: { value: 'dashboard', status: 'framework-only', note: 'framework dashboard host consumed by example widgets' },
|
|
3575
|
+
notification: { value: 'notification', status: 'framework-only', note: 'framework notification host consumed by the example registry' },
|
|
3576
|
+
integration: { value: 'integration', status: 'framework-only', note: 'framework integration detail host consumed by example widgets' },
|
|
3577
|
+
'specialized-registry': { value: 'specialized-registry', status: 'framework-only', note: 'framework-owned specialist registries accept example entries' },
|
|
3578
|
+
'module-override': { value: 'module-override', status: 'framework-only', note: 'framework override catalog owns these hosts' },
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
const HOST_CAPABILITY_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3582
|
+
'render-widget': { value: 'render-widget', status: 'emitted-example', note: 'example render widget host and contributions' },
|
|
3583
|
+
'headless-widget': { value: 'headless-widget', status: 'currently-unbound', note: 'injection-table facts do not yet distinguish headless widget payloads' },
|
|
3584
|
+
'menu-item': { value: 'menu-item', status: 'currently-unbound', note: 'injection-table facts do not yet distinguish menu item payloads' },
|
|
3585
|
+
'column-widget': { value: 'column-widget', status: 'emitted-example', note: 'example DataTable column host' },
|
|
3586
|
+
'row-action': { value: 'row-action', status: 'emitted-example', note: 'example DataTable row-action host' },
|
|
3587
|
+
'bulk-action': { value: 'bulk-action', status: 'emitted-example', note: 'example DataTable bulk-action host' },
|
|
3588
|
+
'filter-widget': { value: 'filter-widget', status: 'emitted-example', note: 'example DataTable filter host' },
|
|
3589
|
+
'toolbar-widget': { value: 'toolbar-widget', status: 'emitted-example', note: 'example DataTable toolbar host' },
|
|
3590
|
+
'field-widget': { value: 'field-widget', status: 'emitted-example', note: 'example CrudForm field host' },
|
|
3591
|
+
'lifecycle-handler': { value: 'lifecycle-handler', status: 'emitted-example', note: 'example CrudForm lifecycle host' },
|
|
3592
|
+
'component-replacement': { value: 'component-replacement', status: 'emitted-example', note: 'example replaceable DataTable, CrudForm and component hosts' },
|
|
3593
|
+
'response-enricher': { value: 'response-enricher', status: 'emitted-example', note: 'example entity enricher host' },
|
|
3594
|
+
'query-enricher': { value: 'query-enricher', status: 'emitted-example', note: 'example query-enricher host' },
|
|
3595
|
+
'api-interceptor': { value: 'api-interceptor', status: 'emitted-example', note: 'example API interceptor contribution and route bridge' },
|
|
3596
|
+
'command-interceptor': { value: 'command-interceptor', status: 'emitted-example', note: 'example command interceptor and exact command owner' },
|
|
3597
|
+
'mutation-guard': { value: 'mutation-guard', status: 'emitted-example', note: 'example entity mutation guard host' },
|
|
3598
|
+
'entity-extension': { value: 'entity-extension', status: 'emitted-example', note: 'example entity extension host' },
|
|
3599
|
+
'async-subscriber': { value: 'async-subscriber', status: 'emitted-example', note: 'example event subscriber host' },
|
|
3600
|
+
'sync-subscriber': { value: 'sync-subscriber', status: 'emitted-example', note: 'example synchronous event subscriber host' },
|
|
3601
|
+
'browser-client': { value: 'browser-client', status: 'emitted-example', note: 'example client-broadcast event host' },
|
|
3602
|
+
'browser-portal': { value: 'browser-portal', status: 'emitted-example', note: 'example portal-broadcast event host' },
|
|
3603
|
+
'registry-contribution': { value: 'registry-contribution', status: 'emitted-example', note: 'example specialized registry contributions' },
|
|
3604
|
+
'module-override': { value: 'module-override', status: 'framework-only', note: 'framework override hosts; app configuration is not a contribution' },
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
const HOST_ACTIVATION_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3608
|
+
always: { value: 'always', status: 'emitted-example', note: 'always-active example declarations' },
|
|
3609
|
+
'host-opt-in': { value: 'host-opt-in', status: 'emitted-example', note: 'example entity extension and response-enricher hosts' },
|
|
3610
|
+
'caller-opt-in': { value: 'caller-opt-in', status: 'emitted-example', note: 'example query lifecycle and query enricher' },
|
|
3611
|
+
'feature-gated': { value: 'feature-gated', status: 'currently-unbound', note: 'feature requirements are emitted but are not a distinct activation value' },
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
const TARGET_KIND_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3615
|
+
module: { value: 'module', status: 'emitted-example', note: 'example module-scoped registry target' },
|
|
3616
|
+
entity: { value: 'entity', status: 'emitted-example', note: 'example entity extension, enricher and guard targets' },
|
|
3617
|
+
'api-route': { value: 'api-route', status: 'emitted-example', note: 'example API interceptor targets' },
|
|
3618
|
+
command: { value: 'command', status: 'emitted-example', note: 'example exact and wildcard command targets' },
|
|
3619
|
+
'widget-spot': { value: 'widget-spot', status: 'emitted-example', note: 'example injection targets' },
|
|
3620
|
+
component: { value: 'component', status: 'emitted-example', note: 'example component override targets' },
|
|
3621
|
+
event: { value: 'event', status: 'emitted-example', note: 'example subscriber targets' },
|
|
3622
|
+
notification: { value: 'notification', status: 'emitted-example', note: 'example notification registry target' },
|
|
3623
|
+
wildcard: { value: 'wildcard', status: 'emitted-example', note: 'example wildcard subscriber and interceptor targets' },
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
const HOST_RESOLUTION_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3627
|
+
exact: { value: 'exact', status: 'emitted-example', note: 'example exact declared hosts' },
|
|
3628
|
+
pattern: { value: 'pattern', status: 'emitted-example', note: 'example patterned host surfaces' },
|
|
3629
|
+
framework: { value: 'framework', status: 'framework-only', note: 'framework catalog host identity' },
|
|
3630
|
+
'fact-ref': { value: 'fact-ref', status: 'emitted-example', note: 'example entity, event and query fact-reference hosts' },
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
const TARGET_RESOLUTION_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3634
|
+
exact: { value: 'exact', status: 'emitted-example', note: 'example exact target correlation' },
|
|
3635
|
+
pattern: { value: 'pattern', status: 'emitted-example', note: 'example wildcard and patterned target correlation' },
|
|
3636
|
+
framework: { value: 'framework', status: 'emitted-example', note: 'example contribution to a framework host' },
|
|
3637
|
+
'fact-ref': { value: 'fact-ref', status: 'emitted-example', note: 'example contribution to a generated fact owner' },
|
|
3638
|
+
'optional-external': { value: 'optional-external', status: 'emitted-example', note: 'example optional module target remains fail-soft' },
|
|
3639
|
+
unresolved: { value: 'unresolved', status: 'negative-fixture', note: 'only malformed targets remain unresolved' },
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
const TOPOLOGY_RESOLUTION_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3643
|
+
bound: { value: 'bound', status: 'emitted-example', note: 'example contribution reaches a verified runtime activation' },
|
|
3644
|
+
'capability-only': { value: 'capability-only', status: 'emitted-example', note: 'declaration-backed capability without a separate caller activation' },
|
|
3645
|
+
'optional-target-missing': { value: 'optional-target-missing', status: 'emitted-example', note: 'optional external target absent from the selected module set' },
|
|
3646
|
+
wildcard: { value: 'wildcard', status: 'emitted-example', note: 'wildcard contribution resolves against selected targets' },
|
|
3647
|
+
unresolved: { value: 'unresolved', status: 'negative-fixture', note: 'only malformed first-party targets remain unresolved' },
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
const SPECIALIZED_REGISTRY_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3651
|
+
notification: { value: 'notification', status: 'emitted-example', note: 'example notification type registry' },
|
|
3652
|
+
integration: { value: 'integration', status: 'emitted-example', note: 'example credential-free integration bundle identity' },
|
|
3653
|
+
search: { value: 'search', status: 'emitted-example', note: 'example Todo search registry' },
|
|
3654
|
+
vector: { value: 'vector', status: 'emitted-example', note: 'example deterministic Todo vector registry' },
|
|
3655
|
+
ai: { value: 'ai', status: 'emitted-example', note: 'example AI tool and agent registries' },
|
|
3656
|
+
payment: { value: 'payment', status: 'emitted-example', note: 'example mock payment descriptors' },
|
|
3657
|
+
shipping: { value: 'shipping', status: 'emitted-example', note: 'example mock shipping registry identity' },
|
|
3658
|
+
currency: { value: 'currency', status: 'emitted-example', note: 'example deterministic currency provider identity' },
|
|
3659
|
+
workflow: { value: 'workflow', status: 'emitted-example', note: 'example event-triggered Todo workflow identity' },
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
const COMPONENT_MODE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3663
|
+
replace: { value: 'replace', status: 'emitted-example', note: 'example replacement component' },
|
|
3664
|
+
wrapper: { value: 'wrapper', status: 'emitted-example', note: 'example decorating wrapper component' },
|
|
3665
|
+
props: { value: 'props', status: 'emitted-example', note: 'example props-transform component' },
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
const FACT_DIAGNOSTIC_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3669
|
+
'duplicate-source': { value: 'duplicate-source', status: 'negative-fixture', note: 'duplicate provenance fixture; canonical output emits none' },
|
|
3670
|
+
'unresolved-static-contract': { value: 'unresolved-static-contract', status: 'negative-fixture', note: 'dynamic contract fixture; canonical output emits none' },
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
const UNRESOLVED_REASON_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3674
|
+
'unclassified-binding': { value: 'unclassified-binding', status: 'negative-fixture', note: 'malformed extension declaration fixture' },
|
|
3675
|
+
'unbound-declaration': { value: 'unbound-declaration', status: 'negative-fixture', note: 'declared host without a real caller fixture' },
|
|
3676
|
+
'dynamic-without-pattern': { value: 'dynamic-without-pattern', status: 'negative-fixture', note: 'dynamic host without a declared pattern fixture' },
|
|
3677
|
+
'unresolved-first-party-target': { value: 'unresolved-first-party-target', status: 'negative-fixture', note: 'broken first-party target fixture' },
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
const DATA_TABLE_SURFACE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3681
|
+
header: { value: 'header', status: 'emitted-example', note: 'bound Todo table header' },
|
|
3682
|
+
footer: { value: 'footer', status: 'emitted-example', note: 'bound Todo table footer' },
|
|
3683
|
+
toolbar: { value: 'toolbar', status: 'emitted-example', note: 'bound Todo table toolbar' },
|
|
3684
|
+
searchTrailing: { value: 'searchTrailing', status: 'emitted-example', note: 'bound Todo table search trailing surface' },
|
|
3685
|
+
columns: { value: 'columns', status: 'emitted-example', note: 'bound Todo table columns' },
|
|
3686
|
+
rowActions: { value: 'rowActions', status: 'emitted-example', note: 'bound Todo table row actions' },
|
|
3687
|
+
bulkActions: { value: 'bulkActions', status: 'emitted-example', note: 'bound Todo table bulk actions' },
|
|
3688
|
+
filters: { value: 'filters', status: 'emitted-example', note: 'bound Todo table filters' },
|
|
3689
|
+
replacement: { value: 'replacement', status: 'emitted-example', note: 'bound Todo table replacement' },
|
|
3690
|
+
emptyState: { value: 'emptyState', status: 'currently-unbound', note: 'catalog-declared DataTable surface has no runtime host' },
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
const CRUD_FORM_SURFACE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3694
|
+
base: { value: 'base', status: 'emitted-example', note: 'bound Todo form base' },
|
|
3695
|
+
header: { value: 'header', status: 'emitted-example', note: 'bound Todo form header' },
|
|
3696
|
+
fields: { value: 'fields', status: 'emitted-example', note: 'bound Todo form fields' },
|
|
3697
|
+
replacement: { value: 'replacement', status: 'emitted-example', note: 'bound Todo form replacement' },
|
|
3698
|
+
beforeFields: { value: 'beforeFields', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3699
|
+
afterFields: { value: 'afterFields', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3700
|
+
footer: { value: 'footer', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3701
|
+
sidebar: { value: 'sidebar', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3702
|
+
group: { value: 'group', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3703
|
+
fieldBefore: { value: 'fieldBefore', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3704
|
+
fieldAfter: { value: 'fieldAfter', status: 'currently-unbound', note: 'catalog-declared CrudForm surface has no runtime host' },
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
const CRUD_FORM_LIFECYCLE_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3708
|
+
transformValidation: { value: 'transformValidation', status: 'emitted-example', note: 'example Todo form lifecycle transformValidation' },
|
|
3709
|
+
transformDisplayData: { value: 'transformDisplayData', status: 'emitted-example', note: 'example Todo form lifecycle transformDisplayData' },
|
|
3710
|
+
onBeforeNavigate: { value: 'onBeforeNavigate', status: 'emitted-example', note: 'example Todo form lifecycle onBeforeNavigate' },
|
|
3711
|
+
onAppEvent: { value: 'onAppEvent', status: 'emitted-example', note: 'example Todo form lifecycle onAppEvent' },
|
|
3712
|
+
onVisibilityChange: { value: 'onVisibilityChange', status: 'emitted-example', note: 'example Todo form lifecycle onVisibilityChange' },
|
|
3713
|
+
onBeforeDelete: { value: 'onBeforeDelete', status: 'emitted-example', note: 'example Todo form lifecycle onBeforeDelete' },
|
|
3714
|
+
onDelete: { value: 'onDelete', status: 'emitted-example', note: 'example Todo form lifecycle onDelete' },
|
|
3715
|
+
onAfterDelete: { value: 'onAfterDelete', status: 'emitted-example', note: 'example Todo form lifecycle onAfterDelete' },
|
|
3716
|
+
onDeleteError: { value: 'onDeleteError', status: 'emitted-example', note: 'example Todo form lifecycle onDeleteError' },
|
|
3717
|
+
onFieldChange: { value: 'onFieldChange', status: 'emitted-example', note: 'example Todo form lifecycle onFieldChange' },
|
|
3718
|
+
transformFormData: { value: 'transformFormData', status: 'emitted-example', note: 'example Todo form lifecycle transformFormData' },
|
|
3719
|
+
onBeforeSave: { value: 'onBeforeSave', status: 'emitted-example', note: 'example Todo form lifecycle onBeforeSave' },
|
|
3720
|
+
onSave: { value: 'onSave', status: 'emitted-example', note: 'example Todo form lifecycle onSave' },
|
|
3721
|
+
onAfterSave: { value: 'onAfterSave', status: 'emitted-example', note: 'example Todo form lifecycle onAfterSave' },
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
const CRUD_FORM_OPERATION_COVERAGE: Readonly<Record<string, ModuleFactCoverageRow>> = {
|
|
3725
|
+
create: { value: 'create', status: 'emitted-example', note: 'example Todo create lifecycle' },
|
|
3726
|
+
update: { value: 'update', status: 'currently-unbound', note: 'example lifecycle contribution is currently registered for create only' },
|
|
3727
|
+
delete: { value: 'delete', status: 'currently-unbound', note: 'example lifecycle contribution is currently registered for create only' },
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
/**
|
|
3731
|
+
* One family of the ledger. Both directions are enforced: an enumerated value with
|
|
3732
|
+
* no row, and a row classifying a value the enum no longer carries, each throw — so
|
|
3733
|
+
* neither an unclassified new value nor a stale row for a removed one can ship.
|
|
3734
|
+
*/
|
|
3735
|
+
export function buildCoverageFamily(
|
|
3736
|
+
family: string,
|
|
3737
|
+
enumSource: string,
|
|
3738
|
+
values: readonly string[],
|
|
3739
|
+
coverage: Readonly<Record<string, ModuleFactCoverageRow>>,
|
|
3740
|
+
): ModuleFactCoverageFamily {
|
|
3741
|
+
const rows: ModuleFactCoverageRow[] = []
|
|
3742
|
+
const missing: string[] = []
|
|
3743
|
+
for (const value of values) {
|
|
3744
|
+
const row = coverage[value]
|
|
3745
|
+
if (!row) {
|
|
3746
|
+
missing.push(value)
|
|
3747
|
+
continue
|
|
3748
|
+
}
|
|
3749
|
+
rows.push(row)
|
|
3750
|
+
}
|
|
3751
|
+
if (missing.length > 0) {
|
|
3752
|
+
throw new Error(
|
|
3753
|
+
`[module-facts] factCoverage family "${family}" has no row for: ${missing.join(', ')}. ` +
|
|
3754
|
+
'Add an explicit classification instead of leaving the value unclassified.',
|
|
3755
|
+
)
|
|
3756
|
+
}
|
|
3757
|
+
const extra = Object.keys(coverage).filter((value) => !values.includes(value))
|
|
3758
|
+
if (extra.length > 0) {
|
|
3759
|
+
throw new Error(
|
|
3760
|
+
`[module-facts] factCoverage family "${family}" classifies values absent from the enum: ${extra.join(', ')}.`,
|
|
3761
|
+
)
|
|
3762
|
+
}
|
|
3763
|
+
return { family, enumSource, rows }
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
/**
|
|
3767
|
+
* Builds the whole ledger, throwing when an enumerated value lacks a row or a row
|
|
3768
|
+
* classifies a value the enum no longer carries. {@link extractAllModuleFacts} calls
|
|
3769
|
+
* it on every run, so either violation fails fact generation — not only the unit
|
|
3770
|
+
* test — and the built ledger is returned on the generation result so the published
|
|
3771
|
+
* mirrors have a single generated authority to be checked against.
|
|
3772
|
+
*/
|
|
3773
|
+
export function buildModuleFactCoverageLedger(): ModuleFactCoverageFamily[] {
|
|
3774
|
+
return [
|
|
3775
|
+
buildCoverageFamily(
|
|
3776
|
+
'ModuleFactSourceKind',
|
|
3777
|
+
'packages/cli/src/lib/generators/module-facts.ts#MODULE_FACT_SOURCE_KINDS',
|
|
3778
|
+
MODULE_FACT_SOURCE_KINDS,
|
|
3779
|
+
FACT_SOURCE_KIND_COVERAGE,
|
|
3780
|
+
),
|
|
3781
|
+
buildCoverageFamily(
|
|
3782
|
+
'ModuleOwnedContractKind',
|
|
3783
|
+
'packages/cli/src/lib/generators/module-facts.ts#MODULE_OWNED_CONTRACT_KINDS',
|
|
3784
|
+
MODULE_OWNED_CONTRACT_KINDS,
|
|
3785
|
+
OWNED_CONTRACT_KIND_COVERAGE,
|
|
3786
|
+
),
|
|
3787
|
+
buildCoverageFamily(
|
|
3788
|
+
'ModuleOverrideDomain',
|
|
3789
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#ALL_OVERRIDE_DOMAINS',
|
|
3790
|
+
ALL_OVERRIDE_DOMAINS,
|
|
3791
|
+
OVERRIDE_DOMAIN_COVERAGE,
|
|
3792
|
+
),
|
|
3793
|
+
buildCoverageFamily(
|
|
3794
|
+
'ModuleOverrideMode',
|
|
3795
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#MODULE_OVERRIDE_MODES',
|
|
3796
|
+
MODULE_OVERRIDE_MODES,
|
|
3797
|
+
OVERRIDE_MODE_COVERAGE,
|
|
3798
|
+
),
|
|
3799
|
+
buildCoverageFamily(
|
|
3800
|
+
'ModuleOverrideTargetNote',
|
|
3801
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#MODULE_OVERRIDE_TARGET_NOTES',
|
|
3802
|
+
MODULE_OVERRIDE_TARGET_NOTES,
|
|
3803
|
+
OVERRIDE_NOTE_COVERAGE,
|
|
3804
|
+
),
|
|
3805
|
+
buildCoverageFamily(
|
|
3806
|
+
'ModuleOverrideTargetDiagnosticCode',
|
|
3807
|
+
'packages/cli/src/lib/generators/module-override-targets.ts#MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES',
|
|
3808
|
+
MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES,
|
|
3809
|
+
OVERRIDE_DIAGNOSTIC_COVERAGE,
|
|
3810
|
+
),
|
|
3811
|
+
buildCoverageFamily(
|
|
3812
|
+
'ModuleExtensionContributionKind',
|
|
3813
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_CONTRIBUTION_KINDS',
|
|
3814
|
+
MODULE_EXTENSION_CONTRIBUTION_KINDS,
|
|
3815
|
+
CONTRIBUTION_KIND_COVERAGE,
|
|
3816
|
+
),
|
|
3817
|
+
buildCoverageFamily(
|
|
3818
|
+
'ModuleExtensionActivationKind',
|
|
3819
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_ACTIVATION_KINDS',
|
|
3820
|
+
MODULE_EXTENSION_ACTIVATION_KINDS,
|
|
3821
|
+
ACTIVATION_KIND_COVERAGE,
|
|
3822
|
+
),
|
|
3823
|
+
buildCoverageFamily(
|
|
3824
|
+
'ExtensionHostFamily',
|
|
3825
|
+
'packages/shared/src/modules/widgets/extension-points.ts#EXTENSION_HOST_FAMILIES',
|
|
3826
|
+
EXTENSION_HOST_FAMILIES,
|
|
3827
|
+
HOST_FAMILY_COVERAGE,
|
|
3828
|
+
),
|
|
3829
|
+
buildCoverageFamily(
|
|
3830
|
+
'ExtensionHostCapability',
|
|
3831
|
+
'packages/shared/src/modules/widgets/extension-points.ts#EXTENSION_HOST_CAPABILITIES',
|
|
3832
|
+
EXTENSION_HOST_CAPABILITIES,
|
|
3833
|
+
HOST_CAPABILITY_COVERAGE,
|
|
3834
|
+
),
|
|
3835
|
+
buildCoverageFamily(
|
|
3836
|
+
'ExtensionHostActivation',
|
|
3837
|
+
'packages/shared/src/modules/widgets/extension-points.ts#EXTENSION_HOST_ACTIVATIONS',
|
|
3838
|
+
EXTENSION_HOST_ACTIVATIONS,
|
|
3839
|
+
HOST_ACTIVATION_COVERAGE,
|
|
3840
|
+
),
|
|
3841
|
+
buildCoverageFamily(
|
|
3842
|
+
'ModuleExtensionTargetKind',
|
|
3843
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_TARGET_KINDS',
|
|
3844
|
+
MODULE_EXTENSION_TARGET_KINDS,
|
|
3845
|
+
TARGET_KIND_COVERAGE,
|
|
3846
|
+
),
|
|
3847
|
+
buildCoverageFamily(
|
|
3848
|
+
'ModuleExtensionHostResolution',
|
|
3849
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_HOST_RESOLUTIONS',
|
|
3850
|
+
MODULE_EXTENSION_HOST_RESOLUTIONS,
|
|
3851
|
+
HOST_RESOLUTION_COVERAGE,
|
|
3852
|
+
),
|
|
3853
|
+
buildCoverageFamily(
|
|
3854
|
+
'ModuleExtensionTargetResolution',
|
|
3855
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_TARGET_RESOLUTIONS',
|
|
3856
|
+
MODULE_EXTENSION_TARGET_RESOLUTIONS,
|
|
3857
|
+
TARGET_RESOLUTION_COVERAGE,
|
|
3858
|
+
),
|
|
3859
|
+
buildCoverageFamily(
|
|
3860
|
+
'ModuleExtensionResolution',
|
|
3861
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_RESOLUTIONS',
|
|
3862
|
+
MODULE_EXTENSION_RESOLUTIONS,
|
|
3863
|
+
TOPOLOGY_RESOLUTION_COVERAGE,
|
|
3864
|
+
),
|
|
3865
|
+
buildCoverageFamily(
|
|
3866
|
+
'SpecializedRegistry',
|
|
3867
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_SPECIALIZED_REGISTRIES',
|
|
3868
|
+
MODULE_SPECIALIZED_REGISTRIES,
|
|
3869
|
+
SPECIALIZED_REGISTRY_COVERAGE,
|
|
3870
|
+
),
|
|
3871
|
+
buildCoverageFamily(
|
|
3872
|
+
'ComponentOverrideMode',
|
|
3873
|
+
'packages/shared/src/modules/widgets/extension-points.ts#COMPONENT_OVERRIDE_MODES',
|
|
3874
|
+
COMPONENT_OVERRIDE_MODES,
|
|
3875
|
+
COMPONENT_MODE_COVERAGE,
|
|
3876
|
+
),
|
|
3877
|
+
buildCoverageFamily(
|
|
3878
|
+
'ModuleFactDiagnosticCode',
|
|
3879
|
+
'packages/cli/src/lib/generators/module-facts.ts#MODULE_FACT_DIAGNOSTIC_CODES',
|
|
3880
|
+
MODULE_FACT_DIAGNOSTIC_CODES,
|
|
3881
|
+
FACT_DIAGNOSTIC_COVERAGE,
|
|
3882
|
+
),
|
|
3883
|
+
buildCoverageFamily(
|
|
3884
|
+
'ModuleExtensionUnresolvedReason',
|
|
3885
|
+
'packages/shared/src/modules/widgets/extension-points.ts#MODULE_EXTENSION_UNRESOLVED_REASONS',
|
|
3886
|
+
MODULE_EXTENSION_UNRESOLVED_REASONS,
|
|
3887
|
+
UNRESOLVED_REASON_COVERAGE,
|
|
3888
|
+
),
|
|
3889
|
+
buildCoverageFamily(
|
|
3890
|
+
'DataTableExtensionSurface',
|
|
3891
|
+
'packages/shared/src/modules/widgets/extension-points.ts#DATA_TABLE_EXTENSION_SURFACE_KEYS',
|
|
3892
|
+
DATA_TABLE_EXTENSION_SURFACE_KEYS,
|
|
3893
|
+
DATA_TABLE_SURFACE_COVERAGE,
|
|
3894
|
+
),
|
|
3895
|
+
buildCoverageFamily(
|
|
3896
|
+
'CrudFormExtensionSurface',
|
|
3897
|
+
'packages/shared/src/modules/widgets/extension-points.ts#CRUD_FORM_EXTENSION_SURFACE_KEYS',
|
|
3898
|
+
CRUD_FORM_EXTENSION_SURFACE_KEYS,
|
|
3899
|
+
CRUD_FORM_SURFACE_COVERAGE,
|
|
3900
|
+
),
|
|
3901
|
+
buildCoverageFamily(
|
|
3902
|
+
'CrudFormLifecyclePhase',
|
|
3903
|
+
'packages/shared/src/modules/widgets/extension-points.ts#CRUD_FORM_LIFECYCLE_PHASES',
|
|
3904
|
+
CRUD_FORM_LIFECYCLE_PHASES,
|
|
3905
|
+
CRUD_FORM_LIFECYCLE_COVERAGE,
|
|
3906
|
+
),
|
|
3907
|
+
buildCoverageFamily(
|
|
3908
|
+
'CrudFormOperation',
|
|
3909
|
+
'packages/shared/src/modules/widgets/extension-points.ts#CRUD_FORM_OPERATIONS',
|
|
3910
|
+
CRUD_FORM_OPERATIONS,
|
|
3911
|
+
CRUD_FORM_OPERATION_COVERAGE,
|
|
3912
|
+
),
|
|
3913
|
+
]
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
/**
|
|
3917
|
+
* Real per-value counts for every ledger family, read from an extracted module's
|
|
3918
|
+
* facts. This is the anti-silent-zero oracle: a ledger claim is checked against what
|
|
3919
|
+
* the extractor actually published, never against a hand-maintained inventory row.
|
|
3920
|
+
*/
|
|
3921
|
+
export function countModuleFactCoverage(facts: ModuleFacts): Record<string, Record<string, number>> {
|
|
3922
|
+
const increment = (counts: Record<string, number>, value: string): void => {
|
|
3923
|
+
counts[value] = (counts[value] ?? 0) + 1
|
|
3924
|
+
}
|
|
3925
|
+
const factSourceKind: Record<string, number> = {}
|
|
3926
|
+
for (const entry of facts.factSources ?? []) {
|
|
3927
|
+
increment(factSourceKind, entry.kind)
|
|
3928
|
+
}
|
|
3929
|
+
const ownedContractKind: Record<string, number> = {}
|
|
3930
|
+
for (const [kind, list] of Object.entries(facts.ownedContracts ?? {})) {
|
|
3931
|
+
ownedContractKind[kind] = (list ?? []).length
|
|
3932
|
+
}
|
|
3933
|
+
const overrideDomain: Record<string, number> = {}
|
|
3934
|
+
const overrideMode: Record<string, number> = {}
|
|
3935
|
+
const overrideNote: Record<string, number> = {}
|
|
3936
|
+
for (const target of facts.overrideTargets ?? []) {
|
|
3937
|
+
overrideDomain[target.domain] = (overrideDomain[target.domain] ?? 0) + 1
|
|
3938
|
+
for (const mode of target.modes) overrideMode[mode] = (overrideMode[mode] ?? 0) + 1
|
|
3939
|
+
for (const note of target.notes ?? []) overrideNote[note] = (overrideNote[note] ?? 0) + 1
|
|
3940
|
+
}
|
|
3941
|
+
const overrideDiagnosticCode: Record<string, number> = {}
|
|
3942
|
+
for (const diagnostic of facts.overrideTargetDiagnostics ?? []) {
|
|
3943
|
+
increment(overrideDiagnosticCode, diagnostic.code)
|
|
3944
|
+
}
|
|
3945
|
+
const contributionKind: Record<string, number> = {}
|
|
3946
|
+
const activationKind: Record<string, number> = {}
|
|
3947
|
+
const hostFamily: Record<string, number> = {}
|
|
3948
|
+
const hostCapability: Record<string, number> = {}
|
|
3949
|
+
const hostActivation: Record<string, number> = {}
|
|
3950
|
+
const targetKind: Record<string, number> = {}
|
|
3951
|
+
const hostResolution: Record<string, number> = {}
|
|
3952
|
+
const targetResolution: Record<string, number> = {}
|
|
3953
|
+
const topologyResolution: Record<string, number> = {}
|
|
3954
|
+
const specializedRegistry: Record<string, number> = {}
|
|
3955
|
+
const componentMode: Record<string, number> = {}
|
|
3956
|
+
const factDiagnosticCode: Record<string, number> = {}
|
|
3957
|
+
const unresolvedReason: Record<string, number> = {}
|
|
3958
|
+
const dataTableSurface: Record<string, number> = {}
|
|
3959
|
+
const crudFormSurface: Record<string, number> = {}
|
|
3960
|
+
const crudFormLifecycle: Record<string, number> = {}
|
|
3961
|
+
const crudFormOperation: Record<string, number> = {}
|
|
3962
|
+
const surfaces = facts.extensionSurfaces
|
|
3963
|
+
for (const host of surfaces?.hosts ?? []) {
|
|
3964
|
+
increment(hostFamily, host.family)
|
|
3965
|
+
increment(hostResolution, host.resolution)
|
|
3966
|
+
increment(hostActivation, host.activation ?? 'always')
|
|
3967
|
+
for (const capability of host.capabilities) increment(hostCapability, capability)
|
|
3968
|
+
for (const phase of host.phases ?? []) increment(crudFormLifecycle, phase)
|
|
3969
|
+
for (const operation of host.operations ?? []) increment(crudFormOperation, operation)
|
|
3970
|
+
const [owner, surface] = host.key.split('.')
|
|
3971
|
+
if (owner === 'todosTable' && surface) increment(dataTableSurface, surface)
|
|
3972
|
+
if (owner === 'todoForm' && surface) increment(crudFormSurface, surface)
|
|
3973
|
+
}
|
|
3974
|
+
for (const contribution of surfaces?.contributions ?? []) {
|
|
3975
|
+
increment(contributionKind, contribution.kind)
|
|
3976
|
+
increment(hostActivation, contribution.activation ?? 'always')
|
|
3977
|
+
for (const target of contribution.targets) increment(targetResolution, target.resolution)
|
|
3978
|
+
for (const operation of contribution.operations ?? []) increment(crudFormOperation, operation)
|
|
3979
|
+
if (contribution.kind === 'specialized-registry') increment(specializedRegistry, contribution.details.registry)
|
|
3980
|
+
if (contribution.kind === 'component-override') increment(componentMode, contribution.details.mode)
|
|
3981
|
+
if (contribution.kind === 'widget') {
|
|
3982
|
+
if (contribution.details.payload === 'headless') increment(hostCapability, 'headless-widget')
|
|
3983
|
+
if (contribution.details.payload === 'menu') increment(hostCapability, 'menu-item')
|
|
3984
|
+
if (contribution.details.payload === 'dashboard' || contribution.details.payload === 'notification' || contribution.details.payload === 'integration') {
|
|
3985
|
+
increment(hostCapability, 'registry-contribution')
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
if (contribution.kind === 'api-interceptor') increment(hostCapability, 'api-interceptor')
|
|
3989
|
+
if (contribution.kind === 'command-interceptor') increment(hostCapability, 'command-interceptor')
|
|
3990
|
+
}
|
|
3991
|
+
for (const activation of surfaces?.activations ?? []) increment(activationKind, activation.kind)
|
|
3992
|
+
for (const resolution of surfaces?.contributionResolutions ?? []) {
|
|
3993
|
+
increment(targetKind, resolution.target.kind)
|
|
3994
|
+
increment(topologyResolution, resolution.resolution)
|
|
3995
|
+
for (const id of resolution.activationIds) {
|
|
3996
|
+
const kind = MODULE_EXTENSION_ACTIVATION_KINDS.find((candidate) => id.endsWith(`:${candidate}`))
|
|
3997
|
+
if (kind) increment(activationKind, kind)
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
for (const diagnostic of facts.factDiagnostics ?? []) increment(factDiagnosticCode, diagnostic.code)
|
|
4001
|
+
for (const unresolved of surfaces?.unresolved ?? []) increment(unresolvedReason, unresolved.reason)
|
|
4002
|
+
return {
|
|
4003
|
+
ModuleFactSourceKind: factSourceKind,
|
|
4004
|
+
ModuleOwnedContractKind: ownedContractKind,
|
|
4005
|
+
ModuleOverrideDomain: overrideDomain,
|
|
4006
|
+
ModuleOverrideMode: overrideMode,
|
|
4007
|
+
ModuleOverrideTargetNote: overrideNote,
|
|
4008
|
+
ModuleOverrideTargetDiagnosticCode: overrideDiagnosticCode,
|
|
4009
|
+
ModuleExtensionContributionKind: contributionKind,
|
|
4010
|
+
ModuleExtensionActivationKind: activationKind,
|
|
4011
|
+
ExtensionHostFamily: hostFamily,
|
|
4012
|
+
ExtensionHostCapability: hostCapability,
|
|
4013
|
+
ExtensionHostActivation: hostActivation,
|
|
4014
|
+
ModuleExtensionTargetKind: targetKind,
|
|
4015
|
+
ModuleExtensionHostResolution: hostResolution,
|
|
4016
|
+
ModuleExtensionTargetResolution: targetResolution,
|
|
4017
|
+
ModuleExtensionResolution: topologyResolution,
|
|
4018
|
+
SpecializedRegistry: specializedRegistry,
|
|
4019
|
+
ComponentOverrideMode: componentMode,
|
|
4020
|
+
ModuleFactDiagnosticCode: factDiagnosticCode,
|
|
4021
|
+
ModuleExtensionUnresolvedReason: unresolvedReason,
|
|
4022
|
+
DataTableExtensionSurface: dataTableSurface,
|
|
4023
|
+
CrudFormExtensionSurface: crudFormSurface,
|
|
4024
|
+
CrudFormLifecyclePhase: crudFormLifecycle,
|
|
4025
|
+
CrudFormOperation: crudFormOperation,
|
|
4026
|
+
}
|
|
2783
4027
|
}
|