@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.41
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 +835 -13
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +27 -6
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +1570 -105
- package/src/command.ts +986 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +233 -23
- package/src/fix.ts +126 -0
- package/src/formatters.ts +78 -13
- package/src/guide.ts +245 -0
- package/src/index.ts +247 -15
- package/src/project-context.ts +446 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +79 -15
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +155 -0
- package/src/rules/draft-visible-debt.ts +82 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +438 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +583 -0
- package/src/rules/index.ts +277 -10
- package/src/rules/intent-propagation.ts +125 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +871 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +204 -0
- package/src/rules/no-sync-result-assumption.ts +1141 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +15 -8
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +149 -0
- package/src/rules/owner-projection-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +573 -0
- package/src/rules/public-internal-deep-imports.ts +456 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +167 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +633 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +653 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +354 -8
- package/src/rules/unmaterialized-activation-source.ts +85 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +89 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +160 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +237 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +224 -0
- package/src/workspaces.ts +199 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -218
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -161
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -15
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -34
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -47
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -129
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -227
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/ast.ts +0 -213
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isDraftId } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import { isDraftMarkedFile } from '../draft.js';
|
|
4
|
+
import { collectFrameworkDraftPrefixConstantOffsets } from './source/drafts.js';
|
|
5
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
|
|
6
|
+
import { findStringLiterals, offsetToLine, parse } from '@ontrails/source';
|
|
7
|
+
import type { StringLiteralMatch } from '@ontrails/source';
|
|
8
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
9
|
+
|
|
10
|
+
const messageForMissingMarker = (draftId: string): string =>
|
|
11
|
+
`Draft id "${draftId}" appears in source, but the file is not draft-marked. ` +
|
|
12
|
+
'Rename it with an _draft. prefix or a .draft. trailing segment.';
|
|
13
|
+
|
|
14
|
+
const makeDiagnostic = (
|
|
15
|
+
sourceCode: string,
|
|
16
|
+
filePath: string,
|
|
17
|
+
start: number,
|
|
18
|
+
message: string,
|
|
19
|
+
severity: WardenDiagnostic['severity']
|
|
20
|
+
): WardenDiagnostic => ({
|
|
21
|
+
filePath,
|
|
22
|
+
line: offsetToLine(sourceCode, start),
|
|
23
|
+
message,
|
|
24
|
+
rule: 'draft-file-marking',
|
|
25
|
+
severity,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const collectDraftMatches = (
|
|
29
|
+
sourceCode: string,
|
|
30
|
+
filePath: string,
|
|
31
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
32
|
+
): StringLiteralMatch[] => {
|
|
33
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
34
|
+
ast,
|
|
35
|
+
filePath
|
|
36
|
+
);
|
|
37
|
+
const lines = splitSourceLines(sourceCode);
|
|
38
|
+
return findStringLiterals(ast, (value) => isDraftId(value)).filter(
|
|
39
|
+
(match) => {
|
|
40
|
+
if (frameworkConstantOffsets.has(match.start)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start))
|
|
45
|
+
) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const draftMissingMarkerDiagnostic = (
|
|
54
|
+
sourceCode: string,
|
|
55
|
+
filePath: string,
|
|
56
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
57
|
+
): WardenDiagnostic | null => {
|
|
58
|
+
const draftMatches = collectDraftMatches(sourceCode, filePath, ast);
|
|
59
|
+
if (!draftMatches.length || isDraftMarkedFile(filePath)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [first] = draftMatches;
|
|
64
|
+
if (!first) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return makeDiagnostic(
|
|
69
|
+
sourceCode,
|
|
70
|
+
filePath,
|
|
71
|
+
first.start,
|
|
72
|
+
messageForMissingMarker(first.value),
|
|
73
|
+
'error'
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const draftMarkedWithoutIdsDiagnostic = (
|
|
78
|
+
filePath: string,
|
|
79
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
80
|
+
): WardenDiagnostic | null => {
|
|
81
|
+
// Deciding whether the file's `_draft.` marker is still warranted is a
|
|
82
|
+
// question about *all* draft ids present in source, not just the unsuppressed
|
|
83
|
+
// ones. Pragma-suppressed ids still justify a draft-marked filename — a user
|
|
84
|
+
// intentionally silencing them has not removed the draft content. We
|
|
85
|
+
// therefore filter only the framework-constant declarations (which are not
|
|
86
|
+
// draft ids at all) and bypass the pragma filter that `collectDraftMatches`
|
|
87
|
+
// applies.
|
|
88
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
89
|
+
ast,
|
|
90
|
+
filePath
|
|
91
|
+
);
|
|
92
|
+
const unsuppressedDraftIds = findStringLiterals(ast, (value) =>
|
|
93
|
+
isDraftId(value)
|
|
94
|
+
).filter((match) => !frameworkConstantOffsets.has(match.start));
|
|
95
|
+
|
|
96
|
+
if (unsuppressedDraftIds.length > 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!isDraftMarkedFile(filePath)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
filePath,
|
|
106
|
+
line: 1,
|
|
107
|
+
message:
|
|
108
|
+
'File is draft-marked but no longer contains draft ids. Remove the draft filename marker or finish the promotion cleanup.',
|
|
109
|
+
rule: 'draft-file-marking',
|
|
110
|
+
severity: 'warn',
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const collectDraftFileMarkingDiagnostics = (
|
|
115
|
+
sourceCode: string,
|
|
116
|
+
filePath: string,
|
|
117
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
118
|
+
): WardenDiagnostic[] => {
|
|
119
|
+
const missingMarkerDiagnostic = draftMissingMarkerDiagnostic(
|
|
120
|
+
sourceCode,
|
|
121
|
+
filePath,
|
|
122
|
+
ast
|
|
123
|
+
);
|
|
124
|
+
if (missingMarkerDiagnostic) {
|
|
125
|
+
return [missingMarkerDiagnostic];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const markedWithoutIdsDiagnostic = draftMarkedWithoutIdsDiagnostic(
|
|
129
|
+
filePath,
|
|
130
|
+
ast
|
|
131
|
+
);
|
|
132
|
+
if (markedWithoutIdsDiagnostic) {
|
|
133
|
+
return [markedWithoutIdsDiagnostic];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ensures files containing draft ids are visibly marked as draft-bearing files.
|
|
141
|
+
*/
|
|
142
|
+
export const draftFileMarking: WardenRule = {
|
|
143
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
144
|
+
const ast = parse(filePath, sourceCode);
|
|
145
|
+
if (!ast) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return collectDraftFileMarkingDiagnostics(sourceCode, filePath, ast);
|
|
150
|
+
},
|
|
151
|
+
description:
|
|
152
|
+
'Require draft-bearing files to use _draft.* or *.draft.* filename markers.',
|
|
153
|
+
name: 'draft-file-marking',
|
|
154
|
+
severity: 'error',
|
|
155
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isDraftId } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import { collectFrameworkDraftPrefixConstantOffsets } from './source/drafts.js';
|
|
4
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
|
|
5
|
+
import { findStringLiterals, offsetToLine, parse } from '@ontrails/source';
|
|
6
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
7
|
+
|
|
8
|
+
const createDiagnostic = (
|
|
9
|
+
sourceCode: string,
|
|
10
|
+
filePath: string,
|
|
11
|
+
match: { start: number; value: string }
|
|
12
|
+
): WardenDiagnostic => ({
|
|
13
|
+
filePath,
|
|
14
|
+
line: offsetToLine(sourceCode, match.start),
|
|
15
|
+
message:
|
|
16
|
+
`Draft id "${match.value}" is still visible debt. ` +
|
|
17
|
+
'Established surfaces, lock export, and OpenAPI generation will reject it until it is promoted.',
|
|
18
|
+
rule: 'draft-visible-debt',
|
|
19
|
+
severity: 'warn',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const isSuppressedMatch = (
|
|
23
|
+
match: { start: number },
|
|
24
|
+
sourceCode: string,
|
|
25
|
+
lines: readonly string[],
|
|
26
|
+
frameworkConstantOffsets: ReadonlySet<number>
|
|
27
|
+
): boolean =>
|
|
28
|
+
frameworkConstantOffsets.has(match.start) ||
|
|
29
|
+
hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start));
|
|
30
|
+
|
|
31
|
+
const collectDraftVisibleDebtDiagnostics = (
|
|
32
|
+
sourceCode: string,
|
|
33
|
+
filePath: string,
|
|
34
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
35
|
+
): WardenDiagnostic[] => {
|
|
36
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
37
|
+
ast,
|
|
38
|
+
filePath
|
|
39
|
+
);
|
|
40
|
+
const lines = splitSourceLines(sourceCode);
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
|
|
43
|
+
return findStringLiterals(ast, (value) => isDraftId(value)).flatMap(
|
|
44
|
+
(match) => {
|
|
45
|
+
if (
|
|
46
|
+
isSuppressedMatch(match, sourceCode, lines, frameworkConstantOffsets)
|
|
47
|
+
) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const key = `${match.value}:${String(match.start)}`;
|
|
51
|
+
if (seen.has(key)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
seen.add(key);
|
|
55
|
+
return [createDiagnostic(sourceCode, filePath, match)];
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Warns when draft ids are still present so the debt stays visible during
|
|
62
|
+
* review even when the file is correctly marked.
|
|
63
|
+
*
|
|
64
|
+
* Severity is intentionally `warn`, not `error`. The hard rejection layer for
|
|
65
|
+
* draft state leaking into established outputs is `validateEstablishedTopo` at
|
|
66
|
+
* runtime — it blocks topo compile, surface projection, and lockfile writes.
|
|
67
|
+
* This rule surfaces the debt for human reviewers without duplicating that layer.
|
|
68
|
+
*/
|
|
69
|
+
export const draftVisibleDebt: WardenRule = {
|
|
70
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
71
|
+
const ast = parse(filePath, sourceCode);
|
|
72
|
+
if (!ast) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return collectDraftVisibleDebtDiagnostics(sourceCode, filePath, ast);
|
|
77
|
+
},
|
|
78
|
+
description:
|
|
79
|
+
'Warn when draft ids remain in source so the debt stays visible during review.',
|
|
80
|
+
name: 'draft-visible-debt',
|
|
81
|
+
severity: 'warn',
|
|
82
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectAwareWardenRule,
|
|
3
|
+
ProjectContext,
|
|
4
|
+
WardenDiagnostic,
|
|
5
|
+
WardenExportedSymbolDefinition,
|
|
6
|
+
} from './types.js';
|
|
7
|
+
|
|
8
|
+
const RULE_NAME = 'duplicate-exported-symbol';
|
|
9
|
+
|
|
10
|
+
const ALLOWED_DUPLICATE_EXPORT_GROUPS: readonly {
|
|
11
|
+
readonly names: readonly string[];
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
readonly workspaceNames: readonly string[];
|
|
14
|
+
}[] = [
|
|
15
|
+
{
|
|
16
|
+
names: ['surface'],
|
|
17
|
+
reason: 'peer surface packages expose the same conventional entry point',
|
|
18
|
+
workspaceNames: [
|
|
19
|
+
'@ontrails/commander',
|
|
20
|
+
'@ontrails/hono',
|
|
21
|
+
'@ontrails/http',
|
|
22
|
+
'@ontrails/library',
|
|
23
|
+
'@ontrails/mcp',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
names: ['createApp'],
|
|
28
|
+
reason: 'HTTP native and Hono bindings share app-construction vocabulary',
|
|
29
|
+
workspaceNames: ['@ontrails/hono', '@ontrails/http'],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
names: ['CreateAppOptions'],
|
|
33
|
+
reason: 'HTTP native and Hono bindings share option vocabulary',
|
|
34
|
+
workspaceNames: ['@ontrails/hono', '@ontrails/http'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
names: ['SurfaceHttpResult'],
|
|
38
|
+
reason: 'HTTP native and Hono bindings share runtime handle vocabulary',
|
|
39
|
+
workspaceNames: ['@ontrails/hono', '@ontrails/http'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
names: ['AnyTrail', 'Field'],
|
|
43
|
+
reason: '@ontrails/cli mirrors core command-model types for CLI consumers',
|
|
44
|
+
workspaceNames: ['@ontrails/cli', '@ontrails/core'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
names: [
|
|
48
|
+
'Logger',
|
|
49
|
+
'LogFormatter',
|
|
50
|
+
'LogLevel',
|
|
51
|
+
'LogRecord',
|
|
52
|
+
'LogSink',
|
|
53
|
+
'ObserveCapabilities',
|
|
54
|
+
'ObserveConfig',
|
|
55
|
+
'ObserveInput',
|
|
56
|
+
],
|
|
57
|
+
reason: '@ontrails/observe mirrors core observability contracts',
|
|
58
|
+
workspaceNames: ['@ontrails/core', '@ontrails/observe'],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
names: [
|
|
62
|
+
'DEFAULT_MEMORY_SINK_MAX_RECORDS',
|
|
63
|
+
'MemorySinkOptions',
|
|
64
|
+
'MemoryTraceSink',
|
|
65
|
+
'createBoundedMemorySink',
|
|
66
|
+
'createMemorySink',
|
|
67
|
+
],
|
|
68
|
+
reason:
|
|
69
|
+
'@ontrails/tracing compatibility memory sink mirrors @ontrails/observe',
|
|
70
|
+
workspaceNames: ['@ontrails/observe', '@ontrails/tracing'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
names: [
|
|
74
|
+
'ActivationTraceRecordName',
|
|
75
|
+
'NOOP_SINK',
|
|
76
|
+
'SignalTraceRecordName',
|
|
77
|
+
'TRACE_CONTEXT_KEY',
|
|
78
|
+
'TraceFn',
|
|
79
|
+
'clearTraceSink',
|
|
80
|
+
'createActivationTraceRecord',
|
|
81
|
+
'createSignalTraceRecord',
|
|
82
|
+
'createTraceRecord',
|
|
83
|
+
'getTraceContext',
|
|
84
|
+
'getTraceSink',
|
|
85
|
+
'registerTraceSink',
|
|
86
|
+
'traceContextFromRecord',
|
|
87
|
+
'writeActivationTraceRecord',
|
|
88
|
+
'writeSignalTraceRecord',
|
|
89
|
+
],
|
|
90
|
+
reason:
|
|
91
|
+
'@ontrails/tracing mirrors core tracing contracts for compatibility',
|
|
92
|
+
workspaceNames: ['@ontrails/core', '@ontrails/tracing'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
names: ['TraceContext', 'TraceRecord', 'TraceSink'],
|
|
96
|
+
reason:
|
|
97
|
+
'@ontrails/observe and @ontrails/tracing mirror core trace contracts',
|
|
98
|
+
workspaceNames: [
|
|
99
|
+
'@ontrails/core',
|
|
100
|
+
'@ontrails/observe',
|
|
101
|
+
'@ontrails/tracing',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
names: ['AuthError', 'PermitError'],
|
|
106
|
+
reason:
|
|
107
|
+
'@ontrails/core owns the TrailsError subclass; @ontrails/permits owns the auth-adapter payload type',
|
|
108
|
+
workspaceNames: ['@ontrails/core', '@ontrails/permits'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
names: ['Result', 'Topo', 'TrailContextInit', 'TrailInput', 'TrailOutput'],
|
|
112
|
+
reason: '@ontrails/library mirrors core runtime types for library users',
|
|
113
|
+
workspaceNames: ['@ontrails/core', '@ontrails/library'],
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const definitionKey = (definition: WardenExportedSymbolDefinition): string =>
|
|
118
|
+
`${definition.workspaceName}:${definition.filePath}:${definition.line}:${definition.name}`;
|
|
119
|
+
|
|
120
|
+
const isAllowedDuplicateGroup = (
|
|
121
|
+
definitions: readonly WardenExportedSymbolDefinition[]
|
|
122
|
+
): boolean => {
|
|
123
|
+
const [first] = definitions;
|
|
124
|
+
if (!first) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const workspaceNames = new Set(
|
|
128
|
+
definitions.map((definition) => definition.workspaceName)
|
|
129
|
+
);
|
|
130
|
+
return ALLOWED_DUPLICATE_EXPORT_GROUPS.some(
|
|
131
|
+
(group) =>
|
|
132
|
+
group.names.includes(first.name) &&
|
|
133
|
+
[...workspaceNames].every((name) => group.workspaceNames.includes(name))
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const duplicateGroupsForFile = (
|
|
138
|
+
context: ProjectContext,
|
|
139
|
+
filePath: string
|
|
140
|
+
): readonly {
|
|
141
|
+
readonly current: WardenExportedSymbolDefinition;
|
|
142
|
+
readonly definitions: readonly WardenExportedSymbolDefinition[];
|
|
143
|
+
}[] => {
|
|
144
|
+
const groups: {
|
|
145
|
+
current: WardenExportedSymbolDefinition;
|
|
146
|
+
definitions: readonly WardenExportedSymbolDefinition[];
|
|
147
|
+
}[] = [];
|
|
148
|
+
|
|
149
|
+
for (const definitions of context.exportedSymbolDefinitionsByName?.values() ??
|
|
150
|
+
[]) {
|
|
151
|
+
const workspaceNames = new Set(
|
|
152
|
+
definitions.map((definition) => definition.workspaceName)
|
|
153
|
+
);
|
|
154
|
+
if (workspaceNames.size < 2) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (isAllowedDuplicateGroup(definitions)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const definition of definitions) {
|
|
162
|
+
if (definition.filePath === filePath) {
|
|
163
|
+
groups.push({ current: definition, definitions });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return groups;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const formatOtherDefinitions = (
|
|
172
|
+
current: WardenExportedSymbolDefinition,
|
|
173
|
+
definitions: readonly WardenExportedSymbolDefinition[]
|
|
174
|
+
): string =>
|
|
175
|
+
definitions
|
|
176
|
+
.filter(
|
|
177
|
+
(definition) => definitionKey(definition) !== definitionKey(current)
|
|
178
|
+
)
|
|
179
|
+
.map(
|
|
180
|
+
(definition) =>
|
|
181
|
+
`${definition.workspaceName} (${definition.filePath}:${definition.line})`
|
|
182
|
+
)
|
|
183
|
+
.join(', ');
|
|
184
|
+
|
|
185
|
+
export const duplicateExportedSymbol: ProjectAwareWardenRule = {
|
|
186
|
+
check(): readonly WardenDiagnostic[] {
|
|
187
|
+
return [];
|
|
188
|
+
},
|
|
189
|
+
checkWithContext(
|
|
190
|
+
_sourceCode: string,
|
|
191
|
+
filePath: string,
|
|
192
|
+
context: ProjectContext
|
|
193
|
+
): readonly WardenDiagnostic[] {
|
|
194
|
+
return duplicateGroupsForFile(context, filePath).map(
|
|
195
|
+
({ current, definitions }) => ({
|
|
196
|
+
filePath,
|
|
197
|
+
line: current.line,
|
|
198
|
+
message: `Exported symbol "${current.name}" is defined by ${current.workspaceName} and also by ${formatOtherDefinitions(
|
|
199
|
+
current,
|
|
200
|
+
definitions
|
|
201
|
+
)}. Keep one package as the owner, rename one side, or document a deliberate ownership mirror before exporting both symbols.`,
|
|
202
|
+
rule: RULE_NAME,
|
|
203
|
+
severity: 'warn',
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
description:
|
|
208
|
+
'Warn when the same exported symbol is defined by multiple first-party packages.',
|
|
209
|
+
name: RULE_NAME,
|
|
210
|
+
severity: 'warn',
|
|
211
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { deriveTopoGraph } from '@ontrails/topography';
|
|
2
|
+
import type { TopoGraph, TopoGraphEntry } from '@ontrails/topography';
|
|
3
|
+
|
|
4
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
5
|
+
|
|
6
|
+
const RULE_NAME = 'duplicate-public-contract';
|
|
7
|
+
const TOPO_FILE = '<topo>';
|
|
8
|
+
const INVERSE_OPERATION_PAIRS = new Map([
|
|
9
|
+
['archive', 'restore'],
|
|
10
|
+
['disable', 'enable'],
|
|
11
|
+
['pause', 'resume'],
|
|
12
|
+
['star', 'unstar'],
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const resolveGraph = (
|
|
16
|
+
topo: Parameters<TopoAwareWardenRule['checkTopo']>[0],
|
|
17
|
+
graph: TopoGraph | undefined
|
|
18
|
+
): TopoGraph => graph ?? deriveTopoGraph(topo);
|
|
19
|
+
|
|
20
|
+
const canonicalize = (value: unknown): unknown => {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map(canonicalize);
|
|
23
|
+
}
|
|
24
|
+
if (value && typeof value === 'object') {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(value as Record<string, unknown>)
|
|
27
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
28
|
+
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
29
|
+
.map(([key, entryValue]) => [key, canonicalize(entryValue)])
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const contractFingerprint = (entry: TopoGraphEntry): string =>
|
|
36
|
+
JSON.stringify(
|
|
37
|
+
canonicalize({
|
|
38
|
+
composes: entry.composes,
|
|
39
|
+
detours: entry.detours,
|
|
40
|
+
dryRunCapable: entry.dryRunCapable,
|
|
41
|
+
entities: entry.entities,
|
|
42
|
+
fires: entry.fires,
|
|
43
|
+
idempotent: entry.idempotent,
|
|
44
|
+
input: entry.input,
|
|
45
|
+
intent: entry.intent,
|
|
46
|
+
meta: entry.meta,
|
|
47
|
+
on: entry.on,
|
|
48
|
+
output: entry.output,
|
|
49
|
+
permit: entry.permit,
|
|
50
|
+
resources: entry.resources,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const isCandidate = (entry: TopoGraphEntry): boolean =>
|
|
55
|
+
entry.kind === 'trail' &&
|
|
56
|
+
!entry.id.startsWith('warden.rule.') &&
|
|
57
|
+
entry.deprecated !== true &&
|
|
58
|
+
entry.meta?.['internal'] !== true &&
|
|
59
|
+
entry.input !== undefined &&
|
|
60
|
+
entry.output !== undefined &&
|
|
61
|
+
Boolean(entry.cli || entry.surfaces.length > 0);
|
|
62
|
+
|
|
63
|
+
const renderTrailIds = (trailIds: readonly string[]): string =>
|
|
64
|
+
trailIds
|
|
65
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
66
|
+
.map((trailId) => `"${trailId}"`)
|
|
67
|
+
.join(', ');
|
|
68
|
+
|
|
69
|
+
const splitOperation = (
|
|
70
|
+
trailId: string
|
|
71
|
+
): { readonly scope: string; readonly operation: string } | undefined => {
|
|
72
|
+
const separatorIndex = trailId.lastIndexOf('.');
|
|
73
|
+
|
|
74
|
+
if (separatorIndex <= 0 || separatorIndex === trailId.length - 1) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
operation: trailId.slice(separatorIndex + 1),
|
|
80
|
+
scope: trailId.slice(0, separatorIndex),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const areInverseOperations = (left: string, right: string): boolean =>
|
|
85
|
+
INVERSE_OPERATION_PAIRS.get(left) === right ||
|
|
86
|
+
INVERSE_OPERATION_PAIRS.get(right) === left;
|
|
87
|
+
|
|
88
|
+
const isAcceptedInverseOperationPair = (
|
|
89
|
+
trailIds: readonly string[]
|
|
90
|
+
): boolean => {
|
|
91
|
+
if (trailIds.length !== 2) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const [left, right] = trailIds.map(splitOperation);
|
|
96
|
+
|
|
97
|
+
return Boolean(
|
|
98
|
+
left &&
|
|
99
|
+
right &&
|
|
100
|
+
left.scope === right.scope &&
|
|
101
|
+
areInverseOperations(left.operation, right.operation)
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const buildDiagnostic = (trailIds: readonly string[]): WardenDiagnostic => ({
|
|
106
|
+
filePath: TOPO_FILE,
|
|
107
|
+
line: 1,
|
|
108
|
+
message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources, entities, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.`,
|
|
109
|
+
rule: RULE_NAME,
|
|
110
|
+
severity: 'warn',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const duplicatePublicContract: TopoAwareWardenRule = {
|
|
114
|
+
checkTopo(topo, context) {
|
|
115
|
+
const graph = resolveGraph(topo, context?.graph);
|
|
116
|
+
const groups = new Map<string, string[]>();
|
|
117
|
+
|
|
118
|
+
for (const entry of graph.entries) {
|
|
119
|
+
if (!isCandidate(entry)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const key = contractFingerprint(entry);
|
|
123
|
+
groups.set(key, [...(groups.get(key) ?? []), entry.id]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return [...groups.values()]
|
|
127
|
+
.filter(
|
|
128
|
+
(trailIds) =>
|
|
129
|
+
trailIds.length > 1 && !isAcceptedInverseOperationPair(trailIds)
|
|
130
|
+
)
|
|
131
|
+
.map(buildDiagnostic);
|
|
132
|
+
},
|
|
133
|
+
description:
|
|
134
|
+
'Warn when public surface trails expose the same normalized contract facts.',
|
|
135
|
+
name: RULE_NAME,
|
|
136
|
+
severity: 'warn',
|
|
137
|
+
};
|