@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,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI check — a published port's **shape**: it declares no optional method
|
|
3
|
+
* (D-97.3), the container name in its doc block is the name it is actually
|
|
4
|
+
* registered under (issue #192), a module resolves, cross-module, only a name
|
|
5
|
+
* some contract publishes (issue #196, D-98.2), and an interface a module
|
|
6
|
+
* declares for *another* module to implement is named at that provider's
|
|
7
|
+
* `implements` clause (D-171.1).
|
|
8
|
+
*
|
|
9
|
+
* Four signals over one population, because all four read the same thing: the
|
|
10
|
+
* doc block that makes an interface a published port, and what the module tree
|
|
11
|
+
* does with the name in it.
|
|
12
|
+
*
|
|
13
|
+
* ## The population, and why it is two places rather than one
|
|
14
|
+
*
|
|
15
|
+
* A published port was `packages/contracts/src` and nothing else until D-171.1.
|
|
16
|
+
* That was never the rule — it was the only place a port *could* be published,
|
|
17
|
+
* because a module in `backend/src` has no supported name for anything it
|
|
18
|
+
* declares. D-171 changed that: a module package's `./ports` subpath is contract
|
|
19
|
+
* surface, so an interface declared there is published in exactly the sense this
|
|
20
|
+
* check means — a consumer may name it, and the container name in its doc block
|
|
21
|
+
* is the literal that consumer copies into `lazyPort`.
|
|
22
|
+
*
|
|
23
|
+
* So the population is **every exported `interface` carrying the
|
|
24
|
+
* `Container name:` marker, in `packages/contracts/src` or on a module
|
|
25
|
+
* package's declared `./ports` subpath**. The second half is what
|
|
26
|
+
* {@link RESOLUTIONS_OF_UNPUBLISHED_NAMES}' own entries have named as their
|
|
27
|
+
* retiring condition since T048 — *"this check's own population —
|
|
28
|
+
* `packages/contracts/src` — is what has to widen for the entry to go"* — and
|
|
29
|
+
* widening it retires them as their owners are packaged.
|
|
30
|
+
*
|
|
31
|
+
* **The `exports` map decides membership, not the path**, and the difference is
|
|
32
|
+
* load-bearing rather than pedantic. D-171's designation is derived from the
|
|
33
|
+
* artefact, and *"a `./ports` subpath exists only on a package"*: a module still
|
|
34
|
+
* in the application tree has a `ports/` directory with no supported name, so
|
|
35
|
+
* its interface is not published and a consumer still reaches it relatively —
|
|
36
|
+
* which is the ledger entry that has not yet retired. Reading the directory
|
|
37
|
+
* instead would retire those entries the moment this check widened, recording a
|
|
38
|
+
* repair that had not happened. The path-text alternative is refused for a
|
|
39
|
+
* second reason too: D-171 says `surfaceOf` answers `'port'` for a relative
|
|
40
|
+
* `services/ports/foo.ts`, which is a private file, and that a path heuristic
|
|
41
|
+
* is *"correct for choosing a remedy sentence and wrong as a boundary
|
|
42
|
+
* decision"*.
|
|
43
|
+
*
|
|
44
|
+
* The population is therefore derived in {@link main} and handed in as
|
|
45
|
+
* {@link PortShapeInput.modulePorts}: nothing in the analysis below spells a
|
|
46
|
+
* path, so a fixture enters with the same standing a real run has.
|
|
47
|
+
*
|
|
48
|
+
* ## Signal 4 — an interface declared by one module and implemented by another
|
|
49
|
+
*
|
|
50
|
+
* D-171 §4 used to refuse consumer-side declaration outright, on the ground
|
|
51
|
+
* that `lazyPort<T>` is an unchecked cast. It is, and that is about the wrong
|
|
52
|
+
* seam: conformance is checked at the **provider's** `implements` clause
|
|
53
|
+
* (TS2420) and at its explicitly typed `providePort<T>` registration, whose
|
|
54
|
+
* `Registration<T> = Resolver<T>` puts `T` in return position (TS2345). Both
|
|
55
|
+
* resolve the interface wherever it was declared.
|
|
56
|
+
*
|
|
57
|
+
* D-171.1 therefore licenses the placement — for a mutual pair, on the side the
|
|
58
|
+
* binding manifest `dependencies` edge points to — **against a condition**, and
|
|
59
|
+
* this signal is the condition. What it refuses is
|
|
60
|
+
* `declared-elsewhere-without-implements`: an interface declared in module A's
|
|
61
|
+
* `ports/`, registered by a typed `providePort<T>` in module B ≠ A, which no
|
|
62
|
+
* class in B names at an `implements` clause. That is precisely D-77's rejected
|
|
63
|
+
* alternative — the consumer declares the seam *"in its own file, importing
|
|
64
|
+
* nothing"* and the provider names it **nowhere**, so the only relation between
|
|
65
|
+
* the two types is the cast, which is none.
|
|
66
|
+
*
|
|
67
|
+
* **No ledger, and for signal 1's reason.** An entry could only license the one
|
|
68
|
+
* arrangement the condition exists to refuse.
|
|
69
|
+
*
|
|
70
|
+
* Two limits, in the header rather than discovered later. It asks whether the
|
|
71
|
+
* **provider module** names the interface at an `implements` clause, not
|
|
72
|
+
* whether the class the registration resolves to does: reading the registration
|
|
73
|
+
* argument back to a class declaration is analysis this check does not have,
|
|
74
|
+
* and a provider that implements the interface on a class it does not register
|
|
75
|
+
* passes. And the complementary direction needs no code here — a registration
|
|
76
|
+
* that drops its explicit type argument stops being seen as a registration at
|
|
77
|
+
* all, so its interface lands in {@link PORTS_WITHOUT_A_REGISTRATION}, which is
|
|
78
|
+
* empty and two-way.
|
|
79
|
+
*
|
|
80
|
+
* ## Signal 3 — the resolution side of the same name (issue #196)
|
|
81
|
+
*
|
|
82
|
+
* Signals 2 and 3 are **complements, not overlaps**, and the tree is the proof.
|
|
83
|
+
* Signal 2 reads *contract doc → registration*: it starts at a published port's
|
|
84
|
+
* doc block and asks whether the name it gives is registered and gated. It
|
|
85
|
+
* found and corrected eight wrong doc lines. The three consumers that were
|
|
86
|
+
* resolving the wrong name **stayed wrong**, because nothing walked the other
|
|
87
|
+
* way. Signal 3 is that direction: *consumer resolution → publication*.
|
|
88
|
+
*
|
|
89
|
+
* The rule is **on the name, at the resolution site**, and it has to be,
|
|
90
|
+
* because there is nowhere else it could bite. `lazyPort<T>(ctx, name)` is
|
|
91
|
+
* `new Proxy({} as T, …)` (`src/kernel/lazy-port.ts`): `T` is a free type
|
|
92
|
+
* parameter asserted onto an empty object and `name` is a `string`, so nothing
|
|
93
|
+
* in the language relates the two. `tsc` is not satisfied because the entity
|
|
94
|
+
* happens to be assignable to the record — `tsc` is satisfied because it was
|
|
95
|
+
* never asked. That is why D-98.2 rejected branding the record types: a brand
|
|
96
|
+
* can only bite where the compiler compares a value to the branded type, and
|
|
97
|
+
* at a resolution it never does. What actually happens is that a **string gets
|
|
98
|
+
* copied**, so the string is what this checks.
|
|
99
|
+
*
|
|
100
|
+
* One shape is refused — `resolution-of-unpublished-name` — over the two-way
|
|
101
|
+
* {@link RESOLUTIONS_OF_UNPUBLISHED_NAMES} ledger, which states the population
|
|
102
|
+
* and the two deliberate exclusions in full.
|
|
103
|
+
*
|
|
104
|
+
* ## Signal 2 — the documented container name (issue #192)
|
|
105
|
+
*
|
|
106
|
+
* `port-publication.md` §1.2 makes the container name part of the contract: it
|
|
107
|
+
* is the literal a consumer copies into `lazyPort<T>(ctx, 'thatName')`. Nothing
|
|
108
|
+
* checked it, and on the tree this signal was written against it was wrong six
|
|
109
|
+
* times out of 97 — four naming a container nothing registers, and **two naming
|
|
110
|
+
* the ungated legacy service the port was published to replace**. That second
|
|
111
|
+
* pair is why this is a build failure rather than tidiness: a consumer following
|
|
112
|
+
* `AdminRolePort`'s doc resolved `adminRoleService`, a plain `di.register` with
|
|
113
|
+
* no presence gate, whose `list()` returns `AdminRole` **entities** — and
|
|
114
|
+
* `AdminRole` is structurally assignable to `AdminRoleRecord`, so `tsc` said
|
|
115
|
+
* nothing. A doc that teaches a Principle XVII violation is worse than no doc.
|
|
116
|
+
*
|
|
117
|
+
* Two shapes are refused:
|
|
118
|
+
*
|
|
119
|
+
* - **`container-name-unregistered`** — the documented name appears in no
|
|
120
|
+
* `di.register` and no `di.providePort` anywhere under `src/modules` or
|
|
121
|
+
* `src/apps`. A consumer copying it gets
|
|
122
|
+
* `[kernel] '…' is not registered in this composition` at first call.
|
|
123
|
+
* - **`container-name-not-the-gated-registration`** — the port type *is*
|
|
124
|
+
* registered, by a typed `di.providePort<T>('X', …)`, and the doc names some
|
|
125
|
+
* other container. Whatever `Y` is, it is not the gate; where it happens to
|
|
126
|
+
* exist it is the ungated twin, which is the fail-open trap above.
|
|
127
|
+
*
|
|
128
|
+
* The second shape needs the registration's **type argument**, so it sees only
|
|
129
|
+
* the 81 of 134 `providePort` calls that carry one. That is not a weakness of
|
|
130
|
+
* this check but the reason A12 wants `providePort<T>` made non-inferrable: an
|
|
131
|
+
* untyped registration compares nothing, here or in `tsc`.
|
|
132
|
+
*
|
|
133
|
+
* `PORTS_WITHOUT_A_REGISTRATION` is the ledger for the first shape, two-way and
|
|
134
|
+
* empty since D-98.5. It is not a debt list to grow — an entry says why a
|
|
135
|
+
* published interface with no provider is standing, and the check refuses a
|
|
136
|
+
* stale one.
|
|
137
|
+
*
|
|
138
|
+
* ## Signal 1 — why the optional-method rule is a type rule and not advice
|
|
139
|
+
*
|
|
140
|
+
* A consumer that wants to know whether a provider implements something asks
|
|
141
|
+
* the object. Through a port it cannot: `lazyPort` hands back a `Proxy` whose
|
|
142
|
+
* `get` trap answers *every* non-symbol property with a function
|
|
143
|
+
* (`src/kernel/lazy-port.ts`), so `x.maybe`, `!x.maybe` and
|
|
144
|
+
* `typeof x.maybe === 'function'` are all truthy whatever is registered, and
|
|
145
|
+
* `x.maybe?.()` therefore always calls — into a provider that does not
|
|
146
|
+
* implement it, where the forward throws `'…' is not a function`.
|
|
147
|
+
*
|
|
148
|
+
* **And the proxy cannot be repaired.** Making the trap honest means resolving
|
|
149
|
+
* the name at *property-access* time to look at the real object, and for a
|
|
150
|
+
* gated port that turns a `typeof` probe into a `ModuleDisabledError`: a
|
|
151
|
+
* presence check that throws when the module is absent is worse than the hazard
|
|
152
|
+
* it fixes, and deferring resolution to the call is the entire reason `lazyPort`
|
|
153
|
+
* exists. The runtime cannot be made to answer, so the type has to forbid the
|
|
154
|
+
* question.
|
|
155
|
+
*
|
|
156
|
+
* `tsc` is silent on both halves: the member is optional, and `lazyPort<T>`'s
|
|
157
|
+
* `T` is an assertion rather than a check. That is why a check reads the shape.
|
|
158
|
+
*
|
|
159
|
+
* The one occurrence this rule was written from —
|
|
160
|
+
* `catalog`'s `AttributeDefinitionSource`, widening the custom-field read port
|
|
161
|
+
* with `publishInvalidate?` — fired its recovery branch on every integrity
|
|
162
|
+
* error and turned a benign, self-healing cache window into a 500 on the
|
|
163
|
+
* attribute screens. D-97.1 deleted it, so this check is born green with red
|
|
164
|
+
* proofs behind it and **no ledger**: there is nothing to drain, and an entry
|
|
165
|
+
* would be a licence to re-open the only hole the rule has ever had.
|
|
166
|
+
*
|
|
167
|
+
* ## What it reads
|
|
168
|
+
*
|
|
169
|
+
* 1. **Published ports** — an exported `interface` in `packages/contracts/src`
|
|
170
|
+
* **or on a module package's declared `./ports` subpath** (D-171.1) whose
|
|
171
|
+
* doc block carries the `Container name:` line every port in the tree is
|
|
172
|
+
* introduced by. One parse, `portDocOf`, answers both "is this a port?" and
|
|
173
|
+
* "which container does it name?", so the signals cannot come to disagree
|
|
174
|
+
* about the population.
|
|
175
|
+
* 2. **Interfaces extending one** — an `interface X extends <port>` anywhere in
|
|
176
|
+
* `backend/src/modules` or `backend/src/apps`, or in the contracts package
|
|
177
|
+
* itself. A widening is the shape that actually happened; refusing it only
|
|
178
|
+
* at the published type would refuse the tidy half.
|
|
179
|
+
* 3. **Registrations** — `di.register` keys and `di.providePort` names in the
|
|
180
|
+
* same module sources, read through `check-port-dependencies.ts`'s
|
|
181
|
+
* `registeredNames` / `providedPorts`. Imported rather than re-derived: a
|
|
182
|
+
* second expression of "this call is a registration" is a rule that can go
|
|
183
|
+
* half-missing while the check still prints `violations=0`.
|
|
184
|
+
*
|
|
185
|
+
* A member is an optional method when it is a `foo?(…): T` signature **or** a
|
|
186
|
+
* `foo?: (…) => T` property, because those are the same promise written twice.
|
|
187
|
+
* Optional *parameters* and optional *data properties* are untouched — all 34
|
|
188
|
+
* optional members across the tree's ports are of that kind, and none is
|
|
189
|
+
* affected.
|
|
190
|
+
*
|
|
191
|
+
* Usage: `tsx scripts/check-port-shape.ts [--list]`
|
|
192
|
+
* Exit 0 = clean; exit 1 = at least one finding, of any signal;
|
|
193
|
+
* exit 2 = nothing was read. Six conditions, one per input any signal could be
|
|
194
|
+
* silently missing (issue #113), because a green must never be able to mean
|
|
195
|
+
* "not looking": no sources; no port type in the contracts package; **no
|
|
196
|
+
* registration in the module scan** — an empty registration map would report
|
|
197
|
+
* every published port as unregistered, and a reader who "fixed" that by
|
|
198
|
+
* widening the ledger would have turned the whole check off; **no published
|
|
199
|
+
* container name** — signal 3 compares a consumer's literal against that set,
|
|
200
|
+
* and an empty one makes every cross-module resolution in the tree a finding;
|
|
201
|
+
* **no `lazyPort` resolution at all** — signal 3's population, whose emptiness
|
|
202
|
+
* would otherwise read as a clean bill; and **no module-declared port**, which
|
|
203
|
+
* is signal 4's whole population and the half of signal 3's the widening added.
|
|
204
|
+
* That last one is the one to understand: the widening is what retires the
|
|
205
|
+
* resolution ledger's entries, so a walk that stopped producing module ports
|
|
206
|
+
* would report those entries stale and invite an author to delete them — the
|
|
207
|
+
* exact inverse of the repair.
|
|
208
|
+
*
|
|
209
|
+
* ## One analysis, two hosts
|
|
210
|
+
*
|
|
211
|
+
* This file is the analysis (`specs/101-endora-check/contracts/package-scope-layout.md`
|
|
212
|
+
* §6). `backend/scripts/check-port-shape.ts` hosts it over this repository's
|
|
213
|
+
* contracts sources, module walk roots and `./ports` subpaths, and holds both
|
|
214
|
+
* ledgers and the two platform-name sets: each is a statement about *this*
|
|
215
|
+
* composition and none of them travels. `endora check` hosts it over one module
|
|
216
|
+
* package, where the ledgers and the platform sets are empty and signal 3 is
|
|
217
|
+
* declared unevaluated — it asks whether a module resolves a container name no
|
|
218
|
+
* contract publishes, which needs the published surface of every installed peer.
|
|
219
|
+
*/
|
|
220
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
221
|
+
import { join } from 'node:path';
|
|
222
|
+
import ts from 'typescript';
|
|
223
|
+
import { NO_HOST_RESIDENT_MODULES, } from '../lib/module-population.js';
|
|
224
|
+
import { moduleOf, providedPorts, registeredNames, resolvedNames, } from '../lib/port-registrations.js';
|
|
225
|
+
/** The line every port's doc block carries, and the only marker that finds one. */
|
|
226
|
+
/**
|
|
227
|
+
* The doc-block marker that makes an interface a published port
|
|
228
|
+
* (`contracts/port-shape.md` §1.2). Exported because both hosts read it: the
|
|
229
|
+
* marker is part of the rule, not of a host.
|
|
230
|
+
*/
|
|
231
|
+
export const PORT_DOC_MARKER = 'Container name:';
|
|
232
|
+
function parse(file, text) {
|
|
233
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The declaration's port doc block, or `null` when it has none.
|
|
237
|
+
*
|
|
238
|
+
* The single answer to "is this a published port?" — signal 1 asks it to build
|
|
239
|
+
* the population, signal 2 asks it for the name and signal 3 asks it for the
|
|
240
|
+
* set of published names, so none of the three can drift into its own idea of
|
|
241
|
+
* what a port is.
|
|
242
|
+
*/
|
|
243
|
+
function portDocOf(node, text) {
|
|
244
|
+
const ranges = ts.getLeadingCommentRanges(text, node.pos) ?? [];
|
|
245
|
+
const block = ranges
|
|
246
|
+
.map((range) => text.slice(range.pos, range.end))
|
|
247
|
+
.find((comment) => comment.includes(PORT_DOC_MARKER));
|
|
248
|
+
if (block === undefined)
|
|
249
|
+
return null;
|
|
250
|
+
const containers = [...block.matchAll(/Container name:\s*`([^`]+)`/g)]
|
|
251
|
+
.map((match) => match[1])
|
|
252
|
+
.filter((name) => name !== undefined);
|
|
253
|
+
return { containers };
|
|
254
|
+
}
|
|
255
|
+
function interfaces(sf) {
|
|
256
|
+
const found = [];
|
|
257
|
+
const visit = (node) => {
|
|
258
|
+
if (ts.isInterfaceDeclaration(node))
|
|
259
|
+
found.push(node);
|
|
260
|
+
node.forEachChild(visit);
|
|
261
|
+
};
|
|
262
|
+
sf.forEachChild(visit);
|
|
263
|
+
return found;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Every interface name a class in this file names at an `implements` clause.
|
|
267
|
+
*
|
|
268
|
+
* Signal 4's evidence, and the one `tsc` acts on: `implements` is resolved
|
|
269
|
+
* against the type wherever it was declared, so a missing member is TS2420
|
|
270
|
+
* whichever package the interface came from. A class `extends` clause is
|
|
271
|
+
* deliberately not read — it is not a conformance claim about an interface.
|
|
272
|
+
*/
|
|
273
|
+
function implementedNames(sf) {
|
|
274
|
+
const names = [];
|
|
275
|
+
const visit = (node) => {
|
|
276
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
277
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
278
|
+
if (clause.token !== ts.SyntaxKind.ImplementsKeyword)
|
|
279
|
+
continue;
|
|
280
|
+
for (const type of clause.types) {
|
|
281
|
+
if (ts.isIdentifier(type.expression))
|
|
282
|
+
names.push(type.expression.text);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
node.forEachChild(visit);
|
|
287
|
+
};
|
|
288
|
+
sf.forEachChild(visit);
|
|
289
|
+
return names;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The module a source key belongs to.
|
|
293
|
+
*
|
|
294
|
+
* `moduleOf` keys on `/src/modules/<id>/` and falls back to the first
|
|
295
|
+
* `modules/<id>/` segment, while this function's inputs are keyed however the
|
|
296
|
+
* caller likes — so the key is normalised for it and the caller's own key is
|
|
297
|
+
* what gets reported. Normalising is not re-deciding: the id still comes from
|
|
298
|
+
* the one function that owns that question.
|
|
299
|
+
*/
|
|
300
|
+
function moduleOfKey(file, hostResident = NO_HOST_RESIDENT_MODULES) {
|
|
301
|
+
return moduleOf(forOwnerLookup(file), hostResident);
|
|
302
|
+
}
|
|
303
|
+
/** The key spelling `moduleOf`, `registeredNames` and `resolvedNames` expect. */
|
|
304
|
+
function forOwnerLookup(file) {
|
|
305
|
+
return file.includes('/src/') ? file : `/src/${file.replace(/^\/+/, '')}`;
|
|
306
|
+
}
|
|
307
|
+
/** The names an interface extends, as written (type arguments dropped). */
|
|
308
|
+
function extendedNames(node) {
|
|
309
|
+
const names = [];
|
|
310
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
311
|
+
if (clause.token !== ts.SyntaxKind.ExtendsKeyword)
|
|
312
|
+
continue;
|
|
313
|
+
for (const type of clause.types) {
|
|
314
|
+
if (ts.isIdentifier(type.expression))
|
|
315
|
+
names.push(type.expression.text);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return names;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The optional **methods** of an interface.
|
|
322
|
+
*
|
|
323
|
+
* Both spellings, because they are one promise: `foo?(): void` is a
|
|
324
|
+
* `MethodSignature` with a question token, `foo?: () => void` a
|
|
325
|
+
* `PropertySignature` whose type is a function. A `foo?: string` is a data
|
|
326
|
+
* property and is deliberately outside the population.
|
|
327
|
+
*/
|
|
328
|
+
function optionalMethods(node) {
|
|
329
|
+
return node.members.filter((member) => {
|
|
330
|
+
if (member.questionToken === undefined)
|
|
331
|
+
return false;
|
|
332
|
+
if (ts.isMethodSignature(member))
|
|
333
|
+
return true;
|
|
334
|
+
if (ts.isPropertySignature(member) && member.type !== undefined) {
|
|
335
|
+
return ts.isFunctionTypeNode(member.type);
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function memberName(member) {
|
|
341
|
+
const name = member.name;
|
|
342
|
+
if (name === undefined)
|
|
343
|
+
return '(computed)';
|
|
344
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Every published port, and every optional method on one or on a widening
|
|
348
|
+
* of one.
|
|
349
|
+
*
|
|
350
|
+
* The input is **source text**, so a fixture enters exactly where a real run
|
|
351
|
+
* does (issue #130): nothing above this function classifies anything.
|
|
352
|
+
*/
|
|
353
|
+
export function checkPortShape(input) {
|
|
354
|
+
const hostResident = input.hostResidentModules ?? NO_HOST_RESIDENT_MODULES;
|
|
355
|
+
const parsedContracts = new Map();
|
|
356
|
+
const portTypes = new Set();
|
|
357
|
+
/** Every published port, with where it is declared and what it documents. */
|
|
358
|
+
const published = [];
|
|
359
|
+
/**
|
|
360
|
+
* One sweep for both halves of the population — the contracts package and the
|
|
361
|
+
* modules' own `ports/` directories. `portDocOf` decides membership in both,
|
|
362
|
+
* so the two cannot come to disagree about what a published port is.
|
|
363
|
+
*/
|
|
364
|
+
const collect = (file, text, declaringModule, remember) => {
|
|
365
|
+
const sf = parse(file, text);
|
|
366
|
+
remember(sf);
|
|
367
|
+
let found = 0;
|
|
368
|
+
for (const node of interfaces(sf)) {
|
|
369
|
+
const doc = portDocOf(node, text);
|
|
370
|
+
if (doc === null)
|
|
371
|
+
continue;
|
|
372
|
+
found += 1;
|
|
373
|
+
portTypes.add(node.name.text);
|
|
374
|
+
published.push({
|
|
375
|
+
portName: node.name.text,
|
|
376
|
+
file,
|
|
377
|
+
line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1,
|
|
378
|
+
containers: doc.containers,
|
|
379
|
+
declaringModule,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return found;
|
|
383
|
+
};
|
|
384
|
+
for (const [file, text] of input.contracts) {
|
|
385
|
+
collect(file, text, null, (sf) => parsedContracts.set(file, sf));
|
|
386
|
+
}
|
|
387
|
+
const modulePorts = input.modulePorts ?? new Map();
|
|
388
|
+
const parsedModulePorts = new Map();
|
|
389
|
+
let modulePortCount = 0;
|
|
390
|
+
for (const [file, text] of modulePorts) {
|
|
391
|
+
modulePortCount += collect(file, text, moduleOfKey(file, hostResident), (sf) => parsedModulePorts.set(file, sf));
|
|
392
|
+
}
|
|
393
|
+
const findings = [];
|
|
394
|
+
const scan = (file, sf) => {
|
|
395
|
+
for (const node of interfaces(sf)) {
|
|
396
|
+
const own = portTypes.has(node.name.text);
|
|
397
|
+
const extended = extendedNames(node).find((name) => portTypes.has(name));
|
|
398
|
+
if (!own && extended === undefined)
|
|
399
|
+
continue;
|
|
400
|
+
// A port type declaring an optional method is the stronger finding, so a
|
|
401
|
+
// type that is both a port and an extension is reported as the port.
|
|
402
|
+
const kind = own
|
|
403
|
+
? 'optional-method-on-port'
|
|
404
|
+
: 'optional-method-on-port-extension';
|
|
405
|
+
const portName = own ? node.name.text : extended;
|
|
406
|
+
for (const member of optionalMethods(node)) {
|
|
407
|
+
findings.push({
|
|
408
|
+
file,
|
|
409
|
+
line: sf.getLineAndCharacterOfPosition(member.getStart(sf)).line + 1,
|
|
410
|
+
typeName: node.name.text,
|
|
411
|
+
portName,
|
|
412
|
+
member: memberName(member),
|
|
413
|
+
kind,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
for (const [file, sf] of parsedContracts)
|
|
419
|
+
scan(file, sf);
|
|
420
|
+
// A ports file is walked by both halves of a real run — it is a module source
|
|
421
|
+
// and it is a port declaration — so signal 1 scans the union keyed by file,
|
|
422
|
+
// never the concatenation, which would report an optional method twice.
|
|
423
|
+
const moduleScan = new Map();
|
|
424
|
+
for (const [file, text] of input.modules)
|
|
425
|
+
moduleScan.set(file, parse(file, text));
|
|
426
|
+
for (const [file, sf] of parsedModulePorts)
|
|
427
|
+
if (!moduleScan.has(file))
|
|
428
|
+
moduleScan.set(file, sf);
|
|
429
|
+
for (const [file, sf] of moduleScan)
|
|
430
|
+
scan(file, sf);
|
|
431
|
+
findings.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file.localeCompare(b.file)));
|
|
432
|
+
// --- signal 2: the documented container name -------------------------------
|
|
433
|
+
//
|
|
434
|
+
// Both registration readers come from `check-port-dependencies.ts`, which owns
|
|
435
|
+
// the "this call is a registration" predicate. `registeredNames` already
|
|
436
|
+
// covers `providePort` names, so `plainNames` is derived by subtraction rather
|
|
437
|
+
// than by a second parse deciding the same thing differently.
|
|
438
|
+
const allNames = new Set();
|
|
439
|
+
const gatedNames = new Set();
|
|
440
|
+
/** Container name a typed `providePort<T>` gives each port type. */
|
|
441
|
+
const gatedNameOfType = new Map();
|
|
442
|
+
/** The module that typed registration sits in — signal 4's other half. */
|
|
443
|
+
const gatedModuleOfType = new Map();
|
|
444
|
+
/** Module id → every interface name a class in it `implements`. */
|
|
445
|
+
const implementsByModule = new Map();
|
|
446
|
+
for (const [file, text] of input.modules) {
|
|
447
|
+
for (const name of registeredNames(text, file))
|
|
448
|
+
allNames.add(name);
|
|
449
|
+
for (const port of providedPorts(text, file)) {
|
|
450
|
+
gatedNames.add(port.name);
|
|
451
|
+
if (port.typeName !== null && !gatedNameOfType.has(port.typeName)) {
|
|
452
|
+
gatedNameOfType.set(port.typeName, port.name);
|
|
453
|
+
const owner = moduleOfKey(file, hostResident);
|
|
454
|
+
if (owner !== null)
|
|
455
|
+
gatedModuleOfType.set(port.typeName, owner);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const [file, sf] of moduleScan) {
|
|
460
|
+
const owner = moduleOfKey(file, hostResident);
|
|
461
|
+
if (owner === null)
|
|
462
|
+
continue;
|
|
463
|
+
for (const name of implementedNames(sf)) {
|
|
464
|
+
const claimed = implementsByModule.get(owner);
|
|
465
|
+
if (claimed)
|
|
466
|
+
claimed.add(name);
|
|
467
|
+
else
|
|
468
|
+
implementsByModule.set(owner, new Set([name]));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const ledger = input.unregisteredLedger ?? {};
|
|
472
|
+
const nameFindings = [];
|
|
473
|
+
const ledgerHits = new Set();
|
|
474
|
+
const registrationKindOf = (container) => gatedNames.has(container) ? 'gated' : allNames.has(container) ? 'plain' : null;
|
|
475
|
+
for (const port of published) {
|
|
476
|
+
if (port.containers.length === 0)
|
|
477
|
+
continue;
|
|
478
|
+
const gatedName = gatedNameOfType.get(port.portName);
|
|
479
|
+
const unregistered = port.containers.filter((name) => registrationKindOf(name) === null);
|
|
480
|
+
// "No registration behind this published interface at all" is the shape the
|
|
481
|
+
// ledger answers for, so it is asked once per port and not once per
|
|
482
|
+
// documented name: a port with two providers where one is missing is a
|
|
483
|
+
// wrong doc line, not an unprovided port.
|
|
484
|
+
if (unregistered.length === port.containers.length) {
|
|
485
|
+
if (ledger[port.portName] !== undefined) {
|
|
486
|
+
ledgerHits.add(port.portName);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const documented of unregistered) {
|
|
491
|
+
nameFindings.push({
|
|
492
|
+
file: port.file,
|
|
493
|
+
line: port.line,
|
|
494
|
+
portName: port.portName,
|
|
495
|
+
documented,
|
|
496
|
+
registered: gatedName ?? null,
|
|
497
|
+
documentedRegistrationKind: null,
|
|
498
|
+
kind: 'container-name-unregistered',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
// The gated-registration shape asks whether the doc names the gate **at
|
|
502
|
+
// all**. With two providers only one of them can be the typed
|
|
503
|
+
// `providePort<T>` this map records, so requiring every documented name to
|
|
504
|
+
// be it would refuse the two-provider shape rather than the defect.
|
|
505
|
+
if (gatedName !== undefined && !port.containers.includes(gatedName)) {
|
|
506
|
+
const documented = port.containers[0];
|
|
507
|
+
nameFindings.push({
|
|
508
|
+
file: port.file,
|
|
509
|
+
line: port.line,
|
|
510
|
+
portName: port.portName,
|
|
511
|
+
documented,
|
|
512
|
+
registered: gatedName,
|
|
513
|
+
documentedRegistrationKind: registrationKindOf(documented),
|
|
514
|
+
kind: 'container-name-not-the-gated-registration',
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
nameFindings.sort((a, b) => a.portName.localeCompare(b.portName));
|
|
519
|
+
const staleLedgerEntries = Object.keys(ledger)
|
|
520
|
+
.filter((portName) => !ledgerHits.has(portName))
|
|
521
|
+
.sort();
|
|
522
|
+
// --- signal 4: the condition D-171.1 licenses the placement against ---------
|
|
523
|
+
//
|
|
524
|
+
// Only a port a **module** declares can be in this population: a port in
|
|
525
|
+
// `packages/contracts` belongs to no module, so "declared elsewhere" has no
|
|
526
|
+
// meaning for it. And only where the typed registration is in a *different*
|
|
527
|
+
// module — the ordinary case, where the owner declares and implements its own
|
|
528
|
+
// interface, is what D-171 §4 already describes and is untouched.
|
|
529
|
+
const declaredElsewhere = [];
|
|
530
|
+
for (const port of published) {
|
|
531
|
+
if (port.declaringModule === null)
|
|
532
|
+
continue;
|
|
533
|
+
const provider = gatedModuleOfType.get(port.portName);
|
|
534
|
+
if (provider === undefined || provider === port.declaringModule)
|
|
535
|
+
continue;
|
|
536
|
+
if (implementsByModule.get(provider)?.has(port.portName) === true)
|
|
537
|
+
continue;
|
|
538
|
+
declaredElsewhere.push({
|
|
539
|
+
portName: port.portName,
|
|
540
|
+
file: port.file,
|
|
541
|
+
line: port.line,
|
|
542
|
+
declaringModule: port.declaringModule,
|
|
543
|
+
providingModule: provider,
|
|
544
|
+
container: gatedNameOfType.get(port.portName),
|
|
545
|
+
kind: 'declared-elsewhere-without-implements',
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
declaredElsewhere.sort((a, b) => a.portName.localeCompare(b.portName));
|
|
549
|
+
// --- signal 3: the resolution side of the same name ------------------------
|
|
550
|
+
//
|
|
551
|
+
// Signal 2 above walks contract doc -> registration. This walks consumer
|
|
552
|
+
// resolution -> publication, which is a different edge in the other
|
|
553
|
+
// direction, and the one nothing in the tree could see: !698 corrected six
|
|
554
|
+
// doc blocks while three consumers went on resolving the class name.
|
|
555
|
+
//
|
|
556
|
+
// Every predicate it needs already exists. "Which module registers this
|
|
557
|
+
// name" is `registeredNames` + `moduleOf`; "this is a `lazyPort` resolution"
|
|
558
|
+
// is `resolvedNames`, through the `via` field it records; "this name is
|
|
559
|
+
// published" is `portDocOf`, the same parse signals 1 and 2 use. Nothing here
|
|
560
|
+
// decides any of those a second time.
|
|
561
|
+
const publishedContainers = new Set();
|
|
562
|
+
for (const port of published)
|
|
563
|
+
for (const name of port.containers)
|
|
564
|
+
publishedContainers.add(name);
|
|
565
|
+
/** Container name -> the module whose sources register it. */
|
|
566
|
+
const ownerOfName = new Map();
|
|
567
|
+
const lazyResolutions = [];
|
|
568
|
+
for (const [file, text] of input.modules) {
|
|
569
|
+
// `moduleOf` and `resolvedNames` read the module id out of the path, and
|
|
570
|
+
// they key on `/src/modules/<id>/` — while this function's inputs are keyed
|
|
571
|
+
// however the caller likes, which is what lets a fixture enter at the top.
|
|
572
|
+
// So the path is normalised for them and the caller's own key is what gets
|
|
573
|
+
// reported. Normalising is not re-deciding: the module id still comes from
|
|
574
|
+
// the one function that owns that question.
|
|
575
|
+
const lookupKey = forOwnerLookup(file);
|
|
576
|
+
const moduleId = moduleOf(lookupKey, hostResident);
|
|
577
|
+
if (moduleId === null)
|
|
578
|
+
continue;
|
|
579
|
+
for (const name of registeredNames(text, lookupKey))
|
|
580
|
+
ownerOfName.set(name, moduleId);
|
|
581
|
+
for (const resolution of resolvedNames(text, lookupKey)) {
|
|
582
|
+
if (resolution.via !== 'lazyPort')
|
|
583
|
+
continue;
|
|
584
|
+
lazyResolutions.push({
|
|
585
|
+
moduleId: resolution.moduleId,
|
|
586
|
+
name: resolution.name,
|
|
587
|
+
file,
|
|
588
|
+
line: resolution.line,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const resolutionLedger = input.unpublishedResolutionLedger ?? {};
|
|
593
|
+
const platformOwnedNames = input.platformOwnedNames ?? new Set();
|
|
594
|
+
const hostRegisteredPorts = input.hostRegisteredPorts ?? {};
|
|
595
|
+
const unpublishedResolutions = [];
|
|
596
|
+
const resolutionLedgerHits = new Set();
|
|
597
|
+
for (const resolution of lazyResolutions) {
|
|
598
|
+
// Platform names are supplied by a composition root or the kernel, neither
|
|
599
|
+
// of which has a contracts file to publish from; `HOST_REGISTERED_PORTS`
|
|
600
|
+
// is the same fact for a port a root still bridges on its owner's behalf.
|
|
601
|
+
if (platformOwnedNames.has(resolution.name))
|
|
602
|
+
continue;
|
|
603
|
+
if (hostRegisteredPorts[resolution.name] !== undefined)
|
|
604
|
+
continue;
|
|
605
|
+
const owner = ownerOfName.get(resolution.name);
|
|
606
|
+
// A name nothing registers is `check-port-dependencies`' `unowned-name`,
|
|
607
|
+
// and reporting it here as well would give one defect two voices.
|
|
608
|
+
if (owner === undefined)
|
|
609
|
+
continue;
|
|
610
|
+
// A module naming its own registration crosses no boundary.
|
|
611
|
+
if (owner === resolution.moduleId)
|
|
612
|
+
continue;
|
|
613
|
+
if (publishedContainers.has(resolution.name))
|
|
614
|
+
continue;
|
|
615
|
+
const key = `${resolution.moduleId}:${resolution.name}`;
|
|
616
|
+
if (resolutionLedger[key] !== undefined) {
|
|
617
|
+
resolutionLedgerHits.add(key);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
unpublishedResolutions.push({
|
|
621
|
+
moduleId: resolution.moduleId,
|
|
622
|
+
owner,
|
|
623
|
+
name: resolution.name,
|
|
624
|
+
file: resolution.file,
|
|
625
|
+
line: resolution.line,
|
|
626
|
+
kind: 'resolution-of-unpublished-name',
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
unpublishedResolutions.sort((a, b) => a.file === b.file ? a.line - b.line : a.file.localeCompare(b.file));
|
|
630
|
+
const staleUnpublishedResolutions = Object.keys(resolutionLedger)
|
|
631
|
+
.filter((key) => !resolutionLedgerHits.has(key))
|
|
632
|
+
.sort();
|
|
633
|
+
return {
|
|
634
|
+
portTypes: [...portTypes].sort(),
|
|
635
|
+
findings,
|
|
636
|
+
nameFindings,
|
|
637
|
+
registeredNameCount: allNames.size,
|
|
638
|
+
staleLedgerEntries,
|
|
639
|
+
unpublishedResolutions,
|
|
640
|
+
publishedContainerCount: publishedContainers.size,
|
|
641
|
+
lazyPortResolutionCount: lazyResolutions.length,
|
|
642
|
+
staleUnpublishedResolutions,
|
|
643
|
+
declaredElsewhere,
|
|
644
|
+
modulePortCount,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Every TypeScript source under `dir`, with the rule's own prunes applied.
|
|
649
|
+
*
|
|
650
|
+
* Exported because the population is part of the rule: two hosts computing
|
|
651
|
+
* "which files this check reads" two ways is the shape that lets one of them go
|
|
652
|
+
* half-blind.
|
|
653
|
+
*/
|
|
654
|
+
export function collectPortShapeSources(dir, out = []) {
|
|
655
|
+
if (!existsSync(dir))
|
|
656
|
+
return out;
|
|
657
|
+
for (const name of readdirSync(dir)) {
|
|
658
|
+
const full = join(dir, name);
|
|
659
|
+
if (statSync(full).isDirectory()) {
|
|
660
|
+
if (name === 'node_modules' || name === 'dist')
|
|
661
|
+
continue;
|
|
662
|
+
collectPortShapeSources(full, out);
|
|
663
|
+
}
|
|
664
|
+
else if (name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.endsWith('.d.ts')) {
|
|
665
|
+
out.push(full);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=port-shape.js.map
|