@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,16 @@
1
- import { deriveTopoGraph } from '@ontrails/topographer';
2
- import type { TopoGraph, TopoGraphEntry } from '@ontrails/topographer';
1
+ import { deriveTopoGraph } from '@ontrails/topography';
2
+ import type { TopoGraph, TopoGraphEntry } from '@ontrails/topography';
3
3
 
4
4
  import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
5
5
 
6
6
  const RULE_NAME = 'duplicate-public-contract';
7
7
  const TOPO_FILE = '<topo>';
8
+ const INVERSE_OPERATION_PAIRS = new Map([
9
+ ['archive', 'restore'],
10
+ ['disable', 'enable'],
11
+ ['pause', 'resume'],
12
+ ['star', 'unstar'],
13
+ ]);
8
14
 
9
15
  const resolveGraph = (
10
16
  topo: Parameters<TopoAwareWardenRule['checkTopo']>[0],
@@ -30,9 +36,9 @@ const contractFingerprint = (entry: TopoGraphEntry): string =>
30
36
  JSON.stringify(
31
37
  canonicalize({
32
38
  composes: entry.composes,
33
- contours: entry.contours,
34
39
  detours: entry.detours,
35
40
  dryRunCapable: entry.dryRunCapable,
41
+ entities: entry.entities,
36
42
  fires: entry.fires,
37
43
  idempotent: entry.idempotent,
38
44
  input: entry.input,
@@ -60,10 +66,46 @@ const renderTrailIds = (trailIds: readonly string[]): string =>
60
66
  .map((trailId) => `"${trailId}"`)
61
67
  .join(', ');
62
68
 
69
+ const splitOperation = (
70
+ trailId: string
71
+ ): { readonly scope: string; readonly operation: string } | undefined => {
72
+ const separatorIndex = trailId.lastIndexOf('.');
73
+
74
+ if (separatorIndex <= 0 || separatorIndex === trailId.length - 1) {
75
+ return undefined;
76
+ }
77
+
78
+ return {
79
+ operation: trailId.slice(separatorIndex + 1),
80
+ scope: trailId.slice(0, separatorIndex),
81
+ };
82
+ };
83
+
84
+ const areInverseOperations = (left: string, right: string): boolean =>
85
+ INVERSE_OPERATION_PAIRS.get(left) === right ||
86
+ INVERSE_OPERATION_PAIRS.get(right) === left;
87
+
88
+ const isAcceptedInverseOperationPair = (
89
+ trailIds: readonly string[]
90
+ ): boolean => {
91
+ if (trailIds.length !== 2) {
92
+ return false;
93
+ }
94
+
95
+ const [left, right] = trailIds.map(splitOperation);
96
+
97
+ return Boolean(
98
+ left &&
99
+ right &&
100
+ left.scope === right.scope &&
101
+ areInverseOperations(left.operation, right.operation)
102
+ );
103
+ };
104
+
63
105
  const buildDiagnostic = (trailIds: readonly string[]): WardenDiagnostic => ({
64
106
  filePath: TOPO_FILE,
65
107
  line: 1,
66
- message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources, contours, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.`,
108
+ message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources, entities, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.`,
67
109
  rule: RULE_NAME,
68
110
  severity: 'warn',
69
111
  });
@@ -82,7 +124,10 @@ export const duplicatePublicContract: TopoAwareWardenRule = {
82
124
  }
83
125
 
84
126
  return [...groups.values()]
85
- .filter((trailIds) => trailIds.length > 1)
127
+ .filter(
128
+ (trailIds) =>
129
+ trailIds.length > 1 && !isAcceptedInverseOperationPair(trailIds)
130
+ )
86
131
  .map(buildDiagnostic);
87
132
  },
88
133
  description:
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  buildUserNamespaceContext,
3
- collectContourDefinitionIds,
3
+ collectEntityDefinitionIds,
4
4
  collectImportAliasMap,
5
- collectNamedContourIds,
6
- deriveContourIdentifierName,
5
+ collectNamedEntityIds,
6
+ deriveEntityIdentifierName,
7
+ isUserNamespaceReceiverAllowed,
8
+ } from './source/entities.js';
9
+ import {
7
10
  extractFirstStringArg,
8
11
  findConfigProperty,
9
12
  findTrailDefinitions,
@@ -12,12 +15,12 @@ import {
12
15
  getNodeProperty,
13
16
  identifierName,
14
17
  isMemberAccessNonComputed,
15
- isUserNamespaceReceiverAllowed,
16
18
  offsetToLine,
17
19
  parse,
18
- } from './ast.js';
19
- import type { AstNode, TrailDefinition, UserNamespaceContext } from './ast.js';
20
- import { mergeKnownContourIds } from './contour-ids.js';
20
+ } from '@ontrails/source';
21
+ import type { AstNode, TrailDefinition } from '@ontrails/source';
22
+ import type { UserNamespaceContext } from './source/entities.js';
23
+ import { mergeKnownEntityIds } from './entity-ids.js';
21
24
  import { isTestFile } from './scan.js';
22
25
  import type {
23
26
  ProjectAwareWardenRule,
@@ -25,17 +28,17 @@ import type {
25
28
  WardenDiagnostic,
26
29
  } from './types.js';
27
30
 
28
- const isContourCall = (node: AstNode): boolean =>
31
+ const isEntityCall = (node: AstNode): boolean =>
29
32
  node.type === 'CallExpression' &&
30
- identifierName(getNodeCallee(node)) === 'contour';
33
+ identifierName(getNodeCallee(node)) === 'entity';
31
34
 
32
- const getContourElements = (config: AstNode): readonly AstNode[] => {
33
- const contoursProp = findConfigProperty(config, 'contours');
34
- if (!contoursProp) {
35
+ const getEntityElements = (config: AstNode): readonly AstNode[] => {
36
+ const entitiesProp = findConfigProperty(config, 'entities');
37
+ if (!entitiesProp) {
35
38
  return [];
36
39
  }
37
40
 
38
- const arrayNode = contoursProp.value;
41
+ const arrayNode = entitiesProp.value;
39
42
  if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
40
43
  return [];
41
44
  }
@@ -47,13 +50,13 @@ const getContourElements = (config: AstNode): readonly AstNode[] => {
47
50
  };
48
51
 
49
52
  /**
50
- * Resolve `contours.user` to its contour name. When `userNamespace` carries a
53
+ * Resolve `entities.user` to its entity name. When `userNamespace` carries a
51
54
  * scope-aware `safeMemberStarts` set, the member access must appear in it —
52
- * rejecting cases where `contours` is shadowed by a local binding such as a
53
- * function parameter or `const contours = ...`. Without the set, falls back
55
+ * rejecting cases where `entities` is shadowed by a local binding such as a
56
+ * function parameter or `const entities = ...`. Without the set, falls back
54
57
  * to the bare name check for backward compatibility.
55
58
  */
56
- const resolveNamespaceMemberContourName = (
59
+ const resolveNamespaceMemberEntityName = (
57
60
  element: AstNode,
58
61
  userNamespace: UserNamespaceContext
59
62
  ): string | null => {
@@ -72,27 +75,27 @@ const resolveNamespaceMemberContourName = (
72
75
  return property ? identifierName(property) : null;
73
76
  };
74
77
 
75
- const resolveDeclaredContourName = (
78
+ const resolveDeclaredEntityName = (
76
79
  element: AstNode,
77
- contourIdsByName: ReadonlyMap<string, string>,
78
- knownContourIds?: ReadonlySet<string>,
80
+ entityIdsByName: ReadonlyMap<string, string>,
81
+ knownEntityIds?: ReadonlySet<string>,
79
82
  importAliases?: ReadonlyMap<string, string>,
80
83
  userNamespace?: UserNamespaceContext
81
84
  ): string | null => {
82
85
  if (element.type === 'Identifier') {
83
86
  const name = identifierName(element);
84
87
  return name
85
- ? deriveContourIdentifierName(
88
+ ? deriveEntityIdentifierName(
86
89
  name,
87
- contourIdsByName,
88
- knownContourIds,
90
+ entityIdsByName,
91
+ knownEntityIds,
89
92
  importAliases
90
93
  )
91
94
  : null;
92
95
  }
93
96
 
94
97
  if (userNamespace && userNamespace.bindings.size > 0) {
95
- const namespaceTarget = resolveNamespaceMemberContourName(
98
+ const namespaceTarget = resolveNamespaceMemberEntityName(
96
99
  element,
97
100
  userNamespace
98
101
  );
@@ -101,40 +104,40 @@ const resolveDeclaredContourName = (
101
104
  }
102
105
  }
103
106
 
104
- return isContourCall(element) ? extractFirstStringArg(element) : null;
107
+ return isEntityCall(element) ? extractFirstStringArg(element) : null;
105
108
  };
106
109
 
107
- const extractDeclaredContourNames = (
110
+ const extractDeclaredEntityNames = (
108
111
  config: AstNode,
109
- contourIdsByName: ReadonlyMap<string, string>,
110
- knownContourIds?: ReadonlySet<string>,
112
+ entityIdsByName: ReadonlyMap<string, string>,
113
+ knownEntityIds?: ReadonlySet<string>,
111
114
  importAliases?: ReadonlyMap<string, string>,
112
115
  userNamespace?: UserNamespaceContext
113
116
  ): readonly string[] => [
114
117
  ...new Set(
115
- getContourElements(config).flatMap((element) => {
116
- const contourName = resolveDeclaredContourName(
118
+ getEntityElements(config).flatMap((element) => {
119
+ const entityName = resolveDeclaredEntityName(
117
120
  element,
118
- contourIdsByName,
119
- knownContourIds,
121
+ entityIdsByName,
122
+ knownEntityIds,
120
123
  importAliases,
121
124
  userNamespace
122
125
  );
123
- return contourName ? [contourName] : [];
126
+ return entityName ? [entityName] : [];
124
127
  })
125
128
  ),
126
129
  ];
127
130
 
128
- const buildMissingContourDiagnostic = (
131
+ const buildMissingEntityDiagnostic = (
129
132
  trailId: string,
130
- contourName: string,
133
+ entityName: string,
131
134
  filePath: string,
132
135
  line: number
133
136
  ): WardenDiagnostic => ({
134
137
  filePath,
135
138
  line,
136
- message: `Trail "${trailId}" declares contour "${contourName}" which is not defined in the project. Define it with contour('${contourName}', ...) and include it in the topo, or fix the contours entry if this is a typo.`,
137
- rule: 'contour-exists',
139
+ message: `Trail "${trailId}" declares entity "${entityName}" which is not defined in the project. Define it with entity('${entityName}', ...) and include it in the topo, or fix the entities entry if this is a typo.`,
140
+ rule: 'entity-exists',
138
141
  severity: 'error',
139
142
  });
140
143
 
@@ -142,8 +145,8 @@ const buildDiagnosticsForDefinition = (
142
145
  definition: TrailDefinition,
143
146
  sourceCode: string,
144
147
  filePath: string,
145
- knownContourIds: ReadonlySet<string>,
146
- contourIdsByName: ReadonlyMap<string, string>,
148
+ knownEntityIds: ReadonlySet<string>,
149
+ entityIdsByName: ReadonlyMap<string, string>,
147
150
  importAliases: ReadonlyMap<string, string>,
148
151
  userNamespace: UserNamespaceContext
149
152
  ): readonly WardenDiagnostic[] => {
@@ -152,19 +155,19 @@ const buildDiagnosticsForDefinition = (
152
155
  }
153
156
 
154
157
  const line = offsetToLine(sourceCode, definition.start);
155
- return extractDeclaredContourNames(
158
+ return extractDeclaredEntityNames(
156
159
  definition.config,
157
- contourIdsByName,
158
- knownContourIds,
160
+ entityIdsByName,
161
+ knownEntityIds,
159
162
  importAliases,
160
163
  userNamespace
161
- ).flatMap((contourName) =>
162
- knownContourIds.has(contourName)
164
+ ).flatMap((entityName) =>
165
+ knownEntityIds.has(entityName)
163
166
  ? []
164
167
  : [
165
- buildMissingContourDiagnostic(
168
+ buildMissingEntityDiagnostic(
166
169
  definition.id,
167
- contourName,
170
+ entityName,
168
171
  filePath,
169
172
  line
170
173
  ),
@@ -172,13 +175,13 @@ const buildDiagnosticsForDefinition = (
172
175
  );
173
176
  };
174
177
 
175
- const buildContourDiagnostics = (
178
+ const buildEntityDiagnostics = (
176
179
  ast: AstNode,
177
180
  sourceCode: string,
178
181
  filePath: string,
179
- knownContourIds: ReadonlySet<string>
182
+ knownEntityIds: ReadonlySet<string>
180
183
  ): readonly WardenDiagnostic[] => {
181
- const contourIdsByName = collectNamedContourIds(ast);
184
+ const entityIdsByName = collectNamedEntityIds(ast);
182
185
  const importAliases = collectImportAliasMap(ast);
183
186
  const userNamespace = buildUserNamespaceContext(ast);
184
187
 
@@ -187,43 +190,43 @@ const buildContourDiagnostics = (
187
190
  definition,
188
191
  sourceCode,
189
192
  filePath,
190
- knownContourIds,
191
- contourIdsByName,
193
+ knownEntityIds,
194
+ entityIdsByName,
192
195
  importAliases,
193
196
  userNamespace
194
197
  )
195
198
  );
196
199
  };
197
200
 
198
- const checkContourDeclarations = (
201
+ const checkEntityDeclarations = (
199
202
  ast: AstNode,
200
203
  sourceCode: string,
201
204
  filePath: string,
202
- knownContourIds: ReadonlySet<string>
205
+ knownEntityIds: ReadonlySet<string>
203
206
  ): readonly WardenDiagnostic[] => {
204
207
  if (isTestFile(filePath)) {
205
208
  return [];
206
209
  }
207
210
 
208
- return buildContourDiagnostics(ast, sourceCode, filePath, knownContourIds);
211
+ return buildEntityDiagnostics(ast, sourceCode, filePath, knownEntityIds);
209
212
  };
210
213
 
211
214
  /**
212
- * Checks that every contour declared in a trail `contours` array resolves to a
213
- * known contour definition.
215
+ * Checks that every entity declared in a trail `entities` array resolves to a
216
+ * known entity definition.
214
217
  */
215
- export const contourExists: ProjectAwareWardenRule = {
218
+ export const entityExists: ProjectAwareWardenRule = {
216
219
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
217
220
  const ast = parse(filePath, sourceCode);
218
221
  if (!ast) {
219
222
  return [];
220
223
  }
221
224
 
222
- return checkContourDeclarations(
225
+ return checkEntityDeclarations(
223
226
  ast,
224
227
  sourceCode,
225
228
  filePath,
226
- collectContourDefinitionIds(ast)
229
+ collectEntityDefinitionIds(ast)
227
230
  );
228
231
  },
229
232
  checkWithContext(
@@ -236,16 +239,16 @@ export const contourExists: ProjectAwareWardenRule = {
236
239
  return [];
237
240
  }
238
241
 
239
- const localContourIds = collectContourDefinitionIds(ast);
240
- return checkContourDeclarations(
242
+ const localEntityIds = collectEntityDefinitionIds(ast);
243
+ return checkEntityDeclarations(
241
244
  ast,
242
245
  sourceCode,
243
246
  filePath,
244
- mergeKnownContourIds(localContourIds, context.knownContourIds)
247
+ mergeKnownEntityIds(localEntityIds, context.knownEntityIds)
245
248
  );
246
249
  },
247
250
  description:
248
- 'Ensure every contour declared on a trail resolves to a known contour definition.',
249
- name: 'contour-exists',
251
+ 'Ensure every entity declared on a trail resolves to a known entity definition.',
252
+ name: 'entity-exists',
250
253
  severity: 'error',
251
254
  };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Merge a file's locally-defined entity IDs with the project-wide set.
3
+ *
4
+ * Rules that run with a `ProjectContext` need to treat both local and
5
+ * project-wide entity definitions as "known" so that declarations and
6
+ * references resolve correctly. When no project context is available — e.g.
7
+ * single-file lint runs via `check` — the local set is returned as-is.
8
+ */
9
+ export const mergeKnownEntityIds = (
10
+ localEntityIds: ReadonlySet<string>,
11
+ projectEntityIds?: ReadonlySet<string>
12
+ ): ReadonlySet<string> =>
13
+ projectEntityIds
14
+ ? new Set([...projectEntityIds, ...localEntityIds])
15
+ : localEntityIds;
@@ -18,8 +18,8 @@ import {
18
18
  offsetToLine,
19
19
  parse,
20
20
  walk,
21
- } from './ast.js';
22
- import type { AstNode } from './ast.js';
21
+ } from '@ontrails/source';
22
+ import type { AstNode } from '@ontrails/source';
23
23
  import { isTestFile } from './scan.js';
24
24
  import type { WardenDiagnostic, WardenRule } from './types.js';
25
25