@biffo/cli 0.297.1 → 0.297.2

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 +195 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.297.1",
3
+ "version": "0.297.2",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/claim.sh CHANGED
@@ -107,6 +107,21 @@
107
107
  # - **Excludes the branch being pushed, and any PR whose head IS that
108
108
  # branch**, from counting as a conflict. Without this, pushing your own
109
109
  # branch a second time blocks you on your own work.
110
+ # - **Excludes a SUPERSEDED PREDECESSOR** — a remote branch naming the same
111
+ # issue whose every commit is already carried by the branch being pushed,
112
+ # which is what a rebase leaves behind. An issue number has no lineage, so
113
+ # the number-only comparison could not tell that branch from a rival
114
+ # session's, and a dead predecessor blocked every later push naming the
115
+ # issue (tabsii-com/tabsii-platform#1112). Decided from the object graph by
116
+ # `branch_is_absorbed` below, which fails CLOSED — anything it cannot
117
+ # positively demonstrate stays a conflict, INCLUDING a candidate carrying
118
+ # no commits of its own. A branch sitting at `dev`'s tip is a reservation
119
+ # (AGENTS.md asks for exactly that: "push your branch as soon as it
120
+ # exists"), and "every commit on it is already carried" is vacuously true
121
+ # of a branch with no commits — so without that requirement the discount
122
+ # waved through the most ordinary rival there is. Note the discount applies
123
+ # to the BRANCH signal only: an OPEN PR is a live claim regardless of
124
+ # lineage and is never discounted.
110
125
  # - **A real conflict — another branch or another open PR naming the same
111
126
  # issue — exits 1**, naming what was found. AGENTS.md permits stealing a
112
127
  # claim that is over an hour stale, with a comment; the message points
@@ -308,6 +323,146 @@ derive_branch_issue() {
308
323
  printf '%s' "$_dbi_n" | sed 's/^0*\([0-9]\)/\1/'
309
324
  }
310
325
 
326
+ # --- lineage: is remote branch tip $1 already carried by local branch $2? -----
327
+ #
328
+ # The branch check below compares ISSUE NUMBERS, and a number has no lineage.
329
+ # So it could not tell a genuine RIVAL CLAIMANT -- another session working the
330
+ # same issue -- from the pusher's OWN SUPERSEDED PREDECESSOR, the branch they
331
+ # abandoned and rebased away from. Measured live on tabsii-platform:
332
+ # `fix/1050-1033-1061-upstream-carry` is still on the remote at 5b1b8977 while
333
+ # its successor `fix/1050-1033-1061-carry-rebased` was auto-deleted on merge,
334
+ # so every future push naming 1050 is refused by a dead branch
335
+ # (tabsii-com/tabsii-platform#1112).
336
+ #
337
+ # **`git merge-base --is-ancestor` is not the primitive.** A rebase rewrites
338
+ # every commit, so the predecessor's tip stops being an ancestor of its own
339
+ # successor -- verified by experiment (`is-ancestor` rc=1 on exactly the pair
340
+ # `git cherry` reports as fully equivalent). Ancestry answers a strictly
341
+ # narrower question and would have caught none of the reported case.
342
+ #
343
+ # So the question asked here is not "is this branch mine?" -- identity is not a
344
+ # trustworthy signal in this estate and this script never compares it -- but the
345
+ # stronger, decidable one:
346
+ #
347
+ # **does the candidate carry any work the branch being pushed does not
348
+ # already have?**
349
+ #
350
+ # If it does not, there is nothing to collide over whoever made it: pushing
351
+ # cannot duplicate or lose work that is already in hand. That is a property of
352
+ # the object graph, not of a name, and it is what makes the discount safe to
353
+ # grant automatically rather than via a hand-maintained skip list.
354
+ #
355
+ # **A claim reserves FUTURE work; a content test can only see PAST work.**
356
+ # This is the predicate's structural limit, and the next reader should have it
357
+ # rather than rediscover it. `git cherry` compares commits that exist; a claim
358
+ # is about commits that do not exist yet. So no content-subset test can ever be
359
+ # complete, and the only safe posture is the one taken here: discount ONLY on
360
+ # positive evidence that the candidate is a replay of work already in hand, and
361
+ # read everything else -- including its own silence -- as a rival.
362
+ #
363
+ # It FAILS CLOSED at every step -- each `return 1` means "conflict", and the
364
+ # only route to `return 0` is positive evidence:
365
+ #
366
+ # 1. Both refs must be known. No local tip, no candidate sha, no discount.
367
+ # 2. The candidate's objects must be present LOCALLY. A branch this machine
368
+ # has never fetched cannot be shown to be superseded, and absence of
369
+ # evidence is not evidence of supersession.
370
+ # 3. The candidate must carry AT LEAST ONE COMMIT OF ITS OWN. Step 5 asks
371
+ # "does the candidate carry work this push does not already have?", and
372
+ # that question is VACUOUSLY TRUE of a candidate with no commits at all --
373
+ # a branch pointing at `dev`'s tip, or at anything else already reachable
374
+ # from the pusher. `git cherry` then emits nothing, no `+` line is found,
375
+ # and a LIVE RESERVATION is discounted while the notice calls it a
376
+ # superseded predecessor. Reproduced: with `fix/1050-other-agent` pushed
377
+ # at `dev`'s tip, `--guard fix/1050-mine` printed `discounted
378
+ # fix/1050-other-agent` and exited 0.
379
+ #
380
+ # That is the LIKELY rival shape, not an exotic one. AGENTS.md asks for
381
+ # exactly it -- "push your branch as soon as it exists. The claim is a
382
+ # reservation; the branch is the evidence" -- so an agent that stakes an
383
+ # issue before writing code produces a commitless branch by following the
384
+ # documented protocol. It is also symmetric: two sessions staking one
385
+ # issue seconds apart both sit at `dev`'s tip and would each discount the
386
+ # other, which is the 2026-08-03 collision shape. Nothing else catches it
387
+ # either -- a commitless branch has no open PR to find, and `--guard`
388
+ # deliberately never reads the `in-progress` label.
389
+ #
390
+ # **Own commits are counted against the branch being pushed, not against
391
+ # `dev`** -- `git rev-list <candidate> --not <local tip>`, i.e. the
392
+ # commits on the candidate's side of the merge base with this push. Three
393
+ # reasons that is the right base:
394
+ # - It is the SAME frame of reference steps 4 and 5 already use, so the
395
+ # three cannot disagree about what "beyond" means. A second base would
396
+ # be a second authority, which is this estate's most-repeated defect.
397
+ # - `dev` is not knowable here. `--guard` is handed one branch name by a
398
+ # pre-push hook, is never told the integration branch, and a candidate
399
+ # need not derive from it anyway.
400
+ # - It answers the question actually being asked. Every candidate that
401
+ # must not be discounted counts zero against it: at `dev`'s tip, at
402
+ # the pusher's own tip (the symmetric race), and at any older ancestor
403
+ # -- all are already reachable, so none carries anything of its own.
404
+ # A candidate whose only commits are MERGES counts non-zero here and is
405
+ # refused by step 4 instead; this step is deliberately not the one that
406
+ # decides that case.
407
+ # 4. No unmerged MERGE COMMIT. `git cherry` compares non-merge commits by
408
+ # patch id and omits merges entirely -- verified: a candidate carrying one
409
+ # merge commit had that commit listed by neither `+` nor `-`, so an evil
410
+ # merge's own resolution would be invisible. Refuse rather than guess.
411
+ # 5. `git cherry <local tip> <candidate>` must emit no `+` line. `-` means an
412
+ # equivalent patch is already present (what a rebase produces); `+` means
413
+ # the candidate carries something this push does not.
414
+ #
415
+ # Known FALSE POSITIVES (still blocks, conservatively):
416
+ # - a predecessor that was SQUASHED or amended rather than replayed -- the
417
+ # patch ids differ, so it reads as a rival. Verified: `git merge --squash`
418
+ # of the predecessor produces `+` on both of its commits.
419
+ # - a predecessor whose objects are not in this clone (fresh machine).
420
+ # - a candidate carrying a merge commit.
421
+ # - a predecessor left pointing at a commit already reachable from this push
422
+ # (someone reset it back onto `dev`). It carries nothing of its own, so
423
+ # step 3 refuses it -- correctly, because that branch is indistinguishable
424
+ # from a rival's fresh reservation.
425
+ #
426
+ # Known FALSE NEGATIVES (discounts something that was not ours):
427
+ # - a genuine rival that has committed real work, EVERY commit of which has
428
+ # a patch-id equivalent already in the pushed branch (a cherry-pick of
429
+ # exactly this work and nothing more), AND whose objects happen to be in
430
+ # this clone. Their work is then already fully in hand, so there is no
431
+ # duplicated effort left to warn about -- the discount is right for the
432
+ # wrong reason. This is the residual gap. Step 3 narrows it to rivals who
433
+ # have actually duplicated this push's content, rather than leaving it open
434
+ # to anyone who merely staked a branch, but it cannot close it: see the
435
+ # future/past limit at the top of this comment.
436
+ # - the reverse of (4) cannot happen: a merge is refused, never absorbed.
437
+ branch_is_absorbed() {
438
+ _abs_cand="$1"
439
+ _abs_tip="$2"
440
+
441
+ [ -n "$_abs_cand" ] || return 1
442
+ [ -n "$_abs_tip" ] || return 1
443
+
444
+ git cat-file -e "${_abs_cand}^{commit}" 2>/dev/null || return 1
445
+
446
+ # Step 3 (see above): zero own commits is a RESERVATION, never a
447
+ # supersession. Counted against the branch being pushed, which is the same
448
+ # base the two checks below use. A count that cannot be computed, or that
449
+ # comes back non-numeric, is a cannot-tell and fails closed like everything
450
+ # else here -- `[` itself returns non-zero on a non-integer operand, so the
451
+ # `|| return 1` covers that without a second parse.
452
+ _abs_own=$(git rev-list --count "$_abs_cand" --not "$_abs_tip" 2>/dev/null) || return 1
453
+ [ -n "$_abs_own" ] || return 1
454
+ [ "$_abs_own" -gt 0 ] 2>/dev/null || return 1
455
+
456
+ _abs_merges=$(git rev-list --merges --count "$_abs_cand" --not "$_abs_tip" 2>/dev/null) || return 1
457
+ [ "$_abs_merges" = "0" ] || return 1
458
+
459
+ _abs_cherry=$(git cherry "$_abs_tip" "$_abs_cand" 2>/dev/null) || return 1
460
+ if printf '%s\n' "$_abs_cherry" | grep -q '^+'; then
461
+ return 1
462
+ fi
463
+ return 0
464
+ }
465
+
311
466
  # --- the structural claim predicate (#1411, class #1362 instance 8) ---------
312
467
  #
313
468
  # "Does this open PR claim issue $1?" used to be answered independently at
@@ -467,6 +622,12 @@ if [ -n "$GUARD_BRANCH" ]; then
467
622
  conflict=0
468
623
  findings=""
469
624
  cannot_tell_reasons=""
625
+ absorbed_branches=""
626
+
627
+ # The local tip of the branch being pushed, resolved once. Empty when it
628
+ # cannot be resolved (the caller named a branch this repo does not have), and
629
+ # `branch_is_absorbed` then discounts nothing -- exactly today's behaviour.
630
+ guard_tip=$(git rev-parse --verify --quiet "refs/heads/$GUARD_BRANCH^{commit}" 2>/dev/null) || guard_tip=""
470
631
 
471
632
  # --- an open PR referencing the issue, excluding our own branch's PR --------
472
633
  slug=$(repo_slug)
@@ -502,22 +663,49 @@ if [ -n "$GUARD_BRANCH" ]; then
502
663
  # derived (#1672), and compare the two normalised numbers -- not a
503
664
  # substring search for $guard_issue inside the candidate's raw text. See
504
665
  # `derive_branch_issue` above for why that asymmetry was the defect.
505
- other_branch=$(printf '%s\n' "$raw_branches" |
506
- sed 's|.*refs/heads/||' |
507
- grep -v -x "$GUARD_BRANCH" |
508
- while IFS= read -r _cand; do
666
+ # Each candidate that names the same issue is then classified by LINEAGE
667
+ # (see `branch_is_absorbed`): `rival` is a conflict, `absorbed` is this
668
+ # pusher's own superseded predecessor and is reported but not blocked.
669
+ # The candidate's SHA -- `git ls-remote`'s first, tab-separated field -- is
670
+ # what makes that question answerable, so it is no longer thrown away by a
671
+ # `sed` that kept only the name.
672
+ _classified=$(printf '%s\n' "$raw_branches" |
673
+ while IFS= read -r _line; do
674
+ case "$_line" in
675
+ *"refs/heads/"*) ;;
676
+ *) continue ;;
677
+ esac
678
+ _cand=${_line##*refs/heads/}
509
679
  [ -n "$_cand" ] || continue
680
+ [ "$_cand" = "$GUARD_BRANCH" ] && continue
510
681
  _cand_issue=$(derive_branch_issue "$_cand")
511
- if [ -n "$_cand_issue" ] && [ "$_cand_issue" = "$guard_issue" ]; then
512
- printf '%s\n' "$_cand"
682
+ [ -n "$_cand_issue" ] || continue
683
+ [ "$_cand_issue" = "$guard_issue" ] || continue
684
+ _cand_sha=$(printf '%s\n' "$_line" | cut -f1)
685
+ if branch_is_absorbed "$_cand_sha" "$guard_tip"; then
686
+ printf 'absorbed %s\n' "$_cand"
687
+ else
688
+ printf 'rival %s\n' "$_cand"
513
689
  fi
514
- done | head -1)
690
+ done)
691
+
692
+ other_branch=$(printf '%s\n' "$_classified" | sed -n 's/^rival //p' | head -1)
693
+ absorbed_branches=$(printf '%s\n' "$_classified" | sed -n 's/^absorbed //p')
515
694
  if [ -n "$other_branch" ]; then
516
695
  conflict=1
517
696
  findings="${findings} ${RED}branch${OFF} $other_branch\n"
518
697
  fi
519
698
  fi
520
699
 
700
+ # A discount a guard grants silently is a guard nobody can audit, so say so
701
+ # -- on stderr, only when it actually fired, and whether or not a real rival
702
+ # was also found.
703
+ if [ -n "$absorbed_branches" ]; then
704
+ printf '%b' "${DIM}claim --guard: discounted $(printf '%s' "$absorbed_branches" | tr '\n' ' ') ${OFF}" >&2
705
+ echo "${DIM}-- every commit on it is already carried by $GUARD_BRANCH, so it is a${OFF}" >&2
706
+ echo "${DIM}superseded predecessor rather than a rival claim (#1112).${OFF}" >&2
707
+ fi
708
+
521
709
  if [ "$conflict" -eq 1 ]; then
522
710
  printf '%b' "${RED}claim --guard: issue #$guard_issue looks claimed by someone else.${OFF}\n$findings"
523
711
  echo