@starci/hfs 4.0.10 → 4.2.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/CHANGELOG.md +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { lsFiles } from '../../api/git/ls-files.mjs';
|
|
4
4
|
import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
|
|
5
5
|
import { gitOutputOf } from '../../lib/git.mjs';
|
|
6
|
+
import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
@@ -48,8 +49,20 @@ function diskFiles(root, relative = '') {
|
|
|
48
49
|
return out;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
let runTrees = null;
|
|
53
|
+
|
|
54
|
+
/** Runs `run` with one tree read per root: every checker of one machine run shares the repository listing instead of spawning git again. */
|
|
55
|
+
export function withTreeCache(run) {
|
|
56
|
+
const outer = runTrees;
|
|
57
|
+
runTrees = outer ?? new Map();
|
|
58
|
+
try {
|
|
59
|
+
return run();
|
|
60
|
+
} finally {
|
|
61
|
+
runTrees = outer;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readTree(root) {
|
|
53
66
|
const list = gitFiles(root) ?? diskFiles(root);
|
|
54
67
|
const files = new Set(list);
|
|
55
68
|
const directories = new Set();
|
|
@@ -60,6 +73,13 @@ export function treeOf(root) {
|
|
|
60
73
|
return { files, directories };
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
/** The repository tree as file and directory sets over posix relatives. */
|
|
77
|
+
export function treeOf(root) {
|
|
78
|
+
if (!runTrees) return readTree(root);
|
|
79
|
+
if (!runTrees.has(root)) runTrees.set(root, readTree(root));
|
|
80
|
+
return runTrees.get(root);
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
/**
|
|
64
84
|
* The prelude every tree-walking architecture checker shares: the input's config/graph/context, the compiler,
|
|
65
85
|
* the slot resolver and the tracked repository tree.
|
|
@@ -68,28 +88,27 @@ export function checkerScope({ config, graph, context }) {
|
|
|
68
88
|
return { config, graph, context, ts: context.ts, resolver: graph.resolver, tree: treeOf(config.root) };
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
const
|
|
72
|
-
|
|
91
|
+
const rootOfTarget = target => {
|
|
92
|
+
if (target.endsWith('/')) return trimTrailingSlashes(target);
|
|
93
|
+
const parent = path.posix.dirname(target);
|
|
94
|
+
return parent === '.' ? '' : parent;
|
|
95
|
+
};
|
|
73
96
|
|
|
74
97
|
function ruleFor(slotId, target, rootExists) {
|
|
75
|
-
if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(
|
|
98
|
+
if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(trimTrailingSlashes(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
|
|
76
99
|
return 'HFS_REQUIRED_FILE_MISSING';
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const slot = resolver.slot(slotId);
|
|
84
|
-
return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
|
|
85
|
-
};
|
|
102
|
+
function slotIsJudged(resolver, slotId) {
|
|
103
|
+
const slot = resolver.slot(slotId);
|
|
104
|
+
return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
|
|
105
|
+
}
|
|
86
106
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
function addRequirement(requirements, slot, target, root) {
|
|
108
|
+
if (!requirements.has(target)) requirements.set(target, { slot, target, root });
|
|
109
|
+
}
|
|
90
110
|
|
|
91
|
-
|
|
92
|
-
const declared = resolver.requiredPaths();
|
|
111
|
+
function collectDeclaredRequirements(declared, requirements, instances, judged) {
|
|
93
112
|
const currentRoot = new Map();
|
|
94
113
|
for (const item of declared.paths) {
|
|
95
114
|
if (!judged(item.slot)) continue;
|
|
@@ -97,84 +116,116 @@ export function checkRequiredFiles({ config, graph }) {
|
|
|
97
116
|
const root = rootOfTarget(item.path);
|
|
98
117
|
currentRoot.set(item.slot, root);
|
|
99
118
|
instances.add(`${item.slot}|${root}`);
|
|
100
|
-
addRequirement(item.slot, item.path, root);
|
|
119
|
+
addRequirement(requirements, item.slot, item.path, root);
|
|
101
120
|
} else {
|
|
102
|
-
addRequirement(item.slot, item.path, currentRoot.get(item.slot) ?? '');
|
|
121
|
+
addRequirement(requirements, item.slot, item.path, currentRoot.get(item.slot) ?? '');
|
|
103
122
|
}
|
|
104
123
|
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function addInstanceRequirements(resolver, classified, representative, requirements, instances, seenInstances, judged) {
|
|
127
|
+
const { slot: slotId, root } = classified;
|
|
128
|
+
const key = `${slotId}|${root}`;
|
|
129
|
+
if (seenInstances.has(key) || !judged(slotId)) return;
|
|
130
|
+
seenInstances.add(key);
|
|
131
|
+
instances.add(key);
|
|
132
|
+
for (const target of resolver.requiredFiles(representative)) addRequirement(requirements, slotId, target, root);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function addOwnerRequirements(resolver, owner, requirements, instances, seenInstances, judged) {
|
|
136
|
+
const key = `${owner.slot}|${owner.root}`;
|
|
137
|
+
if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
|
|
138
|
+
seenInstances.add(key);
|
|
139
|
+
instances.add(key);
|
|
140
|
+
for (const entry of resolver.slot(owner.slot).requires ?? []) {
|
|
141
|
+
const rooted = entry.startsWith('/');
|
|
142
|
+
const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
|
|
143
|
+
addRequirement(requirements, owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
105
146
|
|
|
106
|
-
|
|
147
|
+
function noteInstance(found, slotId, root) {
|
|
148
|
+
if (!found.has(slotId)) found.set(slotId, new Set());
|
|
149
|
+
found.get(slotId).add(root);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function collectTreeRequirements(tree, resolver, requirements, instances, judged) {
|
|
107
153
|
const seenInstances = new Set();
|
|
108
|
-
const instanceRequires = (slotId, root, representative) => {
|
|
109
|
-
const key = `${slotId}|${root}`;
|
|
110
|
-
if (seenInstances.has(key) || !judged(slotId)) return;
|
|
111
|
-
seenInstances.add(key);
|
|
112
|
-
instances.add(key);
|
|
113
|
-
for (const target of resolver.requiredFiles(representative)) addRequirement(slotId, target, root);
|
|
114
|
-
};
|
|
115
|
-
const ownerRequires = owner => {
|
|
116
|
-
const key = `${owner.slot}|${owner.root}`;
|
|
117
|
-
if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
|
|
118
|
-
seenInstances.add(key);
|
|
119
|
-
instances.add(key);
|
|
120
|
-
for (const entry of resolver.slot(owner.slot).requires ?? []) {
|
|
121
|
-
const rooted = entry.startsWith('/');
|
|
122
|
-
const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
|
|
123
|
-
addRequirement(owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
154
|
const found = new Map(); // slot -> Set(root) of instances seen while walking
|
|
127
|
-
const note = (slotId, root) => {
|
|
128
|
-
if (!found.has(slotId)) found.set(slotId, new Set());
|
|
129
|
-
found.get(slotId).add(root);
|
|
130
|
-
};
|
|
131
155
|
for (const file of tree.files) {
|
|
132
156
|
const classified = resolver.classifyPath(file);
|
|
133
157
|
if (classified.status === 'owned' && classified.slot) {
|
|
134
|
-
|
|
135
|
-
|
|
158
|
+
noteInstance(found, classified.slot, classified.root);
|
|
159
|
+
addInstanceRequirements(resolver, classified, file, requirements, instances, seenInstances, judged);
|
|
136
160
|
}
|
|
137
161
|
const owner = resolver.ownerOf(file);
|
|
138
162
|
if (owner) {
|
|
139
|
-
|
|
163
|
+
noteInstance(found, owner.slot, owner.root);
|
|
140
164
|
// an owner root may classify to a more specific slot (modules/i18n) or a nested one, so expand the owner slot's own requires
|
|
141
|
-
|
|
165
|
+
addOwnerRequirements(resolver, owner, requirements, instances, seenInstances, judged);
|
|
142
166
|
}
|
|
143
167
|
}
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
144
170
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (exists(requirement.target)) continue;
|
|
157
|
-
const rootExists = !requirement.root || tree.directories.has(requirement.root);
|
|
158
|
-
if (!rootExists) {
|
|
159
|
-
report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
|
|
160
|
-
`Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
|
|
164
|
-
report(ruleFor(requirement.slot, requirement.target, true), strip(requirement.target), requirement.slot, requirement.root, { kind },
|
|
165
|
-
`Required ${kind} ${strip(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${requirement.root ? ` (${requirement.root})` : ''}.`);
|
|
171
|
+
function requirementExists(tree, target) {
|
|
172
|
+
return target.endsWith('/') ? tree.directories.has(trimTrailingSlashes(target)) : tree.files.has(target);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function reportMissingRequirement(requirement, tree, report) {
|
|
176
|
+
if (requirementExists(tree, requirement.target)) return;
|
|
177
|
+
const rootExists = !requirement.root || tree.directories.has(requirement.root);
|
|
178
|
+
if (!rootExists) {
|
|
179
|
+
report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
|
|
180
|
+
`Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
|
|
181
|
+
return;
|
|
166
182
|
}
|
|
183
|
+
const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
|
|
184
|
+
const rootNote = requirement.root ? ` (${requirement.root})` : '';
|
|
185
|
+
report(ruleFor(requirement.slot, requirement.target, true), trimTrailingSlashes(requirement.target), requirement.slot, requirement.root, { kind },
|
|
186
|
+
`Required ${kind} ${trimTrailingSlashes(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${rootNote}.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function reportMissingRequirements(requirements, tree, report) {
|
|
190
|
+
for (const requirement of requirements.values()) reportMissingRequirement(requirement, tree, report);
|
|
191
|
+
}
|
|
167
192
|
|
|
168
|
-
|
|
169
|
-
for (const minimum of
|
|
193
|
+
function reportMinimumRequirements(minimums, resolver, judged, found, report) {
|
|
194
|
+
for (const minimum of minimums) {
|
|
170
195
|
if (!judged(minimum.slot)) continue;
|
|
171
196
|
const count = minimum.appKind !== undefined
|
|
172
197
|
? resolver.repo.apps.filter(app => app.kind === minimum.appKind).length
|
|
173
198
|
: (found.get(minimum.slot)?.size ?? 0);
|
|
174
199
|
if (count >= minimum.min) continue;
|
|
200
|
+
const appKindNote = minimum.appKind ? ' (an app of kind ' + minimum.appKind + ' declared in hfs.json)' : '';
|
|
175
201
|
report('HFS_REQUIRED_FILE_MISSING', 'hfs.json', minimum.slot, '', { minimum: minimum.min, found: count },
|
|
176
|
-
`Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${
|
|
202
|
+
`Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${appKindNote}; the repository has ${count}.`);
|
|
177
203
|
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function checkRequiredFiles({ config, graph }) {
|
|
207
|
+
const resolver = graph?.resolver ?? config.hfs;
|
|
208
|
+
const tree = treeOf(config.root);
|
|
209
|
+
const judged = slotId => slotIsJudged(resolver, slotId);
|
|
210
|
+
|
|
211
|
+
const requirements = new Map(); // repository-relative target (as written, dirs end with /) -> {slot, target, root}
|
|
212
|
+
const instances = new Set();
|
|
213
|
+
const declared = resolver.requiredPaths();
|
|
214
|
+
collectDeclaredRequirements(declared, requirements, instances, judged);
|
|
215
|
+
|
|
216
|
+
const found = collectTreeRequirements(tree, resolver, requirements, instances, judged);
|
|
217
|
+
|
|
218
|
+
const violations = [];
|
|
219
|
+
const reported = new Set();
|
|
220
|
+
const report = (ruleId, target, slotId, root, extra, message) => {
|
|
221
|
+
const key = `${ruleId}|${target}`;
|
|
222
|
+
if (reported.has(key)) return;
|
|
223
|
+
reported.add(key);
|
|
224
|
+
violations.push({ ruleId, path: target, slot: slotId, ...(root ? { root } : {}), ...extra, message });
|
|
225
|
+
};
|
|
226
|
+
reportMissingRequirements(requirements, tree, report);
|
|
227
|
+
|
|
228
|
+
reportMinimumRequirements(declared.minimums, resolver, judged, found, report);
|
|
178
229
|
|
|
179
230
|
return { violations, coverage: { status: 'checked', instances: instances.size, requirements: requirements.size, missing: violations.length } };
|
|
180
231
|
}
|
|
@@ -24,74 +24,99 @@ const HOOK_ROUTES = new Set([...MOUNTING_ROUTES, 'page']);
|
|
|
24
24
|
const HOOK_NAME = /^use[A-Z0-9]/u;
|
|
25
25
|
const HOOKS_SLOT = 'fe.hooks';
|
|
26
26
|
|
|
27
|
+
const isFunction = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
|
|
28
|
+
const jsxOf = (ts, node) => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
|
|
29
|
+
|
|
30
|
+
function tagOf(ts, node) {
|
|
31
|
+
if (ts.isJsxElement(node)) return node.openingElement.tagName;
|
|
32
|
+
if (ts.isJsxSelfClosingElement(node)) return node.tagName;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addExportAssignmentDefaults(ts, statement, kit, checker, defaults) {
|
|
37
|
+
const target = statement.expression;
|
|
38
|
+
if (!ts.isIdentifier(target)) { defaults.add(target); return; }
|
|
39
|
+
for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function routeDefaults(ts, file, kit, checker) {
|
|
43
|
+
const defaults = new Set();
|
|
44
|
+
for (const statement of file.sourceFile.statements) {
|
|
45
|
+
if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
|
|
46
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) addExportAssignmentDefaults(ts, statement, kit, checker, defaults);
|
|
47
|
+
}
|
|
48
|
+
return defaults;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when the function contains JSX that is not inside a nested function. */
|
|
52
|
+
function drawsDirectly(ts, fn) {
|
|
53
|
+
let found = false;
|
|
54
|
+
const visit = node => {
|
|
55
|
+
if (found) return;
|
|
56
|
+
if (node !== fn && isFunction(ts, node)) return;
|
|
57
|
+
if (jsxOf(ts, node)) { found = true; return; }
|
|
58
|
+
ts.forEachChild(node, visit);
|
|
59
|
+
};
|
|
60
|
+
visit(fn);
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reportRoute(violations, kit, file, stem, node, message, extra = {}) {
|
|
65
|
+
violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inspectMountingTag(tag, { graph, kit, ts, file, stem, checker, violations, state }) {
|
|
69
|
+
if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) {
|
|
70
|
+
reportRoute(violations, kit, file, stem, tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
|
|
71
|
+
} else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) {
|
|
72
|
+
state.owners += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inspectHookCall(node, { graph, kit, ts, file, stem, checker, violations }) {
|
|
77
|
+
const callee = ts.isIdentifier(node.expression) ? node.expression : null;
|
|
78
|
+
if (!callee || !HOOK_NAME.test(callee.text)) return;
|
|
79
|
+
const binding = kit.importBinding(checker, callee);
|
|
80
|
+
const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
|
|
81
|
+
if (binding || inHooks) reportRoute(violations, kit, file, stem, callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inspectRouteNode(node, context) {
|
|
85
|
+
const { kit, ts, file, stem, mounting, defaults, violations, state } = context;
|
|
86
|
+
if (jsxOf(ts, node)) state.hasJsx = true;
|
|
87
|
+
const tag = tagOf(ts, node);
|
|
88
|
+
if (mounting && tag) inspectMountingTag(tag, context);
|
|
89
|
+
if (mounting && isFunction(ts, node) && !defaults.has(node) && drawsDirectly(ts, node)) {
|
|
90
|
+
reportRoute(violations, kit, file, stem, node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
|
|
91
|
+
}
|
|
92
|
+
if (ts.isCallExpression(node)) inspectHookCall(node, context);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inspectRouteFile(graph, kit, file, stem, violations) {
|
|
97
|
+
const { ts } = kit;
|
|
98
|
+
const mounting = MOUNTING_ROUTES.has(stem);
|
|
99
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
100
|
+
const defaults = routeDefaults(ts, file, kit, checker);
|
|
101
|
+
const state = { hasJsx: false, owners: 0 };
|
|
102
|
+
kit.walk(file.sourceFile, node => inspectRouteNode(node, { graph, kit, ts, file, stem, mounting, checker, defaults, violations, state }));
|
|
103
|
+
if (mounting && state.hasJsx && state.owners !== 1) {
|
|
104
|
+
violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners: state.owners,
|
|
105
|
+
message: `${file.rel} mounts ${state.owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
27
109
|
export function checkRouteFilesThin(input) {
|
|
28
110
|
const { graph } = input;
|
|
29
111
|
const kit = machineKit(input);
|
|
30
|
-
const { ts } = kit;
|
|
31
112
|
const violations = [];
|
|
32
113
|
let routes = 0;
|
|
33
|
-
|
|
34
114
|
for (const file of graph.files.values()) {
|
|
35
115
|
if (file.slot !== ROUTE_SLOT) continue;
|
|
36
116
|
const stem = path.posix.basename(file.rel).replace(/\.[cm]?[jt]sx?$/u, '');
|
|
37
117
|
if (!HOOK_ROUTES.has(stem)) continue;
|
|
38
118
|
routes += 1;
|
|
39
|
-
|
|
40
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
41
|
-
const report = (node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
|
|
42
|
-
|
|
43
|
-
// The default export function: `export default function X`, `export default X` (X a function), `export default () => ...`.
|
|
44
|
-
const defaults = new Set();
|
|
45
|
-
for (const statement of file.sourceFile.statements) {
|
|
46
|
-
if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
|
|
47
|
-
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
48
|
-
const target = statement.expression;
|
|
49
|
-
if (ts.isIdentifier(target)) for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
|
|
50
|
-
else defaults.add(target);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const isFunction = node => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
|
|
54
|
-
const jsxOf = node => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
|
|
55
|
-
const tagOf = node => (ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : null);
|
|
56
|
-
/** True when the function contains JSX that is not inside a nested function. */
|
|
57
|
-
const drawsDirectly = fn => {
|
|
58
|
-
let found = false;
|
|
59
|
-
const visit = node => {
|
|
60
|
-
if (found) return;
|
|
61
|
-
if (node !== fn && isFunction(node)) return;
|
|
62
|
-
if (jsxOf(node)) { found = true; return; }
|
|
63
|
-
ts.forEachChild(node, visit);
|
|
64
|
-
};
|
|
65
|
-
visit(fn);
|
|
66
|
-
return found;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
let hasJsx = false;
|
|
70
|
-
let owners = 0;
|
|
71
|
-
kit.walk(file.sourceFile, node => {
|
|
72
|
-
if (jsxOf(node)) hasJsx = true;
|
|
73
|
-
const tag = tagOf(node);
|
|
74
|
-
if (mounting && tag) {
|
|
75
|
-
if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) report(tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
|
|
76
|
-
else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) owners += 1;
|
|
77
|
-
}
|
|
78
|
-
if (mounting && isFunction(node) && !defaults.has(node) && drawsDirectly(node)) {
|
|
79
|
-
report(node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
|
|
80
|
-
}
|
|
81
|
-
if (ts.isCallExpression(node)) {
|
|
82
|
-
const callee = ts.isIdentifier(node.expression) ? node.expression : null;
|
|
83
|
-
if (callee && HOOK_NAME.test(callee.text)) {
|
|
84
|
-
const binding = kit.importBinding(checker, callee);
|
|
85
|
-
const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
|
|
86
|
-
if (binding || inHooks) report(callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return true;
|
|
90
|
-
});
|
|
91
|
-
if (mounting && hasJsx && owners !== 1) {
|
|
92
|
-
violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners,
|
|
93
|
-
message: `${file.rel} mounts ${owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
|
|
94
|
-
}
|
|
119
|
+
inspectRouteFile(graph, kit, file, stem, violations);
|
|
95
120
|
}
|
|
96
121
|
return { violations, coverage: { status: 'checked', routes } };
|
|
97
122
|
}
|