@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,1347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package-scope hosts — one per rule whose `EstateEntry.host` is `'built'`
|
|
3
|
+
* (`specs/101-endora-check/contracts/package-scope-layout.md` §6).
|
|
4
|
+
*
|
|
5
|
+
* A host does three things and no more: it derives the rule's population from
|
|
6
|
+
* the {@link PackageLayout}, it calls the **relocated analysis** — the same
|
|
7
|
+
* function `backend/scripts/check-<name>.ts` calls — and it turns the result
|
|
8
|
+
* into a {@link RuleResult}. It re-implements no predicate. A second
|
|
9
|
+
* implementation of an analysis is the defect this repository is built against,
|
|
10
|
+
* in its purest form.
|
|
11
|
+
*
|
|
12
|
+
* The order the verdicts are decided in is `exit-reduction.md` §2's, and it is
|
|
13
|
+
* the same three lines in every host:
|
|
14
|
+
*
|
|
15
|
+
* 1. no declaration for the subject → `not-applicable`, naming the absent
|
|
16
|
+
* declaration;
|
|
17
|
+
* 2. a declaration and a walk that came back short → `unreadable`, exit 2;
|
|
18
|
+
* 3. otherwise `ran`, with the estate's own `read:` line.
|
|
19
|
+
*/
|
|
20
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
21
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
22
|
+
import ts from 'typescript';
|
|
23
|
+
import { SUPPORTED_LANGUAGES } from '@endora-commerce/contracts';
|
|
24
|
+
import { barrelKeyOf, publishedSurface, PUBLISHED_SUBPATHS, } from '../lib/platform-surface.js';
|
|
25
|
+
import { declaresRegisterModule } from '../lib/module-roots.js';
|
|
26
|
+
import { providedPortNames } from '../lib/port-registrations.js';
|
|
27
|
+
import { readSizeRefusal } from '../lib/read-size.js';
|
|
28
|
+
import { checkBundlePairing } from '../rules/bundle-pairing.js';
|
|
29
|
+
import { analyzeSource as commandCoverageAnalyse, collectScannedFiles, isScannedPath as commandCoverageOpens, } from '../rules/command-coverage.js';
|
|
30
|
+
import { analyzeSource as containerAnalyse, collectModuleFiles, } from '../rules/container-imports.js';
|
|
31
|
+
import { checkNulBytes, isScannablePath, SKIPPED_DIRECTORIES, } from '../rules/nul-bytes.js';
|
|
32
|
+
import { analyzeSource as channelAnalyse, collectChannelSources, } from '../rules/channel-resolution.js';
|
|
33
|
+
import { checkDefaultLanguageProse, collectProseSources, DETECTED_LANGUAGES, isScannedPath as defaultLanguageProseOpens, ledgerKey as proseLedgerKey, } from '../rules/default-language-prose.js';
|
|
34
|
+
import { checkDiacriticFolds, collectFoldSources, isScannablePath as foldsScannablePath, } from '../rules/diacritic-folds.js';
|
|
35
|
+
import { collectEntryScopeSources, declaredProgramEntryPoints, findEntrySites, keyOf as entryScopeKeyOf, violationsOf as entryScopeViolations, } from '../rules/entry-scope.js';
|
|
36
|
+
import { analyzeSource as relationAnalyse, collectSources as collectRelationSources, findingKey as relationKey, isViolation as isRelationViolation, RELATION_DECORATOR_HINT, } from '../rules/kernel-boundary.js';
|
|
37
|
+
import { checkPlatformSurface, collectPlatformSurfaceSources, keyOf as platformSurfaceKeyOf, remedyOf as platformSurfaceRemedy, } from '../rules/platform-surface.js';
|
|
38
|
+
import { checkPortShape, collectPortShapeSources, } from '../rules/port-shape.js';
|
|
39
|
+
import { checkTransactionContext, collectTransactionSources, keyOf as transactionKeyOf, } from '../rules/transaction-context.js';
|
|
40
|
+
import { checkSubscribeSeam, checkWorkerSeam, collectSeamFiles, keyOf as subscriptionKeyOf, workerKeyOf, } from '../rules/subscribe-seam.js';
|
|
41
|
+
import { checkQueueNames, collectQueueNameFiles, keyOf as queueNameKeyOf, remedyFor as queueNameRemedy, } from '../rules/queue-names.js';
|
|
42
|
+
import { checkEntryPresence, collectPresenceFiles, keyOf as presenceKeyOf, remedyFor as presenceRemedy, } from '../rules/entry-presence.js';
|
|
43
|
+
import { checkPortCatches, collectPortCatchFiles, keyOf as portCatchKeyOf, resolvedPortNames, } from '../rules/port-catches.js';
|
|
44
|
+
import { readPeerOwners } from './peer-owners.js';
|
|
45
|
+
import { analyzeEmittedFiles, classifyFindings, declaredEntityClasses, PREFIX as TENANT_PREFIX, remedyFor as tenantRemedy, walkEmitted, } from '../rules/entity-tenant-classification.js';
|
|
46
|
+
import { estateEntry } from './estate.js';
|
|
47
|
+
import { isFile, layerExpectation, } from './layout.js';
|
|
48
|
+
/** The estate's `sources=` token for a package: its own `exports` map. */
|
|
49
|
+
const PACKAGE_EXPORTS = 'package-exports';
|
|
50
|
+
function entryOf(id) {
|
|
51
|
+
const entry = estateEntry(id);
|
|
52
|
+
if (entry === undefined) {
|
|
53
|
+
// Unreachable through `runCheck`, which iterates the estate; a host called
|
|
54
|
+
// directly with an unknown id must not answer with a default.
|
|
55
|
+
throw new Error(`[endora check] no estate entry for \`${id}\``);
|
|
56
|
+
}
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
function notApplicable(id, explanation) {
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
verdict: 'not-applicable',
|
|
63
|
+
findings: [],
|
|
64
|
+
acknowledged: [],
|
|
65
|
+
readSize: null,
|
|
66
|
+
explanation,
|
|
67
|
+
unevaluatedSignals: entryOf(id).partial ?? [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function unreadable(id, explanation, readSize) {
|
|
71
|
+
return {
|
|
72
|
+
id,
|
|
73
|
+
verdict: 'unreadable',
|
|
74
|
+
findings: [],
|
|
75
|
+
acknowledged: [],
|
|
76
|
+
readSize,
|
|
77
|
+
explanation,
|
|
78
|
+
unevaluatedSignals: entryOf(id).partial ?? [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function ran(id, readSize, findings) {
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
verdict: 'ran',
|
|
85
|
+
findings,
|
|
86
|
+
acknowledged: [],
|
|
87
|
+
readSize,
|
|
88
|
+
explanation: '',
|
|
89
|
+
unevaluatedSignals: entryOf(id).partial ?? [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The one sentence a `not-applicable`-from-a-declaration verdict prints.
|
|
94
|
+
*
|
|
95
|
+
* It names the declaration the rule looked for and did not find. That sentence
|
|
96
|
+
* is what makes the verdict auditable by its reader, and it is the whole
|
|
97
|
+
* difference between this verdict and a skip.
|
|
98
|
+
*/
|
|
99
|
+
function absentDeclaration(entry) {
|
|
100
|
+
const declaration = entry.subjectDeclaration?.declaration ?? 'subject';
|
|
101
|
+
return `no ${declaration}: this rule has no subject in this package`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Turn a walk plus its declared expectation into either `ran`'s read line or the
|
|
105
|
+
* short-walk refusal, in one place.
|
|
106
|
+
*
|
|
107
|
+
* `read-size.ts`'s three refusal kinds are already the estate's, and a
|
|
108
|
+
* `no-expectation` verdict is caught by the caller before it gets here — it is
|
|
109
|
+
* `not-applicable`, not a refusal (§3).
|
|
110
|
+
*/
|
|
111
|
+
function readSizeOrShortWalk(id, input) {
|
|
112
|
+
const refusal = readSizeRefusal(input);
|
|
113
|
+
if (refusal === null)
|
|
114
|
+
return { ok: true };
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
result: unreadable(id, `${refusal.message}. Supply the sources the package's \`exports\` map declares, or ` +
|
|
118
|
+
`withdraw the declaration.`, input),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The coverage token for a rule's walk.
|
|
123
|
+
*
|
|
124
|
+
* Three answers, and they are `exit-reduction.md` §2's three states: a
|
|
125
|
+
* refusal the layout could not derive at all, `null` for *no expectation* —
|
|
126
|
+
* which is `not-applicable`, never `0/0` — and the token.
|
|
127
|
+
*/
|
|
128
|
+
function coverageOf(layout, walked, opens) {
|
|
129
|
+
if (layout.layerRefusal !== null)
|
|
130
|
+
return { refusal: layout.layerRefusal };
|
|
131
|
+
const expectation = layerExpectation(layout, walked, opens);
|
|
132
|
+
if (expectation.expected.length === 0)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
source: PACKAGE_EXPORTS,
|
|
136
|
+
expected: expectation.expected.length,
|
|
137
|
+
covered: expectation.covered.length,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** `true` when {@link coverageOf} answered with a refusal rather than a token. */
|
|
141
|
+
function isRefusal(value) {
|
|
142
|
+
return value !== null && 'refusal' in value;
|
|
143
|
+
}
|
|
144
|
+
const opensTypeScript = (path) => path.endsWith('.ts') && !path.endsWith('.d.ts') && !path.endsWith('.test.ts');
|
|
145
|
+
/* ---------------------------------------------------------------- nul-bytes */
|
|
146
|
+
/**
|
|
147
|
+
* The one rule with no repository input at all: its population is every file the
|
|
148
|
+
* package holds, minus the same declared exclusions the repository host uses.
|
|
149
|
+
*
|
|
150
|
+
* `self-reported`, exactly as in this repository — nothing else derives "every
|
|
151
|
+
* file that is not binary", and inventing a `package-exports` expectation for it
|
|
152
|
+
* would be a second author that agrees with the walk by construction.
|
|
153
|
+
*/
|
|
154
|
+
const nulBytes = (layout) => {
|
|
155
|
+
const id = 'check:nul-bytes';
|
|
156
|
+
const files = [];
|
|
157
|
+
for (const absolute of walkEverything(layout.packageRoot)) {
|
|
158
|
+
const path = relative(layout.packageRoot, absolute).split(sep).join('/');
|
|
159
|
+
if (!isScannablePath(path))
|
|
160
|
+
continue;
|
|
161
|
+
files.push({ path, bytes: readFileSync(absolute) });
|
|
162
|
+
}
|
|
163
|
+
const result = checkNulBytes(files);
|
|
164
|
+
const readSize = { prefix: '[nul-bytes]', files: result.scanned };
|
|
165
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
166
|
+
if (!short.ok)
|
|
167
|
+
return short.result;
|
|
168
|
+
return ran(id, readSize, result.violations.map((finding) => ({
|
|
169
|
+
rule: id,
|
|
170
|
+
key: finding.path,
|
|
171
|
+
location: `${finding.path}:${finding.line}:${finding.column}`,
|
|
172
|
+
message: `${finding.count} raw NUL byte(s). Git classifies the file as binary, so every diff ` +
|
|
173
|
+
`of it reads "Binary files differ" and the file stops being reviewable. Spell the ` +
|
|
174
|
+
`byte as \`\\0\` — the same byte at runtime, and the diff comes back.`,
|
|
175
|
+
})));
|
|
176
|
+
};
|
|
177
|
+
/* ----------------------------------------------------------- bundle-pairing */
|
|
178
|
+
/**
|
|
179
|
+
* The two-language floor, and the single most likely thing a stranger gets
|
|
180
|
+
* wrong.
|
|
181
|
+
*
|
|
182
|
+
* The subject is the manifest's `i18n.bundlesDir`, read from the **emitted**
|
|
183
|
+
* manifest where the package emits — the platform composes a package through its
|
|
184
|
+
* published artefact, so that is the value the platform will read. A package
|
|
185
|
+
* declaring no bundles directory owes no translation and is `not-applicable`.
|
|
186
|
+
*
|
|
187
|
+
* `undeclared-bundle-dir` is declared unevaluated on the rule's line: the
|
|
188
|
+
* directory names that signal probes come from *other* modules' manifests
|
|
189
|
+
* (`declaredBundleDirectories`), and a lone package supplies none.
|
|
190
|
+
*/
|
|
191
|
+
const bundlePairing = (layout) => {
|
|
192
|
+
const id = 'check:bundle-pairing';
|
|
193
|
+
const entry = entryOf(id);
|
|
194
|
+
const manifest = readEmittedManifest(layout);
|
|
195
|
+
if (manifest === null) {
|
|
196
|
+
return unreadable(id, `the package's manifest could not be read at its \`exports\` root target. Build the ` +
|
|
197
|
+
`package (\`pnpm run build\` in its directory) — the platform composes a module ` +
|
|
198
|
+
`through its published artefact, so answering from source would make this verdict ` +
|
|
199
|
+
`differ from the platform's.`, null);
|
|
200
|
+
}
|
|
201
|
+
const bundlesDir = manifest.bundlesDir;
|
|
202
|
+
if (bundlesDir === null || bundlesDir.length === 0) {
|
|
203
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
204
|
+
}
|
|
205
|
+
const languages = [...SUPPORTED_LANGUAGES];
|
|
206
|
+
if (languages.length === 0) {
|
|
207
|
+
return unreadable(id, `the platform's shipped-language set is empty, so every module is vacuously paired ` +
|
|
208
|
+
`and the predicate has nothing to compare. Install \`@endora-commerce/contracts\`.`, null);
|
|
209
|
+
}
|
|
210
|
+
const module = {
|
|
211
|
+
moduleId: layout.moduleId,
|
|
212
|
+
directory: layout.packageRoot,
|
|
213
|
+
bundlesDir,
|
|
214
|
+
};
|
|
215
|
+
const result = checkBundlePairing({ modules: [module], languages });
|
|
216
|
+
const readSize = {
|
|
217
|
+
prefix: '[bundle-pairing]',
|
|
218
|
+
files: result.filesRead.length,
|
|
219
|
+
sites: result.classified.length,
|
|
220
|
+
coverage: [
|
|
221
|
+
{ source: 'shipped-languages', expected: languages.length, covered: result.languagesProbed },
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
225
|
+
if (!short.ok)
|
|
226
|
+
return short.result;
|
|
227
|
+
return ran(id, readSize, result.findings.map((finding) => ({
|
|
228
|
+
rule: id,
|
|
229
|
+
key: `${finding.kind}|${finding.language ?? '-'}|${layout.keyOf(finding.path)}`,
|
|
230
|
+
location: layout.keyOf(finding.path),
|
|
231
|
+
message: `${finding.kind}: ${finding.detail}`,
|
|
232
|
+
})));
|
|
233
|
+
};
|
|
234
|
+
/* -------------------------------------------------------- container-imports */
|
|
235
|
+
const containerImports = (layout) => {
|
|
236
|
+
const id = 'check:container-imports';
|
|
237
|
+
const entry = entryOf(id);
|
|
238
|
+
const files = collectModuleFiles([layout.sourceRoot]);
|
|
239
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
240
|
+
if (isRefusal(coverage))
|
|
241
|
+
return unreadable(id, coverage.refusal, null);
|
|
242
|
+
if (coverage === null)
|
|
243
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
244
|
+
const readSize = {
|
|
245
|
+
prefix: '[container-imports]',
|
|
246
|
+
files: files.length,
|
|
247
|
+
coverage: [coverage],
|
|
248
|
+
};
|
|
249
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
250
|
+
if (!short.ok)
|
|
251
|
+
return short.result;
|
|
252
|
+
// Attribution keys on the declared `endora.id`, never on a `modules/<id>/`
|
|
253
|
+
// path segment: D-141 is this repository's convention and a stranger's
|
|
254
|
+
// checkout has no reason to hold it.
|
|
255
|
+
const hostResident = new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]);
|
|
256
|
+
const findings = files.flatMap((file) => containerAnalyse(readFileSync(file, 'utf8'), layout.keyOf(file), hostResident));
|
|
257
|
+
return ran(id, readSize, findings.map((finding) => ({
|
|
258
|
+
rule: id,
|
|
259
|
+
key: `${finding.file}|${finding.specifier}`,
|
|
260
|
+
location: `${finding.file}:${finding.line}`,
|
|
261
|
+
message: `imports '${finding.specifier}'. A module sees exactly one kernel surface — ` +
|
|
262
|
+
`\`ModuleContext\` — and spells the container's vocabulary through \`ctx.asClass\` / ` +
|
|
263
|
+
`\`ctx.asFunction\` / \`ctx.asValue\`. A module that reaches the container directly ` +
|
|
264
|
+
`can register a route, a worker or a subscriber that never passes through its ` +
|
|
265
|
+
`gating seam, and nothing would notice.`,
|
|
266
|
+
})));
|
|
267
|
+
};
|
|
268
|
+
/* ----------------------------------------------------------- subscribe-seam */
|
|
269
|
+
const subscribeSeam = (layout) => {
|
|
270
|
+
const id = 'check:subscribe-seam';
|
|
271
|
+
const entry = entryOf(id);
|
|
272
|
+
const files = collectSeamFiles([layout.sourceRoot]);
|
|
273
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
274
|
+
if (isRefusal(coverage))
|
|
275
|
+
return unreadable(id, coverage.refusal, null);
|
|
276
|
+
if (coverage === null)
|
|
277
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
278
|
+
const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
|
|
279
|
+
const readSize = {
|
|
280
|
+
prefix: '[subscribe-seam]',
|
|
281
|
+
files: sources.size,
|
|
282
|
+
coverage: [coverage],
|
|
283
|
+
};
|
|
284
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
285
|
+
if (!short.ok)
|
|
286
|
+
return short.result;
|
|
287
|
+
const hostResidentModules = new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]);
|
|
288
|
+
const input = { sources, hostResidentModules };
|
|
289
|
+
const subscriptions = checkSubscribeSeam(input, {});
|
|
290
|
+
const workers = checkWorkerSeam(input, {});
|
|
291
|
+
const findings = [
|
|
292
|
+
...subscriptions.violations.map((site) => ({
|
|
293
|
+
rule: id,
|
|
294
|
+
key: subscriptionKeyOf(site),
|
|
295
|
+
location: `${site.file}:${site.line}`,
|
|
296
|
+
message: `${site.receiver}.on('${site.event}') is an EventBus subscription outside the ` +
|
|
297
|
+
`module's gating seam, so the handler keeps running with the module switched off ` +
|
|
298
|
+
`(Constitution XVII). Register it from \`backend.ts\` with ` +
|
|
299
|
+
`\`ctx.subscribe(event, handler)\`.`,
|
|
300
|
+
})),
|
|
301
|
+
...workers.violations.map((site) => ({
|
|
302
|
+
rule: id,
|
|
303
|
+
key: workerKeyOf(site),
|
|
304
|
+
location: `${site.file}:${site.line}`,
|
|
305
|
+
message: `${site.spelling} is a BullMQ queue consumer outside the module's gating seam, so ` +
|
|
306
|
+
`the platform cannot stop it: it is in no per-module registry and the presence ` +
|
|
307
|
+
`reconcile has nothing to reconcile. Hand it to \`ctx.worker(worker)\`.`,
|
|
308
|
+
})),
|
|
309
|
+
];
|
|
310
|
+
const result = ran(id, readSize, findings);
|
|
311
|
+
// The repository-scope host refuses a run that read no worker site at all,
|
|
312
|
+
// because *that* tree is known to hold queue consumers. One package holding
|
|
313
|
+
// none is the ordinary case, so the floor is stated rather than enforced.
|
|
314
|
+
if (workers.sites.length > 0) {
|
|
315
|
+
return { ...result, unevaluatedSignals: [] };
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
};
|
|
319
|
+
/* -------------------------------------------------------------- queue-names */
|
|
320
|
+
const queueNames = (layout) => {
|
|
321
|
+
const id = 'check:queue-names';
|
|
322
|
+
const entry = entryOf(id);
|
|
323
|
+
const files = collectQueueNameFiles([layout.sourceRoot]);
|
|
324
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
325
|
+
if (isRefusal(coverage))
|
|
326
|
+
return unreadable(id, coverage.refusal, null);
|
|
327
|
+
if (coverage === null)
|
|
328
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
329
|
+
const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
|
|
330
|
+
const readSize = {
|
|
331
|
+
prefix: '[queue-names]',
|
|
332
|
+
files: sources.size,
|
|
333
|
+
coverage: [coverage],
|
|
334
|
+
};
|
|
335
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
336
|
+
if (!short.ok)
|
|
337
|
+
return short.result;
|
|
338
|
+
const result = checkQueueNames({
|
|
339
|
+
sources,
|
|
340
|
+
hostResidentModules: new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]),
|
|
341
|
+
});
|
|
342
|
+
const findings = result.findings.map((finding) => ({
|
|
343
|
+
rule: id,
|
|
344
|
+
key: queueNameKeyOf(finding.site),
|
|
345
|
+
location: `${finding.site.file}:${finding.site.line}`,
|
|
346
|
+
message: queueNameRemedy(finding),
|
|
347
|
+
}));
|
|
348
|
+
const outcome = ran(id, { ...readSize, sites: result.sites.length }, findings);
|
|
349
|
+
// The repository-scope host refuses a run that resolved no queue name at all,
|
|
350
|
+
// because *that* tree is known to hold queues. One package holding none is
|
|
351
|
+
// the ordinary case — most modules ship no worker — so the floor is stated
|
|
352
|
+
// rather than enforced.
|
|
353
|
+
if (result.resolved.length > 0) {
|
|
354
|
+
return { ...outcome, unevaluatedSignals: [] };
|
|
355
|
+
}
|
|
356
|
+
return outcome;
|
|
357
|
+
};
|
|
358
|
+
/* --------------------------------------------------------- command-coverage */
|
|
359
|
+
/**
|
|
360
|
+
* Principle XIII, and the one rule here whose repository host stages its
|
|
361
|
+
* findings.
|
|
362
|
+
*
|
|
363
|
+
* That staging is `MIGRATED_MODULES`, a rollout ledger of *these* modules, and a
|
|
364
|
+
* third-party package is in no rollout. So every finding is blocking, which is
|
|
365
|
+
* what `--strict` already means in this repository and what CI already runs.
|
|
366
|
+
*/
|
|
367
|
+
const commandCoverage = (layout) => {
|
|
368
|
+
const id = 'check:command-coverage';
|
|
369
|
+
const entry = entryOf(id);
|
|
370
|
+
const files = collectScannedFiles(layout.sourceRoot);
|
|
371
|
+
// The rule's own membership predicate, not a `.ts` test: this walk prunes
|
|
372
|
+
// `migrations` by design, so a package that publishes a `./migrations` layer
|
|
373
|
+
// must not be reported short for a layer the rule excludes.
|
|
374
|
+
const coverage = coverageOf(layout, files, (path) => commandCoverageOpens(path, layout.sourceRoot));
|
|
375
|
+
if (isRefusal(coverage))
|
|
376
|
+
return unreadable(id, coverage.refusal, null);
|
|
377
|
+
if (coverage === null)
|
|
378
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
379
|
+
const readSize = {
|
|
380
|
+
prefix: '[command-coverage]',
|
|
381
|
+
files: files.length,
|
|
382
|
+
coverage: [coverage],
|
|
383
|
+
};
|
|
384
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
385
|
+
if (!short.ok)
|
|
386
|
+
return short.result;
|
|
387
|
+
const findings = files.flatMap((file) => {
|
|
388
|
+
const key = layout.keyOf(file);
|
|
389
|
+
return commandCoverageAnalyse(key, readFileSync(file, 'utf8'));
|
|
390
|
+
});
|
|
391
|
+
return ran(id, readSize, findings.map((finding) => ({
|
|
392
|
+
rule: id,
|
|
393
|
+
key: `${finding.kind}|${finding.filePath}|${finding.method}`,
|
|
394
|
+
location: `${finding.filePath}:${finding.line ?? '?'}`,
|
|
395
|
+
message: `${finding.kind}: ${finding.message}`,
|
|
396
|
+
})));
|
|
397
|
+
};
|
|
398
|
+
/* ------------------------------------------------------- channel:resolution */
|
|
399
|
+
/**
|
|
400
|
+
* Constitution XII — the request's sales channel is resolved once, by the
|
|
401
|
+
* canonical resolver, and no module re-derives it.
|
|
402
|
+
*
|
|
403
|
+
* Unconditional: every module's backend sources are the subject, and there is
|
|
404
|
+
* no declaration a package can decline to make. This repository's rollout
|
|
405
|
+
* allow-list does not travel — a stranger's package is in no rollout of ours —
|
|
406
|
+
* so every violation is blocking.
|
|
407
|
+
*/
|
|
408
|
+
const channelResolution = (layout) => {
|
|
409
|
+
const id = 'channel:resolution';
|
|
410
|
+
const entry = entryOf(id);
|
|
411
|
+
const files = collectChannelSources(layout.sourceRoot);
|
|
412
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
413
|
+
if (isRefusal(coverage))
|
|
414
|
+
return unreadable(id, coverage.refusal, null);
|
|
415
|
+
if (coverage === null)
|
|
416
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
417
|
+
const readSize = {
|
|
418
|
+
prefix: '[channel-resolution]',
|
|
419
|
+
files: files.length,
|
|
420
|
+
coverage: [coverage],
|
|
421
|
+
};
|
|
422
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
423
|
+
if (!short.ok)
|
|
424
|
+
return short.result;
|
|
425
|
+
const findings = files.flatMap((file) => channelAnalyse(readFileSync(file, 'utf8'), layout.keyOf(file)));
|
|
426
|
+
return ran(id, readSize, findings.map((violation) => ({
|
|
427
|
+
rule: id,
|
|
428
|
+
key: `${violation.file}|${violation.kind}|${violation.line}`,
|
|
429
|
+
location: `${violation.file}:${violation.line}`,
|
|
430
|
+
message: `${violation.kind}: ${violation.detail}. The current sales channel is resolved once, ` +
|
|
431
|
+
`by the platform's own resolver, and read through \`getResolvedChannel()\` — a module ` +
|
|
432
|
+
`that re-derives it answers a different question from the one the request asked ` +
|
|
433
|
+
`(Constitution XII).`,
|
|
434
|
+
})));
|
|
435
|
+
};
|
|
436
|
+
/* -------------------------------------------------- default-language-prose */
|
|
437
|
+
/**
|
|
438
|
+
* The owner ruling of 2026-09-01, clause 1: a module's own prose is English by
|
|
439
|
+
* default.
|
|
440
|
+
*
|
|
441
|
+
* Unconditional, and the ledger is the **package's** — this repository's shards
|
|
442
|
+
* are a statement about its own 46 sites and a stranger's package predates none
|
|
443
|
+
* of them, so the analysis is handed no shard and the author's own
|
|
444
|
+
* acknowledgements are applied by `ledger.ts` over every rule at once.
|
|
445
|
+
*
|
|
446
|
+
* Detection is Polish only, which is the rule's declared bound rather than this
|
|
447
|
+
* host's: `DETECTED_LANGUAGES` is reconciled against the platform's shipped
|
|
448
|
+
* languages on the `read:` line, so a third shipped language is a short walk
|
|
449
|
+
* here exactly as it is in this repository.
|
|
450
|
+
*/
|
|
451
|
+
const defaultLanguageProse = (layout) => {
|
|
452
|
+
const id = 'check:default-language-prose';
|
|
453
|
+
const entry = entryOf(id);
|
|
454
|
+
const files = collectProseSources(layout.sourceRoot);
|
|
455
|
+
// The rule's own membership predicate, not a `.ts` test: this walk prunes
|
|
456
|
+
// `migrations` by design (an applied migration cannot be edited, so a ledger
|
|
457
|
+
// entry over a literal in one would never drain), so a package that publishes
|
|
458
|
+
// a `./migrations` layer must not be reported short for a layer the rule
|
|
459
|
+
// excludes.
|
|
460
|
+
const coverage = coverageOf(layout, files, (path) => defaultLanguageProseOpens(path, layout.sourceRoot));
|
|
461
|
+
if (isRefusal(coverage))
|
|
462
|
+
return unreadable(id, coverage.refusal, null);
|
|
463
|
+
if (coverage === null)
|
|
464
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
465
|
+
const languages = [...SUPPORTED_LANGUAGES];
|
|
466
|
+
const detectable = languages.filter((language) => language === 'en' || DETECTED_LANGUAGES.includes(language.split('-')[0] ?? ''));
|
|
467
|
+
const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
|
|
468
|
+
const result = checkDefaultLanguageProse({ sources, languages }, [], () => layout.moduleId);
|
|
469
|
+
const readSize = {
|
|
470
|
+
prefix: '[default-language-prose]',
|
|
471
|
+
files: sources.size,
|
|
472
|
+
sites: result.classified,
|
|
473
|
+
coverage: [
|
|
474
|
+
coverage,
|
|
475
|
+
{ source: 'detected-languages', expected: languages.length, covered: detectable.length },
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
479
|
+
if (!short.ok)
|
|
480
|
+
return short.result;
|
|
481
|
+
return ran(id, readSize, result.violations.map((site) => ({
|
|
482
|
+
rule: id,
|
|
483
|
+
key: proseLedgerKey(site),
|
|
484
|
+
location: `${site.file}:${site.line}`,
|
|
485
|
+
message: `${site.kind}: ${site.language} prose in a ${site.placement} — ${site.text}`,
|
|
486
|
+
})));
|
|
487
|
+
};
|
|
488
|
+
/* ------------------------------------------------------- diacritic-folds */
|
|
489
|
+
/**
|
|
490
|
+
* Issues #240 and #245 — the fold and the slug builder have one owner.
|
|
491
|
+
*
|
|
492
|
+
* The package is one population root, and `SHARED_FOLD_HELPER` is *right* to
|
|
493
|
+
* match nothing here: a package has no exempt file. It imports `foldDiacritics`
|
|
494
|
+
* and `slugify` from `@endora-commerce/contracts` like every other consumer,
|
|
495
|
+
* which is exactly what the exemption exists to make true — so both ledgers are
|
|
496
|
+
* empty and every finding is blocking.
|
|
497
|
+
*/
|
|
498
|
+
const diacriticFolds = (layout) => {
|
|
499
|
+
const id = 'check:diacritic-folds';
|
|
500
|
+
const entry = entryOf(id);
|
|
501
|
+
// One root, named after the package's own source directory, so the rule's
|
|
502
|
+
// population predicate answers for a package-relative key exactly as it does
|
|
503
|
+
// for a repo-relative one.
|
|
504
|
+
const rootName = layout.keyOf(layout.sourceRoot);
|
|
505
|
+
const roots = { [rootName]: 'the package under check — its whole source tree.' };
|
|
506
|
+
const scanned = [];
|
|
507
|
+
for (const file of collectFoldSources(layout.sourceRoot)) {
|
|
508
|
+
const path = layout.keyOf(file);
|
|
509
|
+
if (!foldsScannablePath(path, roots))
|
|
510
|
+
continue;
|
|
511
|
+
scanned.push({ path, source: readFileSync(file, 'utf8') });
|
|
512
|
+
}
|
|
513
|
+
const coverage = coverageOf(layout, scanned.map((file) => join(layout.packageRoot, file.path)),
|
|
514
|
+
// `layerExpectation` hands an **absolute** layer entry; the rule's predicate
|
|
515
|
+
// reads the key namespace the walk keyed with, so it is asked in that one.
|
|
516
|
+
(absolute) => foldsScannablePath(layout.keyOf(absolute), roots));
|
|
517
|
+
if (isRefusal(coverage))
|
|
518
|
+
return unreadable(id, coverage.refusal, null);
|
|
519
|
+
if (coverage === null)
|
|
520
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
521
|
+
const result = checkDiacriticFolds(scanned, {}, {}, roots);
|
|
522
|
+
const readSize = {
|
|
523
|
+
prefix: '[diacritic-folds]',
|
|
524
|
+
files: result.scanned,
|
|
525
|
+
sites: result.replaceSites,
|
|
526
|
+
coverage: [coverage],
|
|
527
|
+
};
|
|
528
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
529
|
+
if (!short.ok)
|
|
530
|
+
return short.result;
|
|
531
|
+
return ran(id, readSize, result.violations.map((finding) => ({
|
|
532
|
+
rule: id,
|
|
533
|
+
key: `${finding.kind}|${finding.path}|${finding.literal}`,
|
|
534
|
+
location: `${finding.path}:${finding.line}:${finding.column}`,
|
|
535
|
+
message: `${finding.kind}: ${finding.literal}. Import \`foldDiacritics\` or \`slugify\` from ` +
|
|
536
|
+
`\`@endora-commerce/contracts\` — the obvious one-liner reads as complete and is ` +
|
|
537
|
+
`not: \`ł\` has no canonical decomposition, so NFD leaves it alone and the strip ` +
|
|
538
|
+
`has nothing to remove.`,
|
|
539
|
+
})));
|
|
540
|
+
};
|
|
541
|
+
/* ------------------------------------------------------------- entry-scope */
|
|
542
|
+
/**
|
|
543
|
+
* Feature 072 FR-020 — a non-HTTP entry point establishes its own scope.
|
|
544
|
+
*
|
|
545
|
+
* The subject is a `package.json` script running a source path, a worker, a
|
|
546
|
+
* repeating timer or a `process.on` handler. A package with none of those has
|
|
547
|
+
* nothing to scope, and the rule says so rather than reporting clean over a
|
|
548
|
+
* population it never had (`exit-reduction.md` §2).
|
|
549
|
+
*/
|
|
550
|
+
const entryScope = (layout) => {
|
|
551
|
+
const id = 'check:entry-scope';
|
|
552
|
+
const entry = entryOf(id);
|
|
553
|
+
const files = collectEntryScopeSources(layout.sourceRoot);
|
|
554
|
+
const coverage = coverageOf(layout, files, (path) => path.endsWith('.ts') && !path.endsWith('.d.ts'));
|
|
555
|
+
if (isRefusal(coverage))
|
|
556
|
+
return unreadable(id, coverage.refusal, null);
|
|
557
|
+
if (coverage === null)
|
|
558
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
559
|
+
const declared = declaredProgramsOf(layout);
|
|
560
|
+
const sites = files.flatMap((file) => findEntrySites(file, readFileSync(file, 'utf8'), declared, layout.keyOf));
|
|
561
|
+
if (sites.length === 0)
|
|
562
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
563
|
+
const readSize = {
|
|
564
|
+
prefix: '[entry-scope]',
|
|
565
|
+
files: files.length,
|
|
566
|
+
sites: sites.length,
|
|
567
|
+
coverage: [coverage],
|
|
568
|
+
};
|
|
569
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
570
|
+
if (!short.ok)
|
|
571
|
+
return short.result;
|
|
572
|
+
return ran(id, readSize, entryScopeViolations(sites, {}).map((site) => ({
|
|
573
|
+
rule: id,
|
|
574
|
+
key: entryScopeKeyOf(site),
|
|
575
|
+
location: `${site.file}:${site.line}`,
|
|
576
|
+
message: `${site.kind}/${site.construct} in \`${site.scheduler}\` establishes no scope. A ` +
|
|
577
|
+
`non-HTTP entry point has no caller to answer, so it opens its own: call ` +
|
|
578
|
+
`\`enterPlatformScope\` or \`enterSystemScope\` in the callback itself.`,
|
|
579
|
+
})));
|
|
580
|
+
};
|
|
581
|
+
/* ---------------------------------------------------------- kernel-boundary */
|
|
582
|
+
/**
|
|
583
|
+
* Feature 072 D-32, rule A — an ORM relation stays inside its own module or
|
|
584
|
+
* points at the platform.
|
|
585
|
+
*
|
|
586
|
+
* Rules B and C are the platform roots' and a package holds none; they are
|
|
587
|
+
* declared unevaluated on this rule's line rather than counted zero.
|
|
588
|
+
*/
|
|
589
|
+
const kernelBoundary = (layout) => {
|
|
590
|
+
const id = 'check:kernel-boundary';
|
|
591
|
+
const entry = entryOf(id);
|
|
592
|
+
const files = collectRelationSources(layout.sourceRoot);
|
|
593
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
594
|
+
if (isRefusal(coverage))
|
|
595
|
+
return unreadable(id, coverage.refusal, null);
|
|
596
|
+
if (coverage === null)
|
|
597
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
598
|
+
const hostResident = hostResidentOf(layout);
|
|
599
|
+
const relationFiles = files.filter((file) => RELATION_DECORATOR_HINT.test(readFileSync(file, 'utf8')));
|
|
600
|
+
const readSize = {
|
|
601
|
+
prefix: '[kernel-boundary]',
|
|
602
|
+
files: files.length,
|
|
603
|
+
sites: relationFiles.length,
|
|
604
|
+
coverage: [coverage],
|
|
605
|
+
};
|
|
606
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
607
|
+
if (!short.ok)
|
|
608
|
+
return short.result;
|
|
609
|
+
const findings = relationFiles.flatMap((file) => relationAnalyse(readFileSync(file, 'utf8'), file, hostResident));
|
|
610
|
+
return ran(id, readSize, findings.filter(isRelationViolation).map((finding) => ({
|
|
611
|
+
rule: id,
|
|
612
|
+
key: relationKey(finding),
|
|
613
|
+
location: layout.keyOf(finding.file),
|
|
614
|
+
message: `${finding.className}.${finding.property} (@${finding.decorator}) relates ` +
|
|
615
|
+
`${finding.sourceOwner} -> ${finding.targetOwner}. A relation across a module ` +
|
|
616
|
+
`boundary is a foreign key the ORM will create, in a schema neither module can be ` +
|
|
617
|
+
`detached from (Constitution I). Reach the other module through its port instead.`,
|
|
618
|
+
})));
|
|
619
|
+
};
|
|
620
|
+
/* --------------------------------------------------------- platform-surface */
|
|
621
|
+
/**
|
|
622
|
+
* D-160.8 — a module reaches only the platform surface the host publishes.
|
|
623
|
+
*
|
|
624
|
+
* The relative-specifier half is vacuous here and is declared vacuous rather
|
|
625
|
+
* than counted zero: a module in a package reaches the host by **bare**
|
|
626
|
+
* specifier only. The host package is the installed `@endora-commerce/platform`,
|
|
627
|
+
* whose barrels this run reads; without it every host reach is `unreadable` and
|
|
628
|
+
* no rule is reported clean on that basis.
|
|
629
|
+
*/
|
|
630
|
+
const platformSurface = (layout) => {
|
|
631
|
+
const id = 'check:platform-surface';
|
|
632
|
+
const entry = entryOf(id);
|
|
633
|
+
const files = collectPlatformSurfaceSources(layout.sourceRoot);
|
|
634
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
635
|
+
if (isRefusal(coverage))
|
|
636
|
+
return unreadable(id, coverage.refusal, null);
|
|
637
|
+
if (coverage === null)
|
|
638
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
639
|
+
const host = installedPlatform(layout);
|
|
640
|
+
if (host === null) {
|
|
641
|
+
return unreadable(id, `\`@endora-commerce/platform\` is not installed beside this package, so the surface ` +
|
|
642
|
+
`it publishes cannot be read and a reach into it cannot be judged. Install the ` +
|
|
643
|
+
`platform — a shorter published set reports *fewer* findings, which is why this is ` +
|
|
644
|
+
`a refusal rather than a clean run.`, null);
|
|
645
|
+
}
|
|
646
|
+
const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
|
|
647
|
+
const readSize = {
|
|
648
|
+
prefix: '[platform-surface]',
|
|
649
|
+
files: sources.size,
|
|
650
|
+
coverage: [
|
|
651
|
+
coverage,
|
|
652
|
+
{ source: 'platform-barrels', expected: host.barrels, covered: host.barrels },
|
|
653
|
+
],
|
|
654
|
+
};
|
|
655
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
656
|
+
if (!short.ok)
|
|
657
|
+
return short.result;
|
|
658
|
+
const result = checkPlatformSurface({
|
|
659
|
+
sources,
|
|
660
|
+
files: new Set(sources.keys()),
|
|
661
|
+
surface: host.surface,
|
|
662
|
+
moduleIdOf: () => layout.moduleId,
|
|
663
|
+
canonicalTargetOf: (target) => target,
|
|
664
|
+
host: host.package,
|
|
665
|
+
platformSourceRoot: host.sourceRoot,
|
|
666
|
+
}, {});
|
|
667
|
+
return ran(id, readSize, result.violations.map((finding) => ({
|
|
668
|
+
rule: id,
|
|
669
|
+
key: platformSurfaceKeyOf(finding),
|
|
670
|
+
location: `${finding.file}:${finding.line}`,
|
|
671
|
+
message: `${finding.kind}: ${platformSurfaceRemedy(finding)}`,
|
|
672
|
+
})));
|
|
673
|
+
};
|
|
674
|
+
/* --------------------------------------------------------------- port-shape */
|
|
675
|
+
/**
|
|
676
|
+
* D-97.3 and issue #192 — signals 1 and 2 on a published port.
|
|
677
|
+
*
|
|
678
|
+
* Signal 3 asks whether a module resolves a container name **no contract
|
|
679
|
+
* publishes**, which needs the published surface of every installed peer; it is
|
|
680
|
+
* declared unevaluated on the rule's own line. Both ledgers and both
|
|
681
|
+
* platform-name sets are empty, which is what makes the two signals that do run
|
|
682
|
+
* blocking.
|
|
683
|
+
*/
|
|
684
|
+
const portShape = (layout) => {
|
|
685
|
+
const id = 'check:port-shape';
|
|
686
|
+
const entry = entryOf(id);
|
|
687
|
+
const ports = layout.layers.find((layer) => layer.subpath === './ports');
|
|
688
|
+
if (ports === undefined)
|
|
689
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
690
|
+
const portFiles = collectPortShapeSources(ports.directory);
|
|
691
|
+
const moduleFiles = collectPortShapeSources(layout.sourceRoot);
|
|
692
|
+
const coverage = coverageOf(layout, moduleFiles, opensTypeScript);
|
|
693
|
+
if (isRefusal(coverage))
|
|
694
|
+
return unreadable(id, coverage.refusal, null);
|
|
695
|
+
if (coverage === null)
|
|
696
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
697
|
+
const readSize = {
|
|
698
|
+
prefix: '[port-shape]',
|
|
699
|
+
files: moduleFiles.length,
|
|
700
|
+
sites: portFiles.length,
|
|
701
|
+
coverage: [
|
|
702
|
+
coverage,
|
|
703
|
+
{ source: 'ports-subpaths', expected: 1, covered: portFiles.length > 0 ? 1 : 0 },
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
707
|
+
if (!short.ok)
|
|
708
|
+
return short.result;
|
|
709
|
+
const keyed = (list) => new Map(list.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
|
|
710
|
+
const result = checkPortShape({
|
|
711
|
+
contracts: new Map(),
|
|
712
|
+
modules: keyed(moduleFiles),
|
|
713
|
+
modulePorts: keyed(portFiles),
|
|
714
|
+
hostResidentModules: hostResidentOf(layout),
|
|
715
|
+
});
|
|
716
|
+
// Signal 1 only. Signal 2 compares a doc block's container name to the name
|
|
717
|
+
// the port is **registered** under, and a port's provider is routinely another
|
|
718
|
+
// module — `orders` publishes `PaymentPlacementApplyPort` and `payments`
|
|
719
|
+
// registers it — so over one package every such port reads
|
|
720
|
+
// `container-name-unregistered`. That is the state §5 of
|
|
721
|
+
// `contracts/package-scope-layout.md` rules on: a port whose owner is not
|
|
722
|
+
// installed is `unreadable` **for that edge**, never unowned, because the
|
|
723
|
+
// wiring may be right and the map short. Both signals are declared on the
|
|
724
|
+
// rule's own line rather than counted zero.
|
|
725
|
+
return ran(id, readSize, result.findings.map((finding) => ({
|
|
726
|
+
rule: id,
|
|
727
|
+
key: `${finding.kind}|${finding.portName}|${finding.member}`,
|
|
728
|
+
location: `${finding.file}:${finding.line}`,
|
|
729
|
+
message: `${finding.kind}: ${finding.portName}.${finding.member} — feature detection through a ` +
|
|
730
|
+
`port is impossible by construction. \`lazyPort\`'s proxy answers every property ` +
|
|
731
|
+
`with a function, so \`if (port.maybe)\` is always true and the forward throws when ` +
|
|
732
|
+
`the provider has none.`,
|
|
733
|
+
})));
|
|
734
|
+
};
|
|
735
|
+
/* ------------------------------------------------------ transaction-context */
|
|
736
|
+
/**
|
|
737
|
+
* Issue #200 — SQL written inside a transaction that does not run inside it.
|
|
738
|
+
*
|
|
739
|
+
* The subject is the package's backend sources, which is what an `exports`
|
|
740
|
+
* subpath publishing them declares. The ledger argument is the **package's**,
|
|
741
|
+
* never this repository's: `CONNECTION_LEVEL_SQL_IN_TRANSACTIONS` is a statement
|
|
742
|
+
* about this tree's debt and a stranger's package predates none of it, so the
|
|
743
|
+
* analysis is handed an empty map and the author's own acknowledgements are
|
|
744
|
+
* applied later, by `ledger.ts`, over every rule at once.
|
|
745
|
+
*/
|
|
746
|
+
const transactionContext = (layout) => {
|
|
747
|
+
const id = 'check:transaction-context';
|
|
748
|
+
const entry = entryOf(id);
|
|
749
|
+
const files = collectTransactionSources(layout.sourceRoot);
|
|
750
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
751
|
+
if (isRefusal(coverage))
|
|
752
|
+
return unreadable(id, coverage.refusal, null);
|
|
753
|
+
if (coverage === null)
|
|
754
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
755
|
+
const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
|
|
756
|
+
const readSize = {
|
|
757
|
+
prefix: '[transaction-context]',
|
|
758
|
+
files: sources.size,
|
|
759
|
+
coverage: [coverage],
|
|
760
|
+
};
|
|
761
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
762
|
+
if (!short.ok)
|
|
763
|
+
return short.result;
|
|
764
|
+
const result = checkTransactionContext({ sources }, {});
|
|
765
|
+
return ran(id, readSize, result.violations.map((escape) => ({
|
|
766
|
+
rule: id,
|
|
767
|
+
key: transactionKeyOf(escape),
|
|
768
|
+
location: `${escape.file}:${escape.line}`,
|
|
769
|
+
message: `[${escape.scope}/${escape.shape}/${escape.direction}] ${escape.statement} — a ` +
|
|
770
|
+
`connection-level handle takes its own pooled connection, so this statement ` +
|
|
771
|
+
`commits the moment it runs: the enclosing rollback cannot reach it and a read ` +
|
|
772
|
+
`cannot see what the transaction has written. Use the EntityManager's own ` +
|
|
773
|
+
`\`em.execute(sql, params)\`, which passes the transaction context and is ` +
|
|
774
|
+
`identical outside a transaction.`,
|
|
775
|
+
})));
|
|
776
|
+
};
|
|
777
|
+
/** `[<package-relative source root>] -> <module id>`, the attribution a package declares. */
|
|
778
|
+
function hostResidentOf(layout) {
|
|
779
|
+
return new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* The `package.json` scripts that run a source path — `check:entry-scope`'s
|
|
783
|
+
* second population source (issue #228), spelled the same way its keys are.
|
|
784
|
+
*/
|
|
785
|
+
function declaredProgramsOf(layout) {
|
|
786
|
+
try {
|
|
787
|
+
const manifest = JSON.parse(readFileSync(join(layout.packageRoot, 'package.json'), 'utf8'));
|
|
788
|
+
return new Set(declaredProgramEntryPoints(JSON.stringify({ scripts: manifest.scripts ?? {} })));
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
return new Set();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* The installed `@endora-commerce/platform`, its published barrels and the
|
|
796
|
+
* surface they declare — or `null` when it is not installed.
|
|
797
|
+
*
|
|
798
|
+
* `check:platform-surface` refuses rather than degrades on its absence, because
|
|
799
|
+
* a shorter published set reports *fewer* findings: the obvious repair for a
|
|
800
|
+
* finding is to widen the barrel, and the whole of D-160.8 is that the barrel is
|
|
801
|
+
* not widened quietly.
|
|
802
|
+
*/
|
|
803
|
+
function installedPlatform(layout) {
|
|
804
|
+
const dir = join(layout.packageRoot, 'node_modules', '@endora-commerce', 'platform');
|
|
805
|
+
if (!isFile(join(dir, 'package.json')))
|
|
806
|
+
return null;
|
|
807
|
+
let manifest;
|
|
808
|
+
try {
|
|
809
|
+
manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
const name = manifest.name;
|
|
815
|
+
if (typeof name !== 'string')
|
|
816
|
+
return null;
|
|
817
|
+
const barrelSources = new Map();
|
|
818
|
+
const barrelTargets = new Map();
|
|
819
|
+
for (const subpath of PUBLISHED_SUBPATHS) {
|
|
820
|
+
const source = join(dir, 'src', subpath, 'index.ts');
|
|
821
|
+
if (!isFile(source))
|
|
822
|
+
continue;
|
|
823
|
+
barrelSources.set(barrelKeyOf(subpath), readFileSync(source, 'utf8'));
|
|
824
|
+
barrelTargets.set(subpath, barrelKeyOf(subpath));
|
|
825
|
+
}
|
|
826
|
+
if (barrelSources.size === 0)
|
|
827
|
+
return null;
|
|
828
|
+
// The installed host's own `exports` map, which declares more than the five
|
|
829
|
+
// barrels: `./composition` is host composition surface no module may name
|
|
830
|
+
// (D-160.14), and telling its author that the map "refuses the path at
|
|
831
|
+
// resolution time" would be false — it resolves.
|
|
832
|
+
const declaredSubpaths = new Set(Object.keys(manifest.exports ?? {})
|
|
833
|
+
.filter((key) => key.startsWith('./') && key !== './package.json')
|
|
834
|
+
.map((key) => key.slice(2)));
|
|
835
|
+
const surface = publishedSurface(barrelSources, (fromKey, specifier) => resolvePlatformTarget(fromKey, specifier, dir));
|
|
836
|
+
return {
|
|
837
|
+
package: { name, subpathTargets: barrelTargets, declaredSubpaths },
|
|
838
|
+
surface,
|
|
839
|
+
sourceRoot: 'src',
|
|
840
|
+
barrels: barrelSources.size,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/** A barrel's relative specifier, resolved against the installed platform's sources. */
|
|
844
|
+
function resolvePlatformTarget(fromKey, specifier, dir) {
|
|
845
|
+
const from = join(dir, 'src', fromKey.replace(/^src\//, ''));
|
|
846
|
+
for (const candidate of [
|
|
847
|
+
resolve(dirname(from), `${specifier.replace(/\.js$/, '')}.ts`),
|
|
848
|
+
resolve(dirname(from), specifier.replace(/\.js$/, ''), 'index.ts'),
|
|
849
|
+
]) {
|
|
850
|
+
if (isFile(candidate))
|
|
851
|
+
return posixKey(relative(dir, candidate));
|
|
852
|
+
}
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
function posixKey(path) {
|
|
856
|
+
return path.split(sep).join('/');
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* The module manifest, read where the **platform** reads it.
|
|
860
|
+
*
|
|
861
|
+
* A rule whose subject is something the platform *loads* reads the emitted file
|
|
862
|
+
* — that is not a shortcut, it is the only thing a published package has, and
|
|
863
|
+
* answering from source would make this command's verdict differ from the
|
|
864
|
+
* platform's, which is the one thing a conformance command must not do
|
|
865
|
+
* (`contracts/package-scope-layout.md` §4). The artefact's currency is decided
|
|
866
|
+
* first: a source strictly newer than its emitted target is `stale-artefact`,
|
|
867
|
+
* and is never answered from source as a convenience.
|
|
868
|
+
*
|
|
869
|
+
* It is read as **text through the compiler API** rather than `await import`ed,
|
|
870
|
+
* for two reasons and not one. A host is synchronous, which is Phase 1's shape
|
|
871
|
+
* and the reason the whole estate can be iterated in one pass; and an import
|
|
872
|
+
* evaluates whatever the artefact's own import graph reaches, which for a module
|
|
873
|
+
* is the platform an author may not have installed. Reading literal AST nodes
|
|
874
|
+
* costs the same discipline every analysis in this estate already keeps.
|
|
875
|
+
*
|
|
876
|
+
* **One reader, and it fails closed.** A field it cannot read as a literal is
|
|
877
|
+
* `null` / absent rather than guessed at, and the rule that needs it reports
|
|
878
|
+
* `unreadable` — never a clean run over a manifest this run could not see.
|
|
879
|
+
*/
|
|
880
|
+
function readEmittedManifest(layout) {
|
|
881
|
+
const root = layout.layers.find((layer) => layer.subpath === '.');
|
|
882
|
+
if (root === undefined)
|
|
883
|
+
return null;
|
|
884
|
+
const artefact = join(layout.packageRoot, ...root.target.replace(/^\.\//, '').split('/'));
|
|
885
|
+
try {
|
|
886
|
+
const artefactStat = statSync(artefact);
|
|
887
|
+
const sourceStat = statSync(root.entry);
|
|
888
|
+
if (sourceStat.mtimeMs > artefactStat.mtimeMs)
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
const sf = ts.createSourceFile(artefact, readFileSync(artefact, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
895
|
+
let bundlesDir = null;
|
|
896
|
+
let nonDeactivatable = false;
|
|
897
|
+
const actions = [];
|
|
898
|
+
const literal = (node) => node !== undefined && ts.isStringLiteralLike(node) ? node.text : null;
|
|
899
|
+
const property = (object, name) => object.properties.find((member) => ts.isPropertyAssignment(member) &&
|
|
900
|
+
(ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
|
|
901
|
+
member.name.text === name)?.initializer;
|
|
902
|
+
const visit = (node) => {
|
|
903
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
904
|
+
const dir = literal(property(node, 'bundlesDir'));
|
|
905
|
+
if (dir !== null)
|
|
906
|
+
bundlesDir = dir;
|
|
907
|
+
const activation = property(node, 'activation');
|
|
908
|
+
if (activation !== undefined && ts.isObjectLiteralExpression(activation)) {
|
|
909
|
+
const locked = property(activation, 'nonDeactivatable');
|
|
910
|
+
if (locked !== undefined && locked.kind === ts.SyntaxKind.TrueKeyword) {
|
|
911
|
+
nonDeactivatable = true;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const declared = property(node, 'actions');
|
|
915
|
+
if (declared !== undefined && ts.isArrayLiteralExpression(declared)) {
|
|
916
|
+
for (const element of declared.elements) {
|
|
917
|
+
if (!ts.isObjectLiteralExpression(element))
|
|
918
|
+
continue;
|
|
919
|
+
const actionId = literal(property(element, 'id'));
|
|
920
|
+
const targetRoute = literal(property(element, 'targetRoute'));
|
|
921
|
+
if (actionId === null || targetRoute === null)
|
|
922
|
+
continue;
|
|
923
|
+
const required = literal(property(element, 'requiredPermission'));
|
|
924
|
+
actions.push({
|
|
925
|
+
id: actionId,
|
|
926
|
+
targetRoute,
|
|
927
|
+
...(required === null ? {} : { requiredPermission: required }),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
node.forEachChild(visit);
|
|
933
|
+
};
|
|
934
|
+
sf.forEachChild(visit);
|
|
935
|
+
return { bundlesDir, actions, nonDeactivatable };
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Every file under `dir`, with the rule's own declared directory prunes applied.
|
|
939
|
+
*
|
|
940
|
+
* The prune is for speed and the population is {@link isScannablePath}'s, which
|
|
941
|
+
* is the rule — one decision, taken in one place, exactly as the repository
|
|
942
|
+
* host's walk takes it.
|
|
943
|
+
*/
|
|
944
|
+
function walkEverything(dir, out = []) {
|
|
945
|
+
let entries;
|
|
946
|
+
try {
|
|
947
|
+
entries = readdirSync(dir);
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
for (const name of entries) {
|
|
953
|
+
if (SKIPPED_DIRECTORIES[name] !== undefined)
|
|
954
|
+
continue;
|
|
955
|
+
const full = join(dir, name);
|
|
956
|
+
let stat;
|
|
957
|
+
try {
|
|
958
|
+
stat = statSync(full);
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (stat.isDirectory())
|
|
964
|
+
walkEverything(full, out);
|
|
965
|
+
else if (stat.isFile())
|
|
966
|
+
out.push(full);
|
|
967
|
+
}
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Every declared layer's artefact, with its currency decided.
|
|
972
|
+
*
|
|
973
|
+
* Decided per layer rather than for the package as a whole, because the two
|
|
974
|
+
* refusals name different remedies: `absent` says *build the package*, `stale`
|
|
975
|
+
* says *rebuild it*, and answering either from source would make this command's
|
|
976
|
+
* verdict differ from the platform's (§4).
|
|
977
|
+
*/
|
|
978
|
+
function artefactStates(layout) {
|
|
979
|
+
return layout.layers.map((layer) => {
|
|
980
|
+
const artefact = join(layout.packageRoot, ...layer.target.replace(/^\.\//, '').split('/'));
|
|
981
|
+
let state;
|
|
982
|
+
try {
|
|
983
|
+
const artefactStat = statSync(artefact);
|
|
984
|
+
const sourceStat = statSync(layer.entry);
|
|
985
|
+
state = sourceStat.mtimeMs > artefactStat.mtimeMs ? 'stale' : 'current';
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
state = 'absent';
|
|
989
|
+
}
|
|
990
|
+
return { layer, artefact, state };
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Principle XI's out-of-tree instrument, and the reason Phase 3 exists.
|
|
995
|
+
*
|
|
996
|
+
* The subject is *the entity classes this package hands the ORM*, so the read is
|
|
997
|
+
* the **artefact** (§4) — the platform composes a module package through its
|
|
998
|
+
* published output, and a rule that answered from source would pass a build that
|
|
999
|
+
* dropped the decorators, which is precisely the silence this rule exists to
|
|
1000
|
+
* remove.
|
|
1001
|
+
*
|
|
1002
|
+
* ## Which subpath, derived rather than spelled
|
|
1003
|
+
*
|
|
1004
|
+
* Nothing here spells `./backend`. The subpath that publishes entity classes is
|
|
1005
|
+
* *the one whose artefact declares an `entities` array* — the package's own
|
|
1006
|
+
* statement about itself, and the estate's own words for the declaration
|
|
1007
|
+
* (`subjectDeclaration`). A package whose artefacts declare none publishes no
|
|
1008
|
+
* entity class and is `not-applicable`; a package with an artefact this run
|
|
1009
|
+
* could not read is `unreadable`, because the layer it could not read is a layer
|
|
1010
|
+
* that might have been the one.
|
|
1011
|
+
*
|
|
1012
|
+
* ## The floor is the composition's own list
|
|
1013
|
+
*
|
|
1014
|
+
* `entities` is a finer and more independent author than the layer count: the
|
|
1015
|
+
* walk deliberately reaches exactly one layer, so a per-layer expectation would
|
|
1016
|
+
* read `1/1` by construction — the tautology §3 refuses. `declared-entities`
|
|
1017
|
+
* compares the classes the walk classified against the classes the composition
|
|
1018
|
+
* hands the ORM, and a short answer is a refusal. That is also the guard on the
|
|
1019
|
+
* emit shape itself: if TypeScript's decorator lowering changed, this walk would
|
|
1020
|
+
* classify nothing and the token would say so, rather than the run reporting
|
|
1021
|
+
* every entity classified over an artefact it could not read.
|
|
1022
|
+
*/
|
|
1023
|
+
const entityTenantClassification = (layout) => {
|
|
1024
|
+
const id = 'check-entity-tenant-classification';
|
|
1025
|
+
const entry = entryOf(id);
|
|
1026
|
+
if (layout.layerRefusal !== null)
|
|
1027
|
+
return unreadable(id, layout.layerRefusal, null);
|
|
1028
|
+
const states = artefactStates(layout);
|
|
1029
|
+
const stale = states.filter((state) => state.state === 'stale');
|
|
1030
|
+
if (stale.length > 0) {
|
|
1031
|
+
return unreadable(id, `the source behind ${stale
|
|
1032
|
+
.map((state) => state.layer.subpath)
|
|
1033
|
+
.join(', ')} is newer than the artefact the platform would load, so what this run read ` +
|
|
1034
|
+
`is not what the platform reads. Rebuild the package; this rule is never answered from ` +
|
|
1035
|
+
`source as a convenience.`, null);
|
|
1036
|
+
}
|
|
1037
|
+
const absent = states.filter((state) => state.state === 'absent');
|
|
1038
|
+
if (absent.length > 0) {
|
|
1039
|
+
return unreadable(id, `${absent
|
|
1040
|
+
.map((state) => state.layer.subpath)
|
|
1041
|
+
.join(', ')} is declared in the \`exports\` map and its target is not on disk, so the ` +
|
|
1042
|
+
`subpath that publishes this package's entity classes may be one this run could not ` +
|
|
1043
|
+
`open. Build the package (\`pnpm run build\` in its directory).`, null);
|
|
1044
|
+
}
|
|
1045
|
+
const publishing = states
|
|
1046
|
+
.map((state) => ({
|
|
1047
|
+
state,
|
|
1048
|
+
declared: declaredEntityClasses(readFileSync(state.artefact, 'utf8'), state.artefact),
|
|
1049
|
+
}))
|
|
1050
|
+
.find((candidate) => candidate.declared.length > 0);
|
|
1051
|
+
if (publishing === undefined)
|
|
1052
|
+
return notApplicable(id, absentDeclaration(entry));
|
|
1053
|
+
const files = walkEmitted(dirname(publishing.state.artefact));
|
|
1054
|
+
const findings = analyzeEmittedFiles(files);
|
|
1055
|
+
const classified = new Set(findings.map((finding) => finding.className));
|
|
1056
|
+
const readSize = {
|
|
1057
|
+
prefix: TENANT_PREFIX,
|
|
1058
|
+
files: files.length,
|
|
1059
|
+
sites: findings.length,
|
|
1060
|
+
coverage: [
|
|
1061
|
+
{
|
|
1062
|
+
source: 'declared-entities',
|
|
1063
|
+
expected: publishing.declared.length,
|
|
1064
|
+
covered: publishing.declared.filter((name) => classified.has(name)).length,
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
};
|
|
1068
|
+
const short = readSizeRefusal(readSize);
|
|
1069
|
+
if (short !== null) {
|
|
1070
|
+
return unreadable(id, `${short.message}. The classes this package's composition hands the ORM are the ` +
|
|
1071
|
+
`independent second author here: ${publishing.declared
|
|
1072
|
+
.filter((name) => !classified.has(name))
|
|
1073
|
+
.join(', ')} could not be read out of the artefact.`, readSize);
|
|
1074
|
+
}
|
|
1075
|
+
const outcome = classifyFindings(findings);
|
|
1076
|
+
return ran(id, readSize, [...outcome.unclassified, ...outcome.multiple].map((finding) => ({
|
|
1077
|
+
rule: id,
|
|
1078
|
+
key: `${layout.moduleId}|${finding.className}`,
|
|
1079
|
+
location: layout.keyOf(finding.file),
|
|
1080
|
+
message: tenantRemedy(finding),
|
|
1081
|
+
})));
|
|
1082
|
+
};
|
|
1083
|
+
/* --------------------------------------------- entry-presence (Phase 3) */
|
|
1084
|
+
/**
|
|
1085
|
+
* Constitution XVII's hardest seam: an entry point nothing can catch a throw
|
|
1086
|
+
* from decides presence *before* it works.
|
|
1087
|
+
*
|
|
1088
|
+
* ## Unconditional, and that is a correction
|
|
1089
|
+
*
|
|
1090
|
+
* The estate filed this rule's subject as *"module manifest declaring an
|
|
1091
|
+
* activation control"*, and a host built on that reading would have answered
|
|
1092
|
+
* `not-applicable` for a `nonDeactivatable` package. It is wrong, and the rule's
|
|
1093
|
+
* own input says so: `lockedModules` takes a module's **boot hooks** out of the
|
|
1094
|
+
* population and leaves its **timers** in — `_lifecycle`'s lease heartbeat is
|
|
1095
|
+
* ledgered rather than exempted, which is only meaningful because a locked
|
|
1096
|
+
* module's timers are still judged. So the declaration is corrected to `null`
|
|
1097
|
+
* and the manifest decides an *exemption*, not applicability.
|
|
1098
|
+
*
|
|
1099
|
+
* ## The manifest is read where it can be, and its absence over-reports loudly
|
|
1100
|
+
*
|
|
1101
|
+
* `lockedModules` defaults to empty on purpose: forgetting it widens the
|
|
1102
|
+
* population rather than narrowing it, so a caller that cannot read the manifest
|
|
1103
|
+
* over-reports instead of going quietly blind. A package whose artefact is not
|
|
1104
|
+
* current therefore still gets a run — the rule's subject is source text — and
|
|
1105
|
+
* the `locked-owner-exemption` signal is printed on its own line, because a
|
|
1106
|
+
* finding an author cannot reproduce is one they learn to ignore (§5.1). Where
|
|
1107
|
+
* the manifest *is* read the signal is dropped, which is what makes its presence
|
|
1108
|
+
* mean something.
|
|
1109
|
+
*
|
|
1110
|
+
* ## The ledger
|
|
1111
|
+
*
|
|
1112
|
+
* The repository's two ledgers are not consulted: they are entries about *these*
|
|
1113
|
+
* modules and a third-party package is in none of them. The package's own ledger
|
|
1114
|
+
* is applied by the frame, one layer up, exactly as for every other rule.
|
|
1115
|
+
*/
|
|
1116
|
+
const entryPresence = (layout) => {
|
|
1117
|
+
const id = 'check:entry-presence';
|
|
1118
|
+
const files = collectPresenceFiles([layout.sourceRoot]);
|
|
1119
|
+
const coverage = coverageOf(layout, files, opensTypeScript);
|
|
1120
|
+
if (isRefusal(coverage))
|
|
1121
|
+
return unreadable(id, coverage.refusal, null);
|
|
1122
|
+
if (coverage === null)
|
|
1123
|
+
return notApplicable(id, absentDeclaration(entryOf(id)));
|
|
1124
|
+
// Keyed **relative to the source root**, which is what the repository host's
|
|
1125
|
+
// keys are relative to (`modules/<id>/…` under `src/`). The analysis attributes
|
|
1126
|
+
// a file to a module through `moduleOf`, whose second source matches on the
|
|
1127
|
+
// tail after the last `/src/` — so a package-relative key would hand it
|
|
1128
|
+
// `/src/src/backend/…` and it would attribute nothing, and the rule would
|
|
1129
|
+
// report `violations=0` over every package for ever.
|
|
1130
|
+
const sources = new Map(files.map((file) => [posixKey(relative(layout.sourceRoot, file)), readFileSync(file, 'utf8')]));
|
|
1131
|
+
const readSize = {
|
|
1132
|
+
prefix: '[entry-presence]',
|
|
1133
|
+
files: sources.size,
|
|
1134
|
+
coverage: [coverage],
|
|
1135
|
+
};
|
|
1136
|
+
const short = readSizeOrShortWalk(id, readSize);
|
|
1137
|
+
if (!short.ok)
|
|
1138
|
+
return short.result;
|
|
1139
|
+
// Every top-level name under the source root belongs to this one module — the
|
|
1140
|
+
// package half of `module-roots`' own derivation (D-142: identity is
|
|
1141
|
+
// `endora.id`, never a directory name), taken off the walk rather than spelled.
|
|
1142
|
+
const hostResidentModules = new Map();
|
|
1143
|
+
for (const key of sources.keys()) {
|
|
1144
|
+
const [head] = key.split('/');
|
|
1145
|
+
if (head !== undefined && head.length > 0)
|
|
1146
|
+
hostResidentModules.set(head, layout.moduleId);
|
|
1147
|
+
}
|
|
1148
|
+
const manifest = readEmittedManifest(layout);
|
|
1149
|
+
const result = checkEntryPresence({
|
|
1150
|
+
sources,
|
|
1151
|
+
lockedModules: manifest !== null && manifest.nonDeactivatable ? new Set([layout.moduleId]) : new Set(),
|
|
1152
|
+
hostResidentModules,
|
|
1153
|
+
}, {});
|
|
1154
|
+
const outcome = ran(id, readSize, result.violations.map((finding) => ({
|
|
1155
|
+
rule: id,
|
|
1156
|
+
key: presenceKeyOf(finding),
|
|
1157
|
+
location: `${layout.keyOf(join(layout.sourceRoot, finding.file))}:${finding.line}`,
|
|
1158
|
+
message: presenceRemedy(finding),
|
|
1159
|
+
})));
|
|
1160
|
+
// The exemption was derived, so the degradation does not apply to this run.
|
|
1161
|
+
if (manifest !== null)
|
|
1162
|
+
return { ...outcome, unevaluatedSignals: [] };
|
|
1163
|
+
return outcome;
|
|
1164
|
+
};
|
|
1165
|
+
/* ------------------------------------------------ port-catches (Phase 3) */
|
|
1166
|
+
/**
|
|
1167
|
+
* A `catch` around a gated-port call may not swallow the module's presence
|
|
1168
|
+
* answer — Constitution XVII at the one seam where the throw has somewhere to go
|
|
1169
|
+
* and gets eaten anyway.
|
|
1170
|
+
*
|
|
1171
|
+
* ## The peer owner map is not optional here, and the measurement says why
|
|
1172
|
+
*
|
|
1173
|
+
* The analysis admits `lazyPort(ctx, '<name>')` as a port only when `<name>` is
|
|
1174
|
+
* in its owner map, and that map is built from `di.providePort` in the **owner's**
|
|
1175
|
+
* composing file. So a consuming package seeds nothing on its own. Over this
|
|
1176
|
+
* repository's module packages: 226 sites attributed to 41 packages by the
|
|
1177
|
+
* whole-tree run, **19** in 10 packages when each is analysed alone, and **31 of
|
|
1178
|
+
* the 41 lose every site** — the four PIM connectors and `product_feeds` among
|
|
1179
|
+
* them. {@link readPeerOwners} is the input that closes it, out of the installed
|
|
1180
|
+
* and workspace peers, synchronously and with no new dependency.
|
|
1181
|
+
*
|
|
1182
|
+
* ## `sources=owners:<n>/<m>`, and the floor that comes with it
|
|
1183
|
+
*
|
|
1184
|
+
* The estate's idiom for *"I read n of the m things I needed"* is the `read:`
|
|
1185
|
+
* line's `sources=` token, and this rule's `m` is the number of gated-port names
|
|
1186
|
+
* the package **writes** ({@link resolvedPortNames}) — a denominator the package
|
|
1187
|
+
* itself authors, independent of any owner map. `n` is how many of those an owner
|
|
1188
|
+
* could be found for. A reader, and a later ratchet, can then see a partially
|
|
1189
|
+
* resolved run for what it is instead of taking a caveat in prose on trust.
|
|
1190
|
+
*
|
|
1191
|
+
* The floor falls out of the same idiom rather than being bolted on: **`n === 0`
|
|
1192
|
+
* with `m > 0` is `unreadable`**, because a run that resolved none of the names
|
|
1193
|
+
* it was asked to resolve has judged nothing — which is `read-size.ts`'
|
|
1194
|
+
* `read-nothing` refusal one granularity in, and the same refusal the repository
|
|
1195
|
+
* host makes about an owner map that resolved zero. A package that writes no
|
|
1196
|
+
* `lazyPort` name at all has nothing to resolve and is not short: `m === 0` is
|
|
1197
|
+
* `ran`, and the token is omitted rather than printed `0/0`.
|
|
1198
|
+
*
|
|
1199
|
+
* ## `OWNER LOCKED` still over-reports, and still says so
|
|
1200
|
+
*
|
|
1201
|
+
* The estate's `owner-locked-merge` signal is unchanged and is printed whenever
|
|
1202
|
+
* the peers' **manifests** were not read — which is every package-scope run,
|
|
1203
|
+
* because `readPeerOwners` reads registrations out of artefacts and locks out of
|
|
1204
|
+
* manifests are a separate question. Absent, a locked owner's site reads as a
|
|
1205
|
+
* violation rather than as retired: over-reporting, the safe direction, declared.
|
|
1206
|
+
*/
|
|
1207
|
+
const portCatches = (layout) => {
|
|
1208
|
+
const id = 'check:port-catches';
|
|
1209
|
+
const files = collectPortCatchFiles([layout.sourceRoot]);
|
|
1210
|
+
const layerCoverage = coverageOf(layout, files, opensTypeScript);
|
|
1211
|
+
if (isRefusal(layerCoverage))
|
|
1212
|
+
return unreadable(id, layerCoverage.refusal, null);
|
|
1213
|
+
if (layerCoverage === null)
|
|
1214
|
+
return notApplicable(id, absentDeclaration(entryOf(id)));
|
|
1215
|
+
// Keyed relative to the source root, for the same reason `check:entry-presence`
|
|
1216
|
+
// is: `moduleOf` matches on the tail after the last `/src/`, and a
|
|
1217
|
+
// package-relative key would attribute every file to no module.
|
|
1218
|
+
const sources = new Map(files.map((file) => [posixKey(relative(layout.sourceRoot, file)), readFileSync(file, 'utf8')]));
|
|
1219
|
+
const hostResidentModules = new Map();
|
|
1220
|
+
for (const key of sources.keys()) {
|
|
1221
|
+
const [head] = key.split('/');
|
|
1222
|
+
if (head !== undefined && head.length > 0)
|
|
1223
|
+
hostResidentModules.set(head, layout.moduleId);
|
|
1224
|
+
}
|
|
1225
|
+
const peers = readPeerOwners(layout);
|
|
1226
|
+
const written = resolvedPortNames(sources);
|
|
1227
|
+
const attributed = [...written].filter((name) => peers.portOwners.has(name) || ownProvidedNames(sources).has(name));
|
|
1228
|
+
const coverage = [layerCoverage];
|
|
1229
|
+
if (written.size > 0) {
|
|
1230
|
+
coverage.push({ source: 'owners', expected: written.size, covered: attributed.length });
|
|
1231
|
+
}
|
|
1232
|
+
const readSize = {
|
|
1233
|
+
prefix: '[port-catches]',
|
|
1234
|
+
files: sources.size + peers.filesRead,
|
|
1235
|
+
coverage,
|
|
1236
|
+
};
|
|
1237
|
+
// **The `owners` token is printed but is deliberately not put through the
|
|
1238
|
+
// short-walk refusal**, and the distinction is the whole of the owner-map
|
|
1239
|
+
// ruling. `read-size.ts` refuses any `covered < expected`, which would make a
|
|
1240
|
+
// package with one unresolved peer lose the rule entirely — fail-closed in the
|
|
1241
|
+
// wrong dimension, trading a small honest gap for a total blind spot, and for a
|
|
1242
|
+
// package reaching another paid module that is the common case. So the layer
|
|
1243
|
+
// walk keeps the standard refusal, and `owners` carries its own floor:
|
|
1244
|
+
//
|
|
1245
|
+
// * `covered > 0` → `ran`, with the fraction stating its own incompleteness
|
|
1246
|
+
// in a form something can ratchet on later;
|
|
1247
|
+
// * `covered === 0` with `expected > 0` → `unreadable`, because a run that
|
|
1248
|
+
// attributed none of the names it was asked to attribute has judged
|
|
1249
|
+
// nothing. That is `read-nothing` one granularity in, and the same refusal
|
|
1250
|
+
// the repository host makes about an owner map that resolved zero;
|
|
1251
|
+
// * `expected === 0` → nothing to resolve, token omitted, never `0/0`.
|
|
1252
|
+
const short = readSizeOrShortWalk(id, { ...readSize, coverage: [layerCoverage] });
|
|
1253
|
+
if (!short.ok)
|
|
1254
|
+
return short.result;
|
|
1255
|
+
const unattributed = [...written].filter((name) => !attributed.includes(name));
|
|
1256
|
+
// **The floor is "no owner map was read", not "these names did not resolve",
|
|
1257
|
+
// and the difference is five false refusals.** Built the second way first, and
|
|
1258
|
+
// measured: `audit_logs`, `google_tag_manager`, `linkedin_ads`, `meta_ads` and
|
|
1259
|
+
// `prompt_actions` each resolve exactly one gated name, `settingsReadPort` —
|
|
1260
|
+
// which the **platform** owns (`compose-app.ts` contributes it), not an
|
|
1261
|
+
// uninstalled module. All five refused, and the remedy they printed, *install
|
|
1262
|
+
// the owning module*, named work an author cannot do and does not need to.
|
|
1263
|
+
//
|
|
1264
|
+
// **The wrong remedy was worse than the wrong verdict.** A refusal telling an
|
|
1265
|
+
// author to install a module they do not need and cannot identify is a worse
|
|
1266
|
+
// outcome than a `ran` line that states what it could not attribute — the first
|
|
1267
|
+
// sends them looking for something that is not missing, the second tells them
|
|
1268
|
+
// exactly what this run did and did not judge. That is §5.1's failure, where a
|
|
1269
|
+
// finding an author cannot reproduce is one they learn to ignore, and it is the
|
|
1270
|
+
// reason the floor moved rather than the verdict being softened.
|
|
1271
|
+
//
|
|
1272
|
+
// So the refusal is the honest analogue of `read-nothing`: the run consulted an
|
|
1273
|
+
// owner map and the map was **empty**. That keeps the fixture case — a package
|
|
1274
|
+
// resolving a peer's port with no peer installed anywhere — and drops all five
|
|
1275
|
+
// false ones, because those runs read 174 owners and simply do not own this
|
|
1276
|
+
// name between them.
|
|
1277
|
+
//
|
|
1278
|
+
// **What this leaves, stated rather than discovered:** a package whose every
|
|
1279
|
+
// resolved name belongs to an uninstalled *module*, in a tree that has other
|
|
1280
|
+
// peers, reads `owners:0/1` on a `ran` line rather than refusing. It is printed
|
|
1281
|
+
// and named, not silent. Closing it properly means deriving the platform's own
|
|
1282
|
+
// registrations — `PLATFORM_OWNED_NAMES` is today a curated list because
|
|
1283
|
+
// `compose-app.ts` registers them through `contribute({ … })`, which neither
|
|
1284
|
+
// `registeredNames` nor `providedPortNames` recognises — and that recogniser is
|
|
1285
|
+
// the next thing to build here, not a weakening to accept for ever.
|
|
1286
|
+
if (written.size > 0 && attributed.length === 0 && peers.portOwners.size === 0) {
|
|
1287
|
+
return unreadable(id, `this package resolves ${written.size} gated port name(s) — ${unattributed.join(', ')} — ` +
|
|
1288
|
+
`and no owner map could be read at all: no module package was found installed beside ` +
|
|
1289
|
+
`it or in a workspace with it. The analysis admits none of those calls as a port, so ` +
|
|
1290
|
+
`every \`catch\` around one is unjudged. That is not clean: install the modules that ` +
|
|
1291
|
+
`own them, or read this rule as not having run.`, readSize);
|
|
1292
|
+
}
|
|
1293
|
+
const result = checkPortCatches({ sources, hostResidentModules, peerOwners: peers.portOwners }, {});
|
|
1294
|
+
const findings = result.violations.map((finding) => ({
|
|
1295
|
+
rule: id,
|
|
1296
|
+
key: portCatchKeyOf(finding),
|
|
1297
|
+
location: `${layout.keyOf(join(layout.sourceRoot, finding.file))}:${finding.line}`,
|
|
1298
|
+
message: `a \`catch\` around \`${finding.port}\` (${finding.form}) swallows ` +
|
|
1299
|
+
`\`ModuleDisabledError\`, so a switched-off owner reads as "no data" instead of "this ` +
|
|
1300
|
+
`capability is off" (Constitution XVII). Delete it if it was only defensive, move the ` +
|
|
1301
|
+
`degrade into the port's return type, or keep it and add ` +
|
|
1302
|
+
`\`rethrowIfModuleDisabled(error)\`.`,
|
|
1303
|
+
}));
|
|
1304
|
+
const outcome = ran(id, { ...readSize, sites: result.total }, findings);
|
|
1305
|
+
if (unattributed.length === 0)
|
|
1306
|
+
return outcome;
|
|
1307
|
+
// Partially resolved. The fraction is already on the `read:` line; this names
|
|
1308
|
+
// the ones, because "owners:3/5" does not tell an author which two to install.
|
|
1309
|
+
return {
|
|
1310
|
+
...outcome,
|
|
1311
|
+
explanation: `${unattributed.length} of the ${written.size} gated port name(s) this package resolves ` +
|
|
1312
|
+
`could not be attributed to an owner — ${unattributed.join(', ')}. Each is either a ` +
|
|
1313
|
+
`module you have not installed or a name the platform itself registers, and this run ` +
|
|
1314
|
+
`cannot tell those apart; a \`catch\` around one of them is unjudged rather than clean.`,
|
|
1315
|
+
};
|
|
1316
|
+
};
|
|
1317
|
+
/** The gated port names the package's **own** composing files provide. */
|
|
1318
|
+
function ownProvidedNames(sources) {
|
|
1319
|
+
const names = new Set();
|
|
1320
|
+
for (const [file, text] of sources) {
|
|
1321
|
+
if (!declaresRegisterModule(text))
|
|
1322
|
+
continue;
|
|
1323
|
+
for (const name of providedPortNames(text, file))
|
|
1324
|
+
names.add(name);
|
|
1325
|
+
}
|
|
1326
|
+
return names;
|
|
1327
|
+
}
|
|
1328
|
+
export const PACKAGE_HOSTS = new Map([
|
|
1329
|
+
['channel:resolution', channelResolution],
|
|
1330
|
+
['check-entity-tenant-classification', entityTenantClassification],
|
|
1331
|
+
['check:bundle-pairing', bundlePairing],
|
|
1332
|
+
['check:command-coverage', commandCoverage],
|
|
1333
|
+
['check:container-imports', containerImports],
|
|
1334
|
+
['check:default-language-prose', defaultLanguageProse],
|
|
1335
|
+
['check:entry-presence', entryPresence],
|
|
1336
|
+
['check:diacritic-folds', diacriticFolds],
|
|
1337
|
+
['check:entry-scope', entryScope],
|
|
1338
|
+
['check:kernel-boundary', kernelBoundary],
|
|
1339
|
+
['check:nul-bytes', nulBytes],
|
|
1340
|
+
['check:platform-surface', platformSurface],
|
|
1341
|
+
['check:port-catches', portCatches],
|
|
1342
|
+
['check:port-shape', portShape],
|
|
1343
|
+
['check:queue-names', queueNames],
|
|
1344
|
+
['check:subscribe-seam', subscribeSeam],
|
|
1345
|
+
['check:transaction-context', transactionContext],
|
|
1346
|
+
]);
|
|
1347
|
+
//# sourceMappingURL=hosts.js.map
|