@biffo/cli 0.296.7 → 0.296.9

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/claim.sh +72 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.296.7",
3
+ "version": "0.296.9",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/claim.sh CHANGED
@@ -285,6 +285,29 @@ remote_branches() {
285
285
  fi
286
286
  }
287
287
 
288
+ # What issue does $1 derive, per the `<type>/<number>-<slug>` convention,
289
+ # with leading zeros stripped -- or empty if $1 names no issue at all (#1672).
290
+ #
291
+ # ONE place both sides of a collision check go through. `--guard`'s own
292
+ # derivation used to run this pattern once for its own branch and then match
293
+ # CANDIDATE branches with a completely different technique -- a boundary-
294
+ # anchored substring search over the raw, unstripped candidate text. That
295
+ # let a zero-padded branch (`feat/0010-x`) go unmatched against an identical
296
+ # zero-padded sibling (`feat/0010-y`), because the search side had been
297
+ # stripped to `10` and `10` cannot match inside literal `0010` at a boundary
298
+ # (the character before it is `0`, itself alphanumeric). Deriving BOTH sides
299
+ # through this one function and comparing the two normalised numbers, rather
300
+ # than searching for one inside the other's text, removes the asymmetry
301
+ # structurally: whatever stripping happens to the target happens identically
302
+ # to the candidate. It is also deliberately narrower than a bare substring
303
+ # search -- `dm-04` and `104` carry no `/`, so nothing is derived from them,
304
+ # which is what keeps a bare `4` from colliding with either.
305
+ derive_branch_issue() {
306
+ _dbi_n=$(printf '%s' "$1" | sed -n 's#^[^/]*/\([0-9][0-9]*\)-.*#\1#p')
307
+ [ -n "$_dbi_n" ] || return 0
308
+ printf '%s' "$_dbi_n" | sed 's/^0*\([0-9]\)/\1/'
309
+ }
310
+
288
311
  # --- the structural claim predicate (#1411, class #1362 instance 8) ---------
289
312
  #
290
313
  # "Does this open PR claim issue $1?" used to be answered independently at
@@ -304,8 +327,24 @@ remote_branches() {
304
327
  # 1. `closingIssuesReferences` -- GitHub's OWN parse of a recognised closing
305
328
  # keyword (Closes/Fixes/Resolves and their inflections). Authoritative;
306
329
  # never re-derived by a regex here.
307
- # 2. the branch name (`<type>/<number>-slug`), the same test used
308
- # elsewhere in this file.
330
+ # 2. the branch name (`<type>/<number>-slug`) -- DERIVED the same way on
331
+ # BOTH sides (#1672), not matched as a substring of one. A boundary
332
+ # regex over the raw `headRefName` text, tested against a search number
333
+ # already stripped of leading zeros, is how a zero-padded PR went blind:
334
+ # `0010` strips to `10`, and `(^|[^0-9A-Za-z])10([^0-9A-Za-z]|$)` cannot
335
+ # match literal `0010`, because the preceding `0` is alphanumeric. Fixed
336
+ # by extracting the headRefName's OWN `<type>/<number>-` prefix via a jq
337
+ # capture, stripping ITS leading zeros the same way `guard_issue` is
338
+ # stripped below, and comparing the two normalised numbers for equality
339
+ # -- rather than searching for one inside the literal text of the other.
340
+ # This is deliberately narrower than a bare boundary-anchored substring
341
+ # search: `dm-04` and `104` carry no `/`, so nothing is derived from
342
+ # them at all, and a bare digit run that merely CONTAINS the target
343
+ # (`fix/13520-thing` vs issue 1352) still does not equal it once both
344
+ # are normalised. `capture()` produces NO output (not `false`) when the
345
+ # pattern does not match, which would silently drop the whole `or`
346
+ # chain for a PR like `chore/rename` if used bare -- wrapped in `[...]`
347
+ # first so a non-match becomes an empty array (one output), never zero.
309
348
  # 3. the PR body, but ONLY AGENTS.md's own `Refs #N` convention -- the form
310
349
  # this estate mandates for a PR that must reference an issue WITHOUT
311
350
  # closing it (DDL PRs, "instance of a class" PRs like this one). A
@@ -319,12 +358,17 @@ remote_branches() {
319
358
  # NOT claim #N" carries no claiming keyword adjacent to the `#N` and so
320
359
  # correctly does not match either (#1327, #1311's exact shape).
321
360
  #
361
+ # $1 is normalised (leading zeros stripped) HERE, once, rather than trusting
362
+ # every caller to have done it -- the plain (non-`--guard`) path passes the
363
+ # raw `$ISSUE` a caller typed, which may itself be zero-padded and was never
364
+ # stripped before reaching this function.
365
+ #
322
366
  # $2 is the repo slug (`owner/name`) the closing-reference check compares
323
367
  # against -- callers already resolve this via `repo_slug()` before calling.
324
368
  claim_select_expr() {
325
- _n="$1"
369
+ _n=$(printf '%s' "$1" | sed 's/^0*\([0-9]\)/\1/')
326
370
  _slug="$2"
327
- printf '(([.closingIssuesReferences[]? | select(.number == %s and ((.repository.owner.login + "/" + .repository.name) == "%s"))] | length > 0) or (.headRefName | test("(^|[^0-9A-Za-z])%s([^0-9A-Za-z]|$)")) or ((.body // "") | test("(^|[^0-9A-Za-z])(refs?|references?)[ \\t]*:?[ \\t]*#%s([^0-9A-Za-z]|$)"; "i")))' \
371
+ printf '(([.closingIssuesReferences[]? | select(.number == %s and ((.repository.owner.login + "/" + .repository.name) == "%s"))] | length > 0) or (([.headRefName | capture("^[^/]*/(?<n>[0-9]+)-")] | if length > 0 then ((.[0].n | sub("^0+";"")) as $s | if $s == "" then "0" else $s end) else "" end) == "%s") or ((.body // "") | test("(^|[^0-9A-Za-z])(refs?|references?)[ \\t]*:?[ \\t]*#%s([^0-9A-Za-z]|$)"; "i")))' \
328
372
  "$_n" "$_slug" "$_n" "$_n"
329
373
  }
330
374
 
@@ -399,7 +443,18 @@ fi
399
443
  # question ("would this push collide with someone else's live work?" rather
400
444
  # than "is this issue free to claim?").
401
445
  if [ -n "$GUARD_BRANCH" ]; then
402
- guard_issue=$(printf '%s' "$GUARD_BRANCH" | sed -n 's#^[^/]*/\([0-9][0-9]*\)-.*#\1#p')
446
+ # A BATCH BRANCH NAMES A SEQUENCE, NOT AN ISSUE.
447
+ #
448
+ # `batch/04-stale-reconverge` fits `<type>/<number>-<slug>` exactly, so this read `04` as
449
+ # an issue and refused the push. Measured 2026-08-21: it blocked the FIRST reconverge the
450
+ # Lander ever attempted -- and batching is the remedy `fleet-land` prescribes for a strict
451
+ # branch, so this defect blocks the strategy meant to fix landing. `batch/02-gated-trio`
452
+ # escaped only because no open branch happened to contain `-02-`.
453
+ case "$GUARD_BRANCH" in
454
+ batch/*) exit 0 ;;
455
+ esac
456
+
457
+ guard_issue=$(derive_branch_issue "$GUARD_BRANCH")
403
458
 
404
459
  # No issue named by the branch — most branches, e.g.
405
460
  # `security/brace-expansion-5-0-9`. Skip silently, and — this is the point —
@@ -443,10 +498,20 @@ if [ -n "$GUARD_BRANCH" ]; then
443
498
  cannot_tell=1
444
499
  cannot_tell_reasons="${cannot_tell_reasons}${branch_err_text} "
445
500
  else
501
+ # Derive each CANDIDATE branch's issue the same way $guard_issue was
502
+ # derived (#1672), and compare the two normalised numbers -- not a
503
+ # substring search for $guard_issue inside the candidate's raw text. See
504
+ # `derive_branch_issue` above for why that asymmetry was the defect.
446
505
  other_branch=$(printf '%s\n' "$raw_branches" |
447
506
  sed 's|.*refs/heads/||' |
448
- grep -E "(^|[^0-9A-Za-z])$guard_issue([^0-9A-Za-z]|$)" |
449
- grep -v -x "$GUARD_BRANCH" | head -1)
507
+ grep -v -x "$GUARD_BRANCH" |
508
+ while IFS= read -r _cand; do
509
+ [ -n "$_cand" ] || continue
510
+ _cand_issue=$(derive_branch_issue "$_cand")
511
+ if [ -n "$_cand_issue" ] && [ "$_cand_issue" = "$guard_issue" ]; then
512
+ printf '%s\n' "$_cand"
513
+ fi
514
+ done | head -1)
450
515
  if [ -n "$other_branch" ]; then
451
516
  conflict=1
452
517
  findings="${findings} ${RED}branch${OFF} $other_branch\n"