@ontrails/warden 1.0.0-beta.3 → 1.0.0-beta.32
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 +674 -9
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +28 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +137 -0
- package/src/cli.ts +1451 -104
- package/src/command.ts +966 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +122 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +217 -14
- package/src/project-context.ts +192 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +4008 -86
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +837 -0
- package/src/rules/context-no-surface-types.ts +78 -15
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/duplicate-public-contract.ts +91 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +735 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +581 -0
- package/src/rules/index.ts +235 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +755 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +194 -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 +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1135 -98
- 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 +371 -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 +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +562 -0
- package/src/rules/public-internal-deep-imports.ts +457 -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 +155 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +693 -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 +649 -0
- package/src/rules/surface-facet-coherence.ts +362 -0
- package/src/rules/trail-fork-coaching.ts +616 -0
- package/src/rules/trail-versioning-source.ts +1072 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +297 -8
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1114 -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/cli-command-route-coherence.trail.ts +47 -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/dead-public-trail.trail.ts +31 -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/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -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 +83 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -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 +198 -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/trail-fork-coaching.trail.ts +42 -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 +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +214 -0
- package/src/workspaces.ts +199 -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 -277
- 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 -128
- 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 -215
- 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,77 @@
|
|
|
1
|
+
import { Result, resource, signal, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { signalGraphCoaching } from '../rules/signal-graph-coaching.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const unusedSignal = signal('invoice.unused', {
|
|
8
|
+
payload: z.object({ invoiceId: z.string() }),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const producedSignal = signal('invoice.created', {
|
|
12
|
+
payload: z.object({ invoiceId: z.string() }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const producerTrail = trail('invoice.create', {
|
|
16
|
+
blaze: async (_input, ctx) => {
|
|
17
|
+
await ctx.fire?.(producedSignal, { invoiceId: 'inv_1' });
|
|
18
|
+
return Result.ok({ invoiceId: 'inv_1' });
|
|
19
|
+
},
|
|
20
|
+
fires: [producedSignal],
|
|
21
|
+
input: z.object({}),
|
|
22
|
+
output: z.object({ invoiceId: z.string() }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const resourceSignal = signal('store:invoice.created', {
|
|
26
|
+
payload: z.object({ invoiceId: z.string() }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const invoiceStore = resource('store', {
|
|
30
|
+
create: () => Result.ok({ ok: true }),
|
|
31
|
+
signals: [resourceSignal],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const signalGraphCoachingTrail = wrapTopoRule({
|
|
35
|
+
examples: [
|
|
36
|
+
{
|
|
37
|
+
expected: {
|
|
38
|
+
diagnostics: [
|
|
39
|
+
{
|
|
40
|
+
filePath: '<topo>',
|
|
41
|
+
line: 1,
|
|
42
|
+
message:
|
|
43
|
+
'Signal "invoice.created" is produced by producer trail "invoice.create" but has no consumer trails. Add an on: consumer if the signal is meant to drive reactive work, or remove the unused fires:/producer declaration.',
|
|
44
|
+
rule: 'signal-graph-coaching',
|
|
45
|
+
severity: 'warn',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
filePath: '<topo>',
|
|
49
|
+
line: 1,
|
|
50
|
+
message:
|
|
51
|
+
'Signal "invoice.unused" is declared in the topo but has no producer trails, producer resources, or consumer trails. Add fires:/on: edges, attach producer metadata, or remove the unused signal contract.',
|
|
52
|
+
rule: 'signal-graph-coaching',
|
|
53
|
+
severity: 'warn',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
filePath: '<topo>',
|
|
57
|
+
line: 1,
|
|
58
|
+
message:
|
|
59
|
+
'Signal "store:invoice.created" is produced by producer resource "store" but has no consumer trails. Add an on: consumer if the signal is meant to drive reactive work, or remove the unused fires:/producer declaration.',
|
|
60
|
+
rule: 'signal-graph-coaching',
|
|
61
|
+
severity: 'warn',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
input: {
|
|
66
|
+
topo: topo('trl-447-signal-graph-coaching', {
|
|
67
|
+
invoiceStore,
|
|
68
|
+
producedSignal,
|
|
69
|
+
producerTrail,
|
|
70
|
+
unusedSignal,
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
name: 'Declared and produced signals without consumers get coaching',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
rule: signalGraphCoaching,
|
|
77
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { staticResourceAccessorPreference } from '../rules/static-resource-accessor-preference.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const staticResourceAccessorPreferenceTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
sourceCode: `const db = resource("db.main", {
|
|
11
|
+
create: () => Result.ok({ source: "factory" }),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
trail("entity.show", {
|
|
15
|
+
resources: [db],
|
|
16
|
+
blaze: async (_input, ctx) => {
|
|
17
|
+
return Result.ok(db.from(ctx));
|
|
18
|
+
}
|
|
19
|
+
})`,
|
|
20
|
+
},
|
|
21
|
+
name: 'Static resource helper access',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
rule: staticResourceAccessorPreference,
|
|
25
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { surfaceFacetCoherence } from '../rules/surface-facet-coherence.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const surfaceFacetCoherenceTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'mcp-options.ts',
|
|
10
|
+
sourceCode: `export const facets = {
|
|
11
|
+
inspect: {
|
|
12
|
+
description: "Inspect topo state.",
|
|
13
|
+
trails: ["survey", "survey.brief"],
|
|
14
|
+
},
|
|
15
|
+
governance: {
|
|
16
|
+
description: "Run diagnostics.",
|
|
17
|
+
trails: ["warden", "doctor"],
|
|
18
|
+
},
|
|
19
|
+
};`,
|
|
20
|
+
},
|
|
21
|
+
name: 'Reviewable trailhead map',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
rule: surfaceFacetCoherence,
|
|
25
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { trailForkCoaching } from '../rules/trail-fork-coaching.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const trailForkCoachingTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: {
|
|
8
|
+
diagnostics: [
|
|
9
|
+
{
|
|
10
|
+
filePath: 'users.ts',
|
|
11
|
+
line: 9,
|
|
12
|
+
message:
|
|
13
|
+
'Trail "users.manage" branches on input.action ("create", "delete"). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a trailhead that preserves member identity.',
|
|
14
|
+
rule: 'trail-fork-coaching',
|
|
15
|
+
severity: 'warn',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
input: {
|
|
20
|
+
filePath: 'users.ts',
|
|
21
|
+
sourceCode: `import { Result, trail } from "@ontrails/core";
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
|
|
24
|
+
export const usersManage = trail("users.manage", {
|
|
25
|
+
input: z.object({
|
|
26
|
+
action: z.enum(["create", "delete"]),
|
|
27
|
+
}),
|
|
28
|
+
blaze: async (input) => {
|
|
29
|
+
switch (input.action) {
|
|
30
|
+
case "create":
|
|
31
|
+
return Result.ok({ created: true });
|
|
32
|
+
case "delete":
|
|
33
|
+
return Result.ok({ deleted: true });
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});`,
|
|
37
|
+
},
|
|
38
|
+
name: 'Possible trail fork hidden behind action',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
rule: trailForkCoaching,
|
|
42
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Result, schedule, signal, topo, trail, webhook } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { unmaterializedActivationSource } from '../rules/unmaterialized-activation-source.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const invoicePaidWebhook = webhook('webhook.invoice.paid', {
|
|
8
|
+
parse: z.object({ invoiceId: z.string() }),
|
|
9
|
+
path: '/webhooks/invoice/paid',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const webhookConsumer = trail('invoice.audit-webhook', {
|
|
13
|
+
blaze: () => Result.ok({ ok: true }),
|
|
14
|
+
input: z.object({ invoiceId: z.string() }),
|
|
15
|
+
on: [invoicePaidWebhook],
|
|
16
|
+
output: z.object({ ok: z.boolean() }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const invoiceCreated = signal('invoice.created', {
|
|
20
|
+
from: ['invoice.create'],
|
|
21
|
+
payload: z.object({ invoiceId: z.string() }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const signalProducer = trail('invoice.create', {
|
|
25
|
+
blaze: async (_input, ctx) => {
|
|
26
|
+
await ctx.fire?.(invoiceCreated, { invoiceId: 'inv_1' });
|
|
27
|
+
return Result.ok({ invoiceId: 'inv_1' });
|
|
28
|
+
},
|
|
29
|
+
fires: [invoiceCreated],
|
|
30
|
+
input: z.object({}),
|
|
31
|
+
output: z.object({ invoiceId: z.string() }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const signalConsumer = trail('invoice.index', {
|
|
35
|
+
blaze: () => Result.ok({ ok: true }),
|
|
36
|
+
input: z.object({ invoiceId: z.string() }),
|
|
37
|
+
on: [invoiceCreated],
|
|
38
|
+
output: z.object({ ok: z.boolean() }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const scheduleConsumer = trail('invoice.reconcile', {
|
|
42
|
+
blaze: () => Result.ok({ ok: true }),
|
|
43
|
+
input: z.object({}),
|
|
44
|
+
on: [schedule('schedule.invoice.reconcile', { cron: '0 * * * *' })],
|
|
45
|
+
output: z.object({ ok: z.boolean() }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const unmaterializedActivationSourceTrail = wrapTopoRule({
|
|
49
|
+
examples: [
|
|
50
|
+
{
|
|
51
|
+
expected: { diagnostics: [] },
|
|
52
|
+
input: {
|
|
53
|
+
topo: topo('trl-461-webhook-materialized', { webhookConsumer }),
|
|
54
|
+
},
|
|
55
|
+
name: 'Webhook activation sources are materialized by HTTP',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
expected: { diagnostics: [] },
|
|
59
|
+
input: {
|
|
60
|
+
topo: topo('trl-496-materialized-sources', {
|
|
61
|
+
invoiceCreated,
|
|
62
|
+
scheduleConsumer,
|
|
63
|
+
signalConsumer,
|
|
64
|
+
signalProducer,
|
|
65
|
+
webhookConsumer,
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
name: 'Signal, schedule, and webhook activation sources are materialized',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
rule: unmaterializedActivationSource,
|
|
72
|
+
});
|
|
@@ -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,64 @@
|
|
|
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
|
+
expected: {
|
|
45
|
+
diagnostics: [
|
|
46
|
+
{
|
|
47
|
+
filePath: fakeRulePath,
|
|
48
|
+
line: 1,
|
|
49
|
+
message:
|
|
50
|
+
'warden-rules-use-ast: raw AST node-field cast should use typed helpers from ./ast.js (callee -> getNodeCallee). Raw node-field casts drift from the curated @ontrails/warden/ast guard surface.',
|
|
51
|
+
rule: 'warden-rules-use-ast',
|
|
52
|
+
severity: 'warn',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
input: {
|
|
57
|
+
filePath: fakeRulePath,
|
|
58
|
+
sourceCode: `export const r = { check() { const callee = (node as unknown as { callee?: AstNode }).callee; return callee ? [] : []; } };\n`,
|
|
59
|
+
},
|
|
60
|
+
name: 'Warns on hand-cast AST node fields',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
rule: wardenRulesUseAst,
|
|
64
|
+
});
|
|
@@ -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
|
+
});
|