@ontrails/warden 1.0.0-beta.18 → 1.0.0-beta.19
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 +79 -0
- package/README.md +12 -30
- package/bin/warden.ts +29 -1
- package/package.json +9 -8
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +238 -60
- package/src/command.ts +26 -0
- package/src/drift.ts +1 -1
- package/src/fix.ts +120 -0
- package/src/formatters.ts +14 -2
- package/src/guide.ts +11 -0
- package/src/index.ts +31 -1
- package/src/rules/ast.ts +84 -25
- package/src/rules/circular-refs.ts +1 -1
- package/src/rules/{cross-declarations.ts → composes-declarations.ts} +198 -89
- package/src/rules/context-no-surface-types.ts +4 -4
- package/src/rules/contour-exists.ts +1 -1
- package/src/rules/dead-internal-trail.ts +22 -9
- package/src/rules/fires-declarations.ts +3 -3
- package/src/rules/implementation-returns-result.ts +269 -76
- package/src/rules/index.ts +51 -3
- package/src/rules/intent-propagation.ts +6 -6
- package/src/rules/metadata.ts +117 -12
- package/src/rules/missing-visibility.ts +14 -14
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-direct-implementation-call.ts +2 -2
- package/src/rules/no-legacy-layer-imports.ts +19 -1
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-sync-result-assumption.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +2 -3
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +1 -1
- package/src/rules/reference-exists.ts +1 -1
- package/src/rules/registry-names.ts +28 -2
- package/src/rules/resolved-import-boundary.ts +2 -2
- package/src/rules/resource-declarations.ts +4 -4
- package/src/rules/resource-exists.ts +1 -1
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +39 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +87 -5
- package/src/rules/valid-detour-contract.ts +1 -1
- package/src/rules/warden-export-symmetry.ts +1 -1
- package/src/rules/warden-rules-use-ast.ts +2 -2
- package/src/trails/activation-orphan.trail.ts +4 -1
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/dead-internal-trail.trail.ts +4 -4
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/index.ts +12 -1
- package/src/trails/intent-propagation.trail.ts +3 -3
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-visibility.trail.ts +2 -2
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-direct-implementation-call.trail.ts +2 -2
- package/src/trails/no-legacy-layer-imports.trail.ts +6 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/public-internal-deep-imports.trail.ts +1 -1
- package/src/trails/resolved-import-boundary.trail.ts +4 -4
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +2 -2
- package/src/trails/schema.ts +32 -6
- package/src/trails/signal-graph-coaching.trail.ts +4 -1
- package/src/trails/unmaterialized-activation-source.trail.ts +4 -1
- package/src/trails/valid-detour-contract.trail.ts +1 -1
- 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/wrap-rule.ts +5 -3
- package/src/trails/cross-declarations.trail.ts +0 -22
package/src/rules/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { activationOrphan } from './activation-orphan.js';
|
|
|
2
2
|
import { circularRefs } from './circular-refs.js';
|
|
3
3
|
import { contourExists } from './contour-exists.js';
|
|
4
4
|
import { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
5
|
-
import {
|
|
5
|
+
import { composesDeclarations } from './composes-declarations.js';
|
|
6
6
|
import { deadInternalTrail } from './dead-internal-trail.js';
|
|
7
7
|
import { draftFileMarking } from './draft-file-marking.js';
|
|
8
8
|
import { draftVisibleDebt } from './draft-visible-debt.js';
|
|
@@ -17,12 +17,15 @@ import { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
|
17
17
|
import { missingVisibility } from './missing-visibility.js';
|
|
18
18
|
import { missingReconcile } from './missing-reconcile.js';
|
|
19
19
|
import { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
20
|
+
import { noDestructuredCompose } from './no-destructured-compose.js';
|
|
20
21
|
import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
|
|
21
22
|
import { noDirectImplementationCall } from './no-direct-implementation-call.js';
|
|
22
23
|
import { noNativeErrorResult } from './no-native-error-result.js';
|
|
24
|
+
import { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
|
|
23
25
|
import { noSyncResultAssumption } from './no-sync-result-assumption.js';
|
|
24
26
|
import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
|
|
25
27
|
import { noThrowInImplementation } from './no-throw-in-implementation.js';
|
|
28
|
+
import { noTopLevelSurface } from './no-top-level-surface.js';
|
|
26
29
|
import { onReferencesExist } from './on-references-exist.js';
|
|
27
30
|
import { orphanedSignal } from './orphaned-signal.js';
|
|
28
31
|
import { ownerProjectionParity } from './owner-projection-parity.js';
|
|
@@ -37,9 +40,21 @@ import { resolvedImportBoundary } from './resolved-import-boundary.js';
|
|
|
37
40
|
import { resourceDeclarations } from './resource-declarations.js';
|
|
38
41
|
import { resourceExists } from './resource-exists.js';
|
|
39
42
|
import { resourceIdGrammar } from './resource-id-grammar.js';
|
|
43
|
+
import { resourceMockCoverage } from './resource-mock-coverage.js';
|
|
40
44
|
import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
41
45
|
import { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
42
46
|
import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
47
|
+
import {
|
|
48
|
+
forkWithoutPreservedBlaze,
|
|
49
|
+
markerSchemaUnsupported,
|
|
50
|
+
versionPinnedCompose,
|
|
51
|
+
} from './trail-versioning-source.js';
|
|
52
|
+
import {
|
|
53
|
+
deprecationWithoutGuidance,
|
|
54
|
+
pendingForce,
|
|
55
|
+
versionGap,
|
|
56
|
+
versionWithoutExamples,
|
|
57
|
+
} from './trail-versioning-topo.js';
|
|
43
58
|
import type { TopoAwareWardenRule, WardenRule } from './types.js';
|
|
44
59
|
import { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
|
|
45
60
|
import { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
|
|
@@ -50,6 +65,11 @@ import { wardenRulesUseAst } from './warden-rules-use-ast.js';
|
|
|
50
65
|
import { webhookRouteCollision } from './webhook-route-collision.js';
|
|
51
66
|
|
|
52
67
|
export type {
|
|
68
|
+
WardenFix,
|
|
69
|
+
WardenFixCapability,
|
|
70
|
+
WardenFixClass,
|
|
71
|
+
WardenFixEdit,
|
|
72
|
+
WardenFixSafety,
|
|
53
73
|
WardenGuidance,
|
|
54
74
|
WardenGuidanceLink,
|
|
55
75
|
WardenRuleLifecycle,
|
|
@@ -70,6 +90,8 @@ export {
|
|
|
70
90
|
builtinWardenRuleMetadata,
|
|
71
91
|
getWardenRuleMetadata,
|
|
72
92
|
listWardenRuleMetadata,
|
|
93
|
+
wardenFixClasses,
|
|
94
|
+
wardenFixSafeties,
|
|
73
95
|
wardenRuleConcerns,
|
|
74
96
|
wardenRuleLifecycleStates,
|
|
75
97
|
wardenRuleScopes,
|
|
@@ -82,7 +104,7 @@ export { noThrowInImplementation } from './no-throw-in-implementation.js';
|
|
|
82
104
|
export { circularRefs } from './circular-refs.js';
|
|
83
105
|
export { contourExists } from './contour-exists.js';
|
|
84
106
|
export { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
85
|
-
export {
|
|
107
|
+
export { composesDeclarations } from './composes-declarations.js';
|
|
86
108
|
export { deadInternalTrail } from './dead-internal-trail.js';
|
|
87
109
|
export { draftFileMarking } from './draft-file-marking.js';
|
|
88
110
|
export { draftVisibleDebt } from './draft-visible-debt.js';
|
|
@@ -97,12 +119,15 @@ export { missingVisibility } from './missing-visibility.js';
|
|
|
97
119
|
export { missingReconcile } from './missing-reconcile.js';
|
|
98
120
|
export { onReferencesExist } from './on-references-exist.js';
|
|
99
121
|
export { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
122
|
+
export { noDestructuredCompose } from './no-destructured-compose.js';
|
|
100
123
|
export { noLegacyLayerImports } from './no-legacy-layer-imports.js';
|
|
101
124
|
export { noDirectImplementationCall } from './no-direct-implementation-call.js';
|
|
102
125
|
export { noNativeErrorResult } from './no-native-error-result.js';
|
|
126
|
+
export { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
|
|
103
127
|
export { noSyncResultAssumption } from './no-sync-result-assumption.js';
|
|
104
128
|
export { implementationReturnsResult } from './implementation-returns-result.js';
|
|
105
129
|
export { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
|
|
130
|
+
export { noTopLevelSurface } from './no-top-level-surface.js';
|
|
106
131
|
export { orphanedSignal } from './orphaned-signal.js';
|
|
107
132
|
export { ownerProjectionParity } from './owner-projection-parity.js';
|
|
108
133
|
export { permitGovernance } from './permit-governance.js';
|
|
@@ -116,9 +141,21 @@ export { resolvedImportBoundary } from './resolved-import-boundary.js';
|
|
|
116
141
|
export { resourceDeclarations } from './resource-declarations.js';
|
|
117
142
|
export { resourceExists } from './resource-exists.js';
|
|
118
143
|
export { resourceIdGrammar } from './resource-id-grammar.js';
|
|
144
|
+
export { resourceMockCoverage } from './resource-mock-coverage.js';
|
|
119
145
|
export { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
120
146
|
export { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
121
147
|
export { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
148
|
+
export {
|
|
149
|
+
forkWithoutPreservedBlaze,
|
|
150
|
+
markerSchemaUnsupported,
|
|
151
|
+
versionPinnedCompose,
|
|
152
|
+
} from './trail-versioning-source.js';
|
|
153
|
+
export {
|
|
154
|
+
deprecationWithoutGuidance,
|
|
155
|
+
pendingForce,
|
|
156
|
+
versionGap,
|
|
157
|
+
versionWithoutExamples,
|
|
158
|
+
} from './trail-versioning-topo.js';
|
|
122
159
|
export { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
|
|
123
160
|
export { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
|
|
124
161
|
export { validDetourContract } from './valid-detour-contract.js';
|
|
@@ -134,7 +171,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
134
171
|
[circularRefs.name, circularRefs],
|
|
135
172
|
[contourExists.name, contourExists],
|
|
136
173
|
[contextNoSurfaceTypes.name, contextNoSurfaceTypes],
|
|
137
|
-
[
|
|
174
|
+
[composesDeclarations.name, composesDeclarations],
|
|
138
175
|
[deadInternalTrail.name, deadInternalTrail],
|
|
139
176
|
[draftFileMarking.name, draftFileMarking],
|
|
140
177
|
[draftVisibleDebt.name, draftVisibleDebt],
|
|
@@ -156,19 +193,26 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
156
193
|
[resolvedImportBoundary.name, resolvedImportBoundary],
|
|
157
194
|
[resourceIdGrammar.name, resourceIdGrammar],
|
|
158
195
|
[resourceExists.name, resourceExists],
|
|
196
|
+
[resourceMockCoverage.name, resourceMockCoverage],
|
|
159
197
|
[preferSchemaInference.name, preferSchemaInference],
|
|
160
198
|
[staticResourceAccessorPreference.name, staticResourceAccessorPreference],
|
|
161
199
|
[validDescribeRefs.name, validDescribeRefs],
|
|
162
200
|
[noDevPermitInSource.name, noDevPermitInSource],
|
|
201
|
+
[noDestructuredCompose.name, noDestructuredCompose],
|
|
163
202
|
[noDirectImplementationCall.name, noDirectImplementationCall],
|
|
164
203
|
[noLegacyLayerImports.name, noLegacyLayerImports],
|
|
165
204
|
[noNativeErrorResult.name, noNativeErrorResult],
|
|
205
|
+
[noRedundantResultErrorWrap.name, noRedundantResultErrorWrap],
|
|
166
206
|
[noSyncResultAssumption.name, noSyncResultAssumption],
|
|
167
207
|
[implementationReturnsResult.name, implementationReturnsResult],
|
|
168
208
|
[noThrowInDetourRecover.name, noThrowInDetourRecover],
|
|
209
|
+
[noTopLevelSurface.name, noTopLevelSurface],
|
|
169
210
|
[unreachableDetourShadowing.name, unreachableDetourShadowing],
|
|
170
211
|
[wardenExportSymmetry.name, wardenExportSymmetry],
|
|
171
212
|
[wardenRulesUseAst.name, wardenRulesUseAst],
|
|
213
|
+
[forkWithoutPreservedBlaze.name, forkWithoutPreservedBlaze],
|
|
214
|
+
[markerSchemaUnsupported.name, markerSchemaUnsupported],
|
|
215
|
+
[versionPinnedCompose.name, versionPinnedCompose],
|
|
172
216
|
]);
|
|
173
217
|
|
|
174
218
|
/**
|
|
@@ -195,5 +239,9 @@ export const wardenTopoRules: ReadonlyMap<string, TopoAwareWardenRule> =
|
|
|
195
239
|
[signalGraphCoaching.name, signalGraphCoaching],
|
|
196
240
|
[unmaterializedActivationSource.name, unmaterializedActivationSource],
|
|
197
241
|
[validDetourContract.name, validDetourContract],
|
|
242
|
+
[deprecationWithoutGuidance.name, deprecationWithoutGuidance],
|
|
243
|
+
[pendingForce.name, pendingForce],
|
|
244
|
+
[versionGap.name, versionGap],
|
|
245
|
+
[versionWithoutExamples.name, versionWithoutExamples],
|
|
198
246
|
[webhookRouteCollision.name, webhookRouteCollision],
|
|
199
247
|
]);
|
|
@@ -2,7 +2,7 @@ import type { Intent } from '@ontrails/core';
|
|
|
2
2
|
import {
|
|
3
3
|
collectNamedTrailIds,
|
|
4
4
|
collectTrailIntentsById,
|
|
5
|
-
|
|
5
|
+
extractDefinitionComposeTargetIds,
|
|
6
6
|
findTrailDefinitions,
|
|
7
7
|
offsetToLine,
|
|
8
8
|
parse,
|
|
@@ -24,12 +24,12 @@ const buildIntentPropagationDiagnostic = (
|
|
|
24
24
|
): WardenDiagnostic => ({
|
|
25
25
|
filePath,
|
|
26
26
|
line,
|
|
27
|
-
message: `Trail "${trailId}" declares intent: 'read' but
|
|
27
|
+
message: `Trail "${trailId}" declares intent: 'read' but composes "${targetTrailId}" with intent: '${targetIntent}'. Read trails must not compose write or destroy side effects.`,
|
|
28
28
|
rule: 'intent-propagation',
|
|
29
29
|
severity: 'warn',
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const buildDiagnosticsForComposeTargets = (
|
|
33
33
|
trailId: string,
|
|
34
34
|
targetTrailIds: readonly string[],
|
|
35
35
|
filePath: string,
|
|
@@ -64,9 +64,9 @@ const buildDiagnosticsForTrail = (
|
|
|
64
64
|
return [];
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
return
|
|
67
|
+
return buildDiagnosticsForComposeTargets(
|
|
68
68
|
def.id,
|
|
69
|
-
|
|
69
|
+
extractDefinitionComposeTargetIds(def.config, sourceCode, namedTrailIds),
|
|
70
70
|
filePath,
|
|
71
71
|
offsetToLine(sourceCode, def.start),
|
|
72
72
|
trailIntentsById
|
|
@@ -121,7 +121,7 @@ export const intentPropagation: ProjectAwareWardenRule = {
|
|
|
121
121
|
return checkIntentPropagation(ast, sourceCode, filePath, trailIntentsById);
|
|
122
122
|
},
|
|
123
123
|
description:
|
|
124
|
-
"Warn when a trail declaring intent: 'read'
|
|
124
|
+
"Warn when a trail declaring intent: 'read' composes a trail whose normalized intent is write or destroy.",
|
|
125
125
|
name: 'intent-propagation',
|
|
126
126
|
severity: 'warn',
|
|
127
127
|
};
|
package/src/rules/metadata.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
WardenFixClass,
|
|
3
|
+
WardenFixSafety,
|
|
2
4
|
WardenRule,
|
|
3
5
|
WardenRuleConcern,
|
|
4
6
|
WardenRuleLifecycleState,
|
|
@@ -41,6 +43,15 @@ export const wardenRuleLifecycleStates = [
|
|
|
41
43
|
'deprecated',
|
|
42
44
|
] as const satisfies readonly WardenRuleLifecycleState[];
|
|
43
45
|
|
|
46
|
+
export const wardenFixClasses = [
|
|
47
|
+
'term-rewrite',
|
|
48
|
+
] as const satisfies readonly WardenFixClass[];
|
|
49
|
+
|
|
50
|
+
export const wardenFixSafeties = [
|
|
51
|
+
'review',
|
|
52
|
+
'safe',
|
|
53
|
+
] as const satisfies readonly WardenFixSafety[];
|
|
54
|
+
|
|
44
55
|
type BuiltinWardenRuleMetadataInput = Omit<
|
|
45
56
|
WardenRuleMetadata,
|
|
46
57
|
'concern' | 'depth'
|
|
@@ -57,25 +68,31 @@ const depthByTier = {
|
|
|
57
68
|
|
|
58
69
|
const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
|
|
59
70
|
'activation-orphan': 'signals',
|
|
71
|
+
'composes-declarations': 'composition',
|
|
60
72
|
'context-no-surface-types': 'composition',
|
|
61
|
-
'cross-declarations': 'composition',
|
|
62
73
|
'dead-internal-trail': 'composition',
|
|
74
|
+
'deprecation-without-guidance': 'lifecycle',
|
|
63
75
|
'draft-file-marking': 'lifecycle',
|
|
64
76
|
'draft-visible-debt': 'lifecycle',
|
|
65
77
|
'error-mapping-completeness': 'results',
|
|
66
78
|
'fires-declarations': 'signals',
|
|
79
|
+
'fork-without-preserved-blaze': 'lifecycle',
|
|
67
80
|
'implementation-returns-result': 'results',
|
|
68
81
|
'intent-propagation': 'composition',
|
|
82
|
+
'marker-schema-unsupported': 'lifecycle',
|
|
69
83
|
'missing-reconcile': 'resources',
|
|
70
84
|
'missing-visibility': 'composition',
|
|
85
|
+
'no-destructured-compose': 'composition',
|
|
71
86
|
'no-dev-permit-in-source': 'permits',
|
|
72
87
|
'no-direct-implementation-call': 'composition',
|
|
73
88
|
'no-native-error-result': 'results',
|
|
89
|
+
'no-redundant-result-error-wrap': 'results',
|
|
74
90
|
'no-sync-result-assumption': 'results',
|
|
75
91
|
'no-throw-in-detour-recover': 'results',
|
|
76
92
|
'no-throw-in-implementation': 'results',
|
|
77
93
|
'on-references-exist': 'signals',
|
|
78
94
|
'orphaned-signal': 'signals',
|
|
95
|
+
'pending-force': 'lifecycle',
|
|
79
96
|
'permit-governance': 'permits',
|
|
80
97
|
'public-output-schema': 'results',
|
|
81
98
|
'read-intent-fires': 'signals',
|
|
@@ -83,11 +100,15 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
|
|
|
83
100
|
'resource-declarations': 'resources',
|
|
84
101
|
'resource-exists': 'resources',
|
|
85
102
|
'resource-id-grammar': 'resources',
|
|
103
|
+
'resource-mock-coverage': 'resources',
|
|
86
104
|
'scheduled-destroy-intent': 'lifecycle',
|
|
87
105
|
'signal-graph-coaching': 'signals',
|
|
88
106
|
'static-resource-accessor-preference': 'resources',
|
|
89
107
|
'unmaterialized-activation-source': 'lifecycle',
|
|
90
108
|
'valid-detour-contract': 'results',
|
|
109
|
+
'version-gap': 'lifecycle',
|
|
110
|
+
'version-pinned-compose': 'composition',
|
|
111
|
+
'version-without-examples': 'lifecycle',
|
|
91
112
|
'webhook-route-collision': 'composition',
|
|
92
113
|
};
|
|
93
114
|
|
|
@@ -128,6 +149,11 @@ const builtinWardenRuleMetadataInput = {
|
|
|
128
149
|
invariant: 'Contour reference graphs must be acyclic.',
|
|
129
150
|
tier: 'project-static',
|
|
130
151
|
},
|
|
152
|
+
'composes-declarations': {
|
|
153
|
+
...durableExternal,
|
|
154
|
+
invariant: 'Declared composes stay aligned with ctx.compose() usage.',
|
|
155
|
+
tier: 'source-static',
|
|
156
|
+
},
|
|
131
157
|
'context-no-surface-types': {
|
|
132
158
|
...durableExternal,
|
|
133
159
|
invariant: 'Trail logic stays surface-agnostic.',
|
|
@@ -138,16 +164,17 @@ const builtinWardenRuleMetadataInput = {
|
|
|
138
164
|
invariant: 'Declared contour references resolve to known contours.',
|
|
139
165
|
tier: 'project-static',
|
|
140
166
|
},
|
|
141
|
-
'cross-declarations': {
|
|
142
|
-
...durableExternal,
|
|
143
|
-
invariant: 'Declared crosses stay aligned with ctx.cross() usage.',
|
|
144
|
-
tier: 'source-static',
|
|
145
|
-
},
|
|
146
167
|
'dead-internal-trail': {
|
|
147
168
|
...durableExternal,
|
|
148
|
-
invariant: 'Internal trails should be reachable through declared
|
|
169
|
+
invariant: 'Internal trails should be reachable through declared composes.',
|
|
149
170
|
tier: 'project-static',
|
|
150
171
|
},
|
|
172
|
+
'deprecation-without-guidance': {
|
|
173
|
+
...durableExternal,
|
|
174
|
+
invariant:
|
|
175
|
+
'Deprecated trail version entries carry successor, migration, or note guidance.',
|
|
176
|
+
tier: 'topo-aware',
|
|
177
|
+
},
|
|
151
178
|
'draft-file-marking': {
|
|
152
179
|
...durableExternal,
|
|
153
180
|
invariant: 'Draft-authored state is visibly marked in filenames.',
|
|
@@ -181,9 +208,14 @@ const builtinWardenRuleMetadataInput = {
|
|
|
181
208
|
invariant: 'Declared fires stay aligned with signal firing usage.',
|
|
182
209
|
tier: 'source-static',
|
|
183
210
|
},
|
|
211
|
+
'fork-without-preserved-blaze': {
|
|
212
|
+
...durableExternal,
|
|
213
|
+
invariant: 'Fork version entries preserve their historical blaze.',
|
|
214
|
+
tier: 'source-static',
|
|
215
|
+
},
|
|
184
216
|
'implementation-returns-result': {
|
|
185
217
|
...durableExternal,
|
|
186
|
-
invariant: '
|
|
218
|
+
invariant: 'Blazes return Result values.',
|
|
187
219
|
tier: 'source-static',
|
|
188
220
|
},
|
|
189
221
|
'incomplete-accessor-for-standard-op': {
|
|
@@ -198,7 +230,7 @@ const builtinWardenRuleMetadataInput = {
|
|
|
198
230
|
},
|
|
199
231
|
'intent-propagation': {
|
|
200
232
|
...durableExternal,
|
|
201
|
-
invariant: 'Composite trail intent cannot be safer than
|
|
233
|
+
invariant: 'Composite trail intent cannot be safer than composed trails.',
|
|
202
234
|
tier: 'project-static',
|
|
203
235
|
},
|
|
204
236
|
'layer-field-name-drift': {
|
|
@@ -207,6 +239,12 @@ const builtinWardenRuleMetadataInput = {
|
|
|
207
239
|
'Layer input field reserved names are shared across surface projections.',
|
|
208
240
|
tier: 'source-static',
|
|
209
241
|
},
|
|
242
|
+
'marker-schema-unsupported': {
|
|
243
|
+
...durableExternal,
|
|
244
|
+
invariant:
|
|
245
|
+
'Versioned schemas stay inside the supported marker projection subset.',
|
|
246
|
+
tier: 'source-static',
|
|
247
|
+
},
|
|
210
248
|
'missing-reconcile': {
|
|
211
249
|
...durableExternal,
|
|
212
250
|
invariant: 'Versioned CRUD store tables provide reconcile coverage.',
|
|
@@ -217,6 +255,12 @@ const builtinWardenRuleMetadataInput = {
|
|
|
217
255
|
invariant: 'Composition-only trails declare internal visibility.',
|
|
218
256
|
tier: 'project-static',
|
|
219
257
|
},
|
|
258
|
+
'no-destructured-compose': {
|
|
259
|
+
...durableExternal,
|
|
260
|
+
invariant:
|
|
261
|
+
'Trail blazes compose through ctx.compose() directly instead of destructuring compose from the context.',
|
|
262
|
+
tier: 'source-static',
|
|
263
|
+
},
|
|
220
264
|
'no-dev-permit-in-source': {
|
|
221
265
|
...durableExternal,
|
|
222
266
|
invariant:
|
|
@@ -225,10 +269,11 @@ const builtinWardenRuleMetadataInput = {
|
|
|
225
269
|
},
|
|
226
270
|
'no-direct-implementation-call': {
|
|
227
271
|
...durableExternal,
|
|
228
|
-
invariant: 'Application code composes trails through ctx.
|
|
272
|
+
invariant: 'Application code composes trails through ctx.compose().',
|
|
229
273
|
tier: 'source-static',
|
|
230
274
|
},
|
|
231
275
|
'no-legacy-layer-imports': {
|
|
276
|
+
fix: { class: 'term-rewrite', safety: 'review' },
|
|
232
277
|
invariant:
|
|
233
278
|
'Legacy layer exports removed across TRL-475/TRL-476 (authLayer, autoIterateLayer, dateShortcutsLayer) do not reappear in committed source.',
|
|
234
279
|
lifecycle: {
|
|
@@ -244,6 +289,12 @@ const builtinWardenRuleMetadataInput = {
|
|
|
244
289
|
invariant: 'Result error boundaries carry specific TrailsError subclasses.',
|
|
245
290
|
tier: 'source-static',
|
|
246
291
|
},
|
|
292
|
+
'no-redundant-result-error-wrap': {
|
|
293
|
+
...durableExternal,
|
|
294
|
+
invariant:
|
|
295
|
+
'Result error pass-throughs preserve the original Result boundary.',
|
|
296
|
+
tier: 'source-static',
|
|
297
|
+
},
|
|
247
298
|
'no-sync-result-assumption': {
|
|
248
299
|
...durableExternal,
|
|
249
300
|
invariant:
|
|
@@ -265,9 +316,24 @@ const builtinWardenRuleMetadataInput = {
|
|
|
265
316
|
'Use detours for recoverable runtime strategies instead of throwing inside the blaze.',
|
|
266
317
|
],
|
|
267
318
|
summary:
|
|
268
|
-
'Convert thrown
|
|
319
|
+
'Convert thrown failures in blazes into explicit Result.err() outcomes.',
|
|
269
320
|
},
|
|
270
|
-
invariant: '
|
|
321
|
+
invariant: 'Blazes return Result.err() instead of throwing.',
|
|
322
|
+
tier: 'source-static',
|
|
323
|
+
},
|
|
324
|
+
'no-top-level-surface': {
|
|
325
|
+
...durableExternal,
|
|
326
|
+
guidance: {
|
|
327
|
+
docs: [{ label: 'Architecture', path: 'docs/architecture.md' }],
|
|
328
|
+
relatedRules: ['context-no-surface-types'],
|
|
329
|
+
steps: [
|
|
330
|
+
'Keep the topo-export module focused on exporting `topo(...)` as `default`, `graph`, or `app`.',
|
|
331
|
+
'Move `surface(...)`, `connectStdio(...)`, server start, or `.listen(...)` calls into a separate entry or bin module.',
|
|
332
|
+
],
|
|
333
|
+
summary:
|
|
334
|
+
'Keep topo entry modules side-effect-free for survey, guide, compile, and lock generation.',
|
|
335
|
+
},
|
|
336
|
+
invariant: 'Topo export modules do not open surfaces at module top level.',
|
|
271
337
|
tier: 'source-static',
|
|
272
338
|
},
|
|
273
339
|
'on-references-exist': {
|
|
@@ -287,6 +353,12 @@ const builtinWardenRuleMetadataInput = {
|
|
|
287
353
|
scope: 'internal',
|
|
288
354
|
tier: 'source-static',
|
|
289
355
|
},
|
|
356
|
+
'pending-force': {
|
|
357
|
+
...durableExternal,
|
|
358
|
+
invariant:
|
|
359
|
+
'Forced topo break audit events do not remain pending indefinitely.',
|
|
360
|
+
tier: 'topo-aware',
|
|
361
|
+
},
|
|
290
362
|
'permit-governance': {
|
|
291
363
|
...durableExternal,
|
|
292
364
|
guidance: {
|
|
@@ -395,6 +467,22 @@ const builtinWardenRuleMetadataInput = {
|
|
|
395
467
|
invariant: 'Resource identifiers stay out of the scope separator grammar.',
|
|
396
468
|
tier: 'source-static',
|
|
397
469
|
},
|
|
470
|
+
'resource-mock-coverage': {
|
|
471
|
+
...durableExternal,
|
|
472
|
+
guidance: {
|
|
473
|
+
docs: [trailContractDocs],
|
|
474
|
+
relatedRules: ['resource-declarations', 'resource-exists'],
|
|
475
|
+
steps: [
|
|
476
|
+
'Add a mock() factory so testAll(app) can provision the resource without production configuration.',
|
|
477
|
+
'If the resource genuinely cannot be mocked, declare unmockable: { reason } to record that intent.',
|
|
478
|
+
],
|
|
479
|
+
summary:
|
|
480
|
+
'Make each resource declare a test mock or an explicit unmockable reason.',
|
|
481
|
+
},
|
|
482
|
+
invariant:
|
|
483
|
+
'Resource definitions declare a mock factory or an explicit unmockable reason.',
|
|
484
|
+
tier: 'source-static',
|
|
485
|
+
},
|
|
398
486
|
'scheduled-destroy-intent': {
|
|
399
487
|
...durableExternal,
|
|
400
488
|
invariant:
|
|
@@ -448,6 +536,23 @@ const builtinWardenRuleMetadataInput = {
|
|
|
448
536
|
'Runtime detour contracts use error constructors and recover functions.',
|
|
449
537
|
tier: 'topo-aware',
|
|
450
538
|
},
|
|
539
|
+
'version-gap': {
|
|
540
|
+
...durableExternal,
|
|
541
|
+
invariant:
|
|
542
|
+
'Trail version coverage remains contiguous through the current version.',
|
|
543
|
+
tier: 'topo-aware',
|
|
544
|
+
},
|
|
545
|
+
'version-pinned-compose': {
|
|
546
|
+
...durableExternal,
|
|
547
|
+
invariant:
|
|
548
|
+
'Version-pinned ctx.compose() calls stay visible migration debt.',
|
|
549
|
+
tier: 'source-static',
|
|
550
|
+
},
|
|
551
|
+
'version-without-examples': {
|
|
552
|
+
...durableExternal,
|
|
553
|
+
invariant: 'Live historical version entries include examples.',
|
|
554
|
+
tier: 'topo-aware',
|
|
555
|
+
},
|
|
451
556
|
'warden-export-symmetry': {
|
|
452
557
|
...durableRepoLocal,
|
|
453
558
|
invariant: 'The Warden package exports trail wrappers, not raw rules.',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectComposeTargetTrailIds, parse } from './ast.js';
|
|
2
2
|
import { isTestFile } from './scan.js';
|
|
3
3
|
import {
|
|
4
4
|
findTrailLikeSpecs,
|
|
@@ -28,13 +28,13 @@ const trailVisibility = (spec: TrailLikeSpec): 'internal' | 'public' => {
|
|
|
28
28
|
return hasLegacyMetaInternal(spec) ? 'internal' : 'public';
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
31
|
+
const hasRequiredComposeInput = (spec: TrailLikeSpec): boolean => {
|
|
32
|
+
const composeInput = spec.properties.get('composeInput');
|
|
33
|
+
if (!composeInput) {
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const fields = parseZodObjectShape(
|
|
37
|
+
const fields = parseZodObjectShape(composeInput.value);
|
|
38
38
|
return [...fields.values()].some((field) => field.required);
|
|
39
39
|
};
|
|
40
40
|
|
|
@@ -45,7 +45,7 @@ const buildMissingVisibilityDiagnostic = (
|
|
|
45
45
|
): WardenDiagnostic => ({
|
|
46
46
|
filePath,
|
|
47
47
|
line,
|
|
48
|
-
message: `Trail "${trailId}" is
|
|
48
|
+
message: `Trail "${trailId}" is composed elsewhere and declares required composeInput fields, but it is still public. Consider visibility: 'internal' so surfaces do not expose a trail that only works through ctx.compose().`,
|
|
49
49
|
rule: 'missing-visibility',
|
|
50
50
|
severity: 'warn',
|
|
51
51
|
});
|
|
@@ -53,7 +53,7 @@ const buildMissingVisibilityDiagnostic = (
|
|
|
53
53
|
const checkMissingVisibility = (
|
|
54
54
|
sourceCode: string,
|
|
55
55
|
filePath: string,
|
|
56
|
-
|
|
56
|
+
composedTrailIds: ReadonlySet<string>
|
|
57
57
|
): readonly WardenDiagnostic[] => {
|
|
58
58
|
if (isTestFile(filePath)) {
|
|
59
59
|
return [];
|
|
@@ -65,8 +65,8 @@ const checkMissingVisibility = (
|
|
|
65
65
|
if (
|
|
66
66
|
spec.kind !== 'trail' ||
|
|
67
67
|
trailVisibility(spec) === 'internal' ||
|
|
68
|
-
!
|
|
69
|
-
!
|
|
68
|
+
!composedTrailIds.has(spec.id) ||
|
|
69
|
+
!hasRequiredComposeInput(spec)
|
|
70
70
|
) {
|
|
71
71
|
continue;
|
|
72
72
|
}
|
|
@@ -85,7 +85,7 @@ export const missingVisibility: ProjectAwareWardenRule = {
|
|
|
85
85
|
return checkMissingVisibility(
|
|
86
86
|
sourceCode,
|
|
87
87
|
filePath,
|
|
88
|
-
ast ?
|
|
88
|
+
ast ? collectComposeTargetTrailIds(ast, sourceCode) : new Set<string>()
|
|
89
89
|
);
|
|
90
90
|
},
|
|
91
91
|
checkWithContext(
|
|
@@ -94,17 +94,17 @@ export const missingVisibility: ProjectAwareWardenRule = {
|
|
|
94
94
|
context: ProjectContext
|
|
95
95
|
): readonly WardenDiagnostic[] {
|
|
96
96
|
const ast = parse(filePath, sourceCode);
|
|
97
|
-
const
|
|
98
|
-
?
|
|
97
|
+
const localComposeTargetTrailIds = ast
|
|
98
|
+
? collectComposeTargetTrailIds(ast, sourceCode)
|
|
99
99
|
: new Set<string>();
|
|
100
100
|
return checkMissingVisibility(
|
|
101
101
|
sourceCode,
|
|
102
102
|
filePath,
|
|
103
|
-
context.
|
|
103
|
+
context.composeTargetTrailIds ?? localComposeTargetTrailIds
|
|
104
104
|
);
|
|
105
105
|
},
|
|
106
106
|
description:
|
|
107
|
-
'Coach when a
|
|
107
|
+
'Coach when a composed trail still looks composition-only because it declares required composeInput but remains public.',
|
|
108
108
|
name: 'missing-visibility',
|
|
109
109
|
severity: 'warn',
|
|
110
110
|
};
|