@bensandee/tooling 0.84.0 → 0.84.1

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/dist/bin.mjs +135 -51
  2. package/package.json +1 -1
package/dist/bin.mjs CHANGED
@@ -269,7 +269,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
269
269
  */
270
270
  const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
271
271
  name: "bst",
272
- version: "0.84.0"
272
+ version: "0.84.1"
273
273
  });
274
274
  //#endregion
275
275
  //#region ../cli-kit/src/logger.ts
@@ -7163,7 +7163,7 @@ function buildManagedDevDeps(ctx) {
7163
7163
  const devDeps = { ...rootDevDeps(ctx.config) };
7164
7164
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
7165
7165
  if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.5";
7166
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.84.0";
7166
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.84.1";
7167
7167
  if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.15.2";
7168
7168
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
7169
7169
  "@changesets/cli": "3.0.3",
@@ -11950,6 +11950,10 @@ const GIT_GUIDE_BODY = [
11950
11950
  "",
11951
11951
  "Start a pull request's title with the stage it carries: `Spec:` for a spec, `Plan:` for a plan, and no stem once it carries the implementation. A draft keeps its `WIP:` marker in front (`WIP: Plan: Add retry to uploads`). When a pull request moves on to the next stage, move the stem with it. This is informational -- nothing checks it and nothing fails without it -- but it tells a reviewer at a glance what they are reading.",
11952
11952
  "",
11953
+ "**Drop `WIP:` and fix the stem when the work is done.** Once the pull request carries finished work you are handing over for review -- a spec or a plan ready to approve, or an implementation whose check passes -- remove the `WIP:` marker and make the stem match what it now carries: no stem for an implementation. Do it in the same step that reports the work done, without being asked. A marker left behind tells a reviewer the work is still moving.",
11954
+ "",
11955
+ "`pr:update` never changes a pull request's draft state, so it keeps `WIP:` on a retitle. On Forgejo a draft is only that title prefix: retitle the pull request without it (`bst-fj issue:update <n> --title \"<title>\"` where `bst-fj` is installed). On GitHub, run `gh pr ready` and then edit the title.",
11956
+ "",
11953
11957
  "## Merging the default branch in",
11954
11958
  "",
11955
11959
  "**A merge with no conflicts is not yet a merge that works.** Read the incoming diff for what your branch builds on -- a field made required, a shared schema, a size budget -- and run the full check before you push the merge. Fix what it finds in the merge commit itself, so that commit passes on its own.",
@@ -12692,7 +12696,7 @@ function reportFailures(failed, command) {
12692
12696
  function generateMigratePrompt(results, config, detected) {
12693
12697
  const sections = [];
12694
12698
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12695
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.84.0 repo:sync\` on ${timestamp}_\n`);
12699
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.84.1 repo:sync\` on ${timestamp}_\n`);
12696
12700
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
12697
12701
  if (changed.length > 0) {
12698
12702
  const items = changed.map((r) => {
@@ -27877,13 +27881,39 @@ function normalizeIssue(raw) {
27877
27881
  };
27878
27882
  }
27879
27883
  /**
27884
+ * Parse the backing issue number out of a PR body's `Closes #<n>` line. Returns null when the
27885
+ * body carries no such line (a hand-written PR, or a body that could not be read). Lives beside
27886
+ * {@link toIssueSummaries}, which keeps it for every pull request a search returns.
27887
+ *
27888
+ * `Closes` only, deliberately -- narrower than the `Closes`/`Fixes`/`Resolves` vocabulary the
27889
+ * closing-ref machinery in `closing-refs.ts` recognizes, and it must stay
27890
+ * that way. A body whose *first* closing reference names some other issue is what a caller
27891
+ * resolves the backing issue against, and a branch name derived from that number follows it, so
27892
+ * every keyword added here widens the set of bodies that can be misrouted. The
27893
+ * cost of the narrow set is the opposite failure and the cheaper one: an adopted body already
27894
+ * saying `Fixes #<n>` gets no `Closes` line appended (it already closes the issue), so this
27895
+ * returns null and the PR is treated as having no backing issue -- which still closes its issue
27896
+ * on merge and still runs on the PR's own head ref, losing only the issue mapping.
27897
+ */
27898
+ function backingIssueNumber(prBody) {
27899
+ const match = /Closes #(\d+)/i.exec(prBody);
27900
+ if (match === null) return null;
27901
+ const parsed = Number(match[1]);
27902
+ return Number.isNaN(parsed) ? null : parsed;
27903
+ }
27904
+ /**
27880
27905
  * Normalize a raw issue list, drop pull requests (both platforms serve them
27881
27906
  * through the issues endpoint), and strip bodies down to summaries. Shared by
27882
27907
  * the Forgejo and GitHub list transports so the post-processing chain lives
27883
27908
  * in exactly one place.
27884
27909
  */
27885
27910
  function toIssueSummaries(raw, type = "issues") {
27886
- const summaries = raw.map(normalizeIssue).map(({ body: _body, ...summary }) => summary);
27911
+ const summaries = raw.map(normalizeIssue).map(({ body, ...summary }) => {
27912
+ const result = summary;
27913
+ const backingIssue = summary.isPullRequest ? backingIssueNumber(body) : null;
27914
+ if (backingIssue !== null) result.backingIssue = backingIssue;
27915
+ return result;
27916
+ });
27887
27917
  return type === "issues" ? summaries.filter((s) => !s.isPullRequest) : summaries;
27888
27918
  }
27889
27919
  /** Fetch a single issue by number. */
@@ -30533,6 +30563,71 @@ function writeReviewPackage(planPath, base, head, cwd, out) {
30533
30563
  };
30534
30564
  }
30535
30565
  //#endregion
30566
+ //#region src/plan-workspace/task-label.ts
30567
+ /** The line shapes `plan:brief` and `plan:status` both read a task's number from. */
30568
+ const HEADING = /^(#{1,6})[ \t]+(.*)$/;
30569
+ const BOX = /^([ \t]*)[-*] \[[ xX]\][ \t]*(.*)$/;
30570
+ const LABEL = /^Task[ \t]+([0-9]+)(?![0-9])/;
30571
+ /** A line that opens a fence: three or more of one character, after any amount of whitespace. */
30572
+ const FENCE_OPEN = /^[ \t]*(`{3,}|~{3,})/;
30573
+ /** The same shape with nothing but whitespace after the run, which is what closes a fence. */
30574
+ const FENCE_CLOSE = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
30575
+ /** Whether a line closes the fence opened by `fenceChar` repeated `fenceLength` times. */
30576
+ function closesFence(text, fenceChar, fenceLength) {
30577
+ const run = FENCE_CLOSE.exec(text)?.[1];
30578
+ if (run === void 0) return false;
30579
+ return run[0] === fenceChar && run.length >= fenceLength;
30580
+ }
30581
+ /**
30582
+ * Which lines of a document a rendered markdown viewer would show as code rather than parse as
30583
+ * markdown: true at every fence line and every line inside one, false elsewhere. The rules are
30584
+ * CommonMark's, with two choices that belong to these readers:
30585
+ *
30586
+ * - Any indentation opens a fence, not the 0-3 spaces CommonMark allows, because plans nest
30587
+ * example fences under list items and none of the readers tracks list nesting.
30588
+ * - An unclosed fence runs to the end of the document, which is how a renderer shows one.
30589
+ *
30590
+ * A closer is the opener's own character repeated at least as many times, with nothing after it
30591
+ * but whitespace; a run of the other character, a shorter run, or a run carrying an info string is
30592
+ * code content and keeps the fence open.
30593
+ *
30594
+ * `stripFencedCodeBlocks` in `docs-check/links.ts` walks fences by the same rule but is a
30595
+ * different reader with different rules (it limits the opener's indent and tracks blockquote
30596
+ * depth), which is why this walk is not shared with it.
30597
+ */
30598
+ function fencedLines(lines) {
30599
+ const fenced = lines.map(() => false);
30600
+ let fenceChar;
30601
+ let fenceLength = 0;
30602
+ for (let index = 0; index < lines.length; index++) {
30603
+ const text = lines[index] ?? "";
30604
+ if (fenceChar === void 0) {
30605
+ const run = FENCE_OPEN.exec(text)?.[1];
30606
+ if (run === void 0) continue;
30607
+ fenceChar = run[0];
30608
+ fenceLength = run.length;
30609
+ fenced[index] = true;
30610
+ continue;
30611
+ }
30612
+ fenced[index] = true;
30613
+ if (closesFence(text, fenceChar, fenceLength)) {
30614
+ fenceChar = void 0;
30615
+ fenceLength = 0;
30616
+ }
30617
+ }
30618
+ return fenced;
30619
+ }
30620
+ /**
30621
+ * The `<n>` of a heading or checkbox text that starts `Task <n>` (after an opening `**` or `__`),
30622
+ * or null. A number with a leading zero reads as null, because `plan:brief <n>` matches the digits
30623
+ * as written and could never find `Task 05`.
30624
+ */
30625
+ function taskLabelNumber(text) {
30626
+ const digits = LABEL.exec(text.replace(/^(\*\*|__)/, ""))?.[1];
30627
+ if (digits === void 0 || String(Number(digits)) !== digits) return null;
30628
+ return Number(digits);
30629
+ }
30630
+ //#endregion
30536
30631
  //#region src/plan-workspace/plan-boxes.ts
30537
30632
  /**
30538
30633
  * Read a plan document's checkboxes and the notes committed under them. One strict parser, fail-
@@ -30545,8 +30640,14 @@ function writeReviewPackage(planPath, base, head, cwd, out) {
30545
30640
  * boundary matters because a batched plan is a sequence of `### <title>`
30546
30641
  * sections and a note must never leak across one.
30547
30642
  *
30548
- * The agent imports this module (`run-outcome.ts`, `run-gates.ts`, `prompt.ts`, `run.ts`), so a
30549
- * change here changes how the agent reads a plan as well as what `plan:status` reports.
30643
+ * A line inside a fenced code block (`fencedLines`, `task-label.ts`) is invisible in every role:
30644
+ * a fenced box is not counted, a fenced heading does not end a block, and a fenced note or
30645
+ * declination is not one. The fail-safe direction holds -- an ambiguous line degrades to "not
30646
+ * declined".
30647
+ *
30648
+ * The agent imports this module (`run-outcome.ts`, `run-gates.ts`, `run.ts`, `plan-box-diff.ts`,
30649
+ * `plan-box-watch.ts`, `plan-box-walk.ts`, `plan-progress.ts`), so a change here changes how the
30650
+ * agent reads a plan as well as what `plan:status` reports.
30550
30651
  *
30551
30652
  * An unchecked box is **declined** when its block carries a line matching
30552
30653
  * `^[ \t]*(?:[-*][ \t]+)?\*\*Declined:\*\*[ \t]*(.*)$` whose trimmed remainder is non-empty; that
@@ -30599,8 +30700,10 @@ function unwrapEmphasis(rawTitle) {
30599
30700
  /** Parse every checkbox on a plan document, each with its own block already resolved. */
30600
30701
  function parsePlanBoxes(planText) {
30601
30702
  const lines = planText.split("\n");
30703
+ const fenced = fencedLines(lines);
30602
30704
  const boxStarts = [];
30603
30705
  for (let index = 0; index < lines.length; index++) {
30706
+ if (fenced[index] === true) continue;
30604
30707
  const match = BOX_LINE_REGEX.exec(lines[index] ?? "");
30605
30708
  if (match === null) continue;
30606
30709
  boxStarts.push({
@@ -30612,13 +30715,17 @@ function parsePlanBoxes(planText) {
30612
30715
  return boxStarts.map((box, position) => {
30613
30716
  const nextBoxIndex = boxStarts[position + 1]?.index ?? lines.length;
30614
30717
  let blockEnd = nextBoxIndex;
30615
- for (let index = box.index + 1; index < nextBoxIndex; index++) if (HEADING_REGEX.test(lines[index] ?? "")) {
30616
- blockEnd = index;
30617
- break;
30718
+ for (let index = box.index + 1; index < nextBoxIndex; index++) {
30719
+ if (fenced[index] === true) continue;
30720
+ if (HEADING_REGEX.test(lines[index] ?? "")) {
30721
+ blockEnd = index;
30722
+ break;
30723
+ }
30618
30724
  }
30619
30725
  const title = unwrapEmphasis(box.rawTitle);
30620
30726
  const retireTask = RETIRE_TASK_REGEX.test(title);
30621
30727
  if (box.checked) return {
30728
+ line: box.index,
30622
30729
  checked: true,
30623
30730
  declined: false,
30624
30731
  reason: null,
@@ -30628,6 +30735,7 @@ function parsePlanBoxes(planText) {
30628
30735
  };
30629
30736
  const notes = [];
30630
30737
  for (let index = box.index + 1; index < blockEnd; index++) {
30738
+ if (fenced[index] === true) continue;
30631
30739
  const noteMatch = NOTE_LINE_REGEX.exec(lines[index] ?? "");
30632
30740
  if (noteMatch === null) continue;
30633
30741
  const text = (noteMatch[2] ?? "").trim();
@@ -30638,6 +30746,7 @@ function parsePlanBoxes(planText) {
30638
30746
  }
30639
30747
  let reason = null;
30640
30748
  for (let index = box.index + 1; index < blockEnd; index++) {
30749
+ if (fenced[index] === true) continue;
30641
30750
  const declinedMatch = DECLINED_LINE_REGEX.exec(lines[index] ?? "");
30642
30751
  if (declinedMatch === null) continue;
30643
30752
  const remainder = (declinedMatch[1] ?? "").trim();
@@ -30647,6 +30756,7 @@ function parsePlanBoxes(planText) {
30647
30756
  }
30648
30757
  }
30649
30758
  return {
30759
+ line: box.index,
30650
30760
  checked: false,
30651
30761
  declined: reason !== null,
30652
30762
  reason,
@@ -30657,45 +30767,23 @@ function parsePlanBoxes(planText) {
30657
30767
  });
30658
30768
  }
30659
30769
  //#endregion
30660
- //#region src/plan-workspace/task-label.ts
30661
- /** The line shapes `plan:brief` and `plan:status` both read a task's number from. */
30662
- const FENCE = /^[ \t]*(```|~~~)/;
30663
- const HEADING = /^(#{1,6})[ \t]+(.*)$/;
30664
- const BOX = /^([ \t]*)[-*] \[[ xX]\][ \t]*(.*)$/;
30665
- const LABEL = /^Task[ \t]+([0-9]+)(?![0-9])/;
30666
- /**
30667
- * The `<n>` of a heading or checkbox text that starts `Task <n>` (after an opening `**` or `__`),
30668
- * or null. A number with a leading zero reads as null, because `plan:brief <n>` matches the digits
30669
- * as written and could never find `Task 05`.
30670
- */
30671
- function taskLabelNumber(text) {
30672
- const digits = LABEL.exec(text.replace(/^(\*\*|__)/, ""))?.[1];
30673
- if (digits === void 0 || String(Number(digits)) !== digits) return null;
30674
- return Number(digits);
30675
- }
30676
- //#endregion
30677
30770
  //#region src/plan-workspace/status.ts
30678
30771
  const RETIRED_SEGMENT = /(^|\/)retired\//;
30679
30772
  /**
30680
- * The nearest `Task <n>` label at or before the plan's `ordinal`-th box line. Box lines are counted
30681
- * fence-blind, as `parsePlanBoxes` counts them, so the ordinal lands on the same line; only the
30682
- * label tracking skips fences, as `plan:brief` does.
30773
+ * The nearest `Task <n>` label at or before the box on `line`, the box's own title winning. The
30774
+ * walk shares `fencedLines` with `plan:brief`, so a fenced `Task <n>` line is no more a label here
30775
+ * than it is a task boundary there.
30683
30776
  */
30684
- function enclosingTaskNumber(planText, ordinal) {
30685
- let inFence = false;
30686
- let boxes = -1;
30777
+ function enclosingTaskNumber(planText, line) {
30778
+ const lines = planText.split("\n");
30779
+ const fenced = fencedLines(lines);
30687
30780
  let label = null;
30688
- for (const line of planText.split("\n")) {
30689
- const box = BOX.exec(line);
30690
- if (box !== null) boxes++;
30691
- if (box !== null && boxes === ordinal) return taskLabelNumber(box[2] ?? "") ?? label;
30692
- if (FENCE.test(line)) {
30693
- inFence = !inFence;
30694
- continue;
30695
- }
30696
- if (inFence) continue;
30697
- const text = box?.[2] ?? HEADING.exec(line)?.[2];
30698
- const n = text === void 0 ? null : taskLabelNumber(text);
30781
+ for (let index = 0; index < lines.length; index++) {
30782
+ const text = lines[index] ?? "";
30783
+ if (index === line) return taskLabelNumber(BOX.exec(text)?.[2] ?? "") ?? label;
30784
+ if (fenced[index] === true) continue;
30785
+ const title = BOX.exec(text)?.[2] ?? HEADING.exec(text)?.[2];
30786
+ const n = title === void 0 ? null : taskLabelNumber(title);
30699
30787
  if (n !== null) label = n;
30700
30788
  }
30701
30789
  return null;
@@ -30718,7 +30806,7 @@ function planStatus(planText, planPath) {
30718
30806
  outstanding: open.length,
30719
30807
  declined: boxes.filter((box) => box.declined).length,
30720
30808
  next: nextBox === void 0 || nextBox.title === "" ? null : nextBox.title,
30721
- nextTask: nextBox === void 0 ? null : enclosingTaskNumber(planText, boxes.indexOf(nextBox)),
30809
+ nextTask: nextBox === void 0 ? null : enclosingTaskNumber(planText, nextBox.line),
30722
30810
  onlyRetireLeft: open.length > 0 && open.every((box) => box.retireTask),
30723
30811
  notes
30724
30812
  };
@@ -30735,17 +30823,13 @@ function isTaskLabel(text, n) {
30735
30823
  */
30736
30824
  function extractTask(planText, n) {
30737
30825
  const lines = planText.split("\n");
30738
- let inFence = false;
30826
+ const fenced = fencedLines(lines);
30739
30827
  let start = -1;
30740
30828
  let shape = null;
30741
30829
  let end = lines.length;
30742
30830
  for (let index = 0; index < lines.length; index++) {
30831
+ if (fenced[index] === true) continue;
30743
30832
  const line = lines[index] ?? "";
30744
- if (FENCE.test(line)) {
30745
- inFence = !inFence;
30746
- continue;
30747
- }
30748
- if (inFence) continue;
30749
30833
  const heading = HEADING.exec(line);
30750
30834
  const box = BOX.exec(line);
30751
30835
  if (shape === null) {
@@ -31171,7 +31255,7 @@ setForgejoLogger(log$1);
31171
31255
  * `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
31172
31256
  * can't pass itself off as the published release whose version it carries.
31173
31257
  */
31174
- const displayVersion = "0.84.0";
31258
+ const displayVersion = "0.84.1";
31175
31259
  /**
31176
31260
  * The registered `bst` subcommand map, exported so both the JSON entry path below (resolving a
31177
31261
  * command name before citty parses anything) and the coverage test (enumerating every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.84.0",
3
+ "version": "0.84.1",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {