@biffo/cli 0.298.22 → 0.298.24

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.
package/dist/index.js CHANGED
@@ -371,6 +371,39 @@ function isTemplateOwned(relPath, manifest) {
371
371
  const u = longestMatch(relPath, manifest.userOwned);
372
372
  return t > u;
373
373
  }
374
+ function leadingSegmentOverlap(relPath, pattern) {
375
+ const pathSegs = relPath.split("/");
376
+ const patternSegs = pattern.replace(/\/$/, "").split("/");
377
+ let shared = 0;
378
+ for (let i = 0; i < Math.min(pathSegs.length, patternSegs.length); i++) {
379
+ if (patternSegs[i] !== pathSegs[i]) break;
380
+ shared++;
381
+ }
382
+ return shared;
383
+ }
384
+ function explainOwnership(relPath, manifest) {
385
+ let templateOwnedMatch = null;
386
+ let bestTemplateLen = -1;
387
+ for (const p of manifest.templateOwned) {
388
+ const len = matchLength(relPath, p);
389
+ if (len > bestTemplateLen) {
390
+ bestTemplateLen = len;
391
+ templateOwnedMatch = p;
392
+ }
393
+ }
394
+ let bestShared = 0;
395
+ let nearestUserOwnedEntries = [];
396
+ for (const p of manifest.userOwned) {
397
+ const shared = leadingSegmentOverlap(relPath, p);
398
+ if (shared > bestShared) {
399
+ bestShared = shared;
400
+ nearestUserOwnedEntries = [p];
401
+ } else if (shared === bestShared && shared > 0) {
402
+ nearestUserOwnedEntries.push(p);
403
+ }
404
+ }
405
+ return { templateOwnedMatch, nearestUserOwnedEntries };
406
+ }
374
407
  function toPosix(p) {
375
408
  return sep === "/" ? p : p.split(sep).join("/");
376
409
  }
@@ -11755,8 +11788,8 @@ async function runOwnershipCheck(argv) {
11755
11788
  const { stdout } = await execa("git", ["diff", "--cached", "--name-status"], { cwd: root });
11756
11789
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
11757
11790
  if (messageFile) {
11758
- const { readFileSync: readFileSync45, existsSync: existsSync54 } = await import("fs");
11759
- if (existsSync54(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
11791
+ const { readFileSync: readFileSync45, existsSync: existsSync55 } = await import("fs");
11792
+ if (existsSync55(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
11760
11793
  }
11761
11794
  } else {
11762
11795
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -12463,6 +12496,71 @@ ${BOLD2}What to do${OFF2}
12463
12496
  process.exit(1);
12464
12497
  }
12465
12498
 
12499
+ // src/scripts/check-orphan-ratchet.ts
12500
+ import { existsSync as existsSync46 } from "fs";
12501
+ import { basename as basename2 } from "path";
12502
+ function reportOrphan(entry, manifest) {
12503
+ console.error(` ${entry.path}`);
12504
+ const { templateOwnedMatch, nearestUserOwnedEntries } = explainOwnership(entry.path, manifest);
12505
+ console.error(
12506
+ ` claimed by templateOwned entry: ${templateOwnedMatch ?? "(none \u2014 unexpected)"}`
12507
+ );
12508
+ if (nearestUserOwnedEntries.length > 0) {
12509
+ console.error(` nearest sanctioned carve-out(s): ${nearestUserOwnedEntries.join(", ")}`);
12510
+ console.error(
12511
+ " \u2014 move it under one of those, OR add it to biffo.divergence.json with a Core-Divergence trailer if it belongs there permanently."
12512
+ );
12513
+ } else {
12514
+ console.error(
12515
+ " no userOwned entry shares any leading path segment with this file \u2014 core-manifest.json has no carve-out anywhere near it today."
12516
+ );
12517
+ console.error(
12518
+ " Either this is a core capability that belongs upstream in biffo-template, or it needs a new carve-out declared in core-manifest.json. classify()/isTemplateOwned() cannot tell those apart automatically (#1714) \u2014 that judgement is the author's."
12519
+ );
12520
+ }
12521
+ }
12522
+ async function runOrphanRatchetCheck(opts = {}) {
12523
+ if (opts.instanceDir !== void 0 && !existsSync46(opts.instanceDir)) {
12524
+ console.error(
12525
+ `\u2717 orphan-ratchet guard: --instance-dir ${opts.instanceDir} does not exist \u2014 cannot tell whether it carries unsanctioned files, and that is not the same as a clean pass.`
12526
+ );
12527
+ process.exit(2);
12528
+ return;
12529
+ }
12530
+ const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12531
+ const theirsDir = opts.theirsDir ?? root;
12532
+ const instanceDir = opts.instanceDir ?? theirsDir;
12533
+ const baseDir = opts.baseDir ?? theirsDir;
12534
+ const label = opts.label ?? (basename2(instanceDir) || instanceDir);
12535
+ if (instanceDir === theirsDir && baseDir === theirsDir) {
12536
+ console.log(
12537
+ "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."
12538
+ );
12539
+ }
12540
+ const manifest = readCoreManifest(theirsDir);
12541
+ const plan = await planCoreUpgrade({ baseDir, oursDir: instanceDir, theirsDir, manifest });
12542
+ const baseline = readOrphanBaseline(instanceDir);
12543
+ const ratchet = checkOrphanRatchet(plan.orphaned.length, baseline);
12544
+ console.log(
12545
+ `examined ${label}: ${plan.orphaned.length} unsanctioned file(s) under a template-owned path (baseline ${baseline === null ? "none recorded" : String(baseline.count)}), against template tree ${theirsDir}`
12546
+ );
12547
+ if (plan.orphaned.length === 0) {
12548
+ console.log(`\u2713 orphan-ratchet guard (${label}): no unsanctioned files`);
12549
+ return;
12550
+ }
12551
+ console.error(
12552
+ `${ratchet.increased ? "\u2717" : "\xB7"} orphan-ratchet guard (${label}): ${String(plan.orphaned.length)} unsanctioned instance file(s) under a template-owned path (#1026)${ratchet.increased ? " \u2014 INCREASED over the recorded baseline" : ""}:`
12553
+ );
12554
+ for (const entry of plan.orphaned) reportOrphan(entry, manifest);
12555
+ if (ratchet.increased) {
12556
+ console.error(
12557
+ `
12558
+ baseline was ${String(ratchet.baseline)}, now ${String(ratchet.count)}. See biffo-template#1026 and biffo-template#1714.`
12559
+ );
12560
+ process.exit(1);
12561
+ }
12562
+ }
12563
+
12466
12564
  // src/scripts/check-pipe-trap.ts
12467
12565
  import { readFileSync as readFileSync37, readdirSync as readdirSync22 } from "fs";
12468
12566
  import { join as join52, relative as relative9 } from "path";
@@ -12737,16 +12835,16 @@ async function runPluginAllowlistConventionCheck() {
12737
12835
  }
12738
12836
 
12739
12837
  // src/scripts/check-plugin-collisions.ts
12740
- import { existsSync as existsSync47 } from "fs";
12838
+ import { existsSync as existsSync48 } from "fs";
12741
12839
  import { join as join55 } from "path";
12742
12840
 
12743
12841
  // src/lib/plugin-collision-guard.ts
12744
- import { existsSync as existsSync46, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12842
+ import { existsSync as existsSync47, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12745
12843
  import { join as join54 } from "path";
12746
12844
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
12747
12845
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
12748
12846
  function subdirectories(dir) {
12749
- if (!existsSync46(dir)) return [];
12847
+ if (!existsSync47(dir)) return [];
12750
12848
  return readdirSync23(dir).filter((entry) => {
12751
12849
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
12752
12850
  try {
@@ -12757,12 +12855,12 @@ function subdirectories(dir) {
12757
12855
  });
12758
12856
  }
12759
12857
  function regularPackagesOf(pluginDir2) {
12760
- return subdirectories(pluginDir2).filter((name) => existsSync46(join54(pluginDir2, name, "__init__.py"))).sort();
12858
+ return subdirectories(pluginDir2).filter((name) => existsSync47(join54(pluginDir2, name, "__init__.py"))).sort();
12761
12859
  }
12762
12860
  function bareTestModulesOf(pluginDir2) {
12763
12861
  const testsDir = join54(pluginDir2, "tests");
12764
- if (!existsSync46(testsDir)) return [];
12765
- if (existsSync46(join54(testsDir, "__init__.py"))) return [];
12862
+ if (!existsSync47(testsDir)) return [];
12863
+ if (existsSync47(join54(testsDir, "__init__.py"))) return [];
12766
12864
  return readdirSync23(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
12767
12865
  }
12768
12866
  function findCollisions(servicesDir, pluginDirs) {
@@ -12810,7 +12908,7 @@ function formatCollisions(collisions) {
12810
12908
  async function runPluginCollisionCheck() {
12811
12909
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12812
12910
  const servicesDir = join55(root, "services");
12813
- if (!existsSync47(servicesDir)) {
12911
+ if (!existsSync48(servicesDir)) {
12814
12912
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
12815
12913
  return;
12816
12914
  }
@@ -12827,7 +12925,7 @@ async function runPluginCollisionCheck() {
12827
12925
  }
12828
12926
 
12829
12927
  // src/lib/plugin-terraform-guard.ts
12830
- import { existsSync as existsSync48, readFileSync as readFileSync39, readdirSync as readdirSync24 } from "fs";
12928
+ import { existsSync as existsSync49, readFileSync as readFileSync39, readdirSync as readdirSync24 } from "fs";
12831
12929
  import { dirname as dirname10, join as join56, relative as relative10, sep as sep4 } from "path";
12832
12930
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
12833
12931
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
@@ -12870,14 +12968,14 @@ function readSubscriptions(absManifestPath) {
12870
12968
  }
12871
12969
  function checkPluginTerraform(root) {
12872
12970
  const violations = [];
12873
- const coreManifest = existsSync48(join56(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12971
+ const coreManifest = existsSync49(join56(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12874
12972
  for (const manifest of findPluginManifests(root)) {
12875
12973
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
12876
12974
  const absManifest = join56(root, manifest);
12877
12975
  const subscriptions = readSubscriptions(absManifest);
12878
12976
  if (subscriptions === null) continue;
12879
12977
  const pluginDir2 = dirname10(absManifest);
12880
- if (existsSync48(join56(pluginDir2, "terraform"))) continue;
12978
+ if (existsSync49(join56(pluginDir2, "terraform"))) continue;
12881
12979
  const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
12882
12980
  violations.push({
12883
12981
  manifest,
@@ -12908,11 +13006,11 @@ async function runPluginTerraformCheck() {
12908
13006
  }
12909
13007
 
12910
13008
  // src/scripts/check-plugin-tool-supply.ts
12911
- import { existsSync as existsSync50 } from "fs";
13009
+ import { existsSync as existsSync51 } from "fs";
12912
13010
  import { join as join58 } from "path";
12913
13011
 
12914
13012
  // src/lib/plugin-tool-supply-audit.ts
12915
- import { existsSync as existsSync49, readFileSync as readFileSync40, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
13013
+ import { existsSync as existsSync50, readFileSync as readFileSync40, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
12916
13014
  import { join as join57 } from "path";
12917
13015
 
12918
13016
  // src/lib/openrouter-model-snapshot.ts
@@ -13646,8 +13744,8 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
13646
13744
  const now = options.now ?? /* @__PURE__ */ new Date();
13647
13745
  const configPath = join57(repoRoot, CONFIG_PY_PATH);
13648
13746
  const orchestrationPath = join57(repoRoot, ORCHESTRATION_SCHEMA_PATH);
13649
- const configMissing = !existsSync49(configPath);
13650
- const orchestrationSchemaMissing = !existsSync49(orchestrationPath);
13747
+ const configMissing = !existsSync50(configPath);
13748
+ const orchestrationSchemaMissing = !existsSync50(orchestrationPath);
13651
13749
  const knownSet = new Set(knownModelIds);
13652
13750
  const snapshotEmpty = knownModelIds.length === 0;
13653
13751
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -13864,7 +13962,7 @@ async function runPluginToolSupplyCheck() {
13864
13962
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13865
13963
  let allOk = true;
13866
13964
  const pluginsRoot = join58(root, "services", "_plugins");
13867
- if (!existsSync50(pluginsRoot)) {
13965
+ if (!existsSync51(pluginsRoot)) {
13868
13966
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
13869
13967
  } else {
13870
13968
  const report = auditPluginToolSupply(pluginsRoot);
@@ -13895,7 +13993,7 @@ async function runPluginToolSupplyCheck() {
13895
13993
  }
13896
13994
  }
13897
13995
  const servicesApiRoot = join58(root, "services", "api");
13898
- if (!existsSync50(servicesApiRoot)) {
13996
+ if (!existsSync51(servicesApiRoot)) {
13899
13997
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
13900
13998
  } else {
13901
13999
  const modelReport = auditDeclaredModelIds(root);
@@ -14272,7 +14370,7 @@ async function runSharedFileReductionCheck(args) {
14272
14370
  }
14273
14371
 
14274
14372
  // src/scripts/check-skeleton-drift.ts
14275
- import { existsSync as existsSync51, readdirSync as readdirSync27 } from "fs";
14373
+ import { existsSync as existsSync52, readdirSync as readdirSync27 } from "fs";
14276
14374
  import { join as join60 } from "path";
14277
14375
 
14278
14376
  // src/lib/skeleton-drift-guard.ts
@@ -14399,7 +14497,7 @@ function discoverSkeletons(root) {
14399
14497
  } catch {
14400
14498
  return [];
14401
14499
  }
14402
- return entries.filter((name) => existsSync51(join60(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
14500
+ return entries.filter((name) => existsSync52(join60(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
14403
14501
  }
14404
14502
  async function runSkeletonDriftCheck() {
14405
14503
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
@@ -14408,7 +14506,7 @@ async function runSkeletonDriftCheck() {
14408
14506
  for (const name of skeletons) {
14409
14507
  const skeletonRoot = join60(root, "_skeletons", name);
14410
14508
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
14411
- if (existsSync51(join60(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
14509
+ if (existsSync52(join60(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
14412
14510
  filesConsidered += 1;
14413
14511
  }
14414
14512
  }
@@ -14458,7 +14556,7 @@ async function runTerraformInputCheck() {
14458
14556
 
14459
14557
  // src/commands/check.ts
14460
14558
  var checkCommand = new Command24("check").description(
14461
- "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
14559
+ "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, orphan-ratchet, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
14462
14560
  );
14463
14561
  checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
14464
14562
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -14521,6 +14619,22 @@ checkCommand.command("instance-adoption").description(
14521
14619
  ).action(async (opts) => {
14522
14620
  await runInstanceAdoptionCheck(opts);
14523
14621
  });
14622
+ checkCommand.command("orphan-ratchet").description(
14623
+ "Refuse an instance-written file under a template-owned path with no sanctioned carve-out (#1026), reusing planCoreUpgrade's classify() and checkOrphanRatchet \u2014 previously reachable only from inside `biffo core upgrade`, so drift was discovered only ~90 core versions later by the upgrade it blocks (#1714). --instance-dir defaults to a SELF-CHECK (this repo's own tree, unlike instance-adoption above) that can only ever report zero \u2014 see the entrypoint's own doc comment for why that default exists anyway. Pass a real instance checkout for a check that can actually find something."
14624
+ ).option(
14625
+ "--instance-dir <dir>",
14626
+ "The real instance tree to check (oursDir). Defaults to a self-check of this repo's own root, which always reports 0 orphans (#1714) \u2014 pass a real instance checkout for a meaningful result"
14627
+ ).option(
14628
+ "--theirs-dir <dir>",
14629
+ "Template tree that defines ownership (theirsDir); defaults to this repo root"
14630
+ ).option(
14631
+ "--base-dir <dir>",
14632
+ "Merge-base template tree (baseDir) \u2014 the template at the instance's CURRENT core version; defaults to --theirs-dir, which is only correct when the instance is already on the latest version"
14633
+ ).option("--label <name>", "Label for the report; defaults to the basename of --instance-dir").action(
14634
+ async (opts) => {
14635
+ await runOrphanRatchetCheck(opts);
14636
+ }
14637
+ );
14524
14638
  checkCommand.command("cognito-invite-template").description(
14525
14639
  "Refuse a Cognito invite_message_template missing a required member or placeholder (#356) \u2014 terraform validate is silent on this, so a fresh deploy fails on its first apply"
14526
14640
  ).action(async () => {
@@ -14587,7 +14701,7 @@ function rawArgsAfter(subcommand) {
14587
14701
  }
14588
14702
 
14589
14703
  // src/commands/doctor.ts
14590
- import { existsSync as existsSync52, readFileSync as readFileSync44 } from "fs";
14704
+ import { existsSync as existsSync53, readFileSync as readFileSync44 } from "fs";
14591
14705
  import { join as join61, resolve as resolve20 } from "path";
14592
14706
  import chalk21 from "chalk";
14593
14707
  import { Command as Command25 } from "commander";
@@ -14768,7 +14882,7 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
14768
14882
  }
14769
14883
  function readLocalCoreVersion(cwd) {
14770
14884
  const path = join61(cwd, INSTANCE_CORE_FILE);
14771
- if (!existsSync52(path)) return null;
14885
+ if (!existsSync53(path)) return null;
14772
14886
  try {
14773
14887
  return extractVersionField(readFileSync44(path, "utf8"));
14774
14888
  } catch {
@@ -14791,7 +14905,7 @@ function extractVersionField(contents) {
14791
14905
  }
14792
14906
  function readFossil(cwd) {
14793
14907
  const path = join61(cwd, CORE_VERSION_FILE);
14794
- if (!existsSync52(path)) return null;
14908
+ if (!existsSync53(path)) return null;
14795
14909
  try {
14796
14910
  const value = readFileSync44(path, "utf8").trim();
14797
14911
  return value === "" ? null : value;
@@ -15242,13 +15356,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
15242
15356
  import { Command as Command27 } from "commander";
15243
15357
 
15244
15358
  // src/lib/packaged-scripts.ts
15245
- import { existsSync as existsSync53 } from "fs";
15359
+ import { existsSync as existsSync54 } from "fs";
15246
15360
  import { dirname as dirname11, join as join62 } from "path";
15247
15361
  function findPackagedScript(startDir, relativePath) {
15248
15362
  let dir = startDir;
15249
15363
  for (; ; ) {
15250
15364
  const candidate = join62(dir, relativePath);
15251
- if (existsSync53(candidate)) return candidate;
15365
+ if (existsSync54(candidate)) return candidate;
15252
15366
  const parent = dirname11(dir);
15253
15367
  if (parent === dir) return null;
15254
15368
  dir = parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.298.22",
3
+ "version": "0.298.24",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/claim.sh CHANGED
@@ -243,6 +243,21 @@ while [ $# -gt 0 ]; do
243
243
  shift 2
244
244
  ;;
245
245
  -h | --help) usage ;;
246
+ -*)
247
+ # An unrecognized flag must never fall into the catch-all below (#1741).
248
+ # It used to: a token that starts with `-` but matches none of the
249
+ # cases above fell straight into `*) ISSUE="$1"; shift ;;`, so
250
+ # `--bogus` silently became the "issue number" -- corrupting ISSUE with
251
+ # flag text instead of the real positional argument -- and the script
252
+ # went on to fail the `[!0-9]*` check below with "give an issue
253
+ # number", a message that describes the SYMPTOM (ISSUE is not
254
+ # numeric) and hides the actual CAUSE (an unsupported flag was typed).
255
+ # Refusing here, at the point the flag is actually unrecognized, says
256
+ # what is really wrong instead of a confusing knock-on error.
257
+ echo "${RED}claim: unrecognized flag '$1'${OFF}" >&2
258
+ echo "${DIM} Known flags: -R/--repo, --check, --as, --release, --guard, -h/--help.${OFF}" >&2
259
+ exit 2
260
+ ;;
246
261
  *)
247
262
  ISSUE="$1"
248
263
  shift