@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/index.js +138 -17
- package/dist/index.js.map +1 -1
- package/dist/program.js +138 -17
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3253,8 +3253,8 @@ function buildNoteEvent(input) {
|
|
|
3253
3253
|
};
|
|
3254
3254
|
}
|
|
3255
3255
|
function buildAdHocLabel2(body) {
|
|
3256
|
-
const
|
|
3257
|
-
const truncated =
|
|
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(program2) {
|
|
@@ -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;
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
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";
|