@biffo/cli 0.301.7 → 0.301.8
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.
|
@@ -252,6 +252,41 @@ export const DEPLOY_ONLY_PREFIXES = [
|
|
|
252
252
|
/** An issue reference GitHub linkifies: `#12` or `owner/repo#12`. */
|
|
253
253
|
const REFERENCE = '(?:[\\w.-]+/[\\w.-]+)?#\\d+'
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The whitespace allowed between a closing keyword (plus its optional `:`)
|
|
257
|
+
* and the issue reference it governs, in `closingReferences`,
|
|
258
|
+
* `deliberateClosingReferences` and `negatedClosingReferences` alike — all
|
|
259
|
+
* three end their pattern with the identical `\b:?\s+(REFERENCE)` tail, and
|
|
260
|
+
* all three had the identical bug.
|
|
261
|
+
*
|
|
262
|
+
* A bare `\s+` matches across an ARBITRARY run of blank lines, so a
|
|
263
|
+
* markdown heading ending in a closing keyword matched against a `#N` that
|
|
264
|
+
* starts a completely unrelated paragraph two lines later — nothing in the
|
|
265
|
+
* character class stopped it from spanning the blank line between them.
|
|
266
|
+
* Real instance, PR #1789's own body:
|
|
267
|
+
*
|
|
268
|
+
* ## What this fixes
|
|
269
|
+
*
|
|
270
|
+
* #1717 fixed one of two acceptance routes ...
|
|
271
|
+
*
|
|
272
|
+
* "fixes" is the last word of the heading; "#1717" is the first token of
|
|
273
|
+
* the next paragraph's own, unrelated sentence. `closingReferences`
|
|
274
|
+
* nonetheless reported `#1717` as a hit — `\s+` matched the `"\n\n"`
|
|
275
|
+
* between them — and that false hit reached the `deploy-only-path` branch
|
|
276
|
+
* of `assess`, which (unlike the two ground-truth branches) has no
|
|
277
|
+
* reconciliation step and fails on whatever `closingReferences` reports
|
|
278
|
+
* verbatim. Confirmed against GitHub's own ground truth for that PR
|
|
279
|
+
* (`gh pr view 1789 --json closingIssuesReferences`): only `#1718` — the
|
|
280
|
+
* genuine `Closes #1718` trailer — is ever returned; #1717 never was.
|
|
281
|
+
*
|
|
282
|
+
* A single line break is still allowed: nothing in this repo's corpus
|
|
283
|
+
* needs it, but there is no evidence GitHub REQUIRES same-line adjacency
|
|
284
|
+
* either, and the conservative/fail-closed choice for a guard whose job is
|
|
285
|
+
* "don't miss a real close" is to keep allowing one newline and exclude
|
|
286
|
+
* only a genuine blank line (two or more).
|
|
287
|
+
*/
|
|
288
|
+
const KEYWORD_REFERENCE_GAP = '(?:[ \\t]+|[ \\t]*\\n[ \\t]*)'
|
|
289
|
+
|
|
255
290
|
/**
|
|
256
291
|
* Blank out fenced code blocks and inline code spans, preserving line count.
|
|
257
292
|
*
|
|
@@ -290,7 +325,10 @@ export function stripCode(body) {
|
|
|
290
325
|
export function closingReferences(body, { code = true } = {}) {
|
|
291
326
|
if (!body) return []
|
|
292
327
|
const withoutCode = code ? stripCode(body) : body
|
|
293
|
-
const pattern = new RegExp(
|
|
328
|
+
const pattern = new RegExp(
|
|
329
|
+
`\\b(${CLOSING_KEYWORDS.join('|')})\\b:?${KEYWORD_REFERENCE_GAP}(${REFERENCE})`,
|
|
330
|
+
'gi',
|
|
331
|
+
)
|
|
294
332
|
return [...withoutCode.matchAll(pattern)].map((m) => m[2])
|
|
295
333
|
}
|
|
296
334
|
|
|
@@ -361,7 +399,7 @@ export function deliberateClosingReferences(text, { code = true } = {}) {
|
|
|
361
399
|
while ((m = boundary.exec(stripped))) starts.add(m.index + m[0].length)
|
|
362
400
|
|
|
363
401
|
const pattern = new RegExp(
|
|
364
|
-
`^${CLAUSE_DECORATION}\\s*(${CLOSING_KEYWORDS.join('|')})\\b
|
|
402
|
+
`^${CLAUSE_DECORATION}\\s*(${CLOSING_KEYWORDS.join('|')})\\b:?${KEYWORD_REFERENCE_GAP}(${REFERENCE})`,
|
|
365
403
|
'i',
|
|
366
404
|
)
|
|
367
405
|
const found = []
|
|
@@ -435,7 +473,7 @@ export function negatedClosingReferences(body, { code = true } = {}) {
|
|
|
435
473
|
const text = code ? stripCode(body) : body
|
|
436
474
|
const authored = body.split('\n')
|
|
437
475
|
const pattern = new RegExp(
|
|
438
|
-
`(?:${NEGATIONS.join('|')})\\s+(?:${CLOSING_KEYWORDS.join('|')})\\b
|
|
476
|
+
`(?:${NEGATIONS.join('|')})\\s+(?:${CLOSING_KEYWORDS.join('|')})\\b:?${KEYWORD_REFERENCE_GAP}(${REFERENCE})`,
|
|
439
477
|
'gi',
|
|
440
478
|
)
|
|
441
479
|
return [...text.matchAll(pattern)].map((m) => {
|
|
@@ -252,6 +252,41 @@ export const DEPLOY_ONLY_PREFIXES = [
|
|
|
252
252
|
/** An issue reference GitHub linkifies: `#12` or `owner/repo#12`. */
|
|
253
253
|
const REFERENCE = '(?:[\\w.-]+/[\\w.-]+)?#\\d+'
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The whitespace allowed between a closing keyword (plus its optional `:`)
|
|
257
|
+
* and the issue reference it governs, in `closingReferences`,
|
|
258
|
+
* `deliberateClosingReferences` and `negatedClosingReferences` alike — all
|
|
259
|
+
* three end their pattern with the identical `\b:?\s+(REFERENCE)` tail, and
|
|
260
|
+
* all three had the identical bug.
|
|
261
|
+
*
|
|
262
|
+
* A bare `\s+` matches across an ARBITRARY run of blank lines, so a
|
|
263
|
+
* markdown heading ending in a closing keyword matched against a `#N` that
|
|
264
|
+
* starts a completely unrelated paragraph two lines later — nothing in the
|
|
265
|
+
* character class stopped it from spanning the blank line between them.
|
|
266
|
+
* Real instance, PR #1789's own body:
|
|
267
|
+
*
|
|
268
|
+
* ## What this fixes
|
|
269
|
+
*
|
|
270
|
+
* #1717 fixed one of two acceptance routes ...
|
|
271
|
+
*
|
|
272
|
+
* "fixes" is the last word of the heading; "#1717" is the first token of
|
|
273
|
+
* the next paragraph's own, unrelated sentence. `closingReferences`
|
|
274
|
+
* nonetheless reported `#1717` as a hit — `\s+` matched the `"\n\n"`
|
|
275
|
+
* between them — and that false hit reached the `deploy-only-path` branch
|
|
276
|
+
* of `assess`, which (unlike the two ground-truth branches) has no
|
|
277
|
+
* reconciliation step and fails on whatever `closingReferences` reports
|
|
278
|
+
* verbatim. Confirmed against GitHub's own ground truth for that PR
|
|
279
|
+
* (`gh pr view 1789 --json closingIssuesReferences`): only `#1718` — the
|
|
280
|
+
* genuine `Closes #1718` trailer — is ever returned; #1717 never was.
|
|
281
|
+
*
|
|
282
|
+
* A single line break is still allowed: nothing in this repo's corpus
|
|
283
|
+
* needs it, but there is no evidence GitHub REQUIRES same-line adjacency
|
|
284
|
+
* either, and the conservative/fail-closed choice for a guard whose job is
|
|
285
|
+
* "don't miss a real close" is to keep allowing one newline and exclude
|
|
286
|
+
* only a genuine blank line (two or more).
|
|
287
|
+
*/
|
|
288
|
+
const KEYWORD_REFERENCE_GAP = '(?:[ \\t]+|[ \\t]*\\n[ \\t]*)'
|
|
289
|
+
|
|
255
290
|
/**
|
|
256
291
|
* Blank out fenced code blocks and inline code spans, preserving line count.
|
|
257
292
|
*
|
|
@@ -290,7 +325,10 @@ export function stripCode(body) {
|
|
|
290
325
|
export function closingReferences(body, { code = true } = {}) {
|
|
291
326
|
if (!body) return []
|
|
292
327
|
const withoutCode = code ? stripCode(body) : body
|
|
293
|
-
const pattern = new RegExp(
|
|
328
|
+
const pattern = new RegExp(
|
|
329
|
+
`\\b(${CLOSING_KEYWORDS.join('|')})\\b:?${KEYWORD_REFERENCE_GAP}(${REFERENCE})`,
|
|
330
|
+
'gi',
|
|
331
|
+
)
|
|
294
332
|
return [...withoutCode.matchAll(pattern)].map((m) => m[2])
|
|
295
333
|
}
|
|
296
334
|
|
|
@@ -361,7 +399,7 @@ export function deliberateClosingReferences(text, { code = true } = {}) {
|
|
|
361
399
|
while ((m = boundary.exec(stripped))) starts.add(m.index + m[0].length)
|
|
362
400
|
|
|
363
401
|
const pattern = new RegExp(
|
|
364
|
-
`^${CLAUSE_DECORATION}\\s*(${CLOSING_KEYWORDS.join('|')})\\b
|
|
402
|
+
`^${CLAUSE_DECORATION}\\s*(${CLOSING_KEYWORDS.join('|')})\\b:?${KEYWORD_REFERENCE_GAP}(${REFERENCE})`,
|
|
365
403
|
'i',
|
|
366
404
|
)
|
|
367
405
|
const found = []
|
|
@@ -435,7 +473,7 @@ export function negatedClosingReferences(body, { code = true } = {}) {
|
|
|
435
473
|
const text = code ? stripCode(body) : body
|
|
436
474
|
const authored = body.split('\n')
|
|
437
475
|
const pattern = new RegExp(
|
|
438
|
-
`(?:${NEGATIONS.join('|')})\\s+(?:${CLOSING_KEYWORDS.join('|')})\\b
|
|
476
|
+
`(?:${NEGATIONS.join('|')})\\s+(?:${CLOSING_KEYWORDS.join('|')})\\b:?${KEYWORD_REFERENCE_GAP}(${REFERENCE})`,
|
|
439
477
|
'gi',
|
|
440
478
|
)
|
|
441
479
|
return [...text.matchAll(pattern)].map((m) => {
|
package/dist/index.js
CHANGED
|
@@ -646,7 +646,7 @@ import {
|
|
|
646
646
|
writeFileSync as writeFileSync2
|
|
647
647
|
} from "fs";
|
|
648
648
|
import { tmpdir } from "os";
|
|
649
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
649
|
+
import { basename, dirname as dirname3, join as join5 } from "path";
|
|
650
650
|
import { z as z4 } from "zod";
|
|
651
651
|
|
|
652
652
|
// src/lib/core-ownership-guard.ts
|
|
@@ -806,6 +806,76 @@ var EMPTY_SUMMARY = () => ({
|
|
|
806
806
|
removed: 0,
|
|
807
807
|
"remove-conflict": 0
|
|
808
808
|
});
|
|
809
|
+
function pathHasDivergenceTrailerInHistory(oursDir, path, manifest, git = defaultGit) {
|
|
810
|
+
let out;
|
|
811
|
+
try {
|
|
812
|
+
out = git(["-C", oursDir, "log", "--follow", "--format=%x02%H%x01%B", "--", path]);
|
|
813
|
+
} catch {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
const entries = out.split("").map((entry) => entry.split("")).filter((parts) => parts.length === 2 && parts[0] !== "");
|
|
817
|
+
const withTrailer = entries.filter(([, message]) => parseDivergenceTrailer(message) !== null);
|
|
818
|
+
if (withTrailer.length === 0) return false;
|
|
819
|
+
const graftedBoundary = shallowGraftCommits(oursDir, git);
|
|
820
|
+
for (const [hash, message] of withTrailer) {
|
|
821
|
+
if (graftedBoundary.has(hash)) continue;
|
|
822
|
+
let touched;
|
|
823
|
+
try {
|
|
824
|
+
touched = git([
|
|
825
|
+
"-C",
|
|
826
|
+
oursDir,
|
|
827
|
+
"diff-tree",
|
|
828
|
+
"--no-commit-id",
|
|
829
|
+
"--name-only",
|
|
830
|
+
"-r",
|
|
831
|
+
"--root",
|
|
832
|
+
hash
|
|
833
|
+
]).split("\n").filter((p) => p !== "");
|
|
834
|
+
} catch {
|
|
835
|
+
touched = [];
|
|
836
|
+
}
|
|
837
|
+
const templateOwnedTouched = touched.filter((p) => isTemplateOwned(p, manifest));
|
|
838
|
+
if (templateOwnedTouched.length === 1 && templateOwnedTouched[0] === path) return true;
|
|
839
|
+
const reason = parseDivergenceTrailer(message);
|
|
840
|
+
if (reason !== null && trailerNamesPath(reason, path, templateOwnedTouched)) return true;
|
|
841
|
+
}
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
function shallowGraftCommits(oursDir, git) {
|
|
845
|
+
let isShallow;
|
|
846
|
+
try {
|
|
847
|
+
isShallow = git(["-C", oursDir, "rev-parse", "--is-shallow-repository"]).trim();
|
|
848
|
+
} catch {
|
|
849
|
+
return /* @__PURE__ */ new Set();
|
|
850
|
+
}
|
|
851
|
+
if (isShallow !== "true") return /* @__PURE__ */ new Set();
|
|
852
|
+
try {
|
|
853
|
+
const out = git(["-C", oursDir, "rev-list", "--max-parents=0", "HEAD"]);
|
|
854
|
+
return new Set(
|
|
855
|
+
out.split("\n").map((s) => s.trim()).filter((s) => s !== "")
|
|
856
|
+
);
|
|
857
|
+
} catch {
|
|
858
|
+
return /* @__PURE__ */ new Set();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function trailerNamesPath(reason, path, templateOwnedTouched) {
|
|
862
|
+
const isBoundary = (ch) => ch === void 0 || !/[A-Za-z0-9_.-]/.test(ch);
|
|
863
|
+
const namedAt = (needle) => {
|
|
864
|
+
if (needle === "") return false;
|
|
865
|
+
let idx = 0;
|
|
866
|
+
for (; ; ) {
|
|
867
|
+
idx = reason.indexOf(needle, idx);
|
|
868
|
+
if (idx === -1) return false;
|
|
869
|
+
if (isBoundary(reason[idx - 1]) && isBoundary(reason[idx + needle.length])) return true;
|
|
870
|
+
idx += 1;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
if (namedAt(path)) return true;
|
|
874
|
+
const base = basename(path);
|
|
875
|
+
if (!namedAt(base)) return false;
|
|
876
|
+
const sharingBasename = templateOwnedTouched.filter((p) => basename(p) === base);
|
|
877
|
+
return sharingBasename.length <= 1;
|
|
878
|
+
}
|
|
809
879
|
async function planCoreUpgrade(options) {
|
|
810
880
|
const mergeFile = options.mergeFile ?? gitMergeFile;
|
|
811
881
|
const trackedOnly = { trackedOnly: true };
|
|
@@ -819,7 +889,21 @@ async function planCoreUpgrade(options) {
|
|
|
819
889
|
const ours = new Set(listTemplateOwnedFiles(options.oursDir, options.manifest));
|
|
820
890
|
const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest, trackedOnly));
|
|
821
891
|
const divergentPrefixes = readDivergenceConfig(options.oursDir).warnOnly.map((e) => e.prefix);
|
|
822
|
-
const
|
|
892
|
+
const trailerDivergenceCache = /* @__PURE__ */ new Map();
|
|
893
|
+
const isDeclaredDivergent = (path) => {
|
|
894
|
+
if (divergentPrefixes.some((prefix) => path.startsWith(prefix))) return true;
|
|
895
|
+
let viaTrailer = trailerDivergenceCache.get(path);
|
|
896
|
+
if (viaTrailer === void 0) {
|
|
897
|
+
viaTrailer = pathHasDivergenceTrailerInHistory(
|
|
898
|
+
options.oursDir,
|
|
899
|
+
path,
|
|
900
|
+
options.manifest,
|
|
901
|
+
options.git
|
|
902
|
+
);
|
|
903
|
+
trailerDivergenceCache.set(path, viaTrailer);
|
|
904
|
+
}
|
|
905
|
+
return viaTrailer;
|
|
906
|
+
};
|
|
823
907
|
const paths = [.../* @__PURE__ */ new Set([...base, ...ours, ...theirs])].sort();
|
|
824
908
|
const entries = [];
|
|
825
909
|
const divergenceSkips = [];
|
|
@@ -3795,7 +3879,7 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
|
|
|
3795
3879
|
}
|
|
3796
3880
|
printNewInstanceSeams(newSeams);
|
|
3797
3881
|
printAdoptionReport(adoption);
|
|
3798
|
-
printOrphanReport(plan.orphaned, orphanRatchet);
|
|
3882
|
+
printOrphanReport(plan.orphaned, orphanRatchet, plan.entries.length);
|
|
3799
3883
|
if (orphanRatchet.increased) {
|
|
3800
3884
|
throw new Error(
|
|
3801
3885
|
`${String(plan.orphaned.length)} unsanctioned instance file(s) under a template-owned path (baseline ${String(orphanRatchet.baseline)}) \u2014 see the list above (#1026). Move new instance-written files under a sanctioned carve-out (services/api/tests/instance/, modules/**/instance/) or otherwise reduce the count, then re-run.`
|
|
@@ -4370,11 +4454,11 @@ function printTargetFidelity(report, toVersion) {
|
|
|
4370
4454
|
for (const f of report.findings) console.log(` ${chalk4.red(f.path)} \u2014 ${f.reason}`);
|
|
4371
4455
|
console.log();
|
|
4372
4456
|
}
|
|
4373
|
-
function printOrphanReport(orphaned, ratchet) {
|
|
4457
|
+
function printOrphanReport(orphaned, ratchet, examined) {
|
|
4374
4458
|
if (orphaned.length === 0 && ratchet.baseline === null) return;
|
|
4375
4459
|
console.log(
|
|
4376
4460
|
chalk4.bold(
|
|
4377
|
-
` ${String(orphaned.length)} unsanctioned instance file(s) under a template-owned path (#1026):`
|
|
4461
|
+
` ${String(orphaned.length)} unsanctioned instance file(s) under a template-owned path (#1026), of ${String(examined)} template-owned path(s) examined:`
|
|
4378
4462
|
)
|
|
4379
4463
|
);
|
|
4380
4464
|
for (const e of orphaned) console.log(` ${chalk4.yellow(e.path)}`);
|
|
@@ -9000,7 +9084,7 @@ function vendorPluginSeed(pluginSourceDir, manifest, cwd) {
|
|
|
9000
9084
|
|
|
9001
9085
|
// src/lib/plugin-source-copy.ts
|
|
9002
9086
|
import { copyFileSync as copyFileSync2, cpSync as cpSync4, mkdirSync as mkdirSync10 } from "fs";
|
|
9003
|
-
import { basename, dirname as dirname9, join as join31 } from "path";
|
|
9087
|
+
import { basename as basename2, dirname as dirname9, join as join31 } from "path";
|
|
9004
9088
|
var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
|
|
9005
9089
|
".git",
|
|
9006
9090
|
".venv",
|
|
@@ -9028,7 +9112,7 @@ async function copyPluginSource(sourceDir, targetDir) {
|
|
|
9028
9112
|
mkdirSync10(targetDir, { recursive: true });
|
|
9029
9113
|
cpSync4(sourceDir, targetDir, {
|
|
9030
9114
|
recursive: true,
|
|
9031
|
-
filter: (src) => !LOCAL_COPY_EXCLUDES.has(
|
|
9115
|
+
filter: (src) => !LOCAL_COPY_EXCLUDES.has(basename2(src))
|
|
9032
9116
|
});
|
|
9033
9117
|
return { usedGitIgnoreRules: false };
|
|
9034
9118
|
}
|
|
@@ -12591,7 +12675,7 @@ ${BOLD2}What to do${OFF2}
|
|
|
12591
12675
|
|
|
12592
12676
|
// src/scripts/check-orphan-ratchet.ts
|
|
12593
12677
|
import { existsSync as existsSync46 } from "fs";
|
|
12594
|
-
import { basename as
|
|
12678
|
+
import { basename as basename3 } from "path";
|
|
12595
12679
|
function reportOrphan(entry, manifest) {
|
|
12596
12680
|
console.error(` ${entry.path}`);
|
|
12597
12681
|
const { templateOwnedMatch, nearestUserOwnedEntries } = explainOwnership(entry.path, manifest);
|
|
@@ -12624,7 +12708,7 @@ async function runOrphanRatchetCheck(opts = {}) {
|
|
|
12624
12708
|
const theirsDir = opts.theirsDir ?? root;
|
|
12625
12709
|
const instanceDir = opts.instanceDir ?? theirsDir;
|
|
12626
12710
|
const baseDir = opts.baseDir ?? theirsDir;
|
|
12627
|
-
const label = opts.label ?? (
|
|
12711
|
+
const label = opts.label ?? (basename3(instanceDir) || instanceDir);
|
|
12628
12712
|
if (instanceDir === theirsDir && baseDir === theirsDir) {
|
|
12629
12713
|
console.log(
|
|
12630
12714
|
"self-check mode: --instance-dir was not given, so it defaulted to --theirs-dir (this repo's own root) alongside --base-dir \u2014 all three trees are the same, so this run can only ever find zero orphans by construction. See this script's doc comment. Pass a real instance tree with --instance-dir for a check that can actually find something. biffo-template#1714."
|