vouchington-tooling 0.13.2 → 0.13.4
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/README.md +5 -0
- package/ast-grep/rules/no-three-sequential-awaits-tsx.yml +178 -0
- package/ast-grep/rules/no-three-sequential-awaits.yml +169 -0
- package/dist/ast-grep-examples/companion-parity.d.mts +19 -0
- package/dist/ast-grep-examples/companion-parity.mjs +156 -0
- package/dist/ast-grep-examples/index.d.mts +2 -1
- package/dist/ast-grep-examples/index.mjs +1 -0
- package/dist/coverage-transport/outcome.d.mts +6 -5
- package/dist/coverage-transport/outcome.mjs +6 -8
- package/dist/gha-check-run/cli.d.mts +2 -0
- package/dist/gha-check-run/cli.mjs +81 -0
- package/dist/gha-check-run/github.d.mts +30 -0
- package/dist/gha-check-run/github.mjs +63 -0
- package/dist/gha-check-run/index.d.mts +2 -0
- package/dist/gha-check-run/index.mjs +2 -0
- package/dist/gha-post-review/cli.mjs +1 -1
- package/dist/gha-post-review/github.d.mts +6 -22
- package/dist/gha-post-review/github.mjs +8 -92
- package/dist/gha-post-review/post.d.mts +7 -1
- package/dist/gha-post-review/post.mjs +11 -3
- package/dist/gha-post-review/pull-io.d.mts +13 -0
- package/dist/gha-post-review/pull-io.mjs +88 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -1
- package/dist/scc-complexity/baseline.mjs +5 -2
- package/dist/scc-complexity/constants.d.mts +1 -0
- package/dist/scc-complexity/constants.mjs +1 -0
- package/dist/scc-complexity/index.d.mts +1 -1
- package/dist/scc-complexity/index.mjs +24 -36
- package/dist/scc-complexity/paths.d.mts +2 -1
- package/dist/scc-complexity/paths.mjs +35 -2
- package/dist/scc-complexity/scope-config.d.mts +3 -0
- package/dist/scc-complexity/scope-config.mjs +35 -0
- package/dist/scc-complexity/workflow-command.d.mts +2 -0
- package/dist/scc-complexity/workflow-command.mjs +6 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -93,6 +93,11 @@ ancestor of `HEAD`. `gitleaks-directory-scan` builds and scans isolated staged-i
|
|
|
93
93
|
nonignored-working-tree mirrors with an explicit config; `--directory` selects the repository root.
|
|
94
94
|
`ast-grep-examples` runs native `ast-grep test`, then validates each scoped rule's `files:` and
|
|
95
95
|
`ignores:` examples with project `languageGlobs` replay from its root `--config`.
|
|
96
|
+
`compareAstGrepCompanions` compares recursive `<name>.yml`/`.yaml` and `<name>-tsx` companion
|
|
97
|
+
rules across every YAML field, returns stable JSON-pointer differences, and rejects unsafe or
|
|
98
|
+
ambiguous rule paths. The supplied rule path and every ancestor must be physical directories; YAML
|
|
99
|
+
aliases and non-JSON tagged values are rejected before comparison. Callers own any explicit
|
|
100
|
+
normalization for language-specific differences.
|
|
96
101
|
`ast-grep-pack` prints JSON `{ rules, config }` for the shipped unconditional rule pack. Point a
|
|
97
102
|
consumer `sgconfig.yml` `ruleDirs` at `rules` and keep product-specific YAML locally.
|
|
98
103
|
`gha-workspace-policy` checks tracked workflow and composite-action files in the current repository;
|
|
@@ -12,6 +12,10 @@ ignores:
|
|
|
12
12
|
- 'web/test-helpers/**'
|
|
13
13
|
rule:
|
|
14
14
|
all:
|
|
15
|
+
- not:
|
|
16
|
+
any:
|
|
17
|
+
- matches: async-disposable-acquisition
|
|
18
|
+
- matches: async-disposable-commit
|
|
15
19
|
- not:
|
|
16
20
|
follows:
|
|
17
21
|
stopBy: neighbor
|
|
@@ -72,6 +76,10 @@ rule:
|
|
|
72
76
|
pattern: $W = await Promise.all($$$)
|
|
73
77
|
- not:
|
|
74
78
|
pattern: $W = await Promise.allSettled($$$)
|
|
79
|
+
not:
|
|
80
|
+
any:
|
|
81
|
+
- matches: async-disposable-acquisition
|
|
82
|
+
- matches: async-disposable-commit
|
|
75
83
|
- any:
|
|
76
84
|
- all:
|
|
77
85
|
- pattern:
|
|
@@ -189,8 +197,16 @@ rule:
|
|
|
189
197
|
pattern: $Y = await Promise.all($$$)
|
|
190
198
|
- not:
|
|
191
199
|
pattern: $Y = await Promise.allSettled($$$)
|
|
200
|
+
- not:
|
|
201
|
+
any:
|
|
202
|
+
- matches: async-disposable-acquisition
|
|
203
|
+
- matches: async-disposable-commit
|
|
192
204
|
- precedes:
|
|
193
205
|
stopBy: neighbor
|
|
206
|
+
not:
|
|
207
|
+
any:
|
|
208
|
+
- matches: async-disposable-acquisition
|
|
209
|
+
- matches: async-disposable-commit
|
|
194
210
|
any:
|
|
195
211
|
- all:
|
|
196
212
|
- pattern:
|
|
@@ -261,6 +277,69 @@ constraints:
|
|
|
261
277
|
P:
|
|
262
278
|
not:
|
|
263
279
|
regex: '^Promise\.all(Settled)?\b'
|
|
280
|
+
utils:
|
|
281
|
+
async-disposable-acquisition:
|
|
282
|
+
all:
|
|
283
|
+
- pattern:
|
|
284
|
+
context: |
|
|
285
|
+
async function run() {
|
|
286
|
+
await using $RESOURCE = await $ACQUIRE
|
|
287
|
+
}
|
|
288
|
+
selector: expression_statement
|
|
289
|
+
- precedes:
|
|
290
|
+
# Pair only with the nearest lifecycle endpoint to the right.
|
|
291
|
+
# Overlapping-resource policy belongs to a separate lifecycle rule.
|
|
292
|
+
stopBy:
|
|
293
|
+
any:
|
|
294
|
+
- pattern:
|
|
295
|
+
context: |
|
|
296
|
+
async function run() {
|
|
297
|
+
await using $_ = await $_
|
|
298
|
+
}
|
|
299
|
+
selector: expression_statement
|
|
300
|
+
- pattern:
|
|
301
|
+
context: |
|
|
302
|
+
async function run() {
|
|
303
|
+
await $_.commit($$$)
|
|
304
|
+
}
|
|
305
|
+
selector: expression_statement
|
|
306
|
+
pattern:
|
|
307
|
+
context: |
|
|
308
|
+
async function run() {
|
|
309
|
+
await $RESOURCE.commit($$$)
|
|
310
|
+
}
|
|
311
|
+
selector: expression_statement
|
|
312
|
+
async-disposable-commit:
|
|
313
|
+
all:
|
|
314
|
+
- pattern:
|
|
315
|
+
context: |
|
|
316
|
+
async function run() {
|
|
317
|
+
await $RESOURCE.commit($$$)
|
|
318
|
+
}
|
|
319
|
+
selector: expression_statement
|
|
320
|
+
- follows:
|
|
321
|
+
# Pair only with the nearest lifecycle endpoint to the left.
|
|
322
|
+
# Overlapping-resource policy belongs to a separate lifecycle rule.
|
|
323
|
+
stopBy:
|
|
324
|
+
any:
|
|
325
|
+
- pattern:
|
|
326
|
+
context: |
|
|
327
|
+
async function run() {
|
|
328
|
+
await using $_ = await $_
|
|
329
|
+
}
|
|
330
|
+
selector: expression_statement
|
|
331
|
+
- pattern:
|
|
332
|
+
context: |
|
|
333
|
+
async function run() {
|
|
334
|
+
await $_.commit($$$)
|
|
335
|
+
}
|
|
336
|
+
selector: expression_statement
|
|
337
|
+
pattern:
|
|
338
|
+
context: |
|
|
339
|
+
async function run() {
|
|
340
|
+
await using $RESOURCE = await $ACQUIRE
|
|
341
|
+
}
|
|
342
|
+
selector: expression_statement
|
|
264
343
|
examples:
|
|
265
344
|
- code: |
|
|
266
345
|
async function run() {
|
|
@@ -286,3 +365,102 @@ examples:
|
|
|
286
365
|
}
|
|
287
366
|
isValid: true
|
|
288
367
|
file: 'web/app/example.tsx'
|
|
368
|
+
- code: |
|
|
369
|
+
async function run() {
|
|
370
|
+
await using transaction = await beginTransaction()
|
|
371
|
+
await applyChange(transaction)
|
|
372
|
+
await transaction.commit()
|
|
373
|
+
return <div />
|
|
374
|
+
}
|
|
375
|
+
isValid: true
|
|
376
|
+
file: 'web/app/example.tsx'
|
|
377
|
+
- code: |
|
|
378
|
+
async function run() {
|
|
379
|
+
await using transaction = await beginBoundedTransaction({ timeoutMs: 1000 })
|
|
380
|
+
await applyFirstChange(transaction)
|
|
381
|
+
await applySecondChange(transaction)
|
|
382
|
+
await transaction.commit()
|
|
383
|
+
return <div />
|
|
384
|
+
}
|
|
385
|
+
isValid: true
|
|
386
|
+
file: 'web/app/example.tsx'
|
|
387
|
+
- code: |
|
|
388
|
+
async function run() {
|
|
389
|
+
await using transaction = await beginTransaction()
|
|
390
|
+
await applyFirstChange(transaction)
|
|
391
|
+
await applySecondChange(transaction)
|
|
392
|
+
await applyThirdChange(transaction)
|
|
393
|
+
await transaction.commit()
|
|
394
|
+
return <div />
|
|
395
|
+
}
|
|
396
|
+
isValid: false
|
|
397
|
+
file: 'web/app/example.tsx'
|
|
398
|
+
- code: |
|
|
399
|
+
async function run() {
|
|
400
|
+
await unrelatedFirst()
|
|
401
|
+
await unrelatedSecond()
|
|
402
|
+
await unrelatedThird()
|
|
403
|
+
await using transaction = await beginTransaction()
|
|
404
|
+
await applyChange(transaction)
|
|
405
|
+
await transaction.commit()
|
|
406
|
+
return <div />
|
|
407
|
+
}
|
|
408
|
+
isValid: false
|
|
409
|
+
file: 'web/app/example.tsx'
|
|
410
|
+
- code: |
|
|
411
|
+
async function run() {
|
|
412
|
+
await using transaction = await beginTransaction()
|
|
413
|
+
await applyFirstChange(transaction)
|
|
414
|
+
await applySecondChange(transaction)
|
|
415
|
+
await other.commit()
|
|
416
|
+
return <div />
|
|
417
|
+
}
|
|
418
|
+
isValid: false
|
|
419
|
+
file: 'web/app/example.tsx'
|
|
420
|
+
- code: |
|
|
421
|
+
async function run() {
|
|
422
|
+
await using transaction = await beginTransaction()
|
|
423
|
+
await applyChange(transaction)
|
|
424
|
+
await transaction.commit()
|
|
425
|
+
await afterFirst()
|
|
426
|
+
await afterSecond()
|
|
427
|
+
await afterThird()
|
|
428
|
+
return <div />
|
|
429
|
+
}
|
|
430
|
+
isValid: false
|
|
431
|
+
file: 'web/app/example.tsx'
|
|
432
|
+
- code: |
|
|
433
|
+
async function run() {
|
|
434
|
+
await using outer = await acquire()
|
|
435
|
+
{
|
|
436
|
+
await using inner = await acquire()
|
|
437
|
+
await innerFirst(inner)
|
|
438
|
+
await innerSecond(inner)
|
|
439
|
+
await innerThird(inner)
|
|
440
|
+
await inner.commit()
|
|
441
|
+
}
|
|
442
|
+
await outer.commit()
|
|
443
|
+
return <div />
|
|
444
|
+
}
|
|
445
|
+
isValid: false
|
|
446
|
+
file: 'web/app/example.tsx'
|
|
447
|
+
- code: |
|
|
448
|
+
async function run() {
|
|
449
|
+
await using transaction = await beginTransaction()
|
|
450
|
+
await applyFirstChange(transaction)
|
|
451
|
+
await applySecondChange(transaction)
|
|
452
|
+
await transaction.rollback()
|
|
453
|
+
return <div />
|
|
454
|
+
}
|
|
455
|
+
isValid: false
|
|
456
|
+
file: 'web/app/example.tsx'
|
|
457
|
+
- code: |
|
|
458
|
+
async function run() {
|
|
459
|
+
await using resource = await acquire()
|
|
460
|
+
await useFirst(resource)
|
|
461
|
+
await useSecond(resource)
|
|
462
|
+
await resource.dispose()
|
|
463
|
+
return <div />
|
|
464
|
+
}
|
|
465
|
+
isValid: false
|
|
466
|
+
file: 'web/app/example.tsx'
|
|
@@ -33,6 +33,10 @@ ignores:
|
|
|
33
33
|
- 'backend/scripts/explain-analyze/**'
|
|
34
34
|
rule:
|
|
35
35
|
all:
|
|
36
|
+
- not:
|
|
37
|
+
any:
|
|
38
|
+
- matches: async-disposable-acquisition
|
|
39
|
+
- matches: async-disposable-commit
|
|
36
40
|
- not:
|
|
37
41
|
follows:
|
|
38
42
|
stopBy: neighbor
|
|
@@ -73,6 +77,10 @@ rule:
|
|
|
73
77
|
pattern: $W = await Promise.all($$$)
|
|
74
78
|
- not:
|
|
75
79
|
pattern: $W = await Promise.allSettled($$$)
|
|
80
|
+
not:
|
|
81
|
+
any:
|
|
82
|
+
- matches: async-disposable-acquisition
|
|
83
|
+
- matches: async-disposable-commit
|
|
76
84
|
- any:
|
|
77
85
|
- all:
|
|
78
86
|
- pattern:
|
|
@@ -150,8 +158,16 @@ rule:
|
|
|
150
158
|
pattern: $Y = await Promise.all($$$)
|
|
151
159
|
- not:
|
|
152
160
|
pattern: $Y = await Promise.allSettled($$$)
|
|
161
|
+
- not:
|
|
162
|
+
any:
|
|
163
|
+
- matches: async-disposable-acquisition
|
|
164
|
+
- matches: async-disposable-commit
|
|
153
165
|
- precedes:
|
|
154
166
|
stopBy: neighbor
|
|
167
|
+
not:
|
|
168
|
+
any:
|
|
169
|
+
- matches: async-disposable-acquisition
|
|
170
|
+
- matches: async-disposable-commit
|
|
155
171
|
any:
|
|
156
172
|
- all:
|
|
157
173
|
- pattern:
|
|
@@ -202,6 +218,69 @@ constraints:
|
|
|
202
218
|
P:
|
|
203
219
|
not:
|
|
204
220
|
regex: '^Promise\.all(Settled)?\b'
|
|
221
|
+
utils:
|
|
222
|
+
async-disposable-acquisition:
|
|
223
|
+
all:
|
|
224
|
+
- pattern:
|
|
225
|
+
context: |
|
|
226
|
+
async function run() {
|
|
227
|
+
await using $RESOURCE = await $ACQUIRE
|
|
228
|
+
}
|
|
229
|
+
selector: expression_statement
|
|
230
|
+
- precedes:
|
|
231
|
+
# Pair only with the nearest lifecycle endpoint to the right.
|
|
232
|
+
# Overlapping-resource policy belongs to a separate lifecycle rule.
|
|
233
|
+
stopBy:
|
|
234
|
+
any:
|
|
235
|
+
- pattern:
|
|
236
|
+
context: |
|
|
237
|
+
async function run() {
|
|
238
|
+
await using $_ = await $_
|
|
239
|
+
}
|
|
240
|
+
selector: expression_statement
|
|
241
|
+
- pattern:
|
|
242
|
+
context: |
|
|
243
|
+
async function run() {
|
|
244
|
+
await $_.commit($$$)
|
|
245
|
+
}
|
|
246
|
+
selector: expression_statement
|
|
247
|
+
pattern:
|
|
248
|
+
context: |
|
|
249
|
+
async function run() {
|
|
250
|
+
await $RESOURCE.commit($$$)
|
|
251
|
+
}
|
|
252
|
+
selector: expression_statement
|
|
253
|
+
async-disposable-commit:
|
|
254
|
+
all:
|
|
255
|
+
- pattern:
|
|
256
|
+
context: |
|
|
257
|
+
async function run() {
|
|
258
|
+
await $RESOURCE.commit($$$)
|
|
259
|
+
}
|
|
260
|
+
selector: expression_statement
|
|
261
|
+
- follows:
|
|
262
|
+
# Pair only with the nearest lifecycle endpoint to the left.
|
|
263
|
+
# Overlapping-resource policy belongs to a separate lifecycle rule.
|
|
264
|
+
stopBy:
|
|
265
|
+
any:
|
|
266
|
+
- pattern:
|
|
267
|
+
context: |
|
|
268
|
+
async function run() {
|
|
269
|
+
await using $_ = await $_
|
|
270
|
+
}
|
|
271
|
+
selector: expression_statement
|
|
272
|
+
- pattern:
|
|
273
|
+
context: |
|
|
274
|
+
async function run() {
|
|
275
|
+
await $_.commit($$$)
|
|
276
|
+
}
|
|
277
|
+
selector: expression_statement
|
|
278
|
+
pattern:
|
|
279
|
+
context: |
|
|
280
|
+
async function run() {
|
|
281
|
+
await using $RESOURCE = await $ACQUIRE
|
|
282
|
+
}
|
|
283
|
+
selector: expression_statement
|
|
205
284
|
examples:
|
|
206
285
|
- code: |
|
|
207
286
|
async function run() {
|
|
@@ -240,3 +319,93 @@ examples:
|
|
|
240
319
|
}
|
|
241
320
|
isValid: true
|
|
242
321
|
file: 'backend/services/example.mts'
|
|
322
|
+
- code: |
|
|
323
|
+
async function run() {
|
|
324
|
+
await using transaction = await beginTransaction()
|
|
325
|
+
await applyChange(transaction)
|
|
326
|
+
await transaction.commit()
|
|
327
|
+
}
|
|
328
|
+
isValid: true
|
|
329
|
+
file: 'backend/services/example.mts'
|
|
330
|
+
- code: |
|
|
331
|
+
async function run() {
|
|
332
|
+
await using transaction = await beginBoundedTransaction({ timeoutMs: 1000 })
|
|
333
|
+
await applyFirstChange(transaction)
|
|
334
|
+
await applySecondChange(transaction)
|
|
335
|
+
await transaction.commit()
|
|
336
|
+
}
|
|
337
|
+
isValid: true
|
|
338
|
+
file: 'backend/services/example.mts'
|
|
339
|
+
- code: |
|
|
340
|
+
async function run() {
|
|
341
|
+
await using transaction = await beginTransaction()
|
|
342
|
+
await applyFirstChange(transaction)
|
|
343
|
+
await applySecondChange(transaction)
|
|
344
|
+
await applyThirdChange(transaction)
|
|
345
|
+
await transaction.commit()
|
|
346
|
+
}
|
|
347
|
+
isValid: false
|
|
348
|
+
file: 'backend/services/example.mts'
|
|
349
|
+
- code: |
|
|
350
|
+
async function run() {
|
|
351
|
+
await unrelatedFirst()
|
|
352
|
+
await unrelatedSecond()
|
|
353
|
+
await unrelatedThird()
|
|
354
|
+
await using transaction = await beginTransaction()
|
|
355
|
+
await applyChange(transaction)
|
|
356
|
+
await transaction.commit()
|
|
357
|
+
}
|
|
358
|
+
isValid: false
|
|
359
|
+
file: 'backend/services/example.mts'
|
|
360
|
+
- code: |
|
|
361
|
+
async function run() {
|
|
362
|
+
await using transaction = await beginTransaction()
|
|
363
|
+
await applyFirstChange(transaction)
|
|
364
|
+
await applySecondChange(transaction)
|
|
365
|
+
await other.commit()
|
|
366
|
+
}
|
|
367
|
+
isValid: false
|
|
368
|
+
file: 'backend/services/example.mts'
|
|
369
|
+
- code: |
|
|
370
|
+
async function run() {
|
|
371
|
+
await using transaction = await beginTransaction()
|
|
372
|
+
await applyChange(transaction)
|
|
373
|
+
await transaction.commit()
|
|
374
|
+
await afterFirst()
|
|
375
|
+
await afterSecond()
|
|
376
|
+
await afterThird()
|
|
377
|
+
}
|
|
378
|
+
isValid: false
|
|
379
|
+
file: 'backend/services/example.mts'
|
|
380
|
+
- code: |
|
|
381
|
+
async function run() {
|
|
382
|
+
await using outer = await acquire()
|
|
383
|
+
{
|
|
384
|
+
await using inner = await acquire()
|
|
385
|
+
await innerFirst(inner)
|
|
386
|
+
await innerSecond(inner)
|
|
387
|
+
await innerThird(inner)
|
|
388
|
+
await inner.commit()
|
|
389
|
+
}
|
|
390
|
+
await outer.commit()
|
|
391
|
+
}
|
|
392
|
+
isValid: false
|
|
393
|
+
file: 'backend/services/example.mts'
|
|
394
|
+
- code: |
|
|
395
|
+
async function run() {
|
|
396
|
+
await using transaction = await beginTransaction()
|
|
397
|
+
await applyFirstChange(transaction)
|
|
398
|
+
await applySecondChange(transaction)
|
|
399
|
+
await transaction.rollback()
|
|
400
|
+
}
|
|
401
|
+
isValid: false
|
|
402
|
+
file: 'backend/services/example.mts'
|
|
403
|
+
- code: |
|
|
404
|
+
async function run() {
|
|
405
|
+
await using resource = await acquire()
|
|
406
|
+
await useFirst(resource)
|
|
407
|
+
await useSecond(resource)
|
|
408
|
+
await resource.dispose()
|
|
409
|
+
}
|
|
410
|
+
isValid: false
|
|
411
|
+
file: 'backend/services/example.mts'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AstGrepCompanionDifference {
|
|
2
|
+
readonly baseFile: string;
|
|
3
|
+
readonly companionFile: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly base: unknown;
|
|
6
|
+
readonly companion: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface AstGrepCompanionParityOptions {
|
|
9
|
+
readonly rules: string;
|
|
10
|
+
readonly companionSuffix?: string;
|
|
11
|
+
readonly normalize?: (document: unknown, context: AstGrepCompanionContext) => unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface AstGrepCompanionContext {
|
|
14
|
+
readonly file: string;
|
|
15
|
+
readonly role: 'base' | 'companion';
|
|
16
|
+
}
|
|
17
|
+
export declare function compareCodeUnits(left: string, right: string): number;
|
|
18
|
+
/** Compares recursive `<name>.yml`/`.yaml` rules with `<name>-tsx` companions. */
|
|
19
|
+
export declare function compareAstGrepCompanions(options: AstGrepCompanionParityOptions): AstGrepCompanionDifference[];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { parse as yamlLoad } from 'yaml';
|
|
4
|
+
const YAML_EXTENSION = /\.ya?ml$/u;
|
|
5
|
+
export function compareCodeUnits(left, right) {
|
|
6
|
+
if (left < right)
|
|
7
|
+
return -1;
|
|
8
|
+
if (left > right)
|
|
9
|
+
return 1;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
function assertPhysicalPath(path) {
|
|
13
|
+
let ancestor = path;
|
|
14
|
+
while (true) {
|
|
15
|
+
if (fs.lstatSync(ancestor).isSymbolicLink())
|
|
16
|
+
throw new Error('rules: symbolic links are not allowed');
|
|
17
|
+
const parent = dirname(ancestor);
|
|
18
|
+
if (parent === ancestor)
|
|
19
|
+
return;
|
|
20
|
+
ancestor = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function yamlFiles(directory, relative = '') {
|
|
24
|
+
const entries = fs
|
|
25
|
+
.readdirSync(directory, { withFileTypes: true })
|
|
26
|
+
.toSorted((a, b) => compareCodeUnits(a.name, b.name));
|
|
27
|
+
const files = [];
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const file = relative ? `${relative}/${entry.name}` : entry.name;
|
|
30
|
+
if (entry.isSymbolicLink())
|
|
31
|
+
throw new Error(`${file}: symbolic links are not allowed`);
|
|
32
|
+
if (entry.isDirectory())
|
|
33
|
+
files.push(...yamlFiles(join(directory, entry.name), file));
|
|
34
|
+
else if (entry.isFile() && YAML_EXTENSION.test(entry.name))
|
|
35
|
+
files.push(file);
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
function loadDocument(rules, file) {
|
|
40
|
+
try {
|
|
41
|
+
const document = yamlLoad(fs.readFileSync(join(rules, file), 'utf8'), { maxAliasCount: 0 });
|
|
42
|
+
assertJsonValue(document);
|
|
43
|
+
return document;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new Error(`${file}: invalid YAML: ${String(error)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function stem(file) {
|
|
50
|
+
return file.replace(YAML_EXTENSION, '');
|
|
51
|
+
}
|
|
52
|
+
function pointer(path, key) {
|
|
53
|
+
const segment = String(key).replaceAll('~', '~0').replaceAll('/', '~1');
|
|
54
|
+
return `${path}/${segment}`;
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
58
|
+
return false;
|
|
59
|
+
const prototype = Object.getPrototypeOf(value);
|
|
60
|
+
return prototype === null || prototype === Object.prototype;
|
|
61
|
+
}
|
|
62
|
+
function assertJsonValue(value, ancestors = new WeakSet()) {
|
|
63
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
64
|
+
return;
|
|
65
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
66
|
+
return;
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
if (ancestors.has(value))
|
|
69
|
+
throw new Error('unsupported cyclic YAML value');
|
|
70
|
+
ancestors.add(value);
|
|
71
|
+
for (const item of value)
|
|
72
|
+
assertJsonValue(item, ancestors);
|
|
73
|
+
ancestors.delete(value);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (isRecord(value)) {
|
|
77
|
+
if (ancestors.has(value))
|
|
78
|
+
throw new Error('unsupported cyclic YAML value');
|
|
79
|
+
ancestors.add(value);
|
|
80
|
+
for (const item of Object.values(value))
|
|
81
|
+
assertJsonValue(item, ancestors);
|
|
82
|
+
ancestors.delete(value);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new Error('unsupported non-JSON YAML value');
|
|
86
|
+
}
|
|
87
|
+
function compare(base, companion, path, difference) {
|
|
88
|
+
if (Object.is(base, companion))
|
|
89
|
+
return;
|
|
90
|
+
if (Array.isArray(base) && Array.isArray(companion)) {
|
|
91
|
+
const length = Math.max(base.length, companion.length);
|
|
92
|
+
for (let index = 0; index < length; index++)
|
|
93
|
+
compare(base[index], companion[index], pointer(path, index), difference);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (isRecord(base) && isRecord(companion)) {
|
|
97
|
+
for (const key of [...new Set([...Object.keys(base), ...Object.keys(companion)])].toSorted(compareCodeUnits))
|
|
98
|
+
compare(base[key], companion[key], pointer(path, key), difference);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
difference(path, base, companion);
|
|
102
|
+
}
|
|
103
|
+
function assertSuffix(suffix) {
|
|
104
|
+
if (!suffix || /[\\/]/u.test(suffix))
|
|
105
|
+
throw new Error('companionSuffix must be a non-empty filename suffix');
|
|
106
|
+
}
|
|
107
|
+
/** Compares recursive `<name>.yml`/`.yaml` rules with `<name>-tsx` companions. */
|
|
108
|
+
export function compareAstGrepCompanions(options) {
|
|
109
|
+
const suffix = options.companionSuffix ?? '-tsx';
|
|
110
|
+
assertSuffix(suffix);
|
|
111
|
+
const rules = resolve(options.rules);
|
|
112
|
+
assertPhysicalPath(rules);
|
|
113
|
+
const files = yamlFiles(rules);
|
|
114
|
+
const basesByStem = new Map();
|
|
115
|
+
const companions = [];
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const fileStem = stem(file);
|
|
118
|
+
if (fileStem.endsWith(suffix))
|
|
119
|
+
companions.push({ file, baseStem: fileStem.slice(0, -suffix.length) });
|
|
120
|
+
else
|
|
121
|
+
basesByStem.set(fileStem, [...(basesByStem.get(fileStem) ?? []), file]);
|
|
122
|
+
}
|
|
123
|
+
const companionsByStem = new Map();
|
|
124
|
+
for (const { file, baseStem } of companions)
|
|
125
|
+
companionsByStem.set(baseStem, [...(companionsByStem.get(baseStem) ?? []), file]);
|
|
126
|
+
for (const [baseStem, companionFiles] of companionsByStem)
|
|
127
|
+
if (companionFiles.length > 1)
|
|
128
|
+
throw new Error(`${baseStem}: duplicate companion rules: ${companionFiles.toSorted(compareCodeUnits).join(', ')}`);
|
|
129
|
+
const differences = [];
|
|
130
|
+
for (const { file: companionFile, baseStem } of companions) {
|
|
131
|
+
const bases = basesByStem.get(baseStem) ?? [];
|
|
132
|
+
if (!bases.length)
|
|
133
|
+
throw new Error(`${companionFile}: missing base companion`);
|
|
134
|
+
if (bases.length > 1)
|
|
135
|
+
throw new Error(`${companionFile}: duplicate base companions: ${bases.toSorted(compareCodeUnits).join(', ')}`);
|
|
136
|
+
const baseFile = bases[0];
|
|
137
|
+
const normalize = options.normalize ?? ((document) => document);
|
|
138
|
+
const base = normalize(loadDocument(rules, baseFile), { file: baseFile, role: 'base' });
|
|
139
|
+
const companion = normalize(loadDocument(rules, companionFile), {
|
|
140
|
+
file: companionFile,
|
|
141
|
+
role: 'companion',
|
|
142
|
+
});
|
|
143
|
+
assertJsonValue(base);
|
|
144
|
+
assertJsonValue(companion);
|
|
145
|
+
compare(base, companion, '', (path, baseValue, companionValue) => {
|
|
146
|
+
differences.push({
|
|
147
|
+
baseFile,
|
|
148
|
+
companionFile,
|
|
149
|
+
path,
|
|
150
|
+
base: baseValue,
|
|
151
|
+
companion: companionValue,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return differences;
|
|
156
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { compareAstGrepCompanions } from './companion-parity.mts';
|
|
2
|
+
export type { AstGrepCompanionContext, AstGrepCompanionDifference, AstGrepCompanionParityOptions, } from './companion-parity.mts';
|
|
1
3
|
interface AstGrepResult {
|
|
2
4
|
status: number | null;
|
|
3
5
|
stdout?: string;
|
|
@@ -12,4 +14,3 @@ export interface AstGrepExamplesOptions {
|
|
|
12
14
|
}
|
|
13
15
|
export declare function astGrepExamplesArguments(options: AstGrepExamplesOptions): string[];
|
|
14
16
|
export declare function runAstGrepExamples(options: AstGrepExamplesOptions): number;
|
|
15
|
-
export {};
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import picomatch from 'picomatch';
|
|
6
6
|
import { parse as yamlLoad, stringify as yamlDump } from 'yaml';
|
|
7
|
+
export { compareAstGrepCompanions } from './companion-parity.mjs';
|
|
7
8
|
export function astGrepExamplesArguments(options) {
|
|
8
9
|
return [
|
|
9
10
|
'test',
|
|
@@ -14,15 +14,16 @@ export type BlobPrimaryState = 'true' | 'false' | 'skipped';
|
|
|
14
14
|
export declare function isBlobPrimaryState(value: string | undefined): value is BlobPrimaryState;
|
|
15
15
|
/**
|
|
16
16
|
* Sibling to `assertCoverageTransportOutcome` for the Vitest blob. `true`/`false` are the S3
|
|
17
|
-
* upload step's `blob` output; `skipped` means that step never ran.
|
|
18
|
-
*
|
|
17
|
+
* upload step's `blob` output; `skipped` means that step never ran. Workflows gate the GitHub
|
|
18
|
+
* fallback on `blob != 'true'`, so a persisted S3 primary with no GitHub artifact attempt is the
|
|
19
|
+
* expected steady state, not a degraded one.
|
|
19
20
|
*/
|
|
20
21
|
export declare function assertCoverageTransportBlobOutcome(suite: string, primaryPersisted: BlobPrimaryState, artifactAttempt1: StepOutcome, artifactAttempt2: StepOutcome, emit?: (line: string) => void): boolean;
|
|
21
22
|
export type AppendOutput = (path: string, data: string) => void;
|
|
22
23
|
/**
|
|
23
|
-
* Writes `blob=true|false` to `$GITHUB_OUTPUT` for outcome reporting.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Writes `blob=true|false` to `$GITHUB_OUTPUT` for outcome reporting. Workflows gate the
|
|
25
|
+
* GitHub-fallback blob upload on this signal (`blob != 'true'`). The upload subcommand's exit
|
|
26
|
+
* code tracks the coverage pair, not the blob. A no-op outside CI (`githubOutputPath` unset).
|
|
26
27
|
*/
|
|
27
28
|
export declare function writeUploadOutcomeOutput(outcome: {
|
|
28
29
|
readonly blob: boolean;
|
|
@@ -41,15 +41,13 @@ export function isBlobPrimaryState(value) {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Sibling to `assertCoverageTransportOutcome` for the Vitest blob. `true`/`false` are the S3
|
|
44
|
-
* upload step's `blob` output; `skipped` means that step never ran.
|
|
45
|
-
*
|
|
44
|
+
* upload step's `blob` output; `skipped` means that step never ran. Workflows gate the GitHub
|
|
45
|
+
* fallback on `blob != 'true'`, so a persisted S3 primary with no GitHub artifact attempt is the
|
|
46
|
+
* expected steady state, not a degraded one.
|
|
46
47
|
*/
|
|
47
48
|
export function assertCoverageTransportBlobOutcome(suite, primaryPersisted, artifactAttempt1, artifactAttempt2, emit = (line) => process.stderr.write(`${line}\n`)) {
|
|
48
49
|
const artifactSucceeded = artifactAttempt1 === 'success' || artifactAttempt2 === 'success';
|
|
49
50
|
if (primaryPersisted === 'true') {
|
|
50
|
-
if (!artifactSucceeded) {
|
|
51
|
-
emit(`::warning::Vitest blob persisted only to S3 for suite=${suite}; GitHub artifact fallback is degraded.`);
|
|
52
|
-
}
|
|
53
51
|
return true;
|
|
54
52
|
}
|
|
55
53
|
if (artifactSucceeded) {
|
|
@@ -62,9 +60,9 @@ export function assertCoverageTransportBlobOutcome(suite, primaryPersisted, arti
|
|
|
62
60
|
return false;
|
|
63
61
|
}
|
|
64
62
|
/**
|
|
65
|
-
* Writes `blob=true|false` to `$GITHUB_OUTPUT` for outcome reporting.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
63
|
+
* Writes `blob=true|false` to `$GITHUB_OUTPUT` for outcome reporting. Workflows gate the
|
|
64
|
+
* GitHub-fallback blob upload on this signal (`blob != 'true'`). The upload subcommand's exit
|
|
65
|
+
* code tracks the coverage pair, not the blob. A no-op outside CI (`githubOutputPath` unset).
|
|
68
66
|
*/
|
|
69
67
|
export function writeUploadOutcomeOutput(outcome, githubOutputPath, appendOutput = appendFileSync) {
|
|
70
68
|
if (githubOutputPath)
|