@biffo/cli 0.204.4 → 0.205.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.
@@ -21,6 +21,22 @@ fi
21
21
 
22
22
  root=$(git rev-parse --show-toplevel) || exit 0
23
23
  cd "$root" || exit 0
24
+
25
+ # The rewrite-scope guard runs FIRST, and it is the only thing here that needs
26
+ # the ref list git puts on stdin (<local ref> <local sha> <remote ref>
27
+ # <remote sha>). verify.sh reads no stdin, so consuming it here costs nothing —
28
+ # but it does mean this must run before the `exec` below, which replaces the
29
+ # process entirely.
30
+ #
31
+ # Why it exists: a force-push can quietly add a file to a branch's change set
32
+ # and record a stale copy of somebody else's merged work as your own change.
33
+ # That happened (tabsii-platform#446) and every local gate was green on it.
34
+ if [ -f scripts/rewrite-scope-check.sh ]; then
35
+ sh scripts/rewrite-scope-check.sh || exit 1
36
+ else
37
+ echo "pre-push: no scripts/rewrite-scope-check.sh — rewrite scope NOT checked" >&2
38
+ fi
39
+
24
40
  if [ ! -f scripts/verify.sh ]; then
25
41
  echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
26
42
  exit 0
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env sh
2
+ # Refuse a force-push that has quietly started touching a file somebody else
3
+ # just changed.
4
+ #
5
+ # ## The incident (tabsii-platform#446, 2026-07-31)
6
+ #
7
+ # A branch had to be rebuilt to purge a Secret Scan finding from its history.
8
+ # The rebuild used:
9
+ #
10
+ # git reset --soft origin/dev && git add -A
11
+ #
12
+ # `origin/dev` had advanced by two merged PRs since the branch was cut. So
13
+ # `reset --soft` moved HEAD onto the NEWER dev while the working tree still
14
+ # held the OLDER copy of a doc those PRs had edited — and `git add -A`
15
+ # faithfully staged the difference. The resulting commit, whose subject was
16
+ # about RLS policies, silently REVERTED four lines of somebody else's merged
17
+ # work.
18
+ #
19
+ # Nothing local saw it: `verify.sh` passed, 2884 tests passed, the real-Postgres
20
+ # lane passed twice. It surfaced only because GitHub reported the PR as
21
+ # CONFLICTING and somebody went looking for why. **Had those two PRs touched any
22
+ # other file, there would have been no conflict and the revert would have
23
+ # merged.** A conflict is not a detector; it is the lucky case.
24
+ #
25
+ # ## What this checks, and why it is narrow
26
+ #
27
+ # It fires only on the intersection of two conditions, because either alone is
28
+ # far too noisy to be a gate:
29
+ #
30
+ # 1. The push is a REWRITE — the remote tip is not an ancestor of what you are
31
+ # pushing. An ordinary fast-forward push cannot have this problem.
32
+ # 2. A file the branch changes would be written back to content the
33
+ # integration branch ALREADY HAD earlier in its history. Not new work: an
34
+ # older copy.
35
+ #
36
+ # Condition 2 is the accidental-revert signature exactly, and it is the second
37
+ # attempt. The first asked "has the integration branch moved this file since my
38
+ # base?", which sounds equivalent and is not — `git reset --soft origin/dev`
39
+ # makes your base the integration tip, so the answer is always no. **That
40
+ # version scored a clean pass on a faithful reconstruction of the incident
41
+ # above.** `cli/src/lib/rewrite-scope-check.test.ts` reinstates it to prove the
42
+ # tests can tell the two apart.
43
+ #
44
+ # What stays silent, deliberately: amending to add a file nobody else has
45
+ # touched; rebasing onto a newer integration branch; two branches making
46
+ # genuinely different edits to one shared file. Only a STALE copy is a bug.
47
+ #
48
+ # ## What it deliberately does NOT do
49
+ #
50
+ # It does not try to decide whether a change is "intended". That needs intent,
51
+ # which no gate has. It answers a narrower question answerable from history
52
+ # alone: *is the content I am about to push actually older content?*
53
+ #
54
+ # It also never fails the push because it could not run. A guard that blocks
55
+ # work when its own inputs are missing gets disabled, and then it protects
56
+ # nothing. When it cannot answer it says so, loudly, and exits 0 — the same
57
+ # contract `verify.sh` uses for a check whose tool is absent. Absence is
58
+ # reported, never mistaken for a clean result.
59
+
60
+ set -u
61
+
62
+ ZERO=0000000000000000000000000000000000000000
63
+ ESCAPE_HATCH=BIFFO_ALLOW_SCOPE_CHANGE
64
+
65
+ note() { printf 'rewrite-scope: %s\n' "$1" >&2; }
66
+
67
+ # The integration branch every Biffo repo uses (AGENTS.md §2). Resolved rather
68
+ # than assumed: a repo whose remote HEAD says otherwise is a repo this check
69
+ # should not guess about.
70
+ integration=""
71
+ for candidate in dev main master; do
72
+ if git rev-parse --verify --quiet "refs/remotes/origin/$candidate" >/dev/null 2>&1; then
73
+ integration="origin/$candidate"
74
+ break
75
+ fi
76
+ done
77
+
78
+ if [ -z "$integration" ]; then
79
+ note "no origin/dev, origin/main or origin/master — check did NOT run"
80
+ exit 0
81
+ fi
82
+
83
+ # The inner filter below runs in a subshell (it is on the right of a pipe), so
84
+ # it cannot assign to a variable in this shell. Collect through a file instead.
85
+ FINDINGS_FILE=$(mktemp) || {
86
+ note "could not create a temp file — check did NOT run"
87
+ exit 0
88
+ }
89
+ trap 'rm -f "$FINDINGS_FILE"' EXIT HUP INT TERM
90
+
91
+ # git feeds pre-push one line per ref: <local ref> <local sha> <remote ref> <remote sha>
92
+ while read -r _local_ref local_sha _remote_ref remote_sha; do
93
+ # Branch deletion, or nothing to push.
94
+ [ "$local_sha" = "$ZERO" ] && continue
95
+ # Brand-new remote branch: there is no previous scope to compare against.
96
+ [ "$remote_sha" = "$ZERO" ] && continue
97
+ # The remote may not have the old object locally (shallow clone, pruned).
98
+ git cat-file -e "$remote_sha^{commit}" 2>/dev/null || {
99
+ note "remote tip $remote_sha not available locally — check did NOT run for this ref"
100
+ continue
101
+ }
102
+ # Fast-forward: not a rewrite, cannot exhibit this defect.
103
+ if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
104
+ continue
105
+ fi
106
+
107
+ new_base=$(git merge-base "$local_sha" "$integration" 2>/dev/null) || continue
108
+ old_base=$(git merge-base "$remote_sha" "$integration" 2>/dev/null) || continue
109
+
110
+ # Every file this branch claims to change, relative to where it forked.
111
+ #
112
+ # Iterated with `while read`, not `for file in $scope`: word splitting would
113
+ # break any path containing a space, and a guard that silently skips the file
114
+ # it should have flagged is worse than no guard. Process substitution
115
+ # (`grep -f <(...)`) is avoided for the same class of reason — it is a
116
+ # bashism, and this runs under `sh` from the pre-push hook.
117
+ git diff --name-only "$new_base" "$local_sha" 2>/dev/null | while IFS= read -r file; do
118
+ [ -z "$file" ] && continue
119
+
120
+ mine=$(git rev-parse --quiet --verify "$local_sha:$file" 2>/dev/null) || continue
121
+ theirs=$(git rev-parse --quiet --verify "$integration:$file" 2>/dev/null) || continue
122
+ # Agreeing with the integration branch is the normal, safe case.
123
+ [ "$mine" = "$theirs" ] && continue
124
+
125
+ # The signature of an accidental revert: the content you are about to push
126
+ # is not new work, it is a version this file ALREADY HAD earlier in the
127
+ # integration branch's history.
128
+ #
129
+ # This is what makes the check work at all. The first version asked "has
130
+ # the integration branch moved this file since my base?", which sounds
131
+ # equivalent and is not: `git reset --soft origin/dev` makes your base the
132
+ # integration tip, so the answer is always no and the guard never fires.
133
+ # Verified against a reconstruction of tabsii-platform#446 — the earlier
134
+ # condition scored a clean pass on the exact commit that reverted two
135
+ # merged PRs.
136
+ #
137
+ # Bounded to recent history: an unbounded walk on a long-lived file costs
138
+ # more than a pre-push gate should, and a stale copy older than this is not
139
+ # the accident this catches.
140
+ git log --format=%H -n 200 "$integration" -- "$file" 2>/dev/null | while IFS= read -r commit; do
141
+ [ -z "$commit" ] && continue
142
+ past=$(git rev-parse --quiet --verify "$commit:$file" 2>/dev/null) || continue
143
+ if [ "$past" = "$mine" ]; then
144
+ printf '%s\n' "$file"
145
+ break
146
+ fi
147
+ done
148
+ done >>"$FINDINGS_FILE"
149
+ done
150
+
151
+ findings=$(cat "$FINDINGS_FILE" 2>/dev/null)
152
+
153
+ [ -z "$findings" ] && exit 0
154
+
155
+ if [ -n "${BIFFO_ALLOW_SCOPE_CHANGE:-}" ]; then
156
+ note "scope change allowed via $ESCAPE_HATCH:"
157
+ printf '%s' "$findings" | sed 's/^/ /' >&2
158
+ exit 0
159
+ fi
160
+
161
+ cat >&2 <<EOF
162
+
163
+ This force-push would write a version of these file(s) that $integration
164
+ ALREADY HAD earlier in its history — not new work, an older copy:
165
+
166
+ $(printf '%s' "$findings" | sed 's/^/ /')
167
+
168
+ That is the signature of an accidental revert. A rewrite (rebase, amend,
169
+ reset --soft) picked up a stale copy of somebody else's merged work and
170
+ recorded it as your change. It merges silently unless it happens to
171
+ conflict, and no test can see it — the content is valid, just old.
172
+
173
+ Check what you are actually about to write:
174
+
175
+ git diff $integration...HEAD -- <file>
176
+
177
+ If it is intended, push again with:
178
+
179
+ $ESCAPE_HATCH=1 git push ...
180
+
181
+ If it is not: restore the file and amend —
182
+
183
+ git checkout $integration -- <file> && git commit --amend --no-edit
184
+
185
+ Rebuilding a branch? Reset to a FIXED SHA (the branch point), never to a
186
+ moving ref like $integration.
187
+
188
+ EOF
189
+ exit 1
@@ -518,7 +518,17 @@ if ci_has "gitleaks"; then
518
518
  if [ -n "$LIST" ] || command -v gitleaks >/dev/null 2>&1; then
519
519
  run_check gitleaks gitleaks detect --no-git --redact --exit-code=2
520
520
  else
521
- skip gitleaks "gitleaks not installed - CI still runs both passes"
521
+ # Say how to close it, pinned to the version ci.yml installs. A skip that
522
+ # only reports its own absence stays skipped: this one sat `n/a` long
523
+ # enough for the `\b\d{12}\b` account-id rule to reach CI three times,
524
+ # most recently on two test UUIDs whose last segment happened to be
525
+ # twelve digits (tabsii-platform#446). Thirty seconds of install would
526
+ # have caught it before the push, and version parity matters — an older
527
+ # gitleaks disagreeing with CI reintroduces exactly the local/CI
528
+ # divergence this gate exists to remove.
529
+ skip gitleaks "not installed - CI still runs both passes. Install the version ci.yml pins:
530
+ curl -sSfL -o /tmp/gl.tgz https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \\
531
+ && tar -xzf /tmp/gl.tgz -C \"\$HOME/.local/bin\" gitleaks"
522
532
  fi
523
533
  fi
524
534
 
@@ -21,6 +21,22 @@ fi
21
21
 
22
22
  root=$(git rev-parse --show-toplevel) || exit 0
23
23
  cd "$root" || exit 0
24
+
25
+ # The rewrite-scope guard runs FIRST, and it is the only thing here that needs
26
+ # the ref list git puts on stdin (<local ref> <local sha> <remote ref>
27
+ # <remote sha>). verify.sh reads no stdin, so consuming it here costs nothing —
28
+ # but it does mean this must run before the `exec` below, which replaces the
29
+ # process entirely.
30
+ #
31
+ # Why it exists: a force-push can quietly add a file to a branch's change set
32
+ # and record a stale copy of somebody else's merged work as your own change.
33
+ # That happened (tabsii-platform#446) and every local gate was green on it.
34
+ if [ -f scripts/rewrite-scope-check.sh ]; then
35
+ sh scripts/rewrite-scope-check.sh || exit 1
36
+ else
37
+ echo "pre-push: no scripts/rewrite-scope-check.sh — rewrite scope NOT checked" >&2
38
+ fi
39
+
24
40
  if [ ! -f scripts/verify.sh ]; then
25
41
  echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
26
42
  exit 0
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env sh
2
+ # Refuse a force-push that has quietly started touching a file somebody else
3
+ # just changed.
4
+ #
5
+ # ## The incident (tabsii-platform#446, 2026-07-31)
6
+ #
7
+ # A branch had to be rebuilt to purge a Secret Scan finding from its history.
8
+ # The rebuild used:
9
+ #
10
+ # git reset --soft origin/dev && git add -A
11
+ #
12
+ # `origin/dev` had advanced by two merged PRs since the branch was cut. So
13
+ # `reset --soft` moved HEAD onto the NEWER dev while the working tree still
14
+ # held the OLDER copy of a doc those PRs had edited — and `git add -A`
15
+ # faithfully staged the difference. The resulting commit, whose subject was
16
+ # about RLS policies, silently REVERTED four lines of somebody else's merged
17
+ # work.
18
+ #
19
+ # Nothing local saw it: `verify.sh` passed, 2884 tests passed, the real-Postgres
20
+ # lane passed twice. It surfaced only because GitHub reported the PR as
21
+ # CONFLICTING and somebody went looking for why. **Had those two PRs touched any
22
+ # other file, there would have been no conflict and the revert would have
23
+ # merged.** A conflict is not a detector; it is the lucky case.
24
+ #
25
+ # ## What this checks, and why it is narrow
26
+ #
27
+ # It fires only on the intersection of two conditions, because either alone is
28
+ # far too noisy to be a gate:
29
+ #
30
+ # 1. The push is a REWRITE — the remote tip is not an ancestor of what you are
31
+ # pushing. An ordinary fast-forward push cannot have this problem.
32
+ # 2. A file the branch changes would be written back to content the
33
+ # integration branch ALREADY HAD earlier in its history. Not new work: an
34
+ # older copy.
35
+ #
36
+ # Condition 2 is the accidental-revert signature exactly, and it is the second
37
+ # attempt. The first asked "has the integration branch moved this file since my
38
+ # base?", which sounds equivalent and is not — `git reset --soft origin/dev`
39
+ # makes your base the integration tip, so the answer is always no. **That
40
+ # version scored a clean pass on a faithful reconstruction of the incident
41
+ # above.** `cli/src/lib/rewrite-scope-check.test.ts` reinstates it to prove the
42
+ # tests can tell the two apart.
43
+ #
44
+ # What stays silent, deliberately: amending to add a file nobody else has
45
+ # touched; rebasing onto a newer integration branch; two branches making
46
+ # genuinely different edits to one shared file. Only a STALE copy is a bug.
47
+ #
48
+ # ## What it deliberately does NOT do
49
+ #
50
+ # It does not try to decide whether a change is "intended". That needs intent,
51
+ # which no gate has. It answers a narrower question answerable from history
52
+ # alone: *is the content I am about to push actually older content?*
53
+ #
54
+ # It also never fails the push because it could not run. A guard that blocks
55
+ # work when its own inputs are missing gets disabled, and then it protects
56
+ # nothing. When it cannot answer it says so, loudly, and exits 0 — the same
57
+ # contract `verify.sh` uses for a check whose tool is absent. Absence is
58
+ # reported, never mistaken for a clean result.
59
+
60
+ set -u
61
+
62
+ ZERO=0000000000000000000000000000000000000000
63
+ ESCAPE_HATCH=BIFFO_ALLOW_SCOPE_CHANGE
64
+
65
+ note() { printf 'rewrite-scope: %s\n' "$1" >&2; }
66
+
67
+ # The integration branch every Biffo repo uses (AGENTS.md §2). Resolved rather
68
+ # than assumed: a repo whose remote HEAD says otherwise is a repo this check
69
+ # should not guess about.
70
+ integration=""
71
+ for candidate in dev main master; do
72
+ if git rev-parse --verify --quiet "refs/remotes/origin/$candidate" >/dev/null 2>&1; then
73
+ integration="origin/$candidate"
74
+ break
75
+ fi
76
+ done
77
+
78
+ if [ -z "$integration" ]; then
79
+ note "no origin/dev, origin/main or origin/master — check did NOT run"
80
+ exit 0
81
+ fi
82
+
83
+ # The inner filter below runs in a subshell (it is on the right of a pipe), so
84
+ # it cannot assign to a variable in this shell. Collect through a file instead.
85
+ FINDINGS_FILE=$(mktemp) || {
86
+ note "could not create a temp file — check did NOT run"
87
+ exit 0
88
+ }
89
+ trap 'rm -f "$FINDINGS_FILE"' EXIT HUP INT TERM
90
+
91
+ # git feeds pre-push one line per ref: <local ref> <local sha> <remote ref> <remote sha>
92
+ while read -r _local_ref local_sha _remote_ref remote_sha; do
93
+ # Branch deletion, or nothing to push.
94
+ [ "$local_sha" = "$ZERO" ] && continue
95
+ # Brand-new remote branch: there is no previous scope to compare against.
96
+ [ "$remote_sha" = "$ZERO" ] && continue
97
+ # The remote may not have the old object locally (shallow clone, pruned).
98
+ git cat-file -e "$remote_sha^{commit}" 2>/dev/null || {
99
+ note "remote tip $remote_sha not available locally — check did NOT run for this ref"
100
+ continue
101
+ }
102
+ # Fast-forward: not a rewrite, cannot exhibit this defect.
103
+ if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
104
+ continue
105
+ fi
106
+
107
+ new_base=$(git merge-base "$local_sha" "$integration" 2>/dev/null) || continue
108
+ old_base=$(git merge-base "$remote_sha" "$integration" 2>/dev/null) || continue
109
+
110
+ # Every file this branch claims to change, relative to where it forked.
111
+ #
112
+ # Iterated with `while read`, not `for file in $scope`: word splitting would
113
+ # break any path containing a space, and a guard that silently skips the file
114
+ # it should have flagged is worse than no guard. Process substitution
115
+ # (`grep -f <(...)`) is avoided for the same class of reason — it is a
116
+ # bashism, and this runs under `sh` from the pre-push hook.
117
+ git diff --name-only "$new_base" "$local_sha" 2>/dev/null | while IFS= read -r file; do
118
+ [ -z "$file" ] && continue
119
+
120
+ mine=$(git rev-parse --quiet --verify "$local_sha:$file" 2>/dev/null) || continue
121
+ theirs=$(git rev-parse --quiet --verify "$integration:$file" 2>/dev/null) || continue
122
+ # Agreeing with the integration branch is the normal, safe case.
123
+ [ "$mine" = "$theirs" ] && continue
124
+
125
+ # The signature of an accidental revert: the content you are about to push
126
+ # is not new work, it is a version this file ALREADY HAD earlier in the
127
+ # integration branch's history.
128
+ #
129
+ # This is what makes the check work at all. The first version asked "has
130
+ # the integration branch moved this file since my base?", which sounds
131
+ # equivalent and is not: `git reset --soft origin/dev` makes your base the
132
+ # integration tip, so the answer is always no and the guard never fires.
133
+ # Verified against a reconstruction of tabsii-platform#446 — the earlier
134
+ # condition scored a clean pass on the exact commit that reverted two
135
+ # merged PRs.
136
+ #
137
+ # Bounded to recent history: an unbounded walk on a long-lived file costs
138
+ # more than a pre-push gate should, and a stale copy older than this is not
139
+ # the accident this catches.
140
+ git log --format=%H -n 200 "$integration" -- "$file" 2>/dev/null | while IFS= read -r commit; do
141
+ [ -z "$commit" ] && continue
142
+ past=$(git rev-parse --quiet --verify "$commit:$file" 2>/dev/null) || continue
143
+ if [ "$past" = "$mine" ]; then
144
+ printf '%s\n' "$file"
145
+ break
146
+ fi
147
+ done
148
+ done >>"$FINDINGS_FILE"
149
+ done
150
+
151
+ findings=$(cat "$FINDINGS_FILE" 2>/dev/null)
152
+
153
+ [ -z "$findings" ] && exit 0
154
+
155
+ if [ -n "${BIFFO_ALLOW_SCOPE_CHANGE:-}" ]; then
156
+ note "scope change allowed via $ESCAPE_HATCH:"
157
+ printf '%s' "$findings" | sed 's/^/ /' >&2
158
+ exit 0
159
+ fi
160
+
161
+ cat >&2 <<EOF
162
+
163
+ This force-push would write a version of these file(s) that $integration
164
+ ALREADY HAD earlier in its history — not new work, an older copy:
165
+
166
+ $(printf '%s' "$findings" | sed 's/^/ /')
167
+
168
+ That is the signature of an accidental revert. A rewrite (rebase, amend,
169
+ reset --soft) picked up a stale copy of somebody else's merged work and
170
+ recorded it as your change. It merges silently unless it happens to
171
+ conflict, and no test can see it — the content is valid, just old.
172
+
173
+ Check what you are actually about to write:
174
+
175
+ git diff $integration...HEAD -- <file>
176
+
177
+ If it is intended, push again with:
178
+
179
+ $ESCAPE_HATCH=1 git push ...
180
+
181
+ If it is not: restore the file and amend —
182
+
183
+ git checkout $integration -- <file> && git commit --amend --no-edit
184
+
185
+ Rebuilding a branch? Reset to a FIXED SHA (the branch point), never to a
186
+ moving ref like $integration.
187
+
188
+ EOF
189
+ exit 1
@@ -518,7 +518,17 @@ if ci_has "gitleaks"; then
518
518
  if [ -n "$LIST" ] || command -v gitleaks >/dev/null 2>&1; then
519
519
  run_check gitleaks gitleaks detect --no-git --redact --exit-code=2
520
520
  else
521
- skip gitleaks "gitleaks not installed - CI still runs both passes"
521
+ # Say how to close it, pinned to the version ci.yml installs. A skip that
522
+ # only reports its own absence stays skipped: this one sat `n/a` long
523
+ # enough for the `\b\d{12}\b` account-id rule to reach CI three times,
524
+ # most recently on two test UUIDs whose last segment happened to be
525
+ # twelve digits (tabsii-platform#446). Thirty seconds of install would
526
+ # have caught it before the push, and version parity matters — an older
527
+ # gitleaks disagreeing with CI reintroduces exactly the local/CI
528
+ # divergence this gate exists to remove.
529
+ skip gitleaks "not installed - CI still runs both passes. Install the version ci.yml pins:
530
+ curl -sSfL -o /tmp/gl.tgz https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \\
531
+ && tar -xzf /tmp/gl.tgz -C \"\$HOME/.local/bin\" gitleaks"
522
532
  fi
523
533
  fi
524
534
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.204.4",
3
+ "version": "0.205.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",