@ontrails/regrade 1.0.0-beta.30 → 1.0.0-beta.39
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 +99 -0
- package/package.json +5 -5
- package/src/downstream/ast-rewrite.ts +200 -5
- package/src/downstream/collect.ts +45 -4
- package/src/downstream/export-restructure.ts +1588 -0
- package/src/downstream/report.ts +557 -79
- package/src/downstream/scan-summary.ts +193 -0
- package/src/downstream/vocabulary-registry.ts +98 -0
- package/src/downstream/vocabulary.ts +1933 -0
- package/src/index.ts +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# @ontrails/regrade
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.39
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`65b362f`](https://github.com/outfitter-dev/trails/commit/65b362f65fa2a75e2121d1a8b31882d52fa0376b): Regrade history consolidates per transition, append-only: `regrade apply` appends a run entry stamped `{ planContentHash, lockHashAtRun }` to `.trails/regrade/history/<transition>.json` instead of overwriting lockhash-named files, identical re-runs are recognized as replays, the artifact carries a stable internal transition `id`, and `regrade check <transition>` verifies each recorded run at its own stamped lock. The report `history.status` union widens to `applied | checked | replay`.
|
|
8
|
+
- [`d3f7a25`](https://github.com/outfitter-dev/trails/commit/d3f7a25e66b6f0149ab2f33ac31ae1458860dce3): Regrade review entries carry structured review detail through Warden-backed and AST-backed classes: matched form, candidate replacement, machine-readable signals, preserve cautions, and a `judgment` field that distinguishes unresolved occurrence judgment from completed preserve/rewrite verdicts. Safe rewrite entries keep their existing report shape.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- [`b077fb7`](https://github.com/outfitter-dev/trails/commit/b077fb7ba6d9724cac6f0e59bc3fec9aec28984c): Add the export-restructure Regrade class family (TRL-1210). `export-restructure:cli-aliases` inverts legacy `cliAliases`/`trailsCliAliases` exports into `surfaceOverlay({ cli })` bindings inside the module's `trailsOverlays` export — adding the `@ontrails/core` import, deleting the legacy export, and routing anything it cannot prove safe (computed keys, spreads, in-module `aliases:` references) to `needs-review` with the exact target shape named. `export-restructure:mcp-trailheads` projects call-site MCP trailhead maps into `surfaceOverlay({ mcp })` group bindings: it rewrites in place when the same module exports `trailsOverlays`, and otherwise emits a classified `needs-review` handoff naming the module-overlay target while the call-site map stays as the richer-metadata override-in-context. Warden's fix-class union grows to `'export-restructure' | 'term-rewrite'`, `no-legacy-cli-alias-export` now advertises the `export-restructure` class, and `loadWardenRegradeClasses` supersedes `loadWardenTermRewriteClasses` (still exported) as the full Warden-routed class loader. Class-mode Regrade also gains the full plan lifecycle: `trails regrade plan --type class --class-ids ...` writes a `.trails/regrade/<slug>.json` plan carrying class ids, scope, and intent, `regrade check` re-runs the dry run and gates on outstanding rewrites or review, and `regrade apply` applies and graduates the plan to `.trails/regrade/history/<slug>-<hash>.json` — the same plan → check → apply → history evidence trail vocabulary regrades already had, now available to structural transforms. The class family ships for downstream apps bridging the pre-1.0 gap, so pre-cutover alias exports and trailhead maps migrate mechanically instead of by hand.
|
|
13
|
+
|
|
14
|
+
## 1.0.0-beta.38
|
|
15
|
+
|
|
16
|
+
## 1.0.0-beta.37
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- [`09f15de`](https://github.com/outfitter-dev/trails/commit/09f15def8fcc8c28b0d604f436e6eeed46da8f37): Stage wide-net Regrade expansion candidates as structured plan review inventory with evidence, status, and pending counts.
|
|
21
|
+
|
|
22
|
+
## 1.0.0-beta.36
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- [`6e63e48`](https://github.com/outfitter-dev/trails/commit/6e63e483617b84cb6868d0c4d58d5b5a8d3b9ed2): Complete the v1 grouped surface-entry vocabulary cutover from facet to trailhead, including Regrade dogfood support for governed string literal renames and composed AST rewrite application.
|
|
27
|
+
- [`26786a1`](https://github.com/outfitter-dev/trails/commit/26786a14acbe9ed03f69adbdac22968891e33df1): Persist vocabulary Regrade plan artifacts, expose plan/check/preview/apply flows across CLI and MCP, and write applied plan history for reviewed vocabulary migrations.
|
|
28
|
+
|
|
29
|
+
## 1.0.0-beta.35
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [`3842160`](https://github.com/outfitter-dev/trails/commit/3842160ac4030807cb2dfca8e1da75e03febccf2): Teach vocabulary regrades to discover simple prose morphology as deferred
|
|
34
|
+
review inventory so authored plans surface forms like `blazed` and `blazing`.
|
|
35
|
+
|
|
36
|
+
## 1.0.0-beta.34
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- [`d67558b`](https://github.com/outfitter-dev/trails/commit/d67558bea3bfa363ed57e0f4091b6eccbf2a7710): Run governed AST symbol renames from the registry-backed `trails regrade` command path, including MCP parity, while preserving derived live API forms.
|
|
41
|
+
|
|
42
|
+
## 1.0.0-beta.33
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- [`0c40138`](https://github.com/outfitter-dev/trails/commit/0c40138efb962e779710daa172bdfd756d9d992f): Harden vocabulary regrades by deferring Markdown code contexts for review and
|
|
47
|
+
exposing structured preserve rules through the Trails `regrade` command.
|
|
48
|
+
- [`7d65189`](https://github.com/outfitter-dev/trails/commit/7d65189cc33408755fe07a0b5679f1ed01123455): Expose derived live-API preserve inventory in vocabulary Regrade runs and have the Trails operator regrade surface derive current facet API preserves from live topo and MCP surface facts.
|
|
49
|
+
- [`fc002d5`](https://github.com/outfitter-dev/trails/commit/fc002d5669f4303427e99f45f9998fd0b0172bdb): Add governed AST identifier rename helpers and Warden residue detection for
|
|
50
|
+
active vocabulary symbol transitions.
|
|
51
|
+
- [`6ca0d8f`](https://github.com/outfitter-dev/trails/commit/6ca0d8f776801eee71ddd86cb88c198eaf5815fd): Add a typed governed-vocabulary transition registry that Warden owns and Regrade
|
|
52
|
+
can consume for migration planning.
|
|
53
|
+
- [`04bb8a4`](https://github.com/outfitter-dev/trails/commit/04bb8a42af4ada51a74b1d8c83697db92035b5e9): Expose vocabulary Regrade occurrence dispositions and disposition summary counts alongside mechanical verdicts.
|
|
54
|
+
|
|
55
|
+
Accept explicit preserve-rule dispositions through the `trails regrade` CLI/MCP contract.
|
|
56
|
+
|
|
57
|
+
## 1.0.0-beta.32
|
|
58
|
+
|
|
59
|
+
### Patch Changes
|
|
60
|
+
|
|
61
|
+
- f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
|
|
62
|
+
- fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
|
|
63
|
+
- Updated dependencies [3e5c0fc]
|
|
64
|
+
- Updated dependencies [f3c4fef]
|
|
65
|
+
- Updated dependencies [cb0a9d8]
|
|
66
|
+
- Updated dependencies [21c6dda]
|
|
67
|
+
- Updated dependencies [fe72b84]
|
|
68
|
+
- @ontrails/core@1.0.0-beta.32
|
|
69
|
+
- @ontrails/warden@1.0.0-beta.32
|
|
70
|
+
|
|
71
|
+
## 1.0.0-beta.31
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- e2f3d23: Default Regrade reports to actionable entries, add skip counts grouped by
|
|
76
|
+
reason, and expose an `includeEntries` option for full report inventories.
|
|
77
|
+
- 9be2b7e: Load project-local Warden term-rewrite rules from the Regrade root so repo-owned
|
|
78
|
+
migration classes can run through `trails regrade`.
|
|
79
|
+
- 47f782c: Add occurrence-level vocabulary regrade reports with plan, ledger,
|
|
80
|
+
and completion-gate facts. The Trails `regrade` operator command now supports
|
|
81
|
+
positional `<from> <to>` regrade runs and exposes the same capability through
|
|
82
|
+
the curated MCP surface.
|
|
83
|
+
- ee9f3ae: Let Warden fix capabilities declare downstream scan targets and have Regrade
|
|
84
|
+
honor those targets for Warden-backed term-rewrite classes.
|
|
85
|
+
|
|
86
|
+
Dogfood the first safe facet-to-trailhead prose rewrite through project-local
|
|
87
|
+
Warden rules and Regrade.
|
|
88
|
+
|
|
89
|
+
- 982a4d7: Add Regrade path-scope exclusion globs for vocabulary runs and expose them
|
|
90
|
+
through the `trails regrade` CLI/MCP contract.
|
|
91
|
+
- 1540233: Add Regrade scan inventory summaries that group matched files by extension and
|
|
92
|
+
top-level path, with occurrence counts for vocabulary regrade reports.
|
|
93
|
+
- a079073: Rename Regrade path-scope scan controls from `ignore` to `exclude` across CLI, MCP, and project config.
|
|
94
|
+
- Updated dependencies [ee9f3ae]
|
|
95
|
+
- Updated dependencies [a0126d9]
|
|
96
|
+
- Updated dependencies [4cd5d4e]
|
|
97
|
+
- Updated dependencies [6a26a08]
|
|
98
|
+
- Updated dependencies [38907cc]
|
|
99
|
+
- @ontrails/warden@1.0.0-beta.31
|
|
100
|
+
- @ontrails/core@1.0.0-beta.31
|
|
101
|
+
|
|
3
102
|
## 1.0.0-beta.30
|
|
4
103
|
|
|
5
104
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/regrade",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.39",
|
|
4
4
|
"description": "Downstream migration reporting and safe rewrite helpers for Trails.",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/**/*.ts",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
26
|
-
"@ontrails/warden": "^1.0.0-beta.
|
|
25
|
+
"@ontrails/core": "^1.0.0-beta.39",
|
|
26
|
+
"@ontrails/warden": "^1.0.0-beta.39",
|
|
27
27
|
"zod": "^4.3.5"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
31
|
-
"@ontrails/topographer": "^1.0.0-beta.
|
|
30
|
+
"@ontrails/testing": "^1.0.0-beta.39",
|
|
31
|
+
"@ontrails/topographer": "^1.0.0-beta.39"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -6,12 +6,15 @@ import type {
|
|
|
6
6
|
import {
|
|
7
7
|
applySourceEdits,
|
|
8
8
|
createSourceEdit,
|
|
9
|
+
getStringValue,
|
|
9
10
|
identifierName,
|
|
11
|
+
isStringLiteral,
|
|
10
12
|
offsetToLineColumn,
|
|
11
13
|
parseWithDiagnostics,
|
|
12
14
|
validateSourceEdits,
|
|
13
15
|
walkWithScopeContext,
|
|
14
16
|
} from '@ontrails/warden/ast';
|
|
17
|
+
import type { GovernedVocabularyTransition } from '@ontrails/warden';
|
|
15
18
|
|
|
16
19
|
import type {
|
|
17
20
|
RegradeClass,
|
|
@@ -231,12 +234,45 @@ export interface AstIdentifierRenameClassOptions {
|
|
|
231
234
|
readonly from: string;
|
|
232
235
|
readonly id?: string;
|
|
233
236
|
readonly reviewDeclarationTypes?: ReadonlySet<string>;
|
|
237
|
+
readonly shouldPreserve?: (
|
|
238
|
+
occurrence: AstIdentifierRenameOccurrence
|
|
239
|
+
) => boolean;
|
|
240
|
+
readonly to: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface AstIdentifierRenameOccurrence {
|
|
244
|
+
readonly end: number;
|
|
245
|
+
readonly from: string;
|
|
246
|
+
readonly path: string;
|
|
247
|
+
readonly source: string;
|
|
248
|
+
readonly start: number;
|
|
249
|
+
readonly to: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface AstStringLiteralRenameClassOptions {
|
|
253
|
+
readonly describe?: string;
|
|
254
|
+
readonly from: string;
|
|
255
|
+
readonly id?: string;
|
|
256
|
+
readonly shouldPreserve?: (
|
|
257
|
+
occurrence: AstIdentifierRenameOccurrence
|
|
258
|
+
) => boolean;
|
|
234
259
|
readonly to: string;
|
|
235
260
|
}
|
|
236
261
|
|
|
237
262
|
const isIdentifierNamed = (node: AstNode, name: string): boolean =>
|
|
238
263
|
node.type === 'Identifier' && identifierName(node) === name;
|
|
239
264
|
|
|
265
|
+
const identifierTokenSpan = (
|
|
266
|
+
node: AstNode,
|
|
267
|
+
source: string,
|
|
268
|
+
name: string
|
|
269
|
+
): { readonly end: number; readonly start: number } | null => {
|
|
270
|
+
const end = node.start + name.length;
|
|
271
|
+
return source.slice(node.start, end) === name
|
|
272
|
+
? { end, start: node.start }
|
|
273
|
+
: null;
|
|
274
|
+
};
|
|
275
|
+
|
|
240
276
|
export const createAstIdentifierRenameClass = (
|
|
241
277
|
options: AstIdentifierRenameClassOptions
|
|
242
278
|
): RegradeClass => {
|
|
@@ -253,14 +289,20 @@ export const createAstIdentifierRenameClass = (
|
|
|
253
289
|
return null;
|
|
254
290
|
}
|
|
255
291
|
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
292
|
+
const span = identifierTokenSpan(node, context.source, options.from);
|
|
293
|
+
if (span === null) {
|
|
258
294
|
const location = offsetToLineColumn(context.source, node.start);
|
|
295
|
+
const caution = `Identifier "${options.from}" token span could not be verified; routed to review.`;
|
|
259
296
|
return {
|
|
260
297
|
detail: {
|
|
298
|
+
candidateReplacement: options.to,
|
|
261
299
|
expectedTarget: `Rename identifier "${options.from}" to "${options.to}".`,
|
|
300
|
+
judgment: 'unresolved',
|
|
301
|
+
matchedForm: options.from,
|
|
262
302
|
nodeKind: node.type,
|
|
263
|
-
|
|
303
|
+
preserveCautions: [caution],
|
|
304
|
+
reason: 'ast-identifier-token-span-unverified',
|
|
305
|
+
signals: ['ast:identifier-rename'],
|
|
264
306
|
span: {
|
|
265
307
|
column: location.column,
|
|
266
308
|
end: node.end,
|
|
@@ -271,16 +313,169 @@ export const createAstIdentifierRenameClass = (
|
|
|
271
313
|
symbol: options.from,
|
|
272
314
|
},
|
|
273
315
|
kind: 'review',
|
|
274
|
-
note:
|
|
316
|
+
note: caution,
|
|
317
|
+
reason: 'ast-identifier-token-span-unverified',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
options.shouldPreserve?.({
|
|
323
|
+
end: span.end,
|
|
324
|
+
from: options.from,
|
|
325
|
+
path: context.path,
|
|
326
|
+
source: context.source,
|
|
327
|
+
start: span.start,
|
|
328
|
+
to: options.to,
|
|
329
|
+
}) === true
|
|
330
|
+
) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const declaration = context.getDeclaration(options.from);
|
|
335
|
+
if (declaration && reviewDeclarationTypes.has(declaration.type)) {
|
|
336
|
+
const location = offsetToLineColumn(context.source, span.start);
|
|
337
|
+
const caution = `Identifier "${options.from}" resolves to ${declaration.type}; routed to review.`;
|
|
338
|
+
return {
|
|
339
|
+
detail: {
|
|
340
|
+
candidateReplacement: options.to,
|
|
341
|
+
expectedTarget: `Rename identifier "${options.from}" to "${options.to}".`,
|
|
342
|
+
judgment: 'unresolved',
|
|
343
|
+
matchedForm: options.from,
|
|
344
|
+
nodeKind: node.type,
|
|
345
|
+
preserveCautions: [caution],
|
|
346
|
+
reason: 'ast-identifier-review-declaration',
|
|
347
|
+
signals: ['ast:identifier-rename'],
|
|
348
|
+
span: {
|
|
349
|
+
column: location.column,
|
|
350
|
+
end: span.end,
|
|
351
|
+
line: location.line,
|
|
352
|
+
start: span.start,
|
|
353
|
+
},
|
|
354
|
+
suggestedValidation: 'bun run typecheck',
|
|
355
|
+
symbol: options.from,
|
|
356
|
+
},
|
|
357
|
+
kind: 'review',
|
|
358
|
+
note: caution,
|
|
275
359
|
reason: 'ast-identifier-review-declaration',
|
|
276
360
|
};
|
|
277
361
|
}
|
|
278
362
|
|
|
279
363
|
return {
|
|
280
|
-
edit: createSourceEdit(
|
|
364
|
+
edit: createSourceEdit(span.start, span.end, options.to),
|
|
281
365
|
kind: 'edit',
|
|
282
366
|
note: `Renamed identifier "${options.from}" to "${options.to}".`,
|
|
283
367
|
};
|
|
284
368
|
},
|
|
285
369
|
});
|
|
286
370
|
};
|
|
371
|
+
|
|
372
|
+
const stringLiteralValueSpan = (
|
|
373
|
+
node: AstNode,
|
|
374
|
+
source: string,
|
|
375
|
+
value: string
|
|
376
|
+
): { readonly end: number; readonly start: number } | null => {
|
|
377
|
+
const raw = source.slice(node.start, node.end);
|
|
378
|
+
const relativeStart = raw.indexOf(value);
|
|
379
|
+
if (relativeStart === -1 || raw.includes(value, relativeStart + 1)) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const start = node.start + relativeStart;
|
|
383
|
+
return { end: start + value.length, start };
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export const createAstStringLiteralRenameClass = (
|
|
387
|
+
options: AstStringLiteralRenameClassOptions
|
|
388
|
+
): RegradeClass =>
|
|
389
|
+
createAstRewriteClass({
|
|
390
|
+
describe:
|
|
391
|
+
options.describe ??
|
|
392
|
+
`Rename string literal "${options.from}" to "${options.to}".`,
|
|
393
|
+
id:
|
|
394
|
+
options.id ?? `ast-string-literal-rename:${options.from}->${options.to}`,
|
|
395
|
+
visit: (node, context) => {
|
|
396
|
+
if (!isStringLiteral(node) || getStringValue(node) !== options.from) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const span = stringLiteralValueSpan(node, context.source, options.from);
|
|
401
|
+
if (span === null) {
|
|
402
|
+
const location = offsetToLineColumn(context.source, node.start);
|
|
403
|
+
const caution = `String literal "${options.from}" token span could not be verified; routed to review.`;
|
|
404
|
+
return {
|
|
405
|
+
detail: {
|
|
406
|
+
candidateReplacement: options.to,
|
|
407
|
+
expectedTarget: `Rename string literal "${options.from}" to "${options.to}".`,
|
|
408
|
+
judgment: 'unresolved',
|
|
409
|
+
matchedForm: options.from,
|
|
410
|
+
nodeKind: node.type,
|
|
411
|
+
preserveCautions: [caution],
|
|
412
|
+
reason: 'ast-string-literal-token-span-unverified',
|
|
413
|
+
signals: ['ast:string-literal-rename'],
|
|
414
|
+
span: {
|
|
415
|
+
column: location.column,
|
|
416
|
+
end: node.end,
|
|
417
|
+
line: location.line,
|
|
418
|
+
start: node.start,
|
|
419
|
+
},
|
|
420
|
+
suggestedValidation: 'bun run typecheck',
|
|
421
|
+
symbol: options.from,
|
|
422
|
+
},
|
|
423
|
+
kind: 'review',
|
|
424
|
+
note: caution,
|
|
425
|
+
reason: 'ast-string-literal-token-span-unverified',
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (
|
|
430
|
+
options.shouldPreserve?.({
|
|
431
|
+
end: span.end,
|
|
432
|
+
from: options.from,
|
|
433
|
+
path: context.path,
|
|
434
|
+
source: context.source,
|
|
435
|
+
start: span.start,
|
|
436
|
+
to: options.to,
|
|
437
|
+
}) === true
|
|
438
|
+
) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
edit: createSourceEdit(span.start, span.end, options.to),
|
|
444
|
+
kind: 'edit',
|
|
445
|
+
note: `Renamed string literal "${options.from}" to "${options.to}".`,
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
export const createGovernedAstIdentifierRenameClasses = (
|
|
451
|
+
transition: GovernedVocabularyTransition,
|
|
452
|
+
options: {
|
|
453
|
+
readonly shouldPreserve?: (
|
|
454
|
+
occurrence: AstIdentifierRenameOccurrence
|
|
455
|
+
) => boolean;
|
|
456
|
+
} = {}
|
|
457
|
+
): readonly RegradeClass[] => [
|
|
458
|
+
...transition.symbolRenames.map((rename) =>
|
|
459
|
+
createAstIdentifierRenameClass({
|
|
460
|
+
describe: `Rename governed symbol "${rename.from}" to "${rename.to}" for ${transition.id}.`,
|
|
461
|
+
from: rename.from,
|
|
462
|
+
id: `ast-symbol-rename:${transition.id}:${rename.from}->${rename.to}`,
|
|
463
|
+
reviewDeclarationTypes: new Set(rename.reviewDeclarationTypes),
|
|
464
|
+
...(options.shouldPreserve === undefined
|
|
465
|
+
? {}
|
|
466
|
+
: { shouldPreserve: options.shouldPreserve }),
|
|
467
|
+
to: rename.to,
|
|
468
|
+
})
|
|
469
|
+
),
|
|
470
|
+
...transition.stringLiteralRenames.map((rename) =>
|
|
471
|
+
createAstStringLiteralRenameClass({
|
|
472
|
+
describe: `Rename governed string literal "${rename.from}" to "${rename.to}" for ${transition.id}.`,
|
|
473
|
+
from: rename.from,
|
|
474
|
+
id: `ast-string-literal-rename:${transition.id}:${rename.from}->${rename.to}`,
|
|
475
|
+
...(options.shouldPreserve === undefined
|
|
476
|
+
? {}
|
|
477
|
+
: { shouldPreserve: options.shouldPreserve }),
|
|
478
|
+
to: rename.to,
|
|
479
|
+
})
|
|
480
|
+
),
|
|
481
|
+
];
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
NotFoundError,
|
|
3
|
+
Result,
|
|
4
|
+
matchesAnyPathGlob,
|
|
5
|
+
trail,
|
|
6
|
+
} from '@ontrails/core';
|
|
2
7
|
import { readdirSync } from 'node:fs';
|
|
3
8
|
import { join, posix, relative, resolve, sep } from 'node:path';
|
|
4
9
|
import { z } from 'zod';
|
|
@@ -66,6 +71,10 @@ export interface DownstreamCollectionOptions {
|
|
|
66
71
|
readonly ignoredDirectories?: readonly string[];
|
|
67
72
|
/** Source extensions to collect. Defaults to {@link DEFAULT_SOURCE_EXTENSIONS}. */
|
|
68
73
|
readonly extensions?: readonly string[];
|
|
74
|
+
/** Root-relative path globs to skip before collection. */
|
|
75
|
+
readonly exclude?: readonly string[];
|
|
76
|
+
/** Root-relative path globs to collect. Omit to collect all matching files. */
|
|
77
|
+
readonly include?: readonly string[];
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/** Outcome of classifying a single directory entry. */
|
|
@@ -79,6 +88,16 @@ const extensionOf = (name: string): string => {
|
|
|
79
88
|
return dot <= 0 ? '' : name.slice(dot);
|
|
80
89
|
};
|
|
81
90
|
|
|
91
|
+
const normalizeExtension = (extension: string): string =>
|
|
92
|
+
extension === '' || extension.startsWith('.') ? extension : `.${extension}`;
|
|
93
|
+
|
|
94
|
+
const collectionExtensions = (
|
|
95
|
+
extensions: readonly string[] | undefined
|
|
96
|
+
): readonly string[] =>
|
|
97
|
+
extensions === undefined
|
|
98
|
+
? DEFAULT_SOURCE_EXTENSIONS
|
|
99
|
+
: extensions.map(normalizeExtension);
|
|
100
|
+
|
|
82
101
|
/**
|
|
83
102
|
* Decide what to do with a single directory entry. Pure: no filesystem access,
|
|
84
103
|
* so collection policy can be tested directly with synthetic entries.
|
|
@@ -90,7 +109,7 @@ export const classifyDownstreamEntry = (
|
|
|
90
109
|
): DownstreamEntryClassification => {
|
|
91
110
|
const ignoredDirectories =
|
|
92
111
|
options.ignoredDirectories ?? DEFAULT_IGNORED_DIRECTORIES;
|
|
93
|
-
const extensions = options.extensions
|
|
112
|
+
const extensions = collectionExtensions(options.extensions);
|
|
94
113
|
|
|
95
114
|
if (kind === 'directory') {
|
|
96
115
|
return ignoredDirectories.includes(name)
|
|
@@ -100,7 +119,7 @@ export const classifyDownstreamEntry = (
|
|
|
100
119
|
if (kind === 'other') {
|
|
101
120
|
return { action: 'skip', reason: 'unsupported-entry' };
|
|
102
121
|
}
|
|
103
|
-
return extensions.includes(extensionOf(name))
|
|
122
|
+
return extensions.length === 0 || extensions.includes(extensionOf(name))
|
|
104
123
|
? { action: 'collect' }
|
|
105
124
|
: { action: 'skip', reason: 'unsupported-extension' };
|
|
106
125
|
};
|
|
@@ -177,13 +196,25 @@ export const collectDownstreamSources = (
|
|
|
177
196
|
for (const entry of read.entries) {
|
|
178
197
|
const absolutePath = join(current, entry.name);
|
|
179
198
|
const path = toPosixRelative(absoluteRoot, absolutePath);
|
|
199
|
+
if (matchesAnyPathGlob(path, options.exclude)) {
|
|
200
|
+
skipped.push({ path, reason: 'ignored-glob' });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
180
203
|
const classification = classifyDownstreamEntry(
|
|
181
204
|
entry.name,
|
|
182
205
|
entry.kind,
|
|
183
206
|
options
|
|
184
207
|
);
|
|
185
208
|
if (classification.action === 'collect') {
|
|
186
|
-
|
|
209
|
+
if (
|
|
210
|
+
options.include !== undefined &&
|
|
211
|
+
options.include.length > 0 &&
|
|
212
|
+
!matchesAnyPathGlob(path, options.include)
|
|
213
|
+
) {
|
|
214
|
+
skipped.push({ path, reason: 'not-included-glob' });
|
|
215
|
+
} else {
|
|
216
|
+
files.push({ absolutePath, path });
|
|
217
|
+
}
|
|
187
218
|
} else if (classification.action === 'recurse') {
|
|
188
219
|
queue.push(absolutePath);
|
|
189
220
|
} else {
|
|
@@ -198,6 +229,10 @@ export const collectDownstreamSources = (
|
|
|
198
229
|
};
|
|
199
230
|
|
|
200
231
|
export const collectDownstreamSourcesInput = z.object({
|
|
232
|
+
exclude: z
|
|
233
|
+
.array(z.string())
|
|
234
|
+
.optional()
|
|
235
|
+
.describe('Root-relative path globs to skip before collection'),
|
|
201
236
|
extensions: z
|
|
202
237
|
.array(z.string())
|
|
203
238
|
.optional()
|
|
@@ -206,6 +241,10 @@ export const collectDownstreamSourcesInput = z.object({
|
|
|
206
241
|
.array(z.string())
|
|
207
242
|
.optional()
|
|
208
243
|
.describe('Directory names to skip during collection'),
|
|
244
|
+
include: z
|
|
245
|
+
.array(z.string())
|
|
246
|
+
.optional()
|
|
247
|
+
.describe('Root-relative path globs to collect'),
|
|
209
248
|
root: z
|
|
210
249
|
.string()
|
|
211
250
|
.describe('Absolute path to the downstream repo root to scan'),
|
|
@@ -247,6 +286,8 @@ export const collectDownstreamSourcesTrail = trail(
|
|
|
247
286
|
...(input.extensions === undefined
|
|
248
287
|
? {}
|
|
249
288
|
: { extensions: input.extensions }),
|
|
289
|
+
...(input.exclude === undefined ? {} : { exclude: input.exclude }),
|
|
290
|
+
...(input.include === undefined ? {} : { include: input.include }),
|
|
250
291
|
...(input.ignoredDirectories === undefined
|
|
251
292
|
? {}
|
|
252
293
|
: { ignoredDirectories: input.ignoredDirectories }),
|