@basou/cli 0.35.0 → 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 +145 -17
- package/dist/index.js.map +1 -1
- package/dist/program.js +145 -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) {
|
|
@@ -3415,6 +3415,8 @@ async function runImport(adapter, fn) {
|
|
|
3415
3415
|
skippedNoAction: readCount(json.skipped_no_action),
|
|
3416
3416
|
skippedAlreadyImported: readCount(json.skipped_already_imported),
|
|
3417
3417
|
skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
|
|
3418
|
+
skippedDecreased: readCount(json.skipped_decreased),
|
|
3419
|
+
skippedDuplicate: readCount(json.skipped_duplicate),
|
|
3418
3420
|
skippedUnverifiable: readCount(json.skipped_unverifiable),
|
|
3419
3421
|
eventTotal: readCount(json.event_total),
|
|
3420
3422
|
dryRun: json.dry_run === true
|
|
@@ -7596,6 +7598,11 @@ function describeImport(outcome) {
|
|
|
7596
7598
|
if (outcome.skippedAlreadyImported > 0)
|
|
7597
7599
|
parts.push(`${outcome.skippedAlreadyImported} already imported`);
|
|
7598
7600
|
if (outcome.skippedLegacyUntracked > 0) parts.push(`${outcome.skippedLegacyUntracked} legacy`);
|
|
7601
|
+
if (outcome.skippedNoAction > 0) parts.push(`${outcome.skippedNoAction} with no actions`);
|
|
7602
|
+
if (outcome.skippedDecreased > 0) parts.push(`${outcome.skippedDecreased} shrank`);
|
|
7603
|
+
if (outcome.skippedDuplicate > 0) parts.push(`${outcome.skippedDuplicate} duplicated`);
|
|
7604
|
+
if (outcome.skippedUnverifiable > 0)
|
|
7605
|
+
parts.push(`${outcome.skippedUnverifiable} unverifiable (run 'basou verify')`);
|
|
7599
7606
|
return `${outcome.adapter}: ${verb} ${parts.join(", ")}`;
|
|
7600
7607
|
}
|
|
7601
7608
|
function printRefreshSummary(result) {
|
|
@@ -7728,8 +7735,10 @@ import {
|
|
|
7728
7735
|
buildReviewRecordLabel,
|
|
7729
7736
|
createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
|
|
7730
7737
|
findErrorCode as findErrorCode11,
|
|
7738
|
+
findUnbindableRepos,
|
|
7731
7739
|
parseReviewRecordInput,
|
|
7732
7740
|
readManifest as readManifest7,
|
|
7741
|
+
resolveRepoRoot,
|
|
7733
7742
|
sanitizePath as sanitizePath2
|
|
7734
7743
|
} from "@basou/core";
|
|
7735
7744
|
function registerReviewCommand(program2) {
|
|
@@ -7745,6 +7754,8 @@ Input format (a single JSON object describing one review):
|
|
|
7745
7754
|
{
|
|
7746
7755
|
"reviewer": "codex",
|
|
7747
7756
|
"target": "working-tree",
|
|
7757
|
+
"repos": ["~/projects/basou"],
|
|
7758
|
+
"commits": ["a1b2c3d"],
|
|
7748
7759
|
"verdict": "needs-attention",
|
|
7749
7760
|
"findings": [
|
|
7750
7761
|
{ "title": "Off-by-one in pager", "severity": "medium", "location": "src/page.ts:42", "summary": "..." }
|
|
@@ -7754,17 +7765,37 @@ Input format (a single JSON object describing one review):
|
|
|
7754
7765
|
]
|
|
7755
7766
|
}
|
|
7756
7767
|
|
|
7757
|
-
Only "reviewer" and "target" are required;
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
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.
|
|
7764
7795
|
|
|
7765
7796
|
Example (heredoc on stdin):
|
|
7766
7797
|
basou review record <<'JSON'
|
|
7767
|
-
{ "reviewer": "codex", "target": "working-tree", "verdict": "pass", "blocked": [] }
|
|
7798
|
+
{ "reviewer": "codex", "target": "working-tree", "repos": ["~/projects/basou"], "verdict": "pass", "blocked": [] }
|
|
7768
7799
|
JSON
|
|
7769
7800
|
`;
|
|
7770
7801
|
async function runReviewRecord(options, ctx = {}) {
|
|
@@ -7785,6 +7816,8 @@ async function doRunReviewRecord(options, ctx) {
|
|
|
7785
7816
|
await assertWorkspaceInitialized10(paths.root);
|
|
7786
7817
|
const raw = await readReviewInput(options, ctx);
|
|
7787
7818
|
const review = parseReviewRecordInput(raw);
|
|
7819
|
+
assertReposCanBind(review);
|
|
7820
|
+
const reposResolved = (review.repos ?? []).map((r) => resolveRepoRoot(r)).filter((r) => r !== null);
|
|
7788
7821
|
if (options.dryRun === true) {
|
|
7789
7822
|
printReviewPreview(options, review);
|
|
7790
7823
|
return;
|
|
@@ -7808,7 +7841,7 @@ async function doRunReviewRecord(options, ctx) {
|
|
|
7808
7841
|
workingDirectory: repositoryRoot,
|
|
7809
7842
|
invocation: { command: "basou review record", args: invocationArgs },
|
|
7810
7843
|
targetEventBuilders: [
|
|
7811
|
-
(sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review })
|
|
7844
|
+
(sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review, reposResolved })
|
|
7812
7845
|
]
|
|
7813
7846
|
});
|
|
7814
7847
|
printReviewResult(options, {
|
|
@@ -7817,6 +7850,22 @@ async function doRunReviewRecord(options, ctx) {
|
|
|
7817
7850
|
review
|
|
7818
7851
|
});
|
|
7819
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
|
+
}
|
|
7820
7869
|
async function readReviewInput(options, ctx) {
|
|
7821
7870
|
if (options.file !== void 0) {
|
|
7822
7871
|
try {
|
|
@@ -7849,6 +7898,8 @@ function reviewToPayload(review) {
|
|
|
7849
7898
|
reviewer: review.reviewer,
|
|
7850
7899
|
target: review.target
|
|
7851
7900
|
};
|
|
7901
|
+
if (review.repos !== void 0) payload.repos = review.repos;
|
|
7902
|
+
if (review.commits !== void 0) payload.commits = review.commits;
|
|
7852
7903
|
if (review.verdict !== void 0) payload.verdict = review.verdict;
|
|
7853
7904
|
if (review.findings !== void 0) payload.findings = review.findings;
|
|
7854
7905
|
if (review.blocked !== void 0) payload.blocked = review.blocked;
|
|
@@ -7857,6 +7908,9 @@ function reviewToPayload(review) {
|
|
|
7857
7908
|
function reviewSummaryLine(review) {
|
|
7858
7909
|
const parts = [];
|
|
7859
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
|
+
}
|
|
7860
7914
|
if (review.findings !== void 0) {
|
|
7861
7915
|
parts.push(`${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}`);
|
|
7862
7916
|
}
|
|
@@ -7973,19 +8027,45 @@ function relAge(iso, now) {
|
|
|
7973
8027
|
if (hours >= 1) return `${hours}h ago`;
|
|
7974
8028
|
return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
|
|
7975
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
|
+
}
|
|
7976
8056
|
function unitLine(u, now) {
|
|
7977
8057
|
const when = relAge(u.lastCommitAt, now);
|
|
7978
8058
|
const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
|
|
7979
8059
|
if (u.verdict === "near_unbound") {
|
|
7980
8060
|
const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
|
|
7981
|
-
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)}`;
|
|
7982
8062
|
}
|
|
7983
|
-
return `${head} \u2014 no bound cross-model review`;
|
|
8063
|
+
return `${head} \u2014 no bound cross-model review${selfReportSuffix(u, true)}`;
|
|
7984
8064
|
}
|
|
7985
8065
|
function candidateLine(u, now) {
|
|
7986
8066
|
const when = relAge(u.lastCommitAt, now);
|
|
7987
8067
|
const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
|
|
7988
|
-
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)}`;
|
|
7989
8069
|
}
|
|
7990
8070
|
function renderReviewGaps(summary) {
|
|
7991
8071
|
const now = new Date(summary.generatedAt);
|
|
@@ -7993,7 +8073,12 @@ function renderReviewGaps(summary) {
|
|
|
7993
8073
|
const scope = summary.scope ? summary.scope.join(", ") : "all repositories";
|
|
7994
8074
|
lines.push(`# Review-trail gaps (${scope})`);
|
|
7995
8075
|
lines.push("");
|
|
7996
|
-
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) {
|
|
7997
8082
|
lines.push("\u2705 Within the captured range, no unit of work landed without a review trail.");
|
|
7998
8083
|
} else {
|
|
7999
8084
|
lines.push(`\u26A0\uFE0F Units of work that landed without a review trail: ${summary.gaps.length}`);
|
|
@@ -8010,22 +8095,65 @@ function renderReviewGaps(summary) {
|
|
|
8010
8095
|
if (summary.unknowns.length > 0) {
|
|
8011
8096
|
const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
|
|
8012
8097
|
lines.push(
|
|
8013
|
-
`## 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`
|
|
8014
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
|
+
}
|
|
8015
8105
|
lines.push("");
|
|
8016
8106
|
}
|
|
8017
8107
|
lines.push("## By repository");
|
|
8018
8108
|
for (const r of summary.repos) {
|
|
8019
8109
|
lines.push(
|
|
8020
|
-
`- ${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}` : ""})`
|
|
8021
8111
|
);
|
|
8022
8112
|
}
|
|
8023
8113
|
lines.push("");
|
|
8024
8114
|
lines.push(
|
|
8025
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.`
|
|
8026
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
|
+
}
|
|
8027
8128
|
return lines.join("\n");
|
|
8028
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
|
+
}
|
|
8029
8157
|
|
|
8030
8158
|
// src/commands/run.ts
|
|
8031
8159
|
import { mkdir as mkdir2 } from "fs/promises";
|