@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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 +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- 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 +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- 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 +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- 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-export-example-coverage.ts +553 -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 +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- 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 +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -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/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -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 +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -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/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -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/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -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 +213 -0
- 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 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- 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/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 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- 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 -46
- 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/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/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 -52
- 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 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -75
- 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__/rules.test.ts +0 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { unreachableDetourShadowing } from '../rules/unreachable-detour-shadowing.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const unreachableDetourShadowingTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
sourceCode: `trail("entity.save", {
|
|
11
|
+
detours: [
|
|
12
|
+
{ on: ConflictError, recover: async () => Result.ok({ winner: "specific" }) },
|
|
13
|
+
{ on: TrailsError, recover: async () => Result.ok({ winner: "broad" }) },
|
|
14
|
+
],
|
|
15
|
+
});`,
|
|
16
|
+
},
|
|
17
|
+
name: 'Specific detours can precede broader ones',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
expected: {
|
|
21
|
+
diagnostics: [
|
|
22
|
+
{
|
|
23
|
+
filePath: 'shadowed.ts',
|
|
24
|
+
line: 4,
|
|
25
|
+
message:
|
|
26
|
+
'Trail "entity.save" declares detour on "ConflictError" after earlier detour on "TrailsError". Because "TrailsError" matches "ConflictError" first, the later detour is unreachable.',
|
|
27
|
+
rule: 'unreachable-detour-shadowing',
|
|
28
|
+
severity: 'error',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
input: {
|
|
33
|
+
filePath: 'shadowed.ts',
|
|
34
|
+
sourceCode: `trail("entity.save", {
|
|
35
|
+
detours: [
|
|
36
|
+
{ on: TrailsError, recover: async () => Result.ok({ winner: "broad" }) },
|
|
37
|
+
{ on: ConflictError, recover: async () => Result.ok({ winner: "specific" }) },
|
|
38
|
+
],
|
|
39
|
+
});`,
|
|
40
|
+
},
|
|
41
|
+
name: 'Broader detours declared first shadow later specific ones',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
rule: unreachableDetourShadowing,
|
|
45
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { validDescribeRefs } from '../rules/valid-describe-refs.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const validDescribeRefsTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
sourceCode: `const schema = z.object({
|
|
11
|
+
name: z.string().describe("User display name"),
|
|
12
|
+
});`,
|
|
13
|
+
},
|
|
14
|
+
name: 'Describe without @see refs',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
rule: validDescribeRefs,
|
|
18
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ConflictError, Result, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { validDetourContract } from '../rules/valid-detour-contract.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const validTrail = trail('entity.save', {
|
|
8
|
+
blaze: () => Result.ok({ ok: true }),
|
|
9
|
+
detours: [
|
|
10
|
+
{
|
|
11
|
+
on: ConflictError,
|
|
12
|
+
recover: async () => {
|
|
13
|
+
const result = await Promise.resolve(Result.ok({ ok: true }));
|
|
14
|
+
return result;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
input: z.object({}),
|
|
19
|
+
output: z.object({ ok: z.boolean() }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const invalidContractTrail = {
|
|
23
|
+
...validTrail,
|
|
24
|
+
detours: [
|
|
25
|
+
{
|
|
26
|
+
on: 'ConflictError',
|
|
27
|
+
recover: 'not a function',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
} as unknown as typeof validTrail;
|
|
31
|
+
|
|
32
|
+
export const validDetourContractTrail = wrapTopoRule({
|
|
33
|
+
examples: [
|
|
34
|
+
{
|
|
35
|
+
expected: { diagnostics: [] },
|
|
36
|
+
input: {
|
|
37
|
+
topo: topo('trl-380-valid-detour-contract', { validTrail }),
|
|
38
|
+
},
|
|
39
|
+
name: 'Detours with an error constructor and recover function stay clean',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
expected: {
|
|
43
|
+
diagnostics: [
|
|
44
|
+
{
|
|
45
|
+
filePath: '<topo>',
|
|
46
|
+
line: 1,
|
|
47
|
+
message:
|
|
48
|
+
'Trail "entity.save" detour[0] must declare an error constructor in on:. Received ConflictError.',
|
|
49
|
+
rule: 'valid-detour-contract',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
filePath: '<topo>',
|
|
54
|
+
line: 1,
|
|
55
|
+
message:
|
|
56
|
+
'Trail "entity.save" detour[0] must declare a callable recover function. Expected recover: (attempt, ctx) => Promise<Result<...>>; inspect attempt.error for the matched error and return Result.err(...) for unrecoverable cases.',
|
|
57
|
+
rule: 'valid-detour-contract',
|
|
58
|
+
severity: 'error',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
input: {
|
|
63
|
+
topo: topo('trl-380-invalid-detour-contract', {
|
|
64
|
+
invalidContractTrail,
|
|
65
|
+
} as Record<string, unknown>),
|
|
66
|
+
},
|
|
67
|
+
name: 'Malformed detour contracts emit diagnostics',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
rule: validDetourContract,
|
|
71
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Result, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { versionGap } from '../rules/trail-versioning-topo.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const versionedTrail = trail('version.gap.clean', {
|
|
8
|
+
blaze: () => Result.ok({ ok: true }),
|
|
9
|
+
input: z.object({}),
|
|
10
|
+
output: z.object({ ok: z.boolean() }),
|
|
11
|
+
version: 2,
|
|
12
|
+
versions: {
|
|
13
|
+
1: {
|
|
14
|
+
input: z.object({}),
|
|
15
|
+
output: z.object({ ok: z.boolean() }),
|
|
16
|
+
transpose: {
|
|
17
|
+
input: () => ({}),
|
|
18
|
+
output: ({ output }) => output,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const versionGapTrail = wrapTopoRule({
|
|
25
|
+
examples: [
|
|
26
|
+
{
|
|
27
|
+
expected: { diagnostics: [] },
|
|
28
|
+
input: {
|
|
29
|
+
topo: topo('version-gap-clean', { versionedTrail }),
|
|
30
|
+
},
|
|
31
|
+
name: 'Contiguous versions pass',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
rule: versionGap,
|
|
35
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { versionPinnedCompose } from '../rules/trail-versioning-source.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const versionPinnedComposeTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'src/trails/current.ts',
|
|
10
|
+
sourceCode: `
|
|
11
|
+
trail('current.parent', {
|
|
12
|
+
blaze: async (_input, ctx) => {
|
|
13
|
+
await ctx.compose('current.child', {});
|
|
14
|
+
return Result.ok({});
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
`,
|
|
18
|
+
},
|
|
19
|
+
name: 'Current composition has no version-pin warning',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
rule: versionPinnedCompose,
|
|
23
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Result, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { versionWithoutExamples } from '../rules/trail-versioning-topo.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const archivedWithoutExamples = trail('version.examples.archived', {
|
|
8
|
+
blaze: () => Result.ok({ ok: true }),
|
|
9
|
+
input: z.object({}),
|
|
10
|
+
output: z.object({ ok: z.boolean() }),
|
|
11
|
+
version: 2,
|
|
12
|
+
versions: {
|
|
13
|
+
1: {
|
|
14
|
+
input: z.object({}),
|
|
15
|
+
output: z.object({ ok: z.boolean() }),
|
|
16
|
+
status: { state: 'archived' },
|
|
17
|
+
transpose: {
|
|
18
|
+
input: () => ({}),
|
|
19
|
+
output: ({ output }) => output,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const versionWithoutExamplesTrail = wrapTopoRule({
|
|
26
|
+
examples: [
|
|
27
|
+
{
|
|
28
|
+
expected: { diagnostics: [] },
|
|
29
|
+
input: {
|
|
30
|
+
topo: topo('version-without-examples-clean', {
|
|
31
|
+
archivedWithoutExamples,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
name: 'Archived entries are exempt from example warnings',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
rule: versionWithoutExamples,
|
|
38
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { wardenExportSymmetry } from '../rules/warden-export-symmetry.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const wardenExportSymmetryTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'packages/other-pkg/src/index.ts',
|
|
10
|
+
sourceCode: `export { somethingElse } from './other.js';\n`,
|
|
11
|
+
},
|
|
12
|
+
name: 'Ignores files outside the warden barrel',
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
rule: wardenExportSymmetry,
|
|
16
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { wardenRulesUseAst } from '../rules/warden-rules-use-ast.js';
|
|
3
|
+
import { wrapRule } from './wrap-rule.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a filePath inside this package's `src/rules/` directory so the
|
|
7
|
+
* positive example fires the path-anchored scope check. Anchoring via
|
|
8
|
+
* `import.meta.url` keeps the example robust to the cwd under which tests run.
|
|
9
|
+
*/
|
|
10
|
+
const fakeRulePath = fileURLToPath(
|
|
11
|
+
new URL('../rules/fake-rule.ts', import.meta.url)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const wardenRulesUseAstTrail = wrapRule({
|
|
15
|
+
examples: [
|
|
16
|
+
{
|
|
17
|
+
expected: { diagnostics: [] },
|
|
18
|
+
input: {
|
|
19
|
+
filePath: 'packages/other-pkg/src/index.ts',
|
|
20
|
+
sourceCode: `const lines = sourceCode.split('\\n');\n`,
|
|
21
|
+
},
|
|
22
|
+
name: 'Ignores files outside the warden rules directory',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
expected: {
|
|
26
|
+
diagnostics: [
|
|
27
|
+
{
|
|
28
|
+
filePath: fakeRulePath,
|
|
29
|
+
line: 1,
|
|
30
|
+
message:
|
|
31
|
+
'warden-rules-use-ast: sourceCode.split(...) treats source text as a string. Warden rules must inspect the AST via packages/warden/src/rules/ast.ts helpers, not regex-scan raw source text. Use findStringLiterals, findTrailDefinitions, findConfigProperty, or a similar AST walker. Raw-text scanning produces false positives on string literals, template payloads, and docstrings — see TRL-335, ADR-0036.',
|
|
32
|
+
rule: 'warden-rules-use-ast',
|
|
33
|
+
severity: 'error',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
input: {
|
|
38
|
+
filePath: fakeRulePath,
|
|
39
|
+
sourceCode: `export const r = { check(sourceCode: string) { return sourceCode.split('\\n'); } };\n`,
|
|
40
|
+
},
|
|
41
|
+
name: 'Flags sourceCode.split(...) in a rule file',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
rule: wardenRulesUseAst,
|
|
45
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Result, topo, trail, webhook } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { webhookRouteCollision } from '../rules/webhook-route-collision.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const paymentWebhook = webhook('webhook.payment.received', {
|
|
8
|
+
parse: z.object({ paymentId: z.string() }),
|
|
9
|
+
path: '/webhooks/payment',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const paymentReceiver = trail('payment.receive', {
|
|
13
|
+
blaze: () => Result.ok({ ok: true }),
|
|
14
|
+
input: z.object({ paymentId: z.string() }),
|
|
15
|
+
on: [paymentWebhook],
|
|
16
|
+
output: z.object({ ok: z.boolean() }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const directRoute = trail('webhooks.payment', {
|
|
20
|
+
blaze: () => Result.ok({ ok: true }),
|
|
21
|
+
input: z.object({}),
|
|
22
|
+
output: z.object({ ok: z.boolean() }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const webhookRouteCollisionTrail = wrapTopoRule({
|
|
26
|
+
examples: [
|
|
27
|
+
{
|
|
28
|
+
expected: {
|
|
29
|
+
diagnostics: [
|
|
30
|
+
{
|
|
31
|
+
filePath: '<topo>',
|
|
32
|
+
line: 1,
|
|
33
|
+
message:
|
|
34
|
+
'HTTP webhook route collision on POST /webhooks/payment: derived trail route "webhooks.payment", webhook source "webhook.payment.received" on trail "payment.receive". Give each webhook source a distinct method/path pair or move the direct trail route before materializing the HTTP surface.',
|
|
35
|
+
rule: 'webhook-route-collision',
|
|
36
|
+
severity: 'error',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
input: {
|
|
41
|
+
topo: topo('trl-461-webhook-route-collision', {
|
|
42
|
+
directRoute,
|
|
43
|
+
paymentReceiver,
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
name: 'Webhook route colliding with a derived direct HTTP route',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
rule: webhookRouteCollision,
|
|
50
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory that wraps a WardenRule as a trail.
|
|
3
|
+
*
|
|
4
|
+
* Keeps each rule trail file minimal — just the import + examples.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { InternalError, trail, Result } from '@ontrails/core';
|
|
8
|
+
import type { Trail } from '@ontrails/core';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ProjectAwareWardenRule,
|
|
12
|
+
ProjectContext,
|
|
13
|
+
TopoAwareWardenRule,
|
|
14
|
+
WardenRule,
|
|
15
|
+
} from '../rules/types.js';
|
|
16
|
+
import { getWardenRuleMetadata } from '../rules/metadata.js';
|
|
17
|
+
import {
|
|
18
|
+
projectAwareRuleInput,
|
|
19
|
+
ruleInput,
|
|
20
|
+
ruleOutput,
|
|
21
|
+
topoAwareRuleInput,
|
|
22
|
+
} from './schema.js';
|
|
23
|
+
import type {
|
|
24
|
+
ProjectAwareRuleInput,
|
|
25
|
+
RuleInput,
|
|
26
|
+
RuleOutput,
|
|
27
|
+
TopoAwareRuleInput,
|
|
28
|
+
} from './schema.js';
|
|
29
|
+
|
|
30
|
+
interface WrapRuleOptions {
|
|
31
|
+
/** The existing warden rule to wrap. */
|
|
32
|
+
readonly rule: WardenRule;
|
|
33
|
+
/** Trail examples for testing and documentation. */
|
|
34
|
+
readonly examples: Trail<RuleInput, RuleOutput>['examples'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WrapProjectAwareRuleOptions {
|
|
38
|
+
/** The existing project-aware warden rule to wrap. */
|
|
39
|
+
readonly rule: ProjectAwareWardenRule;
|
|
40
|
+
/** Trail examples for testing and documentation. */
|
|
41
|
+
readonly examples: Trail<ProjectAwareRuleInput, RuleOutput>['examples'];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const buildRuleMeta = (rule: WardenRule | TopoAwareWardenRule) => {
|
|
45
|
+
const metadata = getWardenRuleMetadata(rule);
|
|
46
|
+
return {
|
|
47
|
+
category: 'governance',
|
|
48
|
+
...(metadata ? { warden: metadata } : {}),
|
|
49
|
+
severity: rule.severity,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const buildProjectContext = (input: ProjectAwareRuleInput): ProjectContext => ({
|
|
54
|
+
...(input.contourReferencesByName
|
|
55
|
+
? {
|
|
56
|
+
contourReferencesByName: new Map(
|
|
57
|
+
Object.entries(input.contourReferencesByName)
|
|
58
|
+
),
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
...(input.crudTableIds ? { crudTableIds: new Set(input.crudTableIds) } : {}),
|
|
62
|
+
...(input.crudCoverageByEntity
|
|
63
|
+
? {
|
|
64
|
+
crudCoverageByEntity: new Map(
|
|
65
|
+
Object.entries(input.crudCoverageByEntity).map(
|
|
66
|
+
([entityId, operations]) => [
|
|
67
|
+
entityId,
|
|
68
|
+
new Set(operations) as ReadonlySet<string>,
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
: {}),
|
|
74
|
+
...(input.knownContourIds
|
|
75
|
+
? { knownContourIds: new Set(input.knownContourIds) }
|
|
76
|
+
: {}),
|
|
77
|
+
...(input.importResolutionsByFile
|
|
78
|
+
? {
|
|
79
|
+
importResolutionsByFile: new Map(
|
|
80
|
+
Object.entries(input.importResolutionsByFile)
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
: {}),
|
|
84
|
+
...(input.documentedImportResolutionsByFile
|
|
85
|
+
? {
|
|
86
|
+
documentedImportResolutionsByFile: new Map(
|
|
87
|
+
Object.entries(input.documentedImportResolutionsByFile)
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
91
|
+
...(input.publicWorkspaces
|
|
92
|
+
? { publicWorkspaces: new Map(Object.entries(input.publicWorkspaces)) }
|
|
93
|
+
: {}),
|
|
94
|
+
knownTrailIds: input.knownTrailIds
|
|
95
|
+
? new Set(input.knownTrailIds)
|
|
96
|
+
: new Set<string>(),
|
|
97
|
+
...(input.composeTargetTrailIds
|
|
98
|
+
? { composeTargetTrailIds: new Set(input.composeTargetTrailIds) }
|
|
99
|
+
: {}),
|
|
100
|
+
...(input.knownResourceIds
|
|
101
|
+
? { knownResourceIds: new Set(input.knownResourceIds) }
|
|
102
|
+
: {}),
|
|
103
|
+
...(input.knownSignalIds
|
|
104
|
+
? { knownSignalIds: new Set(input.knownSignalIds) }
|
|
105
|
+
: {}),
|
|
106
|
+
...(input.onTargetSignalIds
|
|
107
|
+
? { onTargetSignalIds: new Set(input.onTargetSignalIds) }
|
|
108
|
+
: {}),
|
|
109
|
+
...(input.reconcileTableIds
|
|
110
|
+
? { reconcileTableIds: new Set(input.reconcileTableIds) }
|
|
111
|
+
: {}),
|
|
112
|
+
...(input.trailIntentsById
|
|
113
|
+
? { trailIntentsById: new Map(Object.entries(input.trailIntentsById)) }
|
|
114
|
+
: {}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wrap an existing `WardenRule` as a trail with typed input/output.
|
|
119
|
+
*
|
|
120
|
+
* The trail ID follows the pattern `warden.rule.<rule-name>`.
|
|
121
|
+
*/
|
|
122
|
+
export function wrapRule(
|
|
123
|
+
options: WrapProjectAwareRuleOptions
|
|
124
|
+
): Trail<ProjectAwareRuleInput, RuleOutput>;
|
|
125
|
+
export function wrapRule(
|
|
126
|
+
options: WrapRuleOptions
|
|
127
|
+
): Trail<RuleInput, RuleOutput>;
|
|
128
|
+
export function wrapRule(
|
|
129
|
+
options: WrapRuleOptions | WrapProjectAwareRuleOptions
|
|
130
|
+
): Trail<RuleInput, RuleOutput> | Trail<ProjectAwareRuleInput, RuleOutput> {
|
|
131
|
+
const { rule, examples } = options;
|
|
132
|
+
const isProjectAware = 'checkWithContext' in rule;
|
|
133
|
+
|
|
134
|
+
if (isProjectAware) {
|
|
135
|
+
const projectAwareRule = rule as ProjectAwareWardenRule;
|
|
136
|
+
return trail(`warden.rule.${rule.name}`, {
|
|
137
|
+
blaze: (input: ProjectAwareRuleInput) => {
|
|
138
|
+
const diagnostics = projectAwareRule.checkWithContext(
|
|
139
|
+
input.sourceCode,
|
|
140
|
+
input.filePath,
|
|
141
|
+
buildProjectContext(input)
|
|
142
|
+
);
|
|
143
|
+
return Result.ok({ diagnostics: [...diagnostics] });
|
|
144
|
+
},
|
|
145
|
+
description: rule.description,
|
|
146
|
+
examples: examples as Trail<
|
|
147
|
+
ProjectAwareRuleInput,
|
|
148
|
+
RuleOutput
|
|
149
|
+
>['examples'],
|
|
150
|
+
input: projectAwareRuleInput,
|
|
151
|
+
intent: 'read',
|
|
152
|
+
meta: buildRuleMeta(rule),
|
|
153
|
+
output: ruleOutput,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return trail(`warden.rule.${rule.name}`, {
|
|
158
|
+
blaze: (input: RuleInput) => {
|
|
159
|
+
const diagnostics = rule.check(input.sourceCode, input.filePath);
|
|
160
|
+
return Result.ok({ diagnostics: [...diagnostics] });
|
|
161
|
+
},
|
|
162
|
+
description: rule.description,
|
|
163
|
+
examples: examples as Trail<RuleInput, RuleOutput>['examples'],
|
|
164
|
+
input: ruleInput,
|
|
165
|
+
intent: 'read',
|
|
166
|
+
meta: buildRuleMeta(rule),
|
|
167
|
+
output: ruleOutput,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface WrapTopoRuleOptions {
|
|
172
|
+
/** The existing topo-aware warden rule to wrap. */
|
|
173
|
+
readonly rule: TopoAwareWardenRule;
|
|
174
|
+
/** Trail examples for testing and documentation. */
|
|
175
|
+
readonly examples: Trail<TopoAwareRuleInput, RuleOutput>['examples'];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Wrap an existing `TopoAwareWardenRule` as a trail.
|
|
180
|
+
*
|
|
181
|
+
* Mirrors `wrapRule` for the per-topo dispatch path. Topo-aware rules run
|
|
182
|
+
* once per topo against the compiled runtime graph rather than per file,
|
|
183
|
+
* so the trail accepts the live `Topo` as input.
|
|
184
|
+
*/
|
|
185
|
+
export const wrapTopoRule = (
|
|
186
|
+
options: WrapTopoRuleOptions
|
|
187
|
+
): Trail<TopoAwareRuleInput, RuleOutput> => {
|
|
188
|
+
const { rule, examples } = options;
|
|
189
|
+
return trail(`warden.rule.${rule.name}`, {
|
|
190
|
+
blaze: async (input: TopoAwareRuleInput) => {
|
|
191
|
+
try {
|
|
192
|
+
const diagnostics = await rule.checkTopo(input.topo, {
|
|
193
|
+
graph: input.graph,
|
|
194
|
+
});
|
|
195
|
+
return Result.ok({ diagnostics: [...diagnostics] });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
198
|
+
return Result.err(
|
|
199
|
+
new InternalError(
|
|
200
|
+
`Topo-aware rule "${rule.name}" threw while inspecting topo: ${cause.message}`,
|
|
201
|
+
{ cause }
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
description: rule.description,
|
|
207
|
+
examples,
|
|
208
|
+
input: topoAwareRuleInput,
|
|
209
|
+
intent: 'read',
|
|
210
|
+
meta: buildRuleMeta(rule),
|
|
211
|
+
output: ruleOutput,
|
|
212
|
+
});
|
|
213
|
+
};
|