vouchington-tooling 0.13.3 → 0.14.0

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.
@@ -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'
@@ -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. GitHub fallback is always
18
- * attempted when enabled — workflows must not gate it on `blob != 'true'`.
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. GitHub-fallback blob upload
24
- * must always be attempted when enabled; do not gate it on this signal. The upload subcommand's
25
- * exit code tracks the coverage pair, not the blob. A no-op outside CI (`githubOutputPath` unset).
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. GitHub fallback is always
45
- * attempted when enabled — workflows must not gate it on `blob != 'true'`.
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. GitHub-fallback blob upload
66
- * must always be attempted when enabled; do not gate it on this signal. The upload subcommand's
67
- * exit code tracks the coverage pair, not the blob. A no-op outside CI (`githubOutputPath` unset).
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -22,9 +22,12 @@ branching, review, and release policy.
22
22
  checks with a concrete reason.
23
23
  6. Review the diff for accidental files, secrets, generated output, broken documentation links,
24
24
  and assumptions that belong in local instructions instead.
25
+ 7. When feedback arrives on an open change, give every surfaced item a disposition and drive it to
26
+ closure before hand-off; do not leave items unaddressed or silently absorb or defer them.
25
27
 
26
28
  For portable implementation and review checks, read [implementation](references/implementation.md),
27
- [review](references/review.md), and [evidence sweep](references/evidence-sweep.md). The older
29
+ [review](references/review.md), [review response](references/review-response.md), and
30
+ [evidence sweep](references/evidence-sweep.md). The older
28
31
  [implementation and review](references/implementation-and-review.md) remains a compact overview.
29
32
  Local instructions remain authoritative for commands, commits, review systems, and release policy.
30
33
 
@@ -0,0 +1,32 @@
1
+ # Review response
2
+
3
+ Applies when responding to feedback on an open change — a human reviewer, an automated reviewer, or
4
+ a shepherding tool that surfaces both. This is the response side of [review](review.md); its closing
5
+ line, to record unresolved risk as an explicit decision or follow-up and never as an unnoticed gap,
6
+ is what the ladder below operationalizes.
7
+
8
+ Give every surfaced item exactly one disposition before its resolution runs, recorded where the
9
+ review conversation lives. Invalid — wrong, already satisfied by the current diff, or out of an
10
+ already-settled scope — gets a reason and closes with no code change and no follow-up. Blocking —
11
+ correctness, security, data safety, or a gap against a linked requirement — gets fixed, pushed, and
12
+ confirmed on the change's head commit before closing; closing first can leave an unfixed commit
13
+ behind a closed conversation. Non-blocking gets folded into an already-planned push when the fix is
14
+ cheap and low-risk, and otherwise gets recorded as a follow-up only when leaving it undone would
15
+ change behavior, structure, or risk: reuse or extend an existing follow-up before opening a new one,
16
+ and group related items from the same round into one. A correct item that clears none of those bars
17
+ — a style preference, a restatement, polish the change is fine without — is declined with a reason
18
+ and no follow-up; that is the expected outcome for a minor suggestion, not a lapse. Escalate — work
19
+ the change cannot absorb as feedback, such as a large architectural or ownership change — is
20
+ recorded where decisions are tracked and reported for direction rather than implemented or silently
21
+ downgraded to a follow-up.
22
+
23
+ Read every outstanding item before editing and drain the round locally; the cost of iterating is the
24
+ push, not the commit, because a push re-runs checks and re-triggers automated reviewers. Declining a
25
+ round without pushing is what lets it converge instead of regenerating the same suggestions on every
26
+ cycle. By hand-off, every item carries a disposition and reaches closure — an escalation through its
27
+ decision record rather than an open conversation — except where the review surface itself withholds
28
+ the capability to close an item; say so and leave it rather than forcing a resolution it never
29
+ authorized.
30
+
31
+ This skill supplies no review system, resolution mechanism, issue tracker, label, or severity
32
+ vocabulary; a consumer wrapper owns those.