@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,828 @@
|
|
|
1
|
+
// The deployment divergence report — the two renderings, from one derivation,
|
|
2
|
+
// and the assembly the report's two hosts share.
|
|
3
|
+
//
|
|
4
|
+
// ## Why this is in the package
|
|
5
|
+
//
|
|
6
|
+
// It was `backend/src/overlay/divergence-report.ts` until
|
|
7
|
+
// `specs/110-instance-repository/` T138a. The derivation beside it
|
|
8
|
+
// (`divergence.ts`) moved out of `backend/scripts/` in the same task and for the
|
|
9
|
+
// same reason: the report has **two hosts** — `overlay:divergence` over this
|
|
10
|
+
// repository, and `endora generate` inside a client's instance, whose
|
|
11
|
+
// `apps/<deployment>/` tree is the very thing `instance-tree.md` §2.2 writes and
|
|
12
|
+
// which nothing could render a report over. `instance-repository.md` R3.5 is
|
|
13
|
+
// that the population is a parameter and the renderer is one program, which is
|
|
14
|
+
// what `admin-artefacts.ts` and `docs-artefacts.ts` already do for the two other
|
|
15
|
+
// artefact families an instance builds from.
|
|
16
|
+
//
|
|
17
|
+
// `backend/test/unit/kernel/host-residue-partition.test.ts` had already
|
|
18
|
+
// classified the file as platform-shaped residue and named its retiring
|
|
19
|
+
// condition as *"the `overlay/` half of FR-013"* — the platform. That premise is
|
|
20
|
+
// **corrected rather than met**: the platform is a runtime dependency of every
|
|
21
|
+
// instance and a renderer has no runtime reader, while the CLI already has the
|
|
22
|
+
// compiler, is a `devDependency` and is the tool that generates an instance's
|
|
23
|
+
// other artefacts. The ledger entry retires here.
|
|
24
|
+
//
|
|
25
|
+
// **One derivation, two renders** (`data-model.md` §3). The alternative — a
|
|
26
|
+
// second derivation for the human rendering — was rejected as two answers to one
|
|
27
|
+
// question waiting to disagree; `serializeManifestModule` already had this shape
|
|
28
|
+
// and feature 100's generated module map is the precedent for the `.md`.
|
|
29
|
+
//
|
|
30
|
+
// The report supersedes the v2 override manifest, which recorded exactly one
|
|
31
|
+
// fact: which overlay modules a deployment adds. That field survives here as
|
|
32
|
+
// `overlayModules`, and it is legitimately empty for a deployment shipping no
|
|
33
|
+
// overlay module — so `overlay:check`'s `empty` verdict does not apply to either
|
|
34
|
+
// rendering. A deployment that diverges by nothing says so explicitly, which is
|
|
35
|
+
// the whole difference between an empty report and an absent one.
|
|
36
|
+
//
|
|
37
|
+
// ## The headers are parameters, on `admin-artefacts.ts`' precedent
|
|
38
|
+
//
|
|
39
|
+
// The two hosts tell a reader different things to run — `overlay:divergence`
|
|
40
|
+
// here, `endora generate` in an instance — and an artefact naming the wrong one
|
|
41
|
+
// would send a client to a script their tree does not have. The defaults are
|
|
42
|
+
// this repository's exact strings, so its six committed artefacts are
|
|
43
|
+
// byte-identical across the move.
|
|
44
|
+
//
|
|
45
|
+
// **It reaches for nothing outside itself since `specs/110-instance-repository/`
|
|
46
|
+
// T114a.** Its one application dependency was `repoRoot`, imported for exactly
|
|
47
|
+
// one consumer — `repoRelativePath`, an export with no caller anywhere in the
|
|
48
|
+
// repository. (The four apparent hits in `check-module-boundary.ts` are an
|
|
49
|
+
// unrelated *parameter* name, `(repoRelativePath: string) => boolean`.) A dead
|
|
50
|
+
// export is the cheapest possible thing to be blocked on, and leaving it
|
|
51
|
+
// standing is what made this file look blocked: `contracts/application-root-supplier.md`
|
|
52
|
+
// §5.
|
|
53
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
54
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
55
|
+
import ts from 'typescript';
|
|
56
|
+
import { claimFileOnce, deriveDivergence, moduleContextSeams, routeIdentities, } from './divergence.js';
|
|
57
|
+
import { mergeRegistrationOwners, rootSuppliedNames, } from './registration-owners.js';
|
|
58
|
+
import { providedPortNames, registeredNames, rootRegisteredNames, } from './port-registrations.js';
|
|
59
|
+
/**
|
|
60
|
+
* What each rung costs, in the words a reader who has never seen this repository
|
|
61
|
+
* needs (SC-001).
|
|
62
|
+
*
|
|
63
|
+
* The human rendering spells the **cost** rather than the number, because a
|
|
64
|
+
* number is only meaningful to somebody who has the ladder open beside them —
|
|
65
|
+
* and the reader this artefact exists for is a client's developer who has not.
|
|
66
|
+
* The full ladder is `docs/docs/architecture/customisation-ladder.md`; these are
|
|
67
|
+
* its one-line summaries and they are kept in step by
|
|
68
|
+
* `test/unit/overlay/divergence-render.test.ts`.
|
|
69
|
+
*/
|
|
70
|
+
export const RUNG_COSTS = {
|
|
71
|
+
0: 'rung 0 — configuration. Costs nothing and survives every upgrade, because it is data.',
|
|
72
|
+
1: 'rung 1 — subscribe to what the owner already emits. Costs nothing at the seam; you observe, you do not change what the owner did.',
|
|
73
|
+
2: 'rung 2 — run before or after what the owner already serves. Costs a coupling to an endpoint identity, which is versioned public API.',
|
|
74
|
+
3: 'rung 3 — a strategy port the owner published. Costs a dependency edge in your manifest; the owner keeps the seam and keeps fixing it behind you.',
|
|
75
|
+
4: 'rung 4 — change what the container hands out. Costs a contract-version coupling to a shape nothing checks for you: `ctx.di.decorate<T>` asserts `T` at the call site and compares it to nothing.',
|
|
76
|
+
5: 'rung 5 — fork. Costs everything, permanently.',
|
|
77
|
+
};
|
|
78
|
+
/** The heading each kind gets in the human rendering. */
|
|
79
|
+
const KIND_HEADINGS = {
|
|
80
|
+
omission: 'Modules this deployment does not ship',
|
|
81
|
+
registration: 'Services this deployment adds to the container',
|
|
82
|
+
'port-provided': 'Ports this deployment publishes',
|
|
83
|
+
'port-consumed': 'Ports this deployment consumes',
|
|
84
|
+
subscription: 'Events this deployment subscribes to',
|
|
85
|
+
interceptor: 'Endpoints this deployment intercepts',
|
|
86
|
+
decoration: 'Registrations this deployment wraps',
|
|
87
|
+
'root-plugin': 'Plugins this deployment mounts at the server root',
|
|
88
|
+
worker: 'Queues this deployment consumes',
|
|
89
|
+
};
|
|
90
|
+
/** Stable order for the human rendering — most invasive last is not the point;
|
|
91
|
+
* a reader wants the same order every time, so it is the ladder's own. */
|
|
92
|
+
const KIND_ORDER = [
|
|
93
|
+
'omission',
|
|
94
|
+
'subscription',
|
|
95
|
+
'interceptor',
|
|
96
|
+
'port-provided',
|
|
97
|
+
'port-consumed',
|
|
98
|
+
'decoration',
|
|
99
|
+
'root-plugin',
|
|
100
|
+
'registration',
|
|
101
|
+
'worker',
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* The "do not edit" headers this repository's own generator writes.
|
|
105
|
+
*
|
|
106
|
+
* Parameters rather than literals, on `admin-artefacts.ts`' and
|
|
107
|
+
* `docs-artefacts.ts`' precedent: the two hosts tell a reader different things
|
|
108
|
+
* to run, and an artefact naming the wrong one would send a client to a script
|
|
109
|
+
* their tree does not have. The default is this repository's, so its committed
|
|
110
|
+
* artefacts are byte-identical across the move and a host that means the other
|
|
111
|
+
* one says so.
|
|
112
|
+
*/
|
|
113
|
+
export const COMPOSER_DIVERGENCE_MODULE_HEADER = `// AUTO-GENERATED by scripts/generate-divergence.ts — DO NOT EDIT.\n` +
|
|
114
|
+
`// Run \`pnpm --filter backend run overlay:divergence\` (or rebuild) to refresh.\n` +
|
|
115
|
+
`// Deterministic record of this deployment's divergence from core.\n`;
|
|
116
|
+
/** The same, for the human rendering, whose comment syntax is HTML's. */
|
|
117
|
+
export const COMPOSER_DIVERGENCE_MARKDOWN_HEADER = [
|
|
118
|
+
'<!-- AUTO-GENERATED by scripts/generate-divergence.ts \u2014 DO NOT EDIT. -->',
|
|
119
|
+
'<!-- Run `pnpm --filter backend run overlay:divergence` (or rebuild) to refresh. -->',
|
|
120
|
+
];
|
|
121
|
+
/**
|
|
122
|
+
* Emit the report as a committed, typed TS module (mirrors
|
|
123
|
+
* `manifest-index.generated.ts`).
|
|
124
|
+
*
|
|
125
|
+
* `typesImportSpecifier` is the module-resolvable path from the emitted file's
|
|
126
|
+
* directory to `src/overlay/types.js` — it differs between the core output
|
|
127
|
+
* (`./types.js`) and a deployment output (`../../overlay/types.js`), so the
|
|
128
|
+
* caller computes and passes it.
|
|
129
|
+
*/
|
|
130
|
+
export function serializeDivergenceModule(report, typesImportSpecifier, header = COMPOSER_DIVERGENCE_MODULE_HEADER) {
|
|
131
|
+
const body = JSON.stringify(report, null, 2);
|
|
132
|
+
return `${header}
|
|
133
|
+
import type { DivergenceReport } from '${typesImportSpecifier}';
|
|
134
|
+
|
|
135
|
+
export const DIVERGENCE_REPORT: DivergenceReport = ${body};
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
/** Stable JSON serialization, for a caller that wants the shape and no module. */
|
|
139
|
+
export function serializeDivergenceJson(report) {
|
|
140
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
141
|
+
}
|
|
142
|
+
function escapeCell(text) {
|
|
143
|
+
return text.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
144
|
+
}
|
|
145
|
+
function ownerCell(entry) {
|
|
146
|
+
if (entry.kind === 'omission')
|
|
147
|
+
return '—';
|
|
148
|
+
return entry.owner === null ? 'a composition root (no module owns it)' : `\`${entry.owner}\``;
|
|
149
|
+
}
|
|
150
|
+
function subjectCell(entry) {
|
|
151
|
+
if (entry.kind === 'interceptor' && entry.detail.kind === 'interceptor') {
|
|
152
|
+
return `\`${entry.subject}\` (${entry.detail.phase})`;
|
|
153
|
+
}
|
|
154
|
+
return `\`${entry.subject}\``;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The human rendering — D-30's own word for this artefact is *report*.
|
|
158
|
+
*
|
|
159
|
+
* Three rules, all of them SC-001's: no key strings (a reader is not looking
|
|
160
|
+
* anything up), no repository-relative paths (the reader's tree is not this
|
|
161
|
+
* one), and the rung's **cost** spelled out rather than its number.
|
|
162
|
+
*/
|
|
163
|
+
export function renderDivergenceMarkdown(report, header = COMPOSER_DIVERGENCE_MARKDOWN_HEADER) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
const isCore = report.deployment === 'core';
|
|
166
|
+
lines.push(...header);
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(isCore
|
|
169
|
+
? '# Divergence from core: bare core'
|
|
170
|
+
: `# Divergence from core: \`${report.deployment}\``);
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push('Every way this deployment differs from the platform it was built on, derived from its own', 'tree. It is the first thing to consult when an upgrade changes behaviour you depended on.', '', 'Each entry names **what** was changed, **which of this deployment’s modules** changed it,', '**which module owns** the thing that was changed, what the change **costs**, and the', 'sentence this deployment wrote when it made the change.', '');
|
|
173
|
+
if (isCore) {
|
|
174
|
+
lines.push('This is the bare-core build: no deployment is selected, so there is nothing to diverge', 'from core *with*. It is generated and committed anyway, because an absent report and a', 'report that says "nothing" are indistinguishable, and only one of them is a statement.', '');
|
|
175
|
+
}
|
|
176
|
+
lines.push('## What this deployment adds');
|
|
177
|
+
lines.push('');
|
|
178
|
+
if (report.overlayModules.length === 0) {
|
|
179
|
+
lines.push('No overlay modules. This deployment ships the platform’s own module set.');
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
lines.push('| Overlay module |');
|
|
183
|
+
lines.push('| --- |');
|
|
184
|
+
for (const moduleId of report.overlayModules)
|
|
185
|
+
lines.push(`| \`${moduleId}\` |`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push('## What this deployment changes');
|
|
189
|
+
lines.push('');
|
|
190
|
+
if (report.entries.length === 0) {
|
|
191
|
+
lines.push('Nothing. This deployment changes no registration, intercepts no endpoint, subscribes to no', 'event, publishes and consumes no port, mounts no root plugin and omits no module.', '', 'That is a statement rather than an absence: the derivation ran over this deployment’s own', 'tree and found no divergence of any recorded kind.');
|
|
192
|
+
lines.push('');
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
for (const kind of KIND_ORDER) {
|
|
196
|
+
const entries = report.entries.filter((entry) => entry.kind === kind);
|
|
197
|
+
if (entries.length === 0)
|
|
198
|
+
continue;
|
|
199
|
+
lines.push(`### ${KIND_HEADINGS[kind]}`);
|
|
200
|
+
lines.push('');
|
|
201
|
+
const rungs = [...new Set(entries.map((entry) => entry.rung))].filter((rung) => rung !== null);
|
|
202
|
+
for (const rung of rungs.sort((a, b) => a - b)) {
|
|
203
|
+
const cost = RUNG_COSTS[rung];
|
|
204
|
+
if (cost !== undefined)
|
|
205
|
+
lines.push(`${cost}`, '');
|
|
206
|
+
}
|
|
207
|
+
lines.push('| What | Changed by | Owned by | Why |');
|
|
208
|
+
lines.push('| --- | --- | --- | --- |');
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const changedBy = entry.kind === 'omission' ? '—' : `\`${entry.module}\``;
|
|
211
|
+
lines.push(`| ${escapeCell(subjectCell(entry))} | ${changedBy} | ${escapeCell(ownerCell(entry))} | ` +
|
|
212
|
+
`${escapeCell(entry.reason.length === 0 ? '**not declared**' : entry.reason)} |`);
|
|
213
|
+
}
|
|
214
|
+
lines.push('');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push('## What this report does not cover');
|
|
218
|
+
lines.push('');
|
|
219
|
+
lines.push('A report that lists what it records and says nothing about the rest is indistinguishable', 'from a complete one. These are the seams that exist and are deliberately not divergences,', 'and the facts only a running instance can answer.', '');
|
|
220
|
+
lines.push('### Seams that are a module’s own business');
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push('| Seam | Why it is not a divergence |');
|
|
223
|
+
lines.push('| --- | --- |');
|
|
224
|
+
for (const entry of report.boundary.notRecorded) {
|
|
225
|
+
lines.push(`| \`${escapeCell(entry.seam)}\` | ${escapeCell(entry.why)} |`);
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
lines.push('### Facts only a running instance knows');
|
|
229
|
+
lines.push('');
|
|
230
|
+
lines.push('| Fact | Why it is not here |');
|
|
231
|
+
lines.push('| --- | --- |');
|
|
232
|
+
for (const entry of report.boundary.runtimeOnly) {
|
|
233
|
+
lines.push(`| ${escapeCell(entry.fact)} | ${escapeCell(entry.why)} |`);
|
|
234
|
+
}
|
|
235
|
+
lines.push('');
|
|
236
|
+
return `${lines.join('\n')}\n`;
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// The assembly both hosts share
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
/**
|
|
242
|
+
* Every TypeScript or emitted-JavaScript source under a root, recursively.
|
|
243
|
+
*
|
|
244
|
+
* One walk rather than one per host, because the two hosts read two *different
|
|
245
|
+
* dialects of the same tree* — this repository's modules are sources and an
|
|
246
|
+
* instance's are the `dist` those sources compile to — and the analysis is the
|
|
247
|
+
* compiler API either way (`registeredNames` and `routeIdentities` both parse
|
|
248
|
+
* with `ts.createSourceFile`, which reads emitted JavaScript as readily as it
|
|
249
|
+
* reads TypeScript). Measured on `packages/modules/blog`: the six container
|
|
250
|
+
* names and the twenty-nine route identities its sources yield are the same six
|
|
251
|
+
* and the same twenty-nine its `dist/backend` yields.
|
|
252
|
+
*
|
|
253
|
+
* `.d.ts` is excluded here and asked for separately by {@link seamsFromKernel},
|
|
254
|
+
* which is the one input whose instance answer is a declaration file.
|
|
255
|
+
*/
|
|
256
|
+
export function walkAnalysableSources(root, out = []) {
|
|
257
|
+
let entries;
|
|
258
|
+
try {
|
|
259
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
const full = join(root, entry.name);
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
268
|
+
continue;
|
|
269
|
+
walkAnalysableSources(full, out);
|
|
270
|
+
}
|
|
271
|
+
else if (/\.(?:[cm]?js|ts)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
272
|
+
out.push(full);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A deployment's own overlay sources, attributed to the overlay module that
|
|
279
|
+
* owns them.
|
|
280
|
+
*
|
|
281
|
+
* `base` is the root the reported path is relative to — this repository's root
|
|
282
|
+
* here, the instance's root there — so an entry names a path in the reader's own
|
|
283
|
+
* tree and never in ours (SC-001's second rule).
|
|
284
|
+
*/
|
|
285
|
+
export function overlaySourcesUnder(overlayRoot, moduleIds, base) {
|
|
286
|
+
if (overlayRoot === null)
|
|
287
|
+
return [];
|
|
288
|
+
const sources = [];
|
|
289
|
+
for (const moduleId of moduleIds) {
|
|
290
|
+
for (const file of walkAnalysableSources(join(overlayRoot, moduleId))) {
|
|
291
|
+
sources.push({
|
|
292
|
+
moduleId,
|
|
293
|
+
file: relative(base, file).split(sep).join('/'),
|
|
294
|
+
text: readFileSync(file, 'utf8'),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return sources;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* One derivation, N renderings (`data-model.md` §3).
|
|
302
|
+
*
|
|
303
|
+
* The loop is here rather than in either host for the reason the file header
|
|
304
|
+
* gives: a second derivation for a second rendering is two answers to one
|
|
305
|
+
* question waiting to disagree, and the moment there were three renderings
|
|
306
|
+
* across two hosts that stopped being hypothetical.
|
|
307
|
+
*/
|
|
308
|
+
export function renderDivergenceArtefacts(input, specs) {
|
|
309
|
+
const result = deriveDivergence(input);
|
|
310
|
+
const renderings = specs.map((spec) => {
|
|
311
|
+
switch (spec.rendering) {
|
|
312
|
+
case 'module':
|
|
313
|
+
return {
|
|
314
|
+
outputPath: spec.outputPath,
|
|
315
|
+
content: serializeDivergenceModule(result.report, spec.typesImportSpecifier, spec.header ?? COMPOSER_DIVERGENCE_MODULE_HEADER),
|
|
316
|
+
label: spec.label,
|
|
317
|
+
};
|
|
318
|
+
case 'markdown':
|
|
319
|
+
return {
|
|
320
|
+
outputPath: spec.outputPath,
|
|
321
|
+
content: renderDivergenceMarkdown(result.report, spec.header ?? COMPOSER_DIVERGENCE_MARKDOWN_HEADER),
|
|
322
|
+
label: spec.label,
|
|
323
|
+
};
|
|
324
|
+
case 'json':
|
|
325
|
+
return {
|
|
326
|
+
outputPath: spec.outputPath,
|
|
327
|
+
content: serializeDivergenceJson(result.report),
|
|
328
|
+
label: spec.label,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
return { result, renderings };
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Does the overlay tree **spell** a seam call at all?
|
|
336
|
+
*
|
|
337
|
+
* A second author for the "no seam call of any kind read" refusal
|
|
338
|
+
* (`divergence-report.md` §5, refusal 3), and the reason it is a text probe
|
|
339
|
+
* rather than a second walk: what has to be caught is the *syntax walk* going
|
|
340
|
+
* blind, and a second syntax walk would go blind with it. With two overlay
|
|
341
|
+
* modules in this repository, a resolver that stopped recognising
|
|
342
|
+
* `ctx.di.decorate` prints a clean report over a tree full of decorations, and
|
|
343
|
+
* `sites=0` is indistinguishable from a deployment that only registers routes —
|
|
344
|
+
* which is a legal thing for an overlay module to do. This tells the two apart.
|
|
345
|
+
*/
|
|
346
|
+
export function overlayTreeSpellsASeamCall(sources) {
|
|
347
|
+
const spellings = [
|
|
348
|
+
'di.register(',
|
|
349
|
+
'di.providePort(',
|
|
350
|
+
'di.decorate(',
|
|
351
|
+
'.subscribe(',
|
|
352
|
+
'.interceptors(',
|
|
353
|
+
'.rootPlugin(',
|
|
354
|
+
'.worker(',
|
|
355
|
+
'lazyPort(',
|
|
356
|
+
'lazyPort<',
|
|
357
|
+
];
|
|
358
|
+
return sources.some((source) => spellings.some((spelling) => source.text.includes(spelling)));
|
|
359
|
+
}
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// The instance host's half: a composition made of installed packages
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
/**
|
|
364
|
+
* What an instance's report cannot derive, each with a reason (FR-018).
|
|
365
|
+
*
|
|
366
|
+
* **Every one of the nine kinds is derived in an instance exactly as it is
|
|
367
|
+
* here**, because the population of all nine is the deployment's own overlay
|
|
368
|
+
* tree — `apps/<deployment>/modules/**` — which is the client's own source and
|
|
369
|
+
* is read by the same walk. What differs is **attribution**, in one place, and
|
|
370
|
+
* that one place is written down here rather than left to be discovered by a
|
|
371
|
+
* client reading a report with a silent hole in it.
|
|
372
|
+
*
|
|
373
|
+
* `HOST_REGISTERED_PORTS` is the bridging table `check:port-dependencies`
|
|
374
|
+
* carries: names this repository's composition roots register *on an unconverted
|
|
375
|
+
* module's behalf*, each with its own retiring condition. It is a judgement about
|
|
376
|
+
* this repository's roots and nothing derives it, so it has no instance
|
|
377
|
+
* equivalent — in an instance those names are registered by `composeApp` inside
|
|
378
|
+
* `@endora-commerce/platform`, the walk finds them there, and they come out as
|
|
379
|
+
* root-supplied. That answer is *true of the instance*; it is simply less
|
|
380
|
+
* specific than ours, and a client who did not know that would read `a
|
|
381
|
+
* composition root (no module owns it)` and conclude nothing owns it.
|
|
382
|
+
*/
|
|
383
|
+
export const INSTANCE_BOUNDARY_NOTES = [
|
|
384
|
+
{
|
|
385
|
+
seam: 'the module behind a container name the platform registers on its behalf',
|
|
386
|
+
why: "such a name is reported as a composition root's rather than as that module's. The " +
|
|
387
|
+
'mapping is a judgement about a composition root, not a fact a walk produces, and this ' +
|
|
388
|
+
'instance has no composition root of its own — `composeApp` is the platform’s. Every ' +
|
|
389
|
+
'name a module registers for itself is attributed to that module — this deployment’s ' +
|
|
390
|
+
'own overlay modules included, from their own sources — which is every name ' +
|
|
391
|
+
'an overlay module is likely to decorate',
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
seam: 'what an installed module package would register if it shipped its sources',
|
|
395
|
+
why: 'container names and route identities are read from each package’s published `./backend` ' +
|
|
396
|
+
'artefact, which is the module version this instance installed. That is the composition ' +
|
|
397
|
+
'this deployment actually runs; it is not the module’s source tree, and a name reachable ' +
|
|
398
|
+
'only from a layer the package does not publish is in no owner map',
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
/** The absolute path a package's declared `exports` subpath resolves to. */
|
|
402
|
+
function subpathTargetOf(pkg, subpath) {
|
|
403
|
+
const target = pkg.exports.get(subpath);
|
|
404
|
+
if (target === undefined)
|
|
405
|
+
return null;
|
|
406
|
+
return join(pkg.dir, target.replace(/^\.\//, ''));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* `ModuleContext`'s members, out of whatever file beside the platform's
|
|
410
|
+
* `./kernel` entry point declares the interface.
|
|
411
|
+
*
|
|
412
|
+
* A **search** rather than a spelled path, and deliberately: the entry point is
|
|
413
|
+
* a barrel and re-exports the interface rather than declaring it, so the
|
|
414
|
+
* declaration is in a sibling — and which sibling is the platform's own business
|
|
415
|
+
* and its build layout's, neither of which this file may assert (D-100). The
|
|
416
|
+
* interface *name* is the one thing that is contract here, and it is already
|
|
417
|
+
* spelled exactly once, in `moduleContextSeams`.
|
|
418
|
+
*/
|
|
419
|
+
export function seamsFromKernel(kernelEntry) {
|
|
420
|
+
const directory = dirname(kernelEntry);
|
|
421
|
+
let names;
|
|
422
|
+
try {
|
|
423
|
+
names = readdirSync(directory).sort();
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
for (const name of names) {
|
|
429
|
+
if (!/\.ts$/.test(name))
|
|
430
|
+
continue;
|
|
431
|
+
const full = join(directory, name);
|
|
432
|
+
try {
|
|
433
|
+
if (!statSync(full).isFile())
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const seams = moduleContextSeams(readFileSync(full, 'utf8'));
|
|
440
|
+
if (seams.length > 0)
|
|
441
|
+
return seams;
|
|
442
|
+
}
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The composition an instance runs, read out of what it installed.
|
|
447
|
+
*
|
|
448
|
+
* Three of the four facts come from the same two walks: each module package's
|
|
449
|
+
* published `./backend` layer, and the platform's own. The fourth —
|
|
450
|
+
* `ModuleContext`'s members — comes from the platform's shipped kernel
|
|
451
|
+
* declarations, which yield byte-identically what this repository's source does
|
|
452
|
+
* (measured: the same sixteen members, in the same order).
|
|
453
|
+
*
|
|
454
|
+
* A package that publishes no `./backend` subpath is an admin- or
|
|
455
|
+
* storefront-only module: it registers nothing and serves nothing, so it is
|
|
456
|
+
* *readable and empty* rather than unreadable, exactly as
|
|
457
|
+
* `package-declarations.ts` classifies the same state. A package that publishes
|
|
458
|
+
* one whose target the walk cannot open is **named**, because an unread package
|
|
459
|
+
* is a package whose registrations are in no owner map and every decoration of
|
|
460
|
+
* one of its names would read `unowned-subject` — a finding about the run
|
|
461
|
+
* dressed as a finding about the tree.
|
|
462
|
+
*/
|
|
463
|
+
export function instanceComposition(input) {
|
|
464
|
+
const claims = [];
|
|
465
|
+
const platformNames = new Set();
|
|
466
|
+
const routeSources = [];
|
|
467
|
+
const unreadable = [];
|
|
468
|
+
const claimFile = claimFileOnce();
|
|
469
|
+
let filesRead = 0;
|
|
470
|
+
let covered = 0;
|
|
471
|
+
let expected = 0;
|
|
472
|
+
for (const pkg of input.packages) {
|
|
473
|
+
const backend = subpathTargetOf(pkg, './backend');
|
|
474
|
+
if (backend === null)
|
|
475
|
+
continue;
|
|
476
|
+
expected += 1;
|
|
477
|
+
const files = walkAnalysableSources(dirname(backend));
|
|
478
|
+
if (files.length === 0) {
|
|
479
|
+
unreadable.push({
|
|
480
|
+
packageName: pkg.name,
|
|
481
|
+
at: backend,
|
|
482
|
+
reason: 'it publishes a "./backend" subpath beside which the walk found no source at all, so ' +
|
|
483
|
+
'the container names and the routes it owns are in no map',
|
|
484
|
+
});
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
covered += 1;
|
|
488
|
+
for (const file of files) {
|
|
489
|
+
const text = readFileSync(file, 'utf8');
|
|
490
|
+
filesRead += 1;
|
|
491
|
+
if (claimFile(file))
|
|
492
|
+
routeSources.push({ file, text, moduleId: pkg.moduleId });
|
|
493
|
+
for (const name of registeredNames(text, file))
|
|
494
|
+
claims.push({ name, moduleId: pkg.moduleId });
|
|
495
|
+
for (const name of providedPortNames(text, file)) {
|
|
496
|
+
claims.push({ name, moduleId: pkg.moduleId });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
let seams = [];
|
|
501
|
+
if (input.platform !== null) {
|
|
502
|
+
for (const file of walkAnalysableSources(input.platform.dir)) {
|
|
503
|
+
const text = readFileSync(file, 'utf8');
|
|
504
|
+
filesRead += 1;
|
|
505
|
+
if (claimFile(file))
|
|
506
|
+
routeSources.push({ file, text, moduleId: null });
|
|
507
|
+
for (const name of registeredNames(text, file))
|
|
508
|
+
platformNames.add(name);
|
|
509
|
+
for (const name of providedPortNames(text, file))
|
|
510
|
+
platformNames.add(name);
|
|
511
|
+
// The **root** spelling as well, and it is not an optimisation: in an
|
|
512
|
+
// instance `composeApp` is the composition root and it writes
|
|
513
|
+
// `registerValues(container, { … })`, which `registeredNames` does not
|
|
514
|
+
// read. Measured on a real scaffolded instance with only the two module
|
|
515
|
+
// spellings: an overlay module decorating `commandBus` was reported
|
|
516
|
+
// `unowned-subject`, which is the exact state `rootSuppliedNames`' own doc
|
|
517
|
+
// block says it exists to prevent — a finding about the run dressed as one
|
|
518
|
+
// about the tree.
|
|
519
|
+
for (const name of rootRegisteredNames(text, file))
|
|
520
|
+
platformNames.add(name);
|
|
521
|
+
}
|
|
522
|
+
const kernel = subpathTargetOf(input.platform, './kernel');
|
|
523
|
+
if (kernel !== null)
|
|
524
|
+
seams = seamsFromKernel(kernel);
|
|
525
|
+
}
|
|
526
|
+
// `hostRegistered` is empty on purpose and the emptiness is the narrowing
|
|
527
|
+
// `INSTANCE_BOUNDARY_NOTES` states: the bridging table is a judgement about
|
|
528
|
+
// *this repository's* composition roots, and an instance has none of its own.
|
|
529
|
+
const owners = mergeRegistrationOwners({
|
|
530
|
+
hostRegistered: {},
|
|
531
|
+
treeClaims: [],
|
|
532
|
+
packageClaims: claims,
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
environment: {
|
|
536
|
+
owners,
|
|
537
|
+
// The platform's own registrations are both halves at once here: it is the
|
|
538
|
+
// kernel *and* the composition root an instance runs, `composeApp` being
|
|
539
|
+
// the platform's. `rootSuppliedNames` drops every name a module claimed,
|
|
540
|
+
// so a module that registers a name the platform also spells keeps it.
|
|
541
|
+
rootSupplied: rootSuppliedNames({ rootNames: [], kernelNames: platformNames, owners }),
|
|
542
|
+
routes: routeIdentities(routeSources),
|
|
543
|
+
seams,
|
|
544
|
+
filesRead,
|
|
545
|
+
},
|
|
546
|
+
unreadable,
|
|
547
|
+
covered,
|
|
548
|
+
expected,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The scan's environment, with the **deployment's own** overlay registrations
|
|
553
|
+
* merged into the owner map (`specs/124-instance-customisation-gap/` FR-005,
|
|
554
|
+
* FR-006, FR-007).
|
|
555
|
+
*
|
|
556
|
+
* ## Why it is a second step rather than a fourth input to `instanceComposition`
|
|
557
|
+
*
|
|
558
|
+
* That function is documented as *"everything one run needs that does not change
|
|
559
|
+
* between deployments"*, and the overlay claim set is **per deployment** — a
|
|
560
|
+
* report is rendered once per directory under `apps/`. The package half is where
|
|
561
|
+
* the scan's cost is (one platform walk plus one per installed package, against
|
|
562
|
+
* one small overlay tree), so hoisting it and merging here is the shape that
|
|
563
|
+
* repairs the defect without making the expensive half run once per deployment.
|
|
564
|
+
*
|
|
565
|
+
* ## Why it is a conformance repair and not a widening
|
|
566
|
+
*
|
|
567
|
+
* `contracts/divergence-report.md` §3.2 specifies the owner map as *"module
|
|
568
|
+
* sources plus each installed package's `./backend` artefact"*, and this
|
|
569
|
+
* repository's own host already obeys it: `moduleWalkRoots` contains
|
|
570
|
+
* `overlayRoot`. The instance host was the one out of conformance — it built
|
|
571
|
+
* `owners` from installed packages and `rootSupplied` from the platform walk,
|
|
572
|
+
* and the client's own overlay tree was in neither. Measured by A8 of the
|
|
573
|
+
* instance acceptance criterion: one rendering listing
|
|
574
|
+
* `registration:<overlay>:<name>` and reporting `unowned-subject` for `<name>`,
|
|
575
|
+
* whose remedy sentence — *"Composition throws for it at boot"* — was untrue of
|
|
576
|
+
* a tree that had just booted.
|
|
577
|
+
*
|
|
578
|
+
* ## The claim is keyed from `OverlaySource.moduleId`, never from `moduleOf`
|
|
579
|
+
*
|
|
580
|
+
* FR-006, and it is not a preference: `moduleOf`'s overlay branch requires
|
|
581
|
+
* `/src/apps/` in the path and an instance's overlay root is
|
|
582
|
+
* `<root>/apps/<deployment>/modules/`, so a claim placed by path would attribute
|
|
583
|
+
* nothing at all here. `overlaySourcesUnder` already carries the id, from the
|
|
584
|
+
* directory the walk descended into.
|
|
585
|
+
*
|
|
586
|
+
* The precedence is `registration-owners.ts`': the deployment's own tree is the
|
|
587
|
+
* **tree** half and overwrites, an installed package claims only what nothing
|
|
588
|
+
* above claimed. That is the right way round — a name the client registers in
|
|
589
|
+
* their own overlay module is theirs — and it is the same rule this repository's
|
|
590
|
+
* host applies to its own `backend/src/apps/` sources.
|
|
591
|
+
*/
|
|
592
|
+
export function withOverlayRegistrationOwners(environment, sources) {
|
|
593
|
+
const overlayClaims = [];
|
|
594
|
+
for (const source of sources) {
|
|
595
|
+
for (const name of registeredNames(source.text, source.file)) {
|
|
596
|
+
overlayClaims.push({ name, moduleId: source.moduleId });
|
|
597
|
+
}
|
|
598
|
+
for (const name of providedPortNames(source.text, source.file)) {
|
|
599
|
+
overlayClaims.push({ name, moduleId: source.moduleId });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (overlayClaims.length === 0)
|
|
603
|
+
return environment;
|
|
604
|
+
const owners = mergeRegistrationOwners({
|
|
605
|
+
// The scan's map seeds this one, in the slot the merge rule gives to a
|
|
606
|
+
// claim already settled: the packages have been merged against each other
|
|
607
|
+
// and the question here is only what the deployment adds on top.
|
|
608
|
+
hostRegistered: Object.fromEntries(environment.owners),
|
|
609
|
+
treeClaims: overlayClaims,
|
|
610
|
+
packageClaims: [],
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
...environment,
|
|
614
|
+
owners,
|
|
615
|
+
// A name a module owns is not root-supplied, and the deployment's overlay
|
|
616
|
+
// module is a module: without this, a client registering a name the platform
|
|
617
|
+
// also spells would have it in both, and `deriveDivergence` reads
|
|
618
|
+
// `rootSupplied` only to decide that an absent owner is legitimate.
|
|
619
|
+
rootSupplied: new Set([...environment.rootSupplied].filter((name) => owners.get(name) === undefined)),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/** The refusal sentence, or `null` when every package this run needed was readable. */
|
|
623
|
+
export function unreadableCompositionReason(scan) {
|
|
624
|
+
if (scan.unreadable.length === 0)
|
|
625
|
+
return null;
|
|
626
|
+
const named = scan.unreadable
|
|
627
|
+
.map((entry) => `${entry.packageName}: ${entry.reason} (${entry.at})`)
|
|
628
|
+
.join('; ');
|
|
629
|
+
return (`${scan.unreadable.length} of the ${scan.expected} installed module package(s) publishing a ` +
|
|
630
|
+
`"./backend" subpath could not be read, so what they register is in no owner map and every ` +
|
|
631
|
+
`decoration of one of their names would be reported as owned by nobody — ${named}`);
|
|
632
|
+
}
|
|
633
|
+
/** The empty declaration — what a deployment that has declared nothing says. */
|
|
634
|
+
export const EMPTY_DECLARATION = {
|
|
635
|
+
omittedModules: [],
|
|
636
|
+
decorationOrder: {},
|
|
637
|
+
reasons: {},
|
|
638
|
+
};
|
|
639
|
+
/** A string written as a literal, a plain template, or a `+` chain of those. */
|
|
640
|
+
function stringLiteralOf(node) {
|
|
641
|
+
if (node === undefined)
|
|
642
|
+
return null;
|
|
643
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
644
|
+
return node.text;
|
|
645
|
+
if (ts.isParenthesizedExpression(node))
|
|
646
|
+
return stringLiteralOf(node.expression);
|
|
647
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
648
|
+
const left = stringLiteralOf(node.left);
|
|
649
|
+
const right = stringLiteralOf(node.right);
|
|
650
|
+
return left === null || right === null ? null : left + right;
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
/** An object-literal or string-literal property name, as written. */
|
|
655
|
+
function propertyNameOf(name) {
|
|
656
|
+
if (name === undefined)
|
|
657
|
+
return null;
|
|
658
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name))
|
|
659
|
+
return name.text;
|
|
660
|
+
if (ts.isNoSubstitutionTemplateLiteral(name))
|
|
661
|
+
return name.text;
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
function unwrap(node) {
|
|
665
|
+
let current = node;
|
|
666
|
+
for (;;) {
|
|
667
|
+
if (ts.isAsExpression(current) || ts.isSatisfiesExpression(current)) {
|
|
668
|
+
current = current.expression;
|
|
669
|
+
}
|
|
670
|
+
else if (ts.isParenthesizedExpression(current)) {
|
|
671
|
+
current = current.expression;
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
return current;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* A deployment's declaration, out of the file's **source text**.
|
|
680
|
+
*
|
|
681
|
+
* The running platform `import()`s this file; this run may not, and the reason
|
|
682
|
+
* is not a preference. `endora` runs as compiled JavaScript under plain `node`,
|
|
683
|
+
* and an instance's `apps/<deployment>/divergence.ts` is TypeScript — there is
|
|
684
|
+
* no loader in the process that could evaluate it, and adding one would make a
|
|
685
|
+
* scaffolding tool evaluate a client's code in order to describe it. Reading the
|
|
686
|
+
* text is also what the rest of this analysis does: the whole derivation is
|
|
687
|
+
* *"every subject is read as a literal"* (§3.3), and the declaration is held to
|
|
688
|
+
* the same rule as the tree it describes.
|
|
689
|
+
*
|
|
690
|
+
* What that costs is stated rather than discovered: a declaration whose fields
|
|
691
|
+
* are computed — spread from another module, built in a loop — resolves to
|
|
692
|
+
* nothing and is **named** in {@link DeclarationReading.unresolved}, never
|
|
693
|
+
* silently read as empty.
|
|
694
|
+
*/
|
|
695
|
+
export function readDivergenceDeclaration(source, file) {
|
|
696
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
697
|
+
const unresolved = [];
|
|
698
|
+
let literal = null;
|
|
699
|
+
const visit = (node) => {
|
|
700
|
+
if (literal !== null)
|
|
701
|
+
return;
|
|
702
|
+
if (ts.isVariableStatement(node)) {
|
|
703
|
+
for (const declaration of node.declarationList.declarations) {
|
|
704
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
705
|
+
declaration.name.text === 'divergence' &&
|
|
706
|
+
declaration.initializer !== undefined) {
|
|
707
|
+
const initializer = unwrap(declaration.initializer);
|
|
708
|
+
if (ts.isObjectLiteralExpression(initializer))
|
|
709
|
+
literal = initializer;
|
|
710
|
+
else
|
|
711
|
+
unresolved.push('divergence (the declaration is not an object literal)');
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (ts.isExportAssignment(node) && node.isExportEquals !== true) {
|
|
716
|
+
const expression = unwrap(node.expression);
|
|
717
|
+
if (ts.isObjectLiteralExpression(expression))
|
|
718
|
+
literal = expression;
|
|
719
|
+
}
|
|
720
|
+
node.forEachChild(visit);
|
|
721
|
+
};
|
|
722
|
+
sf.forEachChild(visit);
|
|
723
|
+
if (literal === null)
|
|
724
|
+
return { declaration: EMPTY_DECLARATION, unresolved };
|
|
725
|
+
const fields = new Map();
|
|
726
|
+
for (const property of literal.properties) {
|
|
727
|
+
if (ts.isPropertyAssignment(property)) {
|
|
728
|
+
const name = propertyNameOf(property.name);
|
|
729
|
+
if (name !== null)
|
|
730
|
+
fields.set(name, unwrap(property.initializer));
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
// A spread, a shorthand or a method: the field's value is somewhere else,
|
|
734
|
+
// which is exactly the state that must not read as "declares nothing".
|
|
735
|
+
unresolved.push('divergence (a property whose value is not written in place)');
|
|
736
|
+
}
|
|
737
|
+
const omittedModules = [];
|
|
738
|
+
const omitted = fields.get('omittedModules');
|
|
739
|
+
if (omitted !== undefined) {
|
|
740
|
+
if (!ts.isArrayLiteralExpression(omitted)) {
|
|
741
|
+
unresolved.push('omittedModules (not an array literal)');
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
for (const element of omitted.elements) {
|
|
745
|
+
const entry = unwrap(element);
|
|
746
|
+
if (!ts.isObjectLiteralExpression(entry)) {
|
|
747
|
+
unresolved.push('omittedModules (an entry that is not an object literal)');
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
let moduleId = null;
|
|
751
|
+
let reason = null;
|
|
752
|
+
for (const property of entry.properties) {
|
|
753
|
+
if (!ts.isPropertyAssignment(property))
|
|
754
|
+
continue;
|
|
755
|
+
const name = propertyNameOf(property.name);
|
|
756
|
+
if (name === 'moduleId')
|
|
757
|
+
moduleId = stringLiteralOf(property.initializer);
|
|
758
|
+
if (name === 'reason')
|
|
759
|
+
reason = stringLiteralOf(property.initializer);
|
|
760
|
+
}
|
|
761
|
+
if (moduleId === null) {
|
|
762
|
+
unresolved.push('omittedModules (an entry with no literal moduleId)');
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
omittedModules.push({ moduleId, reason: reason ?? '' });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const decorationOrder = {};
|
|
771
|
+
const order = fields.get('decorationOrder');
|
|
772
|
+
if (order !== undefined) {
|
|
773
|
+
if (!ts.isObjectLiteralExpression(order)) {
|
|
774
|
+
unresolved.push('decorationOrder (not an object literal)');
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
for (const property of order.properties) {
|
|
778
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
779
|
+
unresolved.push('decorationOrder (a property whose value is not written in place)');
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const key = propertyNameOf(property.name);
|
|
783
|
+
const value = unwrap(property.initializer);
|
|
784
|
+
if (key === null || !ts.isArrayLiteralExpression(value)) {
|
|
785
|
+
unresolved.push(`decorationOrder.${key ?? '<computed>'} (not a literal array of ids)`);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const ids = [];
|
|
789
|
+
let readable = true;
|
|
790
|
+
for (const element of value.elements) {
|
|
791
|
+
const id = stringLiteralOf(element);
|
|
792
|
+
if (id === null)
|
|
793
|
+
readable = false;
|
|
794
|
+
else
|
|
795
|
+
ids.push(id);
|
|
796
|
+
}
|
|
797
|
+
if (!readable)
|
|
798
|
+
unresolved.push(`decorationOrder.${key} (a member that is not a literal)`);
|
|
799
|
+
else
|
|
800
|
+
decorationOrder[key] = ids;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const reasons = {};
|
|
805
|
+
const declaredReasons = fields.get('reasons');
|
|
806
|
+
if (declaredReasons !== undefined) {
|
|
807
|
+
if (!ts.isObjectLiteralExpression(declaredReasons)) {
|
|
808
|
+
unresolved.push('reasons (not an object literal)');
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
for (const property of declaredReasons.properties) {
|
|
812
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
813
|
+
unresolved.push('reasons (a property whose value is not written in place)');
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const key = propertyNameOf(property.name);
|
|
817
|
+
const value = stringLiteralOf(property.initializer);
|
|
818
|
+
if (key === null || value === null) {
|
|
819
|
+
unresolved.push(`reasons.${key ?? '<computed>'} (not a literal sentence)`);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
reasons[key] = value;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return { declaration: { omittedModules, decorationOrder, reasons }, unresolved };
|
|
827
|
+
}
|
|
828
|
+
//# sourceMappingURL=divergence-artefacts.js.map
|