@ontrails/warden 1.0.0-beta.23 → 1.0.0-beta.29
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 +113 -0
- package/README.md +35 -1
- package/package.json +10 -9
- package/src/ast.ts +109 -0
- package/src/cli.ts +44 -4
- package/src/command.ts +24 -34
- package/src/drift.ts +24 -8
- package/src/index.ts +9 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +8 -7
- package/src/rules/ast.ts +837 -6
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +210 -77
- package/src/rules/context-no-surface-types.ts +15 -12
- package/src/rules/contour-exists.ts +7 -7
- package/src/rules/dead-internal-trail.ts +11 -4
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/duplicate-public-contract.ts +91 -0
- package/src/rules/error-mapping-completeness.ts +5 -3
- package/src/rules/example-valid.ts +6 -12
- package/src/rules/fires-declarations.ts +41 -64
- package/src/rules/implementation-returns-result.ts +208 -76
- package/src/rules/incomplete-crud.ts +20 -19
- package/src/rules/index.ts +15 -0
- package/src/rules/layer-field-name-drift.ts +12 -6
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +102 -1
- package/src/rules/no-destructured-compose.ts +16 -14
- package/src/rules/no-native-error-result.ts +15 -8
- package/src/rules/no-redundant-result-error-wrap.ts +82 -29
- package/src/rules/no-sync-result-assumption.ts +70 -68
- package/src/rules/no-top-level-surface.ts +46 -64
- package/src/rules/on-references-exist.ts +1 -1
- package/src/rules/owner-projection-parity.ts +10 -13
- package/src/rules/public-export-example-coverage.ts +29 -20
- package/src/rules/public-internal-deep-imports.ts +2 -2
- package/src/rules/read-intent-fires.ts +8 -8
- package/src/rules/registry-names.ts +10 -0
- package/src/rules/resource-declarations.ts +20 -31
- package/src/rules/resource-exists.ts +2 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/static-resource-accessor-preference.ts +26 -34
- package/src/rules/surface-facet-coherence.ts +21 -29
- package/src/rules/trail-fork-coaching.ts +616 -0
- package/src/rules/trail-versioning-source.ts +56 -78
- package/src/rules/types.ts +2 -0
- package/src/rules/unreachable-detour-shadowing.ts +16 -21
- package/src/rules/valid-describe-refs.ts +14 -12
- package/src/rules/warden-export-symmetry.ts +42 -35
- package/src/rules/warden-rules-use-ast.ts +168 -50
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/error-mapping-completeness.trail.ts +1 -0
- package/src/trails/index.ts +5 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/schema.ts +4 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/warden-rules-use-ast.trail.ts +19 -0
- package/src/trails/wrap-rule.ts +1 -0
package/src/rules/index.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { activationOrphan } from './activation-orphan.js';
|
|
2
|
+
import { cliCommandRouteCoherence } from './cli-command-route-coherence.js';
|
|
2
3
|
import { circularRefs } from './circular-refs.js';
|
|
3
4
|
import { contourExists } from './contour-exists.js';
|
|
4
5
|
import { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
5
6
|
import { composesDeclarations } from './composes-declarations.js';
|
|
6
7
|
import { deadInternalTrail } from './dead-internal-trail.js';
|
|
8
|
+
import { deadPublicTrail } from './dead-public-trail.js';
|
|
7
9
|
import { draftFileMarking } from './draft-file-marking.js';
|
|
8
10
|
import { draftVisibleDebt } from './draft-visible-debt.js';
|
|
11
|
+
import { duplicatePublicContract } from './duplicate-public-contract.js';
|
|
9
12
|
import { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
10
13
|
import { exampleValid } from './example-valid.js';
|
|
11
14
|
import { firesDeclarations } from './fires-declarations.js';
|
|
@@ -14,6 +17,7 @@ import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-stand
|
|
|
14
17
|
import { incompleteCrud } from './incomplete-crud.js';
|
|
15
18
|
import { intentPropagation } from './intent-propagation.js';
|
|
16
19
|
import { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
20
|
+
import { libraryProjectionCoherence } from './library-projection-coherence.js';
|
|
17
21
|
import { missingVisibility } from './missing-visibility.js';
|
|
18
22
|
import { missingReconcile } from './missing-reconcile.js';
|
|
19
23
|
import { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
@@ -47,6 +51,7 @@ import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
|
47
51
|
import { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
48
52
|
import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
49
53
|
import { surfaceFacetCoherence } from './surface-facet-coherence.js';
|
|
54
|
+
import { trailForkCoaching } from './trail-fork-coaching.js';
|
|
50
55
|
import {
|
|
51
56
|
forkWithoutPreservedBlaze,
|
|
52
57
|
markerSchemaUnsupported,
|
|
@@ -103,20 +108,24 @@ export {
|
|
|
103
108
|
export type { BuiltinWardenRuleName } from './metadata.js';
|
|
104
109
|
|
|
105
110
|
export { activationOrphan } from './activation-orphan.js';
|
|
111
|
+
export { cliCommandRouteCoherence } from './cli-command-route-coherence.js';
|
|
106
112
|
export { noThrowInImplementation } from './no-throw-in-implementation.js';
|
|
107
113
|
export { circularRefs } from './circular-refs.js';
|
|
108
114
|
export { contourExists } from './contour-exists.js';
|
|
109
115
|
export { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
110
116
|
export { composesDeclarations } from './composes-declarations.js';
|
|
111
117
|
export { deadInternalTrail } from './dead-internal-trail.js';
|
|
118
|
+
export { deadPublicTrail } from './dead-public-trail.js';
|
|
112
119
|
export { draftFileMarking } from './draft-file-marking.js';
|
|
113
120
|
export { draftVisibleDebt } from './draft-visible-debt.js';
|
|
121
|
+
export { duplicatePublicContract } from './duplicate-public-contract.js';
|
|
114
122
|
export { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
115
123
|
export { exampleValid } from './example-valid.js';
|
|
116
124
|
export { firesDeclarations } from './fires-declarations.js';
|
|
117
125
|
export { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
|
|
118
126
|
export { incompleteCrud } from './incomplete-crud.js';
|
|
119
127
|
export { intentPropagation } from './intent-propagation.js';
|
|
128
|
+
export { libraryProjectionCoherence } from './library-projection-coherence.js';
|
|
120
129
|
export { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
121
130
|
export { missingVisibility } from './missing-visibility.js';
|
|
122
131
|
export { missingReconcile } from './missing-reconcile.js';
|
|
@@ -150,6 +159,7 @@ export { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
|
150
159
|
export { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
151
160
|
export { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
152
161
|
export { surfaceFacetCoherence } from './surface-facet-coherence.js';
|
|
162
|
+
export { trailForkCoaching } from './trail-fork-coaching.js';
|
|
153
163
|
export {
|
|
154
164
|
forkWithoutPreservedBlaze,
|
|
155
165
|
markerSchemaUnsupported,
|
|
@@ -178,6 +188,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
178
188
|
[contextNoSurfaceTypes.name, contextNoSurfaceTypes],
|
|
179
189
|
[composesDeclarations.name, composesDeclarations],
|
|
180
190
|
[deadInternalTrail.name, deadInternalTrail],
|
|
191
|
+
[deadPublicTrail.name, deadPublicTrail],
|
|
181
192
|
[draftFileMarking.name, draftFileMarking],
|
|
182
193
|
[draftVisibleDebt.name, draftVisibleDebt],
|
|
183
194
|
[errorMappingCompleteness.name, errorMappingCompleteness],
|
|
@@ -203,6 +214,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
203
214
|
[preferSchemaInference.name, preferSchemaInference],
|
|
204
215
|
[staticResourceAccessorPreference.name, staticResourceAccessorPreference],
|
|
205
216
|
[surfaceFacetCoherence.name, surfaceFacetCoherence],
|
|
217
|
+
[trailForkCoaching.name, trailForkCoaching],
|
|
206
218
|
[validDescribeRefs.name, validDescribeRefs],
|
|
207
219
|
[noDevPermitInSource.name, noDevPermitInSource],
|
|
208
220
|
[noDestructuredCompose.name, noDestructuredCompose],
|
|
@@ -239,7 +251,10 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
239
251
|
export const wardenTopoRules: ReadonlyMap<string, TopoAwareWardenRule> =
|
|
240
252
|
new Map<string, TopoAwareWardenRule>([
|
|
241
253
|
[activationOrphan.name, activationOrphan],
|
|
254
|
+
[cliCommandRouteCoherence.name, cliCommandRouteCoherence],
|
|
255
|
+
[duplicatePublicContract.name, duplicatePublicContract],
|
|
242
256
|
[incompleteAccessorForStandardOp.name, incompleteAccessorForStandardOp],
|
|
257
|
+
[libraryProjectionCoherence.name, libraryProjectionCoherence],
|
|
243
258
|
[permitGovernance.name, permitGovernance],
|
|
244
259
|
[publicOutputSchema.name, publicOutputSchema],
|
|
245
260
|
[publicUnionOutputDiscriminants.name, publicUnionOutputDiscriminants],
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { sep } from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getNodeCallee,
|
|
5
|
+
getNodeId,
|
|
6
|
+
getNodeInit,
|
|
7
|
+
identifierName,
|
|
8
|
+
offsetToLine,
|
|
9
|
+
parse,
|
|
10
|
+
walk,
|
|
11
|
+
} from './ast.js';
|
|
4
12
|
import type { AstNode } from './ast.js';
|
|
5
13
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
6
14
|
|
|
@@ -37,7 +45,7 @@ const isSetConstruction = (node: AstNode | undefined): boolean => {
|
|
|
37
45
|
if (node?.type !== 'NewExpression') {
|
|
38
46
|
return false;
|
|
39
47
|
}
|
|
40
|
-
const
|
|
48
|
+
const callee = getNodeCallee(node);
|
|
41
49
|
return identifierName(callee) === 'Set';
|
|
42
50
|
};
|
|
43
51
|
|
|
@@ -73,10 +81,8 @@ export const layerFieldNameDrift: WardenRule = {
|
|
|
73
81
|
if (node.type !== 'VariableDeclarator') {
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
init?: AstNode;
|
|
79
|
-
};
|
|
84
|
+
const id = getNodeId(node);
|
|
85
|
+
const init = getNodeInit(node);
|
|
80
86
|
const name = identifierName(id);
|
|
81
87
|
if (
|
|
82
88
|
name &&
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { deriveTopoGraph } from '@ontrails/topographer';
|
|
2
|
+
import type {
|
|
3
|
+
TopoGraph,
|
|
4
|
+
TopoGraphLibraryProjection,
|
|
5
|
+
} from '@ontrails/topographer';
|
|
6
|
+
|
|
7
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
8
|
+
|
|
9
|
+
const RULE_NAME = 'library-projection-coherence';
|
|
10
|
+
const TOPO_FILE = '<topo>';
|
|
11
|
+
|
|
12
|
+
const resolveGraph = (
|
|
13
|
+
topo: Parameters<TopoAwareWardenRule['checkTopo']>[0],
|
|
14
|
+
graph: TopoGraph | undefined
|
|
15
|
+
): TopoGraph => graph ?? deriveTopoGraph(topo);
|
|
16
|
+
|
|
17
|
+
const renderTrailIds = (trailIds: readonly string[]): string =>
|
|
18
|
+
trailIds
|
|
19
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
20
|
+
.map((trailId) => `"${trailId}"`)
|
|
21
|
+
.join(', ');
|
|
22
|
+
|
|
23
|
+
const collisionDiagnostic = (
|
|
24
|
+
exportName: string,
|
|
25
|
+
trailIds: readonly string[]
|
|
26
|
+
): WardenDiagnostic => ({
|
|
27
|
+
filePath: TOPO_FILE,
|
|
28
|
+
line: 1,
|
|
29
|
+
message: `Library projection export collision on "${exportName}": trails ${renderTrailIds(trailIds)} derive the same package export. Rename one trail or add a library export override before materializing the generated package.`,
|
|
30
|
+
rule: RULE_NAME,
|
|
31
|
+
severity: 'error',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const missingTargetDiagnostic = (
|
|
35
|
+
exportName: string,
|
|
36
|
+
trailId: string
|
|
37
|
+
): WardenDiagnostic => ({
|
|
38
|
+
filePath: TOPO_FILE,
|
|
39
|
+
line: 1,
|
|
40
|
+
message: `Library projection export "${exportName}" targets unknown trail "${trailId}". Resolved library exports must stay attached to existing trail contracts.`,
|
|
41
|
+
rule: RULE_NAME,
|
|
42
|
+
severity: 'error',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const duplicateExportDiagnostic = (
|
|
46
|
+
exportName: string,
|
|
47
|
+
trailIds: readonly string[]
|
|
48
|
+
): WardenDiagnostic => ({
|
|
49
|
+
filePath: TOPO_FILE,
|
|
50
|
+
line: 1,
|
|
51
|
+
message: `Library projection contains duplicate export "${exportName}" for trails ${renderTrailIds(trailIds)}. The resolved projection should record the collision and keep only one emitted export.`,
|
|
52
|
+
rule: RULE_NAME,
|
|
53
|
+
severity: 'error',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const collectDuplicateExportDiagnostics = (
|
|
57
|
+
projection: TopoGraphLibraryProjection
|
|
58
|
+
): readonly WardenDiagnostic[] => {
|
|
59
|
+
const trailIdsByExport = new Map<string, string[]>();
|
|
60
|
+
for (const entry of projection.exports) {
|
|
61
|
+
const trailIds = trailIdsByExport.get(entry.exportName) ?? [];
|
|
62
|
+
trailIds.push(entry.trailId);
|
|
63
|
+
trailIdsByExport.set(entry.exportName, trailIds);
|
|
64
|
+
}
|
|
65
|
+
return [...trailIdsByExport.entries()]
|
|
66
|
+
.filter(([, trailIds]) => trailIds.length > 1)
|
|
67
|
+
.map(([exportName, trailIds]) =>
|
|
68
|
+
duplicateExportDiagnostic(exportName, trailIds)
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const collectMissingTargetDiagnostics = (
|
|
73
|
+
projection: TopoGraphLibraryProjection,
|
|
74
|
+
knownTrailIds: ReadonlySet<string>
|
|
75
|
+
): readonly WardenDiagnostic[] =>
|
|
76
|
+
projection.exports
|
|
77
|
+
.filter((entry) => !knownTrailIds.has(entry.trailId))
|
|
78
|
+
.map((entry) => missingTargetDiagnostic(entry.exportName, entry.trailId));
|
|
79
|
+
|
|
80
|
+
export const libraryProjectionCoherence: TopoAwareWardenRule = {
|
|
81
|
+
checkTopo(topo, context) {
|
|
82
|
+
const graph = resolveGraph(topo, context?.graph);
|
|
83
|
+
if (!graph.library) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const knownTrailIds = new Set(topo.trails.keys());
|
|
88
|
+
return [
|
|
89
|
+
...graph.library.collisions.map((collision) =>
|
|
90
|
+
collisionDiagnostic(collision.exportName, collision.trailIds)
|
|
91
|
+
),
|
|
92
|
+
...collectDuplicateExportDiagnostics(graph.library),
|
|
93
|
+
...collectMissingTargetDiagnostics(graph.library, knownTrailIds),
|
|
94
|
+
];
|
|
95
|
+
},
|
|
96
|
+
description:
|
|
97
|
+
'Ensure resolved library projection exports are collision-free and target existing trails.',
|
|
98
|
+
name: RULE_NAME,
|
|
99
|
+
severity: 'error',
|
|
100
|
+
};
|
package/src/rules/metadata.ts
CHANGED
|
@@ -68,17 +68,21 @@ const depthByTier = {
|
|
|
68
68
|
|
|
69
69
|
const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
|
|
70
70
|
'activation-orphan': 'signals',
|
|
71
|
+
'cli-command-route-coherence': 'meta',
|
|
71
72
|
'composes-declarations': 'composition',
|
|
72
73
|
'context-no-surface-types': 'composition',
|
|
73
74
|
'dead-internal-trail': 'composition',
|
|
75
|
+
'dead-public-trail': 'composition',
|
|
74
76
|
'deprecation-without-guidance': 'lifecycle',
|
|
75
77
|
'draft-file-marking': 'lifecycle',
|
|
76
78
|
'draft-visible-debt': 'lifecycle',
|
|
79
|
+
'duplicate-public-contract': 'meta',
|
|
77
80
|
'error-mapping-completeness': 'results',
|
|
78
81
|
'fires-declarations': 'signals',
|
|
79
82
|
'fork-without-preserved-blaze': 'lifecycle',
|
|
80
83
|
'implementation-returns-result': 'results',
|
|
81
84
|
'intent-propagation': 'composition',
|
|
85
|
+
'library-projection-coherence': 'meta',
|
|
82
86
|
'marker-schema-unsupported': 'lifecycle',
|
|
83
87
|
'missing-reconcile': 'resources',
|
|
84
88
|
'missing-visibility': 'composition',
|
|
@@ -106,6 +110,7 @@ const concernByRuleName: Partial<Record<string, WardenRuleConcern>> = {
|
|
|
106
110
|
'signal-graph-coaching': 'signals',
|
|
107
111
|
'static-resource-accessor-preference': 'resources',
|
|
108
112
|
'surface-facet-coherence': 'meta',
|
|
113
|
+
'trail-fork-coaching': 'meta',
|
|
109
114
|
'unmaterialized-activation-source': 'lifecycle',
|
|
110
115
|
'valid-detour-contract': 'results',
|
|
111
116
|
'version-gap': 'lifecycle',
|
|
@@ -151,6 +156,23 @@ const builtinWardenRuleMetadataInput = {
|
|
|
151
156
|
invariant: 'Contour reference graphs must be acyclic.',
|
|
152
157
|
tier: 'project-static',
|
|
153
158
|
},
|
|
159
|
+
'cli-command-route-coherence': {
|
|
160
|
+
...durableExternal,
|
|
161
|
+
guidance: {
|
|
162
|
+
docs: [
|
|
163
|
+
{
|
|
164
|
+
label: 'CLI command routes ADR',
|
|
165
|
+
path: 'docs/adr/drafts/20260613-cli-command-routes.md',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
relatedRules: ['webhook-route-collision'],
|
|
169
|
+
summary:
|
|
170
|
+
'Keep every CLI command route and alias normalized into one trail contract.',
|
|
171
|
+
},
|
|
172
|
+
invariant:
|
|
173
|
+
'CLI command routes and aliases resolve to one coherent trail contract.',
|
|
174
|
+
tier: 'topo-aware',
|
|
175
|
+
},
|
|
154
176
|
'composes-declarations': {
|
|
155
177
|
...durableExternal,
|
|
156
178
|
invariant: 'Declared composes stay aligned with ctx.compose() usage.',
|
|
@@ -171,6 +193,17 @@ const builtinWardenRuleMetadataInput = {
|
|
|
171
193
|
invariant: 'Internal trails should be reachable through declared composes.',
|
|
172
194
|
tier: 'project-static',
|
|
173
195
|
},
|
|
196
|
+
'dead-public-trail': {
|
|
197
|
+
...durableExternal,
|
|
198
|
+
guidance: {
|
|
199
|
+
relatedRules: ['dead-internal-trail', 'duplicate-public-contract'],
|
|
200
|
+
summary:
|
|
201
|
+
'Anchor exported public trails in a topo, composition edge, or activation source.',
|
|
202
|
+
},
|
|
203
|
+
invariant:
|
|
204
|
+
'Exported public trails are anchored in configured app topos, composition, or activation.',
|
|
205
|
+
tier: 'project-static',
|
|
206
|
+
},
|
|
174
207
|
'deprecation-without-guidance': {
|
|
175
208
|
...durableExternal,
|
|
176
209
|
invariant:
|
|
@@ -187,6 +220,23 @@ const builtinWardenRuleMetadataInput = {
|
|
|
187
220
|
invariant: 'Draft-authored IDs remain visible debt.',
|
|
188
221
|
tier: 'source-static',
|
|
189
222
|
},
|
|
223
|
+
'duplicate-public-contract': {
|
|
224
|
+
...durableExternal,
|
|
225
|
+
guidance: {
|
|
226
|
+
docs: [
|
|
227
|
+
{
|
|
228
|
+
label: 'Surface accommodations',
|
|
229
|
+
path: 'docs/surfaces/surface-accommodations.md',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
relatedRules: ['cli-command-route-coherence', 'trail-fork-coaching'],
|
|
233
|
+
summary:
|
|
234
|
+
'Keep duplicate public contract facts from drifting into separate capabilities.',
|
|
235
|
+
},
|
|
236
|
+
invariant:
|
|
237
|
+
'Public surface trails should not expose duplicate normalized contract facts.',
|
|
238
|
+
tier: 'topo-aware',
|
|
239
|
+
},
|
|
190
240
|
'error-mapping-completeness': {
|
|
191
241
|
...durableExtension,
|
|
192
242
|
invariant: 'Registered surface error mappers cover every error category.',
|
|
@@ -241,6 +291,28 @@ const builtinWardenRuleMetadataInput = {
|
|
|
241
291
|
'Layer input field reserved names are shared across surface projections.',
|
|
242
292
|
tier: 'source-static',
|
|
243
293
|
},
|
|
294
|
+
'library-projection-coherence': {
|
|
295
|
+
...durableExternal,
|
|
296
|
+
guidance: {
|
|
297
|
+
docs: [
|
|
298
|
+
{
|
|
299
|
+
label: 'Library Surface ADR',
|
|
300
|
+
path: 'docs/adr/drafts/20260612-library-surface-and-compiler.md',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
relatedRules: ['cli-command-route-coherence', 'surface-facet-coherence'],
|
|
304
|
+
steps: [
|
|
305
|
+
'Rename one source trail or add an explicit library export override before generating a package.',
|
|
306
|
+
'Keep serialized library projection exports attached to existing trail IDs.',
|
|
307
|
+
'Run the generated-package smoke after repairing projection drift.',
|
|
308
|
+
],
|
|
309
|
+
summary:
|
|
310
|
+
'Keep resolved library projection exports collision-free and attached to one trail contract.',
|
|
311
|
+
},
|
|
312
|
+
invariant:
|
|
313
|
+
'Resolved library projection exports are collision-free and target existing trails.',
|
|
314
|
+
tier: 'topo-aware',
|
|
315
|
+
},
|
|
244
316
|
'marker-schema-unsupported': {
|
|
245
317
|
...durableExternal,
|
|
246
318
|
invariant:
|
|
@@ -554,6 +626,34 @@ const builtinWardenRuleMetadataInput = {
|
|
|
554
626
|
'Surface facet maps avoid selector overlap, hidden visibility widening, and drift-prone dynamic selectors.',
|
|
555
627
|
tier: 'source-static',
|
|
556
628
|
},
|
|
629
|
+
'trail-fork-coaching': {
|
|
630
|
+
...durableExternal,
|
|
631
|
+
guidance: {
|
|
632
|
+
docs: [
|
|
633
|
+
{
|
|
634
|
+
label: 'ADR-0050 Surface Accommodations',
|
|
635
|
+
path: 'docs/adr/0050-surface-accommodations-preserve-trail-identity.md',
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
label: 'Surface Accommodations',
|
|
639
|
+
path: 'docs/surfaces/surface-accommodations.md',
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
relatedRules: ['surface-facet-coherence', 'cli-command-route-coherence'],
|
|
643
|
+
steps: [
|
|
644
|
+
'Check the semantic fork boundary: intent, permits, outputs, errors, lifecycle, and side effects.',
|
|
645
|
+
'Check the structural fork boundary: selected trail identity stays visible instead of hiding behind action vocabulary.',
|
|
646
|
+
'Split real capability forks into distinct trails or a composing trail.',
|
|
647
|
+
'Use a facet only when one surface entry needs to group multiple trails while preserving selected member identity.',
|
|
648
|
+
],
|
|
649
|
+
summary:
|
|
650
|
+
'Keep surface accommodations from hiding several capabilities behind one branching trail input.',
|
|
651
|
+
},
|
|
652
|
+
invariant:
|
|
653
|
+
'Trails avoid hiding distinct capabilities behind branching action or operation inputs.',
|
|
654
|
+
scope: 'advisory',
|
|
655
|
+
tier: 'source-static',
|
|
656
|
+
},
|
|
557
657
|
'unmaterialized-activation-source': {
|
|
558
658
|
...durableExternal,
|
|
559
659
|
invariant:
|
|
@@ -601,7 +701,8 @@ const builtinWardenRuleMetadataInput = {
|
|
|
601
701
|
},
|
|
602
702
|
'warden-rules-use-ast': {
|
|
603
703
|
...durableRepoLocal,
|
|
604
|
-
invariant:
|
|
704
|
+
invariant:
|
|
705
|
+
'Warden source rules use AST helpers instead of ad hoc parsing or raw node-field casts.',
|
|
605
706
|
tier: 'source-static',
|
|
606
707
|
},
|
|
607
708
|
'webhook-route-collision': {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findBlazeBodies,
|
|
3
3
|
findTrailDefinitions,
|
|
4
|
+
getNodeComputed,
|
|
5
|
+
getNodeId,
|
|
6
|
+
getNodeInit,
|
|
7
|
+
getNodeLeft,
|
|
8
|
+
getNodeOperator,
|
|
9
|
+
getNodeParams,
|
|
10
|
+
getNodeProperties,
|
|
11
|
+
getNodeRight,
|
|
4
12
|
getStringValue,
|
|
5
13
|
identifierName,
|
|
6
14
|
isShadowed,
|
|
@@ -19,7 +27,7 @@ const diagnosticMessage = (trailId: string): string =>
|
|
|
19
27
|
`Trail "${trailId}" destructures compose from the blaze context. Use ctx.compose(...) directly so composition stays visible and Warden can recognize composed Result values.`;
|
|
20
28
|
|
|
21
29
|
const propertyKeyName = (property: AstNode): string | null => {
|
|
22
|
-
if ((property
|
|
30
|
+
if (getNodeComputed(property) === true) {
|
|
23
31
|
return null;
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -38,9 +46,7 @@ const findComposeBinding = (pattern: AstNode | undefined): AstNode | null => {
|
|
|
38
46
|
return null;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
const properties =
|
|
42
|
-
(pattern as unknown as { properties?: readonly AstNode[] }).properties ??
|
|
43
|
-
[];
|
|
49
|
+
const properties = getNodeProperties(pattern) ?? [];
|
|
44
50
|
|
|
45
51
|
for (const property of properties) {
|
|
46
52
|
if (
|
|
@@ -55,7 +61,7 @@ const findComposeBinding = (pattern: AstNode | undefined): AstNode | null => {
|
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
const blazeParams = (blaze: AstNode): readonly AstNode[] =>
|
|
58
|
-
(blaze
|
|
64
|
+
getNodeParams(blaze) ?? [];
|
|
59
65
|
|
|
60
66
|
const destructuredComposeFromVariableDeclarator = (
|
|
61
67
|
node: AstNode,
|
|
@@ -65,10 +71,8 @@ const destructuredComposeFromVariableDeclarator = (
|
|
|
65
71
|
return null;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
readonly init?: AstNode;
|
|
71
|
-
};
|
|
74
|
+
const id = getNodeId(node);
|
|
75
|
+
const init = getNodeInit(node);
|
|
72
76
|
|
|
73
77
|
if (identifierName(init) !== contextName) {
|
|
74
78
|
return null;
|
|
@@ -85,11 +89,9 @@ const destructuredComposeFromAssignment = (
|
|
|
85
89
|
return null;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
readonly right?: AstNode;
|
|
92
|
-
};
|
|
92
|
+
const left = getNodeLeft(node);
|
|
93
|
+
const operator = getNodeOperator(node);
|
|
94
|
+
const right = getNodeRight(node);
|
|
93
95
|
|
|
94
96
|
if (operator !== '=' || identifierName(right) !== contextName) {
|
|
95
97
|
return null;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getNodeArguments,
|
|
3
|
+
getNodeCallee,
|
|
4
|
+
getNodeObject,
|
|
5
|
+
getNodeProperty,
|
|
6
|
+
identifierName,
|
|
7
|
+
offsetToLine,
|
|
8
|
+
parse,
|
|
9
|
+
walk,
|
|
10
|
+
} from './ast.js';
|
|
2
11
|
import { isFrameworkInternalFile } from './scan.js';
|
|
3
12
|
import type { AstNode } from './ast.js';
|
|
4
13
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
@@ -24,7 +33,7 @@ const getMemberPropertyName = (node: AstNode): string | null => {
|
|
|
24
33
|
return null;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
return identifierName((node
|
|
36
|
+
return identifierName(getNodeProperty(node));
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
const isResultObject = (node: AstNode | undefined): boolean => {
|
|
@@ -44,12 +53,12 @@ const isResultErrCall = (node: AstNode): boolean => {
|
|
|
44
53
|
return false;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
const
|
|
56
|
+
const callee = getNodeCallee(node);
|
|
48
57
|
if (!callee || getMemberPropertyName(callee) !== 'err') {
|
|
49
58
|
return false;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
return isResultObject((callee
|
|
61
|
+
return isResultObject(getNodeObject(callee));
|
|
53
62
|
};
|
|
54
63
|
|
|
55
64
|
const isNativeErrorConstruction = (node: AstNode | undefined): boolean => {
|
|
@@ -57,16 +66,14 @@ const isNativeErrorConstruction = (node: AstNode | undefined): boolean => {
|
|
|
57
66
|
return false;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
const constructorName = identifierName(
|
|
61
|
-
(node as unknown as { callee?: AstNode }).callee
|
|
62
|
-
);
|
|
69
|
+
const constructorName = identifierName(getNodeCallee(node));
|
|
63
70
|
return constructorName
|
|
64
71
|
? NATIVE_ERROR_CONSTRUCTORS.has(constructorName)
|
|
65
72
|
: false;
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
const getFirstArgument = (node: AstNode): AstNode | undefined =>
|
|
69
|
-
(node
|
|
76
|
+
getNodeArguments(node)?.[0];
|
|
70
77
|
|
|
71
78
|
const createDiagnostic = (
|
|
72
79
|
filePath: string,
|