@endora-commerce/cli 0.100.0
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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/bin/endora.d.ts +28 -0
- package/dist/bin/endora.d.ts.map +1 -0
- package/dist/bin/endora.js +926 -0
- package/dist/bin/endora.js.map +1 -0
- package/dist/check/estate.d.ts +189 -0
- package/dist/check/estate.d.ts.map +1 -0
- package/dist/check/estate.js +1037 -0
- package/dist/check/estate.js.map +1 -0
- package/dist/check/hosts.d.ts +25 -0
- package/dist/check/hosts.d.ts.map +1 -0
- package/dist/check/hosts.js +1347 -0
- package/dist/check/hosts.js.map +1 -0
- package/dist/check/index.d.ts +60 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +111 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/layout.d.ts +136 -0
- package/dist/check/layout.d.ts.map +1 -0
- package/dist/check/layout.js +262 -0
- package/dist/check/layout.js.map +1 -0
- package/dist/check/ledger.d.ts +98 -0
- package/dist/check/ledger.d.ts.map +1 -0
- package/dist/check/ledger.js +173 -0
- package/dist/check/ledger.js.map +1 -0
- package/dist/check/peer-owners.d.ts +116 -0
- package/dist/check/peer-owners.d.ts.map +1 -0
- package/dist/check/peer-owners.js +225 -0
- package/dist/check/peer-owners.js.map +1 -0
- package/dist/check/report.d.ts +33 -0
- package/dist/check/report.d.ts.map +1 -0
- package/dist/check/report.js +107 -0
- package/dist/check/report.js.map +1 -0
- package/dist/check/run.d.ts +147 -0
- package/dist/check/run.d.ts.map +1 -0
- package/dist/check/run.js +111 -0
- package/dist/check/run.js.map +1 -0
- package/dist/checks.d.ts +17 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +17 -0
- package/dist/checks.js.map +1 -0
- package/dist/dev/index.d.ts +83 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +298 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/generate/divergence.d.ts +38 -0
- package/dist/generate/divergence.d.ts.map +1 -0
- package/dist/generate/divergence.js +237 -0
- package/dist/generate/divergence.js.map +1 -0
- package/dist/generate/index.d.ts +90 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +369 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/declaration.d.ts +18 -0
- package/dist/inputs/declaration.d.ts.map +1 -0
- package/dist/inputs/declaration.js +64 -0
- package/dist/inputs/declaration.js.map +1 -0
- package/dist/inputs/env-file.d.ts +73 -0
- package/dist/inputs/env-file.d.ts.map +1 -0
- package/dist/inputs/env-file.js +134 -0
- package/dist/inputs/env-file.js.map +1 -0
- package/dist/inputs/prompt.d.ts +21 -0
- package/dist/inputs/prompt.d.ts.map +1 -0
- package/dist/inputs/prompt.js +59 -0
- package/dist/inputs/prompt.js.map +1 -0
- package/dist/inputs/resolve.d.ts +163 -0
- package/dist/inputs/resolve.d.ts.map +1 -0
- package/dist/inputs/resolve.js +290 -0
- package/dist/inputs/resolve.js.map +1 -0
- package/dist/install/host.d.ts +27 -0
- package/dist/install/host.d.ts.map +1 -0
- package/dist/install/host.js +90 -0
- package/dist/install/host.js.map +1 -0
- package/dist/install/index.d.ts +173 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +793 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/wizard.d.ts +144 -0
- package/dist/install/wizard.d.ts.map +1 -0
- package/dist/install/wizard.js +362 -0
- package/dist/install/wizard.js.map +1 -0
- package/dist/lib/admin-artefacts.d.ts +70 -0
- package/dist/lib/admin-artefacts.d.ts.map +1 -0
- package/dist/lib/admin-artefacts.js +354 -0
- package/dist/lib/admin-artefacts.js.map +1 -0
- package/dist/lib/admin-surfaces.d.ts +298 -0
- package/dist/lib/admin-surfaces.d.ts.map +1 -0
- package/dist/lib/admin-surfaces.js +669 -0
- package/dist/lib/admin-surfaces.js.map +1 -0
- package/dist/lib/delegated-composer.d.ts +85 -0
- package/dist/lib/delegated-composer.d.ts.map +1 -0
- package/dist/lib/delegated-composer.js +241 -0
- package/dist/lib/delegated-composer.js.map +1 -0
- package/dist/lib/divergence-artefacts.d.ts +305 -0
- package/dist/lib/divergence-artefacts.d.ts.map +1 -0
- package/dist/lib/divergence-artefacts.js +828 -0
- package/dist/lib/divergence-artefacts.js.map +1 -0
- package/dist/lib/divergence.d.ts +337 -0
- package/dist/lib/divergence.d.ts.map +1 -0
- package/dist/lib/divergence.js +1005 -0
- package/dist/lib/divergence.js.map +1 -0
- package/dist/lib/docs-artefacts.d.ts +395 -0
- package/dist/lib/docs-artefacts.d.ts.map +1 -0
- package/dist/lib/docs-artefacts.js +781 -0
- package/dist/lib/docs-artefacts.js.map +1 -0
- package/dist/lib/emitted-exports.d.ts +21 -0
- package/dist/lib/emitted-exports.d.ts.map +1 -0
- package/dist/lib/emitted-exports.js +96 -0
- package/dist/lib/emitted-exports.js.map +1 -0
- package/dist/lib/emitted-freshness.d.ts +112 -0
- package/dist/lib/emitted-freshness.d.ts.map +1 -0
- package/dist/lib/emitted-freshness.js +288 -0
- package/dist/lib/emitted-freshness.js.map +1 -0
- package/dist/lib/entity-index-artefact.d.ts +95 -0
- package/dist/lib/entity-index-artefact.d.ts.map +1 -0
- package/dist/lib/entity-index-artefact.js +210 -0
- package/dist/lib/entity-index-artefact.js.map +1 -0
- package/dist/lib/instance-build-inputs.d.ts +108 -0
- package/dist/lib/instance-build-inputs.d.ts.map +1 -0
- package/dist/lib/instance-build-inputs.js +165 -0
- package/dist/lib/instance-build-inputs.js.map +1 -0
- package/dist/lib/module-docs.d.ts +473 -0
- package/dist/lib/module-docs.d.ts.map +1 -0
- package/dist/lib/module-docs.js +711 -0
- package/dist/lib/module-docs.js.map +1 -0
- package/dist/lib/module-package-subpaths.d.ts +56 -0
- package/dist/lib/module-package-subpaths.d.ts.map +1 -0
- package/dist/lib/module-package-subpaths.js +223 -0
- package/dist/lib/module-package-subpaths.js.map +1 -0
- package/dist/lib/module-packages.d.ts +200 -0
- package/dist/lib/module-packages.d.ts.map +1 -0
- package/dist/lib/module-packages.js +580 -0
- package/dist/lib/module-packages.js.map +1 -0
- package/dist/lib/module-population.d.ts +129 -0
- package/dist/lib/module-population.d.ts.map +1 -0
- package/dist/lib/module-population.js +172 -0
- package/dist/lib/module-population.js.map +1 -0
- package/dist/lib/module-roots.d.ts +337 -0
- package/dist/lib/module-roots.d.ts.map +1 -0
- package/dist/lib/module-roots.js +586 -0
- package/dist/lib/module-roots.js.map +1 -0
- package/dist/lib/nested-checkouts.d.ts +33 -0
- package/dist/lib/nested-checkouts.d.ts.map +1 -0
- package/dist/lib/nested-checkouts.js +160 -0
- package/dist/lib/nested-checkouts.js.map +1 -0
- package/dist/lib/platform-root.d.ts +43 -0
- package/dist/lib/platform-root.d.ts.map +1 -0
- package/dist/lib/platform-root.js +134 -0
- package/dist/lib/platform-root.js.map +1 -0
- package/dist/lib/platform-surface.d.ts +235 -0
- package/dist/lib/platform-surface.d.ts.map +1 -0
- package/dist/lib/platform-surface.js +393 -0
- package/dist/lib/platform-surface.js.map +1 -0
- package/dist/lib/port-registrations.d.ts +223 -0
- package/dist/lib/port-registrations.d.ts.map +1 -0
- package/dist/lib/port-registrations.js +532 -0
- package/dist/lib/port-registrations.js.map +1 -0
- package/dist/lib/read-size.d.ts +154 -0
- package/dist/lib/read-size.d.ts.map +1 -0
- package/dist/lib/read-size.js +182 -0
- package/dist/lib/read-size.js.map +1 -0
- package/dist/lib/registration-owners.d.ts +79 -0
- package/dist/lib/registration-owners.d.ts.map +1 -0
- package/dist/lib/registration-owners.js +77 -0
- package/dist/lib/registration-owners.js.map +1 -0
- package/dist/lib/release-index.d.ts +53 -0
- package/dist/lib/release-index.d.ts.map +1 -0
- package/dist/lib/release-index.js +162 -0
- package/dist/lib/release-index.js.map +1 -0
- package/dist/lib/repeating-timers.d.ts +79 -0
- package/dist/lib/repeating-timers.d.ts.map +1 -0
- package/dist/lib/repeating-timers.js +189 -0
- package/dist/lib/repeating-timers.js.map +1 -0
- package/dist/lib/source-text.d.ts +34 -0
- package/dist/lib/source-text.d.ts.map +1 -0
- package/dist/lib/source-text.js +80 -0
- package/dist/lib/source-text.js.map +1 -0
- package/dist/lib/specifiers.d.ts +20 -0
- package/dist/lib/specifiers.d.ts.map +1 -0
- package/dist/lib/specifiers.js +130 -0
- package/dist/lib/specifiers.js.map +1 -0
- package/dist/lib/sql-tables.d.ts +166 -0
- package/dist/lib/sql-tables.d.ts.map +1 -0
- package/dist/lib/sql-tables.js +464 -0
- package/dist/lib/sql-tables.js.map +1 -0
- package/dist/lib/switchable-modules.d.ts +54 -0
- package/dist/lib/switchable-modules.d.ts.map +1 -0
- package/dist/lib/switchable-modules.js +104 -0
- package/dist/lib/switchable-modules.js.map +1 -0
- package/dist/lib/tailwind-sources.d.ts +136 -0
- package/dist/lib/tailwind-sources.d.ts.map +1 -0
- package/dist/lib/tailwind-sources.js +307 -0
- package/dist/lib/tailwind-sources.js.map +1 -0
- package/dist/lib/ui-layer.d.ts +54 -0
- package/dist/lib/ui-layer.d.ts.map +1 -0
- package/dist/lib/ui-layer.js +57 -0
- package/dist/lib/ui-layer.js.map +1 -0
- package/dist/lib/workspace-packages.d.ts +186 -0
- package/dist/lib/workspace-packages.d.ts.map +1 -0
- package/dist/lib/workspace-packages.js +351 -0
- package/dist/lib/workspace-packages.js.map +1 -0
- package/dist/new-instance/deploy.d.ts +211 -0
- package/dist/new-instance/deploy.d.ts.map +1 -0
- package/dist/new-instance/deploy.js +1381 -0
- package/dist/new-instance/deploy.js.map +1 -0
- package/dist/new-instance/docs-toolchain.d.ts +66 -0
- package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
- package/dist/new-instance/docs-toolchain.js +69 -0
- package/dist/new-instance/docs-toolchain.js.map +1 -0
- package/dist/new-instance/host.d.ts +124 -0
- package/dist/new-instance/host.d.ts.map +1 -0
- package/dist/new-instance/host.js +276 -0
- package/dist/new-instance/host.js.map +1 -0
- package/dist/new-instance/index.d.ts +118 -0
- package/dist/new-instance/index.d.ts.map +1 -0
- package/dist/new-instance/index.js +567 -0
- package/dist/new-instance/index.js.map +1 -0
- package/dist/new-instance/modules.d.ts +188 -0
- package/dist/new-instance/modules.d.ts.map +1 -0
- package/dist/new-instance/modules.js +392 -0
- package/dist/new-instance/modules.js.map +1 -0
- package/dist/new-instance/template.d.ts +505 -0
- package/dist/new-instance/template.d.ts.map +1 -0
- package/dist/new-instance/template.js +1886 -0
- package/dist/new-instance/template.js.map +1 -0
- package/dist/new-module/emit.d.ts +67 -0
- package/dist/new-module/emit.d.ts.map +1 -0
- package/dist/new-module/emit.js +1393 -0
- package/dist/new-module/emit.js.map +1 -0
- package/dist/new-module/host.d.ts +67 -0
- package/dist/new-module/host.d.ts.map +1 -0
- package/dist/new-module/host.js +224 -0
- package/dist/new-module/host.js.map +1 -0
- package/dist/new-module/index.d.ts +32 -0
- package/dist/new-module/index.d.ts.map +1 -0
- package/dist/new-module/index.js +193 -0
- package/dist/new-module/index.js.map +1 -0
- package/dist/new-module/spec.d.ts +188 -0
- package/dist/new-module/spec.d.ts.map +1 -0
- package/dist/new-module/spec.js +404 -0
- package/dist/new-module/spec.js.map +1 -0
- package/dist/new-module/text.d.ts +15 -0
- package/dist/new-module/text.d.ts.map +1 -0
- package/dist/new-module/text.js +22 -0
- package/dist/new-module/text.js.map +1 -0
- package/dist/new-storefront/dockerfile.d.ts +20 -0
- package/dist/new-storefront/dockerfile.d.ts.map +1 -0
- package/dist/new-storefront/dockerfile.js +131 -0
- package/dist/new-storefront/dockerfile.js.map +1 -0
- package/dist/new-storefront/gitignore.d.ts +23 -0
- package/dist/new-storefront/gitignore.d.ts.map +1 -0
- package/dist/new-storefront/gitignore.js +39 -0
- package/dist/new-storefront/gitignore.js.map +1 -0
- package/dist/new-storefront/index.d.ts +93 -0
- package/dist/new-storefront/index.d.ts.map +1 -0
- package/dist/new-storefront/index.js +329 -0
- package/dist/new-storefront/index.js.map +1 -0
- package/dist/new-storefront/npmrc.d.ts +98 -0
- package/dist/new-storefront/npmrc.d.ts.map +1 -0
- package/dist/new-storefront/npmrc.js +217 -0
- package/dist/new-storefront/npmrc.js.map +1 -0
- package/dist/new-storefront/reference.d.ts +189 -0
- package/dist/new-storefront/reference.d.ts.map +1 -0
- package/dist/new-storefront/reference.js +430 -0
- package/dist/new-storefront/reference.js.map +1 -0
- package/dist/new-storefront/rewrite.d.ts +171 -0
- package/dist/new-storefront/rewrite.d.ts.map +1 -0
- package/dist/new-storefront/rewrite.js +701 -0
- package/dist/new-storefront/rewrite.js.map +1 -0
- package/dist/release-index.json +284 -0
- package/dist/rules/action-route-permissions.d.ts +141 -0
- package/dist/rules/action-route-permissions.d.ts.map +1 -0
- package/dist/rules/action-route-permissions.js +556 -0
- package/dist/rules/action-route-permissions.js.map +1 -0
- package/dist/rules/bundle-pairing.d.ts +74 -0
- package/dist/rules/bundle-pairing.d.ts.map +1 -0
- package/dist/rules/bundle-pairing.js +281 -0
- package/dist/rules/bundle-pairing.js.map +1 -0
- package/dist/rules/channel-resolution.d.ts +17 -0
- package/dist/rules/channel-resolution.d.ts.map +1 -0
- package/dist/rules/channel-resolution.js +382 -0
- package/dist/rules/channel-resolution.js.map +1 -0
- package/dist/rules/command-coverage.d.ts +210 -0
- package/dist/rules/command-coverage.d.ts.map +1 -0
- package/dist/rules/command-coverage.js +714 -0
- package/dist/rules/command-coverage.js.map +1 -0
- package/dist/rules/container-imports.d.ts +60 -0
- package/dist/rules/container-imports.d.ts.map +1 -0
- package/dist/rules/container-imports.js +158 -0
- package/dist/rules/container-imports.js.map +1 -0
- package/dist/rules/default-language-prose.d.ts +212 -0
- package/dist/rules/default-language-prose.d.ts.map +1 -0
- package/dist/rules/default-language-prose.js +710 -0
- package/dist/rules/default-language-prose.js.map +1 -0
- package/dist/rules/diacritic-folds.d.ts +238 -0
- package/dist/rules/diacritic-folds.d.ts.map +1 -0
- package/dist/rules/diacritic-folds.js +681 -0
- package/dist/rules/diacritic-folds.js.map +1 -0
- package/dist/rules/entity-tenant-classification.d.ts +171 -0
- package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
- package/dist/rules/entity-tenant-classification.js +323 -0
- package/dist/rules/entity-tenant-classification.js.map +1 -0
- package/dist/rules/entry-presence.d.ts +142 -0
- package/dist/rules/entry-presence.d.ts.map +1 -0
- package/dist/rules/entry-presence.js +339 -0
- package/dist/rules/entry-presence.js.map +1 -0
- package/dist/rules/entry-scope.d.ts +91 -0
- package/dist/rules/entry-scope.d.ts.map +1 -0
- package/dist/rules/entry-scope.js +404 -0
- package/dist/rules/entry-scope.js.map +1 -0
- package/dist/rules/env-inputs.d.ts +222 -0
- package/dist/rules/env-inputs.d.ts.map +1 -0
- package/dist/rules/env-inputs.js +951 -0
- package/dist/rules/env-inputs.js.map +1 -0
- package/dist/rules/kernel-boundary.d.ts +37 -0
- package/dist/rules/kernel-boundary.d.ts.map +1 -0
- package/dist/rules/kernel-boundary.js +195 -0
- package/dist/rules/kernel-boundary.js.map +1 -0
- package/dist/rules/nul-bytes.d.ts +233 -0
- package/dist/rules/nul-bytes.d.ts.map +1 -0
- package/dist/rules/nul-bytes.js +332 -0
- package/dist/rules/nul-bytes.js.map +1 -0
- package/dist/rules/platform-surface.d.ts +479 -0
- package/dist/rules/platform-surface.d.ts.map +1 -0
- package/dist/rules/platform-surface.js +749 -0
- package/dist/rules/platform-surface.js.map +1 -0
- package/dist/rules/port-catches.d.ts +225 -0
- package/dist/rules/port-catches.d.ts.map +1 -0
- package/dist/rules/port-catches.js +1374 -0
- package/dist/rules/port-catches.js.map +1 -0
- package/dist/rules/port-shape.d.ts +213 -0
- package/dist/rules/port-shape.d.ts.map +1 -0
- package/dist/rules/port-shape.js +670 -0
- package/dist/rules/port-shape.js.map +1 -0
- package/dist/rules/queue-names.d.ts +108 -0
- package/dist/rules/queue-names.d.ts.map +1 -0
- package/dist/rules/queue-names.js +395 -0
- package/dist/rules/queue-names.js.map +1 -0
- package/dist/rules/singleton-identity.d.ts +205 -0
- package/dist/rules/singleton-identity.d.ts.map +1 -0
- package/dist/rules/singleton-identity.js +830 -0
- package/dist/rules/singleton-identity.js.map +1 -0
- package/dist/rules/subscribe-seam.d.ts +121 -0
- package/dist/rules/subscribe-seam.d.ts.map +1 -0
- package/dist/rules/subscribe-seam.js +594 -0
- package/dist/rules/subscribe-seam.js.map +1 -0
- package/dist/rules/transaction-context.d.ts +40 -0
- package/dist/rules/transaction-context.d.ts.map +1 -0
- package/dist/rules/transaction-context.js +294 -0
- package/dist/rules/transaction-context.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment divergence report — the derivation, the seam classification and
|
|
3
|
+
* the findings (`specs/107-override-report-and-ladder/contracts/divergence-report.md`).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is in the package, and not in either application
|
|
6
|
+
*
|
|
7
|
+
* The walk is the TypeScript compiler API over a deployment's own overlay tree.
|
|
8
|
+
* It was `backend/scripts/lib/divergence.ts` until
|
|
9
|
+
* `specs/110-instance-repository/` T138a, which no client can reach: a scaffolded
|
|
10
|
+
* instance has an `apps/<deployment>/` tree of its very own — that is what
|
|
11
|
+
* `instance-tree.md` §2.2 writes it for — and no way whatever to render a report
|
|
12
|
+
* over it. That is `docs-artefacts.ts`' story one artefact family over, and
|
|
13
|
+
* R3.5's answer is the same one: *"one generator, one derivation … never a
|
|
14
|
+
* second implementation"*, with the **population** as the parameter.
|
|
15
|
+
*
|
|
16
|
+
* It stayed out of `backend/src/**` for a reason that has not changed:
|
|
17
|
+
* `typescript` is a devDependency and `backend/src/**` is compiled into the
|
|
18
|
+
* production image (D-165), so an analysis imported from there would make the
|
|
19
|
+
* compiler a runtime dependency of every instance. The package has the compiler
|
|
20
|
+
* as a declared `dependency` and is a `devDependency` of the instance, which is
|
|
21
|
+
* where an analysis of a tree belongs.
|
|
22
|
+
*
|
|
23
|
+
* ## Everything in here is pure over its input, and that is what made the move a
|
|
24
|
+
* relocation
|
|
25
|
+
*
|
|
26
|
+
* {@link deriveDivergence} takes the deployment's sources, the route table, the
|
|
27
|
+
* owner map, the declaration and `ModuleContext`'s members. Not one of the five
|
|
28
|
+
* is walked for here. The two hosts differ only in how each is assembled —
|
|
29
|
+
* `backend/scripts/generate-divergence.ts` from this repository's module layout,
|
|
30
|
+
* platform sources and composition roots, `generate/divergence.ts` from the
|
|
31
|
+
* packages an instance installed — and `divergence-artefacts.ts` is where the
|
|
32
|
+
* assembly the two share lives.
|
|
33
|
+
*
|
|
34
|
+
* ## The one absolute rule
|
|
35
|
+
*
|
|
36
|
+
* Every subject is read as a **literal**: a string literal, a template literal
|
|
37
|
+
* with no substitution, an object-literal property name, or a file-local `const`
|
|
38
|
+
* bound to one of those. A subject the analysis cannot resolve is a **finding**,
|
|
39
|
+
* never a skip (FR-016, issue #113).
|
|
40
|
+
*
|
|
41
|
+
* That rule matters more here than anywhere else in the estate, and the reason
|
|
42
|
+
* is measurable: the overlay population in this repository is two modules and
|
|
43
|
+
* one decoration. A walk that silently skipped what it could not read would
|
|
44
|
+
* print `entries=1` over a deployment with fifty, and nothing else in the tree
|
|
45
|
+
* would notice.
|
|
46
|
+
*
|
|
47
|
+
* ## What it cannot see, stated here rather than discovered later
|
|
48
|
+
*
|
|
49
|
+
* - a seam call inside a helper the overlay module imports from **outside**
|
|
50
|
+
* its own directory. The population is the deployment's own overlay tree
|
|
51
|
+
* (§3.1), so a `ctx` handed to a function in `admin/` or in a core module is
|
|
52
|
+
* not read. It is also not a shape a deployment can reach: an overlay module
|
|
53
|
+
* may not name a file in another module's directory
|
|
54
|
+
* (`check:module-boundary`), and a helper of the deployment's own lives
|
|
55
|
+
* inside the overlay module;
|
|
56
|
+
* - a `ModuleContext` reached through a value the analysis cannot type — the
|
|
57
|
+
* receiver must be an identifier the file declares with the annotation
|
|
58
|
+
* `ModuleContext`, which is how every `registerModule` in the tree is
|
|
59
|
+
* written. A receiver it cannot place contributes no site, and refusal 3
|
|
60
|
+
* ("no seam call of any kind read") is what stops that from becoming a clean
|
|
61
|
+
* report over a tree full of them;
|
|
62
|
+
* - a queue name a worker takes from a value built at run time. `ctx.worker`
|
|
63
|
+
* takes a **constructed** `Worker`, so the name is read from the
|
|
64
|
+
* `new Worker('<literal>', …)` the call wraps, directly or through a
|
|
65
|
+
* file-local binding. Anything else is `computed-subject`.
|
|
66
|
+
*/
|
|
67
|
+
import { realpathSync } from 'node:fs';
|
|
68
|
+
import ts from 'typescript';
|
|
69
|
+
/**
|
|
70
|
+
* The rung table, keyed by `ModuleContext`'s own member spelling.
|
|
71
|
+
*
|
|
72
|
+
* **The population is not this map** — it is the members the platform's source
|
|
73
|
+
* declares, read by {@link moduleContextSeams}. A member this map does not name
|
|
74
|
+
* is the `unclassified-seam` finding, which is what stops the eleventh seam
|
|
75
|
+
* arriving unclassified the way `rootPlugin` did.
|
|
76
|
+
*
|
|
77
|
+
* The three `rung: null` entries are the ladder's own table's three `—` rows
|
|
78
|
+
* (`escalation-ladder.md` §2, `data-model.md` §2.3): a registration and a worker
|
|
79
|
+
* are a module contributing its own surface to the container and to the queue,
|
|
80
|
+
* and the ladder ranks ways of changing what **core** does. They are recorded
|
|
81
|
+
* because a deployment adding either is a way its tree differs from core; they
|
|
82
|
+
* carry no rung because there is no cost to core in them.
|
|
83
|
+
*/
|
|
84
|
+
export const SEAM_CLASSIFICATION = {
|
|
85
|
+
'di.register': { verdict: 'divergence', kind: 'registration', rung: null },
|
|
86
|
+
'di.providePort': { verdict: 'divergence', kind: 'port-provided', rung: 3 },
|
|
87
|
+
'di.decorate': { verdict: 'divergence', kind: 'decoration', rung: 4 },
|
|
88
|
+
subscribe: { verdict: 'divergence', kind: 'subscription', rung: 1 },
|
|
89
|
+
interceptors: { verdict: 'divergence', kind: 'interceptor', rung: 2 },
|
|
90
|
+
rootPlugin: { verdict: 'divergence', kind: 'root-plugin', rung: 4 },
|
|
91
|
+
worker: { verdict: 'divergence', kind: 'worker', rung: null },
|
|
92
|
+
routes: {
|
|
93
|
+
verdict: 'own-surface',
|
|
94
|
+
why: 'a route an overlay module registers is its own route, gated on its own module — not a change to what core serves',
|
|
95
|
+
},
|
|
96
|
+
ungatedRoutes: {
|
|
97
|
+
verdict: 'own-surface',
|
|
98
|
+
why: "the module's own route, outside the module's own gate; the reason string is required at the call and is read there, and it removes no other module's gate",
|
|
99
|
+
},
|
|
100
|
+
onBoot: {
|
|
101
|
+
verdict: 'own-surface',
|
|
102
|
+
why: "the module's own boot hook, run inside its own system scope; it contributes nothing another module resolves",
|
|
103
|
+
},
|
|
104
|
+
module: { verdict: 'not-a-seam', why: 'the module identity the context was built for' },
|
|
105
|
+
asClass: { verdict: 'not-a-seam', why: 'a registration builder — it constructs a registration, it does not register one' },
|
|
106
|
+
asFunction: { verdict: 'not-a-seam', why: 'a registration builder' },
|
|
107
|
+
asValue: { verdict: 'not-a-seam', why: 'a registration builder' },
|
|
108
|
+
cradle: {
|
|
109
|
+
verdict: 'not-a-seam',
|
|
110
|
+
why: 'the deferred resolution surface; a cross-module read through it is recorded as `port-consumed` from the `lazyPort` call that spells the name',
|
|
111
|
+
},
|
|
112
|
+
log: { verdict: 'not-a-seam', why: "the platform's logger, bound to this module" },
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The `ModuleContext` members the platform's source declares — the population
|
|
116
|
+
* the table above is held against.
|
|
117
|
+
*
|
|
118
|
+
* Read from source text so a fixture enters at the top of the analysis, and read
|
|
119
|
+
* as the interface's own members rather than as a list: `di`'s three methods are
|
|
120
|
+
* returned dotted (`di.register`), because that is how {@link calleeTailOf}
|
|
121
|
+
* spells a call on them and a check whose two halves spell one thing differently
|
|
122
|
+
* cannot reconcile them.
|
|
123
|
+
*/
|
|
124
|
+
export function moduleContextSeams(source) {
|
|
125
|
+
const sf = ts.createSourceFile('module-context.ts', source, ts.ScriptTarget.Latest, true);
|
|
126
|
+
const members = [];
|
|
127
|
+
const nameOf = (member) => member.name !== undefined && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))
|
|
128
|
+
? member.name.text
|
|
129
|
+
: null;
|
|
130
|
+
const visit = (node) => {
|
|
131
|
+
if (ts.isInterfaceDeclaration(node) && node.name.text === 'ModuleContext') {
|
|
132
|
+
for (const member of node.members) {
|
|
133
|
+
const name = nameOf(member);
|
|
134
|
+
if (name === null)
|
|
135
|
+
continue;
|
|
136
|
+
// `di` is a property whose type is an inline object literal of three
|
|
137
|
+
// **methods**, and its members are the seams — so it contributes them
|
|
138
|
+
// and never itself.
|
|
139
|
+
//
|
|
140
|
+
// The expansion is keyed on the members being callable, not on the type
|
|
141
|
+
// being a literal: `module` is also an inline object literal, of two
|
|
142
|
+
// `string` properties, and expanding it would report `ctx.module.id` and
|
|
143
|
+
// `ctx.module.version` as seams the ladder does not classify. A property
|
|
144
|
+
// is a fact the context carries; a method is something a module *does*.
|
|
145
|
+
const type = ts.isPropertySignature(member) ? member.type : undefined;
|
|
146
|
+
if (type !== undefined && ts.isTypeLiteralNode(type)) {
|
|
147
|
+
const callable = type.members.filter((inner) => ts.isMethodSignature(inner) && nameOf(inner) !== null);
|
|
148
|
+
if (callable.length > 0) {
|
|
149
|
+
for (const inner of callable)
|
|
150
|
+
members.push(`${name}.${nameOf(inner) ?? ''}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
members.push(name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
node.forEachChild(visit);
|
|
158
|
+
};
|
|
159
|
+
sf.forEachChild(visit);
|
|
160
|
+
return members;
|
|
161
|
+
}
|
|
162
|
+
/** `ctx.di.register` → `di.register`; `ctx.subscribe` → `subscribe`. */
|
|
163
|
+
export function calleeTailOf(node) {
|
|
164
|
+
const expression = node.expression;
|
|
165
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
166
|
+
return '';
|
|
167
|
+
const inner = expression.expression;
|
|
168
|
+
const prefix = ts.isPropertyAccessExpression(inner) ? `${inner.name.text}.` : '';
|
|
169
|
+
return `${prefix}${expression.name.text}`;
|
|
170
|
+
}
|
|
171
|
+
/** The identifier a seam call is written on, or `null`. */
|
|
172
|
+
function receiverOf(node) {
|
|
173
|
+
const expression = node.expression;
|
|
174
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
175
|
+
return null;
|
|
176
|
+
const inner = expression.expression;
|
|
177
|
+
if (ts.isIdentifier(inner))
|
|
178
|
+
return inner;
|
|
179
|
+
if (ts.isPropertyAccessExpression(inner) && ts.isIdentifier(inner.expression)) {
|
|
180
|
+
return inner.expression;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The identifiers this file declares as a `ModuleContext`.
|
|
186
|
+
*
|
|
187
|
+
* Two ways in, and the second is not a relaxation of the first:
|
|
188
|
+
*
|
|
189
|
+
* 1. **An annotation** — a parameter or a `const` whose type node is the
|
|
190
|
+
* identifier `ModuleContext`. That is how every overlay module in this
|
|
191
|
+
* repository is written and how a test writes one.
|
|
192
|
+
* 2. **The first parameter of an exported `registerModule`**, whatever it is
|
|
193
|
+
* named and whether or not it is annotated.
|
|
194
|
+
*
|
|
195
|
+
* ## Why the second exists, and why it is a contract rather than a heuristic
|
|
196
|
+
*
|
|
197
|
+
* `walkAnalysableSources` admits `.js`, and a `.js` file cannot carry a type
|
|
198
|
+
* annotation. So a client who writes their overlay module in JavaScript — which
|
|
199
|
+
* the loader accepts, `UNIT_EXTENSIONS` being `['.js', '.ts']`, and which is
|
|
200
|
+
* what a compiled deployment ships — got **a clean report over a tree full of
|
|
201
|
+
* decorations** (`specs/124-instance-customisation-gap/` FR-009). That is
|
|
202
|
+
* precisely the state refusal 3 exists to refuse, and nothing in an instance
|
|
203
|
+
* called it until FR-008.
|
|
204
|
+
*
|
|
205
|
+
* The `[NEEDS CLARIFICATION]` FR-009 carried was between *any single-parameter
|
|
206
|
+
* exported `registerModule`* and *a parameter named `ctx`*, to be settled by
|
|
207
|
+
* measuring the overlay corpus. Measured on 2026-09-13: the corpus is
|
|
208
|
+
* the two `backend.ts` files under `backend/src/apps/{acceptance,example}`, plus
|
|
209
|
+
* this package's own fixtures, and **every one of them writes
|
|
210
|
+
* `registerModule(ctx: ModuleContext)`** — so the measurement does not
|
|
211
|
+
* discriminate, and the answer comes from the loader instead. It is not a
|
|
212
|
+
* naming convention that makes the first parameter a context: it is
|
|
213
|
+
* `overlayModuleEntriesUnder`, which refuses a `backend.js`/`backend.ts` that
|
|
214
|
+
* exports no `registerModule` function and calls the one it finds with a
|
|
215
|
+
* `ModuleContext` and nothing else. The parameter's *name* is the client's
|
|
216
|
+
* choice and nothing enforces it; the parameter's *identity* is the platform's.
|
|
217
|
+
* Reading the name would be the heuristic.
|
|
218
|
+
*
|
|
219
|
+
* The **first** parameter rather than "the only one": the loader passes the
|
|
220
|
+
* context as argument 0, so a second parameter is a thing the platform never
|
|
221
|
+
* fills and says nothing about the first.
|
|
222
|
+
*
|
|
223
|
+
* A receiver reached any other way — a local the context was handed to, a field
|
|
224
|
+
* on a class — is still unplaceable, and refusal 3 (`no-seam-call-read`) is what
|
|
225
|
+
* keeps that from reading as "this deployment uses no seam". FR-008 is the floor
|
|
226
|
+
* under this widening, not an alternative to it.
|
|
227
|
+
*/
|
|
228
|
+
function contextBindings(sf) {
|
|
229
|
+
const names = new Set();
|
|
230
|
+
const declaresContext = (type) => type !== undefined &&
|
|
231
|
+
ts.isTypeReferenceNode(type) &&
|
|
232
|
+
ts.isIdentifier(type.typeName) &&
|
|
233
|
+
type.typeName.text === 'ModuleContext';
|
|
234
|
+
/** `export function registerModule(…)` / `export const registerModule = (…) =>`. */
|
|
235
|
+
const registerModuleParameter = (node) => {
|
|
236
|
+
const exported = (modifiers) => modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
|
|
237
|
+
if (ts.isFunctionDeclaration(node) &&
|
|
238
|
+
node.name?.text === 'registerModule' &&
|
|
239
|
+
exported(node.modifiers)) {
|
|
240
|
+
return node.parameters[0] ?? null;
|
|
241
|
+
}
|
|
242
|
+
if (ts.isVariableStatement(node) &&
|
|
243
|
+
exported(node.modifiers)) {
|
|
244
|
+
for (const declaration of node.declarationList.declarations) {
|
|
245
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'registerModule') {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const initializer = declaration.initializer;
|
|
249
|
+
if (initializer !== undefined &&
|
|
250
|
+
(ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
|
|
251
|
+
return initializer.parameters[0] ?? null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
};
|
|
257
|
+
const visit = (node) => {
|
|
258
|
+
if (ts.isParameter(node) && ts.isIdentifier(node.name) && declaresContext(node.type)) {
|
|
259
|
+
names.add(node.name.text);
|
|
260
|
+
}
|
|
261
|
+
if (ts.isVariableDeclaration(node) &&
|
|
262
|
+
ts.isIdentifier(node.name) &&
|
|
263
|
+
declaresContext(node.type)) {
|
|
264
|
+
names.add(node.name.text);
|
|
265
|
+
}
|
|
266
|
+
const parameter = registerModuleParameter(node);
|
|
267
|
+
if (parameter !== null && ts.isIdentifier(parameter.name)) {
|
|
268
|
+
names.add(parameter.name.text);
|
|
269
|
+
}
|
|
270
|
+
node.forEachChild(visit);
|
|
271
|
+
};
|
|
272
|
+
sf.forEachChild(visit);
|
|
273
|
+
return names;
|
|
274
|
+
}
|
|
275
|
+
/** File-local `const x = '<literal>'` bindings, for a subject written once above. */
|
|
276
|
+
function literalBindings(sf) {
|
|
277
|
+
const bindings = new Map();
|
|
278
|
+
const visit = (node) => {
|
|
279
|
+
if (ts.isVariableDeclaration(node) &&
|
|
280
|
+
ts.isIdentifier(node.name) &&
|
|
281
|
+
node.initializer !== undefined) {
|
|
282
|
+
const literal = plainLiteral(node.initializer);
|
|
283
|
+
if (literal !== null)
|
|
284
|
+
bindings.set(node.name.text, literal);
|
|
285
|
+
}
|
|
286
|
+
node.forEachChild(visit);
|
|
287
|
+
};
|
|
288
|
+
sf.forEachChild(visit);
|
|
289
|
+
return bindings;
|
|
290
|
+
}
|
|
291
|
+
/** A string literal or a template with no substitution; nothing else. */
|
|
292
|
+
function plainLiteral(node) {
|
|
293
|
+
if (ts.isStringLiteral(node))
|
|
294
|
+
return node.text;
|
|
295
|
+
if (ts.isNoSubstitutionTemplateLiteral(node))
|
|
296
|
+
return node.text;
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
/** A literal, or a file-local `const` that is one. Never a computation. */
|
|
300
|
+
function resolveSubject(node, bindings) {
|
|
301
|
+
if (node === undefined)
|
|
302
|
+
return null;
|
|
303
|
+
const literal = plainLiteral(node);
|
|
304
|
+
if (literal !== null)
|
|
305
|
+
return literal;
|
|
306
|
+
if (ts.isIdentifier(node))
|
|
307
|
+
return bindings.get(node.text) ?? null;
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function propertyOf(object, name) {
|
|
311
|
+
for (const property of object.properties) {
|
|
312
|
+
if (ts.isPropertyAssignment(property) &&
|
|
313
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
|
314
|
+
property.name.text === name) {
|
|
315
|
+
return property.initializer;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
/** The queue name a `new Worker('<name>', …)` names, directly or one hop back. */
|
|
321
|
+
function queueNameOf(argument, bindings, workers) {
|
|
322
|
+
if (argument === undefined)
|
|
323
|
+
return null;
|
|
324
|
+
if (ts.isNewExpression(argument)) {
|
|
325
|
+
return resolveSubject(argument.arguments?.[0], bindings);
|
|
326
|
+
}
|
|
327
|
+
if (ts.isIdentifier(argument))
|
|
328
|
+
return workers.get(argument.text) ?? null;
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
/** File-local `const w = new Worker('<name>', …)` bindings. */
|
|
332
|
+
function workerBindings(sf, bindings) {
|
|
333
|
+
const workers = new Map();
|
|
334
|
+
const visit = (node) => {
|
|
335
|
+
if (ts.isVariableDeclaration(node) &&
|
|
336
|
+
ts.isIdentifier(node.name) &&
|
|
337
|
+
node.initializer !== undefined &&
|
|
338
|
+
ts.isNewExpression(node.initializer)) {
|
|
339
|
+
const name = resolveSubject(node.initializer.arguments?.[0], bindings);
|
|
340
|
+
if (name !== null)
|
|
341
|
+
workers.set(node.name.text, name);
|
|
342
|
+
}
|
|
343
|
+
node.forEachChild(visit);
|
|
344
|
+
};
|
|
345
|
+
sf.forEachChild(visit);
|
|
346
|
+
return workers;
|
|
347
|
+
}
|
|
348
|
+
const CALL_TEXT_LIMIT = 120;
|
|
349
|
+
function callText(node, sf) {
|
|
350
|
+
const text = node.getText(sf).replace(/\s+/g, ' ');
|
|
351
|
+
return text.length > CALL_TEXT_LIMIT ? `${text.slice(0, CALL_TEXT_LIMIT)}…` : text;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Every seam call in one deployment's overlay tree.
|
|
355
|
+
*
|
|
356
|
+
* The fixture enters here as source text, which is the top of this analysis:
|
|
357
|
+
* every classification below — the receiver, the seam spelling, the literal
|
|
358
|
+
* resolution — runs over it.
|
|
359
|
+
*/
|
|
360
|
+
export function deriveSeamSites(sources) {
|
|
361
|
+
const sites = [];
|
|
362
|
+
for (const source of sources) {
|
|
363
|
+
const sf = ts.createSourceFile(source.file, source.text, ts.ScriptTarget.Latest, true);
|
|
364
|
+
const contexts = contextBindings(sf);
|
|
365
|
+
const bindings = literalBindings(sf);
|
|
366
|
+
const workers = workerBindings(sf, bindings);
|
|
367
|
+
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
368
|
+
const push = (node, seam, rest) => {
|
|
369
|
+
sites.push({
|
|
370
|
+
moduleId: source.moduleId,
|
|
371
|
+
file: source.file,
|
|
372
|
+
line: lineOf(node),
|
|
373
|
+
seam,
|
|
374
|
+
text: callText(node, sf),
|
|
375
|
+
...rest,
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
const visit = (node) => {
|
|
379
|
+
if (ts.isCallExpression(node)) {
|
|
380
|
+
// `lazyPort<T>(ctx, 'name')` — a helper over the cradle rather than a
|
|
381
|
+
// `ModuleContext` member, and the only kind that records a module
|
|
382
|
+
// *reaching* rather than *contributing*.
|
|
383
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === 'lazyPort') {
|
|
384
|
+
push(node, 'lazyPort', { subject: resolveSubject(node.arguments[1], bindings) });
|
|
385
|
+
}
|
|
386
|
+
const receiver = receiverOf(node);
|
|
387
|
+
if (receiver !== null && contexts.has(receiver.text)) {
|
|
388
|
+
const seam = calleeTailOf(node);
|
|
389
|
+
const [first, second] = node.arguments;
|
|
390
|
+
switch (seam) {
|
|
391
|
+
case 'di.register': {
|
|
392
|
+
if (first !== undefined && ts.isObjectLiteralExpression(first)) {
|
|
393
|
+
for (const property of first.properties) {
|
|
394
|
+
const name = property.name !== undefined &&
|
|
395
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
|
|
396
|
+
? property.name.text
|
|
397
|
+
: null;
|
|
398
|
+
push(property, seam, { subject: name });
|
|
399
|
+
}
|
|
400
|
+
// An empty object literal is a call that registers nothing; it
|
|
401
|
+
// is still a site, so the run's `sites` count matches the calls
|
|
402
|
+
// it examined.
|
|
403
|
+
if (first.properties.length === 0)
|
|
404
|
+
push(node, seam, { subject: null });
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
push(node, seam, { subject: null });
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case 'di.providePort':
|
|
412
|
+
case 'di.decorate':
|
|
413
|
+
case 'subscribe': {
|
|
414
|
+
push(node, seam, { subject: resolveSubject(first, bindings) });
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case 'rootPlugin': {
|
|
418
|
+
const reason = resolveSubject(first, bindings);
|
|
419
|
+
push(node, seam, {
|
|
420
|
+
subject: reason,
|
|
421
|
+
...(reason === null ? {} : { declaredReason: reason }),
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case 'worker': {
|
|
426
|
+
push(node, seam, { subject: queueNameOf(first, bindings, workers) });
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case 'interceptors': {
|
|
430
|
+
if (first !== undefined && ts.isArrayLiteralExpression(first)) {
|
|
431
|
+
for (const element of first.elements) {
|
|
432
|
+
if (!ts.isObjectLiteralExpression(element)) {
|
|
433
|
+
push(element, seam, { subject: null });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const target = resolveSubject(propertyOf(element, 'target'), bindings);
|
|
437
|
+
const phaseText = resolveSubject(propertyOf(element, 'phase'), bindings);
|
|
438
|
+
const idText = resolveSubject(propertyOf(element, 'id'), bindings);
|
|
439
|
+
const orderNode = propertyOf(element, 'order');
|
|
440
|
+
const order = orderNode !== undefined && ts.isNumericLiteral(orderNode)
|
|
441
|
+
? Number(orderNode.text)
|
|
442
|
+
: 0;
|
|
443
|
+
// `phase` and `id` are required by the registration type, so a
|
|
444
|
+
// missing one is a `tsc` error rather than this check's
|
|
445
|
+
// business; an unreadable one makes the whole entry a
|
|
446
|
+
// `computed-subject`, because its key carries the phase.
|
|
447
|
+
if (target === null || phaseText === null || idText === null) {
|
|
448
|
+
push(element, seam, { subject: null });
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
push(element, seam, {
|
|
452
|
+
subject: target,
|
|
453
|
+
interceptor: {
|
|
454
|
+
phase: phaseText === 'post' ? 'post' : 'pre',
|
|
455
|
+
order,
|
|
456
|
+
id: idText,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (first.elements.length === 0)
|
|
461
|
+
push(node, seam, { subject: null });
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
push(node, seam, { subject: null });
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
default: {
|
|
469
|
+
// `routes`, `ungatedRoutes`, `onBoot`, the builders and `cradle`
|
|
470
|
+
// are classified `own-surface` / `not-a-seam` above; they are
|
|
471
|
+
// deliberately not sites, because a site is a divergence
|
|
472
|
+
// candidate and those are not.
|
|
473
|
+
if (second !== undefined)
|
|
474
|
+
break;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
node.forEachChild(visit);
|
|
481
|
+
};
|
|
482
|
+
sf.forEachChild(visit);
|
|
483
|
+
}
|
|
484
|
+
return sites;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* "Has this population already got this file?" — the guard that keeps a file two
|
|
488
|
+
* roots both reach from entering the population twice.
|
|
489
|
+
*
|
|
490
|
+
* The environment this check builds is a **union** of several walks, and two of
|
|
491
|
+
* them legitimately overlap: a module walk root can sit *inside* the platform's
|
|
492
|
+
* source root, in which case that module's files are reached once as the
|
|
493
|
+
* module's and once as the platform's. `files` is then the size of a multiset
|
|
494
|
+
* rather than of a population, and it is the one instrument this estate has for
|
|
495
|
+
* spotting a walk that has gone blind — a number wrong for a reason nobody knows
|
|
496
|
+
* is worse than a number that is missing.
|
|
497
|
+
*
|
|
498
|
+
* Three properties, each of them the design and not a detail.
|
|
499
|
+
*
|
|
500
|
+
* - **By real path, never by the spelling.** Two roots reaching one file reach
|
|
501
|
+
* it under two path strings whenever either root is a symlink, and a string
|
|
502
|
+
* comparison would let both through — which is the case a `git worktree` and
|
|
503
|
+
* a linked `node_modules` both produce.
|
|
504
|
+
* - **First claim wins**, so the *narrower* walk's attribution survives: the
|
|
505
|
+
* passes run most-specific first, and a file inside a module is that module's
|
|
506
|
+
* however wide a root also covers it. `routeIdentities` already resolves the
|
|
507
|
+
* same contest the same way for a route it sees twice.
|
|
508
|
+
* - **It is not keyed on any package, root or name.** The next pair of roots
|
|
509
|
+
* that overlaps for some other reason is handled by this same guard, because
|
|
510
|
+
* what it knows about is a file it has already been given.
|
|
511
|
+
*
|
|
512
|
+
* A path `realPathOf` cannot resolve — a file deleted between the walk and the
|
|
513
|
+
* read — falls back to the spelling rather than throwing: this guard's job is to
|
|
514
|
+
* collapse a duplicate, and refusing a run is the caller's decision to take.
|
|
515
|
+
*/
|
|
516
|
+
export function claimFileOnce(realPathOf = (file) => realpathSync.native(file)) {
|
|
517
|
+
const claimed = new Set();
|
|
518
|
+
return (file) => {
|
|
519
|
+
// The fallback lives here rather than inside the default resolver so that it
|
|
520
|
+
// holds for an injected one too: the guarantee is the guard's, and a resolver
|
|
521
|
+
// that throws must not take out a walk that has already read the file.
|
|
522
|
+
let key;
|
|
523
|
+
try {
|
|
524
|
+
key = realPathOf(file);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
key = file;
|
|
528
|
+
}
|
|
529
|
+
if (claimed.has(key))
|
|
530
|
+
return false;
|
|
531
|
+
claimed.add(key);
|
|
532
|
+
return true;
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Every `"<METHOD> <path>"` a route registration in the composition serves, and
|
|
537
|
+
* the module that owns it.
|
|
538
|
+
*
|
|
539
|
+
* `ctx.interceptors`' target is that identity, and the registry accepts one no
|
|
540
|
+
* route matches **silently** — the interceptor simply never runs. So the report
|
|
541
|
+
* reconciles the two, which is the `unmatched-interceptor-target` finding, and
|
|
542
|
+
* the same sweep answers the entry's `owner`: an intercepted endpoint's owner is
|
|
543
|
+
* the module that registered the route, not the module that registered a
|
|
544
|
+
* container name of the same spelling.
|
|
545
|
+
*
|
|
546
|
+
* Deliberately not `check:action-route-permissions`' `findAdminRoutes`: that one
|
|
547
|
+
* filters to `/api/v1/admin/**`, and an interceptor may target any endpoint any
|
|
548
|
+
* module owns.
|
|
549
|
+
*/
|
|
550
|
+
export function routeIdentities(sources) {
|
|
551
|
+
const identities = new Map();
|
|
552
|
+
const methods = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all']);
|
|
553
|
+
const claim = (identity, moduleId) => {
|
|
554
|
+
// First registration wins, and a named owner beats an unnamed one: a route
|
|
555
|
+
// registered inside a module is that module's, whatever a later file in the
|
|
556
|
+
// application's own tree spells.
|
|
557
|
+
const known = identities.get(identity);
|
|
558
|
+
if (known === undefined || (known === null && moduleId !== null)) {
|
|
559
|
+
identities.set(identity, moduleId);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
for (const source of sources) {
|
|
563
|
+
if (!source.text.includes('/api/'))
|
|
564
|
+
continue;
|
|
565
|
+
const sf = ts.createSourceFile(source.file, source.text, ts.ScriptTarget.Latest, true);
|
|
566
|
+
const bindings = literalBindings(sf);
|
|
567
|
+
const visit = (node) => {
|
|
568
|
+
if (ts.isCallExpression(node) &&
|
|
569
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
570
|
+
methods.has(node.expression.name.text)) {
|
|
571
|
+
const path = resolveSubject(node.arguments[0], bindings);
|
|
572
|
+
if (path !== null && path.startsWith('/')) {
|
|
573
|
+
const method = node.expression.name.text.toUpperCase();
|
|
574
|
+
const verbs = method === 'ALL'
|
|
575
|
+
? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
576
|
+
: [method];
|
|
577
|
+
for (const verb of verbs)
|
|
578
|
+
claim(`${verb} ${path}`, source.moduleId);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
node.forEachChild(visit);
|
|
582
|
+
};
|
|
583
|
+
sf.forEachChild(visit);
|
|
584
|
+
}
|
|
585
|
+
return identities;
|
|
586
|
+
}
|
|
587
|
+
/** What each finding means and what to do about it. */
|
|
588
|
+
export const DIVERGENCE_REMEDIES = {
|
|
589
|
+
'computed-subject': 'A seam subject — a decoration name, an interceptor target, an event, a port, a queue or a\n' +
|
|
590
|
+
'registration key — is not a literal, so the report cannot say what this deployment changed.\n' +
|
|
591
|
+
'Reading it as "no divergence" is the direction that agrees with the defect (issue #113), so it\n' +
|
|
592
|
+
'is a finding. Write the subject as a string literal, or as a `const` in the same file.',
|
|
593
|
+
'unowned-subject': 'The name this deployment decorates or consumes is registered by no module in the composition.\n' +
|
|
594
|
+
'Composition throws for it at boot; this is the same refusal in a better place — the merge\n' +
|
|
595
|
+
'request that wrote it. Check the spelling against the owner module’s `ctx.di.register` /\n' +
|
|
596
|
+
'`ctx.di.providePort` call, or the port’s doc block, which names its container name.',
|
|
597
|
+
'unmatched-interceptor-target': 'An interceptor names an endpoint identity no route registration matches. The registry accepts\n' +
|
|
598
|
+
'it silently — the interceptor simply never runs — so nothing else in the platform would tell\n' +
|
|
599
|
+
'you. The identity is `"<METHOD> <route pattern>"`, the pattern exactly as the owner registers\n' +
|
|
600
|
+
'it (`/api/v1/orders/:id`, not `/api/v1/orders/123`).',
|
|
601
|
+
'undeclared-divergence': 'This deployment diverges from core here and its declaration says nothing about it. Add the\n' +
|
|
602
|
+
'entry’s key to `reasons` in `backend/src/apps/<deployment>/divergence.ts`, with a sentence\n' +
|
|
603
|
+
'naming what core does and what this deployment does instead — "we do not need it" is not a\n' +
|
|
604
|
+
'reason (D-101).',
|
|
605
|
+
'stale-reason': 'A reason describes a divergence the tree no longer holds. That is how a deployment silently\n' +
|
|
606
|
+
'reacquires a hazard it once declared (D-101’s own reasoning, applied to a wider subject).\n' +
|
|
607
|
+
'Delete the key, or restore the divergence it describes.',
|
|
608
|
+
'unclassified-seam': '`ModuleContext` declares a member the escalation ladder does not classify. Every seam has\n' +
|
|
609
|
+
'exactly one rung, one `own-surface` reason or one `not-a-seam` reason\n' +
|
|
610
|
+
'(`contracts/escalation-ladder.md` §3.1), and a member with none would be a way to diverge that\n' +
|
|
611
|
+
'the report cannot see. Classify it in `SEAM_CLASSIFICATION` and give the ladder its rung.',
|
|
612
|
+
'stale-decoration-order': 'A `decorationOrder` entry names a registration that no two of this deployment’s overlay\n' +
|
|
613
|
+
'modules decorate. It changes nothing today and will describe the wrong thing the next time a\n' +
|
|
614
|
+
'decoration is added — which is how a deployment silently reacquires an ambiguity it resolved.',
|
|
615
|
+
'foreign-order-member': 'A `decorationOrder` entry names a module that is not one of this deployment’s overlay modules.\n' +
|
|
616
|
+
'Only an overlay module may decorate a name it does not own (D-156.4), so there is no ordering\n' +
|
|
617
|
+
'this entry can resolve — the composer would ignore it.',
|
|
618
|
+
'incomplete-order': 'A `decorationOrder` entry names fewer modules than decorate that registration. A partial order\n' +
|
|
619
|
+
'refuses the composition at boot (`AmbiguousDecorationError`) rather than ordering it, so the\n' +
|
|
620
|
+
'entry reads as a decision and behaves as a comment.',
|
|
621
|
+
};
|
|
622
|
+
/**
|
|
623
|
+
* What the report does not cover, with a reason each (FR-018).
|
|
624
|
+
*
|
|
625
|
+
* `hostNotRecorded` is how a **host** states its own narrowing, and it is the
|
|
626
|
+
* whole answer to the question T138a had to settle: this repository derives the
|
|
627
|
+
* owner map from module *sources* and from a bridging table its composition
|
|
628
|
+
* roots carry, and a client's instance has neither. The kinds are identical —
|
|
629
|
+
* all nine are derived from the deployment's own overlay tree, which is the one
|
|
630
|
+
* population the two hosts share exactly — but the **attribution** differs, and
|
|
631
|
+
* a report that was silently narrower would be worse than no report at all,
|
|
632
|
+
* because a deployment's divergence is exactly the thing a client is asked to
|
|
633
|
+
* trust. So the sentences go in the artefact, in the field FR-018 already has
|
|
634
|
+
* for making silence readable, rather than into a release note nobody reads
|
|
635
|
+
* beside the report. This repository passes none and its six committed
|
|
636
|
+
* artefacts are byte-identical.
|
|
637
|
+
*/
|
|
638
|
+
export function divergenceBoundary(classification = SEAM_CLASSIFICATION, hostNotRecorded = []) {
|
|
639
|
+
const recorded = [
|
|
640
|
+
...new Set(Object.values(classification)
|
|
641
|
+
.filter((entry) => entry.verdict === 'divergence')
|
|
642
|
+
.map((entry) => entry.kind)),
|
|
643
|
+
];
|
|
644
|
+
// `port-consumed` is a kind with no `ModuleContext` member — it comes from
|
|
645
|
+
// `lazyPort` over the cradle — and `omission` comes from the declaration and
|
|
646
|
+
// from no seam at all. Both are recorded, so both are named here.
|
|
647
|
+
for (const kind of ['port-consumed', 'omission']) {
|
|
648
|
+
if (!recorded.includes(kind))
|
|
649
|
+
recorded.push(kind);
|
|
650
|
+
}
|
|
651
|
+
recorded.sort();
|
|
652
|
+
const notRecorded = Object.entries(classification)
|
|
653
|
+
.filter((entry) => entry[1].verdict === 'own-surface')
|
|
654
|
+
.map(([seam, entry]) => ({ seam: `ctx.${seam}`, why: entry.why }))
|
|
655
|
+
.sort((a, b) => a.seam.localeCompare(b.seam));
|
|
656
|
+
notRecorded.push({
|
|
657
|
+
seam: 'manifest.ts declarations (permissions, palette actions, i18n bundles, CLI commands)',
|
|
658
|
+
why: 'each is a module declaring its own surface, and each is already swept by the instrument that owns it — the permission inventory, the bundle-shape test, the action-route check',
|
|
659
|
+
});
|
|
660
|
+
// Appended rather than merged into the sort above: the seam entries are the
|
|
661
|
+
// classification's and are ordered by it, and a host's own narrowing is a
|
|
662
|
+
// different claim — it is about this *run*, not about `ModuleContext`.
|
|
663
|
+
notRecorded.push(...hostNotRecorded);
|
|
664
|
+
return {
|
|
665
|
+
recorded,
|
|
666
|
+
notRecorded,
|
|
667
|
+
runtimeOnly: [
|
|
668
|
+
{
|
|
669
|
+
fact: "an installed extension package's interceptors and subscriptions",
|
|
670
|
+
why: 'a package is discovered at run time and ships compiled output; what it registers is a fact about a process, not about this tree. An installed package cannot decorate at all (D-156.3), which is what keeps the highest-value seam inside this artefact',
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
fact: "the operator's activation choices",
|
|
674
|
+
why: 'presence is the conjunction of two orthogonal axes (Principle XVII), and activation is a Setting in the store. Whether a divergence recorded here is live is the running instance’s answer, and a build-time artefact gating on it would be the shape D-67/D-68 refuse',
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
fact: 'whether each decoration applied, and at what depth',
|
|
678
|
+
why: 'depth is a fact about a composition rather than about a tree: which wrap went innermost is what `decorationOrder` and the composer’s emission order decide together. `detail.depth` is `null` here rather than guessed',
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
/** `<kind>:<module>:<subject>`, with an interceptor's `#<phase>`. */
|
|
684
|
+
export function divergenceKeyOf(entry) {
|
|
685
|
+
const phase = entry.detail.kind === 'interceptor' ? `#${entry.detail.phase}` : '';
|
|
686
|
+
return `${entry.kind}:${entry.module}:${entry.subject}${phase}`;
|
|
687
|
+
}
|
|
688
|
+
const SEAM_TO_KIND = (classification, seam) => {
|
|
689
|
+
if (seam === 'lazyPort')
|
|
690
|
+
return 'port-consumed';
|
|
691
|
+
const entry = classification[seam];
|
|
692
|
+
return entry !== undefined && entry.verdict === 'divergence' ? entry.kind : null;
|
|
693
|
+
};
|
|
694
|
+
const RUNG_OF = (classification, seam) => {
|
|
695
|
+
if (seam === 'lazyPort')
|
|
696
|
+
return 3;
|
|
697
|
+
const entry = classification[seam];
|
|
698
|
+
return entry !== undefined && entry.verdict === 'divergence' ? entry.rung : null;
|
|
699
|
+
};
|
|
700
|
+
/**
|
|
701
|
+
* The whole derivation: one report, and every finding it raised producing it.
|
|
702
|
+
*
|
|
703
|
+
* Pure over its input, so a fixture deployment enters at the top of the analysis
|
|
704
|
+
* (issue #130) and the acceptance instrument (SC-008) needs no tree on disk.
|
|
705
|
+
*/
|
|
706
|
+
export function deriveDivergence(input) {
|
|
707
|
+
const classification = input.classification ?? SEAM_CLASSIFICATION;
|
|
708
|
+
const findings = [];
|
|
709
|
+
const overlayModules = [...input.overlayModules].sort();
|
|
710
|
+
const overlaySet = new Set(overlayModules);
|
|
711
|
+
const note = (kind, where, detail) => {
|
|
712
|
+
findings.push({ kind, deployment: input.deployment, where, detail });
|
|
713
|
+
};
|
|
714
|
+
// FR-031 — every `ModuleContext` member the platform declares has exactly one
|
|
715
|
+
// classification. A member with none is a way to diverge the report cannot
|
|
716
|
+
// see, which is what let `rootPlugin` arrive unclassified.
|
|
717
|
+
for (const seam of input.seams) {
|
|
718
|
+
if (classification[seam] === undefined) {
|
|
719
|
+
note('unclassified-seam', 'packages/platform/src/kernel/module-context.ts', `\`ctx.${seam}\` is a ModuleContext member the escalation ladder does not classify`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const sites = deriveSeamSites(input.sources);
|
|
723
|
+
const entries = [];
|
|
724
|
+
for (const site of sites) {
|
|
725
|
+
const kind = SEAM_TO_KIND(classification, site.seam);
|
|
726
|
+
// A site whose seam is classified `own-surface` or `not-a-seam` produces no
|
|
727
|
+
// entry by construction — `deriveSeamSites` pushes none for those — so a
|
|
728
|
+
// `null` here is an unclassified member, already reported above.
|
|
729
|
+
if (kind === null)
|
|
730
|
+
continue;
|
|
731
|
+
if (site.subject === null) {
|
|
732
|
+
note('computed-subject', `${site.file}:${site.line}`, `\`ctx.${site.seam}\` names a subject the analysis cannot resolve to a literal: ${site.text}`);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const detail = detailFor(kind, site, input.routes);
|
|
736
|
+
if (detail.kind === 'interceptor' && !detail.targetMatched) {
|
|
737
|
+
note('unmatched-interceptor-target', `${site.file}:${site.line}`, `interceptor '${detail.id}' targets '${site.subject}', which no route registration matches`);
|
|
738
|
+
}
|
|
739
|
+
// Who owns the subject is a per-kind question, because the subjects are not
|
|
740
|
+
// one namespace: a decoration and a port name a **container registration**,
|
|
741
|
+
// an interceptor names an **endpoint**, and a subscription names an
|
|
742
|
+
// **event** — for which the platform publishes no catalogue at all
|
|
743
|
+
// (`escalation-ladder.md` rung 1's stated gap), so its owner is honestly
|
|
744
|
+
// unknown rather than absent.
|
|
745
|
+
let owner = null;
|
|
746
|
+
if (kind === 'decoration' || kind === 'port-consumed') {
|
|
747
|
+
const registered = input.owners.get(site.subject);
|
|
748
|
+
if (registered === undefined && !input.rootSupplied.has(site.subject)) {
|
|
749
|
+
note('unowned-subject', `${site.file}:${site.line}`, `'${site.subject}' is registered by no module in the composition`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
owner = registered ?? null;
|
|
753
|
+
}
|
|
754
|
+
else if (kind === 'interceptor') {
|
|
755
|
+
owner = input.routes.get(site.subject) ?? null;
|
|
756
|
+
}
|
|
757
|
+
else if (kind === 'subscription') {
|
|
758
|
+
owner = null;
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
owner = input.owners.get(site.subject) ?? null;
|
|
762
|
+
}
|
|
763
|
+
const key = divergenceKeyOf({ kind, module: site.moduleId, subject: site.subject, detail });
|
|
764
|
+
entries.push({
|
|
765
|
+
key,
|
|
766
|
+
kind,
|
|
767
|
+
module: site.moduleId,
|
|
768
|
+
subject: site.subject,
|
|
769
|
+
owner,
|
|
770
|
+
rung: RUNG_OF(classification, site.seam),
|
|
771
|
+
detail,
|
|
772
|
+
reason: input.declaration.reasons[key] ?? '',
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
// The omissions, from the declaration and from no seam at all. They are the
|
|
776
|
+
// one kind the declaration supplies the population for, and it is D-101's
|
|
777
|
+
// ruling that it does: the boot refuses an omission that is not declared, so
|
|
778
|
+
// the declaration and the composed set are already two-way against each other
|
|
779
|
+
// in the place that can see the composed set.
|
|
780
|
+
for (const omission of input.declaration.omittedModules) {
|
|
781
|
+
const detail = { kind: 'omission' };
|
|
782
|
+
entries.push({
|
|
783
|
+
key: divergenceKeyOf({
|
|
784
|
+
kind: 'omission',
|
|
785
|
+
module: 'core',
|
|
786
|
+
subject: omission.moduleId,
|
|
787
|
+
detail,
|
|
788
|
+
}),
|
|
789
|
+
kind: 'omission',
|
|
790
|
+
module: 'core',
|
|
791
|
+
subject: omission.moduleId,
|
|
792
|
+
owner: omission.moduleId,
|
|
793
|
+
rung: null,
|
|
794
|
+
detail,
|
|
795
|
+
reason: omission.reason,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
799
|
+
// Where the declaration was read, as the **reader's** tree spells it.
|
|
800
|
+
//
|
|
801
|
+
// A parameter since `specs/110-instance-repository/` T138a, and it had to
|
|
802
|
+
// become one: the default below is this repository's own layout, and a client's
|
|
803
|
+
// instance holds its deployment at `apps/<deployment>/` with no `backend/src`
|
|
804
|
+
// above it. A finding naming a directory the reader does not have is SC-001's
|
|
805
|
+
// second rule broken — *"no repository-relative paths; the reader's tree is not
|
|
806
|
+
// this one"* — and it is the finding's whole job to send someone to a file.
|
|
807
|
+
// Measured on a real scaffolded instance before the parameter existed: every
|
|
808
|
+
// `undeclared-divergence` it raised named `backend/src/apps/instance/divergence.ts`.
|
|
809
|
+
const declarationPath = input.declarationPath ??
|
|
810
|
+
(input.deployment === 'core'
|
|
811
|
+
? '(no deployment)'
|
|
812
|
+
: `backend/src/apps/${input.deployment}/divergence.ts`);
|
|
813
|
+
// FR-004, both directions. An omission carries its reason inline, so it is
|
|
814
|
+
// never `undeclared-divergence`; every other kind reads the `reasons` map.
|
|
815
|
+
const derivedKeys = new Set(entries.map((entry) => entry.key));
|
|
816
|
+
for (const entry of entries) {
|
|
817
|
+
if (entry.kind === 'omission')
|
|
818
|
+
continue;
|
|
819
|
+
if (entry.reason.length === 0) {
|
|
820
|
+
note('undeclared-divergence', declarationPath, `no reason for \`${entry.key}\` — ${describeEntry(entry)}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
for (const key of Object.keys(input.declaration.reasons).sort()) {
|
|
824
|
+
if (!derivedKeys.has(key)) {
|
|
825
|
+
note('stale-reason', declarationPath, `\`${key}\` describes no divergence in this tree`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// P3's three build-time findings. The other two — an undeclared ambiguity and
|
|
829
|
+
// a declared order that disagrees with the composition — stay at boot, because
|
|
830
|
+
// an ambiguity is a correctness question and a stale entry is not
|
|
831
|
+
// (`deployment-declaration.md` §3.4).
|
|
832
|
+
const decoratorsOf = new Map();
|
|
833
|
+
for (const entry of entries) {
|
|
834
|
+
if (entry.kind !== 'decoration')
|
|
835
|
+
continue;
|
|
836
|
+
const modules = decoratorsOf.get(entry.subject) ?? new Set();
|
|
837
|
+
modules.add(entry.module);
|
|
838
|
+
decoratorsOf.set(entry.subject, modules);
|
|
839
|
+
}
|
|
840
|
+
for (const [name, declared] of Object.entries(input.declaration.decorationOrder).sort()) {
|
|
841
|
+
const applied = decoratorsOf.get(name) ?? new Set();
|
|
842
|
+
if (applied.size < 2) {
|
|
843
|
+
note('stale-decoration-order', declarationPath, `\`decorationOrder['${name}']\` orders ${applied.size} decorating module(s); an order resolves an ambiguity between two or more`);
|
|
844
|
+
}
|
|
845
|
+
for (const member of declared) {
|
|
846
|
+
if (!overlaySet.has(member)) {
|
|
847
|
+
note('foreign-order-member', declarationPath, `\`decorationOrder['${name}']\` names '${member}', which is not one of this deployment's overlay modules`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const missing = [...applied].filter((module) => !declared.includes(module)).sort();
|
|
851
|
+
if (applied.size >= 2 && missing.length > 0) {
|
|
852
|
+
note('incomplete-order', declarationPath, `\`decorationOrder['${name}']\` omits ${missing.map((id) => `'${id}'`).join(', ')}, which also decorate it`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
findings.sort((a, b) => a.kind === b.kind ? a.where.localeCompare(b.where) : a.kind.localeCompare(b.kind));
|
|
856
|
+
return {
|
|
857
|
+
report: {
|
|
858
|
+
deployment: input.deployment,
|
|
859
|
+
generatedFrom: { overlayRoot: input.overlayRoot },
|
|
860
|
+
overlayModules,
|
|
861
|
+
entries,
|
|
862
|
+
boundary: divergenceBoundary(classification, input.hostNotRecorded ?? []),
|
|
863
|
+
},
|
|
864
|
+
findings,
|
|
865
|
+
sites,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function detailFor(kind, site, routes) {
|
|
869
|
+
switch (kind) {
|
|
870
|
+
case 'decoration':
|
|
871
|
+
return { kind: 'decoration', depth: null };
|
|
872
|
+
case 'interceptor': {
|
|
873
|
+
const facts = site.interceptor ?? { phase: 'pre', order: 0, id: '' };
|
|
874
|
+
return {
|
|
875
|
+
kind: 'interceptor',
|
|
876
|
+
phase: facts.phase,
|
|
877
|
+
order: facts.order,
|
|
878
|
+
id: facts.id,
|
|
879
|
+
targetMatched: site.subject !== null && routes.has(site.subject),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
case 'root-plugin':
|
|
883
|
+
return { kind: 'root-plugin', declaredReason: site.declaredReason ?? '' };
|
|
884
|
+
case 'subscription':
|
|
885
|
+
return { kind: 'subscription' };
|
|
886
|
+
case 'port-provided':
|
|
887
|
+
return { kind: 'port-provided' };
|
|
888
|
+
case 'port-consumed':
|
|
889
|
+
return { kind: 'port-consumed' };
|
|
890
|
+
case 'registration':
|
|
891
|
+
return { kind: 'registration' };
|
|
892
|
+
case 'worker':
|
|
893
|
+
return { kind: 'worker' };
|
|
894
|
+
case 'omission':
|
|
895
|
+
return { kind: 'omission' };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/** One sentence naming the divergence, for a finding a reader can act on. */
|
|
899
|
+
export function describeEntry(entry) {
|
|
900
|
+
const owner = entry.owner === null ? 'a composition root' : `'${entry.owner}'`;
|
|
901
|
+
switch (entry.kind) {
|
|
902
|
+
case 'decoration':
|
|
903
|
+
return `'${entry.module}' wraps '${entry.subject}', which ${owner} registers`;
|
|
904
|
+
case 'interceptor':
|
|
905
|
+
return `'${entry.module}' runs ${entry.detail.kind === 'interceptor' ? entry.detail.phase : ''} on '${entry.subject}'`;
|
|
906
|
+
case 'subscription':
|
|
907
|
+
return `'${entry.module}' subscribes to '${entry.subject}'`;
|
|
908
|
+
case 'port-consumed':
|
|
909
|
+
return `'${entry.module}' resolves the port '${entry.subject}', owned by ${owner}`;
|
|
910
|
+
case 'port-provided':
|
|
911
|
+
return `'${entry.module}' publishes the port '${entry.subject}'`;
|
|
912
|
+
case 'registration':
|
|
913
|
+
return `'${entry.module}' registers '${entry.subject}'`;
|
|
914
|
+
case 'root-plugin':
|
|
915
|
+
return `'${entry.module}' mounts a plugin at the server root`;
|
|
916
|
+
case 'worker':
|
|
917
|
+
return `'${entry.module}' consumes the queue '${entry.subject}'`;
|
|
918
|
+
case 'omission':
|
|
919
|
+
return `this deployment does not ship '${entry.subject}'`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* The names one rendering makes two incompatible statements about
|
|
924
|
+
* (`specs/124-instance-customisation-gap/` FR-010).
|
|
925
|
+
*
|
|
926
|
+
* A8 of the instance acceptance criterion printed both in one line:
|
|
927
|
+
*
|
|
928
|
+
* > 1 entries: `registration:instance_acceptance_overlay:instanceAcceptanceOverlayService`;
|
|
929
|
+
* > findings: … [unowned-subject] … `'instanceAcceptanceOverlayService'` is
|
|
930
|
+
* > registered by no module in the composition
|
|
931
|
+
*
|
|
932
|
+
* The rendering knew the overlay module registered the name — it lists the
|
|
933
|
+
* registration — and then reported a decoration of that same name as owned by
|
|
934
|
+
* nobody, over a tree that had booted. Whatever produced that, it is a finding
|
|
935
|
+
* about the **run** and not about the tree, which is what makes it a refusal
|
|
936
|
+
* rather than a tenth finding kind: the remedy `unowned-subject` prints
|
|
937
|
+
* (*"Composition throws for it at boot"*) sends its reader to fix a spelling
|
|
938
|
+
* that is not wrong.
|
|
939
|
+
*
|
|
940
|
+
* Pure over the result, so both hosts get it from the same expression and a red
|
|
941
|
+
* proof enters where a real run enters.
|
|
942
|
+
*/
|
|
943
|
+
export function selfContradictingSubjects(result) {
|
|
944
|
+
const registered = new Set(result.report.entries
|
|
945
|
+
.filter((entry) => entry.kind === 'registration')
|
|
946
|
+
.map((entry) => entry.subject));
|
|
947
|
+
const contradicted = new Set();
|
|
948
|
+
for (const finding of result.findings) {
|
|
949
|
+
if (finding.kind !== 'unowned-subject')
|
|
950
|
+
continue;
|
|
951
|
+
for (const name of registered) {
|
|
952
|
+
// The detail is the finding's own sentence — `'<name>' is registered by
|
|
953
|
+
// no module in the composition` — and the quotes are what stop
|
|
954
|
+
// `blogService` matching a finding about `blogServiceCache`.
|
|
955
|
+
if (finding.detail.includes(`'${name}'`))
|
|
956
|
+
contradicted.add(name);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return [...contradicted].sort();
|
|
960
|
+
}
|
|
961
|
+
export function divergenceRefusal(input) {
|
|
962
|
+
if (input.seamsClassified <= 0) {
|
|
963
|
+
return {
|
|
964
|
+
kind: 'no-seam-classified',
|
|
965
|
+
message: 'the rung table classifies no seam, so every ModuleContext member would pass ' +
|
|
966
|
+
'unclassified and the ladder would rank nothing; refusing to report a vacuous pass',
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (input.deployments.length === 0 && input.committedDeploymentArtefacts.length > 0) {
|
|
970
|
+
return {
|
|
971
|
+
kind: 'no-deployment-with-committed-artefact',
|
|
972
|
+
message: `no deployment on disk under src/apps/, while ` +
|
|
973
|
+
`${input.committedDeploymentArtefacts.length} committed deployment report(s) name ` +
|
|
974
|
+
'one; the population this run judges is gone, not clean',
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
if (input.ownersResolved <= 0) {
|
|
978
|
+
return {
|
|
979
|
+
kind: 'no-owner-resolved',
|
|
980
|
+
message: 'the port→owner map resolved no container name anywhere in the tree, so every ' +
|
|
981
|
+
'decoration and every consumed port would read as owned by nobody — a finding about ' +
|
|
982
|
+
'the run dressed as one about the tree; refusing to report a vacuous pass',
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const contradictions = input.selfContradictingSubjects ?? [];
|
|
986
|
+
if (contradictions.length > 0) {
|
|
987
|
+
return {
|
|
988
|
+
kind: 'self-contradicting-attribution',
|
|
989
|
+
message: `one rendering lists registration(s) of ${contradictions.map((name) => `'${name}'`).join(', ')} ` +
|
|
990
|
+
'and reports the same name(s) as `unowned-subject` — a derivation contradicting itself ' +
|
|
991
|
+
'inside one file, whose remedy text ("Composition throws for it at boot") is untrue of a ' +
|
|
992
|
+
'tree that composed; refusing to report a finding about the run as one about the tree',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (input.sites <= 0 && input.overlaySpellsASeamCall) {
|
|
996
|
+
return {
|
|
997
|
+
kind: 'no-seam-call-read',
|
|
998
|
+
message: 'no seam call of any kind was read across the overlay walk, while the overlay ' +
|
|
999
|
+
'sources spell one — a resolver that stopped recognising `ctx.di.decorate` prints a ' +
|
|
1000
|
+
'clean report over a tree full of decorations; refusing to report a vacuous pass',
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
//# sourceMappingURL=divergence.js.map
|