@ontrails/warden 1.0.0-beta.39 → 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 +125 -0
- package/README.md +5 -5
- package/package.json +10 -12
- package/src/cli.ts +49 -44
- package/src/command.ts +2 -2
- package/src/drift.ts +4 -3
- package/src/index.ts +3 -2
- package/src/project-context.ts +3 -3
- package/src/resolve.ts +2 -2
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +12 -16
- package/src/rules/cli-command-route-coherence.ts +1 -1
- package/src/rules/composes-declarations.ts +15 -13
- package/src/rules/context-no-surface-types.ts +4 -3
- package/src/rules/dead-internal-trail.ts +3 -3
- package/src/rules/dead-public-trail.ts +3 -3
- package/src/rules/draft-file-marking.ts +4 -9
- package/src/rules/draft-visible-debt.ts +3 -8
- package/src/rules/duplicate-public-contract.ts +50 -5
- package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +2 -2
- package/src/rules/example-valid.ts +84 -84
- package/src/rules/fires-declarations.ts +27 -22
- package/src/rules/governed-symbol-residue.ts +339 -32
- package/src/rules/implementation-returns-result.ts +8 -8
- package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
- package/src/rules/incomplete-crud.ts +42 -40
- package/src/rules/index.ts +9 -6
- package/src/rules/intent-propagation.ts +3 -5
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/library-projection-coherence.ts +2 -2
- package/src/rules/metadata.ts +37 -15
- package/src/rules/missing-reconcile.ts +3 -4
- package/src/rules/missing-visibility.ts +2 -1
- package/src/rules/no-destructured-compose.ts +14 -12
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +2 -2
- package/src/rules/no-native-error-result.ts +2 -2
- package/src/rules/no-redundant-result-error-wrap.ts +10 -10
- package/src/rules/no-sync-result-assumption.ts +84 -78
- package/src/rules/no-throw-in-detour-recover.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +13 -7
- package/src/rules/no-top-level-surface.ts +2 -2
- package/src/rules/on-references-exist.ts +3 -3
- package/src/rules/orphaned-signal.ts +4 -5
- package/src/rules/owner-projection-parity.ts +2 -2
- package/src/rules/public-export-example-coverage.ts +10 -4
- package/src/rules/public-internal-deep-imports.ts +2 -3
- package/src/rules/read-intent-fires.ts +4 -3
- package/src/rules/reference-exists.ts +20 -21
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/resolved-import-boundary.ts +1 -1
- package/src/rules/resource-declarations.ts +21 -17
- package/src/rules/resource-exists.ts +4 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/retired-vocabulary.ts +242 -5
- 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/static-resource-accessor-preference.ts +15 -11
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/surface-trailhead-coherence.ts +2 -2
- package/src/rules/trail-fork-coaching.ts +27 -18
- package/src/rules/trail-versioning-source.ts +16 -12
- package/src/rules/trail-versioning-topo.ts +2 -2
- package/src/rules/trailhead-override-divergence.ts +2 -2
- package/src/rules/types.ts +7 -7
- package/src/rules/unmaterialized-activation-source.ts +1 -0
- package/src/rules/unreachable-detour-shadowing.ts +2 -2
- package/src/rules/valid-describe-refs.ts +2 -2
- package/src/rules/warden-export-symmetry.ts +3 -3
- package/src/rules/warden-rules-use-ast.ts +12 -17
- package/src/trails/activation-orphan.trail.ts +5 -5
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +6 -6
- package/src/trails/cli-command-route-coherence.trail.ts +2 -2
- package/src/trails/composes-declarations.trail.ts +1 -1
- package/src/trails/context-no-surface-types.trail.ts +1 -1
- package/src/trails/dead-internal-trail.trail.ts +2 -2
- package/src/trails/dead-public-trail.trail.ts +1 -1
- package/src/trails/deprecation-without-guidance.trail.ts +1 -1
- package/src/trails/duplicate-public-contract.trail.ts +3 -3
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/example-valid.trail.ts +3 -3
- package/src/trails/fires-declarations.trail.ts +1 -1
- package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
- package/src/trails/index.ts +3 -2
- package/src/trails/intent-propagation.trail.ts +2 -2
- package/src/trails/library-projection-coherence.trail.ts +2 -2
- package/src/trails/marker-schema-unsupported.trail.ts +1 -1
- package/src/trails/missing-visibility.trail.ts +1 -1
- package/src/trails/no-destructured-compose.trail.ts +5 -5
- package/src/trails/no-direct-implementation-call.trail.ts +1 -1
- package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/on-references-exist.trail.ts +1 -1
- package/src/trails/pending-force.trail.ts +1 -1
- package/src/trails/permit-governance.trail.ts +2 -2
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/public-output-schema.trail.ts +2 -2
- package/src/trails/public-union-output-discriminants.trail.ts +1 -1
- package/src/trails/read-intent-fires.trail.ts +1 -1
- package/src/trails/reference-exists.trail.ts +5 -5
- package/src/trails/resource-declarations.trail.ts +1 -1
- package/src/trails/resource-exists.trail.ts +1 -1
- package/src/trails/run.ts +4 -6
- package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
- package/src/trails/schema.ts +9 -9
- package/src/trails/signal-graph-coaching.trail.ts +2 -2
- package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
- package/src/trails/surface-overlay-coherence.trail.ts +1 -1
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
- package/src/trails/valid-detour-contract.trail.ts +1 -1
- package/src/trails/version-gap.trail.ts +1 -1
- package/src/trails/version-pinned-compose.trail.ts +1 -1
- package/src/trails/version-without-examples.trail.ts +1 -1
- package/src/trails/warden-rules-use-ast.trail.ts +2 -2
- package/src/trails/webhook-route-collision.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/src/ast.ts +0 -137
- package/src/rules/ast.ts +0 -4143
- package/src/rules/contour-ids.ts +0 -15
- package/src/trails/contour-exists.trail.ts +0 -21
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractStringLiteral,
|
|
6
|
+
getNodeDeclaration,
|
|
7
|
+
getNodeImportKind,
|
|
8
|
+
getNodeLocal,
|
|
9
|
+
getNodeSource,
|
|
10
|
+
getNodeSpecifiers,
|
|
11
|
+
identifierName,
|
|
12
|
+
isExportDefaultDeclaration,
|
|
13
|
+
isExportNamedDeclaration,
|
|
14
|
+
isImportDeclaration,
|
|
15
|
+
offsetToLine,
|
|
16
|
+
parse,
|
|
17
|
+
walk,
|
|
18
|
+
} from '@ontrails/source';
|
|
19
|
+
import type { WardenImportResolution } from '../resolve.js';
|
|
20
|
+
import type { WardenPublicWorkspace } from '../workspaces.js';
|
|
21
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
|
|
22
|
+
import { isTestFile } from './scan.js';
|
|
23
|
+
import type {
|
|
24
|
+
ProjectAwareWardenRule,
|
|
25
|
+
ProjectContext,
|
|
26
|
+
WardenDiagnostic,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
const RULE_NAME = 'captured-kernel';
|
|
30
|
+
|
|
31
|
+
interface InternalReExportSite {
|
|
32
|
+
readonly importSource: string;
|
|
33
|
+
readonly line: number;
|
|
34
|
+
readonly resolutionLine: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ImportedBindingSite {
|
|
38
|
+
readonly importSource: string;
|
|
39
|
+
readonly resolutionLine: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ExportTarget {
|
|
43
|
+
readonly specifier: string;
|
|
44
|
+
readonly workspace: WardenPublicWorkspace;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
48
|
+
|
|
49
|
+
const normalizeRealPath = (path: string): string => {
|
|
50
|
+
try {
|
|
51
|
+
return normalizePath(realpathSync(path));
|
|
52
|
+
} catch {
|
|
53
|
+
return normalizePath(resolve(path));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const pathIsInside = (filePath: string, rootDir: string): boolean => {
|
|
58
|
+
const absoluteFilePath = normalizeRealPath(filePath);
|
|
59
|
+
const absoluteRootDir = normalizeRealPath(rootDir);
|
|
60
|
+
return (
|
|
61
|
+
absoluteFilePath === absoluteRootDir ||
|
|
62
|
+
absoluteFilePath.startsWith(`${absoluteRootDir}/`)
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const sourcePackageNameForFile = (
|
|
67
|
+
filePath: string,
|
|
68
|
+
workspaces: ReadonlyMap<string, WardenPublicWorkspace>
|
|
69
|
+
): string | undefined => {
|
|
70
|
+
for (const workspace of workspaces.values()) {
|
|
71
|
+
if (pathIsInside(filePath, workspace.rootDir)) {
|
|
72
|
+
return workspace.name;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const exportTargetsForFile = (
|
|
79
|
+
filePath: string,
|
|
80
|
+
workspaces: ReadonlyMap<string, WardenPublicWorkspace>
|
|
81
|
+
): readonly ExportTarget[] => {
|
|
82
|
+
const normalizedFilePath = normalizeRealPath(filePath);
|
|
83
|
+
const targets: ExportTarget[] = [];
|
|
84
|
+
for (const workspace of workspaces.values()) {
|
|
85
|
+
for (const [specifier, target] of Object.entries(
|
|
86
|
+
workspace.exportTargets ?? {}
|
|
87
|
+
)) {
|
|
88
|
+
if (specifier === workspace.name) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (normalizeRealPath(target) === normalizedFilePath) {
|
|
92
|
+
targets.push({ specifier, workspace });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return targets.toSorted((left, right) =>
|
|
97
|
+
left.specifier.localeCompare(right.specifier)
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const collectReExportSites = (
|
|
102
|
+
sourceCode: string,
|
|
103
|
+
filePath: string
|
|
104
|
+
): readonly InternalReExportSite[] => {
|
|
105
|
+
const ast = parse(filePath, sourceCode);
|
|
106
|
+
if (!ast) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sites: InternalReExportSite[] = [];
|
|
111
|
+
const importedBindings = new Map<string, ImportedBindingSite>();
|
|
112
|
+
walk(ast, (node) => {
|
|
113
|
+
if (!isImportDeclaration(node)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (getNodeImportKind(node) === 'type') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const value = extractStringLiteral(getNodeSource(node));
|
|
120
|
+
if (typeof value !== 'string') {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const resolutionLine = offsetToLine(sourceCode, node.start);
|
|
124
|
+
for (const specifier of getNodeSpecifiers(node) ?? []) {
|
|
125
|
+
if (getNodeImportKind(specifier) === 'type') {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const localName = identifierName(getNodeLocal(specifier));
|
|
129
|
+
if (localName !== null) {
|
|
130
|
+
importedBindings.set(localName, {
|
|
131
|
+
importSource: value,
|
|
132
|
+
resolutionLine,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const siteKeys = new Set<string>();
|
|
139
|
+
const addSite = (site: InternalReExportSite): void => {
|
|
140
|
+
const key = `${site.importSource}:${site.line}:${site.resolutionLine}`;
|
|
141
|
+
if (!siteKeys.has(key)) {
|
|
142
|
+
siteKeys.add(key);
|
|
143
|
+
sites.push(site);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
walk(ast, (node) => {
|
|
147
|
+
if (isExportDefaultDeclaration(node)) {
|
|
148
|
+
const localName = identifierName(getNodeDeclaration(node));
|
|
149
|
+
if (localName === null) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const imported = importedBindings.get(localName);
|
|
153
|
+
if (imported !== undefined) {
|
|
154
|
+
addSite({
|
|
155
|
+
...imported,
|
|
156
|
+
line: offsetToLine(sourceCode, node.start),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
node.type !== 'ExportNamedDeclaration' &&
|
|
164
|
+
node.type !== 'ExportAllDeclaration'
|
|
165
|
+
) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const line = offsetToLine(sourceCode, node.start);
|
|
170
|
+
const value = extractStringLiteral(getNodeSource(node));
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
addSite({ importSource: value, line, resolutionLine: line });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isExportNamedDeclaration(node)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const specifier of getNodeSpecifiers(node) ?? []) {
|
|
180
|
+
const localName = identifierName(getNodeLocal(specifier));
|
|
181
|
+
if (localName === null) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const imported = importedBindings.get(localName);
|
|
185
|
+
if (imported !== undefined) {
|
|
186
|
+
addSite({ ...imported, line });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return sites.toSorted((left, right) => left.line - right.line);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const importResolutionsForFile = (
|
|
194
|
+
context: ProjectContext,
|
|
195
|
+
filePath: string
|
|
196
|
+
): readonly WardenImportResolution[] =>
|
|
197
|
+
context.importResolutionsByFile?.get(filePath) ?? [];
|
|
198
|
+
|
|
199
|
+
const reExportResolution = (
|
|
200
|
+
resolutions: readonly WardenImportResolution[],
|
|
201
|
+
site: InternalReExportSite
|
|
202
|
+
): WardenImportResolution | undefined =>
|
|
203
|
+
resolutions.find(
|
|
204
|
+
(resolution) =>
|
|
205
|
+
resolution.importSource === site.importSource &&
|
|
206
|
+
resolution.line === site.resolutionLine
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const isOwnedInternalReExport = (
|
|
210
|
+
workspace: WardenPublicWorkspace,
|
|
211
|
+
resolution: WardenImportResolution | undefined
|
|
212
|
+
): boolean => {
|
|
213
|
+
if (!resolution?.isInternalTarget) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (resolution.packageName === workspace.name) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
resolution.packageRoot &&
|
|
221
|
+
normalizeRealPath(resolution.packageRoot) ===
|
|
222
|
+
normalizeRealPath(workspace.rootDir)
|
|
223
|
+
) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return resolution.resolvedPath
|
|
227
|
+
? pathIsInside(resolution.resolvedPath, workspace.rootDir)
|
|
228
|
+
: false;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const isNonProductionEvidenceFile = (filePath: string): boolean => {
|
|
232
|
+
const normalized = normalizePath(filePath);
|
|
233
|
+
return (
|
|
234
|
+
isTestFile(filePath) ||
|
|
235
|
+
/(?:^|\/)(?:__fixtures__|fixtures?|migrations?|historical|changelogs?|changesets?|agent-notes?)(?:\/|$)/.test(
|
|
236
|
+
normalized
|
|
237
|
+
) ||
|
|
238
|
+
/(?:^|\/)\.(?:changeset|agents)(?:\/|$)/.test(normalized) ||
|
|
239
|
+
/(?:^|\/)(?:CHANGELOG|changeset)\.md$/i.test(normalized)
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const externalProductionConsumerPackages = ({
|
|
244
|
+
context,
|
|
245
|
+
exportSpecifier,
|
|
246
|
+
hostPackage,
|
|
247
|
+
workspaces,
|
|
248
|
+
}: {
|
|
249
|
+
readonly context: ProjectContext;
|
|
250
|
+
readonly exportSpecifier: string;
|
|
251
|
+
readonly hostPackage: string;
|
|
252
|
+
readonly workspaces: ReadonlyMap<string, WardenPublicWorkspace>;
|
|
253
|
+
}): readonly string[] => {
|
|
254
|
+
const consumers = new Set<string>();
|
|
255
|
+
for (const resolutions of context.importResolutionsByFile?.values() ?? []) {
|
|
256
|
+
for (const resolution of resolutions) {
|
|
257
|
+
if (resolution.importSource !== exportSpecifier) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (resolution.errorKind || !resolution.usesPublicExport) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!resolution.crossesPackageBoundary) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (resolution.packageName !== hostPackage) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (isNonProductionEvidenceFile(resolution.importerPath)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const sourcePackageName = sourcePackageNameForFile(
|
|
274
|
+
resolution.importerPath,
|
|
275
|
+
workspaces
|
|
276
|
+
);
|
|
277
|
+
if (!sourcePackageName || sourcePackageName === hostPackage) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
consumers.add(sourcePackageName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [...consumers].toSorted();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const diagnosticMessage = ({
|
|
287
|
+
consumers,
|
|
288
|
+
exportSpecifier,
|
|
289
|
+
hostPackage,
|
|
290
|
+
importSource,
|
|
291
|
+
}: {
|
|
292
|
+
readonly consumers: readonly string[];
|
|
293
|
+
readonly exportSpecifier: string;
|
|
294
|
+
readonly hostPackage: string;
|
|
295
|
+
readonly importSource: string;
|
|
296
|
+
}): string =>
|
|
297
|
+
`${hostPackage} export target "${exportSpecifier}" re-exports internal target "${importSource}" and is consumed by external production packages ${consumers.join(
|
|
298
|
+
', '
|
|
299
|
+
)}. Review ownership of the exported subpath and its captured kernel before it becomes a durable public seam.`;
|
|
300
|
+
|
|
301
|
+
export const capturedKernel: ProjectAwareWardenRule = {
|
|
302
|
+
check(): readonly WardenDiagnostic[] {
|
|
303
|
+
return [];
|
|
304
|
+
},
|
|
305
|
+
checkWithContext(
|
|
306
|
+
sourceCode: string,
|
|
307
|
+
filePath: string,
|
|
308
|
+
context: ProjectContext
|
|
309
|
+
): readonly WardenDiagnostic[] {
|
|
310
|
+
const workspaces = context.publicWorkspaces;
|
|
311
|
+
if (!workspaces || workspaces.size === 0) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const exportTargets = exportTargetsForFile(filePath, workspaces);
|
|
316
|
+
if (exportTargets.length === 0) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const lines = splitSourceLines(sourceCode);
|
|
321
|
+
const resolutions = importResolutionsForFile(context, filePath);
|
|
322
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
323
|
+
|
|
324
|
+
for (const exportTarget of exportTargets) {
|
|
325
|
+
const consumers = externalProductionConsumerPackages({
|
|
326
|
+
context,
|
|
327
|
+
exportSpecifier: exportTarget.specifier,
|
|
328
|
+
hostPackage: exportTarget.workspace.name,
|
|
329
|
+
workspaces,
|
|
330
|
+
});
|
|
331
|
+
if (consumers.length < 2) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const site of collectReExportSites(sourceCode, filePath)) {
|
|
336
|
+
if (hasIgnoreCommentOnLine(lines, site.line)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const resolution = reExportResolution(resolutions, site);
|
|
341
|
+
if (!isOwnedInternalReExport(exportTarget.workspace, resolution)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
diagnostics.push({
|
|
346
|
+
filePath,
|
|
347
|
+
guidance: {
|
|
348
|
+
steps: [
|
|
349
|
+
'Review whether the public subpath should become an owned package surface, move back behind the package root, or be split into a better-owned package.',
|
|
350
|
+
'If the imported capability is reusable source-code machinery, serves at least two independently owned toolchain capabilities, exposes one genuinely shared contract, and owns no verdict, migration plan, graph query, or surface rendering, consider relocating it to @ontrails/source.',
|
|
351
|
+
'Otherwise, preserve the current owner or choose another doctrinal owner.',
|
|
352
|
+
],
|
|
353
|
+
summary:
|
|
354
|
+
'Review ownership before an internal re-exported kernel hardens into a public package seam.',
|
|
355
|
+
},
|
|
356
|
+
line: site.line,
|
|
357
|
+
message: diagnosticMessage({
|
|
358
|
+
consumers,
|
|
359
|
+
exportSpecifier: exportTarget.specifier,
|
|
360
|
+
hostPackage: exportTarget.workspace.name,
|
|
361
|
+
importSource: site.importSource,
|
|
362
|
+
}),
|
|
363
|
+
rule: RULE_NAME,
|
|
364
|
+
severity: 'warn',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return diagnostics;
|
|
370
|
+
},
|
|
371
|
+
description:
|
|
372
|
+
'Flag public subpath exports that capture internal kernels after multiple production packages consume them.',
|
|
373
|
+
name: RULE_NAME,
|
|
374
|
+
severity: 'warn',
|
|
375
|
+
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
offsetToLine,
|
|
5
|
-
parse,
|
|
6
|
-
} from './ast.js';
|
|
7
|
-
import type { AstNode } from './ast.js';
|
|
1
|
+
import { collectEntityReferenceTargetsByName } from './source/entities.js';
|
|
2
|
+
import { findEntityDefinitions, offsetToLine, parse } from '@ontrails/source';
|
|
3
|
+
import type { AstNode } from '@ontrails/source';
|
|
8
4
|
import { isTestFile } from './scan.js';
|
|
9
5
|
import type {
|
|
10
6
|
ProjectAwareWardenRule,
|
|
@@ -75,14 +71,14 @@ const findCyclePath = (
|
|
|
75
71
|
};
|
|
76
72
|
|
|
77
73
|
const buildCircularReferenceDiagnostic = (
|
|
78
|
-
|
|
74
|
+
entityName: string,
|
|
79
75
|
cyclePath: readonly string[],
|
|
80
76
|
filePath: string,
|
|
81
77
|
line: number
|
|
82
78
|
): WardenDiagnostic => ({
|
|
83
79
|
filePath,
|
|
84
80
|
line,
|
|
85
|
-
message: `
|
|
81
|
+
message: `Entity "${entityName}" participates in circular entity references: ${cyclePath.join(' -> ')}. Break the cycle by removing one entity reference, or extract the shared shape into a new entity neither side depends on.`,
|
|
86
82
|
rule: 'circular-refs',
|
|
87
83
|
severity: 'warn',
|
|
88
84
|
});
|
|
@@ -97,7 +93,7 @@ const checkCircularReferences = (
|
|
|
97
93
|
return [];
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
return
|
|
96
|
+
return findEntityDefinitions(ast).flatMap((definition) => {
|
|
101
97
|
const cyclePath = findCyclePath(definition.name, graph);
|
|
102
98
|
if (!cyclePath) {
|
|
103
99
|
return [];
|
|
@@ -115,7 +111,7 @@ const checkCircularReferences = (
|
|
|
115
111
|
};
|
|
116
112
|
|
|
117
113
|
/**
|
|
118
|
-
* Warns when
|
|
114
|
+
* Warns when entity references form a direct or transitive cycle.
|
|
119
115
|
*/
|
|
120
116
|
export const circularRefs: ProjectAwareWardenRule = {
|
|
121
117
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
@@ -124,7 +120,7 @@ export const circularRefs: ProjectAwareWardenRule = {
|
|
|
124
120
|
return [];
|
|
125
121
|
}
|
|
126
122
|
|
|
127
|
-
const graph =
|
|
123
|
+
const graph = collectEntityReferenceTargetsByName(ast);
|
|
128
124
|
return checkCircularReferences(ast, sourceCode, filePath, graph);
|
|
129
125
|
},
|
|
130
126
|
checkWithContext(
|
|
@@ -137,18 +133,18 @@ export const circularRefs: ProjectAwareWardenRule = {
|
|
|
137
133
|
return [];
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
const localGraph =
|
|
136
|
+
const localGraph = collectEntityReferenceTargetsByName(
|
|
141
137
|
ast,
|
|
142
|
-
context.
|
|
138
|
+
context.knownEntityIds
|
|
143
139
|
);
|
|
144
140
|
return checkCircularReferences(
|
|
145
141
|
ast,
|
|
146
142
|
sourceCode,
|
|
147
143
|
filePath,
|
|
148
|
-
mergeReferenceGraphs(localGraph, context.
|
|
144
|
+
mergeReferenceGraphs(localGraph, context.entityReferencesByName)
|
|
149
145
|
);
|
|
150
146
|
},
|
|
151
|
-
description: 'Warn when
|
|
147
|
+
description: 'Warn when entity references form direct or transitive cycles.',
|
|
152
148
|
name: 'circular-refs',
|
|
153
149
|
severity: 'warn',
|
|
154
150
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deriveTrailCliCommandProjection } from '@ontrails/core';
|
|
2
2
|
import type { CliCommandRoute, Topo } from '@ontrails/core';
|
|
3
|
-
import type { TopoGraph } from '@ontrails/
|
|
3
|
+
import type { TopoGraph } from '@ontrails/topography';
|
|
4
4
|
|
|
5
5
|
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
6
6
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validates that `ctx.compose()` calls match the declared `composes` array.
|
|
3
3
|
*
|
|
4
|
-
* Statically analyzes trail `
|
|
4
|
+
* Statically analyzes trail `implementation` functions to find `ctx.compose('trailId', ...)`
|
|
5
5
|
* calls and compares them against the `composes: [...]` declaration in the trail
|
|
6
6
|
* config. Reports errors for undeclared compositions and warnings for unused ones.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
findBlazeBodies,
|
|
11
10
|
findConfigProperty,
|
|
11
|
+
findImplementationBodies,
|
|
12
12
|
findTrailDefinitions,
|
|
13
13
|
getNodeArguments,
|
|
14
14
|
getNodeBodyNode,
|
|
@@ -32,8 +32,8 @@ import {
|
|
|
32
32
|
offsetToLine,
|
|
33
33
|
parse,
|
|
34
34
|
walkWithScopes,
|
|
35
|
-
} from '
|
|
36
|
-
import type { AstNode } from '
|
|
35
|
+
} from '@ontrails/source';
|
|
36
|
+
import type { AstNode } from '@ontrails/source';
|
|
37
37
|
import { isTestFile } from './scan.js';
|
|
38
38
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
39
39
|
|
|
@@ -220,15 +220,17 @@ const extractMemberPair = (
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
* Extract the second parameter name from a
|
|
223
|
+
* Extract the second parameter name from a implementation function node.
|
|
224
224
|
*
|
|
225
225
|
* Handles `(input, ctx) => ...`, `async (input, context) => ...`,
|
|
226
226
|
* `function(input, ctx) { ... }`, and defaulted params like
|
|
227
227
|
* `(input, ctx = fallback) => ...` (AssignmentPattern whose `.left` is the
|
|
228
228
|
* Identifier).
|
|
229
229
|
*/
|
|
230
|
-
const extractContextParamName = (
|
|
231
|
-
|
|
230
|
+
const extractContextParamName = (
|
|
231
|
+
implementationBody: AstNode
|
|
232
|
+
): string | null => {
|
|
233
|
+
const params = implementationBody['params'] as readonly AstNode[] | undefined;
|
|
232
234
|
if (!params || params.length < 2) {
|
|
233
235
|
return null;
|
|
234
236
|
}
|
|
@@ -422,10 +424,10 @@ const extractComposeCall = (
|
|
|
422
424
|
/**
|
|
423
425
|
* Build the set of context parameter names to match against.
|
|
424
426
|
*
|
|
425
|
-
* Returns ONLY the actual second-parameter name from the
|
|
426
|
-
* No seeded defaults: if the
|
|
427
|
+
* Returns ONLY the actual second-parameter name from the implementation signature.
|
|
428
|
+
* No seeded defaults: if the implementation has no second parameter, the returned set
|
|
427
429
|
* is empty and no `ctx.compose(...)` / `context.compose(...)` calls are tracked
|
|
428
|
-
* for that
|
|
430
|
+
* for that implementation. An unrelated closure-scoped `ctx` identifier is not the
|
|
429
431
|
* trail context and must not be treated as one.
|
|
430
432
|
*
|
|
431
433
|
* Mirrors `fires-declarations.ts` and `resource-declarations.ts` for the same
|
|
@@ -613,7 +615,7 @@ interface CalledComposes {
|
|
|
613
615
|
readonly hasUnresolved: boolean;
|
|
614
616
|
}
|
|
615
617
|
|
|
616
|
-
/** Collect compose call results from a single
|
|
618
|
+
/** Collect compose call results from a single implementation body. */
|
|
617
619
|
const collectComposeCallsFromBody = (
|
|
618
620
|
body: AstNode,
|
|
619
621
|
ids: Set<string>,
|
|
@@ -682,7 +684,7 @@ const collectComposeCallsFromBody = (
|
|
|
682
684
|
return foundUnresolved;
|
|
683
685
|
};
|
|
684
686
|
|
|
685
|
-
/** Walk
|
|
687
|
+
/** Walk implementation bodies and collect all statically resolvable ctx.compose() trail IDs. */
|
|
686
688
|
const extractCalledComposes = (
|
|
687
689
|
config: AstNode,
|
|
688
690
|
ast: AstNode,
|
|
@@ -692,7 +694,7 @@ const extractCalledComposes = (
|
|
|
692
694
|
let hasUnresolved = false;
|
|
693
695
|
const helpers = collectComposeHelpers(ast);
|
|
694
696
|
|
|
695
|
-
for (const body of
|
|
697
|
+
for (const body of findImplementationBodies(config)) {
|
|
696
698
|
if (collectComposeCallsFromBody(body, ids, sourceCode, helpers)) {
|
|
697
699
|
hasUnresolved = true;
|
|
698
700
|
}
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
offsetToLine,
|
|
14
14
|
parse,
|
|
15
15
|
walk,
|
|
16
|
-
} from '
|
|
17
|
-
import type { AstNode } from '
|
|
16
|
+
} from '@ontrails/source';
|
|
17
|
+
import type { AstNode } from '@ontrails/source';
|
|
18
18
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
19
19
|
|
|
20
20
|
const SURFACE_MODULES = new Set([
|
|
@@ -82,7 +82,8 @@ const isNamespacedTrailCallee = (callee: AstNode): boolean => {
|
|
|
82
82
|
* `import * as ns from '@ontrails/core'` are detected as trail definitions.
|
|
83
83
|
*
|
|
84
84
|
* The inner `if (found)` guard skips further work in each callback invocation,
|
|
85
|
-
* but the shared `walk` helper
|
|
85
|
+
* but the shared `walk` helper from `@ontrails/source` exposes no abort
|
|
86
|
+
* mechanism, so
|
|
86
87
|
* the full tree is still traversed once a match is seen. Acceptable: `walk`
|
|
87
88
|
* is cheap and this rule only runs on files that already matched a path
|
|
88
89
|
* filter upstream.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { collectComposeTargetTrailIds } from './source/composition.js';
|
|
1
2
|
import {
|
|
2
|
-
collectComposeTargetTrailIds,
|
|
3
3
|
findConfigProperty,
|
|
4
4
|
findTrailDefinitions,
|
|
5
5
|
getNodeValue,
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
isStringLiteral,
|
|
8
8
|
offsetToLine,
|
|
9
9
|
parse,
|
|
10
|
-
} from '
|
|
11
|
-
import type { AstNode } from '
|
|
10
|
+
} from '@ontrails/source';
|
|
11
|
+
import type { AstNode } from '@ontrails/source';
|
|
12
12
|
import { isTestFile } from './scan.js';
|
|
13
13
|
import type {
|
|
14
14
|
ProjectAwareWardenRule,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { collectComposeTargetTrailIds } from './source/composition.js';
|
|
1
2
|
import {
|
|
2
|
-
collectComposeTargetTrailIds,
|
|
3
3
|
findConfigProperty,
|
|
4
4
|
findTrailDefinitions,
|
|
5
5
|
getNodeDeclaration,
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
offsetToLine,
|
|
16
16
|
parse,
|
|
17
17
|
walkWithParents,
|
|
18
|
-
} from '
|
|
19
|
-
import type { AstNode, AstParentContext } from '
|
|
18
|
+
} from '@ontrails/source';
|
|
19
|
+
import type { AstNode, AstParentContext } from '@ontrails/source';
|
|
20
20
|
import { isTestFile } from './scan.js';
|
|
21
21
|
import type {
|
|
22
22
|
ProjectAwareWardenRule,
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { isDraftId } from '@ontrails/core';
|
|
2
2
|
|
|
3
3
|
import { isDraftMarkedFile } from '../draft.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
offsetToLine,
|
|
9
|
-
parse,
|
|
10
|
-
splitSourceLines,
|
|
11
|
-
} from './ast.js';
|
|
12
|
-
import type { StringLiteralMatch } from './ast.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';
|
|
13
8
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
14
9
|
|
|
15
10
|
const messageForMissingMarker = (draftId: string): string =>
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { isDraftId } from '@ontrails/core';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
hasIgnoreCommentOnLine,
|
|
7
|
-
offsetToLine,
|
|
8
|
-
parse,
|
|
9
|
-
splitSourceLines,
|
|
10
|
-
} from './ast.js';
|
|
3
|
+
import { collectFrameworkDraftPrefixConstantOffsets } from './source/drafts.js';
|
|
4
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
|
|
5
|
+
import { findStringLiterals, offsetToLine, parse } from '@ontrails/source';
|
|
11
6
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
12
7
|
|
|
13
8
|
const createDiagnostic = (
|