@ontrails/warden 1.0.0-beta.43 → 1.0.0-beta.46

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.
@@ -25,8 +25,11 @@ import {
25
25
  getNodeImported,
26
26
  getNodeInit,
27
27
  getNodeLocal,
28
+ getNodeLeft,
28
29
  getNodeName,
30
+ getNodeOperator,
29
31
  getNodeReturnType,
32
+ getNodeRight,
30
33
  getNodeSource,
31
34
  getNodeTypeAnnotation,
32
35
  getNodeValue,
@@ -97,12 +100,15 @@ export type ScopedHelperMap = ReadonlyMap<
97
100
 
98
101
  export type MutableScopedHelperMap = Map<ReadonlySet<string>, Set<string>>;
99
102
 
100
- type ScopedResultVariableMap = ReadonlyMap<
103
+ export type ScopedResultVariableMap = ReadonlyMap<
101
104
  ReadonlySet<string>,
102
105
  ReadonlySet<string>
103
106
  >;
104
107
 
105
- type MutableScopedResultVariableMap = Map<ReadonlySet<string>, Set<string>>;
108
+ export type MutableScopedResultVariableMap = Map<
109
+ ReadonlySet<string>,
110
+ Set<string>
111
+ >;
106
112
 
107
113
  export const findNearestBindingScope = (
108
114
  name: string,
@@ -224,8 +230,8 @@ const unwrapReturnExpression = (node: AstNode): AstNode => {
224
230
  return current;
225
231
  };
226
232
 
227
- /** Check if a return argument is an allowed Result value. */
228
- const isAllowedReturnArgument = (
233
+ /** Check whether an expression has visible Result-producing provenance. */
234
+ export const isResultProducingExpression = (
229
235
  argument: AstNode,
230
236
  helperNames: ReadonlySet<string>,
231
237
  resultVars: ScopedResultVariableMap,
@@ -240,7 +246,7 @@ const isAllowedReturnArgument = (
240
246
  return (
241
247
  consequent !== undefined &&
242
248
  alternate !== undefined &&
243
- isAllowedReturnArgument(
249
+ isResultProducingExpression(
244
250
  consequent,
245
251
  helperNames,
246
252
  resultVars,
@@ -248,7 +254,7 @@ const isAllowedReturnArgument = (
248
254
  scopes,
249
255
  scopedHelpers
250
256
  ) &&
251
- isAllowedReturnArgument(
257
+ isResultProducingExpression(
252
258
  alternate,
253
259
  helperNames,
254
260
  resultVars,
@@ -369,6 +375,14 @@ const addScopedResultVariable = (
369
375
  resultVars.set(scope, new Set([name]));
370
376
  };
371
377
 
378
+ const clearScopedResultVariable = (
379
+ resultVars: MutableScopedResultVariableMap,
380
+ scope: ReadonlySet<string>,
381
+ name: string
382
+ ): void => {
383
+ resultVars.get(scope)?.delete(name);
384
+ };
385
+
372
386
  /** Record `const helper = (): Result<...> => ...` declarations for the current lexical scope. */
373
387
  export const trackScopedResultHelperDeclaration = (
374
388
  node: AstNode,
@@ -440,8 +454,14 @@ const trackResultVariable = (
440
454
  }
441
455
  if (
442
456
  hasResultVariableAnnotation(id, sourceCode, resultTypeNames) ||
443
- isResultExpression(init) ||
444
- isHelperCall(init, helperNames, namespaceHelpers, scopes, scopedHelpers)
457
+ isResultProducingExpression(
458
+ init,
459
+ helperNames,
460
+ resultVars,
461
+ namespaceHelpers,
462
+ scopes,
463
+ scopedHelpers
464
+ )
445
465
  ) {
446
466
  const bindingScope = findNearestBindingScope(name, scopes);
447
467
  if (bindingScope) {
@@ -451,6 +471,63 @@ const trackResultVariable = (
451
471
  }
452
472
  };
453
473
 
474
+ export const collectDirectResultAssignments = (
475
+ body: AstNode
476
+ ): ReadonlySet<AstNode> => {
477
+ const assignments = new Set<AstNode>();
478
+ for (const statement of getNodeBodyStatements(body)) {
479
+ if (statement.type !== 'ExpressionStatement') {
480
+ continue;
481
+ }
482
+ const expression = getNodeExpression(statement);
483
+ if (expression?.type === 'AssignmentExpression') {
484
+ assignments.add(expression);
485
+ }
486
+ }
487
+ return assignments;
488
+ };
489
+
490
+ const trackResultAssignment = (
491
+ node: AstNode,
492
+ resultVars: MutableScopedResultVariableMap,
493
+ helperNames: ReadonlySet<string>,
494
+ namespaceHelpers: NamespaceHelperMap,
495
+ directAssignments: ReadonlySet<AstNode>,
496
+ scopes: readonly ReadonlySet<string>[],
497
+ scopedHelpers: ScopedHelperMap
498
+ ): void => {
499
+ const left = getNodeLeft(node);
500
+ const name = identifierName(left);
501
+ if (!name) {
502
+ return;
503
+ }
504
+ const bindingScope = findNearestBindingScope(name, scopes);
505
+ if (!bindingScope) {
506
+ return;
507
+ }
508
+ const right = getNodeRight(node);
509
+ const resultRhs =
510
+ getNodeOperator(node) === '=' &&
511
+ right &&
512
+ isResultProducingExpression(
513
+ right,
514
+ helperNames,
515
+ resultVars,
516
+ namespaceHelpers,
517
+ scopes,
518
+ scopedHelpers
519
+ );
520
+ if (
521
+ resultRhs &&
522
+ (isScopedResultVariableBinding(name, scopes, resultVars) ||
523
+ directAssignments.has(node))
524
+ ) {
525
+ addScopedResultVariable(resultVars, bindingScope, name);
526
+ return;
527
+ }
528
+ clearScopedResultVariable(resultVars, bindingScope, name);
529
+ };
530
+
454
531
  // ---------------------------------------------------------------------------
455
532
  // Return statement checking
456
533
  // ---------------------------------------------------------------------------
@@ -470,6 +547,7 @@ const checkReturnStatements = (
470
547
  const resultVars: MutableScopedResultVariableMap = new Map();
471
548
  const scopedHelpers: MutableScopedHelperMap = new Map();
472
549
  const initialScopes = implScope.size > 0 ? [implScope] : [];
550
+ const directAssignments = collectDirectResultAssignments(blockBody);
473
551
 
474
552
  walkWithScopes(
475
553
  blockBody,
@@ -494,6 +572,18 @@ const checkReturnStatements = (
494
572
  );
495
573
  }
496
574
 
575
+ if (node.type === 'AssignmentExpression') {
576
+ trackResultAssignment(
577
+ node,
578
+ resultVars,
579
+ helperNames,
580
+ namespaceHelpers,
581
+ directAssignments,
582
+ currentScopes,
583
+ scopedHelpers
584
+ );
585
+ }
586
+
497
587
  if (node.type !== 'ReturnStatement') {
498
588
  return;
499
589
  }
@@ -505,7 +595,7 @@ const checkReturnStatements = (
505
595
  }
506
596
 
507
597
  if (
508
- isAllowedReturnArgument(
598
+ isResultProducingExpression(
509
599
  argument,
510
600
  helperNames,
511
601
  resultVars,
@@ -81,7 +81,7 @@ import { webhookRouteCollision } from './webhook-route-collision.js';
81
81
 
82
82
  export type {
83
83
  GovernedVocabularyHistoryEvidence,
84
- GovernedVocabularyHistoryFormObservation,
84
+ GovernedVocabularyHistoryFormJudgment,
85
85
  GovernedVocabularyHistoryIssue,
86
86
  WardenFix,
87
87
  WardenFixCapability,
@@ -23,11 +23,11 @@ import {
23
23
  import type { AstNode, AstParentContext } from '@ontrails/source';
24
24
  import {
25
25
  collectAllResultHelperNames,
26
+ collectDirectResultAssignments,
26
27
  collectNamespaceHelperImports,
27
28
  collectResultTypeNames,
28
29
  findNearestBindingScope,
29
- isHelperCall,
30
- isResultExpression,
30
+ isResultProducingExpression,
31
31
  trackScopedResultHelperDeclaration,
32
32
  } from './implementation-returns-result.js';
33
33
  import { isTestFile } from './scan.js';
@@ -82,16 +82,6 @@ const getErrorSourceVariable = (node: AstNode | null): string | null => {
82
82
  return identifierName(member?.object);
83
83
  };
84
84
 
85
- const isResultProducingExpression = (
86
- node: AstNode,
87
- helperNames: ReadonlySet<string>,
88
- namespaceHelpers: NamespaceHelperMap,
89
- scopes: readonly ReadonlySet<string>[],
90
- scopedHelpers: ScopedHelperMap
91
- ): boolean =>
92
- isResultExpression(node) ||
93
- isHelperCall(node, helperNames, namespaceHelpers, scopes, scopedHelpers);
94
-
95
85
  type ResultProvenance = Map<ReadonlySet<string>, Set<string>>;
96
86
 
97
87
  const markResultVariable = (
@@ -158,6 +148,7 @@ const trackVariableDeclarator = (
158
148
  isResultProducingExpression(
159
149
  init,
160
150
  helperNames,
151
+ provenance,
161
152
  namespaceHelpers,
162
153
  scopes,
163
154
  scopedHelpers
@@ -175,11 +166,10 @@ const trackAssignmentExpression = (
175
166
  helperNames: ReadonlySet<string>,
176
167
  namespaceHelpers: NamespaceHelperMap,
177
168
  scopedHelpers: ScopedHelperMap,
169
+ directAssignments: ReadonlySet<AstNode>,
178
170
  scopes: readonly ReadonlySet<string>[]
179
171
  ): void => {
180
172
  const left = getNodeLeft(node);
181
- const operator = getNodeOperator(node);
182
- const right = getNodeRight(node);
183
173
  const name = identifierName(left);
184
174
  if (!name) {
185
175
  return;
@@ -188,16 +178,22 @@ const trackAssignmentExpression = (
188
178
  if (!scope) {
189
179
  return;
190
180
  }
191
- if (
192
- operator === '=' &&
181
+ const right = getNodeRight(node);
182
+ const resultRhs =
183
+ getNodeOperator(node) === '=' &&
193
184
  right &&
194
185
  isResultProducingExpression(
195
186
  right,
196
187
  helperNames,
188
+ provenance,
197
189
  namespaceHelpers,
198
190
  scopes,
199
191
  scopedHelpers
200
- )
192
+ );
193
+ if (
194
+ resultRhs &&
195
+ (hasResultProvenance(provenance, scope, name) ||
196
+ directAssignments.has(node))
201
197
  ) {
202
198
  markResultVariable(provenance, scope, name);
203
199
  return;
@@ -258,6 +254,7 @@ const checkFunctionBody = (
258
254
  const scopedHelpers: MutableScopedHelperMap = new Map();
259
255
  const implScope = collectScopeFrameBindings(owner);
260
256
  const initialScopes = implScope.size > 0 ? [implScope] : [];
257
+ const directAssignments = collectDirectResultAssignments(body);
261
258
 
262
259
  walkWithScopes(
263
260
  body,
@@ -287,6 +284,7 @@ const checkFunctionBody = (
287
284
  helperNames,
288
285
  namespaceHelpers,
289
286
  scopedHelpers,
287
+ directAssignments,
290
288
  scopes
291
289
  );
292
290
  return;
@@ -44,8 +44,9 @@ import {
44
44
  getNodeValue,
45
45
  offsetToLine,
46
46
  parse,
47
+ parseWithDiagnostics,
47
48
  } from '@ontrails/source';
48
- import type { AstNode } from '@ontrails/source';
49
+ import type { AstNode, SourceComment } from '@ontrails/source';
49
50
  import type { WardenDiagnostic, WardenRule } from './types.js';
50
51
 
51
52
  const RULE_NAME = 'public-export-example-coverage';
@@ -379,44 +380,19 @@ const declarationNameMatches = (
379
380
  return false;
380
381
  };
381
382
 
382
- /**
383
- * Comment texts found in an inter-statement trivia gap. The gap between two
384
- * top-level statements contains only whitespace and comments, so a small
385
- * line/block comment scan recovers the same comment set TypeScript's
386
- * `getLeadingCommentRanges` returns for the statement's full start.
387
- */
388
- const collectCommentTexts = (gapText: string): readonly string[] => {
389
- const comments: string[] = [];
390
- let index = 0;
391
- while (index < gapText.length - 1) {
392
- if (gapText[index] === '/' && gapText[index + 1] === '/') {
393
- const lineEnd = gapText.indexOf('\n', index);
394
- const stop = lineEnd === -1 ? gapText.length : lineEnd;
395
- comments.push(gapText.slice(index, stop));
396
- index = stop + 1;
397
- } else if (gapText[index] === '/' && gapText[index + 1] === '*') {
398
- const blockEnd = gapText.indexOf('*/', index + 2);
399
- const stop = blockEnd === -1 ? gapText.length : blockEnd + 2;
400
- comments.push(gapText.slice(index, stop));
401
- index = stop;
402
- } else {
403
- index += 1;
404
- }
405
- }
406
- return comments;
407
- };
408
-
409
383
  const EXAMPLE_TAG_PATTERN = /@example\b/;
384
+ const LINE_TERMINATOR_PATTERN = /[\n\r\u2028\u2029]/u;
410
385
 
411
386
  /**
412
387
  * True when the exported declaration named `importedName` in `sourceText`
413
- * carries a leading comment containing `@example`. Leading comments are
414
- * recovered from the trivia gap between the preceding top-level statement's
415
- * end (or file start) and the matching export statement's start.
388
+ * carries a leading comment containing `@example`. Parser-native comments in
389
+ * the trivia gap between the preceding top-level statement's end (or file
390
+ * start) and the matching export statement are considered leading.
416
391
  */
417
392
  const hasLeadingExampleForExport = (
418
393
  sourceText: string,
419
394
  ast: AstNode,
395
+ comments: readonly SourceComment[],
420
396
  importedName: string
421
397
  ): boolean => {
422
398
  const body = programBody(ast);
@@ -429,9 +405,16 @@ const hasLeadingExampleForExport = (
429
405
  continue;
430
406
  }
431
407
  const previous = body[statementIndex - 1];
432
- const gapText = sourceText.slice(previous?.end ?? 0, statement.start);
433
- return collectCommentTexts(gapText).some((comment) =>
434
- EXAMPLE_TAG_PATTERN.test(comment)
408
+ const gapStart = previous?.end ?? 0;
409
+ return comments.some(
410
+ (comment) =>
411
+ gapStart <= comment.start &&
412
+ comment.end <= statement.start &&
413
+ (previous === undefined ||
414
+ LINE_TERMINATOR_PATTERN.test(
415
+ sourceText.slice(gapStart, comment.start)
416
+ )) &&
417
+ EXAMPLE_TAG_PATTERN.test(comment.value)
435
418
  );
436
419
  }
437
420
  return false;
@@ -468,8 +451,8 @@ const coverageDiagnosticsForSpecifier = (
468
451
  ),
469
452
  ];
470
453
  }
471
- const sourceAst = parse(sourcePath, sourceText);
472
- if (!sourceAst) {
454
+ const parsedSource = parseWithDiagnostics(sourcePath, sourceText);
455
+ if (!parsedSource.ast || parsedSource.diagnostics.length > 0) {
473
456
  return [
474
457
  diagnostic(
475
458
  sourceCode,
@@ -481,7 +464,12 @@ const coverageDiagnosticsForSpecifier = (
481
464
  ];
482
465
  }
483
466
  if (
484
- hasLeadingExampleForExport(sourceText, sourceAst, specifier.importedName)
467
+ hasLeadingExampleForExport(
468
+ sourceText,
469
+ parsedSource.ast,
470
+ parsedSource.comments,
471
+ specifier.importedName
472
+ )
485
473
  ) {
486
474
  return [];
487
475
  }
@@ -62,6 +62,7 @@ export const governedVocabularySymbolRenameSchema = z.object({
62
62
  from: z.string().min(1),
63
63
  match: z.enum(governedVocabularySymbolRenameMatchModes).default('exact'),
64
64
  reviewDeclarationTypes: z.array(z.string().min(1)).default([]),
65
+ safety: z.enum(['safe', 'review']).optional(),
65
66
  to: z.string().min(1),
66
67
  });
67
68
 
@@ -829,6 +830,9 @@ export const governedVocabularyTransitions =
829
830
  'cliProjection',
830
831
  'cliProjectionSchema',
831
832
  'collectLibraryProjection',
833
+ 'collectionExtensionProjectionForFileRenames',
834
+ 'extensionProjection',
835
+ 'filterProjectedTargetExtensions',
832
836
  'projectActivationEdge',
833
837
  'projectActivationSource',
834
838
  'projectActivationSourceDeclaration',
@@ -860,6 +864,11 @@ export const governedVocabularyTransitions =
860
864
  'projectTrailVersions',
861
865
  'projectVersionDetours',
862
866
  'projectVersionRuntimeRefs',
867
+ 'projectVocabularyText',
868
+ 'projected',
869
+ 'projectedEvidence',
870
+ 'projectedFileInScopeCount',
871
+ 'projectedTargetPaths',
863
872
  'SchemaProjector',
864
873
  'deriveShippedSurfaceProjectionInventory',
865
874
  'deriveTrailCliCommandProjection',
@@ -1190,6 +1199,15 @@ export const governedVocabularyTransitions =
1190
1199
  from: 'collectLibraryProjection',
1191
1200
  to: 'deriveTopoGraphLibrary',
1192
1201
  },
1202
+ {
1203
+ from: 'collectionExtensionProjectionForFileRenames',
1204
+ to: 'deriveCollectionExtensionsForFileRenames',
1205
+ },
1206
+ { from: 'extensionProjection', to: 'derivedExtensions' },
1207
+ {
1208
+ from: 'filterProjectedTargetExtensions',
1209
+ to: 'filterDerivedTargetExtensions',
1210
+ },
1193
1211
  { from: 'projectActivationEdge', to: 'deriveActivationEdge' },
1194
1212
  { from: 'projectActivationSource', to: 'deriveActivationSource' },
1195
1213
  {
@@ -1233,6 +1251,18 @@ export const governedVocabularyTransitions =
1233
1251
  from: 'projectVersionRuntimeRefs',
1234
1252
  to: 'deriveVersionRuntimeRefs',
1235
1253
  },
1254
+ { from: 'projectVocabularyText', to: 'deriveVocabularyText' },
1255
+ {
1256
+ from: 'projected',
1257
+ safety: 'review',
1258
+ to: 'derived',
1259
+ },
1260
+ { from: 'projectedEvidence', to: 'derivedEvidence' },
1261
+ {
1262
+ from: 'projectedFileInScopeCount',
1263
+ to: 'derivedFileInScopeCount',
1264
+ },
1265
+ { from: 'projectedTargetPaths', to: 'derivedTargetPaths' },
1236
1266
  { from: 'SchemaProjector', to: 'SchemaDeriver' },
1237
1267
  {
1238
1268
  from: 'deriveShippedSurfaceProjectionInventory',
@@ -386,22 +386,25 @@ export interface ProjectContext {
386
386
  export interface GovernedVocabularyHistoryEvidence {
387
387
  readonly caseSensitive: boolean;
388
388
  readonly id: string;
389
- readonly latestFormObservations: readonly GovernedVocabularyHistoryFormObservation[];
389
+ readonly latestFormJudgments: readonly GovernedVocabularyHistoryFormJudgment[];
390
390
  readonly path: string;
391
391
  readonly provenance?: GovernedVocabularyHistoryProvenance;
392
392
  readonly runCount: number;
393
393
  readonly transitionId: string;
394
394
  }
395
395
 
396
- /** One vocabulary-form observation preserved in committed Regrade history. */
397
- export interface GovernedVocabularyHistoryFormObservation {
396
+ /** One compact vocabulary-form judgment preserved in committed Regrade history. */
397
+ export interface GovernedVocabularyHistoryFormJudgment {
398
398
  readonly disposition: string;
399
399
  readonly form: string;
400
- readonly line: number;
401
- readonly path: string;
402
- readonly reason: string;
403
- readonly scopeTier?: 'in-scope' | 'policy-classified' | undefined;
404
- readonly verdict: 'applied' | 'deferred' | 'modified' | 'skipped';
400
+ readonly reason?: string | undefined;
401
+ readonly representative?:
402
+ | {
403
+ readonly line: number;
404
+ readonly path: string;
405
+ }
406
+ | undefined;
407
+ readonly target?: string | undefined;
405
408
  }
406
409
 
407
410
  export interface GovernedVocabularyHistoryIssue {
@@ -119,21 +119,24 @@ export const authoredMcpSurfaceBindingSetSchema = z.object({
119
119
  .describe('Trail ids registered in the owning app topo'),
120
120
  });
121
121
 
122
- const governedVocabularyHistoryObservationSchema = z.object({
122
+ const governedVocabularyHistoryFormJudgmentSchema = z.object({
123
123
  disposition: z.string(),
124
124
  form: z.string(),
125
- line: z.number().int().positive(),
126
- path: z.string(),
127
- reason: z.string(),
128
- scopeTier: z.enum(['in-scope', 'policy-classified']).optional(),
129
- verdict: z.enum(['applied', 'deferred', 'modified', 'skipped']),
125
+ reason: z.string().optional(),
126
+ representative: z
127
+ .object({
128
+ line: z.number().int().positive(),
129
+ path: z.string(),
130
+ })
131
+ .optional(),
132
+ target: z.string().optional(),
130
133
  });
131
134
 
132
135
  const governedVocabularyHistoryEvidenceSchema = z.object({
133
136
  caseSensitive: z.boolean(),
134
137
  id: z.string(),
135
- latestFormObservations: z
136
- .array(governedVocabularyHistoryObservationSchema)
138
+ latestFormJudgments: z
139
+ .array(governedVocabularyHistoryFormJudgmentSchema)
137
140
  .readonly(),
138
141
  path: z.string(),
139
142
  runCount: z.number().int().nonnegative(),