@ontrails/warden 1.0.0-beta.14 → 1.0.0-beta.16
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 +149 -0
- package/README.md +77 -34
- package/bin/warden.ts +22 -0
- package/package.json +26 -6
- package/src/ast.ts +28 -0
- package/src/cli.ts +1169 -116
- package/src/command.ts +927 -0
- package/src/config.ts +184 -0
- package/src/drift.ts +76 -34
- package/src/formatters.ts +54 -7
- package/src/guide.ts +234 -0
- package/src/index.ts +151 -38
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +2884 -124
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/{context-no-trailhead-types.ts → context-no-surface-types.ts} +72 -12
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/cross-declarations.ts +277 -69
- package/src/rules/dead-internal-trail.ts +141 -0
- package/src/rules/draft-file-marking.ts +51 -3
- package/src/rules/draft-visible-debt.ts +40 -15
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1042 -122
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +156 -19
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +508 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +1 -1
- package/src/rules/no-legacy-layer-imports.ts +193 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-sync-result-assumption.ts +1131 -96
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +6 -4
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-internal-deep-imports.ts +517 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +113 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/{provision-declarations.ts → resource-declarations.ts} +208 -138
- package/src/rules/{provision-exists.ts → resource-exists.ts} +46 -51
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/scan.ts +0 -26
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +5 -1
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/types.ts +187 -7
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +81 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/{context-no-trailhead-types.trail.ts → context-no-surface-types.trail.ts} +4 -4
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/error-mapping-completeness.trail.ts +29 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +56 -8
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +35 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/{provision-declarations.trail.ts → resource-declarations.trail.ts} +6 -6
- package/src/trails/{provision-exists.trail.ts → resource-exists.trail.ts} +7 -7
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/run.ts +133 -24
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +122 -4
- package/src/trails/signal-graph-coaching.trail.ts +74 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +69 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +133 -14
- package/src/workspaces.ts +238 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -247
- package/dist/cli.js.map +0 -1
- package/dist/draft.d.ts +0 -5
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -16
- package/dist/draft.js.map +0 -1
- package/dist/drift.d.ts +0 -29
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -61
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -30
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -98
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -37
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -82
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -363
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/context-no-trailhead-types.d.ts +0 -12
- package/dist/rules/context-no-trailhead-types.d.ts.map +0 -1
- package/dist/rules/context-no-trailhead-types.js +0 -96
- package/dist/rules/context-no-trailhead-types.js.map +0 -1
- package/dist/rules/cross-declarations.d.ts +0 -13
- package/dist/rules/cross-declarations.d.ts.map +0 -1
- package/dist/rules/cross-declarations.js +0 -264
- package/dist/rules/cross-declarations.js.map +0 -1
- package/dist/rules/draft-file-marking.d.ts +0 -6
- package/dist/rules/draft-file-marking.d.ts.map +0 -1
- package/dist/rules/draft-file-marking.js +0 -65
- package/dist/rules/draft-file-marking.js.map +0 -1
- package/dist/rules/draft-visible-debt.d.ts +0 -12
- package/dist/rules/draft-visible-debt.d.ts.map +0 -1
- package/dist/rules/draft-visible-debt.js +0 -45
- package/dist/rules/draft-visible-debt.js.map +0 -1
- package/dist/rules/follow-declarations.d.ts +0 -13
- package/dist/rules/follow-declarations.d.ts.map +0 -1
- package/dist/rules/follow-declarations.js +0 -264
- package/dist/rules/follow-declarations.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -20
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -49
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -47
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/provision-declarations.d.ts +0 -14
- package/dist/rules/provision-declarations.d.ts.map +0 -1
- package/dist/rules/provision-declarations.js +0 -344
- package/dist/rules/provision-declarations.js.map +0 -1
- package/dist/rules/provision-exists.d.ts +0 -6
- package/dist/rules/provision-exists.d.ts.map +0 -1
- package/dist/rules/provision-exists.js +0 -90
- package/dist/rules/provision-exists.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/service-declarations.d.ts +0 -16
- package/dist/rules/service-declarations.d.ts.map +0 -1
- package/dist/rules/service-declarations.js +0 -346
- package/dist/rules/service-declarations.js.map +0 -1
- package/dist/rules/service-exists.d.ts +0 -8
- package/dist/rules/service-exists.d.ts.map +0 -1
- package/dist/rules/service-exists.js +0 -91
- package/dist/rules/service-exists.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -54
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -117
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/dist/trails/context-no-surface-types.trail.d.ts +0 -13
- package/dist/trails/context-no-surface-types.trail.d.ts.map +0 -1
- package/dist/trails/context-no-surface-types.trail.js +0 -21
- package/dist/trails/context-no-surface-types.trail.js.map +0 -1
- package/dist/trails/context-no-trailhead-types.trail.d.ts +0 -13
- package/dist/trails/context-no-trailhead-types.trail.d.ts.map +0 -1
- package/dist/trails/context-no-trailhead-types.trail.js +0 -21
- package/dist/trails/context-no-trailhead-types.trail.js.map +0 -1
- package/dist/trails/cross-declarations.trail.d.ts +0 -13
- package/dist/trails/cross-declarations.trail.d.ts.map +0 -1
- package/dist/trails/cross-declarations.trail.js +0 -22
- package/dist/trails/cross-declarations.trail.js.map +0 -1
- package/dist/trails/follow-declarations.trail.d.ts +0 -13
- package/dist/trails/follow-declarations.trail.d.ts.map +0 -1
- package/dist/trails/follow-declarations.trail.js +0 -22
- package/dist/trails/follow-declarations.trail.js.map +0 -1
- package/dist/trails/implementation-returns-result.trail.d.ts +0 -13
- package/dist/trails/implementation-returns-result.trail.d.ts.map +0 -1
- package/dist/trails/implementation-returns-result.trail.js +0 -20
- package/dist/trails/implementation-returns-result.trail.js.map +0 -1
- package/dist/trails/index.d.ts +0 -16
- package/dist/trails/index.d.ts.map +0 -1
- package/dist/trails/index.js +0 -15
- package/dist/trails/index.js.map +0 -1
- package/dist/trails/no-direct-impl-in-route.trail.d.ts +0 -13
- package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +0 -1
- package/dist/trails/no-direct-impl-in-route.trail.js +0 -22
- package/dist/trails/no-direct-impl-in-route.trail.js.map +0 -1
- package/dist/trails/no-direct-implementation-call.trail.d.ts +0 -13
- package/dist/trails/no-direct-implementation-call.trail.d.ts.map +0 -1
- package/dist/trails/no-direct-implementation-call.trail.js +0 -16
- package/dist/trails/no-direct-implementation-call.trail.js.map +0 -1
- package/dist/trails/no-sync-result-assumption.trail.d.ts +0 -13
- package/dist/trails/no-sync-result-assumption.trail.d.ts.map +0 -1
- package/dist/trails/no-sync-result-assumption.trail.js +0 -19
- package/dist/trails/no-sync-result-assumption.trail.js.map +0 -1
- package/dist/trails/no-throw-in-detour-target.trail.d.ts +0 -15
- package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +0 -1
- package/dist/trails/no-throw-in-detour-target.trail.js +0 -20
- package/dist/trails/no-throw-in-detour-target.trail.js.map +0 -1
- package/dist/trails/no-throw-in-implementation.trail.d.ts +0 -13
- package/dist/trails/no-throw-in-implementation.trail.d.ts.map +0 -1
- package/dist/trails/no-throw-in-implementation.trail.js +0 -20
- package/dist/trails/no-throw-in-implementation.trail.js.map +0 -1
- package/dist/trails/prefer-schema-inference.trail.d.ts +0 -13
- package/dist/trails/prefer-schema-inference.trail.d.ts.map +0 -1
- package/dist/trails/prefer-schema-inference.trail.js +0 -21
- package/dist/trails/prefer-schema-inference.trail.js.map +0 -1
- package/dist/trails/provision-declarations.trail.d.ts +0 -13
- package/dist/trails/provision-declarations.trail.d.ts.map +0 -1
- package/dist/trails/provision-declarations.trail.js +0 -25
- package/dist/trails/provision-declarations.trail.js.map +0 -1
- package/dist/trails/provision-exists.trail.d.ts +0 -15
- package/dist/trails/provision-exists.trail.d.ts.map +0 -1
- package/dist/trails/provision-exists.trail.js +0 -27
- package/dist/trails/provision-exists.trail.js.map +0 -1
- package/dist/trails/run.d.ts +0 -17
- package/dist/trails/run.d.ts.map +0 -1
- package/dist/trails/run.js +0 -39
- package/dist/trails/run.js.map +0 -1
- package/dist/trails/schema.d.ts +0 -53
- package/dist/trails/schema.d.ts.map +0 -1
- package/dist/trails/schema.js +0 -42
- package/dist/trails/schema.js.map +0 -1
- package/dist/trails/service-declarations.trail.d.ts +0 -26
- package/dist/trails/service-declarations.trail.d.ts.map +0 -1
- package/dist/trails/service-declarations.trail.js +0 -27
- package/dist/trails/service-declarations.trail.js.map +0 -1
- package/dist/trails/service-exists.trail.d.ts +0 -32
- package/dist/trails/service-exists.trail.d.ts.map +0 -1
- package/dist/trails/service-exists.trail.js +0 -29
- package/dist/trails/service-exists.trail.js.map +0 -1
- package/dist/trails/topo.d.ts +0 -3
- package/dist/trails/topo.d.ts.map +0 -1
- package/dist/trails/topo.js +0 -5
- package/dist/trails/topo.js.map +0 -1
- package/dist/trails/valid-describe-refs.trail.d.ts +0 -15
- package/dist/trails/valid-describe-refs.trail.d.ts.map +0 -1
- package/dist/trails/valid-describe-refs.trail.js +0 -18
- package/dist/trails/valid-describe-refs.trail.js.map +0 -1
- package/dist/trails/valid-detour-refs.trail.d.ts +0 -15
- package/dist/trails/valid-detour-refs.trail.d.ts.map +0 -1
- package/dist/trails/valid-detour-refs.trail.js +0 -24
- package/dist/trails/valid-detour-refs.trail.js.map +0 -1
- package/dist/trails/wrap-rule.d.ts +0 -29
- package/dist/trails/wrap-rule.d.ts.map +0 -1
- package/dist/trails/wrap-rule.js +0 -47
- package/dist/trails/wrap-rule.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -311
- package/src/__tests__/cross-declarations.test.ts +0 -303
- package/src/__tests__/drift.test.ts +0 -144
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -129
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/provision-declarations.test.ts +0 -318
- package/src/__tests__/provision-exists.test.ts +0 -122
- package/src/__tests__/rules.test.ts +0 -227
- package/src/__tests__/trails.test.ts +0 -19
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/__tests__/wrap-rule.test.ts +0 -41
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -189
- package/src/trails/no-direct-impl-in-route.trail.ts +0 -22
- package/src/trails/no-throw-in-detour-target.trail.ts +0 -20
- package/src/trails/valid-detour-refs.trail.ts +0 -24
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/cli.ts
CHANGED
|
@@ -8,38 +8,128 @@
|
|
|
8
8
|
import { resolve } from 'node:path';
|
|
9
9
|
|
|
10
10
|
import type { Topo } from '@ontrails/core';
|
|
11
|
+
import { getContourReferences } from '@ontrails/core';
|
|
11
12
|
|
|
13
|
+
import type {
|
|
14
|
+
EffectiveWardenConfig,
|
|
15
|
+
WardenConfigInput,
|
|
16
|
+
WardenConfigLayer,
|
|
17
|
+
WardenDepth,
|
|
18
|
+
WardenFailOn,
|
|
19
|
+
WardenFormat,
|
|
20
|
+
WardenLockMode,
|
|
21
|
+
} from './config.js';
|
|
22
|
+
import { resolveWardenConfig } from './config.js';
|
|
23
|
+
import { isDraftMarkedFile } from './draft.js';
|
|
12
24
|
import type { DriftResult } from './drift.js';
|
|
13
25
|
import { checkDrift } from './drift.js';
|
|
14
26
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
collectProjectDocumentationImportResolutions,
|
|
28
|
+
collectProjectImportResolutions,
|
|
29
|
+
collectPublicWorkspaces,
|
|
30
|
+
} from './project-context.js';
|
|
31
|
+
import {
|
|
32
|
+
collectContourDefinitionIds,
|
|
33
|
+
collectContourReferenceTargetsByName,
|
|
34
|
+
collectCrudTableIds as collectCrudTableIdsFromAst,
|
|
35
|
+
collectCrossTargetTrailIds,
|
|
36
|
+
collectOnTargetSignalIds as collectOnTargetSignalIdsFromAst,
|
|
37
|
+
collectReconcileTableIds as collectReconcileTableIdsFromAst,
|
|
38
|
+
collectResourceDefinitionIds,
|
|
39
|
+
collectSignalDefinitionIds,
|
|
40
|
+
collectTrailIntentsById,
|
|
17
41
|
findTrailDefinitions,
|
|
18
42
|
parse,
|
|
19
|
-
walk,
|
|
20
43
|
} from './rules/ast.js';
|
|
21
|
-
import {
|
|
44
|
+
import { collectFileCrudCoverage } from './rules/incomplete-crud.js';
|
|
45
|
+
import { wardenRules, wardenTopoRules } from './rules/index.js';
|
|
46
|
+
import { getWardenRuleMetadata } from './rules/metadata.js';
|
|
22
47
|
import type {
|
|
23
48
|
ProjectAwareWardenRule,
|
|
24
49
|
ProjectContext,
|
|
50
|
+
TopoAwareWardenRule,
|
|
25
51
|
WardenDiagnostic,
|
|
52
|
+
WardenGuidanceLink,
|
|
26
53
|
WardenRule,
|
|
54
|
+
WardenRuleTier,
|
|
27
55
|
} from './rules/types.js';
|
|
56
|
+
import type { WardenImportResolution } from './resolve.js';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolved topo input for Warden runs that govern multiple apps.
|
|
60
|
+
*/
|
|
61
|
+
export interface WardenTopoTarget {
|
|
62
|
+
/** Stable app/topo label used to tag topo-aware diagnostics. */
|
|
63
|
+
readonly name?: string | undefined;
|
|
64
|
+
/** Resolved topo module to inspect. */
|
|
65
|
+
readonly topo: Topo;
|
|
66
|
+
}
|
|
28
67
|
|
|
29
68
|
/**
|
|
30
|
-
* Options for the
|
|
69
|
+
* Options for the shared Warden runner.
|
|
31
70
|
*/
|
|
32
|
-
export interface
|
|
71
|
+
export interface WardenRunOptions {
|
|
33
72
|
/** Root directory to scan for TypeScript files. Defaults to cwd. */
|
|
34
73
|
readonly rootDir?: string | undefined;
|
|
74
|
+
/** Warden config section from `trails.config.ts`, if already loaded. */
|
|
75
|
+
readonly config?: WardenConfigInput | undefined;
|
|
76
|
+
/** CLI/config-layer app names carried through shared resolution. */
|
|
77
|
+
readonly apps?: readonly string[] | undefined;
|
|
78
|
+
/** Cumulative analysis depth for the final M1 surfaces. */
|
|
79
|
+
readonly depth?: WardenDepth | undefined;
|
|
80
|
+
/** Draft-state handling mode for final M1 surfaces. */
|
|
81
|
+
readonly drafts?: EffectiveWardenConfig['drafts'] | undefined;
|
|
82
|
+
/** Failure threshold used to compute `report.passed`. */
|
|
83
|
+
readonly failOn?: WardenFailOn | undefined;
|
|
84
|
+
/** Output format requested by the caller. */
|
|
85
|
+
readonly format?: WardenFormat | undefined;
|
|
86
|
+
/** Lockfile mode requested by the caller. */
|
|
87
|
+
readonly lock?: WardenLockMode | undefined;
|
|
88
|
+
/** Suppress lockfile mutation for CI/pre-push callers. */
|
|
89
|
+
readonly noLockMutation?: boolean | undefined;
|
|
90
|
+
/** Environment layer for config resolution. Pass `process.env` at process boundaries. */
|
|
91
|
+
readonly env?: Record<string, string | undefined> | undefined;
|
|
35
92
|
/** Only run lint rules, skip drift detection */
|
|
36
93
|
readonly lintOnly?: boolean | undefined;
|
|
37
94
|
/** Only run drift detection, skip lint rules */
|
|
38
95
|
readonly driftOnly?: boolean | undefined;
|
|
39
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Run a single Warden tier. Defaults to all lint tiers plus drift.
|
|
98
|
+
*
|
|
99
|
+
* Selecting a non-drift tier skips drift detection; selecting `drift` skips
|
|
100
|
+
* lint rule dispatch. `lintOnly` and `driftOnly` remain compatibility shims.
|
|
101
|
+
*/
|
|
102
|
+
readonly tier?: WardenRuleTier | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* App topology for drift detection. When provided, enables real topology
|
|
105
|
+
* drift comparison and unlocks the topo-aware rule dispatch path.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* Topo-aware rules (both built-in `wardenTopoRules` and `extraTopoRules`)
|
|
109
|
+
* only fire when a `Topo` is supplied. Runs without a topo silently skip
|
|
110
|
+
* topo-aware dispatch — callers that depend on a topo-aware rule firing
|
|
111
|
+
* must pass `topo` explicitly.
|
|
112
|
+
*/
|
|
40
113
|
readonly topo?: Topo | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Multiple resolved topos to govern in one invocation.
|
|
116
|
+
*
|
|
117
|
+
* Source/project rules run once; topo-aware rules run once per target.
|
|
118
|
+
*/
|
|
119
|
+
readonly topos?: readonly WardenTopoTarget[] | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Extra topo-aware rules to run in addition to the built-in registry.
|
|
122
|
+
*
|
|
123
|
+
* Primarily a test hook — production callers should register rules via
|
|
124
|
+
* `wardenTopoRules` in `rules/index.ts`. These rules are only invoked
|
|
125
|
+
* when `topo` is also supplied (see `topo` remarks).
|
|
126
|
+
*/
|
|
127
|
+
readonly extraTopoRules?: readonly TopoAwareWardenRule[] | undefined;
|
|
41
128
|
}
|
|
42
129
|
|
|
130
|
+
/** Backwards-compatible name for older consumers. */
|
|
131
|
+
export type WardenOptions = WardenRunOptions;
|
|
132
|
+
|
|
43
133
|
/**
|
|
44
134
|
* Result of a warden run.
|
|
45
135
|
*/
|
|
@@ -54,33 +144,138 @@ export interface WardenReport {
|
|
|
54
144
|
readonly drift: DriftResult | null;
|
|
55
145
|
/** Whether the warden run passed (no errors, no drift) */
|
|
56
146
|
readonly passed: boolean;
|
|
147
|
+
/** Effective shared config consumed by this run. */
|
|
148
|
+
readonly effectiveConfig?: EffectiveWardenConfig | undefined;
|
|
149
|
+
/** Resolved topo/app labels governed by this run. */
|
|
150
|
+
readonly topoNames?: readonly string[] | undefined;
|
|
57
151
|
}
|
|
58
152
|
|
|
59
153
|
/**
|
|
60
|
-
* Collect
|
|
154
|
+
* Collect Warden scan targets under a directory, excluding generated and test
|
|
155
|
+
* surfaces that should not contribute most committed-source diagnostics.
|
|
61
156
|
*/
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
157
|
+
const isInfrastructureScanTarget = (match: string): boolean =>
|
|
158
|
+
match.endsWith('.d.ts') ||
|
|
159
|
+
match.startsWith('node_modules/') ||
|
|
160
|
+
match.startsWith('dist/') ||
|
|
161
|
+
match.startsWith('.git/');
|
|
162
|
+
|
|
163
|
+
const isTestScanTarget = (match: string): boolean =>
|
|
164
|
+
match.includes('__tests__/') ||
|
|
165
|
+
match.includes('__test__/') ||
|
|
166
|
+
match.endsWith('.test.ts') ||
|
|
167
|
+
match.endsWith('.spec.ts');
|
|
168
|
+
|
|
169
|
+
const isAllowedScanTarget = (match: string): boolean =>
|
|
170
|
+
!isInfrastructureScanTarget(match) && !isTestScanTarget(match);
|
|
171
|
+
|
|
172
|
+
const isDevPermitTestScanTarget = (match: string): boolean =>
|
|
173
|
+
!isInfrastructureScanTarget(match) && isTestScanTarget(match);
|
|
174
|
+
|
|
175
|
+
const collectFilesMatching = (
|
|
176
|
+
dir: string,
|
|
177
|
+
pattern: string,
|
|
178
|
+
dot = false
|
|
179
|
+
): readonly string[] => {
|
|
180
|
+
const glob = new Bun.Glob(pattern);
|
|
181
|
+
let matches: IterableIterator<string>;
|
|
182
|
+
try {
|
|
183
|
+
matches = glob.scanSync({ cwd: dir, dot, onlyFiles: true });
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const files: string[] = [];
|
|
189
|
+
for (const match of matches) {
|
|
190
|
+
if (isAllowedScanTarget(match)) {
|
|
191
|
+
files.push(`${dir}/${match}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return files;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const collectTsFiles = (dir: string): readonly string[] =>
|
|
198
|
+
collectFilesMatching(dir, '**/*.ts');
|
|
199
|
+
|
|
200
|
+
const draftModeIncludesFile = (
|
|
201
|
+
filePath: string,
|
|
202
|
+
drafts: EffectiveWardenConfig['drafts']
|
|
203
|
+
): boolean => {
|
|
204
|
+
const isDraftFile = isDraftMarkedFile(filePath);
|
|
205
|
+
if (drafts === 'exclude') {
|
|
206
|
+
return !isDraftFile;
|
|
207
|
+
}
|
|
208
|
+
if (drafts === 'only') {
|
|
209
|
+
return isDraftFile;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const filterSourceFilesByDraftMode = (
|
|
215
|
+
sourceFiles: readonly SourceFile[],
|
|
216
|
+
drafts: EffectiveWardenConfig['drafts']
|
|
217
|
+
): readonly SourceFile[] =>
|
|
218
|
+
drafts === 'include'
|
|
219
|
+
? sourceFiles
|
|
220
|
+
: sourceFiles.filter((sourceFile) =>
|
|
221
|
+
draftModeIncludesFile(sourceFile.filePath, drafts)
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const collectDevPermitTestFiles = (dir: string): readonly string[] => {
|
|
73
225
|
const glob = new Bun.Glob('**/*.ts');
|
|
74
226
|
let matches: IterableIterator<string>;
|
|
75
227
|
try {
|
|
76
|
-
matches = glob.scanSync({ cwd: dir,
|
|
228
|
+
matches = glob.scanSync({ cwd: dir, onlyFiles: true });
|
|
77
229
|
} catch {
|
|
78
230
|
return [];
|
|
79
231
|
}
|
|
80
232
|
|
|
81
233
|
const files: string[] = [];
|
|
82
234
|
for (const match of matches) {
|
|
83
|
-
if (
|
|
235
|
+
if (isDevPermitTestScanTarget(match)) {
|
|
236
|
+
files.push(`${dir}/${match}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return files;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const collectTextScanFiles = (dir: string): readonly string[] => [
|
|
243
|
+
...collectFilesMatching(dir, '**/*.sh', true),
|
|
244
|
+
...collectFilesMatching(dir, '**/*.bash', true),
|
|
245
|
+
...collectFilesMatching(dir, '**/*.zsh', true),
|
|
246
|
+
...collectFilesMatching(dir, '**/*.yml', true),
|
|
247
|
+
...collectFilesMatching(dir, '**/*.yaml', true),
|
|
248
|
+
...collectFilesMatching(dir, '**/package.json', true),
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const isDocumentationScanTarget = (match: string): boolean => {
|
|
252
|
+
if (match === 'README.md') {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (/^(?:packages|adapters|apps)\/[^/]+\/README\.md$/.test(match)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return (
|
|
259
|
+
match.startsWith('docs/') &&
|
|
260
|
+
!match.startsWith('docs/adr/') &&
|
|
261
|
+
!match.startsWith('docs/migration/') &&
|
|
262
|
+
!match.startsWith('docs/releases/') &&
|
|
263
|
+
match.endsWith('.md')
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const collectDocumentationFiles = (dir: string): readonly string[] => {
|
|
268
|
+
const glob = new Bun.Glob('**/*.md');
|
|
269
|
+
let matches: IterableIterator<string>;
|
|
270
|
+
try {
|
|
271
|
+
matches = glob.scanSync({ cwd: dir, onlyFiles: true });
|
|
272
|
+
} catch {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const files: string[] = [];
|
|
277
|
+
for (const match of matches) {
|
|
278
|
+
if (isAllowedScanTarget(match) && isDocumentationScanTarget(match)) {
|
|
84
279
|
files.push(`${dir}/${match}`);
|
|
85
280
|
}
|
|
86
281
|
}
|
|
@@ -89,9 +284,131 @@ const collectTsFiles = (dir: string): readonly string[] => {
|
|
|
89
284
|
|
|
90
285
|
interface SourceFile {
|
|
91
286
|
readonly filePath: string;
|
|
287
|
+
readonly kind: 'documentation' | 'text' | 'typescript';
|
|
92
288
|
readonly sourceCode: string;
|
|
93
289
|
}
|
|
94
290
|
|
|
291
|
+
interface MutableProjectContext {
|
|
292
|
+
contourReferencesByName: Map<string, Set<string>>;
|
|
293
|
+
crudTableIds: Set<string>;
|
|
294
|
+
crossTargetTrailIds: Set<string>;
|
|
295
|
+
crudCoverageByEntity: Map<string, Set<string>>;
|
|
296
|
+
knownContourIds: Set<string>;
|
|
297
|
+
knownResourceIds: Set<string>;
|
|
298
|
+
knownSignalIds: Set<string>;
|
|
299
|
+
knownTrailIds: Set<string>;
|
|
300
|
+
importResolutionsByFile: Map<string, readonly WardenImportResolution[]>;
|
|
301
|
+
documentedImportResolutionsByFile: Map<
|
|
302
|
+
string,
|
|
303
|
+
readonly WardenImportResolution[]
|
|
304
|
+
>;
|
|
305
|
+
onTargetSignalIds: Set<string>;
|
|
306
|
+
publicWorkspaces: ReturnType<typeof collectPublicWorkspaces>;
|
|
307
|
+
reconcileTableIds: Set<string>;
|
|
308
|
+
trailIntentsById: Map<string, 'destroy' | 'read' | 'write'>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const createMutableProjectContext = (): MutableProjectContext => ({
|
|
312
|
+
contourReferencesByName: new Map<string, Set<string>>(),
|
|
313
|
+
crossTargetTrailIds: new Set<string>(),
|
|
314
|
+
crudCoverageByEntity: new Map<string, Set<string>>(),
|
|
315
|
+
crudTableIds: new Set<string>(),
|
|
316
|
+
documentedImportResolutionsByFile: new Map<
|
|
317
|
+
string,
|
|
318
|
+
readonly WardenImportResolution[]
|
|
319
|
+
>(),
|
|
320
|
+
importResolutionsByFile: new Map<string, readonly WardenImportResolution[]>(),
|
|
321
|
+
knownContourIds: new Set<string>(),
|
|
322
|
+
knownResourceIds: new Set<string>(),
|
|
323
|
+
knownSignalIds: new Set<string>(),
|
|
324
|
+
knownTrailIds: new Set<string>(),
|
|
325
|
+
onTargetSignalIds: new Set<string>(),
|
|
326
|
+
publicWorkspaces: new Map(),
|
|
327
|
+
reconcileTableIds: new Set<string>(),
|
|
328
|
+
trailIntentsById: new Map<string, 'destroy' | 'read' | 'write'>(),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const addContourReferenceTargets = (
|
|
332
|
+
context: MutableProjectContext,
|
|
333
|
+
contourName: string,
|
|
334
|
+
targets: readonly string[]
|
|
335
|
+
): void => {
|
|
336
|
+
const existing = context.contourReferencesByName.get(contourName);
|
|
337
|
+
if (existing) {
|
|
338
|
+
for (const target of targets) {
|
|
339
|
+
existing.add(target);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
context.contourReferencesByName.set(contourName, new Set(targets));
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const toProjectContext = (context: MutableProjectContext): ProjectContext => ({
|
|
348
|
+
...(context.contourReferencesByName.size > 0
|
|
349
|
+
? {
|
|
350
|
+
contourReferencesByName: new Map(
|
|
351
|
+
[...context.contourReferencesByName.entries()].map(
|
|
352
|
+
([name, targets]) => [name, [...targets]]
|
|
353
|
+
)
|
|
354
|
+
),
|
|
355
|
+
}
|
|
356
|
+
: {}),
|
|
357
|
+
...(context.crudTableIds.size > 0
|
|
358
|
+
? { crudTableIds: context.crudTableIds }
|
|
359
|
+
: {}),
|
|
360
|
+
...(context.crudCoverageByEntity.size > 0
|
|
361
|
+
? {
|
|
362
|
+
crudCoverageByEntity: new Map(
|
|
363
|
+
[...context.crudCoverageByEntity.entries()].map(
|
|
364
|
+
([entityId, operations]) => [
|
|
365
|
+
entityId,
|
|
366
|
+
new Set(operations) as ReadonlySet<string>,
|
|
367
|
+
]
|
|
368
|
+
)
|
|
369
|
+
),
|
|
370
|
+
}
|
|
371
|
+
: {}),
|
|
372
|
+
crossTargetTrailIds: context.crossTargetTrailIds,
|
|
373
|
+
knownContourIds: context.knownContourIds,
|
|
374
|
+
knownResourceIds: context.knownResourceIds,
|
|
375
|
+
knownSignalIds: context.knownSignalIds,
|
|
376
|
+
knownTrailIds: context.knownTrailIds,
|
|
377
|
+
...(context.importResolutionsByFile.size > 0
|
|
378
|
+
? { importResolutionsByFile: context.importResolutionsByFile }
|
|
379
|
+
: {}),
|
|
380
|
+
...(context.documentedImportResolutionsByFile.size > 0
|
|
381
|
+
? {
|
|
382
|
+
documentedImportResolutionsByFile:
|
|
383
|
+
context.documentedImportResolutionsByFile,
|
|
384
|
+
}
|
|
385
|
+
: {}),
|
|
386
|
+
...(context.onTargetSignalIds.size > 0
|
|
387
|
+
? { onTargetSignalIds: context.onTargetSignalIds }
|
|
388
|
+
: {}),
|
|
389
|
+
...(context.publicWorkspaces.size > 0
|
|
390
|
+
? { publicWorkspaces: context.publicWorkspaces }
|
|
391
|
+
: {}),
|
|
392
|
+
...(context.reconcileTableIds.size > 0
|
|
393
|
+
? { reconcileTableIds: context.reconcileTableIds }
|
|
394
|
+
: {}),
|
|
395
|
+
trailIntentsById: context.trailIntentsById,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const collectKnownContourIds = (
|
|
399
|
+
sourceCode: string,
|
|
400
|
+
filePath: string,
|
|
401
|
+
knownContourIds: Set<string>
|
|
402
|
+
): void => {
|
|
403
|
+
const ast = parse(filePath, sourceCode);
|
|
404
|
+
if (!ast) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
for (const id of collectContourDefinitionIds(ast)) {
|
|
408
|
+
knownContourIds.add(id);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
95
412
|
const collectKnownTrailIds = (
|
|
96
413
|
sourceCode: string,
|
|
97
414
|
filePath: string,
|
|
@@ -106,44 +423,122 @@ const collectKnownTrailIds = (
|
|
|
106
423
|
}
|
|
107
424
|
};
|
|
108
425
|
|
|
109
|
-
const
|
|
426
|
+
const collectCrossedTrailIds = (
|
|
110
427
|
sourceCode: string,
|
|
111
428
|
filePath: string,
|
|
112
|
-
|
|
429
|
+
crossTargetTrailIds: Set<string>
|
|
113
430
|
): void => {
|
|
114
431
|
const ast = parse(filePath, sourceCode);
|
|
115
432
|
if (!ast) {
|
|
116
433
|
return;
|
|
117
434
|
}
|
|
118
|
-
for (const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
435
|
+
for (const id of collectCrossTargetTrailIds(ast, sourceCode)) {
|
|
436
|
+
crossTargetTrailIds.add(id);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const collectKnownResourceIds = (
|
|
441
|
+
sourceCode: string,
|
|
442
|
+
filePath: string,
|
|
443
|
+
knownResourceIds: Set<string>
|
|
444
|
+
): void => {
|
|
445
|
+
const ast = parse(filePath, sourceCode);
|
|
446
|
+
if (!ast) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
for (const id of collectResourceDefinitionIds(ast)) {
|
|
450
|
+
knownResourceIds.add(id);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const collectKnownSignalIds = (
|
|
455
|
+
sourceCode: string,
|
|
456
|
+
filePath: string,
|
|
457
|
+
knownSignalIds: Set<string>
|
|
458
|
+
): void => {
|
|
459
|
+
const ast = parse(filePath, sourceCode);
|
|
460
|
+
if (!ast) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
for (const id of collectSignalDefinitionIds(ast)) {
|
|
464
|
+
knownSignalIds.add(id);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const collectTrailIntents = (
|
|
469
|
+
sourceCode: string,
|
|
470
|
+
filePath: string,
|
|
471
|
+
trailIntentsById: Map<string, 'destroy' | 'read' | 'write'>
|
|
472
|
+
): void => {
|
|
473
|
+
const ast = parse(filePath, sourceCode);
|
|
474
|
+
if (!ast) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
for (const [id, intent] of collectTrailIntentsById(ast)) {
|
|
478
|
+
trailIntentsById.set(id, intent);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const collectCrudTableIds = (
|
|
483
|
+
sourceCode: string,
|
|
484
|
+
filePath: string,
|
|
485
|
+
crudTableIds: Set<string>
|
|
486
|
+
): void => {
|
|
487
|
+
const ast = parse(filePath, sourceCode);
|
|
488
|
+
if (!ast) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
for (const id of collectCrudTableIdsFromAst(ast)) {
|
|
492
|
+
crudTableIds.add(id);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const collectOnTargetSignalIds = (
|
|
497
|
+
sourceCode: string,
|
|
498
|
+
filePath: string,
|
|
499
|
+
onTargetSignalIds: Set<string>
|
|
500
|
+
): void => {
|
|
501
|
+
const ast = parse(filePath, sourceCode);
|
|
502
|
+
if (!ast) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
for (const id of collectOnTargetSignalIdsFromAst(ast, sourceCode)) {
|
|
506
|
+
onTargetSignalIds.add(id);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const collectCrudCoverageByEntity = (
|
|
511
|
+
sourceCode: string,
|
|
512
|
+
filePath: string,
|
|
513
|
+
coverageByEntity: Map<string, Set<string>>
|
|
514
|
+
): void => {
|
|
515
|
+
const ast = parse(filePath, sourceCode);
|
|
516
|
+
if (!ast) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
for (const [entityId, operations] of collectFileCrudCoverage(
|
|
520
|
+
ast,
|
|
521
|
+
sourceCode
|
|
522
|
+
)) {
|
|
523
|
+
const bucket = coverageByEntity.get(entityId) ?? new Set<string>();
|
|
524
|
+
for (const operation of operations) {
|
|
525
|
+
bucket.add(operation);
|
|
122
526
|
}
|
|
123
|
-
|
|
124
|
-
walk(detoursProp, (node) => {
|
|
125
|
-
if (node.type !== 'Literal') {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const val = (node as unknown as { value?: string }).value;
|
|
129
|
-
if (val && val.includes('.')) {
|
|
130
|
-
detourTargetTrailIds.add(val);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
527
|
+
coverageByEntity.set(entityId, bucket);
|
|
133
528
|
}
|
|
134
529
|
};
|
|
135
530
|
|
|
136
|
-
const
|
|
531
|
+
const collectReconcileTableIds = (
|
|
137
532
|
sourceCode: string,
|
|
138
533
|
filePath: string,
|
|
139
|
-
|
|
534
|
+
reconcileTableIds: Set<string>
|
|
140
535
|
): void => {
|
|
141
536
|
const ast = parse(filePath, sourceCode);
|
|
142
537
|
if (!ast) {
|
|
143
538
|
return;
|
|
144
539
|
}
|
|
145
|
-
for (const id of
|
|
146
|
-
|
|
540
|
+
for (const id of collectReconcileTableIdsFromAst(ast)) {
|
|
541
|
+
reconcileTableIds.add(id);
|
|
147
542
|
}
|
|
148
543
|
};
|
|
149
544
|
|
|
@@ -156,6 +551,43 @@ const loadSourceFiles = async (
|
|
|
156
551
|
try {
|
|
157
552
|
sourceFiles.push({
|
|
158
553
|
filePath,
|
|
554
|
+
kind: 'typescript',
|
|
555
|
+
sourceCode: await Bun.file(filePath).text(),
|
|
556
|
+
});
|
|
557
|
+
} catch {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (const filePath of collectTextScanFiles(rootDir)) {
|
|
563
|
+
try {
|
|
564
|
+
sourceFiles.push({
|
|
565
|
+
filePath,
|
|
566
|
+
kind: 'text',
|
|
567
|
+
sourceCode: await Bun.file(filePath).text(),
|
|
568
|
+
});
|
|
569
|
+
} catch {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const filePath of collectDocumentationFiles(rootDir)) {
|
|
575
|
+
try {
|
|
576
|
+
sourceFiles.push({
|
|
577
|
+
filePath,
|
|
578
|
+
kind: 'documentation',
|
|
579
|
+
sourceCode: await Bun.file(filePath).text(),
|
|
580
|
+
});
|
|
581
|
+
} catch {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
for (const filePath of collectDevPermitTestFiles(rootDir)) {
|
|
587
|
+
try {
|
|
588
|
+
sourceFiles.push({
|
|
589
|
+
filePath,
|
|
590
|
+
kind: 'text',
|
|
159
591
|
sourceCode: await Bun.file(filePath).text(),
|
|
160
592
|
});
|
|
161
593
|
} catch {
|
|
@@ -166,92 +598,407 @@ const loadSourceFiles = async (
|
|
|
166
598
|
return sourceFiles;
|
|
167
599
|
};
|
|
168
600
|
|
|
169
|
-
const
|
|
170
|
-
appTopo: Topo
|
|
171
|
-
|
|
172
|
-
|
|
601
|
+
const collectTopoKnownIds = (
|
|
602
|
+
appTopo: Topo,
|
|
603
|
+
context: MutableProjectContext
|
|
604
|
+
): void => {
|
|
605
|
+
for (const name of appTopo.contours.keys()) {
|
|
606
|
+
context.knownContourIds.add(name);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
for (const id of appTopo.trails.keys()) {
|
|
610
|
+
context.knownTrailIds.add(id);
|
|
611
|
+
}
|
|
173
612
|
|
|
613
|
+
for (const id of appTopo.resources.keys()) {
|
|
614
|
+
context.knownResourceIds.add(id);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
for (const id of appTopo.signals.keys()) {
|
|
618
|
+
context.knownSignalIds.add(id);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const collectTopoCrossesAndIntents = (
|
|
623
|
+
appTopo: Topo,
|
|
624
|
+
context: MutableProjectContext
|
|
625
|
+
): void => {
|
|
174
626
|
for (const trail of appTopo.trails.values()) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!detours) {
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
for (const targets of Object.values(detours)) {
|
|
182
|
-
for (const id of targets) {
|
|
183
|
-
detourTargetTrailIds.add(id);
|
|
184
|
-
}
|
|
627
|
+
context.trailIntentsById.set(trail.id, trail.intent);
|
|
628
|
+
for (const crossedTrailId of trail.crosses) {
|
|
629
|
+
context.crossTargetTrailIds.add(crossedTrailId);
|
|
185
630
|
}
|
|
186
631
|
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const collectTopoContourReferences = (
|
|
635
|
+
appTopo: Topo,
|
|
636
|
+
context: MutableProjectContext
|
|
637
|
+
): void => {
|
|
638
|
+
for (const contour of appTopo.listContours()) {
|
|
639
|
+
addContourReferenceTargets(
|
|
640
|
+
context,
|
|
641
|
+
contour.name,
|
|
642
|
+
getContourReferences(contour).map((reference) => reference.contour)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
187
646
|
|
|
188
|
-
|
|
647
|
+
const collectTopoTrailContext = (
|
|
648
|
+
appTopo: Topo,
|
|
649
|
+
context: MutableProjectContext
|
|
650
|
+
): void => {
|
|
651
|
+
collectTopoKnownIds(appTopo, context);
|
|
652
|
+
collectTopoCrossesAndIntents(appTopo, context);
|
|
653
|
+
collectTopoContourReferences(appTopo, context);
|
|
189
654
|
};
|
|
190
655
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
656
|
+
const collectFileKnownIds = (
|
|
657
|
+
sourceFile: SourceFile,
|
|
658
|
+
context: MutableProjectContext
|
|
659
|
+
): void => {
|
|
660
|
+
collectKnownContourIds(
|
|
661
|
+
sourceFile.sourceCode,
|
|
662
|
+
sourceFile.filePath,
|
|
663
|
+
context.knownContourIds
|
|
664
|
+
);
|
|
665
|
+
collectKnownTrailIds(
|
|
666
|
+
sourceFile.sourceCode,
|
|
667
|
+
sourceFile.filePath,
|
|
668
|
+
context.knownTrailIds
|
|
669
|
+
);
|
|
670
|
+
collectKnownResourceIds(
|
|
671
|
+
sourceFile.sourceCode,
|
|
672
|
+
sourceFile.filePath,
|
|
673
|
+
context.knownResourceIds
|
|
674
|
+
);
|
|
675
|
+
collectKnownSignalIds(
|
|
676
|
+
sourceFile.sourceCode,
|
|
677
|
+
sourceFile.filePath,
|
|
678
|
+
context.knownSignalIds
|
|
679
|
+
);
|
|
680
|
+
};
|
|
195
681
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
682
|
+
const collectFileTrailRelationships = (
|
|
683
|
+
sourceFile: SourceFile,
|
|
684
|
+
context: MutableProjectContext
|
|
685
|
+
): void => {
|
|
686
|
+
collectCrossedTrailIds(
|
|
687
|
+
sourceFile.sourceCode,
|
|
688
|
+
sourceFile.filePath,
|
|
689
|
+
context.crossTargetTrailIds
|
|
690
|
+
);
|
|
691
|
+
collectTrailIntents(
|
|
692
|
+
sourceFile.sourceCode,
|
|
693
|
+
sourceFile.filePath,
|
|
694
|
+
context.trailIntentsById
|
|
695
|
+
);
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const collectFileSupplementalProjectContext = (
|
|
699
|
+
sourceFile: SourceFile,
|
|
700
|
+
context: MutableProjectContext
|
|
701
|
+
): void => {
|
|
702
|
+
collectCrudTableIds(
|
|
703
|
+
sourceFile.sourceCode,
|
|
704
|
+
sourceFile.filePath,
|
|
705
|
+
context.crudTableIds
|
|
706
|
+
);
|
|
707
|
+
collectOnTargetSignalIds(
|
|
708
|
+
sourceFile.sourceCode,
|
|
709
|
+
sourceFile.filePath,
|
|
710
|
+
context.onTargetSignalIds
|
|
711
|
+
);
|
|
712
|
+
collectReconcileTableIds(
|
|
713
|
+
sourceFile.sourceCode,
|
|
714
|
+
sourceFile.filePath,
|
|
715
|
+
context.reconcileTableIds
|
|
716
|
+
);
|
|
717
|
+
collectCrudCoverageByEntity(
|
|
718
|
+
sourceFile.sourceCode,
|
|
719
|
+
sourceFile.filePath,
|
|
720
|
+
context.crudCoverageByEntity
|
|
721
|
+
);
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const collectFileProjectContext = (
|
|
725
|
+
sourceFile: SourceFile,
|
|
726
|
+
context: MutableProjectContext
|
|
727
|
+
): void => {
|
|
728
|
+
collectFileKnownIds(sourceFile, context);
|
|
729
|
+
collectFileTrailRelationships(sourceFile, context);
|
|
730
|
+
collectFileSupplementalProjectContext(sourceFile, context);
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const collectFileContourReferences = (
|
|
734
|
+
sourceFile: SourceFile,
|
|
735
|
+
context: MutableProjectContext
|
|
736
|
+
): void => {
|
|
737
|
+
const ast = parse(sourceFile.filePath, sourceFile.sourceCode);
|
|
738
|
+
if (!ast) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const referencesByName = collectContourReferenceTargetsByName(
|
|
743
|
+
ast,
|
|
744
|
+
context.knownContourIds
|
|
745
|
+
);
|
|
746
|
+
for (const [contourName, targets] of referencesByName) {
|
|
747
|
+
addContourReferenceTargets(context, contourName, targets);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const collectFileImportResolutions = (
|
|
752
|
+
rootDir: string,
|
|
753
|
+
sourceFiles: readonly SourceFile[],
|
|
754
|
+
context: MutableProjectContext
|
|
755
|
+
): void => {
|
|
756
|
+
const resolutionsByFile = collectProjectImportResolutions({
|
|
757
|
+
rootDir,
|
|
758
|
+
sourceFiles,
|
|
759
|
+
});
|
|
760
|
+
for (const [filePath, resolutions] of resolutionsByFile) {
|
|
761
|
+
context.importResolutionsByFile.set(filePath, resolutions);
|
|
762
|
+
}
|
|
201
763
|
};
|
|
202
764
|
|
|
203
|
-
const
|
|
204
|
-
|
|
765
|
+
const collectFileDocumentedImportResolutions = (
|
|
766
|
+
rootDir: string,
|
|
767
|
+
sourceFiles: readonly SourceFile[],
|
|
768
|
+
context: MutableProjectContext
|
|
769
|
+
): void => {
|
|
770
|
+
const resolutionsByFile = collectProjectDocumentationImportResolutions({
|
|
771
|
+
rootDir,
|
|
772
|
+
sourceFiles,
|
|
773
|
+
});
|
|
774
|
+
for (const [filePath, resolutions] of resolutionsByFile) {
|
|
775
|
+
context.documentedImportResolutionsByFile.set(filePath, resolutions);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const buildProjectContext = (
|
|
780
|
+
sourceFiles: readonly SourceFile[],
|
|
781
|
+
rootDir: string,
|
|
782
|
+
appTopos: readonly Topo[] = []
|
|
205
783
|
): ProjectContext => {
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
784
|
+
const context = createMutableProjectContext();
|
|
785
|
+
const typeScriptSourceFiles = sourceFiles.filter(
|
|
786
|
+
(sourceFile) => sourceFile.kind === 'typescript'
|
|
787
|
+
);
|
|
788
|
+
const documentationSourceFiles = sourceFiles.filter(
|
|
789
|
+
(sourceFile) => sourceFile.kind === 'documentation'
|
|
790
|
+
);
|
|
791
|
+
context.publicWorkspaces = collectPublicWorkspaces(rootDir);
|
|
209
792
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
collectDetourTargetTrailIds(
|
|
222
|
-
sourceFile.sourceCode,
|
|
223
|
-
sourceFile.filePath,
|
|
224
|
-
detourTargetTrailIds
|
|
225
|
-
);
|
|
793
|
+
if (appTopos.length > 0) {
|
|
794
|
+
for (const appTopo of appTopos) {
|
|
795
|
+
collectTopoTrailContext(appTopo, context);
|
|
796
|
+
}
|
|
797
|
+
for (const sourceFile of typeScriptSourceFiles) {
|
|
798
|
+
collectFileSupplementalProjectContext(sourceFile, context);
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
for (const sourceFile of typeScriptSourceFiles) {
|
|
802
|
+
collectFileProjectContext(sourceFile, context);
|
|
803
|
+
}
|
|
226
804
|
}
|
|
227
805
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
806
|
+
for (const sourceFile of typeScriptSourceFiles) {
|
|
807
|
+
collectFileContourReferences(sourceFile, context);
|
|
808
|
+
}
|
|
809
|
+
collectFileImportResolutions(rootDir, typeScriptSourceFiles, context);
|
|
810
|
+
collectFileDocumentedImportResolutions(
|
|
811
|
+
rootDir,
|
|
812
|
+
documentationSourceFiles,
|
|
813
|
+
context
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
return toProjectContext(context);
|
|
233
817
|
};
|
|
234
818
|
|
|
235
819
|
const isProjectAwareRule = (rule: WardenRule): rule is ProjectAwareWardenRule =>
|
|
236
820
|
'checkWithContext' in rule;
|
|
237
821
|
|
|
822
|
+
const createOptionsDiagnostic = (message: string): WardenDiagnostic => ({
|
|
823
|
+
filePath: '<warden-options>',
|
|
824
|
+
line: 1,
|
|
825
|
+
message,
|
|
826
|
+
rule: 'warden-options',
|
|
827
|
+
severity: 'error',
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
interface WardenRuleSelector {
|
|
831
|
+
readonly depth?: WardenDepth | undefined;
|
|
832
|
+
readonly tier?: WardenRuleTier | undefined;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const depthIncludesTier = (
|
|
836
|
+
depth: WardenDepth,
|
|
837
|
+
tier: WardenRuleTier
|
|
838
|
+
): boolean => {
|
|
839
|
+
switch (depth) {
|
|
840
|
+
case 'source': {
|
|
841
|
+
return tier === 'source-static';
|
|
842
|
+
}
|
|
843
|
+
case 'project': {
|
|
844
|
+
return tier === 'source-static' || tier === 'project-static';
|
|
845
|
+
}
|
|
846
|
+
case 'topo': {
|
|
847
|
+
return (
|
|
848
|
+
tier === 'source-static' ||
|
|
849
|
+
tier === 'project-static' ||
|
|
850
|
+
tier === 'topo-aware'
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
case 'all': {
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
default: {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const ruleMatchesTier = (
|
|
863
|
+
metadata: ReturnType<typeof getWardenRuleMetadata>,
|
|
864
|
+
tier: WardenRuleTier | undefined
|
|
865
|
+
): boolean => {
|
|
866
|
+
if (!tier) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (!metadata) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return tier === 'advisory'
|
|
875
|
+
? metadata.scope === 'advisory'
|
|
876
|
+
: metadata.tier === tier;
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
const ruleMatchesDepth = (
|
|
880
|
+
metadata: ReturnType<typeof getWardenRuleMetadata>,
|
|
881
|
+
depth: WardenDepth | undefined
|
|
882
|
+
): boolean => {
|
|
883
|
+
if (!depth) {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (!metadata) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (metadata.scope === 'advisory') {
|
|
892
|
+
return depth === 'all';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return depthIncludesTier(depth, metadata.tier);
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const isSelectedRule = (
|
|
899
|
+
rule: WardenRule | TopoAwareWardenRule,
|
|
900
|
+
selector: WardenRuleSelector
|
|
901
|
+
): boolean => {
|
|
902
|
+
const metadata = getWardenRuleMetadata(rule);
|
|
903
|
+
return selector.tier
|
|
904
|
+
? ruleMatchesTier(metadata, selector.tier)
|
|
905
|
+
: ruleMatchesDepth(metadata, selector.depth);
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const isSelectedTopoRule = (
|
|
909
|
+
rule: TopoAwareWardenRule,
|
|
910
|
+
selector: WardenRuleSelector
|
|
911
|
+
): boolean => {
|
|
912
|
+
const metadata = getWardenRuleMetadata(rule);
|
|
913
|
+
if (selector.tier) {
|
|
914
|
+
return metadata
|
|
915
|
+
? ruleMatchesTier(metadata, selector.tier)
|
|
916
|
+
: selector.tier === 'topo-aware';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return metadata ? ruleMatchesDepth(metadata, selector.depth) : true;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const withDiagnosticGuidance = (
|
|
923
|
+
diagnostic: WardenDiagnostic
|
|
924
|
+
): WardenDiagnostic => {
|
|
925
|
+
if (diagnostic.guidance !== undefined) {
|
|
926
|
+
return diagnostic;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const guidance = getWardenRuleMetadata(diagnostic.rule)?.guidance;
|
|
930
|
+
return guidance === undefined ? diagnostic : { ...diagnostic, guidance };
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
const topoRuleFailureDiagnostic = (
|
|
934
|
+
rule: TopoAwareWardenRule,
|
|
935
|
+
error: unknown
|
|
936
|
+
): WardenDiagnostic => {
|
|
937
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
938
|
+
return {
|
|
939
|
+
filePath: '<topo>',
|
|
940
|
+
line: 1,
|
|
941
|
+
message: `Topo-aware rule "${rule.name}" threw: ${cause.message}`,
|
|
942
|
+
rule: rule.name,
|
|
943
|
+
severity: 'error',
|
|
944
|
+
};
|
|
945
|
+
};
|
|
946
|
+
|
|
238
947
|
/**
|
|
239
|
-
*
|
|
948
|
+
* Run all registered topo-aware rules against the resolved topo.
|
|
949
|
+
*
|
|
950
|
+
* Topo-aware rules fire exactly once per run (not per file) because they
|
|
951
|
+
* inspect the compiled trail graph, not source text.
|
|
240
952
|
*/
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
953
|
+
const lintTopo = async (
|
|
954
|
+
appTopo: Topo,
|
|
955
|
+
extraTopoRules: readonly TopoAwareWardenRule[],
|
|
956
|
+
selector: WardenRuleSelector
|
|
957
|
+
): Promise<readonly WardenDiagnostic[]> => {
|
|
958
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
959
|
+
const rules: readonly TopoAwareWardenRule[] = [
|
|
960
|
+
...wardenTopoRules.values(),
|
|
961
|
+
...extraTopoRules,
|
|
962
|
+
].filter((rule) => isSelectedTopoRule(rule, selector));
|
|
963
|
+
for (const rule of rules) {
|
|
964
|
+
try {
|
|
965
|
+
diagnostics.push(...(await rule.checkTopo(appTopo)));
|
|
966
|
+
} catch (error) {
|
|
967
|
+
diagnostics.push(topoRuleFailureDiagnostic(rule, error));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return diagnostics;
|
|
971
|
+
};
|
|
250
972
|
|
|
973
|
+
const lintSourceFiles = (
|
|
974
|
+
sourceFiles: readonly SourceFile[],
|
|
975
|
+
context: ProjectContext,
|
|
976
|
+
selector: WardenRuleSelector
|
|
977
|
+
): readonly WardenDiagnostic[] => {
|
|
978
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
251
979
|
for (const sourceFile of sourceFiles) {
|
|
252
980
|
for (const rule of wardenRules.values()) {
|
|
981
|
+
if (
|
|
982
|
+
sourceFile.kind === 'text' &&
|
|
983
|
+
rule.name !== 'no-dev-permit-in-source' &&
|
|
984
|
+
rule.name !== 'public-internal-deep-imports'
|
|
985
|
+
) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (
|
|
990
|
+
sourceFile.kind === 'documentation' &&
|
|
991
|
+
rule.name !== 'public-internal-deep-imports'
|
|
992
|
+
) {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (!isSelectedRule(rule, selector)) {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
|
|
253
1000
|
if (isProjectAwareRule(rule)) {
|
|
254
|
-
|
|
1001
|
+
diagnostics.push(
|
|
255
1002
|
...rule.checkWithContext(
|
|
256
1003
|
sourceFile.sourceCode,
|
|
257
1004
|
sourceFile.filePath,
|
|
@@ -260,47 +1007,331 @@ const lintFiles = async (
|
|
|
260
1007
|
);
|
|
261
1008
|
continue;
|
|
262
1009
|
}
|
|
263
|
-
|
|
264
|
-
allDiagnostics.push(
|
|
1010
|
+
diagnostics.push(
|
|
265
1011
|
...rule.check(sourceFile.sourceCode, sourceFile.filePath)
|
|
266
1012
|
);
|
|
267
1013
|
}
|
|
268
1014
|
}
|
|
1015
|
+
return diagnostics;
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const tagTopoDiagnostic = (
|
|
1019
|
+
diagnostic: WardenDiagnostic,
|
|
1020
|
+
topoName: string | undefined
|
|
1021
|
+
): WardenDiagnostic =>
|
|
1022
|
+
topoName === undefined ? diagnostic : { ...diagnostic, topoName };
|
|
1023
|
+
|
|
1024
|
+
const lintTopoTargets = async (
|
|
1025
|
+
topoTargets: readonly WardenTopoTarget[],
|
|
1026
|
+
extraTopoRules: readonly TopoAwareWardenRule[],
|
|
1027
|
+
selector: WardenRuleSelector,
|
|
1028
|
+
tagDiagnostics: boolean
|
|
1029
|
+
): Promise<readonly WardenDiagnostic[]> => {
|
|
1030
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
1031
|
+
|
|
1032
|
+
for (const target of topoTargets) {
|
|
1033
|
+
const topoDiagnostics = await lintTopo(
|
|
1034
|
+
target.topo,
|
|
1035
|
+
extraTopoRules,
|
|
1036
|
+
selector
|
|
1037
|
+
);
|
|
1038
|
+
const topoName = target.name ?? target.topo.name;
|
|
1039
|
+
diagnostics.push(
|
|
1040
|
+
...(tagDiagnostics
|
|
1041
|
+
? topoDiagnostics.map((diagnostic) =>
|
|
1042
|
+
tagTopoDiagnostic(diagnostic, topoName)
|
|
1043
|
+
)
|
|
1044
|
+
: topoDiagnostics)
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return diagnostics;
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
const selectorIncludesTopoRules = (selector: WardenRuleSelector): boolean => {
|
|
1052
|
+
if (selector.tier) {
|
|
1053
|
+
return selector.tier === 'advisory';
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return !selector.depth || depthIncludesTier(selector.depth, 'topo-aware');
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Lint all files against all warden rules.
|
|
1061
|
+
*/
|
|
1062
|
+
const lintFiles = async (
|
|
1063
|
+
rootDir: string,
|
|
1064
|
+
drafts: EffectiveWardenConfig['drafts'],
|
|
1065
|
+
topoTargets: readonly WardenTopoTarget[],
|
|
1066
|
+
extraTopoRules: readonly TopoAwareWardenRule[],
|
|
1067
|
+
selector: WardenRuleSelector
|
|
1068
|
+
): Promise<WardenDiagnostic[]> => {
|
|
1069
|
+
if (selector.tier === 'topo-aware') {
|
|
1070
|
+
return [
|
|
1071
|
+
...(await lintTopoTargets(topoTargets, extraTopoRules, selector, true)),
|
|
1072
|
+
];
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const sourceFiles = filterSourceFilesByDraftMode(
|
|
1076
|
+
await loadSourceFiles(rootDir),
|
|
1077
|
+
drafts
|
|
1078
|
+
);
|
|
1079
|
+
const context = buildProjectContext(
|
|
1080
|
+
sourceFiles,
|
|
1081
|
+
rootDir,
|
|
1082
|
+
topoTargets.map((target) => target.topo)
|
|
1083
|
+
);
|
|
1084
|
+
const allDiagnostics: WardenDiagnostic[] = [
|
|
1085
|
+
...lintSourceFiles(sourceFiles, context, selector),
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
if (
|
|
1089
|
+
topoTargets.length > 0 &&
|
|
1090
|
+
(selector.tier === undefined || selector.tier === 'advisory') &&
|
|
1091
|
+
selectorIncludesTopoRules(selector)
|
|
1092
|
+
) {
|
|
1093
|
+
allDiagnostics.push(
|
|
1094
|
+
...(await lintTopoTargets(
|
|
1095
|
+
topoTargets,
|
|
1096
|
+
extraTopoRules,
|
|
1097
|
+
selector,
|
|
1098
|
+
topoTargets.length > 1
|
|
1099
|
+
))
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
269
1102
|
|
|
270
1103
|
return allDiagnostics;
|
|
271
1104
|
};
|
|
272
1105
|
|
|
1106
|
+
const topoTargetsFromOptions = (
|
|
1107
|
+
options: WardenRunOptions
|
|
1108
|
+
): readonly WardenTopoTarget[] => {
|
|
1109
|
+
if (options.topos !== undefined && options.topos.length > 0) {
|
|
1110
|
+
return options.topos;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return options.topo ? [{ name: options.topo.name, topo: options.topo }] : [];
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const aggregateDriftHash = (
|
|
1117
|
+
topoTargets: readonly WardenTopoTarget[],
|
|
1118
|
+
driftResults: readonly DriftResult[]
|
|
1119
|
+
): string => {
|
|
1120
|
+
const currentHashes = new Set(
|
|
1121
|
+
driftResults.map((result) => result.currentHash)
|
|
1122
|
+
);
|
|
1123
|
+
const [onlyHash] = currentHashes;
|
|
1124
|
+
if (currentHashes.size === 1 && onlyHash !== undefined) {
|
|
1125
|
+
return onlyHash;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const payload = driftResults
|
|
1129
|
+
.map((result, index) => {
|
|
1130
|
+
const target = topoTargets[index];
|
|
1131
|
+
return {
|
|
1132
|
+
currentHash: result.currentHash,
|
|
1133
|
+
topoName: target?.name ?? target?.topo.name ?? `topo-${String(index)}`,
|
|
1134
|
+
};
|
|
1135
|
+
})
|
|
1136
|
+
.toSorted((left, right) => left.topoName.localeCompare(right.topoName));
|
|
1137
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
1138
|
+
hasher.update(JSON.stringify(payload));
|
|
1139
|
+
return hasher.digest('hex');
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
const describeTopoDriftHash = (
|
|
1143
|
+
topoTargets: readonly WardenTopoTarget[],
|
|
1144
|
+
driftResults: readonly DriftResult[]
|
|
1145
|
+
): string =>
|
|
1146
|
+
driftResults
|
|
1147
|
+
.map((result, index) => {
|
|
1148
|
+
const target = topoTargets[index];
|
|
1149
|
+
const topoName =
|
|
1150
|
+
target?.name ?? target?.topo.name ?? `topo-${String(index)}`;
|
|
1151
|
+
return `${topoName}=${result.committedHash ?? '<none>'}`;
|
|
1152
|
+
})
|
|
1153
|
+
.join(', ');
|
|
1154
|
+
|
|
1155
|
+
const checkDriftForTopoTargets = async (
|
|
1156
|
+
rootDir: string,
|
|
1157
|
+
topoTargets: readonly WardenTopoTarget[]
|
|
1158
|
+
): Promise<DriftResult> => {
|
|
1159
|
+
if (topoTargets.length <= 1) {
|
|
1160
|
+
return checkDrift(rootDir, topoTargets[0]?.topo);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const driftResults = await Promise.all(
|
|
1164
|
+
topoTargets.map((target) => checkDrift(rootDir, target.topo))
|
|
1165
|
+
);
|
|
1166
|
+
const committedHashes = new Set(
|
|
1167
|
+
driftResults.map((result) => result.committedHash)
|
|
1168
|
+
);
|
|
1169
|
+
if (committedHashes.size > 1) {
|
|
1170
|
+
return {
|
|
1171
|
+
blockedReason: `multi-topo drift expected one committed trails.lock hash but found conflicting hashes: ${describeTopoDriftHash(topoTargets, driftResults)}`,
|
|
1172
|
+
committedHash: null,
|
|
1173
|
+
currentHash: 'blocked',
|
|
1174
|
+
stale: true,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const committedHash = driftResults[0]?.committedHash ?? null;
|
|
1178
|
+
const blockedReasons = driftResults.flatMap((result, index) => {
|
|
1179
|
+
if (result.blockedReason === undefined) {
|
|
1180
|
+
return [];
|
|
1181
|
+
}
|
|
1182
|
+
const target = topoTargets[index];
|
|
1183
|
+
const topoName =
|
|
1184
|
+
target?.name ?? target?.topo.name ?? `topo-${String(index)}`;
|
|
1185
|
+
return [`${topoName}: ${result.blockedReason}`];
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
if (blockedReasons.length > 0) {
|
|
1189
|
+
return {
|
|
1190
|
+
blockedReason: blockedReasons.join('; '),
|
|
1191
|
+
committedHash,
|
|
1192
|
+
currentHash: 'blocked',
|
|
1193
|
+
stale: true,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const currentHash = aggregateDriftHash(topoTargets, driftResults);
|
|
1198
|
+
return {
|
|
1199
|
+
committedHash,
|
|
1200
|
+
currentHash,
|
|
1201
|
+
stale: committedHash !== null && committedHash !== currentHash,
|
|
1202
|
+
};
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const shouldRunLint = (options: WardenRunOptions): boolean =>
|
|
1206
|
+
options.tier ? options.tier !== 'drift' : !options.driftOnly;
|
|
1207
|
+
|
|
1208
|
+
const shouldRunDrift = (
|
|
1209
|
+
options: WardenRunOptions,
|
|
1210
|
+
effectiveConfig: EffectiveWardenConfig
|
|
1211
|
+
): boolean => {
|
|
1212
|
+
if (effectiveConfig.lock === 'skip') {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (options.tier) {
|
|
1217
|
+
return options.tier === 'drift';
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (options.lintOnly) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return options.driftOnly || effectiveConfig.depth === 'all';
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const reportPassed = ({
|
|
1228
|
+
drift,
|
|
1229
|
+
errorCount,
|
|
1230
|
+
failOn,
|
|
1231
|
+
warnCount,
|
|
1232
|
+
}: {
|
|
1233
|
+
readonly drift: DriftResult | null;
|
|
1234
|
+
readonly errorCount: number;
|
|
1235
|
+
readonly failOn: WardenFailOn;
|
|
1236
|
+
readonly warnCount: number;
|
|
1237
|
+
}): boolean =>
|
|
1238
|
+
errorCount === 0 &&
|
|
1239
|
+
(failOn === 'error' || warnCount === 0) &&
|
|
1240
|
+
!(drift?.stale ?? false) &&
|
|
1241
|
+
drift?.blockedReason === undefined;
|
|
1242
|
+
|
|
1243
|
+
const buildCliConfigLayer = (options: WardenRunOptions): WardenConfigLayer => ({
|
|
1244
|
+
...(options.apps ? { apps: [...options.apps] } : {}),
|
|
1245
|
+
...(options.depth ? { depth: options.depth } : {}),
|
|
1246
|
+
...(options.drafts ? { drafts: options.drafts } : {}),
|
|
1247
|
+
...(options.failOn ? { failOn: options.failOn } : {}),
|
|
1248
|
+
...(options.format ? { format: options.format } : {}),
|
|
1249
|
+
...(options.lock ? { lock: options.lock } : {}),
|
|
1250
|
+
...(options.noLockMutation === undefined
|
|
1251
|
+
? {}
|
|
1252
|
+
: { noLockMutation: options.noLockMutation }),
|
|
1253
|
+
});
|
|
1254
|
+
|
|
273
1255
|
/**
|
|
274
1256
|
* Run all warden checks and return a structured report.
|
|
275
1257
|
*/
|
|
276
1258
|
export const runWarden = async (
|
|
277
|
-
options:
|
|
1259
|
+
options: WardenRunOptions = {}
|
|
278
1260
|
): Promise<WardenReport> => {
|
|
279
1261
|
const rootDir = resolve(options.rootDir ?? process.cwd());
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1262
|
+
const { diagnostics: configDiagnostics, effectiveConfig } =
|
|
1263
|
+
resolveWardenConfig({
|
|
1264
|
+
cli: buildCliConfigLayer(options),
|
|
1265
|
+
config: options.config,
|
|
1266
|
+
env: options.env,
|
|
1267
|
+
});
|
|
1268
|
+
const optionDiagnostics =
|
|
1269
|
+
!options.tier && options.lintOnly && options.driftOnly
|
|
1270
|
+
? [
|
|
1271
|
+
createOptionsDiagnostic(
|
|
1272
|
+
'lintOnly and driftOnly cannot both be true. Use tier to select a single Warden mode.'
|
|
1273
|
+
),
|
|
1274
|
+
]
|
|
1275
|
+
: [];
|
|
1276
|
+
const topoTargets = topoTargetsFromOptions(options);
|
|
1277
|
+
const selector = {
|
|
1278
|
+
depth: options.tier ? undefined : effectiveConfig.depth,
|
|
1279
|
+
tier: options.tier,
|
|
1280
|
+
} satisfies WardenRuleSelector;
|
|
1281
|
+
const runLint = shouldRunLint(options);
|
|
1282
|
+
const runDrift = shouldRunDrift(options, effectiveConfig);
|
|
1283
|
+
|
|
1284
|
+
const rawDiagnostics = [
|
|
1285
|
+
...configDiagnostics,
|
|
1286
|
+
...optionDiagnostics,
|
|
1287
|
+
...(runLint
|
|
1288
|
+
? await lintFiles(
|
|
1289
|
+
rootDir,
|
|
1290
|
+
effectiveConfig.drafts,
|
|
1291
|
+
topoTargets,
|
|
1292
|
+
options.extraTopoRules ?? [],
|
|
1293
|
+
selector
|
|
1294
|
+
)
|
|
1295
|
+
: []),
|
|
1296
|
+
];
|
|
1297
|
+
const allDiagnostics = rawDiagnostics.map(withDiagnosticGuidance);
|
|
1298
|
+
const drift = runDrift
|
|
1299
|
+
? await checkDriftForTopoTargets(rootDir, topoTargets)
|
|
1300
|
+
: null;
|
|
286
1301
|
|
|
287
1302
|
const errorCount = allDiagnostics.filter(
|
|
288
1303
|
(d) => d.severity === 'error'
|
|
289
1304
|
).length;
|
|
290
1305
|
const warnCount = allDiagnostics.filter((d) => d.severity === 'warn').length;
|
|
1306
|
+
const topoNames =
|
|
1307
|
+
topoTargets.length > 0
|
|
1308
|
+
? topoTargets.map((target) => target.name ?? target.topo.name)
|
|
1309
|
+
: undefined;
|
|
291
1310
|
|
|
292
1311
|
return {
|
|
293
1312
|
diagnostics: allDiagnostics,
|
|
294
1313
|
drift,
|
|
1314
|
+
effectiveConfig,
|
|
295
1315
|
errorCount,
|
|
296
|
-
passed:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1316
|
+
passed: reportPassed({
|
|
1317
|
+
drift,
|
|
1318
|
+
errorCount,
|
|
1319
|
+
failOn: effectiveConfig.failOn,
|
|
1320
|
+
warnCount,
|
|
1321
|
+
}),
|
|
1322
|
+
...(topoNames === undefined ? {} : { topoNames }),
|
|
300
1323
|
warnCount,
|
|
301
1324
|
};
|
|
302
1325
|
};
|
|
303
1326
|
|
|
1327
|
+
const formatPlainGuidanceLink = (link: WardenGuidanceLink): string => {
|
|
1328
|
+
const target = link.path ?? link.url;
|
|
1329
|
+
if (target === undefined || target === link.label) {
|
|
1330
|
+
return link.label;
|
|
1331
|
+
}
|
|
1332
|
+
return `${link.label} (${target})`;
|
|
1333
|
+
};
|
|
1334
|
+
|
|
304
1335
|
/**
|
|
305
1336
|
* Format the lint section of the report.
|
|
306
1337
|
*/
|
|
@@ -318,6 +1349,25 @@ const formatLintSection = (report: WardenReport): string[] => {
|
|
|
318
1349
|
lines.push(
|
|
319
1350
|
` ${d.filePath}:${String(d.line)} [${prefix}] ${d.rule} ${d.message}`
|
|
320
1351
|
);
|
|
1352
|
+
if (d.guidance !== undefined) {
|
|
1353
|
+
lines.push(` Next: ${d.guidance.summary}`);
|
|
1354
|
+
for (const [index, step] of (d.guidance.steps ?? []).entries()) {
|
|
1355
|
+
lines.push(` ${String(index + 1)}. ${step}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (d.guidance.commands !== undefined) {
|
|
1358
|
+
lines.push(
|
|
1359
|
+
` Commands: ${d.guidance.commands.map((cmd) => `\`${cmd}\``).join(', ')}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
if (d.guidance.docs !== undefined) {
|
|
1363
|
+
lines.push(
|
|
1364
|
+
` Docs: ${d.guidance.docs.map(formatPlainGuidanceLink).join(', ')}`
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if (d.guidance.relatedRules !== undefined) {
|
|
1368
|
+
lines.push(` Related: ${d.guidance.relatedRules.join(', ')}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
321
1371
|
}
|
|
322
1372
|
|
|
323
1373
|
return lines;
|
|
@@ -334,7 +1384,7 @@ const formatDriftSection = (drift: DriftResult | null): string[] => {
|
|
|
334
1384
|
return [`Drift: blocked (${drift.blockedReason})`, ''];
|
|
335
1385
|
}
|
|
336
1386
|
const label = drift.stale
|
|
337
|
-
? 'Drift: trails.lock is stale (regenerate with `trails topo
|
|
1387
|
+
? 'Drift: trails.lock is stale (regenerate with `trails topo compile`)'
|
|
338
1388
|
: 'Drift: clean';
|
|
339
1389
|
return [label, ''];
|
|
340
1390
|
};
|
|
@@ -350,6 +1400,9 @@ const formatResultLine = (report: WardenReport): string => {
|
|
|
350
1400
|
if (report.errorCount > 0) {
|
|
351
1401
|
parts.push(`${report.errorCount} errors`);
|
|
352
1402
|
}
|
|
1403
|
+
if (report.warnCount > 0 && report.effectiveConfig?.failOn === 'warning') {
|
|
1404
|
+
parts.push(`${report.warnCount} warnings`);
|
|
1405
|
+
}
|
|
353
1406
|
if (report.drift?.blockedReason !== undefined) {
|
|
354
1407
|
parts.push('established exports blocked');
|
|
355
1408
|
} else if (report.drift?.stale) {
|