@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42
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 +837 -13
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +27 -6
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +1570 -105
- package/src/command.ts +986 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +233 -23
- package/src/fix.ts +126 -0
- package/src/formatters.ts +78 -13
- package/src/guide.ts +245 -0
- package/src/index.ts +247 -15
- package/src/project-context.ts +446 -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/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +79 -15
- 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 +155 -0
- package/src/rules/draft-visible-debt.ts +82 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +438 -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 +583 -0
- package/src/rules/index.ts +277 -10
- package/src/rules/intent-propagation.ts +125 -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 +871 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- 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 +204 -0
- package/src/rules/no-sync-result-assumption.ts +1141 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +15 -8
- 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 +149 -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 +573 -0
- package/src/rules/public-internal-deep-imports.ts +456 -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 +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +167 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +633 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +653 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +354 -8
- package/src/rules/unmaterialized-activation-source.ts +85 -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 +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -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/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-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -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-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -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 +89 -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-cli-alias-export.trail.ts +41 -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 +160 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +237 -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-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-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/trailhead-override-divergence.trail.ts +47 -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 +224 -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 -218
- 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 -161
- 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 -15
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -34
- 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/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 -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__/rules.test.ts +0 -227
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/ast.ts +0 -213
- 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 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { publicInternalDeepImports } from '../rules/public-internal-deep-imports.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const publicInternalDeepImportsTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'packages/store/src/trails/example.ts',
|
|
10
|
+
importResolutionsByFile: {
|
|
11
|
+
'packages/store/src/trails/example.ts': [
|
|
12
|
+
{
|
|
13
|
+
crossesPackageBoundary: true,
|
|
14
|
+
importSource: '@ontrails/core/trails',
|
|
15
|
+
importerPath: 'packages/store/src/trails/example.ts',
|
|
16
|
+
isInternalTarget: false,
|
|
17
|
+
line: 1,
|
|
18
|
+
packageName: '@ontrails/core',
|
|
19
|
+
packageRoot: 'packages/core',
|
|
20
|
+
resolvedPath: 'packages/core/src/trails/index.ts',
|
|
21
|
+
usesPublicExport: true,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
knownTrailIds: [],
|
|
26
|
+
publicWorkspaces: {
|
|
27
|
+
'@ontrails/core': {
|
|
28
|
+
hasExports: true,
|
|
29
|
+
name: '@ontrails/core',
|
|
30
|
+
packageJsonPath: 'packages/core/package.json',
|
|
31
|
+
rootDir: 'packages/core',
|
|
32
|
+
},
|
|
33
|
+
'@ontrails/store': {
|
|
34
|
+
hasExports: true,
|
|
35
|
+
name: '@ontrails/store',
|
|
36
|
+
packageJsonPath: 'packages/store/package.json',
|
|
37
|
+
rootDir: 'packages/store',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
sourceCode: `import { deriveTrail } from '@ontrails/core/trails';\n`,
|
|
41
|
+
},
|
|
42
|
+
name: 'Allows exported package subpaths',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
expected: {
|
|
46
|
+
diagnostics: [
|
|
47
|
+
{
|
|
48
|
+
filePath: 'packages/store/src/trails/example.ts',
|
|
49
|
+
line: 1,
|
|
50
|
+
message:
|
|
51
|
+
'@ontrails specifier "@ontrails/core/src/internal/hidden" is not exported by @ontrails/core. Use the package root or an exported subpath; if the API is missing, add an owner export follow-up instead of importing internals.',
|
|
52
|
+
rule: 'public-internal-deep-imports',
|
|
53
|
+
severity: 'error',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
input: {
|
|
58
|
+
filePath: 'packages/store/src/trails/example.ts',
|
|
59
|
+
importResolutionsByFile: {
|
|
60
|
+
'packages/store/src/trails/example.ts': [
|
|
61
|
+
{
|
|
62
|
+
crossesPackageBoundary: true,
|
|
63
|
+
errorKind: 'package-path-not-exported',
|
|
64
|
+
importSource: '@ontrails/core/src/internal/hidden',
|
|
65
|
+
importerPath: 'packages/store/src/trails/example.ts',
|
|
66
|
+
isInternalTarget: false,
|
|
67
|
+
line: 1,
|
|
68
|
+
packageName: '@ontrails/core',
|
|
69
|
+
usesPublicExport: false,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
knownTrailIds: [],
|
|
74
|
+
publicWorkspaces: {
|
|
75
|
+
'@ontrails/core': {
|
|
76
|
+
hasExports: true,
|
|
77
|
+
name: '@ontrails/core',
|
|
78
|
+
packageJsonPath: 'packages/core/package.json',
|
|
79
|
+
rootDir: 'packages/core',
|
|
80
|
+
},
|
|
81
|
+
'@ontrails/store': {
|
|
82
|
+
hasExports: true,
|
|
83
|
+
name: '@ontrails/store',
|
|
84
|
+
packageJsonPath: 'packages/store/package.json',
|
|
85
|
+
rootDir: 'packages/store',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
sourceCode: `import { hidden } from '@ontrails/core/src/internal/hidden';\n`,
|
|
89
|
+
},
|
|
90
|
+
name: 'Flags compose-package imports into owner internals',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
rule: publicInternalDeepImports,
|
|
94
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Result, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { publicOutputSchema } from '../rules/public-output-schema.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const cleanTrail = trail('report.read', {
|
|
8
|
+
implementation: () => Result.ok({ ok: true }),
|
|
9
|
+
input: z.object({}),
|
|
10
|
+
output: z.object({ ok: z.boolean() }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const cleanTopo = topo('public-output-schema-clean', {
|
|
14
|
+
cleanTrail,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const missingOutputTrail = trail('report.missing', {
|
|
18
|
+
implementation: () => Result.ok({ ok: true }),
|
|
19
|
+
input: z.object({}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const missingOutputTopo = topo('public-output-schema-missing', {
|
|
23
|
+
missingOutputTrail,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const publicOutputSchemaTrail = wrapTopoRule({
|
|
27
|
+
examples: [
|
|
28
|
+
{
|
|
29
|
+
expected: { diagnostics: [] },
|
|
30
|
+
input: {
|
|
31
|
+
topo: cleanTopo,
|
|
32
|
+
},
|
|
33
|
+
name: 'Public surface trails declare output schemas',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
expected: {
|
|
37
|
+
diagnostics: [
|
|
38
|
+
{
|
|
39
|
+
filePath: '<topo>',
|
|
40
|
+
line: 1,
|
|
41
|
+
message:
|
|
42
|
+
'Trail "report.missing" is visible to public MCP/HTTP surface projection but does not declare an output schema. Add an explicit output schema, or mark the trail visibility as internal if it is composition-only.',
|
|
43
|
+
rule: 'public-output-schema',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
input: {
|
|
49
|
+
topo: missingOutputTopo,
|
|
50
|
+
},
|
|
51
|
+
name: 'Public surface trails without output schemas are flagged',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
rule: publicOutputSchema,
|
|
55
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Result, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { publicUnionOutputDiscriminants } from '../rules/public-union-output-discriminants.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
const cleanOutput = z.discriminatedUnion('kind', [
|
|
8
|
+
z.object({ kind: z.literal('message'), message: z.string() }),
|
|
9
|
+
z.object({ count: z.number(), kind: z.literal('count') }),
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const cleanTrail = trail('report.read', {
|
|
13
|
+
implementation: () => Result.ok({ kind: 'message' as const, message: 'ok' }),
|
|
14
|
+
input: z.object({}),
|
|
15
|
+
output: cleanOutput,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const cleanTopo = topo('public-union-output-discriminants-clean', {
|
|
19
|
+
cleanTrail,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const publicUnionOutputDiscriminantsTrail = wrapTopoRule({
|
|
23
|
+
examples: [
|
|
24
|
+
{
|
|
25
|
+
expected: { diagnostics: [] },
|
|
26
|
+
input: {
|
|
27
|
+
topo: cleanTopo,
|
|
28
|
+
},
|
|
29
|
+
name: 'Public output object unions expose a literal discriminator',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
rule: publicUnionOutputDiscriminants,
|
|
33
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readIntentFires } from '../rules/read-intent-fires.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const readIntentFiresTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
sourceCode: `const entityLoaded = signal('entity.loaded', { payload: z.object({}) });
|
|
11
|
+
trail('entity.read', {
|
|
12
|
+
intent: 'read',
|
|
13
|
+
implementation: async () => Result.ok({}),
|
|
14
|
+
});`,
|
|
15
|
+
},
|
|
16
|
+
name: 'Read trails without fires stay quiet',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
rule: readIntentFires,
|
|
20
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { referenceExists } from '../rules/reference-exists.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const referenceExistsTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'entities.ts',
|
|
10
|
+
knownEntityIds: ['gist', 'user'],
|
|
11
|
+
knownTrailIds: [],
|
|
12
|
+
sourceCode: `const user = entity("user", {
|
|
13
|
+
id: z.string().uuid(),
|
|
14
|
+
}, { identity: "id" });
|
|
15
|
+
|
|
16
|
+
const gist = entity("gist", {
|
|
17
|
+
id: z.string().uuid(),
|
|
18
|
+
ownerId: user.id(),
|
|
19
|
+
}, { identity: "id" });`,
|
|
20
|
+
},
|
|
21
|
+
name: 'Entity references resolve to known project entities',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
rule: referenceExists,
|
|
25
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolvedImportBoundary } from '../rules/resolved-import-boundary.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const resolvedImportBoundaryTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: {
|
|
8
|
+
diagnostics: [
|
|
9
|
+
{
|
|
10
|
+
filePath: 'packages/app/src/index.ts',
|
|
11
|
+
line: 1,
|
|
12
|
+
message:
|
|
13
|
+
'Import "@fixture/core/internal/secret" is not exported by @fixture/core. Import the package root or an exported subpath instead.',
|
|
14
|
+
rule: 'resolved-import-boundary',
|
|
15
|
+
severity: 'error',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
input: {
|
|
20
|
+
filePath: 'packages/app/src/index.ts',
|
|
21
|
+
importResolutionsByFile: {
|
|
22
|
+
'packages/app/src/index.ts': [
|
|
23
|
+
{
|
|
24
|
+
crossesPackageBoundary: true,
|
|
25
|
+
errorKind: 'package-path-not-exported',
|
|
26
|
+
importSource: '@fixture/core/internal/secret',
|
|
27
|
+
importerPath: 'packages/app/src/index.ts',
|
|
28
|
+
isInternalTarget: false,
|
|
29
|
+
line: 1,
|
|
30
|
+
packageName: '@fixture/core',
|
|
31
|
+
usesPublicExport: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
knownTrailIds: [],
|
|
36
|
+
sourceCode: "import { secret } from '@fixture/core/internal/secret';\n",
|
|
37
|
+
},
|
|
38
|
+
name: 'Compose-package imports must use exported package subpaths',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
expected: {
|
|
42
|
+
diagnostics: [
|
|
43
|
+
{
|
|
44
|
+
filePath: 'packages/app/src/index.ts',
|
|
45
|
+
line: 1,
|
|
46
|
+
message:
|
|
47
|
+
'Local import "../../core/src/public" composes into @fixture/core. Import the target package public surface instead.',
|
|
48
|
+
rule: 'resolved-import-boundary',
|
|
49
|
+
severity: 'error',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
input: {
|
|
54
|
+
filePath: 'packages/app/src/index.ts',
|
|
55
|
+
importResolutionsByFile: {
|
|
56
|
+
'packages/app/src/index.ts': [
|
|
57
|
+
{
|
|
58
|
+
crossesPackageBoundary: true,
|
|
59
|
+
importSource: '../../core/src/public',
|
|
60
|
+
importerPath: 'packages/app/src/index.ts',
|
|
61
|
+
isInternalTarget: false,
|
|
62
|
+
line: 1,
|
|
63
|
+
packageName: '@fixture/core',
|
|
64
|
+
usesPublicExport: false,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
knownTrailIds: [],
|
|
69
|
+
sourceCode: "import { pub } from '../../core/src/public';\n",
|
|
70
|
+
},
|
|
71
|
+
name: 'Relative imports must not compose package boundaries',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
expected: {
|
|
75
|
+
diagnostics: [
|
|
76
|
+
{
|
|
77
|
+
filePath: 'packages/app/src/index.ts',
|
|
78
|
+
line: 1,
|
|
79
|
+
message:
|
|
80
|
+
'Import "../../core/src/internal/secret" targets internal/private files in @fixture/core. Import the target package public surface instead.',
|
|
81
|
+
rule: 'resolved-import-boundary',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
input: {
|
|
87
|
+
filePath: 'packages/app/src/index.ts',
|
|
88
|
+
importResolutionsByFile: {
|
|
89
|
+
'packages/app/src/index.ts': [
|
|
90
|
+
{
|
|
91
|
+
crossesPackageBoundary: true,
|
|
92
|
+
importSource: '../../core/src/internal/secret',
|
|
93
|
+
importerPath: 'packages/app/src/index.ts',
|
|
94
|
+
isInternalTarget: true,
|
|
95
|
+
line: 1,
|
|
96
|
+
packageName: '@fixture/core',
|
|
97
|
+
usesPublicExport: false,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
knownTrailIds: [],
|
|
102
|
+
sourceCode:
|
|
103
|
+
"import { secret } from '../../core/src/internal/secret';\n",
|
|
104
|
+
},
|
|
105
|
+
name: 'Compose-package imports must not target internals',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
rule: resolvedImportBoundary,
|
|
109
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resourceDeclarations } from '../rules/resource-declarations.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const resourceDeclarationsTrail = 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
|
+
implementation: async (_input, ctx) => {
|
|
17
|
+
return Result.ok(db.from(ctx));
|
|
18
|
+
}
|
|
19
|
+
})`,
|
|
20
|
+
},
|
|
21
|
+
name: 'Matched resource declarations and usage',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
rule: resourceDeclarations,
|
|
25
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resourceExists } from '../rules/resource-exists.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const resourceExistsTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
knownResourceIds: ['db.main'],
|
|
11
|
+
knownTrailIds: ['entity.show'],
|
|
12
|
+
sourceCode: `const db = resource("db.main", {
|
|
13
|
+
create: () => Result.ok({ source: "factory" }),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
trail("entity.show", {
|
|
17
|
+
resources: [db],
|
|
18
|
+
implementation: async (_input, ctx) => {
|
|
19
|
+
return Result.ok(db.from(ctx));
|
|
20
|
+
}
|
|
21
|
+
})`,
|
|
22
|
+
},
|
|
23
|
+
name: 'Declared resources resolve to known project resources',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
rule: resourceExists,
|
|
27
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resourceIdGrammar } from '../rules/resource-id-grammar.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const resourceIdGrammarTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
sourceCode: `const db = resource("db.main", {
|
|
11
|
+
create: () => Result.ok({}),
|
|
12
|
+
});`,
|
|
13
|
+
},
|
|
14
|
+
name: 'Resource ids stay free of the scope separator',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
expected: {
|
|
18
|
+
diagnostics: [
|
|
19
|
+
{
|
|
20
|
+
filePath: 'invalid.ts',
|
|
21
|
+
line: 1,
|
|
22
|
+
message:
|
|
23
|
+
'Resource "billing:primary" is invalid because resource ids may not contain ":".',
|
|
24
|
+
rule: 'resource-id-grammar',
|
|
25
|
+
severity: 'error',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
input: {
|
|
30
|
+
filePath: 'invalid.ts',
|
|
31
|
+
sourceCode: `const db = resource("billing:primary", {
|
|
32
|
+
create: () => Result.ok({}),
|
|
33
|
+
});`,
|
|
34
|
+
},
|
|
35
|
+
name: 'Colon-separated resource ids are rejected',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
rule: resourceIdGrammar,
|
|
39
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resourceMockCoverage } from '../rules/resource-mock-coverage.js';
|
|
2
|
+
import { wrapRule } from './wrap-rule.js';
|
|
3
|
+
|
|
4
|
+
export const resourceMockCoverageTrail = wrapRule({
|
|
5
|
+
examples: [
|
|
6
|
+
{
|
|
7
|
+
expected: { diagnostics: [] },
|
|
8
|
+
input: {
|
|
9
|
+
filePath: 'clean.ts',
|
|
10
|
+
sourceCode: `const db = resource("db.main", {
|
|
11
|
+
create: () => Result.ok(openDatabase()),
|
|
12
|
+
mock: () => createInMemoryDb(),
|
|
13
|
+
});`,
|
|
14
|
+
},
|
|
15
|
+
name: 'Resource declaring a mock factory passes',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
expected: {
|
|
19
|
+
diagnostics: [
|
|
20
|
+
{
|
|
21
|
+
filePath: 'missing-mock.ts',
|
|
22
|
+
line: 1,
|
|
23
|
+
message:
|
|
24
|
+
'Resource "db.main" declares no mock factory. Add a mock() so testAll(app) runs without configuration, or declare unmockable: { reason } if it intentionally cannot be mocked.',
|
|
25
|
+
rule: 'resource-mock-coverage',
|
|
26
|
+
severity: 'warn',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
input: {
|
|
31
|
+
filePath: 'missing-mock.ts',
|
|
32
|
+
sourceCode: `const db = resource("db.main", {
|
|
33
|
+
create: () => Result.ok(openDatabase()),
|
|
34
|
+
});`,
|
|
35
|
+
},
|
|
36
|
+
name: 'Resource without a mock factory or unmockable reason is flagged',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
rule: resourceMockCoverage,
|
|
40
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run file-scoped warden rule trails against a single source file.
|
|
3
|
+
*
|
|
4
|
+
* Returns a flat array of diagnostics from every source-aware rule. Built-in
|
|
5
|
+
* topo-aware rules are dispatched separately via `runTopoAwareWardenTrails()`
|
|
6
|
+
* so callers that loop files do not duplicate graph-level findings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Intent, Topo } from '@ontrails/core';
|
|
10
|
+
import { run } from '@ontrails/core';
|
|
11
|
+
|
|
12
|
+
import { wardenTopoRules } from '../rules/index.js';
|
|
13
|
+
import type { WardenDiagnostic } from '../rules/types.js';
|
|
14
|
+
import type { WardenImportResolution } from '../resolve.js';
|
|
15
|
+
import type { WardenPublicWorkspace } from '../workspaces.js';
|
|
16
|
+
import type { RuleOutput } from './schema.js';
|
|
17
|
+
import { wardenTopo } from './topo.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run all file-scoped warden rule trails for a given file and collect diagnostics.
|
|
21
|
+
*
|
|
22
|
+
* Each rule trail runs independently. Errors from individual trails are
|
|
23
|
+
* silently skipped so that one broken rule does not block the rest.
|
|
24
|
+
*/
|
|
25
|
+
const appendDiagnostics = (
|
|
26
|
+
target: WardenDiagnostic[],
|
|
27
|
+
diagnostics: readonly WardenDiagnostic[]
|
|
28
|
+
): void => {
|
|
29
|
+
for (const diagnostic of diagnostics) {
|
|
30
|
+
target.push(diagnostic);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type TrailIntentMap = Readonly<Record<string, Intent>>;
|
|
35
|
+
|
|
36
|
+
interface ProjectRuleOptions {
|
|
37
|
+
readonly entityReferencesByName?: Readonly<Record<string, readonly string[]>>;
|
|
38
|
+
readonly composeTargetTrailIds?: readonly string[];
|
|
39
|
+
readonly crudTableIds?: readonly string[];
|
|
40
|
+
readonly crudCoverageByEntity?: Readonly<Record<string, readonly string[]>>;
|
|
41
|
+
readonly knownEntityIds?: readonly string[];
|
|
42
|
+
readonly importResolutionsByFile?: Readonly<
|
|
43
|
+
Record<string, readonly WardenImportResolution[]>
|
|
44
|
+
>;
|
|
45
|
+
readonly documentedImportResolutionsByFile?: Readonly<
|
|
46
|
+
Record<string, readonly WardenImportResolution[]>
|
|
47
|
+
>;
|
|
48
|
+
readonly knownResourceIds?: readonly string[];
|
|
49
|
+
readonly knownSignalIds?: readonly string[];
|
|
50
|
+
readonly knownTrailIds?: readonly string[];
|
|
51
|
+
readonly onTargetSignalIds?: readonly string[];
|
|
52
|
+
readonly publicWorkspaces?: Readonly<Record<string, WardenPublicWorkspace>>;
|
|
53
|
+
readonly reconcileTableIds?: readonly string[];
|
|
54
|
+
readonly trailIntentsById?: TrailIntentMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PROJECT_OPTION_KEYS = [
|
|
58
|
+
'entityReferencesByName',
|
|
59
|
+
'composeTargetTrailIds',
|
|
60
|
+
'crudTableIds',
|
|
61
|
+
'crudCoverageByEntity',
|
|
62
|
+
'knownEntityIds',
|
|
63
|
+
'importResolutionsByFile',
|
|
64
|
+
'documentedImportResolutionsByFile',
|
|
65
|
+
'knownResourceIds',
|
|
66
|
+
'knownSignalIds',
|
|
67
|
+
'knownTrailIds',
|
|
68
|
+
'onTargetSignalIds',
|
|
69
|
+
'publicWorkspaces',
|
|
70
|
+
'reconcileTableIds',
|
|
71
|
+
'trailIntentsById',
|
|
72
|
+
] as const satisfies readonly (keyof ProjectRuleOptions)[];
|
|
73
|
+
|
|
74
|
+
const hasProjectOptions = (options?: ProjectRuleOptions): boolean =>
|
|
75
|
+
Boolean(
|
|
76
|
+
options && PROJECT_OPTION_KEYS.some((key) => options[key] !== undefined)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const collectProjectOptions = (
|
|
80
|
+
options?: ProjectRuleOptions
|
|
81
|
+
): ProjectRuleOptions => {
|
|
82
|
+
if (!options) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Object.fromEntries(
|
|
87
|
+
PROJECT_OPTION_KEYS.flatMap((key) => {
|
|
88
|
+
const value = options[key];
|
|
89
|
+
return value === undefined ? [] : [[key, value] as const];
|
|
90
|
+
})
|
|
91
|
+
) as ProjectRuleOptions;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const buildRuleInput = (
|
|
95
|
+
filePath: string,
|
|
96
|
+
sourceCode: string,
|
|
97
|
+
options?: ProjectRuleOptions
|
|
98
|
+
): {
|
|
99
|
+
readonly filePath: string;
|
|
100
|
+
readonly sourceCode: string;
|
|
101
|
+
} & ProjectRuleOptions => {
|
|
102
|
+
const base = { filePath, sourceCode };
|
|
103
|
+
if (!hasProjectOptions(options)) {
|
|
104
|
+
return base;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { ...base, ...collectProjectOptions(options) };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const topoAwareTrailIds = new Set(
|
|
111
|
+
[...wardenTopoRules.keys()].map((ruleName) => `warden.rule.${ruleName}`)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export const runWardenTrails = async (
|
|
115
|
+
filePath: string,
|
|
116
|
+
sourceCode: string,
|
|
117
|
+
options?: ProjectRuleOptions
|
|
118
|
+
): Promise<readonly WardenDiagnostic[]> => {
|
|
119
|
+
const allDiagnostics: WardenDiagnostic[] = [];
|
|
120
|
+
const input = buildRuleInput(filePath, sourceCode, options);
|
|
121
|
+
|
|
122
|
+
for (const id of wardenTopo.ids()) {
|
|
123
|
+
if (topoAwareTrailIds.has(id)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const result = await run(wardenTopo, id, input);
|
|
127
|
+
if (result.isOk()) {
|
|
128
|
+
appendDiagnostics(
|
|
129
|
+
allDiagnostics,
|
|
130
|
+
(result.value as RuleOutput).diagnostics
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return allDiagnostics;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run the built-in topo-aware warden rule trails once against a resolved topo.
|
|
140
|
+
*
|
|
141
|
+
* Unlike `runWardenTrails()`, which is file-scoped, topo-aware rules inspect
|
|
142
|
+
* the compiled graph and should only be dispatched once per topo.
|
|
143
|
+
*/
|
|
144
|
+
export const runTopoAwareWardenTrails = async (
|
|
145
|
+
topo: Topo
|
|
146
|
+
): Promise<readonly WardenDiagnostic[]> => {
|
|
147
|
+
const allDiagnostics: WardenDiagnostic[] = [];
|
|
148
|
+
|
|
149
|
+
for (const id of topoAwareTrailIds) {
|
|
150
|
+
const result = await run(wardenTopo, id, { topo });
|
|
151
|
+
if (result.isOk()) {
|
|
152
|
+
appendDiagnostics(
|
|
153
|
+
allDiagnostics,
|
|
154
|
+
(result.value as RuleOutput).diagnostics
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return allDiagnostics;
|
|
160
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Result, schedule, topo, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { scheduledDestroyIntent } from '../rules/scheduled-destroy-intent.js';
|
|
5
|
+
import { wrapTopoRule } from './wrap-rule.js';
|
|
6
|
+
|
|
7
|
+
export const scheduledDestroyTrail = trail('billing.purge-expired', {
|
|
8
|
+
implementation: () => Result.ok({ ok: true }),
|
|
9
|
+
input: z.object({}),
|
|
10
|
+
intent: 'destroy',
|
|
11
|
+
on: [
|
|
12
|
+
schedule('schedule.billing.purge-expired', {
|
|
13
|
+
cron: '0 2 * * *',
|
|
14
|
+
}),
|
|
15
|
+
],
|
|
16
|
+
output: z.object({ ok: z.boolean() }),
|
|
17
|
+
permit: { scopes: ['billing:purge'] },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const scheduledWriteTrail = trail('billing.reconcile', {
|
|
21
|
+
implementation: () => Result.ok({ ok: true }),
|
|
22
|
+
input: z.object({}),
|
|
23
|
+
on: [schedule('schedule.billing.reconcile', { cron: '0 * * * *' })],
|
|
24
|
+
output: z.object({ ok: z.boolean() }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const scheduledDestroyIntentTrail = wrapTopoRule({
|
|
28
|
+
examples: [
|
|
29
|
+
{
|
|
30
|
+
expected: {
|
|
31
|
+
diagnostics: [
|
|
32
|
+
{
|
|
33
|
+
filePath: '<topo>',
|
|
34
|
+
line: 1,
|
|
35
|
+
message:
|
|
36
|
+
'Trail "billing.purge-expired" declares intent: \'destroy\' and is activated by schedule source "schedule.billing.purge-expired". Scheduled destroy work should make cadence, permit scope, idempotency, and recovery explicit before it runs unattended.',
|
|
37
|
+
rule: 'scheduled-destroy-intent',
|
|
38
|
+
severity: 'warn',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
input: {
|
|
43
|
+
topo: topo('trl-457-scheduled-destroy', { scheduledDestroyTrail }),
|
|
44
|
+
},
|
|
45
|
+
name: 'Scheduled destroy trails emit coaching',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
expected: { diagnostics: [] },
|
|
49
|
+
input: {
|
|
50
|
+
topo: topo('trl-457-scheduled-write', { scheduledWriteTrail }),
|
|
51
|
+
},
|
|
52
|
+
name: 'Scheduled write trails do not emit destroy coaching',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
rule: scheduledDestroyIntent,
|
|
56
|
+
});
|