@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,310 @@
|
|
|
1
|
+
/** Warden-private store/factory pattern helpers. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractBindingName,
|
|
5
|
+
findConfigProperty,
|
|
6
|
+
getPropertyName,
|
|
7
|
+
identifierName,
|
|
8
|
+
walk,
|
|
9
|
+
} from '@ontrails/source';
|
|
10
|
+
import type { AstNode } from '@ontrails/source';
|
|
11
|
+
|
|
12
|
+
export interface StoreTableDefinition {
|
|
13
|
+
/** Table name declared inside store({ ... }). */
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/**
|
|
16
|
+
* Local binding name of the enclosing `store(...)` declaration, if the
|
|
17
|
+
* `store(...)` call is bound to a `const`/`let`/`var` (e.g. `db` in
|
|
18
|
+
* `const db = store({ ... })`). Null for anonymous stores.
|
|
19
|
+
*/
|
|
20
|
+
readonly storeBinding: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Stable composite key for this table in the form `${storeBinding}:${name}`,
|
|
23
|
+
* falling back to the bare `name` when the store is anonymous. Use this for
|
|
24
|
+
* compose-rule / compose-file keying so two stores with the same table name
|
|
25
|
+
* never collide.
|
|
26
|
+
*/
|
|
27
|
+
readonly key: string;
|
|
28
|
+
/** Start offset of the table property declaration. */
|
|
29
|
+
readonly start: number;
|
|
30
|
+
/** Whether the authored table opts into version tracking. */
|
|
31
|
+
readonly versioned: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a composite key for a store table: `${storeBinding}:${tableName}`,
|
|
36
|
+
* falling back to the bare `tableName` when the enclosing store has no local
|
|
37
|
+
* binding. Centralized so rule keying stays stable.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The key is intentionally file-local (no module path prefix). Compose-file
|
|
41
|
+
* aggregation in `ProjectContext` merges keys from all files, so two files
|
|
42
|
+
* with `const db = store({ notes: ... })` both produce `db:notes` — this is
|
|
43
|
+
* the desired behavior because the warden checks for *pattern completeness*
|
|
44
|
+
* across the project and matching keys signals that the same logical table
|
|
45
|
+
* is covered. If two genuinely different tables share a binding and name,
|
|
46
|
+
* that is a code-level naming collision the developer should resolve.
|
|
47
|
+
*/
|
|
48
|
+
export const makeStoreTableKey = (
|
|
49
|
+
storeBinding: string | null,
|
|
50
|
+
tableName: string
|
|
51
|
+
): string => (storeBinding ? `${storeBinding}:${tableName}` : tableName);
|
|
52
|
+
|
|
53
|
+
const isBooleanLiteral = (node: AstNode | undefined): boolean =>
|
|
54
|
+
Boolean(
|
|
55
|
+
node &&
|
|
56
|
+
((node.type === 'BooleanLiteral' &&
|
|
57
|
+
(node as unknown as { value?: unknown }).value === true) ||
|
|
58
|
+
(node.type === 'Literal' &&
|
|
59
|
+
(node as unknown as { value?: unknown }).value === true))
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a node is a `CallExpression` to the identifier `name`.
|
|
64
|
+
*
|
|
65
|
+
* e.g. `isNamedCall(node, 'store')` matches `store({...})` but not
|
|
66
|
+
* `someObj.store()` or `storeAlt()`.
|
|
67
|
+
*/
|
|
68
|
+
export const isNamedCall = (node: AstNode | undefined, name: string): boolean =>
|
|
69
|
+
!!node &&
|
|
70
|
+
node.type === 'CallExpression' &&
|
|
71
|
+
identifierName((node as unknown as { callee?: AstNode }).callee) === name;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Narrow a member-expression node (`a.b` or `a['b']`) to its `object` /
|
|
75
|
+
* `property` pair, returning `null` for anything else.
|
|
76
|
+
*/
|
|
77
|
+
export const getMemberExpression = (
|
|
78
|
+
node: AstNode | undefined
|
|
79
|
+
): { readonly object?: AstNode; readonly property?: AstNode } | null => {
|
|
80
|
+
if (
|
|
81
|
+
!node ||
|
|
82
|
+
(node.type !== 'MemberExpression' && node.type !== 'StaticMemberExpression')
|
|
83
|
+
) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return node as unknown as {
|
|
88
|
+
readonly object?: AstNode;
|
|
89
|
+
readonly property?: AstNode;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a `<store>.tables.<name>` member expression to its store binding
|
|
95
|
+
* and table name.
|
|
96
|
+
*
|
|
97
|
+
* Returns `null` for anything that isn't a two-level member access ending in
|
|
98
|
+
* `.tables.<name>`. The store binding is the identifier of the object owning
|
|
99
|
+
* `.tables` — typically the local binding from `const db = store(...)`.
|
|
100
|
+
*/
|
|
101
|
+
export const extractStoreTableFromMember = (
|
|
102
|
+
node: AstNode | undefined
|
|
103
|
+
): {
|
|
104
|
+
readonly storeBinding: string | null;
|
|
105
|
+
readonly tableName: string;
|
|
106
|
+
} | null => {
|
|
107
|
+
const member = getMemberExpression(node);
|
|
108
|
+
const tableName = member ? getPropertyName(member.property) : null;
|
|
109
|
+
const tablesMember = member ? getMemberExpression(member.object) : null;
|
|
110
|
+
if (!tableName || !tablesMember) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (getPropertyName(tablesMember.property) !== 'tables') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const storeBinding = identifierName(tablesMember.object) ?? null;
|
|
119
|
+
return { storeBinding, tableName };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Collect `const foo = <store>.tables.<name>` bindings from a parsed file,
|
|
124
|
+
* keyed by the local binding name. Values are the composite table key
|
|
125
|
+
* (`${storeBinding}:${tableName}`) so callers can dedupe across stores that
|
|
126
|
+
* share a table name.
|
|
127
|
+
*/
|
|
128
|
+
export const collectNamedStoreTableIds = (
|
|
129
|
+
ast: AstNode
|
|
130
|
+
): ReadonlyMap<string, string> => {
|
|
131
|
+
const ids = new Map<string, string>();
|
|
132
|
+
|
|
133
|
+
walk(ast, (node) => {
|
|
134
|
+
if (node.type !== 'VariableDeclarator') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { id, init } = node as unknown as {
|
|
139
|
+
readonly id?: AstNode;
|
|
140
|
+
readonly init?: AstNode;
|
|
141
|
+
};
|
|
142
|
+
const name = extractBindingName(id);
|
|
143
|
+
const table = extractStoreTableFromMember(init);
|
|
144
|
+
if (name && table) {
|
|
145
|
+
ids.set(name, makeStoreTableKey(table.storeBinding, table.tableName));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return ids;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve an argument node to a composite store-table key
|
|
154
|
+
* (`${storeBinding}:${tableName}` or bare `tableName` when anonymous).
|
|
155
|
+
*
|
|
156
|
+
* Handles the two authoring patterns:
|
|
157
|
+
* - direct member access: `db.tables.notes`
|
|
158
|
+
* - identifier reference: `const notesTable = db.tables.notes; crud(notesTable, …)`
|
|
159
|
+
*/
|
|
160
|
+
export const deriveStoreTableId = (
|
|
161
|
+
node: AstNode | undefined,
|
|
162
|
+
namedStoreTableIds: ReadonlyMap<string, string>
|
|
163
|
+
): string | null => {
|
|
164
|
+
if (!node) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (node.type === 'Identifier') {
|
|
169
|
+
const name = identifierName(node);
|
|
170
|
+
return name ? (namedStoreTableIds.get(name) ?? null) : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const member = extractStoreTableFromMember(node);
|
|
174
|
+
return member
|
|
175
|
+
? makeStoreTableKey(member.storeBinding, member.tableName)
|
|
176
|
+
: null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const extractStoreTableDefinitions = (
|
|
180
|
+
node: AstNode,
|
|
181
|
+
storeBinding: string | null
|
|
182
|
+
): readonly StoreTableDefinition[] => {
|
|
183
|
+
if (!isNamedCall(node, 'store')) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const [tablesArg] = ((node as unknown as { arguments?: readonly AstNode[] })
|
|
188
|
+
.arguments ?? []) as readonly AstNode[];
|
|
189
|
+
if (!tablesArg || tablesArg.type !== 'ObjectExpression') {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const properties = tablesArg['properties'] as readonly AstNode[] | undefined;
|
|
194
|
+
if (!properties) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return properties.flatMap((property) => {
|
|
199
|
+
if (property.type !== 'Property') {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const name = getPropertyName(property.key);
|
|
204
|
+
const value = property.value as AstNode | undefined;
|
|
205
|
+
if (!name || value?.type !== 'ObjectExpression') {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const versionedProp = findConfigProperty(value, 'versioned');
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
key: makeStoreTableKey(storeBinding, name),
|
|
213
|
+
name,
|
|
214
|
+
start: property.start,
|
|
215
|
+
storeBinding,
|
|
216
|
+
versioned: isBooleanLiteral(
|
|
217
|
+
versionedProp?.value as AstNode | undefined
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const findStoreTableDefinitions = (
|
|
225
|
+
ast: AstNode
|
|
226
|
+
): readonly StoreTableDefinition[] => {
|
|
227
|
+
const definitions: StoreTableDefinition[] = [];
|
|
228
|
+
const seenStoreCalls = new WeakSet<AstNode>();
|
|
229
|
+
|
|
230
|
+
// First pass: bound stores (walk VariableDeclarators so we know the binding).
|
|
231
|
+
walk(ast, (node) => {
|
|
232
|
+
if (node.type !== 'VariableDeclarator') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { id, init } = node as unknown as {
|
|
237
|
+
readonly id?: AstNode;
|
|
238
|
+
readonly init?: AstNode;
|
|
239
|
+
};
|
|
240
|
+
if (!init || !isNamedCall(init, 'store')) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
seenStoreCalls.add(init);
|
|
245
|
+
const storeBinding = extractBindingName(id);
|
|
246
|
+
definitions.push(...extractStoreTableDefinitions(init, storeBinding));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Second pass: anonymous `store({...})` calls not bound to a variable
|
|
250
|
+
// (e.g. an inline default export). Use the bare table name as the key.
|
|
251
|
+
walk(ast, (node) => {
|
|
252
|
+
if (!isNamedCall(node, 'store') || seenStoreCalls.has(node)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
definitions.push(...extractStoreTableDefinitions(node, null));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return definitions;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const collectCrudTableIds = (ast: AstNode): ReadonlySet<string> => {
|
|
262
|
+
const ids = new Set<string>();
|
|
263
|
+
const namedStoreTableIds = collectNamedStoreTableIds(ast);
|
|
264
|
+
|
|
265
|
+
walk(ast, (node) => {
|
|
266
|
+
if (!isNamedCall(node, 'crud')) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const [tableArg] = ((node as unknown as { arguments?: readonly AstNode[] })
|
|
271
|
+
.arguments ?? []) as readonly AstNode[];
|
|
272
|
+
const tableId = deriveStoreTableId(tableArg, namedStoreTableIds);
|
|
273
|
+
if (tableId) {
|
|
274
|
+
ids.add(tableId);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return ids;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const collectReconcileTableIds = (ast: AstNode): ReadonlySet<string> => {
|
|
282
|
+
const ids = new Set<string>();
|
|
283
|
+
const namedStoreTableIds = collectNamedStoreTableIds(ast);
|
|
284
|
+
|
|
285
|
+
walk(ast, (node) => {
|
|
286
|
+
if (!isNamedCall(node, 'reconcile')) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const [configArg] = ((
|
|
291
|
+
node as unknown as {
|
|
292
|
+
arguments?: readonly AstNode[];
|
|
293
|
+
}
|
|
294
|
+
).arguments ?? []) as readonly AstNode[];
|
|
295
|
+
if (!configArg || configArg.type !== 'ObjectExpression') {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const tableProp = findConfigProperty(configArg, 'table');
|
|
300
|
+
const tableId = deriveStoreTableId(
|
|
301
|
+
tableProp?.value as AstNode | undefined,
|
|
302
|
+
namedStoreTableIds
|
|
303
|
+
);
|
|
304
|
+
if (tableId) {
|
|
305
|
+
ids.add(tableId);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return ids;
|
|
310
|
+
};
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* Dynamic IDs and generic framework internals remain outside its scope.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { collectNamedResourceIds } from './source/resources.js';
|
|
9
10
|
import {
|
|
10
|
-
collectNamedResourceIds,
|
|
11
11
|
extractFirstStringArg,
|
|
12
|
-
findBlazeBodies,
|
|
13
12
|
findConfigProperty,
|
|
13
|
+
findImplementationBodies,
|
|
14
14
|
findTrailDefinitions,
|
|
15
15
|
getNodeCallee,
|
|
16
16
|
getNodeId,
|
|
@@ -32,8 +32,8 @@ import {
|
|
|
32
32
|
walk,
|
|
33
33
|
walkScope,
|
|
34
34
|
walkWithScopes,
|
|
35
|
-
} from '
|
|
36
|
-
import type { AstNode } from '
|
|
35
|
+
} from '@ontrails/source';
|
|
36
|
+
import type { AstNode } from '@ontrails/source';
|
|
37
37
|
import { isFrameworkInternalFile, isTestFile } from './scan.js';
|
|
38
38
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
39
39
|
|
|
@@ -125,16 +125,20 @@ const extractDeclaredStaticResources = (
|
|
|
125
125
|
return name ? [{ id: resourceIdsByName.get(name) ?? null, name }] : [];
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
const extractContextParamNode = (
|
|
129
|
-
|
|
128
|
+
const extractContextParamNode = (
|
|
129
|
+
implementationBody: AstNode
|
|
130
|
+
): AstNode | null => {
|
|
131
|
+
const params = implementationBody['params'] as readonly AstNode[] | undefined;
|
|
130
132
|
if (!params || params.length < 2) {
|
|
131
133
|
return null;
|
|
132
134
|
}
|
|
133
135
|
return params[1] ?? null;
|
|
134
136
|
};
|
|
135
137
|
|
|
136
|
-
const extractContextParamName = (
|
|
137
|
-
|
|
138
|
+
const extractContextParamName = (
|
|
139
|
+
implementationBody: AstNode
|
|
140
|
+
): string | null => {
|
|
141
|
+
const param = extractContextParamNode(implementationBody);
|
|
138
142
|
if (!param) {
|
|
139
143
|
return null;
|
|
140
144
|
}
|
|
@@ -340,7 +344,7 @@ const collectResourceLookups = (
|
|
|
340
344
|
): readonly ResourceLookup[] => {
|
|
341
345
|
const lookups: ResourceLookup[] = [];
|
|
342
346
|
|
|
343
|
-
for (const body of
|
|
347
|
+
for (const body of findImplementationBodies(config)) {
|
|
344
348
|
const ctxNames = buildCtxNames(body);
|
|
345
349
|
const resourceAliases = new Set([
|
|
346
350
|
...collectParamResourceAliases(body),
|
|
@@ -465,7 +469,7 @@ const collectInlineDependencyConstructions = (
|
|
|
465
469
|
): readonly InlineDependencyConstruction[] => {
|
|
466
470
|
const constructions: InlineDependencyConstruction[] = [];
|
|
467
471
|
|
|
468
|
-
for (const body of
|
|
472
|
+
for (const body of findImplementationBodies(config)) {
|
|
469
473
|
walkWithScopes(
|
|
470
474
|
body,
|
|
471
475
|
(node, scopes) => {
|
|
@@ -517,7 +521,7 @@ const buildInlineDependencyDiagnostic = (
|
|
|
517
521
|
line: offsetToLine(sourceCode, construction.start),
|
|
518
522
|
message:
|
|
519
523
|
`Trail "${trailId}": ${construction.rendered} constructs an external dependency ` +
|
|
520
|
-
'inside
|
|
524
|
+
'inside implementation logic. Move the client behind a resource definition and declare it in resources.',
|
|
521
525
|
rule: RULE_NAME,
|
|
522
526
|
severity: 'warn',
|
|
523
527
|
});
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
SurfaceOverlayBindings,
|
|
28
28
|
Topo,
|
|
29
29
|
} from '@ontrails/core';
|
|
30
|
-
import type { TopoGraph } from '@ontrails/
|
|
30
|
+
import type { TopoGraph } from '@ontrails/topography';
|
|
31
31
|
|
|
32
32
|
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
33
33
|
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
offsetToLine,
|
|
19
19
|
parse,
|
|
20
20
|
walk,
|
|
21
|
-
} from '
|
|
22
|
-
import type { AstNode } from '
|
|
21
|
+
} from '@ontrails/source';
|
|
22
|
+
import type { AstNode } from '@ontrails/source';
|
|
23
23
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
24
24
|
|
|
25
25
|
const RULE_NAME = 'surface-trailhead-coherence';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
extractStringOrTemplateLiteral,
|
|
3
|
-
findBlazeBodies,
|
|
4
3
|
findConfigProperty,
|
|
4
|
+
findImplementationBodies,
|
|
5
5
|
findTrailDefinitions,
|
|
6
6
|
getNodeArgument,
|
|
7
7
|
getNodeArguments,
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
offsetToLine,
|
|
27
27
|
parse,
|
|
28
28
|
walkScope,
|
|
29
|
-
} from '
|
|
30
|
-
import type { AstNode } from '
|
|
29
|
+
} from '@ontrails/source';
|
|
30
|
+
import type { AstNode } from '@ontrails/source';
|
|
31
31
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
32
32
|
|
|
33
33
|
const RULE_NAME = 'trail-fork-coaching';
|
|
@@ -49,7 +49,7 @@ interface SchemaBindingRecord {
|
|
|
49
49
|
|
|
50
50
|
type SchemaBindings = ReadonlyMap<string, readonly SchemaBindingRecord[]>;
|
|
51
51
|
|
|
52
|
-
interface
|
|
52
|
+
interface ImplementationInputBindings {
|
|
53
53
|
readonly aliases: ReadonlyMap<string, string>;
|
|
54
54
|
readonly inputParamName: string | null;
|
|
55
55
|
}
|
|
@@ -344,8 +344,8 @@ const findControlFields = (
|
|
|
344
344
|
return fields;
|
|
345
345
|
};
|
|
346
346
|
|
|
347
|
-
const
|
|
348
|
-
getNodeParams(
|
|
347
|
+
const implementationParams = (implementation: AstNode): readonly AstNode[] =>
|
|
348
|
+
getNodeParams(implementation) ?? [];
|
|
349
349
|
|
|
350
350
|
const memberFieldName = (
|
|
351
351
|
node: AstNode | undefined,
|
|
@@ -397,13 +397,13 @@ const collectDestructuredFieldAliasesFromPattern = (
|
|
|
397
397
|
};
|
|
398
398
|
|
|
399
399
|
const collectDestructuredFieldAliases = (
|
|
400
|
-
|
|
400
|
+
implementation: AstNode,
|
|
401
401
|
inputParamName: string,
|
|
402
402
|
fields: ReadonlySet<string>
|
|
403
403
|
): ReadonlyMap<string, string> => {
|
|
404
404
|
const aliases = new Map<string, string>();
|
|
405
405
|
|
|
406
|
-
walkScope(
|
|
406
|
+
walkScope(implementation, (node) => {
|
|
407
407
|
if (node.type !== 'VariableDeclarator') {
|
|
408
408
|
return;
|
|
409
409
|
}
|
|
@@ -427,15 +427,19 @@ const collectDestructuredFieldAliases = (
|
|
|
427
427
|
return aliases;
|
|
428
428
|
};
|
|
429
429
|
|
|
430
|
-
const
|
|
431
|
-
|
|
430
|
+
const collectImplementationInputBindings = (
|
|
431
|
+
implementation: AstNode,
|
|
432
432
|
fields: ReadonlySet<string>
|
|
433
|
-
):
|
|
434
|
-
const [inputParam] =
|
|
433
|
+
): ImplementationInputBindings | null => {
|
|
434
|
+
const [inputParam] = implementationParams(implementation);
|
|
435
435
|
const inputParamName = identifierName(inputParam);
|
|
436
436
|
if (inputParamName) {
|
|
437
437
|
return {
|
|
438
|
-
aliases: collectDestructuredFieldAliases(
|
|
438
|
+
aliases: collectDestructuredFieldAliases(
|
|
439
|
+
implementation,
|
|
440
|
+
inputParamName,
|
|
441
|
+
fields
|
|
442
|
+
),
|
|
439
443
|
inputParamName,
|
|
440
444
|
};
|
|
441
445
|
}
|
|
@@ -511,13 +515,13 @@ const comparisonBranchesOnField = (
|
|
|
511
515
|
};
|
|
512
516
|
|
|
513
517
|
const branchesOnField = (
|
|
514
|
-
|
|
518
|
+
implementation: AstNode,
|
|
515
519
|
inputParamName: string | null,
|
|
516
520
|
fieldName: string,
|
|
517
521
|
aliases: ReadonlyMap<string, string>
|
|
518
522
|
): AstNode | null => {
|
|
519
523
|
let found: AstNode | null = null;
|
|
520
|
-
walkScope(
|
|
524
|
+
walkScope(implementation, (node) => {
|
|
521
525
|
if (found) {
|
|
522
526
|
return;
|
|
523
527
|
}
|
|
@@ -579,15 +583,20 @@ export const trailForkCoaching: WardenRule = {
|
|
|
579
583
|
}
|
|
580
584
|
const fieldNames = new Set(fields.map((field) => field.name));
|
|
581
585
|
|
|
582
|
-
for (const
|
|
583
|
-
|
|
586
|
+
for (const implementation of findImplementationBodies(
|
|
587
|
+
definition.config
|
|
588
|
+
)) {
|
|
589
|
+
const inputBindings = collectImplementationInputBindings(
|
|
590
|
+
implementation,
|
|
591
|
+
fieldNames
|
|
592
|
+
);
|
|
584
593
|
if (!inputBindings) {
|
|
585
594
|
continue;
|
|
586
595
|
}
|
|
587
596
|
|
|
588
597
|
for (const field of fields) {
|
|
589
598
|
const branchNode = branchesOnField(
|
|
590
|
-
|
|
599
|
+
implementation,
|
|
591
600
|
inputBindings.inputParamName,
|
|
592
601
|
field.name,
|
|
593
602
|
inputBindings.aliases
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
findBlazeBodies,
|
|
3
2
|
findConfigProperty,
|
|
3
|
+
findImplementationBodies,
|
|
4
4
|
findTrailDefinitions,
|
|
5
5
|
getNodeArgument,
|
|
6
6
|
getNodeArguments,
|
|
@@ -23,12 +23,13 @@ import {
|
|
|
23
23
|
parse,
|
|
24
24
|
walk,
|
|
25
25
|
walkScope,
|
|
26
|
-
} from '
|
|
27
|
-
import type { AstNode } from '
|
|
26
|
+
} from '@ontrails/source';
|
|
27
|
+
import type { AstNode } from '@ontrails/source';
|
|
28
28
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
29
29
|
|
|
30
30
|
const VERSION_PINNED_COMPOSE = 'version-pinned-compose';
|
|
31
|
-
const
|
|
31
|
+
const FORK_WITHOUT_PRESERVED_IMPLEMENTATION =
|
|
32
|
+
'fork-without-preserved-implementation';
|
|
32
33
|
const MARKER_SCHEMA_UNSUPPORTED = 'marker-schema-unsupported';
|
|
33
34
|
|
|
34
35
|
interface SchemaBindingRecord {
|
|
@@ -893,8 +894,8 @@ export const versionPinnedCompose: WardenRule = {
|
|
|
893
894
|
}
|
|
894
895
|
|
|
895
896
|
const diagnostics: WardenDiagnostic[] = [];
|
|
896
|
-
for (const
|
|
897
|
-
walk(
|
|
897
|
+
for (const implementation of findImplementationBodies(ast)) {
|
|
898
|
+
walk(implementation, (node) => {
|
|
898
899
|
if (!composeCallHasVersionPin(node)) {
|
|
899
900
|
return;
|
|
900
901
|
}
|
|
@@ -918,7 +919,7 @@ export const versionPinnedCompose: WardenRule = {
|
|
|
918
919
|
severity: 'warn',
|
|
919
920
|
};
|
|
920
921
|
|
|
921
|
-
export const
|
|
922
|
+
export const forkWithoutPreservedImplementation: WardenRule = {
|
|
922
923
|
check(sourceCode, filePath) {
|
|
923
924
|
const ast = parse(filePath, sourceCode);
|
|
924
925
|
if (!ast) {
|
|
@@ -931,17 +932,20 @@ export const forkWithoutPreservedBlaze: WardenRule = {
|
|
|
931
932
|
continue;
|
|
932
933
|
}
|
|
933
934
|
for (const entry of versionEntries(definition.config)) {
|
|
934
|
-
if (
|
|
935
|
+
if (
|
|
936
|
+
hasProperty(entry, 'transpose') ||
|
|
937
|
+
hasProperty(entry, 'implementation')
|
|
938
|
+
) {
|
|
935
939
|
continue;
|
|
936
940
|
}
|
|
937
941
|
diagnostics.push(
|
|
938
942
|
diagnostic(
|
|
939
|
-
|
|
943
|
+
FORK_WITHOUT_PRESERVED_IMPLEMENTATION,
|
|
940
944
|
'error',
|
|
941
945
|
filePath,
|
|
942
946
|
sourceCode,
|
|
943
947
|
entry,
|
|
944
|
-
`Trail "${definition.id}" has a historical version entry without transpose or
|
|
948
|
+
`Trail "${definition.id}" has a historical version entry without transpose or implementation. Add transpose for a revision entry, or preserve the historical implementation for a fork entry.`
|
|
945
949
|
)
|
|
946
950
|
);
|
|
947
951
|
}
|
|
@@ -949,8 +953,8 @@ export const forkWithoutPreservedBlaze: WardenRule = {
|
|
|
949
953
|
return diagnostics;
|
|
950
954
|
},
|
|
951
955
|
description:
|
|
952
|
-
'Require historical fork version entries to preserve
|
|
953
|
-
name:
|
|
956
|
+
'Require historical fork version entries to preserve an implementation, while revision entries declare transpose.',
|
|
957
|
+
name: FORK_WITHOUT_PRESERVED_IMPLEMENTATION,
|
|
954
958
|
severity: 'error',
|
|
955
959
|
};
|
|
956
960
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { deriveTopoGraph } from '@ontrails/
|
|
1
|
+
import { deriveTopoGraph } from '@ontrails/topography';
|
|
2
2
|
import type {
|
|
3
3
|
TopoGraph,
|
|
4
4
|
TopoGraphEntry,
|
|
5
5
|
TopoGraphForceEntry,
|
|
6
6
|
TopoGraphVersionEntry,
|
|
7
|
-
} from '@ontrails/
|
|
7
|
+
} from '@ontrails/topography';
|
|
8
8
|
import type { Topo } from '@ontrails/core';
|
|
9
9
|
|
|
10
10
|
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
offsetToLine,
|
|
40
40
|
parse,
|
|
41
41
|
walk,
|
|
42
|
-
} from '
|
|
43
|
-
import type { AstNode } from '
|
|
42
|
+
} from '@ontrails/source';
|
|
43
|
+
import type { AstNode } from '@ontrails/source';
|
|
44
44
|
import type {
|
|
45
45
|
AuthoredMcpSurfaceBindingSet,
|
|
46
46
|
ProjectAwareWardenRule,
|
package/src/rules/types.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
SurfaceBindings,
|
|
7
7
|
Topo,
|
|
8
8
|
} from '@ontrails/core';
|
|
9
|
-
import type { TopoGraph } from '@ontrails/
|
|
9
|
+
import type { TopoGraph } from '@ontrails/topography';
|
|
10
10
|
|
|
11
11
|
import type { WardenDepth } from '../config.js';
|
|
12
12
|
import type { WardenImportResolution } from '../resolve.js';
|
|
@@ -314,16 +314,16 @@ export interface ProjectContext {
|
|
|
314
314
|
readonly authoredMcpSurfaceBindingSets?:
|
|
315
315
|
| readonly AuthoredMcpSurfaceBindingSet[]
|
|
316
316
|
| undefined;
|
|
317
|
-
/** All known
|
|
318
|
-
readonly
|
|
317
|
+
/** All known entity names in the project. */
|
|
318
|
+
readonly knownEntityIds?: ReadonlySet<string>;
|
|
319
319
|
/** Store table IDs used with the CRUD factory across the project. */
|
|
320
320
|
readonly crudTableIds?: ReadonlySet<string>;
|
|
321
321
|
/** All known trail IDs in the project */
|
|
322
322
|
readonly knownTrailIds: ReadonlySet<string>;
|
|
323
323
|
/** Trail IDs registered in configured app topo targets. */
|
|
324
324
|
readonly topoTrailIds?: ReadonlySet<string>;
|
|
325
|
-
/** Declared
|
|
326
|
-
readonly
|
|
325
|
+
/** Declared entity references keyed by source entity name. */
|
|
326
|
+
readonly entityReferencesByName?: ReadonlyMap<string, readonly string[]>;
|
|
327
327
|
/** All known resource IDs in the project */
|
|
328
328
|
readonly knownResourceIds?: ReadonlySet<string>;
|
|
329
329
|
/** All known signal IDs in the project */
|
|
@@ -356,8 +356,8 @@ export interface ProjectContext {
|
|
|
356
356
|
/**
|
|
357
357
|
* CRUD operation coverage per entity aggregated across the project.
|
|
358
358
|
*
|
|
359
|
-
* Keys are stable entity IDs (authored
|
|
360
|
-
* sentinels for
|
|
359
|
+
* Keys are stable entity IDs (authored entity names, `imported:<local>`
|
|
360
|
+
* sentinels for entities imported from another module, or store-table IDs
|
|
361
361
|
* produced by `deriveStoreTableId`). Values are the set of CRUD operations
|
|
362
362
|
* (`create`, `read`, `update`, `delete`, `list`) observed for that entity.
|
|
363
363
|
*
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
offsetToLine,
|
|
16
16
|
parse,
|
|
17
17
|
walk,
|
|
18
|
-
} from '
|
|
19
|
-
import type { AstNode } from '
|
|
18
|
+
} from '@ontrails/source';
|
|
19
|
+
import type { AstNode } from '@ontrails/source';
|
|
20
20
|
import { isTestFile } from './scan.js';
|
|
21
21
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
22
22
|
|