@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.22
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 +508 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -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 +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- 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 +389 -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 +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -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 +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -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 +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -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/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/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/error-mapping-completeness.trail.ts +29 -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 +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -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 +194 -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/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 +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -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 -231
- 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 -75
- 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 -188
- 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,517 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractStringLiteral,
|
|
6
|
+
hasIgnoreCommentOnLine,
|
|
7
|
+
offsetToLine,
|
|
8
|
+
parse,
|
|
9
|
+
splitSourceLines,
|
|
10
|
+
walk,
|
|
11
|
+
} from './ast.js';
|
|
12
|
+
import type { AstNode } from './ast.js';
|
|
13
|
+
import type {
|
|
14
|
+
ProjectAwareWardenRule,
|
|
15
|
+
ProjectContext,
|
|
16
|
+
WardenDiagnostic,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
import type { WardenImportResolution } from '../resolve.js';
|
|
19
|
+
import type { WardenPublicWorkspace } from '../workspaces.js';
|
|
20
|
+
|
|
21
|
+
const RULE_NAME = 'public-internal-deep-imports';
|
|
22
|
+
const ONTRAILS_SPECIFIER_PATTERN = /^(@ontrails\/[^/]+)(?:\/(.+))?$/;
|
|
23
|
+
const ROOT_BARREL_INTERNAL_RE_EXPORT_ALLOWLIST = new Set([
|
|
24
|
+
'@ontrails/tracing:./internal/dev-state.js',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
interface ReExportSite {
|
|
28
|
+
readonly importSource: string;
|
|
29
|
+
readonly line: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
33
|
+
|
|
34
|
+
const normalizeRealPath = (path: string): string => {
|
|
35
|
+
try {
|
|
36
|
+
return normalizePath(realpathSync(path));
|
|
37
|
+
} catch {
|
|
38
|
+
return normalizePath(resolve(path));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const pathIsInside = (filePath: string, rootDir: string): boolean => {
|
|
43
|
+
const absoluteFilePath = normalizeRealPath(filePath);
|
|
44
|
+
const absoluteRootDir = normalizeRealPath(rootDir);
|
|
45
|
+
return (
|
|
46
|
+
absoluteFilePath === absoluteRootDir ||
|
|
47
|
+
absoluteFilePath.startsWith(`${absoluteRootDir}/`)
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const packageNameFromSpecifier = (specifier: string): string | undefined => {
|
|
52
|
+
const match = ONTRAILS_SPECIFIER_PATTERN.exec(specifier);
|
|
53
|
+
return match?.[1];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const specifierHasSubpath = (specifier: string): boolean =>
|
|
57
|
+
Boolean(ONTRAILS_SPECIFIER_PATTERN.exec(specifier)?.[2]);
|
|
58
|
+
|
|
59
|
+
const sourcePackageNameForFile = (
|
|
60
|
+
filePath: string,
|
|
61
|
+
workspaces: ReadonlyMap<string, WardenPublicWorkspace>
|
|
62
|
+
): string | undefined => {
|
|
63
|
+
for (const workspace of workspaces.values()) {
|
|
64
|
+
if (pathIsInside(filePath, workspace.rootDir)) {
|
|
65
|
+
return workspace.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const importResolutionsForFile = (
|
|
72
|
+
context: ProjectContext,
|
|
73
|
+
filePath: string
|
|
74
|
+
): readonly WardenImportResolution[] =>
|
|
75
|
+
context.importResolutionsByFile?.get(filePath) ?? [];
|
|
76
|
+
|
|
77
|
+
const documentedImportResolutionsForFile = (
|
|
78
|
+
context: ProjectContext,
|
|
79
|
+
filePath: string
|
|
80
|
+
): readonly WardenImportResolution[] =>
|
|
81
|
+
context.documentedImportResolutionsByFile?.get(filePath) ?? [];
|
|
82
|
+
|
|
83
|
+
const diagnosticMessage = (
|
|
84
|
+
resolution: WardenImportResolution,
|
|
85
|
+
packageName: string
|
|
86
|
+
): string => {
|
|
87
|
+
if (resolution.errorKind === 'not-found') {
|
|
88
|
+
return `@ontrails specifier "${resolution.importSource}" could not be resolved from the public workspace package ${packageName}. Use the package root, an exported subpath, or the package binary when the package is bin-only.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
`@ontrails specifier "${resolution.importSource}" is not exported by ${packageName}. ` +
|
|
93
|
+
'Use the package root or an exported subpath; if the API is missing, add an owner export follow-up instead of importing internals.'
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const shouldReportResolution = (
|
|
98
|
+
resolution: WardenImportResolution,
|
|
99
|
+
workspace: WardenPublicWorkspace,
|
|
100
|
+
sourcePackageName: string | undefined,
|
|
101
|
+
isDocumentation: boolean
|
|
102
|
+
): boolean => {
|
|
103
|
+
if (!isDocumentation && sourcePackageName === workspace.name) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (resolution.errorKind === 'package-path-not-exported') {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (resolution.errorKind === 'not-found') {
|
|
112
|
+
return isDocumentation
|
|
113
|
+
? specifierHasSubpath(resolution.importSource)
|
|
114
|
+
: resolution.importSource === workspace.name ||
|
|
115
|
+
specifierHasSubpath(resolution.importSource);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isDocumentation && specifierHasSubpath(resolution.importSource)) {
|
|
119
|
+
return !resolution.usesPublicExport;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const diagnosticsForResolutions = ({
|
|
126
|
+
context,
|
|
127
|
+
filePath,
|
|
128
|
+
isDocumentation,
|
|
129
|
+
sourceCode,
|
|
130
|
+
}: {
|
|
131
|
+
readonly context: ProjectContext;
|
|
132
|
+
readonly filePath: string;
|
|
133
|
+
readonly isDocumentation: boolean;
|
|
134
|
+
readonly sourceCode: string;
|
|
135
|
+
}): readonly WardenDiagnostic[] => {
|
|
136
|
+
const workspaces = context.publicWorkspaces;
|
|
137
|
+
if (!workspaces || workspaces.size === 0) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sourcePackageName = sourcePackageNameForFile(filePath, workspaces);
|
|
142
|
+
const lines = splitSourceLines(sourceCode);
|
|
143
|
+
const resolutions = isDocumentation
|
|
144
|
+
? documentedImportResolutionsForFile(context, filePath)
|
|
145
|
+
: importResolutionsForFile(context, filePath);
|
|
146
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
147
|
+
|
|
148
|
+
for (const resolution of resolutions) {
|
|
149
|
+
if (hasIgnoreCommentOnLine(lines, resolution.line)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const packageName =
|
|
154
|
+
resolution.packageName ??
|
|
155
|
+
packageNameFromSpecifier(resolution.importSource);
|
|
156
|
+
const workspace = packageName ? workspaces.get(packageName) : undefined;
|
|
157
|
+
if (!packageName || !workspace) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
shouldReportResolution(
|
|
163
|
+
resolution,
|
|
164
|
+
workspace,
|
|
165
|
+
sourcePackageName,
|
|
166
|
+
isDocumentation
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
diagnostics.push({
|
|
170
|
+
filePath,
|
|
171
|
+
line: resolution.line,
|
|
172
|
+
message: diagnosticMessage(resolution, packageName),
|
|
173
|
+
rule: RULE_NAME,
|
|
174
|
+
severity: 'error',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return diagnostics;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const isRootBarrel = (
|
|
183
|
+
filePath: string,
|
|
184
|
+
workspace: WardenPublicWorkspace
|
|
185
|
+
): boolean => {
|
|
186
|
+
const rootExportTarget = workspace.exportTargets?.[workspace.name];
|
|
187
|
+
if (rootExportTarget) {
|
|
188
|
+
return normalizeRealPath(filePath) === rootExportTarget;
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
normalizeRealPath(filePath) ===
|
|
192
|
+
normalizeRealPath(resolve(workspace.rootDir, 'src/index.ts'))
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const rootBarrelWorkspace = (
|
|
197
|
+
filePath: string,
|
|
198
|
+
workspaces: ReadonlyMap<string, WardenPublicWorkspace>
|
|
199
|
+
): WardenPublicWorkspace | undefined => {
|
|
200
|
+
for (const workspace of workspaces.values()) {
|
|
201
|
+
if (isRootBarrel(filePath, workspace)) {
|
|
202
|
+
return workspace;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const collectReExportSites = (
|
|
209
|
+
sourceCode: string,
|
|
210
|
+
filePath: string
|
|
211
|
+
): readonly ReExportSite[] => {
|
|
212
|
+
const ast = parse(filePath, sourceCode);
|
|
213
|
+
if (!ast) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const sites: ReExportSite[] = [];
|
|
218
|
+
walk(ast, (node) => {
|
|
219
|
+
if (
|
|
220
|
+
node.type !== 'ExportNamedDeclaration' &&
|
|
221
|
+
node.type !== 'ExportAllDeclaration'
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { source } = node as unknown as { source?: AstNode };
|
|
227
|
+
const value = extractStringLiteral(source);
|
|
228
|
+
if (typeof value !== 'string') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
sites.push({
|
|
233
|
+
importSource: value,
|
|
234
|
+
line: offsetToLine(sourceCode, node.start),
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
return sites;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const reExportResolution = (
|
|
241
|
+
resolutions: readonly WardenImportResolution[],
|
|
242
|
+
site: ReExportSite
|
|
243
|
+
): WardenImportResolution | undefined =>
|
|
244
|
+
resolutions.find(
|
|
245
|
+
(resolution) =>
|
|
246
|
+
resolution.importSource === site.importSource &&
|
|
247
|
+
resolution.line === site.line
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const isAllowlistedRootBarrelInternalExport = (
|
|
251
|
+
workspace: WardenPublicWorkspace,
|
|
252
|
+
importSource: string
|
|
253
|
+
): boolean =>
|
|
254
|
+
ROOT_BARREL_INTERNAL_RE_EXPORT_ALLOWLIST.has(
|
|
255
|
+
`${workspace.name}:${importSource}`
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const rootBarrelDiagnostics = (
|
|
259
|
+
sourceCode: string,
|
|
260
|
+
filePath: string,
|
|
261
|
+
context: ProjectContext
|
|
262
|
+
): readonly WardenDiagnostic[] => {
|
|
263
|
+
const workspaces = context.publicWorkspaces;
|
|
264
|
+
if (!workspaces || workspaces.size === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const workspace = rootBarrelWorkspace(filePath, workspaces);
|
|
269
|
+
if (!workspace) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const resolutions = importResolutionsForFile(context, filePath);
|
|
274
|
+
const lines = splitSourceLines(sourceCode);
|
|
275
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
276
|
+
for (const site of collectReExportSites(sourceCode, filePath)) {
|
|
277
|
+
if (isAllowlistedRootBarrelInternalExport(workspace, site.importSource)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (hasIgnoreCommentOnLine(lines, site.line)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const resolution = reExportResolution(resolutions, site);
|
|
285
|
+
if (!resolution?.isInternalTarget) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
diagnostics.push({
|
|
290
|
+
filePath,
|
|
291
|
+
line: site.line,
|
|
292
|
+
message:
|
|
293
|
+
`${workspace.name} root barrel re-exports internal target "${site.importSource}". ` +
|
|
294
|
+
'Move the symbol behind an explicit public module or keep it private to the package.',
|
|
295
|
+
rule: RULE_NAME,
|
|
296
|
+
severity: 'error',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return diagnostics;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const stripLeadingDotSlash = (path: string): string =>
|
|
304
|
+
path.startsWith('./') ? path.slice(2) : path;
|
|
305
|
+
|
|
306
|
+
const escapeRegExp = (value: string): string =>
|
|
307
|
+
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
|
+
|
|
309
|
+
const wildcardPatternSource = (pattern: string): string => {
|
|
310
|
+
let regexSource = '';
|
|
311
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
312
|
+
const char = pattern[index];
|
|
313
|
+
if (char === '*') {
|
|
314
|
+
if (pattern[index + 1] === '*') {
|
|
315
|
+
regexSource += '.*';
|
|
316
|
+
index += 1;
|
|
317
|
+
} else {
|
|
318
|
+
regexSource += '[^/]*';
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
regexSource += escapeRegExp(char ?? '');
|
|
323
|
+
}
|
|
324
|
+
return regexSource;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const segmentWildcardPatternCovers = (
|
|
328
|
+
filePath: string,
|
|
329
|
+
pattern: string
|
|
330
|
+
): boolean => new RegExp(`^${wildcardPatternSource(pattern)}$`).test(filePath);
|
|
331
|
+
|
|
332
|
+
const deepWildcardPatternCovers = (
|
|
333
|
+
filePath: string,
|
|
334
|
+
pattern: string,
|
|
335
|
+
globIndex: number
|
|
336
|
+
): boolean => {
|
|
337
|
+
const prefix = pattern.slice(0, globIndex + 1);
|
|
338
|
+
if (!filePath.startsWith(prefix)) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const suffixPattern = pattern.slice(globIndex + '/**/'.length);
|
|
342
|
+
if (suffixPattern.length === 0) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
const remainingPath = filePath.slice(prefix.length);
|
|
346
|
+
const regexSource = `^(?:.*/)?${wildcardPatternSource(suffixPattern)}$`;
|
|
347
|
+
return new RegExp(regexSource).test(remainingPath);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const filePatternCovers = (filePath: string, pattern: string): boolean => {
|
|
351
|
+
const normalizedFilePath = normalizePath(stripLeadingDotSlash(filePath));
|
|
352
|
+
const normalizedPattern = normalizePath(stripLeadingDotSlash(pattern));
|
|
353
|
+
if (normalizedPattern.startsWith('!')) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (normalizedPattern === normalizedFilePath) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
if (normalizedPattern === '**') {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
363
|
+
const prefix = normalizedPattern.slice(0, -2);
|
|
364
|
+
return normalizedFilePath.startsWith(prefix);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const globIndex = normalizedPattern.indexOf('/**/');
|
|
368
|
+
if (globIndex === -1) {
|
|
369
|
+
if (normalizedPattern.includes('*')) {
|
|
370
|
+
return segmentWildcardPatternCovers(
|
|
371
|
+
normalizedFilePath,
|
|
372
|
+
normalizedPattern
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return normalizedFilePath.startsWith(`${normalizedPattern}/`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return deepWildcardPatternCovers(
|
|
379
|
+
normalizedFilePath,
|
|
380
|
+
normalizedPattern,
|
|
381
|
+
globIndex
|
|
382
|
+
);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const filesCoverTarget = (
|
|
386
|
+
files: readonly string[] | undefined,
|
|
387
|
+
target: string
|
|
388
|
+
): boolean => {
|
|
389
|
+
if (!files || files.length === 0) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
let covered = false;
|
|
393
|
+
for (const pattern of files) {
|
|
394
|
+
const normalizedPattern = normalizePath(stripLeadingDotSlash(pattern));
|
|
395
|
+
if (normalizedPattern.startsWith('!')) {
|
|
396
|
+
if (filePatternCovers(target, normalizedPattern.slice(1))) {
|
|
397
|
+
covered = false;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (filePatternCovers(target, normalizedPattern)) {
|
|
402
|
+
covered = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return covered;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const workspaceForPackageJson = (
|
|
409
|
+
filePath: string,
|
|
410
|
+
workspaces: ReadonlyMap<string, WardenPublicWorkspace>
|
|
411
|
+
): WardenPublicWorkspace | undefined => {
|
|
412
|
+
const normalizedFilePath = normalizeRealPath(filePath);
|
|
413
|
+
for (const workspace of workspaces.values()) {
|
|
414
|
+
if (normalizePath(workspace.packageJsonPath) === normalizedFilePath) {
|
|
415
|
+
return workspace;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const binSurfaceDiagnostics = (
|
|
422
|
+
filePath: string,
|
|
423
|
+
context: ProjectContext
|
|
424
|
+
): readonly WardenDiagnostic[] => {
|
|
425
|
+
if (
|
|
426
|
+
!filePath.endsWith(`${sep}package.json`) &&
|
|
427
|
+
!filePath.endsWith('/package.json')
|
|
428
|
+
) {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const workspaces = context.publicWorkspaces;
|
|
433
|
+
if (!workspaces || workspaces.size === 0) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const workspace = workspaceForPackageJson(filePath, workspaces);
|
|
438
|
+
if (!workspace) {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
443
|
+
const binEntries = Object.entries(workspace.bin ?? {});
|
|
444
|
+
if (!workspace.hasExports && binEntries.length === 0) {
|
|
445
|
+
diagnostics.push({
|
|
446
|
+
filePath,
|
|
447
|
+
line: 1,
|
|
448
|
+
message:
|
|
449
|
+
`Public workspace ${workspace.name} has no exports map and no bin surface. ` +
|
|
450
|
+
'Add an exports map for library APIs or declare the package binary surface explicitly.',
|
|
451
|
+
rule: RULE_NAME,
|
|
452
|
+
severity: 'error',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const [binName, target] of binEntries) {
|
|
457
|
+
const targetPath = resolve(workspace.rootDir, target);
|
|
458
|
+
if (!existsSync(targetPath)) {
|
|
459
|
+
diagnostics.push({
|
|
460
|
+
filePath,
|
|
461
|
+
line: 1,
|
|
462
|
+
message: `Bin "${binName}" for ${workspace.name} points at missing file ${target}.`,
|
|
463
|
+
rule: RULE_NAME,
|
|
464
|
+
severity: 'error',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!filesCoverTarget(workspace.files, target)) {
|
|
469
|
+
diagnostics.push({
|
|
470
|
+
filePath,
|
|
471
|
+
line: 1,
|
|
472
|
+
message:
|
|
473
|
+
`Bin "${binName}" for ${workspace.name} points at ${target}, ` +
|
|
474
|
+
'but the package files list does not include that target.',
|
|
475
|
+
rule: RULE_NAME,
|
|
476
|
+
severity: 'error',
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return diagnostics;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
export const publicInternalDeepImports: ProjectAwareWardenRule = {
|
|
485
|
+
check(): readonly WardenDiagnostic[] {
|
|
486
|
+
return [];
|
|
487
|
+
},
|
|
488
|
+
checkWithContext(
|
|
489
|
+
sourceCode: string,
|
|
490
|
+
filePath: string,
|
|
491
|
+
context: ProjectContext
|
|
492
|
+
): readonly WardenDiagnostic[] {
|
|
493
|
+
if (filePath.endsWith('.md')) {
|
|
494
|
+
return diagnosticsForResolutions({
|
|
495
|
+
context,
|
|
496
|
+
filePath,
|
|
497
|
+
isDocumentation: true,
|
|
498
|
+
sourceCode,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return [
|
|
503
|
+
...diagnosticsForResolutions({
|
|
504
|
+
context,
|
|
505
|
+
filePath,
|
|
506
|
+
isDocumentation: false,
|
|
507
|
+
sourceCode,
|
|
508
|
+
}),
|
|
509
|
+
...rootBarrelDiagnostics(sourceCode, filePath, context),
|
|
510
|
+
...binSurfaceDiagnostics(filePath, context),
|
|
511
|
+
];
|
|
512
|
+
},
|
|
513
|
+
description:
|
|
514
|
+
'Keep @ontrails/* imports, docs specifiers, root barrels, and bin-only surfaces aligned with public package exports.',
|
|
515
|
+
name: RULE_NAME,
|
|
516
|
+
severity: 'error',
|
|
517
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { filterSurfaceTrails } from '@ontrails/core';
|
|
2
|
+
import type { AnyTrail, Topo } from '@ontrails/core';
|
|
3
|
+
|
|
4
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
5
|
+
|
|
6
|
+
const RULE_NAME = 'public-output-schema';
|
|
7
|
+
const TOPO_FILE = '<topo>';
|
|
8
|
+
|
|
9
|
+
const diagnosticForTrail = (trail: AnyTrail): WardenDiagnostic => ({
|
|
10
|
+
filePath: TOPO_FILE,
|
|
11
|
+
line: 1,
|
|
12
|
+
message:
|
|
13
|
+
`Trail "${trail.id}" is visible to public MCP/HTTP surface projection but does not declare an output schema. ` +
|
|
14
|
+
'Add an explicit output schema, or mark the trail visibility as internal if it is composition-only.',
|
|
15
|
+
rule: RULE_NAME,
|
|
16
|
+
severity: 'error',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const publicOutputSchema: TopoAwareWardenRule = {
|
|
20
|
+
checkTopo(topo: Topo): readonly WardenDiagnostic[] {
|
|
21
|
+
return filterSurfaceTrails(topo.list()).flatMap((trail) =>
|
|
22
|
+
trail.output === undefined ? [diagnosticForTrail(trail)] : []
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
description:
|
|
26
|
+
'Require public MCP/HTTP surface-eligible trails to declare output schemas.',
|
|
27
|
+
name: RULE_NAME,
|
|
28
|
+
severity: 'error',
|
|
29
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { filterSurfaceTrails, zodToJsonSchema } from '@ontrails/core';
|
|
2
|
+
import type { AnyTrail, Topo } from '@ontrails/core';
|
|
3
|
+
|
|
4
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
5
|
+
|
|
6
|
+
const RULE_NAME = 'public-union-output-discriminants';
|
|
7
|
+
|
|
8
|
+
type JsonSchema = Readonly<Record<string, unknown>>;
|
|
9
|
+
|
|
10
|
+
interface ObjectBranch {
|
|
11
|
+
readonly properties: Readonly<Record<string, JsonSchema>>;
|
|
12
|
+
readonly required: ReadonlySet<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
|
+
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
17
|
+
|
|
18
|
+
const isJsonSchema = (value: unknown): value is JsonSchema => isRecord(value);
|
|
19
|
+
|
|
20
|
+
const schemaForTrailOutput = (trail: AnyTrail): JsonSchema | undefined => {
|
|
21
|
+
if (!trail.output) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return zodToJsonSchema(trail.output);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const readProperties = (
|
|
32
|
+
schema: JsonSchema
|
|
33
|
+
): Readonly<Record<string, JsonSchema>> | undefined => {
|
|
34
|
+
const { properties } = schema;
|
|
35
|
+
if (!isRecord(properties)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const entries = Object.entries(properties);
|
|
39
|
+
if (!entries.every(([, value]) => isJsonSchema(value))) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return properties as Readonly<Record<string, JsonSchema>>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const readRequired = (schema: JsonSchema): ReadonlySet<string> => {
|
|
46
|
+
const { required } = schema;
|
|
47
|
+
if (!Array.isArray(required)) {
|
|
48
|
+
return new Set();
|
|
49
|
+
}
|
|
50
|
+
return new Set(
|
|
51
|
+
required.filter((entry): entry is string => typeof entry === 'string')
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const objectBranchFromSchema = (schema: unknown): ObjectBranch | undefined => {
|
|
56
|
+
if (!isJsonSchema(schema) || schema['type'] !== 'object') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const properties = readProperties(schema);
|
|
60
|
+
if (!properties) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return { properties, required: readRequired(schema) };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const objectBranchesFromAnyOf = (
|
|
67
|
+
schema: JsonSchema
|
|
68
|
+
): readonly ObjectBranch[] | undefined => {
|
|
69
|
+
const { anyOf } = schema;
|
|
70
|
+
if (!Array.isArray(anyOf) || anyOf.length < 2) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const branches = anyOf.flatMap((branch) => {
|
|
74
|
+
const objectBranch = objectBranchFromSchema(branch);
|
|
75
|
+
return objectBranch ? [objectBranch] : [];
|
|
76
|
+
});
|
|
77
|
+
return branches.length >= 2 ? branches : undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const hasConst = (schema: JsonSchema): boolean =>
|
|
81
|
+
Object.hasOwn(schema, 'const');
|
|
82
|
+
|
|
83
|
+
const constValue = (schema: JsonSchema): unknown => schema['const'];
|
|
84
|
+
|
|
85
|
+
const constKey = (value: unknown): string => JSON.stringify(value);
|
|
86
|
+
|
|
87
|
+
const branchLiteralForKey = (
|
|
88
|
+
branch: ObjectBranch,
|
|
89
|
+
key: string
|
|
90
|
+
): unknown | undefined => {
|
|
91
|
+
if (!branch.required.has(key)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const property = branch.properties[key];
|
|
95
|
+
return property && hasConst(property) ? constValue(property) : undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const hasRequiredLiteralDiscriminant = (
|
|
99
|
+
branches: readonly ObjectBranch[]
|
|
100
|
+
): boolean => {
|
|
101
|
+
const [first] = branches;
|
|
102
|
+
if (!first) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Object.keys(first.properties).some((key) => {
|
|
107
|
+
const values = branches.map((branch) => branchLiteralForKey(branch, key));
|
|
108
|
+
return (
|
|
109
|
+
values.every((value) => value !== undefined) &&
|
|
110
|
+
new Set(values.map(constKey)).size === branches.length
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const diagnosticForTrail = (trail: AnyTrail): WardenDiagnostic => ({
|
|
116
|
+
filePath: '<topo>',
|
|
117
|
+
line: 1,
|
|
118
|
+
message:
|
|
119
|
+
`Trail "${trail.id}" exposes a public output anyOf with object variants but no required literal discriminator. ` +
|
|
120
|
+
'Add a shared z.literal(...) field or z.discriminatedUnion(...) so surfaces and agents can select the output branch.',
|
|
121
|
+
rule: RULE_NAME,
|
|
122
|
+
severity: 'error',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const diagnoseTrail = (trail: AnyTrail): WardenDiagnostic | undefined => {
|
|
126
|
+
const schema = schemaForTrailOutput(trail);
|
|
127
|
+
if (!schema) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const objectBranches = objectBranchesFromAnyOf(schema);
|
|
131
|
+
if (!objectBranches) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return hasRequiredLiteralDiscriminant(objectBranches)
|
|
135
|
+
? undefined
|
|
136
|
+
: diagnosticForTrail(trail);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const publicUnionOutputDiscriminants: TopoAwareWardenRule = {
|
|
140
|
+
checkTopo(topo: Topo): readonly WardenDiagnostic[] {
|
|
141
|
+
return filterSurfaceTrails(topo.list()).flatMap((trail) => {
|
|
142
|
+
const diagnostic = diagnoseTrail(trail);
|
|
143
|
+
return diagnostic ? [diagnostic] : [];
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
description:
|
|
147
|
+
'Require public trail output object unions to expose a required literal discriminator.',
|
|
148
|
+
name: RULE_NAME,
|
|
149
|
+
severity: 'error',
|
|
150
|
+
};
|