@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { filterSurfaceTrails, zodToJsonSchema } from '@ontrails/core';
|
|
2
|
+
import type { AnyTrail, Topo } from '@ontrails/core';
|
|
3
|
+
|
|
4
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
5
|
+
|
|
6
|
+
const RULE_NAME = 'public-union-output-discriminants';
|
|
7
|
+
|
|
8
|
+
type JsonSchema = Readonly<Record<string, unknown>>;
|
|
9
|
+
|
|
10
|
+
interface ObjectBranch {
|
|
11
|
+
readonly properties: Readonly<Record<string, JsonSchema>>;
|
|
12
|
+
readonly required: ReadonlySet<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
|
+
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
17
|
+
|
|
18
|
+
const isJsonSchema = (value: unknown): value is JsonSchema => isRecord(value);
|
|
19
|
+
|
|
20
|
+
const schemaForTrailOutput = (trail: AnyTrail): JsonSchema | undefined => {
|
|
21
|
+
if (!trail.output) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return zodToJsonSchema(trail.output);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const readProperties = (
|
|
32
|
+
schema: JsonSchema
|
|
33
|
+
): Readonly<Record<string, JsonSchema>> | undefined => {
|
|
34
|
+
const { properties } = schema;
|
|
35
|
+
if (!isRecord(properties)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const entries = Object.entries(properties);
|
|
39
|
+
if (!entries.every(([, value]) => isJsonSchema(value))) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return properties as Readonly<Record<string, JsonSchema>>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const readRequired = (schema: JsonSchema): ReadonlySet<string> => {
|
|
46
|
+
const { required } = schema;
|
|
47
|
+
if (!Array.isArray(required)) {
|
|
48
|
+
return new Set();
|
|
49
|
+
}
|
|
50
|
+
return new Set(
|
|
51
|
+
required.filter((entry): entry is string => typeof entry === 'string')
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const objectBranchFromSchema = (schema: unknown): ObjectBranch | undefined => {
|
|
56
|
+
if (!isJsonSchema(schema) || schema['type'] !== 'object') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const properties = readProperties(schema);
|
|
60
|
+
if (!properties) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return { properties, required: readRequired(schema) };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const objectBranchesFromAnyOf = (
|
|
67
|
+
schema: JsonSchema
|
|
68
|
+
): readonly ObjectBranch[] | undefined => {
|
|
69
|
+
const { anyOf } = schema;
|
|
70
|
+
if (!Array.isArray(anyOf) || anyOf.length < 2) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const branches = anyOf.flatMap((branch) => {
|
|
74
|
+
const objectBranch = objectBranchFromSchema(branch);
|
|
75
|
+
return objectBranch ? [objectBranch] : [];
|
|
76
|
+
});
|
|
77
|
+
return branches.length >= 2 ? branches : undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const hasConst = (schema: JsonSchema): boolean =>
|
|
81
|
+
Object.hasOwn(schema, 'const');
|
|
82
|
+
|
|
83
|
+
const constValue = (schema: JsonSchema): unknown => schema['const'];
|
|
84
|
+
|
|
85
|
+
const constKey = (value: unknown): string => JSON.stringify(value);
|
|
86
|
+
|
|
87
|
+
const branchLiteralForKey = (
|
|
88
|
+
branch: ObjectBranch,
|
|
89
|
+
key: string
|
|
90
|
+
): unknown | undefined => {
|
|
91
|
+
if (!branch.required.has(key)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const property = branch.properties[key];
|
|
95
|
+
return property && hasConst(property) ? constValue(property) : undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const hasRequiredLiteralDiscriminant = (
|
|
99
|
+
branches: readonly ObjectBranch[]
|
|
100
|
+
): boolean => {
|
|
101
|
+
const [first] = branches;
|
|
102
|
+
if (!first) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Object.keys(first.properties).some((key) => {
|
|
107
|
+
const values = branches.map((branch) => branchLiteralForKey(branch, key));
|
|
108
|
+
return (
|
|
109
|
+
values.every((value) => value !== undefined) &&
|
|
110
|
+
new Set(values.map(constKey)).size === branches.length
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const diagnosticForTrail = (trail: AnyTrail): WardenDiagnostic => ({
|
|
116
|
+
filePath: '<topo>',
|
|
117
|
+
line: 1,
|
|
118
|
+
message:
|
|
119
|
+
`Trail "${trail.id}" exposes a public output anyOf with object variants but no required literal discriminator. ` +
|
|
120
|
+
'Add a shared z.literal(...) field or z.discriminatedUnion(...) so surfaces and agents can select the output branch.',
|
|
121
|
+
rule: RULE_NAME,
|
|
122
|
+
severity: 'error',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const diagnoseTrail = (trail: AnyTrail): WardenDiagnostic | undefined => {
|
|
126
|
+
const schema = schemaForTrailOutput(trail);
|
|
127
|
+
if (!schema) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const objectBranches = objectBranchesFromAnyOf(schema);
|
|
131
|
+
if (!objectBranches) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return hasRequiredLiteralDiscriminant(objectBranches)
|
|
135
|
+
? undefined
|
|
136
|
+
: diagnosticForTrail(trail);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const publicUnionOutputDiscriminants: TopoAwareWardenRule = {
|
|
140
|
+
checkTopo(topo: Topo): readonly WardenDiagnostic[] {
|
|
141
|
+
return filterSurfaceTrails(topo.list()).flatMap((trail) => {
|
|
142
|
+
const diagnostic = diagnoseTrail(trail);
|
|
143
|
+
return diagnostic ? [diagnostic] : [];
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
description:
|
|
147
|
+
'Require public trail output object unions to expose a required literal discriminator.',
|
|
148
|
+
name: RULE_NAME,
|
|
149
|
+
severity: 'error',
|
|
150
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSignalIdentifierResolver,
|
|
3
|
+
deriveConstString,
|
|
4
|
+
extractStringLiteral,
|
|
5
|
+
findConfigProperty,
|
|
6
|
+
findTrailDefinitions,
|
|
7
|
+
getStringValue,
|
|
8
|
+
identifierName,
|
|
9
|
+
isStringLiteral,
|
|
10
|
+
offsetToLine,
|
|
11
|
+
parse,
|
|
12
|
+
walk,
|
|
13
|
+
} from './ast.js';
|
|
14
|
+
import type { AstNode, SignalIdentifierResolver } from './ast.js';
|
|
15
|
+
import { isTestFile } from './scan.js';
|
|
16
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
17
|
+
|
|
18
|
+
interface DeclaredFireSummary {
|
|
19
|
+
readonly count: number;
|
|
20
|
+
readonly ids: readonly string[];
|
|
21
|
+
readonly line: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isReadIntent = (config: AstNode): boolean => {
|
|
25
|
+
const intentProp = findConfigProperty(config, 'intent');
|
|
26
|
+
const intentValue = intentProp?.value as AstNode | undefined;
|
|
27
|
+
return isStringLiteral(intentValue) && getStringValue(intentValue) === 'read';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
|
|
31
|
+
const bindings = new Map<string, AstNode>();
|
|
32
|
+
walk(ast, (node) => {
|
|
33
|
+
if (node.type !== 'VariableDeclarator') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { id, init } = node as unknown as {
|
|
38
|
+
readonly id?: AstNode;
|
|
39
|
+
readonly init?: AstNode;
|
|
40
|
+
};
|
|
41
|
+
const name = identifierName(id);
|
|
42
|
+
if (name && init?.type === 'ArrayExpression') {
|
|
43
|
+
bindings.set(name, init);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return bindings;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getFiresArray = (
|
|
50
|
+
config: AstNode,
|
|
51
|
+
arrayBindings: ReadonlyMap<string, AstNode>
|
|
52
|
+
): AstNode | null => {
|
|
53
|
+
const firesProp = findConfigProperty(config, 'fires');
|
|
54
|
+
const value = firesProp?.value as AstNode | undefined;
|
|
55
|
+
if (value?.type === 'ArrayExpression') {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const name = identifierName(value);
|
|
60
|
+
return name ? (arrayBindings.get(name) ?? null) : null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getFiresElements = (
|
|
64
|
+
config: AstNode,
|
|
65
|
+
arrayBindings: ReadonlyMap<string, AstNode>
|
|
66
|
+
): readonly AstNode[] => {
|
|
67
|
+
const array = getFiresArray(config, arrayBindings);
|
|
68
|
+
if (!array) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
(array as unknown as { readonly elements?: readonly (AstNode | null)[] })
|
|
74
|
+
.elements ?? []
|
|
75
|
+
).filter((element): element is AstNode => element !== null);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const resolveFireElementId = (
|
|
79
|
+
element: AstNode,
|
|
80
|
+
sourceCode: string,
|
|
81
|
+
signalIds: SignalIdentifierResolver
|
|
82
|
+
): string | null => {
|
|
83
|
+
const literalValue = extractStringLiteral(element);
|
|
84
|
+
if (literalValue !== null) {
|
|
85
|
+
return literalValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (element.type !== 'Identifier') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resolved = signalIds.resolve(element);
|
|
93
|
+
if (resolved.kind === 'signal' || resolved.kind === 'string') {
|
|
94
|
+
return resolved.id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const name = identifierName(element);
|
|
98
|
+
return name ? deriveConstString(name, sourceCode) : null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const summarizeDeclaredFires = (
|
|
102
|
+
config: AstNode,
|
|
103
|
+
arrayBindings: ReadonlyMap<string, AstNode>,
|
|
104
|
+
sourceCode: string,
|
|
105
|
+
signalIds: SignalIdentifierResolver
|
|
106
|
+
): DeclaredFireSummary | null => {
|
|
107
|
+
const firesProp = findConfigProperty(config, 'fires');
|
|
108
|
+
const elements = getFiresElements(config, arrayBindings);
|
|
109
|
+
if (elements.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ids: string[] = [];
|
|
114
|
+
for (const element of elements) {
|
|
115
|
+
const resolved = resolveFireElementId(element, sourceCode, signalIds);
|
|
116
|
+
if (resolved) {
|
|
117
|
+
ids.push(resolved);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const [firstElement] = elements;
|
|
122
|
+
const lineNode = firesProp ?? firstElement;
|
|
123
|
+
return {
|
|
124
|
+
count: elements.length,
|
|
125
|
+
ids,
|
|
126
|
+
line: lineNode ? offsetToLine(sourceCode, lineNode.start) : 1,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const formatSignalList = (summary: DeclaredFireSummary): string => {
|
|
131
|
+
const named = summary.ids.map((id) => `"${id}"`);
|
|
132
|
+
const unresolvedCount = summary.count - summary.ids.length;
|
|
133
|
+
const unresolved =
|
|
134
|
+
unresolvedCount > 0
|
|
135
|
+
? [
|
|
136
|
+
unresolvedCount === 1
|
|
137
|
+
? '1 unresolved signal reference'
|
|
138
|
+
: `${unresolvedCount} unresolved signal references`,
|
|
139
|
+
]
|
|
140
|
+
: [];
|
|
141
|
+
return [...named, ...unresolved].join(', ');
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const buildDiagnostic = (
|
|
145
|
+
trailId: string,
|
|
146
|
+
summary: DeclaredFireSummary,
|
|
147
|
+
filePath: string
|
|
148
|
+
): WardenDiagnostic => ({
|
|
149
|
+
filePath,
|
|
150
|
+
line: summary.line,
|
|
151
|
+
message: `Trail "${trailId}" declares intent: 'read' but also declares fires: [${formatSignalList(summary)}]. Read trails should remain side-effect-free; change the trail intent or move ctx.fire behavior to an appropriate write trail.`,
|
|
152
|
+
rule: 'read-intent-fires',
|
|
153
|
+
severity: 'warn',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const readIntentFires: WardenRule = {
|
|
157
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
158
|
+
if (isTestFile(filePath)) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ast = parse(filePath, sourceCode);
|
|
163
|
+
if (!ast) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const signalIds = buildSignalIdentifierResolver(ast);
|
|
168
|
+
const arrayBindings = collectArrayBindings(ast);
|
|
169
|
+
return findTrailDefinitions(ast).flatMap((def) => {
|
|
170
|
+
if (def.kind !== 'trail' || !isReadIntent(def.config)) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const summary = summarizeDeclaredFires(
|
|
175
|
+
def.config,
|
|
176
|
+
arrayBindings,
|
|
177
|
+
sourceCode,
|
|
178
|
+
signalIds
|
|
179
|
+
);
|
|
180
|
+
return summary ? [buildDiagnostic(def.id, summary, filePath)] : [];
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
description:
|
|
184
|
+
'Warn when read-intent trails declare signal fires side effects.',
|
|
185
|
+
name: 'read-intent-fires',
|
|
186
|
+
severity: 'warn',
|
|
187
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectContourDefinitionIds,
|
|
3
|
+
collectContourReferenceSites,
|
|
4
|
+
offsetToLine,
|
|
5
|
+
parse,
|
|
6
|
+
} from './ast.js';
|
|
7
|
+
import type { AstNode } from './ast.js';
|
|
8
|
+
import { mergeKnownContourIds } from './contour-ids.js';
|
|
9
|
+
import { isTestFile } from './scan.js';
|
|
10
|
+
import type {
|
|
11
|
+
ProjectAwareWardenRule,
|
|
12
|
+
ProjectContext,
|
|
13
|
+
WardenDiagnostic,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
const buildMissingReferenceDiagnostic = (
|
|
17
|
+
sourceContour: string,
|
|
18
|
+
field: string,
|
|
19
|
+
targetContour: string,
|
|
20
|
+
filePath: string,
|
|
21
|
+
line: number
|
|
22
|
+
): WardenDiagnostic => ({
|
|
23
|
+
filePath,
|
|
24
|
+
line,
|
|
25
|
+
message: `Contour "${sourceContour}" field "${field}" references contour "${targetContour}" which is not defined in the project.`,
|
|
26
|
+
rule: 'reference-exists',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const checkContourReferences = (
|
|
31
|
+
ast: AstNode,
|
|
32
|
+
sourceCode: string,
|
|
33
|
+
filePath: string,
|
|
34
|
+
knownContourIds: ReadonlySet<string>
|
|
35
|
+
): readonly WardenDiagnostic[] => {
|
|
36
|
+
if (isTestFile(filePath)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return collectContourReferenceSites(ast, knownContourIds).flatMap(
|
|
41
|
+
(reference) => {
|
|
42
|
+
if (knownContourIds.has(reference.target)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
buildMissingReferenceDiagnostic(
|
|
48
|
+
reference.source,
|
|
49
|
+
reference.field,
|
|
50
|
+
reference.target,
|
|
51
|
+
filePath,
|
|
52
|
+
offsetToLine(sourceCode, reference.start)
|
|
53
|
+
),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks that every contour `.id()` reference resolves to a known contour.
|
|
61
|
+
*/
|
|
62
|
+
export const referenceExists: ProjectAwareWardenRule = {
|
|
63
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
64
|
+
const ast = parse(filePath, sourceCode);
|
|
65
|
+
if (!ast) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return checkContourReferences(
|
|
70
|
+
ast,
|
|
71
|
+
sourceCode,
|
|
72
|
+
filePath,
|
|
73
|
+
collectContourDefinitionIds(ast)
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
checkWithContext(
|
|
77
|
+
sourceCode: string,
|
|
78
|
+
filePath: string,
|
|
79
|
+
context: ProjectContext
|
|
80
|
+
): readonly WardenDiagnostic[] {
|
|
81
|
+
const ast = parse(filePath, sourceCode);
|
|
82
|
+
if (!ast) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const localContourIds = collectContourDefinitionIds(ast);
|
|
87
|
+
return checkContourReferences(
|
|
88
|
+
ast,
|
|
89
|
+
sourceCode,
|
|
90
|
+
filePath,
|
|
91
|
+
mergeKnownContourIds(localContourIds, context.knownContourIds)
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
description:
|
|
95
|
+
'Ensure every contour field declared via .id() resolves to a known contour.',
|
|
96
|
+
name: 'reference-exists',
|
|
97
|
+
severity: 'error',
|
|
98
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry name snapshot used by `warden-export-symmetry`.
|
|
3
|
+
*
|
|
4
|
+
* Imports each rule module directly to avoid a dependency cycle with
|
|
5
|
+
* `./index.ts`, which itself imports `warden-export-symmetry`. Keep this
|
|
6
|
+
* list in lockstep with `wardenRules` / `wardenTopoRules` in `./index.ts` —
|
|
7
|
+
* the `warden-export-symmetry` rule will fail the build if they drift.
|
|
8
|
+
*/
|
|
9
|
+
import { activationOrphan } from './activation-orphan.js';
|
|
10
|
+
import { circularRefs } from './circular-refs.js';
|
|
11
|
+
import { contourExists } from './contour-exists.js';
|
|
12
|
+
import { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
13
|
+
import { crossDeclarations } from './cross-declarations.js';
|
|
14
|
+
import { deadInternalTrail } from './dead-internal-trail.js';
|
|
15
|
+
import { draftFileMarking } from './draft-file-marking.js';
|
|
16
|
+
import { draftVisibleDebt } from './draft-visible-debt.js';
|
|
17
|
+
import { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
18
|
+
import { exampleValid } from './example-valid.js';
|
|
19
|
+
import { firesDeclarations } from './fires-declarations.js';
|
|
20
|
+
import { implementationReturnsResult } from './implementation-returns-result.js';
|
|
21
|
+
import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
|
|
22
|
+
import { incompleteCrud } from './incomplete-crud.js';
|
|
23
|
+
import { intentPropagation } from './intent-propagation.js';
|
|
24
|
+
import { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
25
|
+
import { missingReconcile } from './missing-reconcile.js';
|
|
26
|
+
import { missingVisibility } from './missing-visibility.js';
|
|
27
|
+
import { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
28
|
+
import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
|
|
29
|
+
import { noDirectImplementationCall } from './no-direct-implementation-call.js';
|
|
30
|
+
import { noNativeErrorResult } from './no-native-error-result.js';
|
|
31
|
+
import { noSyncResultAssumption } from './no-sync-result-assumption.js';
|
|
32
|
+
import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
|
|
33
|
+
import { noThrowInImplementation } from './no-throw-in-implementation.js';
|
|
34
|
+
import { onReferencesExist } from './on-references-exist.js';
|
|
35
|
+
import { orphanedSignal } from './orphaned-signal.js';
|
|
36
|
+
import { ownerProjectionParity } from './owner-projection-parity.js';
|
|
37
|
+
import { permitGovernance } from './permit-governance.js';
|
|
38
|
+
import { preferSchemaInference } from './prefer-schema-inference.js';
|
|
39
|
+
import { publicInternalDeepImports } from './public-internal-deep-imports.js';
|
|
40
|
+
import { publicOutputSchema } from './public-output-schema.js';
|
|
41
|
+
import { publicUnionOutputDiscriminants } from './public-union-output-discriminants.js';
|
|
42
|
+
import { readIntentFires } from './read-intent-fires.js';
|
|
43
|
+
import { referenceExists } from './reference-exists.js';
|
|
44
|
+
import { resolvedImportBoundary } from './resolved-import-boundary.js';
|
|
45
|
+
import { resourceDeclarations } from './resource-declarations.js';
|
|
46
|
+
import { resourceExists } from './resource-exists.js';
|
|
47
|
+
import { resourceIdGrammar } from './resource-id-grammar.js';
|
|
48
|
+
import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
49
|
+
import { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
50
|
+
import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
51
|
+
import { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
|
|
52
|
+
import { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
|
|
53
|
+
import { validDetourContract } from './valid-detour-contract.js';
|
|
54
|
+
import { validDescribeRefs } from './valid-describe-refs.js';
|
|
55
|
+
import { wardenRulesUseAst } from './warden-rules-use-ast.js';
|
|
56
|
+
import { webhookRouteCollision } from './webhook-route-collision.js';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* All non-`warden-export-symmetry` rule identifiers registered in
|
|
60
|
+
* `wardenRules` / `wardenTopoRules`. Excludes the symmetry rule itself to
|
|
61
|
+
* avoid a self-referential check; the symmetry rule adds its own name back in
|
|
62
|
+
* when comparing against the public barrel.
|
|
63
|
+
*/
|
|
64
|
+
export const registeredRuleNames: readonly string[] = [
|
|
65
|
+
activationOrphan.name,
|
|
66
|
+
circularRefs.name,
|
|
67
|
+
contextNoSurfaceTypes.name,
|
|
68
|
+
contourExists.name,
|
|
69
|
+
crossDeclarations.name,
|
|
70
|
+
deadInternalTrail.name,
|
|
71
|
+
draftFileMarking.name,
|
|
72
|
+
draftVisibleDebt.name,
|
|
73
|
+
errorMappingCompleteness.name,
|
|
74
|
+
exampleValid.name,
|
|
75
|
+
firesDeclarations.name,
|
|
76
|
+
implementationReturnsResult.name,
|
|
77
|
+
incompleteAccessorForStandardOp.name,
|
|
78
|
+
incompleteCrud.name,
|
|
79
|
+
intentPropagation.name,
|
|
80
|
+
layerFieldNameDrift.name,
|
|
81
|
+
missingReconcile.name,
|
|
82
|
+
missingVisibility.name,
|
|
83
|
+
noDevPermitInSource.name,
|
|
84
|
+
noLegacyLayerImports.name,
|
|
85
|
+
noDirectImplementationCall.name,
|
|
86
|
+
noNativeErrorResult.name,
|
|
87
|
+
noSyncResultAssumption.name,
|
|
88
|
+
noThrowInDetourRecover.name,
|
|
89
|
+
noThrowInImplementation.name,
|
|
90
|
+
onReferencesExist.name,
|
|
91
|
+
orphanedSignal.name,
|
|
92
|
+
ownerProjectionParity.name,
|
|
93
|
+
permitGovernance.name,
|
|
94
|
+
preferSchemaInference.name,
|
|
95
|
+
publicInternalDeepImports.name,
|
|
96
|
+
publicOutputSchema.name,
|
|
97
|
+
publicUnionOutputDiscriminants.name,
|
|
98
|
+
readIntentFires.name,
|
|
99
|
+
referenceExists.name,
|
|
100
|
+
resolvedImportBoundary.name,
|
|
101
|
+
resourceDeclarations.name,
|
|
102
|
+
resourceExists.name,
|
|
103
|
+
resourceIdGrammar.name,
|
|
104
|
+
scheduledDestroyIntent.name,
|
|
105
|
+
signalGraphCoaching.name,
|
|
106
|
+
staticResourceAccessorPreference.name,
|
|
107
|
+
unmaterializedActivationSource.name,
|
|
108
|
+
unreachableDetourShadowing.name,
|
|
109
|
+
validDetourContract.name,
|
|
110
|
+
validDescribeRefs.name,
|
|
111
|
+
wardenRulesUseAst.name,
|
|
112
|
+
webhookRouteCollision.name,
|
|
113
|
+
];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './ast.js';
|
|
2
|
+
import { isTestFile } from './scan.js';
|
|
3
|
+
import type { WardenImportResolution } from '../resolve.js';
|
|
4
|
+
import type {
|
|
5
|
+
ProjectAwareWardenRule,
|
|
6
|
+
ProjectContext,
|
|
7
|
+
WardenDiagnostic,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const RULE_NAME = 'resolved-import-boundary';
|
|
11
|
+
|
|
12
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
13
|
+
|
|
14
|
+
const isLocalPathImport = (importSource: string): boolean =>
|
|
15
|
+
importSource.startsWith('.') || importSource.startsWith('/');
|
|
16
|
+
|
|
17
|
+
const isFixtureOrMigrationFile = (filePath: string): boolean => {
|
|
18
|
+
const normalized = normalizePath(filePath);
|
|
19
|
+
return /(?:^|\/)(?:__fixtures__|fixtures?|migrations?)(?:\/|$)/.test(
|
|
20
|
+
normalized
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isAllowlistedFile = (filePath: string): boolean =>
|
|
25
|
+
isTestFile(filePath) || isFixtureOrMigrationFile(filePath);
|
|
26
|
+
|
|
27
|
+
const resolutionLabel = (resolution: {
|
|
28
|
+
readonly importSource: string;
|
|
29
|
+
readonly packageName?: string | undefined;
|
|
30
|
+
}): string => resolution.packageName ?? resolution.importSource;
|
|
31
|
+
|
|
32
|
+
const publicSurfaceMessage = (resolution: {
|
|
33
|
+
readonly importSource: string;
|
|
34
|
+
readonly packageName?: string | undefined;
|
|
35
|
+
}): string =>
|
|
36
|
+
`Import "${resolution.importSource}" is not exported by ${resolutionLabel(
|
|
37
|
+
resolution
|
|
38
|
+
)}. Import the package root or an exported subpath instead.`;
|
|
39
|
+
|
|
40
|
+
const localPathBoundaryMessage = (resolution: {
|
|
41
|
+
readonly importSource: string;
|
|
42
|
+
readonly packageName?: string | undefined;
|
|
43
|
+
}): string =>
|
|
44
|
+
`Local import "${resolution.importSource}" crosses into ${resolutionLabel(
|
|
45
|
+
resolution
|
|
46
|
+
)}. Import the target package public surface instead.`;
|
|
47
|
+
|
|
48
|
+
const internalTargetMessage = (resolution: {
|
|
49
|
+
readonly importSource: string;
|
|
50
|
+
readonly packageName?: string | undefined;
|
|
51
|
+
}): string =>
|
|
52
|
+
`Import "${resolution.importSource}" targets internal/private files in ${resolutionLabel(
|
|
53
|
+
resolution
|
|
54
|
+
)}. Import the target package public surface instead.`;
|
|
55
|
+
|
|
56
|
+
const diagnosticForResolution = (
|
|
57
|
+
filePath: string,
|
|
58
|
+
resolution: WardenImportResolution
|
|
59
|
+
): WardenDiagnostic | null => {
|
|
60
|
+
if (!resolution.crossesPackageBoundary) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (resolution.isInternalTarget) {
|
|
65
|
+
return {
|
|
66
|
+
filePath,
|
|
67
|
+
line: resolution.line,
|
|
68
|
+
message: internalTargetMessage(resolution),
|
|
69
|
+
rule: RULE_NAME,
|
|
70
|
+
severity: 'error',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isLocalPathImport(resolution.importSource)) {
|
|
75
|
+
return {
|
|
76
|
+
filePath,
|
|
77
|
+
line: resolution.line,
|
|
78
|
+
message: localPathBoundaryMessage(resolution),
|
|
79
|
+
rule: RULE_NAME,
|
|
80
|
+
severity: 'error',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (resolution.errorKind === 'package-path-not-exported') {
|
|
85
|
+
return {
|
|
86
|
+
filePath,
|
|
87
|
+
line: resolution.line,
|
|
88
|
+
message: publicSurfaceMessage(resolution),
|
|
89
|
+
rule: RULE_NAME,
|
|
90
|
+
severity: 'error',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
resolution.errorKind &&
|
|
96
|
+
resolution.errorKind !== 'builtin' &&
|
|
97
|
+
resolution.errorKind !== 'ignored'
|
|
98
|
+
) {
|
|
99
|
+
return {
|
|
100
|
+
filePath,
|
|
101
|
+
line: resolution.line,
|
|
102
|
+
message: publicSurfaceMessage(resolution),
|
|
103
|
+
rule: RULE_NAME,
|
|
104
|
+
severity: 'error',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const importResolutionsForFile = (context: ProjectContext, filePath: string) =>
|
|
112
|
+
context.importResolutionsByFile?.get(filePath) ?? [];
|
|
113
|
+
|
|
114
|
+
export const resolvedImportBoundary: ProjectAwareWardenRule = {
|
|
115
|
+
check(): readonly WardenDiagnostic[] {
|
|
116
|
+
return [];
|
|
117
|
+
},
|
|
118
|
+
checkWithContext(
|
|
119
|
+
sourceCode: string,
|
|
120
|
+
filePath: string,
|
|
121
|
+
context: ProjectContext
|
|
122
|
+
): readonly WardenDiagnostic[] {
|
|
123
|
+
if (isAllowlistedFile(filePath)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = splitSourceLines(sourceCode);
|
|
128
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
129
|
+
|
|
130
|
+
for (const resolution of importResolutionsForFile(context, filePath)) {
|
|
131
|
+
if (hasIgnoreCommentOnLine(lines, resolution.line)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const diagnostic = diagnosticForResolution(filePath, resolution);
|
|
135
|
+
if (diagnostic) {
|
|
136
|
+
diagnostics.push(diagnostic);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return diagnostics;
|
|
141
|
+
},
|
|
142
|
+
description:
|
|
143
|
+
'Ensure cross-package imports resolve through package-owned public exports.',
|
|
144
|
+
name: RULE_NAME,
|
|
145
|
+
severity: 'error',
|
|
146
|
+
};
|