@biffo/cli 0.312.3 → 0.312.5
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.
|
@@ -166,4 +166,22 @@ fi
|
|
|
166
166
|
# without the file passed its pre-push having checked nothing, which is the
|
|
167
167
|
# same defect one level up from the one #855 recorded. A gate that cannot run
|
|
168
168
|
# is not a gate that passed.
|
|
169
|
-
|
|
169
|
+
#
|
|
170
|
+
# Not a bare `exec` (biffo-template#1985): git's own generic
|
|
171
|
+
# "error: failed to push some refs" line is printed by git itself, after this
|
|
172
|
+
# hook exits, and is the literal last thing in a truncated-tail read of a long
|
|
173
|
+
# agent transcript. Nothing distinguished it from a real auth/network
|
|
174
|
+
# rejection -- three same-day dispatches on tabsii-platform#1403 diagnosed and
|
|
175
|
+
# "fixed" a token before noticing the real cause was a local gate. verify.sh's
|
|
176
|
+
# own failure text (above) already names the cause; this adds one more line,
|
|
177
|
+
# deliberately placed as close as this hook can get to git's trailing output,
|
|
178
|
+
# so a reader who only sees the last few lines still sees it.
|
|
179
|
+
sh scripts/biffo.sh verify
|
|
180
|
+
verify_status=$?
|
|
181
|
+
if [ "$verify_status" -ne 0 ]; then
|
|
182
|
+
printf '\n\033[31m>>> pre-push: BLOCKED BY THE LOCAL GATE ABOVE, not by git or GitHub.\033[0m\n' >&2
|
|
183
|
+
printf ' See "verify failed:" above for which check(s) and why. Any\n' >&2
|
|
184
|
+
printf ' "failed to push some refs" line below this is git relaying that\n' >&2
|
|
185
|
+
printf ' same exit code -- it is not a separate rejection.\n' >&2
|
|
186
|
+
fi
|
|
187
|
+
exit "$verify_status"
|
|
@@ -166,4 +166,22 @@ fi
|
|
|
166
166
|
# without the file passed its pre-push having checked nothing, which is the
|
|
167
167
|
# same defect one level up from the one #855 recorded. A gate that cannot run
|
|
168
168
|
# is not a gate that passed.
|
|
169
|
-
|
|
169
|
+
#
|
|
170
|
+
# Not a bare `exec` (biffo-template#1985): git's own generic
|
|
171
|
+
# "error: failed to push some refs" line is printed by git itself, after this
|
|
172
|
+
# hook exits, and is the literal last thing in a truncated-tail read of a long
|
|
173
|
+
# agent transcript. Nothing distinguished it from a real auth/network
|
|
174
|
+
# rejection -- three same-day dispatches on tabsii-platform#1403 diagnosed and
|
|
175
|
+
# "fixed" a token before noticing the real cause was a local gate. verify.sh's
|
|
176
|
+
# own failure text (above) already names the cause; this adds one more line,
|
|
177
|
+
# deliberately placed as close as this hook can get to git's trailing output,
|
|
178
|
+
# so a reader who only sees the last few lines still sees it.
|
|
179
|
+
sh scripts/biffo.sh verify
|
|
180
|
+
verify_status=$?
|
|
181
|
+
if [ "$verify_status" -ne 0 ]; then
|
|
182
|
+
printf '\n\033[31m>>> pre-push: BLOCKED BY THE LOCAL GATE ABOVE, not by git or GitHub.\033[0m\n' >&2
|
|
183
|
+
printf ' See "verify failed:" above for which check(s) and why. Any\n' >&2
|
|
184
|
+
printf ' "failed to push some refs" line below this is git relaying that\n' >&2
|
|
185
|
+
printf ' same exit code -- it is not a separate rejection.\n' >&2
|
|
186
|
+
fi
|
|
187
|
+
exit "$verify_status"
|
package/dist/index.js
CHANGED
|
@@ -2838,12 +2838,124 @@ function parseBodyChangeDeclaration(content) {
|
|
|
2838
2838
|
}
|
|
2839
2839
|
return { classification: value[1], reason: value[2] };
|
|
2840
2840
|
}
|
|
2841
|
+
var BODY_CHANGE_LINE_ONLY_RE = /^[ \t]*# biffo:body-change:[ \t]*(.*)$/;
|
|
2842
|
+
function pythonStatementGroups(lines, docLineMask) {
|
|
2843
|
+
const groupId = new Array(lines.length);
|
|
2844
|
+
let depth = 0;
|
|
2845
|
+
let quote = null;
|
|
2846
|
+
let currentGroup = -1;
|
|
2847
|
+
for (let li = 0; li < lines.length; li++) {
|
|
2848
|
+
if (docLineMask[li]) {
|
|
2849
|
+
groupId[li] = currentGroup;
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
if (depth === 0 && quote === null) currentGroup++;
|
|
2853
|
+
groupId[li] = currentGroup;
|
|
2854
|
+
const line = lines[li];
|
|
2855
|
+
for (let ci = 0; ci < line.length; ci++) {
|
|
2856
|
+
const ch = line[ci];
|
|
2857
|
+
if (quote) {
|
|
2858
|
+
if (ch === "\\") {
|
|
2859
|
+
ci++;
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
if (ch === quote) quote = null;
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
if (ch === '"' || ch === "'") {
|
|
2866
|
+
quote = ch;
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
if (ch === "#") break;
|
|
2870
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
2871
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth = Math.max(0, depth - 1);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
return groupId;
|
|
2875
|
+
}
|
|
2876
|
+
var MAX_DIFF_CELLS = 4e3 * 4e3;
|
|
2877
|
+
function computeMatchedNewLineIndices(oldLines, newLines) {
|
|
2878
|
+
const n = oldLines.length;
|
|
2879
|
+
const m = newLines.length;
|
|
2880
|
+
if (n * m > MAX_DIFF_CELLS) return /* @__PURE__ */ new Set();
|
|
2881
|
+
const stride = m + 1;
|
|
2882
|
+
const dp = new Uint32Array((n + 1) * stride);
|
|
2883
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
2884
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
2885
|
+
dp[i2 * stride + j2] = oldLines[i2] === newLines[j2] ? dp[(i2 + 1) * stride + (j2 + 1)] + 1 : Math.max(dp[(i2 + 1) * stride + j2], dp[i2 * stride + (j2 + 1)]);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
const matchedNew = /* @__PURE__ */ new Set();
|
|
2889
|
+
let i = 0;
|
|
2890
|
+
let j = 0;
|
|
2891
|
+
while (i < n && j < m) {
|
|
2892
|
+
if (oldLines[i] === newLines[j]) {
|
|
2893
|
+
matchedNew.add(j);
|
|
2894
|
+
i++;
|
|
2895
|
+
j++;
|
|
2896
|
+
} else if (dp[(i + 1) * stride + j] >= dp[i * stride + (j + 1)]) {
|
|
2897
|
+
i++;
|
|
2898
|
+
} else {
|
|
2899
|
+
j++;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return matchedNew;
|
|
2903
|
+
}
|
|
2904
|
+
function findEditScopedBodyChangeDeclarations(oldContent, newContent) {
|
|
2905
|
+
const oldLines = oldContent.split("\n");
|
|
2906
|
+
const newLines = newContent.split("\n");
|
|
2907
|
+
const matchedNew = computeMatchedNewLineIndices(oldLines, newLines);
|
|
2908
|
+
const newDocMask = pythonDocstringLineMask(newContent);
|
|
2909
|
+
const groupIds = pythonStatementGroups(newLines, newDocMask);
|
|
2910
|
+
const oldMarkerLines = new Set(oldLines.filter((line) => BODY_CHANGE_LINE_ONLY_RE.test(line)));
|
|
2911
|
+
const results = [];
|
|
2912
|
+
let i = 0;
|
|
2913
|
+
while (i < newLines.length) {
|
|
2914
|
+
const gid = groupIds[i];
|
|
2915
|
+
let end = i;
|
|
2916
|
+
while (end < newLines.length && groupIds[end] === gid) end++;
|
|
2917
|
+
let needsDeclaration = false;
|
|
2918
|
+
for (let k = i; k < end; k++) {
|
|
2919
|
+
if (!matchedNew.has(k) && isSubstantiveLine(newLines[k], newDocMask[k] ?? false)) {
|
|
2920
|
+
needsDeclaration = true;
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
if (needsDeclaration) {
|
|
2925
|
+
const aboveIdx = i - 1;
|
|
2926
|
+
const above = aboveIdx >= 0 ? newLines[aboveIdx] : null;
|
|
2927
|
+
const aboveMatch = above !== null ? BODY_CHANGE_LINE_ONLY_RE.exec(above) : null;
|
|
2928
|
+
if (!aboveMatch) {
|
|
2929
|
+
results.push({ declaration: null, staleOnly: false });
|
|
2930
|
+
} else if (oldMarkerLines.has(above)) {
|
|
2931
|
+
results.push({ declaration: null, staleOnly: true });
|
|
2932
|
+
} else {
|
|
2933
|
+
const rest = (aboveMatch[1] ?? "").trim();
|
|
2934
|
+
const value = BODY_CHANGE_VALUE_RE.exec(rest);
|
|
2935
|
+
if (!value) {
|
|
2936
|
+
throw new Error(
|
|
2937
|
+
`Malformed ${BODY_CHANGE_MARKER} marker: expected "${BODY_CHANGE_MARKER} replay-safe \u2014 <reason>" or "${BODY_CHANGE_MARKER} outcome-changing \u2014 <reason>", got "${rest}".`
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
results.push({
|
|
2941
|
+
declaration: {
|
|
2942
|
+
classification: value[1],
|
|
2943
|
+
reason: value[2]
|
|
2944
|
+
},
|
|
2945
|
+
staleOnly: false
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
i = end;
|
|
2950
|
+
}
|
|
2951
|
+
return results;
|
|
2952
|
+
}
|
|
2841
2953
|
var DOCSTRING_OPEN_RE = /^[rRuU]?("""|''')/;
|
|
2842
2954
|
var DEF_OR_CLASS_RE = /^(async\s+def|def|class)\b/;
|
|
2843
2955
|
var HEADER_END_RE = /:\s*(#.*)?$/;
|
|
2844
|
-
function
|
|
2956
|
+
function pythonDocstringLineMask(source) {
|
|
2845
2957
|
const lines = source.split("\n");
|
|
2846
|
-
const
|
|
2958
|
+
const mask = new Array(lines.length).fill(false);
|
|
2847
2959
|
let expectDocstring = true;
|
|
2848
2960
|
let inHeader = false;
|
|
2849
2961
|
let i = 0;
|
|
@@ -2851,7 +2963,6 @@ function stripPythonDocstrings(source) {
|
|
|
2851
2963
|
const line = lines[i];
|
|
2852
2964
|
const trimmed = line.trim();
|
|
2853
2965
|
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
2854
|
-
out.push(line);
|
|
2855
2966
|
i++;
|
|
2856
2967
|
continue;
|
|
2857
2968
|
}
|
|
@@ -2859,13 +2970,20 @@ function stripPythonDocstrings(source) {
|
|
|
2859
2970
|
if (open) {
|
|
2860
2971
|
const delim = open[1];
|
|
2861
2972
|
expectDocstring = false;
|
|
2973
|
+
mask[i] = true;
|
|
2862
2974
|
if (trimmed.slice(open[0].length).includes(delim)) {
|
|
2863
2975
|
i++;
|
|
2864
2976
|
continue;
|
|
2865
2977
|
}
|
|
2866
2978
|
i++;
|
|
2867
|
-
while (i < lines.length && !lines[i].includes(delim))
|
|
2868
|
-
|
|
2979
|
+
while (i < lines.length && !lines[i].includes(delim)) {
|
|
2980
|
+
mask[i] = true;
|
|
2981
|
+
i++;
|
|
2982
|
+
}
|
|
2983
|
+
if (i < lines.length) {
|
|
2984
|
+
mask[i] = true;
|
|
2985
|
+
i++;
|
|
2986
|
+
}
|
|
2869
2987
|
continue;
|
|
2870
2988
|
}
|
|
2871
2989
|
if (inHeader || DEF_OR_CLASS_RE.test(trimmed)) {
|
|
@@ -2875,17 +2993,25 @@ function stripPythonDocstrings(source) {
|
|
|
2875
2993
|
} else {
|
|
2876
2994
|
expectDocstring = false;
|
|
2877
2995
|
}
|
|
2878
|
-
out.push(line);
|
|
2879
2996
|
i++;
|
|
2880
2997
|
}
|
|
2881
|
-
return
|
|
2998
|
+
return mask;
|
|
2882
2999
|
}
|
|
2883
3000
|
function migrationBodyHash(content) {
|
|
2884
|
-
const
|
|
2885
|
-
|
|
2886
|
-
).join("\n");
|
|
3001
|
+
const lines = content.split("\n");
|
|
3002
|
+
const docMask = pythonDocstringLineMask(content);
|
|
3003
|
+
const normalised = lines.filter((line, i) => isSubstantiveLine(line, docMask[i] ?? false)).map((line) => line.trimEnd()).join("\n");
|
|
2887
3004
|
return createHash("sha256").update(normalised).digest("hex");
|
|
2888
3005
|
}
|
|
3006
|
+
function isSubstantiveLine(rawLine, inDocstring) {
|
|
3007
|
+
if (inDocstring) return false;
|
|
3008
|
+
const line = rawLine.trimEnd();
|
|
3009
|
+
if (line === "") return false;
|
|
3010
|
+
if (line.trimStart().startsWith("#")) return false;
|
|
3011
|
+
if (REVISION_RE.test(line)) return false;
|
|
3012
|
+
if (DOWN_REVISION_RE.test(line)) return false;
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
2889
3015
|
function migrationSlug(file) {
|
|
2890
3016
|
return file.replace(/\.py$/, "").replace(/^[0-9a-f]+_/i, "");
|
|
2891
3017
|
}
|
|
@@ -13457,6 +13583,7 @@ function checkMigrationBodyChangeMarkers(diffs) {
|
|
|
13457
13583
|
const unchanged = [];
|
|
13458
13584
|
const declared = [];
|
|
13459
13585
|
const violations = [];
|
|
13586
|
+
let examinedChanged = 0;
|
|
13460
13587
|
for (const d of diffs) {
|
|
13461
13588
|
if (d.status === "added") {
|
|
13462
13589
|
exemptAdded.push(d.file);
|
|
@@ -13471,24 +13598,39 @@ function checkMigrationBodyChangeMarkers(diffs) {
|
|
|
13471
13598
|
unchanged.push(d.file);
|
|
13472
13599
|
continue;
|
|
13473
13600
|
}
|
|
13474
|
-
|
|
13601
|
+
examinedChanged++;
|
|
13602
|
+
let results;
|
|
13475
13603
|
try {
|
|
13476
|
-
|
|
13604
|
+
results = findEditScopedBodyChangeDeclarations(d.oldContent ?? "", d.newContent ?? "");
|
|
13477
13605
|
} catch (err) {
|
|
13478
13606
|
violations.push({ file: d.file, reason: err.message });
|
|
13479
13607
|
continue;
|
|
13480
13608
|
}
|
|
13481
|
-
if (
|
|
13482
|
-
declared.push({ file: d.file, classification: decl.classification });
|
|
13483
|
-
} else {
|
|
13609
|
+
if (results.length === 0) {
|
|
13484
13610
|
violations.push({
|
|
13485
13611
|
file: d.file,
|
|
13486
|
-
reason:
|
|
13612
|
+
reason: "this edit changes the migration's hashed body, but no specific statement could be isolated to require a declaration from \u2014 reported as undeclared rather than silently passed."
|
|
13487
13613
|
});
|
|
13614
|
+
continue;
|
|
13615
|
+
}
|
|
13616
|
+
for (const result of results) {
|
|
13617
|
+
if (result.declaration) {
|
|
13618
|
+
declared.push({ file: d.file, classification: result.declaration.classification });
|
|
13619
|
+
} else if (result.staleOnly) {
|
|
13620
|
+
violations.push({
|
|
13621
|
+
file: d.file,
|
|
13622
|
+
reason: `this edit changes the migration's hashed body (DDL, not just a docstring or comment), and the only \`${BODY_CHANGE_MARKER}\` marker directly above it already existed, unchanged, before this edit \u2014 a stale declaration left over from a different change does not cover this one.`
|
|
13623
|
+
});
|
|
13624
|
+
} else {
|
|
13625
|
+
violations.push({
|
|
13626
|
+
file: d.file,
|
|
13627
|
+
reason: `this edit changes the migration's hashed body (DDL, not just a docstring or comment) with no \`${BODY_CHANGE_MARKER}\` declaration directly above it.`
|
|
13628
|
+
});
|
|
13629
|
+
}
|
|
13488
13630
|
}
|
|
13489
13631
|
}
|
|
13490
13632
|
return {
|
|
13491
|
-
examined: unchanged.length +
|
|
13633
|
+
examined: unchanged.length + examinedChanged,
|
|
13492
13634
|
exemptAdded,
|
|
13493
13635
|
unchanged,
|
|
13494
13636
|
declared,
|
|
@@ -13612,6 +13754,10 @@ ${BOLD2}What to do${OFF2}
|
|
|
13612
13754
|
converges it. Neither label is enforced yet (#751 is reporting-only, pending
|
|
13613
13755
|
more examples) \u2014 but recording it now is what lets that decision be made
|
|
13614
13756
|
later instead of never.
|
|
13757
|
+
|
|
13758
|
+
A marker left over from an earlier, already-merged edit to this same file
|
|
13759
|
+
does ${DIM2}not${OFF2} cover a new one \u2014 add a fresh marker beside THIS change,
|
|
13760
|
+
even if the classification happens to match (#751 precondition D).
|
|
13615
13761
|
`);
|
|
13616
13762
|
process.exit(1);
|
|
13617
13763
|
}
|
package/package.json
CHANGED
package/scripts/verify.sh
CHANGED
|
@@ -1273,6 +1273,10 @@ elif [ -z "$PG_TEST_DSN" ]; then
|
|
|
1273
1273
|
printf ' the pg-test lane above did not run. CI treats it as a required check, so a\n'
|
|
1274
1274
|
printf ' push here would report green without having checked the thing most likely\n'
|
|
1275
1275
|
printf ' to need it.\n\n'
|
|
1276
|
+
printf ' This is NOT a credentials or network problem. It is a local gate refusing\n'
|
|
1277
|
+
printf ' to push until the Postgres test lane runs. If git prints its own generic\n'
|
|
1278
|
+
printf ' "failed to push some refs" line below this, that is git relaying THIS exit\n'
|
|
1279
|
+
printf ' code -- not a rejection by GitHub or a sign of a bad token.\n\n'
|
|
1276
1280
|
printf ' Get a DSN and re-run:\n'
|
|
1277
1281
|
printf ' eval "$(sh scripts/pg-test-db.sh --export)" # if this repo ships one\n'
|
|
1278
1282
|
printf ' export BIFFO_TEST_PG_DSN=postgresql+asyncpg://user:pass@host:port/db\n\n'
|