@ontrails/warden 1.0.0-beta.3 → 1.0.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +674 -9
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +28 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +137 -0
- package/src/cli.ts +1451 -104
- package/src/command.ts +966 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +122 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +217 -14
- package/src/project-context.ts +192 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +4008 -86
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +837 -0
- package/src/rules/context-no-surface-types.ts +78 -15
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/duplicate-public-contract.ts +91 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +735 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +581 -0
- package/src/rules/index.ts +235 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +755 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +194 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1135 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +562 -0
- package/src/rules/public-internal-deep-imports.ts +457 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +155 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +693 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +649 -0
- package/src/rules/surface-facet-coherence.ts +362 -0
- package/src/rules/trail-fork-coaching.ts +616 -0
- package/src/rules/trail-versioning-source.ts +1072 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +297 -8
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1114 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +83 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +198 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +214 -0
- package/src/workspaces.ts +199 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -46
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -128
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -215
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectComposeTargetTrailIds,
|
|
3
|
+
findConfigProperty,
|
|
4
|
+
findTrailDefinitions,
|
|
5
|
+
getNodeDeclaration,
|
|
6
|
+
getNodeDeclarations,
|
|
7
|
+
getNodeId,
|
|
8
|
+
getNodeInit,
|
|
9
|
+
getNodeLocal,
|
|
10
|
+
getNodeName,
|
|
11
|
+
getNodeSpecifiers,
|
|
12
|
+
getNodeValue,
|
|
13
|
+
getStringValue,
|
|
14
|
+
isStringLiteral,
|
|
15
|
+
offsetToLine,
|
|
16
|
+
parse,
|
|
17
|
+
walkWithParents,
|
|
18
|
+
} from './ast.js';
|
|
19
|
+
import type { AstNode, AstParentContext } from './ast.js';
|
|
20
|
+
import { isTestFile } from './scan.js';
|
|
21
|
+
import type {
|
|
22
|
+
ProjectAwareWardenRule,
|
|
23
|
+
ProjectContext,
|
|
24
|
+
WardenDiagnostic,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
const RULE_NAME = 'dead-public-trail';
|
|
28
|
+
|
|
29
|
+
const isNonEmptyActivationValue = (onValue: AstNode): boolean => {
|
|
30
|
+
if (onValue.type === 'Identifier') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (onValue.type !== 'ArrayExpression') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const elements = onValue['elements'] as readonly AstNode[] | undefined;
|
|
37
|
+
return (elements?.length ?? 0) > 0;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const hasOnActivation = (config: AstNode): boolean => {
|
|
41
|
+
const onProp = findConfigProperty(config, 'on');
|
|
42
|
+
const onValue = onProp?.value as AstNode | undefined;
|
|
43
|
+
return onValue ? isNonEmptyActivationValue(onValue) : false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const hasExplicitInternalVisibility = (config: AstNode): boolean => {
|
|
47
|
+
const visibilityProp = findConfigProperty(config, 'visibility');
|
|
48
|
+
const visibilityValue = visibilityProp?.value as AstNode | undefined;
|
|
49
|
+
return (
|
|
50
|
+
!!visibilityValue &&
|
|
51
|
+
isStringLiteral(visibilityValue) &&
|
|
52
|
+
getStringValue(visibilityValue) === 'internal'
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasLegacyMetaInternal = (config: AstNode): boolean => {
|
|
57
|
+
const metaProp = findConfigProperty(config, 'meta');
|
|
58
|
+
const metaValue = metaProp?.value as AstNode | undefined;
|
|
59
|
+
if (!metaValue || metaValue.type !== 'ObjectExpression') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const internalProp = findConfigProperty(metaValue, 'internal');
|
|
63
|
+
const internalValue = internalProp?.value as AstNode | undefined;
|
|
64
|
+
return (
|
|
65
|
+
internalValue?.type === 'BooleanLiteral' &&
|
|
66
|
+
getNodeValue(internalValue) === true
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isInternalTrail = (config: AstNode): boolean =>
|
|
71
|
+
hasExplicitInternalVisibility(config) || hasLegacyMetaInternal(config);
|
|
72
|
+
|
|
73
|
+
const isExportDeclaration = (node: AstNode | null | undefined): boolean =>
|
|
74
|
+
node?.type === 'ExportNamedDeclaration' ||
|
|
75
|
+
node?.type === 'ExportDefaultDeclaration';
|
|
76
|
+
|
|
77
|
+
const identifierName = (node: AstNode | undefined): string | null =>
|
|
78
|
+
node?.type === 'Identifier' ? (getNodeName(node) ?? null) : null;
|
|
79
|
+
|
|
80
|
+
const trailBindingStarts = (ast: AstNode): ReadonlyMap<string, number> => {
|
|
81
|
+
const trailStarts = new Set(
|
|
82
|
+
findTrailDefinitions(ast)
|
|
83
|
+
.filter((definition) => definition.kind === 'trail')
|
|
84
|
+
.map((definition) => definition.start)
|
|
85
|
+
);
|
|
86
|
+
const bindings = new Map<string, number>();
|
|
87
|
+
|
|
88
|
+
walkWithParents(ast, (node: AstNode) => {
|
|
89
|
+
if (node.type !== 'VariableDeclarator') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const id = getNodeId(node);
|
|
93
|
+
const init = getNodeInit(node);
|
|
94
|
+
const name = identifierName(id);
|
|
95
|
+
if (name && init && trailStarts.has(init.start)) {
|
|
96
|
+
bindings.set(name, init.start);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return bindings;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const addExportedSpecifierStarts = (
|
|
104
|
+
node: AstNode,
|
|
105
|
+
bindings: ReadonlyMap<string, number>,
|
|
106
|
+
exported: Set<number>
|
|
107
|
+
): void => {
|
|
108
|
+
const specifiers = getNodeSpecifiers(node) ?? [];
|
|
109
|
+
for (const specifier of specifiers) {
|
|
110
|
+
const local = getNodeLocal(specifier);
|
|
111
|
+
const name = identifierName(local);
|
|
112
|
+
const start = name ? bindings.get(name) : undefined;
|
|
113
|
+
if (start !== undefined) {
|
|
114
|
+
exported.add(start);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const exportedTrailStarts = (ast: AstNode): ReadonlySet<number> => {
|
|
120
|
+
const exported = new Set<number>();
|
|
121
|
+
const bindings = trailBindingStarts(ast);
|
|
122
|
+
|
|
123
|
+
walkWithParents(ast, (node: AstNode, context: AstParentContext) => {
|
|
124
|
+
if (node.type !== 'CallExpression') {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Catch `export default trail(...)`. Exported variable declarations are
|
|
129
|
+
// handled by the VariableDeclaration walk below.
|
|
130
|
+
if (isExportDeclaration(context.parent)) {
|
|
131
|
+
exported.add(node.start);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
walkWithParents(ast, (node: AstNode, context: AstParentContext) => {
|
|
136
|
+
if (node.type !== 'VariableDeclaration') {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isExportDeclaration(context.parent)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const declarations = getNodeDeclarations(node) ?? [];
|
|
143
|
+
for (const declaration of declarations) {
|
|
144
|
+
const init = getNodeInit(declaration);
|
|
145
|
+
if (init?.type === 'CallExpression') {
|
|
146
|
+
exported.add(init.start);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
walkWithParents(ast, (node: AstNode) => {
|
|
152
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
153
|
+
addExportedSpecifierStarts(node, bindings, exported);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (node.type !== 'ExportDefaultDeclaration') {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const declaration = getNodeDeclaration(node);
|
|
161
|
+
const name = identifierName(declaration);
|
|
162
|
+
const start = name ? bindings.get(name) : undefined;
|
|
163
|
+
if (start !== undefined) {
|
|
164
|
+
exported.add(start);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return exported;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const buildDiagnostic = (
|
|
172
|
+
trailId: string,
|
|
173
|
+
filePath: string,
|
|
174
|
+
line: number
|
|
175
|
+
): WardenDiagnostic => ({
|
|
176
|
+
filePath,
|
|
177
|
+
line,
|
|
178
|
+
message: `Exported public trail "${trailId}" is not registered in a configured app topo, composed by another trail, or activated by on:. Anchor the contract in a topo, compose it, mark it internal, or remove the public export.`,
|
|
179
|
+
rule: RULE_NAME,
|
|
180
|
+
severity: 'warn',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const unionComposeTargetIds = (
|
|
184
|
+
ast: AstNode | null,
|
|
185
|
+
sourceCode: string,
|
|
186
|
+
context: ProjectContext
|
|
187
|
+
): ReadonlySet<string> => {
|
|
188
|
+
const local = ast
|
|
189
|
+
? collectComposeTargetTrailIds(ast, sourceCode)
|
|
190
|
+
: new Set<string>();
|
|
191
|
+
return context.composeTargetTrailIds
|
|
192
|
+
? new Set([...context.composeTargetTrailIds, ...local])
|
|
193
|
+
: local;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const checkDeadPublicTrails = (
|
|
197
|
+
ast: AstNode | null,
|
|
198
|
+
sourceCode: string,
|
|
199
|
+
filePath: string,
|
|
200
|
+
context: ProjectContext
|
|
201
|
+
): readonly WardenDiagnostic[] => {
|
|
202
|
+
if (isTestFile(filePath) || !ast || !context.topoTrailIds) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const exportedStarts = exportedTrailStarts(ast);
|
|
207
|
+
const composeTargetTrailIds = unionComposeTargetIds(ast, sourceCode, context);
|
|
208
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
209
|
+
|
|
210
|
+
for (const def of findTrailDefinitions(ast)) {
|
|
211
|
+
if (
|
|
212
|
+
def.kind !== 'trail' ||
|
|
213
|
+
isInternalTrail(def.config) ||
|
|
214
|
+
!exportedStarts.has(def.start)
|
|
215
|
+
) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
hasOnActivation(def.config) ||
|
|
221
|
+
composeTargetTrailIds.has(def.id) ||
|
|
222
|
+
context.topoTrailIds.has(def.id)
|
|
223
|
+
) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
diagnostics.push(
|
|
228
|
+
buildDiagnostic(def.id, filePath, offsetToLine(sourceCode, def.start))
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return diagnostics;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const deadPublicTrail: ProjectAwareWardenRule = {
|
|
236
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
237
|
+
const ast = parse(filePath, sourceCode);
|
|
238
|
+
return checkDeadPublicTrails(ast, sourceCode, filePath, {
|
|
239
|
+
knownTrailIds: new Set<string>(),
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
checkWithContext(
|
|
243
|
+
sourceCode: string,
|
|
244
|
+
filePath: string,
|
|
245
|
+
context: ProjectContext
|
|
246
|
+
): readonly WardenDiagnostic[] {
|
|
247
|
+
return checkDeadPublicTrails(
|
|
248
|
+
parse(filePath, sourceCode),
|
|
249
|
+
sourceCode,
|
|
250
|
+
filePath,
|
|
251
|
+
context
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
description:
|
|
255
|
+
'Warn when an exported public trail is neither registered in configured app topos nor reachable through composition or activation.',
|
|
256
|
+
name: RULE_NAME,
|
|
257
|
+
severity: 'warn',
|
|
258
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { isDraftId } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import { isDraftMarkedFile } from '../draft.js';
|
|
4
|
+
import {
|
|
5
|
+
collectFrameworkDraftPrefixConstantOffsets,
|
|
6
|
+
findStringLiterals,
|
|
7
|
+
hasIgnoreCommentOnLine,
|
|
8
|
+
offsetToLine,
|
|
9
|
+
parse,
|
|
10
|
+
splitSourceLines,
|
|
11
|
+
} from './ast.js';
|
|
12
|
+
import type { StringLiteralMatch } from './ast.js';
|
|
13
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
14
|
+
|
|
15
|
+
const messageForMissingMarker = (draftId: string): string =>
|
|
16
|
+
`Draft id "${draftId}" appears in source, but the file is not draft-marked. ` +
|
|
17
|
+
'Rename it with an _draft. prefix or a .draft. trailing segment.';
|
|
18
|
+
|
|
19
|
+
const makeDiagnostic = (
|
|
20
|
+
sourceCode: string,
|
|
21
|
+
filePath: string,
|
|
22
|
+
start: number,
|
|
23
|
+
message: string,
|
|
24
|
+
severity: WardenDiagnostic['severity']
|
|
25
|
+
): WardenDiagnostic => ({
|
|
26
|
+
filePath,
|
|
27
|
+
line: offsetToLine(sourceCode, start),
|
|
28
|
+
message,
|
|
29
|
+
rule: 'draft-file-marking',
|
|
30
|
+
severity,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const collectDraftMatches = (
|
|
34
|
+
sourceCode: string,
|
|
35
|
+
filePath: string,
|
|
36
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
37
|
+
): StringLiteralMatch[] => {
|
|
38
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
39
|
+
ast,
|
|
40
|
+
filePath
|
|
41
|
+
);
|
|
42
|
+
const lines = splitSourceLines(sourceCode);
|
|
43
|
+
return findStringLiterals(ast, (value) => isDraftId(value)).filter(
|
|
44
|
+
(match) => {
|
|
45
|
+
if (frameworkConstantOffsets.has(match.start)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start))
|
|
50
|
+
) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const draftMissingMarkerDiagnostic = (
|
|
59
|
+
sourceCode: string,
|
|
60
|
+
filePath: string,
|
|
61
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
62
|
+
): WardenDiagnostic | null => {
|
|
63
|
+
const draftMatches = collectDraftMatches(sourceCode, filePath, ast);
|
|
64
|
+
if (!draftMatches.length || isDraftMarkedFile(filePath)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [first] = draftMatches;
|
|
69
|
+
if (!first) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return makeDiagnostic(
|
|
74
|
+
sourceCode,
|
|
75
|
+
filePath,
|
|
76
|
+
first.start,
|
|
77
|
+
messageForMissingMarker(first.value),
|
|
78
|
+
'error'
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const draftMarkedWithoutIdsDiagnostic = (
|
|
83
|
+
filePath: string,
|
|
84
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
85
|
+
): WardenDiagnostic | null => {
|
|
86
|
+
// Deciding whether the file's `_draft.` marker is still warranted is a
|
|
87
|
+
// question about *all* draft ids present in source, not just the unsuppressed
|
|
88
|
+
// ones. Pragma-suppressed ids still justify a draft-marked filename — a user
|
|
89
|
+
// intentionally silencing them has not removed the draft content. We
|
|
90
|
+
// therefore filter only the framework-constant declarations (which are not
|
|
91
|
+
// draft ids at all) and bypass the pragma filter that `collectDraftMatches`
|
|
92
|
+
// applies.
|
|
93
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
94
|
+
ast,
|
|
95
|
+
filePath
|
|
96
|
+
);
|
|
97
|
+
const unsuppressedDraftIds = findStringLiterals(ast, (value) =>
|
|
98
|
+
isDraftId(value)
|
|
99
|
+
).filter((match) => !frameworkConstantOffsets.has(match.start));
|
|
100
|
+
|
|
101
|
+
if (unsuppressedDraftIds.length > 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isDraftMarkedFile(filePath)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
filePath,
|
|
111
|
+
line: 1,
|
|
112
|
+
message:
|
|
113
|
+
'File is draft-marked but no longer contains draft ids. Remove the draft filename marker or finish the promotion cleanup.',
|
|
114
|
+
rule: 'draft-file-marking',
|
|
115
|
+
severity: 'warn',
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const collectDraftFileMarkingDiagnostics = (
|
|
120
|
+
sourceCode: string,
|
|
121
|
+
filePath: string,
|
|
122
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
123
|
+
): WardenDiagnostic[] => {
|
|
124
|
+
const missingMarkerDiagnostic = draftMissingMarkerDiagnostic(
|
|
125
|
+
sourceCode,
|
|
126
|
+
filePath,
|
|
127
|
+
ast
|
|
128
|
+
);
|
|
129
|
+
if (missingMarkerDiagnostic) {
|
|
130
|
+
return [missingMarkerDiagnostic];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const markedWithoutIdsDiagnostic = draftMarkedWithoutIdsDiagnostic(
|
|
134
|
+
filePath,
|
|
135
|
+
ast
|
|
136
|
+
);
|
|
137
|
+
if (markedWithoutIdsDiagnostic) {
|
|
138
|
+
return [markedWithoutIdsDiagnostic];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ensures files containing draft ids are visibly marked as draft-bearing files.
|
|
146
|
+
*/
|
|
147
|
+
export const draftFileMarking: WardenRule = {
|
|
148
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
149
|
+
const ast = parse(filePath, sourceCode);
|
|
150
|
+
if (!ast) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return collectDraftFileMarkingDiagnostics(sourceCode, filePath, ast);
|
|
155
|
+
},
|
|
156
|
+
description:
|
|
157
|
+
'Require draft-bearing files to use _draft.* or *.draft.* filename markers.',
|
|
158
|
+
name: 'draft-file-marking',
|
|
159
|
+
severity: 'error',
|
|
160
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isDraftId } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
collectFrameworkDraftPrefixConstantOffsets,
|
|
5
|
+
findStringLiterals,
|
|
6
|
+
hasIgnoreCommentOnLine,
|
|
7
|
+
offsetToLine,
|
|
8
|
+
parse,
|
|
9
|
+
splitSourceLines,
|
|
10
|
+
} from './ast.js';
|
|
11
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
12
|
+
|
|
13
|
+
const createDiagnostic = (
|
|
14
|
+
sourceCode: string,
|
|
15
|
+
filePath: string,
|
|
16
|
+
match: { start: number; value: string }
|
|
17
|
+
): WardenDiagnostic => ({
|
|
18
|
+
filePath,
|
|
19
|
+
line: offsetToLine(sourceCode, match.start),
|
|
20
|
+
message:
|
|
21
|
+
`Draft id "${match.value}" is still visible debt. ` +
|
|
22
|
+
'Established surfaces, lock export, and OpenAPI generation will reject it until it is promoted.',
|
|
23
|
+
rule: 'draft-visible-debt',
|
|
24
|
+
severity: 'warn',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const isSuppressedMatch = (
|
|
28
|
+
match: { start: number },
|
|
29
|
+
sourceCode: string,
|
|
30
|
+
lines: readonly string[],
|
|
31
|
+
frameworkConstantOffsets: ReadonlySet<number>
|
|
32
|
+
): boolean =>
|
|
33
|
+
frameworkConstantOffsets.has(match.start) ||
|
|
34
|
+
hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start));
|
|
35
|
+
|
|
36
|
+
const collectDraftVisibleDebtDiagnostics = (
|
|
37
|
+
sourceCode: string,
|
|
38
|
+
filePath: string,
|
|
39
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
40
|
+
): WardenDiagnostic[] => {
|
|
41
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
42
|
+
ast,
|
|
43
|
+
filePath
|
|
44
|
+
);
|
|
45
|
+
const lines = splitSourceLines(sourceCode);
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
|
|
48
|
+
return findStringLiterals(ast, (value) => isDraftId(value)).flatMap(
|
|
49
|
+
(match) => {
|
|
50
|
+
if (
|
|
51
|
+
isSuppressedMatch(match, sourceCode, lines, frameworkConstantOffsets)
|
|
52
|
+
) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const key = `${match.value}:${String(match.start)}`;
|
|
56
|
+
if (seen.has(key)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
seen.add(key);
|
|
60
|
+
return [createDiagnostic(sourceCode, filePath, match)];
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Warns when draft ids are still present so the debt stays visible during
|
|
67
|
+
* review even when the file is correctly marked.
|
|
68
|
+
*
|
|
69
|
+
* Severity is intentionally `warn`, not `error`. The hard rejection layer for
|
|
70
|
+
* draft state leaking into established outputs is `validateEstablishedTopo` at
|
|
71
|
+
* runtime — it blocks topo compile, surface projection, and lockfile writes.
|
|
72
|
+
* This rule surfaces the debt for human reviewers without duplicating that layer.
|
|
73
|
+
*/
|
|
74
|
+
export const draftVisibleDebt: WardenRule = {
|
|
75
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
76
|
+
const ast = parse(filePath, sourceCode);
|
|
77
|
+
if (!ast) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return collectDraftVisibleDebtDiagnostics(sourceCode, filePath, ast);
|
|
82
|
+
},
|
|
83
|
+
description:
|
|
84
|
+
'Warn when draft ids remain in source so the debt stays visible during review.',
|
|
85
|
+
name: 'draft-visible-debt',
|
|
86
|
+
severity: 'warn',
|
|
87
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { deriveTopoGraph } from '@ontrails/topographer';
|
|
2
|
+
import type { TopoGraph, TopoGraphEntry } from '@ontrails/topographer';
|
|
3
|
+
|
|
4
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
5
|
+
|
|
6
|
+
const RULE_NAME = 'duplicate-public-contract';
|
|
7
|
+
const TOPO_FILE = '<topo>';
|
|
8
|
+
|
|
9
|
+
const resolveGraph = (
|
|
10
|
+
topo: Parameters<TopoAwareWardenRule['checkTopo']>[0],
|
|
11
|
+
graph: TopoGraph | undefined
|
|
12
|
+
): TopoGraph => graph ?? deriveTopoGraph(topo);
|
|
13
|
+
|
|
14
|
+
const canonicalize = (value: unknown): unknown => {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map(canonicalize);
|
|
17
|
+
}
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.entries(value as Record<string, unknown>)
|
|
21
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
22
|
+
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
23
|
+
.map(([key, entryValue]) => [key, canonicalize(entryValue)])
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const contractFingerprint = (entry: TopoGraphEntry): string =>
|
|
30
|
+
JSON.stringify(
|
|
31
|
+
canonicalize({
|
|
32
|
+
composes: entry.composes,
|
|
33
|
+
detours: entry.detours,
|
|
34
|
+
dryRunCapable: entry.dryRunCapable,
|
|
35
|
+
fires: entry.fires,
|
|
36
|
+
idempotent: entry.idempotent,
|
|
37
|
+
input: entry.input,
|
|
38
|
+
intent: entry.intent,
|
|
39
|
+
meta: entry.meta,
|
|
40
|
+
on: entry.on,
|
|
41
|
+
output: entry.output,
|
|
42
|
+
permit: entry.permit,
|
|
43
|
+
resources: entry.resources,
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const isCandidate = (entry: TopoGraphEntry): boolean =>
|
|
48
|
+
entry.kind === 'trail' &&
|
|
49
|
+
!entry.id.startsWith('warden.rule.') &&
|
|
50
|
+
entry.deprecated !== true &&
|
|
51
|
+
entry.meta?.['internal'] !== true &&
|
|
52
|
+
entry.input !== undefined &&
|
|
53
|
+
entry.output !== undefined &&
|
|
54
|
+
Boolean(entry.cli || entry.surfaces.length > 0);
|
|
55
|
+
|
|
56
|
+
const renderTrailIds = (trailIds: readonly string[]): string =>
|
|
57
|
+
trailIds
|
|
58
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
59
|
+
.map((trailId) => `"${trailId}"`)
|
|
60
|
+
.join(', ');
|
|
61
|
+
|
|
62
|
+
const buildDiagnostic = (trailIds: readonly string[]): WardenDiagnostic => ({
|
|
63
|
+
filePath: TOPO_FILE,
|
|
64
|
+
line: 1,
|
|
65
|
+
message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.`,
|
|
66
|
+
rule: RULE_NAME,
|
|
67
|
+
severity: 'warn',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const duplicatePublicContract: TopoAwareWardenRule = {
|
|
71
|
+
checkTopo(topo, context) {
|
|
72
|
+
const graph = resolveGraph(topo, context?.graph);
|
|
73
|
+
const groups = new Map<string, string[]>();
|
|
74
|
+
|
|
75
|
+
for (const entry of graph.entries) {
|
|
76
|
+
if (!isCandidate(entry)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const key = contractFingerprint(entry);
|
|
80
|
+
groups.set(key, [...(groups.get(key) ?? []), entry.id]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [...groups.values()]
|
|
84
|
+
.filter((trailIds) => trailIds.length > 1)
|
|
85
|
+
.map(buildDiagnostic);
|
|
86
|
+
},
|
|
87
|
+
description:
|
|
88
|
+
'Warn when public surface trails expose the same normalized contract facts.',
|
|
89
|
+
name: RULE_NAME,
|
|
90
|
+
severity: 'warn',
|
|
91
|
+
};
|