@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +5 -5
  3. package/package.json +10 -12
  4. package/src/cli.ts +49 -44
  5. package/src/command.ts +2 -2
  6. package/src/drift.ts +4 -3
  7. package/src/index.ts +3 -2
  8. package/src/project-context.ts +3 -3
  9. package/src/resolve.ts +2 -2
  10. package/src/rules/captured-kernel.ts +375 -0
  11. package/src/rules/circular-refs.ts +12 -16
  12. package/src/rules/cli-command-route-coherence.ts +1 -1
  13. package/src/rules/composes-declarations.ts +15 -13
  14. package/src/rules/context-no-surface-types.ts +4 -3
  15. package/src/rules/dead-internal-trail.ts +3 -3
  16. package/src/rules/dead-public-trail.ts +3 -3
  17. package/src/rules/draft-file-marking.ts +4 -9
  18. package/src/rules/draft-visible-debt.ts +3 -8
  19. package/src/rules/duplicate-public-contract.ts +50 -5
  20. package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
  21. package/src/rules/entity-ids.ts +15 -0
  22. package/src/rules/error-mapping-completeness.ts +2 -2
  23. package/src/rules/example-valid.ts +84 -84
  24. package/src/rules/fires-declarations.ts +27 -22
  25. package/src/rules/governed-symbol-residue.ts +339 -32
  26. package/src/rules/implementation-returns-result.ts +8 -8
  27. package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
  28. package/src/rules/incomplete-crud.ts +42 -40
  29. package/src/rules/index.ts +9 -6
  30. package/src/rules/intent-propagation.ts +3 -5
  31. package/src/rules/layer-field-name-drift.ts +2 -2
  32. package/src/rules/library-projection-coherence.ts +2 -2
  33. package/src/rules/metadata.ts +37 -15
  34. package/src/rules/missing-reconcile.ts +3 -4
  35. package/src/rules/missing-visibility.ts +2 -1
  36. package/src/rules/no-destructured-compose.ts +14 -12
  37. package/src/rules/no-direct-implementation-call.ts +12 -7
  38. package/src/rules/no-legacy-cli-alias-export.ts +2 -2
  39. package/src/rules/no-native-error-result.ts +2 -2
  40. package/src/rules/no-redundant-result-error-wrap.ts +10 -10
  41. package/src/rules/no-sync-result-assumption.ts +84 -78
  42. package/src/rules/no-throw-in-detour-recover.ts +2 -2
  43. package/src/rules/no-throw-in-implementation.ts +13 -7
  44. package/src/rules/no-top-level-surface.ts +2 -2
  45. package/src/rules/on-references-exist.ts +3 -3
  46. package/src/rules/orphaned-signal.ts +4 -5
  47. package/src/rules/owner-projection-parity.ts +2 -2
  48. package/src/rules/public-export-example-coverage.ts +10 -4
  49. package/src/rules/public-internal-deep-imports.ts +2 -3
  50. package/src/rules/read-intent-fires.ts +4 -3
  51. package/src/rules/reference-exists.ts +20 -21
  52. package/src/rules/registry-names.ts +6 -4
  53. package/src/rules/resolved-import-boundary.ts +1 -1
  54. package/src/rules/resource-declarations.ts +21 -17
  55. package/src/rules/resource-exists.ts +4 -2
  56. package/src/rules/resource-id-grammar.ts +2 -2
  57. package/src/rules/resource-mock-coverage.ts +2 -2
  58. package/src/rules/retired-vocabulary.ts +242 -5
  59. package/src/rules/source/composition.ts +165 -0
  60. package/src/rules/source/drafts.ts +164 -0
  61. package/src/rules/source/entities.ts +618 -0
  62. package/src/rules/source/pragmas.ts +45 -0
  63. package/src/rules/source/resources.ts +64 -0
  64. package/src/rules/source/signals.ts +397 -0
  65. package/src/rules/source/stores.ts +310 -0
  66. package/src/rules/static-resource-accessor-preference.ts +15 -11
  67. package/src/rules/surface-overlay-coherence.ts +1 -1
  68. package/src/rules/surface-trailhead-coherence.ts +2 -2
  69. package/src/rules/trail-fork-coaching.ts +27 -18
  70. package/src/rules/trail-versioning-source.ts +16 -12
  71. package/src/rules/trail-versioning-topo.ts +2 -2
  72. package/src/rules/trailhead-override-divergence.ts +2 -2
  73. package/src/rules/types.ts +7 -7
  74. package/src/rules/unmaterialized-activation-source.ts +1 -0
  75. package/src/rules/unreachable-detour-shadowing.ts +2 -2
  76. package/src/rules/valid-describe-refs.ts +2 -2
  77. package/src/rules/warden-export-symmetry.ts +3 -3
  78. package/src/rules/warden-rules-use-ast.ts +12 -17
  79. package/src/trails/activation-orphan.trail.ts +5 -5
  80. package/src/trails/captured-kernel.trail.ts +108 -0
  81. package/src/trails/circular-refs.trail.ts +6 -6
  82. package/src/trails/cli-command-route-coherence.trail.ts +2 -2
  83. package/src/trails/composes-declarations.trail.ts +1 -1
  84. package/src/trails/context-no-surface-types.trail.ts +1 -1
  85. package/src/trails/dead-internal-trail.trail.ts +2 -2
  86. package/src/trails/dead-public-trail.trail.ts +1 -1
  87. package/src/trails/deprecation-without-guidance.trail.ts +1 -1
  88. package/src/trails/duplicate-public-contract.trail.ts +3 -3
  89. package/src/trails/entity-exists.trail.ts +21 -0
  90. package/src/trails/example-valid.trail.ts +3 -3
  91. package/src/trails/fires-declarations.trail.ts +1 -1
  92. package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
  93. package/src/trails/implementation-returns-result.trail.ts +1 -1
  94. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
  95. package/src/trails/index.ts +3 -2
  96. package/src/trails/intent-propagation.trail.ts +2 -2
  97. package/src/trails/library-projection-coherence.trail.ts +2 -2
  98. package/src/trails/marker-schema-unsupported.trail.ts +1 -1
  99. package/src/trails/missing-visibility.trail.ts +1 -1
  100. package/src/trails/no-destructured-compose.trail.ts +5 -5
  101. package/src/trails/no-direct-implementation-call.trail.ts +1 -1
  102. package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
  103. package/src/trails/no-sync-result-assumption.trail.ts +2 -2
  104. package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
  105. package/src/trails/no-throw-in-implementation.trail.ts +1 -1
  106. package/src/trails/on-references-exist.trail.ts +1 -1
  107. package/src/trails/pending-force.trail.ts +1 -1
  108. package/src/trails/permit-governance.trail.ts +2 -2
  109. package/src/trails/prefer-schema-inference.trail.ts +1 -1
  110. package/src/trails/public-output-schema.trail.ts +2 -2
  111. package/src/trails/public-union-output-discriminants.trail.ts +1 -1
  112. package/src/trails/read-intent-fires.trail.ts +1 -1
  113. package/src/trails/reference-exists.trail.ts +5 -5
  114. package/src/trails/resource-declarations.trail.ts +1 -1
  115. package/src/trails/resource-exists.trail.ts +1 -1
  116. package/src/trails/run.ts +4 -6
  117. package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
  118. package/src/trails/schema.ts +9 -9
  119. package/src/trails/signal-graph-coaching.trail.ts +2 -2
  120. package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
  121. package/src/trails/surface-overlay-coherence.trail.ts +1 -1
  122. package/src/trails/trail-fork-coaching.trail.ts +1 -1
  123. package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
  124. package/src/trails/valid-detour-contract.trail.ts +1 -1
  125. package/src/trails/version-gap.trail.ts +1 -1
  126. package/src/trails/version-pinned-compose.trail.ts +1 -1
  127. package/src/trails/version-without-examples.trail.ts +1 -1
  128. package/src/trails/warden-rules-use-ast.trail.ts +2 -2
  129. package/src/trails/webhook-route-collision.trail.ts +2 -2
  130. package/src/trails/wrap-rule.ts +17 -17
  131. package/src/ast.ts +0 -137
  132. package/src/rules/ast.ts +0 -4143
  133. package/src/rules/contour-ids.ts +0 -15
  134. package/src/trails/contour-exists.trail.ts +0 -21
@@ -1,10 +1,12 @@
1
1
  import {
2
+ getNodeKey,
3
+ getNodeValueNode,
2
4
  identifierName,
3
5
  offsetToLine,
4
6
  parseWithDiagnostics,
5
- walk,
6
- } from './ast.js';
7
- import type { AstNode } from './ast.js';
7
+ walkWithScopeContext,
8
+ } from '@ontrails/source';
9
+ import type { AstNode, AstScopeContext } from '@ontrails/source';
8
10
  import { listGovernedVocabularyTransitions } from './retired-vocabulary.js';
9
11
  import type {
10
12
  GovernedVocabularySymbolRename,
@@ -34,42 +36,319 @@ const isAllowedSourcePath = (filePath: string): boolean =>
34
36
  normalizePath(filePath).endsWith(suffix)
35
37
  );
36
38
 
37
- const activeSymbolRenames = (): readonly GovernedVocabularySymbolRename[] =>
39
+ interface ActiveSymbolRename {
40
+ readonly rename: GovernedVocabularySymbolRename;
41
+ readonly targetSegments: readonly string[];
42
+ }
43
+
44
+ const activeSymbolRenames = (): readonly ActiveSymbolRename[] =>
38
45
  listGovernedVocabularyTransitions()
39
46
  .filter(
40
47
  (transition) =>
41
48
  ACTIVE_STATUSES.has(transition.status) &&
42
49
  !FIX_OWNED_TRANSITION_IDS.has(transition.id)
43
50
  )
44
- .flatMap((transition) => transition.symbolRenames);
45
-
46
- const renameBySourceSymbol = (): ReadonlyMap<
47
- string,
48
- GovernedVocabularySymbolRename
49
- > =>
50
- new Map(
51
- activeSymbolRenames().map((rename) => [rename.from, rename] as const)
52
- );
51
+ .flatMap((transition) => {
52
+ const targetSegments = [
53
+ ...new Set(transition.symbolRenames.map((rename) => rename.to)),
54
+ ];
55
+ return transition.symbolRenames.map((rename) => ({
56
+ rename,
57
+ targetSegments,
58
+ }));
59
+ });
53
60
 
54
- const safeFixFor = (
61
+ const isIdentifierNamed = (node: AstNode, name: string): boolean =>
62
+ node.type === 'Identifier' && identifierName(node) === name;
63
+
64
+ const identifierTokenSpan = (
65
+ node: AstNode,
55
66
  sourceCode: string,
67
+ name: string
68
+ ): { readonly end: number; readonly start: number } | null => {
69
+ const end = node.start + name.length;
70
+ return sourceCode.slice(node.start, end) === name
71
+ ? { end, start: node.start }
72
+ : null;
73
+ };
74
+
75
+ const toPascalIdentifierSegment = (value: string): string =>
76
+ value.length === 0
77
+ ? value
78
+ : `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}`;
79
+
80
+ const isScreamingSnakeIdentifier = (name: string): boolean =>
81
+ /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/.test(name) && name.includes('_');
82
+
83
+ const replaceScreamingSnakeIdentifierSegment = (
84
+ name: string,
85
+ from: string,
86
+ to: string
87
+ ): string | null => {
88
+ if (!isScreamingSnakeIdentifier(name)) {
89
+ return null;
90
+ }
91
+
92
+ const fromSegment = from.toUpperCase();
93
+ const toSegment = to.toUpperCase();
94
+ let changed = false;
95
+ const nextName = name
96
+ .split('_')
97
+ .map((segment) => {
98
+ if (segment !== fromSegment) {
99
+ return segment;
100
+ }
101
+ changed = true;
102
+ return toSegment;
103
+ })
104
+ .join('_');
105
+
106
+ return changed ? nextName : null;
107
+ };
108
+
109
+ const replaceCamelOrPascalIdentifierSegment = (
110
+ name: string,
111
+ from: string,
112
+ to: string
113
+ ): string | null => {
114
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name)) {
115
+ return null;
116
+ }
117
+
118
+ const fromPascalSegment = toPascalIdentifierSegment(from);
119
+ const toPascalSegment = toPascalIdentifierSegment(to);
120
+ const segmentPattern = /[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z]+|[0-9]+/g;
121
+ let cursor = 0;
122
+ let changed = false;
123
+ let nextName = '';
124
+
125
+ for (const match of name.matchAll(segmentPattern)) {
126
+ const [segment] = match;
127
+ const { index } = match;
128
+ if (index === undefined || index !== cursor) {
129
+ return null;
130
+ }
131
+
132
+ if (segment === from) {
133
+ changed = true;
134
+ nextName += to;
135
+ } else if (segment === fromPascalSegment) {
136
+ changed = true;
137
+ nextName += toPascalSegment;
138
+ } else {
139
+ nextName += segment;
140
+ }
141
+
142
+ cursor = index + segment.length;
143
+ }
144
+
145
+ if (cursor !== name.length || !changed) {
146
+ return null;
147
+ }
148
+
149
+ return nextName;
150
+ };
151
+
152
+ const deriveIdentifierSegmentTarget = (
153
+ name: string,
154
+ from: string,
155
+ to: string
156
+ ): string | null => {
157
+ const leadingUnderscores = /^_+/.exec(name)?.[0] ?? '';
158
+ const coreName =
159
+ leadingUnderscores.length > 0
160
+ ? name.slice(leadingUnderscores.length)
161
+ : name;
162
+ if (coreName.length === 0) {
163
+ return null;
164
+ }
165
+
166
+ const screamingSnakeTarget = replaceScreamingSnakeIdentifierSegment(
167
+ coreName,
168
+ from,
169
+ to
170
+ );
171
+ if (screamingSnakeTarget !== null) {
172
+ return `${leadingUnderscores}${screamingSnakeTarget}`;
173
+ }
174
+
175
+ const camelOrPascalTarget = replaceCamelOrPascalIdentifierSegment(
176
+ coreName,
177
+ from,
178
+ to
179
+ );
180
+ return camelOrPascalTarget === null
181
+ ? null
182
+ : `${leadingUnderscores}${camelOrPascalTarget}`;
183
+ };
184
+
185
+ const hasIdentifierSegment = (name: string, segment: string): boolean =>
186
+ deriveIdentifierSegmentTarget(name, segment, segment) !== null;
187
+
188
+ interface SymbolRenameMatch {
189
+ readonly from: string;
190
+ readonly rename: GovernedVocabularySymbolRename;
191
+ readonly span: { readonly end: number; readonly start: number } | null;
192
+ readonly to: string;
193
+ }
194
+
195
+ const resolveSymbolRenameMatch = (
56
196
  node: AstNode,
197
+ sourceCode: string,
57
198
  rename: GovernedVocabularySymbolRename
58
- ): WardenFix | undefined => {
59
- const end = node.start + rename.from.length;
60
- if (sourceCode.slice(node.start, end) !== rename.from) {
199
+ ): SymbolRenameMatch | null => {
200
+ if (rename.match === 'exact') {
201
+ if (!isIdentifierNamed(node, rename.from)) {
202
+ return null;
203
+ }
204
+
205
+ return {
206
+ from: rename.from,
207
+ rename,
208
+ span: identifierTokenSpan(node, sourceCode, rename.from),
209
+ to: rename.to,
210
+ };
211
+ }
212
+
213
+ if (node.type !== 'Identifier') {
214
+ return null;
215
+ }
216
+
217
+ const name = identifierName(node);
218
+ if (name === null) {
219
+ return null;
220
+ }
221
+ const target = deriveIdentifierSegmentTarget(name, rename.from, rename.to);
222
+ if (target === null) {
223
+ return null;
224
+ }
225
+
226
+ return {
227
+ from: name,
228
+ rename,
229
+ span: identifierTokenSpan(node, sourceCode, name),
230
+ to: target,
231
+ };
232
+ };
233
+
234
+ const authoredPropertyKeyParentTypes = new Set([
235
+ 'AccessorProperty',
236
+ 'MethodDefinition',
237
+ 'Property',
238
+ 'PropertyDefinition',
239
+ 'TSAbstractMethodDefinition',
240
+ 'TSAbstractPropertyDefinition',
241
+ 'TSMethodSignature',
242
+ 'TSPropertySignature',
243
+ ]);
244
+
245
+ const isAuthoredMemberName = (context: AstScopeContext): boolean =>
246
+ (context.key === 'key' &&
247
+ context.parent !== null &&
248
+ context.parent !== undefined &&
249
+ authoredPropertyKeyParentTypes.has(context.parent.type)) ||
250
+ (context.key === 'parameter' &&
251
+ context.parent?.type === 'TSParameterProperty');
252
+
253
+ const isModuleBoundaryName = (context: AstScopeContext): boolean =>
254
+ context.parent?.type === 'ImportSpecifier' ||
255
+ context.parent?.type === 'ImportDefaultSpecifier' ||
256
+ context.parent?.type === 'ImportNamespaceSpecifier' ||
257
+ context.parent?.type === 'ExportSpecifier';
258
+
259
+ const reviewReasonFor = (
260
+ context: AstScopeContext,
261
+ match: SymbolRenameMatch,
262
+ shorthandIdentifiers: ReadonlySet<string>,
263
+ targetSegments: readonly string[]
264
+ ): string | null => {
265
+ if (match.rename.match === 'identifier-segment') {
266
+ const existingTarget = targetSegments.find((segment) =>
267
+ hasIdentifierSegment(match.from, segment)
268
+ );
269
+ if (existingTarget !== undefined) {
270
+ return `already contains target segment '${existingTarget}'`;
271
+ }
272
+ }
273
+
274
+ if (shorthandIdentifiers.has(match.from)) {
275
+ return 'participates in an authored shorthand property';
276
+ }
277
+
278
+ const declaration = context.getDeclaration(match.from);
279
+ if (
280
+ declaration &&
281
+ match.rename.reviewDeclarationTypes.includes(declaration.type)
282
+ ) {
283
+ return `resolves to ${declaration.type}`;
284
+ }
285
+
286
+ if (isModuleBoundaryName(context)) {
287
+ return 'names an import or export boundary';
288
+ }
289
+
290
+ if (
291
+ context.key === 'property' &&
292
+ (context.parent?.type === 'MemberExpression' ||
293
+ context.parent?.type === 'StaticMemberExpression')
294
+ ) {
295
+ return 'is a member property with no governed declaration';
296
+ }
297
+
298
+ if (isAuthoredMemberName(context)) {
299
+ return 'is an authored property key';
300
+ }
301
+
302
+ return null;
303
+ };
304
+
305
+ const isDuplicateShorthandValueVisit = (
306
+ node: AstNode,
307
+ context: AstScopeContext
308
+ ): boolean => {
309
+ if (context.key !== 'value' || context.parent?.type !== 'Property') {
310
+ return false;
311
+ }
312
+
313
+ const key = getNodeKey(context.parent);
314
+ return key?.start === node.start && key.end === node.end;
315
+ };
316
+
317
+ const isShorthandPropertyKeyVisit = (
318
+ node: AstNode,
319
+ context: AstScopeContext
320
+ ): boolean => {
321
+ if (context.key !== 'key' || context.parent?.type !== 'Property') {
322
+ return false;
323
+ }
324
+
325
+ const value = getNodeValueNode(context.parent);
326
+ return value?.start === node.start && value.end === node.end;
327
+ };
328
+
329
+ const reviewFixFor = (
330
+ match: SymbolRenameMatch,
331
+ reviewReason: string
332
+ ): WardenFix => ({
333
+ class: 'term-rewrite',
334
+ reason: `Retired governed symbol '${match.from}' ${reviewReason}; review before migrating to '${match.to}'.`,
335
+ safety: 'review',
336
+ });
337
+
338
+ const safeFixFor = (match: SymbolRenameMatch): WardenFix | undefined => {
339
+ if (match.span === null) {
61
340
  return undefined;
62
341
  }
63
342
  return {
64
343
  class: 'term-rewrite',
65
344
  edits: [
66
345
  {
67
- end,
68
- replacement: rename.to,
69
- start: node.start,
346
+ end: match.span.end,
347
+ replacement: match.to,
348
+ start: match.span.start,
70
349
  },
71
350
  ],
72
- reason: `Retired governed symbol '${rename.from}' has a mechanical replacement '${rename.to}'.`,
351
+ reason: `Retired governed symbol '${match.from}' has a mechanical replacement '${match.to}'.`,
73
352
  safety: 'safe',
74
353
  };
75
354
  };
@@ -78,14 +357,18 @@ const diagnosticFor = (
78
357
  sourceCode: string,
79
358
  filePath: string,
80
359
  node: AstNode,
81
- rename: GovernedVocabularySymbolRename
360
+ match: SymbolRenameMatch,
361
+ reviewReason: string | null
82
362
  ): WardenDiagnostic => {
83
- const fix = safeFixFor(sourceCode, node, rename);
363
+ const fix =
364
+ reviewReason === null
365
+ ? safeFixFor(match)
366
+ : reviewFixFor(match, reviewReason);
84
367
  return {
85
368
  filePath,
86
369
  ...(fix === undefined ? {} : { fix }),
87
370
  line: offsetToLine(sourceCode, node.start),
88
- message: `Retired governed symbol '${rename.from}' should migrate to '${rename.to}'.`,
371
+ message: `Retired governed symbol '${match.from}' should migrate to '${match.to}'.`,
89
372
  rule: RULE_NAME,
90
373
  severity: 'error',
91
374
  };
@@ -97,8 +380,8 @@ export const governedSymbolResidue: WardenRule = {
97
380
  return [];
98
381
  }
99
382
 
100
- const renames = renameBySourceSymbol();
101
- if (renames.size === 0) {
383
+ const renames = activeSymbolRenames();
384
+ if (renames.length === 0) {
102
385
  return [];
103
386
  }
104
387
 
@@ -107,19 +390,43 @@ export const governedSymbolResidue: WardenRule = {
107
390
  return [];
108
391
  }
109
392
 
110
- const diagnostics: WardenDiagnostic[] = [];
111
- walk(parsed.ast, (node) => {
393
+ const shorthandIdentifiers = new Set<string>();
394
+ walkWithScopeContext(parsed.ast, (node, context) => {
395
+ if (!isShorthandPropertyKeyVisit(node, context)) {
396
+ return;
397
+ }
112
398
  const name = identifierName(node);
113
- if (name === null) {
399
+ if (name !== null) {
400
+ shorthandIdentifiers.add(name);
401
+ }
402
+ });
403
+
404
+ const diagnostics: WardenDiagnostic[] = [];
405
+ walkWithScopeContext(parsed.ast, (node, context) => {
406
+ if (isDuplicateShorthandValueVisit(node, context)) {
114
407
  return;
115
408
  }
116
409
 
117
- const rename = renames.get(name);
118
- if (rename === undefined) {
410
+ const activeMatch = renames
411
+ .map(({ rename, targetSegments }) => ({
412
+ match: resolveSymbolRenameMatch(node, sourceCode, rename),
413
+ targetSegments,
414
+ }))
415
+ .find((candidate) => candidate.match !== null);
416
+ if (activeMatch === undefined || activeMatch.match === null) {
119
417
  return;
120
418
  }
419
+ const { match, targetSegments } = activeMatch;
121
420
 
122
- diagnostics.push(diagnosticFor(sourceCode, filePath, node, rename));
421
+ diagnostics.push(
422
+ diagnosticFor(
423
+ sourceCode,
424
+ filePath,
425
+ node,
426
+ match,
427
+ reviewReasonFor(context, match, shorthandIdentifiers, targetSegments)
428
+ )
429
+ );
123
430
  });
124
431
 
125
432
  return diagnostics;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Finds implementations that return raw values instead of `Result`.
3
3
  *
4
- * Uses AST parsing to find `blaze:` bodies and check that
4
+ * Uses AST parsing to find `implementation:` bodies and check that
5
5
  * every return statement returns Result.ok(), Result.err(), ctx.compose(),
6
6
  * or a tracked Result-typed variable.
7
7
  */
@@ -9,12 +9,10 @@
9
9
  import { existsSync, readFileSync } from 'node:fs';
10
10
  import { dirname, isAbsolute, resolve } from 'node:path';
11
11
  import { escapeRegExp } from '@ontrails/core';
12
- import type { AstNode } from './ast.js';
13
12
  import {
14
13
  collectScopeFrameBindings,
15
- findBlazeBodies,
14
+ findImplementationBodies,
16
15
  findTrailDefinitions,
17
- getMemberExpression,
18
16
  getNodeAlternate,
19
17
  getNodeArgument,
20
18
  getNodeBodyNode,
@@ -37,7 +35,9 @@ import {
37
35
  parse,
38
36
  walk,
39
37
  walkWithScopes,
40
- } from './ast.js';
38
+ } from '@ontrails/source';
39
+ import type { AstNode } from '@ontrails/source';
40
+ import { getMemberExpression } from './source/stores.js';
41
41
  import { isTestFile } from './scan.js';
42
42
  import type { WardenDiagnostic, WardenRule } from './types.js';
43
43
 
@@ -61,7 +61,7 @@ const isResultMemberCall = (callee: AstNode): boolean => {
61
61
  if (objName === 'ctx' && propName === 'compose') {
62
62
  return true;
63
63
  }
64
- return propName === 'blaze';
64
+ return propName === 'implementation';
65
65
  };
66
66
 
67
67
  // ---------------------------------------------------------------------------
@@ -131,7 +131,7 @@ const isScopedResultVariableBinding = (
131
131
  *
132
132
  * When a non-empty `scopes` stack is provided, the namespace binding must not
133
133
  * be shadowed by a parameter or local declaration in any enclosing scope at
134
- * the call site. Without this check, any local `ns` (e.g. a blaze parameter
134
+ * the call site. Without this check, any local `ns` (e.g. a implementation parameter
135
135
  * named `ns`, or `const ns = ...` inside the body) would be misread as the
136
136
  * module-scope namespace import.
137
137
  */
@@ -1603,7 +1603,7 @@ const checkAllDefinitions = (
1603
1603
 
1604
1604
  for (const def of findTrailDefinitions(ast)) {
1605
1605
  const info = { id: def.id, label: 'Trail' };
1606
- for (const implValue of findBlazeBodies(def.config as AstNode)) {
1606
+ for (const implValue of findImplementationBodies(def.config as AstNode)) {
1607
1607
  checkImplementation(
1608
1608
  implValue,
1609
1609
  info,
@@ -3,10 +3,10 @@
3
3
  * backing resource accessor is missing the method the trail will call at
4
4
  * runtime.
5
5
  *
6
- * The warden cannot invoke blazes directly, but most resources declare a
6
+ * The warden cannot invoke implementations directly, but most resources declare a
7
7
  * `mock` factory that returns a structurally real accessor for testing. We
8
8
  * exploit that: invoke the mock with no arguments, look up the accessor by
9
- * contour name (which equals every CRUD-emitted trail ID's leading
9
+ * entity name (which equals every CRUD-emitted trail ID's leading
10
10
  * segments), and inspect the method keys.
11
11
  *
12
12
  * The rule is intentionally forgiving — if the mock factory is missing,
@@ -38,7 +38,7 @@ const deriveOperation = (trailId: string): StandardOp | undefined => {
38
38
  : undefined;
39
39
  };
40
40
 
41
- const deriveContourName = (trailId: string): string | undefined => {
41
+ const deriveEntityName = (trailId: string): string | undefined => {
42
42
  const lastDot = trailId.lastIndexOf('.');
43
43
  if (lastDot <= 0) {
44
44
  return undefined;
@@ -105,12 +105,12 @@ const disposeMockConnection = async (
105
105
 
106
106
  const resolveAccessor = (
107
107
  connection: unknown,
108
- contourName: string
108
+ entityName: string
109
109
  ): Record<string, unknown> | undefined => {
110
110
  if (!isPlainObject(connection)) {
111
111
  return undefined;
112
112
  }
113
- const accessor = connection[contourName];
113
+ const accessor = connection[entityName];
114
114
  return isPlainObject(accessor) ? accessor : undefined;
115
115
  };
116
116
 
@@ -124,14 +124,14 @@ const resolveAccessor = (
124
124
  */
125
125
  const inspectAccessorMethods = async (
126
126
  resource: AnyResource,
127
- contourName: string
127
+ entityName: string
128
128
  ): Promise<ReadonlySet<string> | undefined> => {
129
129
  const connection = await invokeMockSafely(resource);
130
130
  if (connection === undefined) {
131
131
  return undefined;
132
132
  }
133
133
  try {
134
- const accessor = resolveAccessor(connection, contourName);
134
+ const accessor = resolveAccessor(connection, entityName);
135
135
  if (accessor === undefined) {
136
136
  return undefined;
137
137
  }
@@ -157,7 +157,7 @@ const formatDiagnostic = (
157
157
  interface StandardOpContext {
158
158
  readonly trailId: string;
159
159
  readonly operation: StandardOp;
160
- readonly contourName: string;
160
+ readonly entityName: string;
161
161
  readonly resource: AnyResource;
162
162
  }
163
163
 
@@ -180,16 +180,16 @@ const extractStandardOpContext = (
180
180
  return undefined;
181
181
  }
182
182
  const operation = deriveOperation(trail.id);
183
- const contourName = deriveContourName(trail.id);
183
+ const entityName = deriveEntityName(trail.id);
184
184
  const resource = extractSoleResource(trail);
185
185
  if (
186
186
  operation === undefined ||
187
- contourName === undefined ||
187
+ entityName === undefined ||
188
188
  resource === undefined
189
189
  ) {
190
190
  return undefined;
191
191
  }
192
- return { contourName, operation, resource, trailId: trail.id };
192
+ return { entityName, operation, resource, trailId: trail.id };
193
193
  };
194
194
 
195
195
  const diagnoseMissingMethod = (
@@ -201,7 +201,7 @@ const diagnoseMissingMethod = (
201
201
  return undefined;
202
202
  }
203
203
  const { fallback } = expectation;
204
- const base = `resource "${ctx.resource.id}" accessor "${ctx.contourName}"`;
204
+ const base = `resource "${ctx.resource.id}" accessor "${ctx.entityName}"`;
205
205
  if (fallback === undefined) {
206
206
  return formatDiagnostic(
207
207
  ctx.trailId,
@@ -234,7 +234,7 @@ const evaluateTrail = async (
234
234
  if (ctx === undefined) {
235
235
  return [];
236
236
  }
237
- const methods = await inspectAccessorMethods(ctx.resource, ctx.contourName);
237
+ const methods = await inspectAccessorMethods(ctx.resource, ctx.entityName);
238
238
  if (methods === undefined) {
239
239
  return [];
240
240
  }
@@ -248,7 +248,7 @@ const evaluateTrail = async (
248
248
 
249
249
  /**
250
250
  * Topo-aware rule that flags CRUD trails whose backing accessor is missing
251
- * the method invoked by the synthesized blaze.
251
+ * the method invoked by the synthesized implementation.
252
252
  *
253
253
  * @remarks
254
254
  * Introspects each resource's `mock()` factory to determine the accessor
@@ -266,7 +266,7 @@ export const incompleteAccessorForStandardOp: TopoAwareWardenRule = {
266
266
  return diagnostics;
267
267
  },
268
268
  description:
269
- 'Flag CRUD-pattern trails whose resource accessor lacks the method the synthesized blaze will call at runtime.',
269
+ 'Flag CRUD-pattern trails whose resource accessor lacks the method the synthesized implementation will call at runtime.',
270
270
  name: RULE_NAME,
271
271
  severity: 'error',
272
272
  };