@basou/cli 0.35.1 → 0.36.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.
package/dist/program.js CHANGED
@@ -3253,8 +3253,8 @@ function buildNoteEvent(input) {
3253
3253
  };
3254
3254
  }
3255
3255
  function buildAdHocLabel2(body) {
3256
- const oneLine = body.replace(/\s+/g, " ").trim();
3257
- const truncated = oneLine.length > LABEL_BODY_MAX ? `${oneLine.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine;
3256
+ const oneLine2 = body.replace(/\s+/g, " ").trim();
3257
+ const truncated = oneLine2.length > LABEL_BODY_MAX ? `${oneLine2.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine2;
3258
3258
  return `Ad-hoc note: ${truncated}`;
3259
3259
  }
3260
3260
  function parseBody(raw) {
@@ -7735,8 +7735,10 @@ import {
7735
7735
  buildReviewRecordLabel,
7736
7736
  createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
7737
7737
  findErrorCode as findErrorCode11,
7738
+ findUnbindableRepos,
7738
7739
  parseReviewRecordInput,
7739
7740
  readManifest as readManifest7,
7741
+ resolveRepoRoot,
7740
7742
  sanitizePath as sanitizePath2
7741
7743
  } from "@basou/core";
7742
7744
  function registerReviewCommand(program) {
@@ -7752,6 +7754,8 @@ Input format (a single JSON object describing one review):
7752
7754
  {
7753
7755
  "reviewer": "codex",
7754
7756
  "target": "working-tree",
7757
+ "repos": ["~/projects/basou"],
7758
+ "commits": ["a1b2c3d"],
7755
7759
  "verdict": "needs-attention",
7756
7760
  "findings": [
7757
7761
  { "title": "Off-by-one in pager", "severity": "medium", "location": "src/page.ts:42", "summary": "..." }
@@ -7761,17 +7765,37 @@ Input format (a single JSON object describing one review):
7761
7765
  ]
7762
7766
  }
7763
7767
 
7764
- Only "reviewer" and "target" are required; verdict / findings / blocked are
7765
- optional. Record blocked findings (spec-deviation / design-reversal) here so the
7766
- adversarial-review protocol's "always report what you blocked" becomes a durable
7767
- trail artifact -- an explicit empty "blocked": [] is encouraged to record that
7768
- you blocked nothing. The review is written into one ad-hoc session timestamped
7769
- now. Run from a workspace-view directory and it resolves to the planning repo,
7770
- like 'basou decision capture' / 'basou note'.
7768
+ Only "reviewer" and "target" are required; the rest are optional. Record blocked
7769
+ findings (spec-deviation / design-reversal) here so the adversarial-review
7770
+ protocol's "always report what you blocked" becomes a durable trail artifact --
7771
+ an explicit empty "blocked": [] is encouraged to record that you blocked
7772
+ nothing. The review is written into one ad-hoc session timestamped now. Run from
7773
+ a workspace-view directory and it resolves to the planning repo, like
7774
+ 'basou decision capture' / 'basou note'.
7775
+
7776
+ Name the repositories you reviewed in "repos". The record lands in the planning
7777
+ repo, so that field is the only thing tying it to the repo under review: without
7778
+ it, 'basou review-gaps' cannot surface this record against the work it covered.
7779
+ Each entry must be an absolute path (or ~/...) to a repository ROOT; a relative
7780
+ path, a path that is not there, or a subdirectory is rejected outright, because
7781
+ it would store a record that can never appear against any work. "commits" is
7782
+ kept as your claim about coverage and is never used to bind.
7783
+
7784
+ A record is paired with work only while that repository is still present on the
7785
+ machine. If it moves or goes away, 'basou review-gaps' reports the record as
7786
+ unverifiable rather than pairing it on a matching path string -- it cannot
7787
+ confirm the two name the same repository, and a guess about whether a review
7788
+ happened is worse than saying so.
7789
+
7790
+ A recorded review is a self-report -- review-gaps labels the unit but does not
7791
+ change what it is, because nothing corroborates the claim: a unit that was a gap
7792
+ is still a gap, and one that already had a review trace is still a candidate.
7793
+ Recording after the commit is fine: the record is still shown, marked as written
7794
+ after the fact.
7771
7795
 
7772
7796
  Example (heredoc on stdin):
7773
7797
  basou review record <<'JSON'
7774
- { "reviewer": "codex", "target": "working-tree", "verdict": "pass", "blocked": [] }
7798
+ { "reviewer": "codex", "target": "working-tree", "repos": ["~/projects/basou"], "verdict": "pass", "blocked": [] }
7775
7799
  JSON
7776
7800
  `;
7777
7801
  async function runReviewRecord(options, ctx = {}) {
@@ -7792,6 +7816,8 @@ async function doRunReviewRecord(options, ctx) {
7792
7816
  await assertWorkspaceInitialized10(paths.root);
7793
7817
  const raw = await readReviewInput(options, ctx);
7794
7818
  const review = parseReviewRecordInput(raw);
7819
+ assertReposCanBind(review);
7820
+ const reposResolved = (review.repos ?? []).map((r) => resolveRepoRoot(r)).filter((r) => r !== null);
7795
7821
  if (options.dryRun === true) {
7796
7822
  printReviewPreview(options, review);
7797
7823
  return;
@@ -7815,7 +7841,7 @@ async function doRunReviewRecord(options, ctx) {
7815
7841
  workingDirectory: repositoryRoot,
7816
7842
  invocation: { command: "basou review record", args: invocationArgs },
7817
7843
  targetEventBuilders: [
7818
- (sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review })
7844
+ (sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review, reposResolved })
7819
7845
  ]
7820
7846
  });
7821
7847
  printReviewResult(options, {
@@ -7824,6 +7850,22 @@ async function doRunReviewRecord(options, ctx) {
7824
7850
  review
7825
7851
  });
7826
7852
  }
7853
+ var REPO_PROBLEM_HINT = {
7854
+ relative: "use an absolute path (or ~/...) to the repository root",
7855
+ absent: "no such path on this machine",
7856
+ not_a_repo_root: "that path is not a repository root"
7857
+ };
7858
+ function assertReposCanBind(review) {
7859
+ const entries = review.repos ?? [];
7860
+ const unbindable = findUnbindableRepos(entries);
7861
+ if (unbindable.length === 0) return;
7862
+ const detail = unbindable.map(({ index, problem }) => ` repos[${index}] \u2014 ${REPO_PROBLEM_HINT[problem]}`).join("\n");
7863
+ throw new Error(
7864
+ `${unbindable.length} of ${review.repos?.length} 'repos' entr${unbindable.length === 1 ? "y" : "ies"} cannot be bound to a repository:
7865
+ ${detail}
7866
+ 'repos' is what ties this record to the repository it reviewed, so an entry that resolves to nothing would leave the record stored but invisible.`
7867
+ );
7868
+ }
7827
7869
  async function readReviewInput(options, ctx) {
7828
7870
  if (options.file !== void 0) {
7829
7871
  try {
@@ -7856,6 +7898,8 @@ function reviewToPayload(review) {
7856
7898
  reviewer: review.reviewer,
7857
7899
  target: review.target
7858
7900
  };
7901
+ if (review.repos !== void 0) payload.repos = review.repos;
7902
+ if (review.commits !== void 0) payload.commits = review.commits;
7859
7903
  if (review.verdict !== void 0) payload.verdict = review.verdict;
7860
7904
  if (review.findings !== void 0) payload.findings = review.findings;
7861
7905
  if (review.blocked !== void 0) payload.blocked = review.blocked;
@@ -7864,6 +7908,9 @@ function reviewToPayload(review) {
7864
7908
  function reviewSummaryLine(review) {
7865
7909
  const parts = [];
7866
7910
  if (review.verdict !== void 0) parts.push(`verdict: ${review.verdict}`);
7911
+ if (review.repos !== void 0) {
7912
+ parts.push(`${review.repos.length} repo${review.repos.length === 1 ? "" : "s"}`);
7913
+ }
7867
7914
  if (review.findings !== void 0) {
7868
7915
  parts.push(`${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}`);
7869
7916
  }
@@ -7980,19 +8027,45 @@ function relAge(iso, now) {
7980
8027
  if (hours >= 1) return `${hours}h ago`;
7981
8028
  return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
7982
8029
  }
8030
+ function flatten(value) {
8031
+ return value.replace(/\s+/gu, " ").trim();
8032
+ }
8033
+ var GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
8034
+ function graphemes(value) {
8035
+ return [...GRAPHEMES.segment(value)].map((s) => s.segment);
8036
+ }
8037
+ function oneLine(value, max) {
8038
+ const flat = graphemes(flatten(value));
8039
+ return flat.length > max ? `${flat.slice(0, max - 1).join("")}\u2026` : flat.join("");
8040
+ }
8041
+ function claimedCommits(commits) {
8042
+ if (commits.length === 0) return "";
8043
+ const shown = commits.slice(0, 3).map((c) => graphemes(flatten(c)).slice(0, 8).join(""));
8044
+ return ` claiming ${shown.join(", ")}${commits.length > shown.length ? ", ..." : ""}`;
8045
+ }
8046
+ var SELF_REPORTS_SHOWN = 3;
8047
+ function selfReportSuffix(u, stillCounted) {
8048
+ if (u.selfReports.length === 0) return "";
8049
+ const parts = u.selfReports.slice(0, SELF_REPORTS_SHOWN).map(
8050
+ (r) => `${oneLine(r.reviewer, 40)}${claimedCommits(r.commits)}${r.recordedAfterCommit ? " (recorded after the commit)" : ""}`
8051
+ );
8052
+ const rest = u.selfReports.length - parts.length;
8053
+ if (rest > 0) parts.push(`+${rest} more`);
8054
+ return ` \xB7 self-reported by ${parts.join("; ")} \u2014 ${stillCounted ? "unverified, still counted" : "unverified"}`;
8055
+ }
7983
8056
  function unitLine(u, now) {
7984
8057
  const when = relAge(u.lastCommitAt, now);
7985
8058
  const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
7986
8059
  if (u.verdict === "near_unbound") {
7987
8060
  const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
7988
- return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]`;
8061
+ return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]${selfReportSuffix(u, true)}`;
7989
8062
  }
7990
- return `${head} \u2014 no bound cross-model review`;
8063
+ return `${head} \u2014 no bound cross-model review${selfReportSuffix(u, true)}`;
7991
8064
  }
7992
8065
  function candidateLine(u, now) {
7993
8066
  const when = relAge(u.lastCommitAt, now);
7994
8067
  const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
7995
- return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}`;
8068
+ return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}${selfReportSuffix(u, false)}`;
7996
8069
  }
7997
8070
  function renderReviewGaps(summary) {
7998
8071
  const now = new Date(summary.generatedAt);
@@ -8000,7 +8073,12 @@ function renderReviewGaps(summary) {
8000
8073
  const scope = summary.scope ? summary.scope.join(", ") : "all repositories";
8001
8074
  lines.push(`# Review-trail gaps (${scope})`);
8002
8075
  lines.push("");
8003
- if (summary.gaps.length === 0) {
8076
+ if (summary.gaps.length === 0 && summary.unknowns.length > 0) {
8077
+ const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
8078
+ lines.push(
8079
+ `\u26A0\uFE0F No unit of work was found without a review trail \u2014 but ${n} captured commit${n === 1 ? "" : "s"} could not be placed in a repository at all, so ${n === 1 ? "it was" : "they were"} never checked (see below).`
8080
+ );
8081
+ } else if (summary.gaps.length === 0) {
8004
8082
  lines.push("\u2705 Within the captured range, no unit of work landed without a review trail.");
8005
8083
  } else {
8006
8084
  lines.push(`\u26A0\uFE0F Units of work that landed without a review trail: ${summary.gaps.length}`);
@@ -8017,22 +8095,65 @@ function renderReviewGaps(summary) {
8017
8095
  if (summary.unknowns.length > 0) {
8018
8096
  const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
8019
8097
  lines.push(
8020
- `## Undeterminable (${summary.unknowns.length} unit${summary.unknowns.length === 1 ? "" : "s"} / ${n} commit${n === 1 ? "" : "s"}) \u2014 repo or timestamp could not be derived from capture; verdict withheld (not a clear)`
8098
+ `## Undeterminable (${summary.unknowns.length} unit${summary.unknowns.length === 1 ? "" : "s"} / ${n} commit${n === 1 ? "" : "s"}) \u2014 repo or timestamp could not be derived from capture; verdict withheld (not a clear). Belongs to no repository, so this is listed even under --repo`
8021
8099
  );
8100
+ for (const u of summary.unknowns) {
8101
+ lines.push(
8102
+ `- ${relAge(u.lastCommitAt, now)} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) [${u.sessionId}]`
8103
+ );
8104
+ }
8022
8105
  lines.push("");
8023
8106
  }
8024
8107
  lines.push("## By repository");
8025
8108
  for (const r of summary.repos) {
8026
8109
  lines.push(
8027
- `- ${r.repo}: ${r.units} unit${r.units === 1 ? "" : "s"} (no trail ${r.omissionUnits} / nearby only ${r.nearUnboundUnits} / to confirm ${r.candidateUnits}${r.unknownUnits > 0 ? ` / unknown ${r.unknownUnits}` : ""})`
8110
+ `- ${r.repo}: ${r.units} unit${r.units === 1 ? "" : "s"} (no trail ${r.omissionUnits} / nearby only ${r.nearUnboundUnits} / to confirm ${r.candidateUnits}${r.unknownUnits > 0 ? ` / unknown ${r.unknownUnits}` : ""}${r.selfReportedGapUnits > 0 ? ` / self-reported ${r.selfReportedGapUnits}` : ""})`
8028
8111
  );
8029
8112
  }
8030
8113
  lines.push("");
8031
8114
  lines.push(
8032
8115
  `Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" : relAge(summary.newestCommitAt, now)}). It never auto-judges that a review "happened", and temporal proximity alone is not a pass. It does not enforce.`
8033
8116
  );
8117
+ if (summary.gaps.some((u) => u.selfReports.length > 0)) {
8118
+ lines.push(
8119
+ 'Note: a "self-reported" unit has a `basou review record` naming this repo, but nothing corroborates it \u2014 it stays in the count above, because an empty record must not be a way to make the number go down.'
8120
+ );
8121
+ }
8122
+ lines.push(...unattachedLines(summary.unattachedSelfReports));
8123
+ if (summary.refusedPairings > 0) {
8124
+ lines.push(
8125
+ `Note: ${summary.refusedPairings} pairing${summary.refusedPairings === 1 ? "" : "s"} between a recorded review and captured work could not be checked, because that work's own repository path was never verified.`
8126
+ );
8127
+ }
8034
8128
  return lines.join("\n");
8035
8129
  }
8130
+ function unattachedLines(u) {
8131
+ if (u.total === 0) return [];
8132
+ const lines = [
8133
+ `Note: ${u.total} recorded review${u.total === 1 ? "" : "s"} changed nothing in this report (counted across all repositories, not just any --repo scope):`
8134
+ ];
8135
+ if (u.noRepos > 0) {
8136
+ lines.push(
8137
+ ` ${u.noRepos} named no repository \u2014 add \`repos\` to the record; without it the record's own location is the planning repo, not the repo reviewed`
8138
+ );
8139
+ }
8140
+ if (u.unresolvableRepo > 0) {
8141
+ lines.push(
8142
+ ` ${u.unresolvableRepo} named a path that is not a repository root on this machine \u2014 unverifiable, so not paired with any work`
8143
+ );
8144
+ }
8145
+ if (u.noMatchingUnit > 0) {
8146
+ lines.push(
8147
+ ` ${u.noMatchingUnit} named a repository, but no captured unit of work fell within the window of it`
8148
+ );
8149
+ }
8150
+ if (u.unverifiableUnit > 0) {
8151
+ lines.push(
8152
+ ` ${u.unverifiableUnit} fell within the window of captured work, but that work's own repository path could not be verified \u2014 the pairing could not be checked either way`
8153
+ );
8154
+ }
8155
+ return lines;
8156
+ }
8036
8157
 
8037
8158
  // src/commands/run.ts
8038
8159
  import { mkdir as mkdir2 } from "fs/promises";