@verevoir/review 0.3.2 → 0.3.3
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/README.md +26 -87
- package/dist/cli/footer.d.ts +13 -1
- package/dist/cli/footer.js +17 -1
- package/dist/cli/review.js +5 -2
- package/dist/context-files.d.ts +26 -0
- package/dist/context-files.js +113 -0
- package/dist/prompt.d.ts +8 -0
- package/dist/prompt.js +5 -0
- package/dist/run-review.d.ts +6 -0
- package/dist/run-review.js +13 -1
- package/dist/tier.d.ts +5 -0
- package/dist/tier.js +2 -1
- package/package.json +1 -2
- package/src/cli/footer.ts +24 -0
- package/src/cli/review.ts +11 -2
- package/src/context-files.ts +111 -0
- package/src/prompt.ts +14 -0
- package/src/run-review.ts +19 -1
- package/src/tier.ts +6 -0
- package/npm-shrinkwrap.json +0 -1679
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @verevoir/review
|
|
2
2
|
|
|
3
|
+
**Adopting it? Start with [docs/ADOPTING.md](docs/ADOPTING.md)**: review a change locally in one
|
|
4
|
+
command, or add it as a pre-push gate or a GitHub review stage.
|
|
5
|
+
|
|
3
6
|
The review panel: lens-based diff review, fanned out across every declared
|
|
4
7
|
lens with a shared cached prefix, aggregated into one verdict with a
|
|
5
8
|
fail-closed outcome rule. One implementation, consumed by pump-internal
|
|
@@ -103,90 +106,20 @@ never folded into a generic failure.
|
|
|
103
106
|
|
|
104
107
|
## Consuming this library
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
never
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`main` moving -- a consumer floating on `main` would pick up a change
|
|
121
|
-
with no review of its own on this side of the boundary. Pin the exact
|
|
122
|
-
sha you have actually run the ratchet against (or, for a mechanical
|
|
123
|
-
bump with no behaviour change, the exact sha you have confirmed
|
|
124
|
-
carries one).
|
|
125
|
-
|
|
126
|
-
2. **Add `REVIEW_READ_TOKEN` as a repository secret** in the consumer's
|
|
127
|
-
own CI -- a fine-grained PAT scoped to `contents: read` on
|
|
128
|
-
`verevoir/review` ONLY. This repository is private, and a consumer's
|
|
129
|
-
ambient `GITHUB_TOKEN` cannot see a different private repository.
|
|
130
|
-
`REVIEW_READ_TOKEN` is the operator's standing secret name for every
|
|
131
|
-
consumer of this library -- used for nothing else, so it can stay
|
|
132
|
-
scoped this narrowly without colliding with any other credential a
|
|
133
|
-
consumer's CI might hold.
|
|
134
|
-
|
|
135
|
-
3. **Before `npm ci` (or equivalent), rewrite git URLs for this
|
|
136
|
-
dependency's exact URL only** -- never every `https://github.com/`
|
|
137
|
-
URL -- then run the install with NO lifecycle scripts, remove the
|
|
138
|
-
rewrite immediately, and only then run the scripts that were skipped:
|
|
139
|
-
|
|
140
|
-
```yaml
|
|
141
|
-
- run: |
|
|
142
|
-
if [ -z "${{ secrets.REVIEW_READ_TOKEN }}" ]; then
|
|
143
|
-
echo "::error::REVIEW_READ_TOKEN is not set (or is blank) -- required to install the private @verevoir/review git dependency over https."
|
|
144
|
-
exit 1
|
|
145
|
-
fi
|
|
146
|
-
- run: git config --global url."https://x-access-token:${{ secrets.REVIEW_READ_TOKEN }}@github.com/verevoir/review.git".insteadOf "https://github.com/verevoir/review.git"
|
|
147
|
-
- run: npm ci --ignore-scripts
|
|
148
|
-
- run: git config --global --unset url."https://x-access-token:${{ secrets.REVIEW_READ_TOKEN }}@github.com/verevoir/review.git".insteadOf
|
|
149
|
-
if: always()
|
|
150
|
-
- run: npm rebuild
|
|
151
|
-
- run: npm run build --prefix node_modules/@verevoir/review
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Why `--ignore-scripts` plus the two steps after, not just the scoped
|
|
155
|
-
rewrite and an unset** -- an earlier version of this guidance claimed
|
|
156
|
-
scoping the rewrite to one URL meant the token was never exposed to
|
|
157
|
-
"any other dependency's install script". That was wrong, caught by a
|
|
158
|
-
lens-review round on a real consumer's cutover branch, scored
|
|
159
|
-
blocking: URL scoping only limits which git operations the rewrite
|
|
160
|
-
applies to. It does nothing to stop a DIFFERENT package's own
|
|
161
|
-
install/postinstall script from reading `~/.gitconfig` directly, as a
|
|
162
|
-
file -- any process in the job can do that regardless of which URL
|
|
163
|
-
the rewrite names. `--ignore-scripts` is what actually closes this:
|
|
164
|
-
no package's lifecycle script runs while the token is present. The
|
|
165
|
-
rewrite is then unset (`if: always()`, so a failed `npm ci` does not
|
|
166
|
-
leave it behind either), and only then do the deferred scripts run --
|
|
167
|
-
`npm rebuild` for every other dependency's own install/postinstall
|
|
168
|
-
(native modules among them, if your consumer has any), and an
|
|
169
|
-
explicit build of **this package's own** `dist/` specifically,
|
|
170
|
-
because it is gitignored here and normally produced by this
|
|
171
|
-
package's `prepare` script -- which `--ignore-scripts` also skipped,
|
|
172
|
-
and which `npm rebuild` does not run (rebuild covers preinstall/
|
|
173
|
-
install/postinstall only, never prepare).
|
|
174
|
-
|
|
175
|
-
**The precise claim this now supports**: the token exists in
|
|
176
|
-
`~/.gitconfig` only across the url.insteadOf write and
|
|
177
|
-
`npm ci --ignore-scripts` itself -- an install that runs no package
|
|
178
|
-
script. Nothing after that point can read it. The one residual,
|
|
179
|
-
accepted exposure is whatever npm does internally to fetch the git
|
|
180
|
-
dependency during that one command -- unavoidable, since that is the
|
|
181
|
-
minimum npm needs to resolve a private git dependency at all.
|
|
182
|
-
|
|
183
|
-
Run this on every CI leg/matrix entry that installs dependencies -- a
|
|
184
|
-
leg that skips it fails `npm ci` outright and loudly (a missing
|
|
185
|
-
credential), never silently.
|
|
186
|
-
|
|
187
|
-
This is the exact mechanism in place on `aigency-runtime-v2`'s own
|
|
188
|
-
`verify.yml`, on its consumer-cutover branch (held for operator review,
|
|
189
|
-
per that repository's own protected-paths rule).
|
|
109
|
+
`@verevoir/review` is **public on npm**. Install it from the registry, pinned to an exact version,
|
|
110
|
+
never a range or a dist-tag:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
npm install --save-exact @verevoir/review@0.3.2
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The package carries its own `dist/`, so no build step is needed. To run the command line without
|
|
117
|
+
installing anything, see "Command line" below and [docs/ADOPTING.md](docs/ADOPTING.md).
|
|
118
|
+
|
|
119
|
+
A pinned version matters because this repo merges on green (see "Merge policy" below), with no
|
|
120
|
+
human gate between a PR and `main` moving: a consumer floating on a range would pick up a change
|
|
121
|
+
with no review of its own on this side of the boundary. Pin the exact version you have actually
|
|
122
|
+
run the ratchet against.
|
|
190
123
|
|
|
191
124
|
## Round-aware review (opt-in)
|
|
192
125
|
|
|
@@ -304,8 +237,8 @@ blocking findings before calling `runPanel`/`runReviewNode` again.
|
|
|
304
237
|
public one that cannot call governance's private reusable workflow:
|
|
305
238
|
|
|
306
239
|
```sh
|
|
307
|
-
npx -y @verevoir/review aigency-review --base origin/main --title "My change"
|
|
308
|
-
git diff origin/main...HEAD | npx -y @verevoir/review aigency-review
|
|
240
|
+
npx -y -p @verevoir/review@0.3.2 aigency-review --base origin/main --title "My change"
|
|
241
|
+
git diff origin/main...HEAD | npx -y -p @verevoir/review@0.3.2 aigency-review
|
|
309
242
|
```
|
|
310
243
|
|
|
311
244
|
**Flags**
|
|
@@ -433,11 +366,17 @@ v2); no YAML library.
|
|
|
433
366
|
- uses: actions/checkout@v4
|
|
434
367
|
with:
|
|
435
368
|
fetch-depth: 0
|
|
436
|
-
- run: npx -y @verevoir/review
|
|
369
|
+
- run: npx -y -p @verevoir/review@0.3.2 aigency-review --base "origin/${BASE_REF}" --title "${PR_TITLE}"
|
|
437
370
|
env:
|
|
438
371
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
372
|
+
BASE_REF: ${{ github.base_ref }}
|
|
373
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
439
374
|
```
|
|
440
375
|
|
|
376
|
+
Values reach the shell through `env:`, never through `${{ }}` inside `run:`. For the hardened,
|
|
377
|
+
copy-and-use version (SHA-pinned actions, minimal permissions, check results, a corpus) see
|
|
378
|
+
[docs/ADOPTING.md](docs/ADOPTING.md) and `examples/github/review.yml`.
|
|
379
|
+
|
|
441
380
|
## Merge policy
|
|
442
381
|
|
|
443
382
|
Merges on green, same as this organisation's other rebuild repos. "Green"
|
package/dist/cli/footer.d.ts
CHANGED
|
@@ -6,11 +6,23 @@ export declare function modelText(requested: string, calls: readonly RawCall[]):
|
|
|
6
6
|
export declare function tierLine(downgraded: number): string | undefined;
|
|
7
7
|
/** The one line saying how many smells tier enforcement promoted to [blocking]; none for 0. */
|
|
8
8
|
export declare function promotedLine(promoted: number): string | undefined;
|
|
9
|
+
/** Said when Reject promotion was switched off because every changed file is documentation. */
|
|
10
|
+
export declare const PROMOTION_OFF_LINE = "Tier: promotion off: every changed file is documentation";
|
|
11
|
+
/** Said when the spec framing was added but there is no tier, so no promotion to switch off. */
|
|
12
|
+
export declare const SPEC_FRAMING_LINE = "Spec framing: every changed file is documentation (no tier signals, so promotion is not in play)";
|
|
13
|
+
/** Said when a changed path could not be determined, so the diff is not treated as documentation. */
|
|
14
|
+
export declare const PATH_UNDETERMINED_LINE = "Documentation check: a changed path could not be determined, so the diff is not treated as documentation (promotion stays on)";
|
|
15
|
+
/** The one line saying how the documentation check affected this review; none when it did not. */
|
|
16
|
+
export declare function docsLine(result: {
|
|
17
|
+
readonly promotionOff: boolean;
|
|
18
|
+
readonly docsOnly: boolean;
|
|
19
|
+
readonly pathUndetermined: boolean;
|
|
20
|
+
}): string | undefined;
|
|
9
21
|
/**
|
|
10
22
|
* The temperature the judging used. The claude-cli route has no temperature option
|
|
11
23
|
* (`claude -p` documents no flag for it), so a requested one is said not to apply.
|
|
12
24
|
*/
|
|
13
25
|
export declare function temperatureLine(route: RouteName, temperature: number | undefined): string;
|
|
14
|
-
export declare function renderFooter(requested: string, route: RouteName, calls: readonly RawCall[], downgraded?: number, promoted?: number, temperature?: number): string;
|
|
26
|
+
export declare function renderFooter(requested: string, route: RouteName, calls: readonly RawCall[], downgraded?: number, promoted?: number, temperature?: number, docsNote?: string): string;
|
|
15
27
|
/** One stderr line per lens. */
|
|
16
28
|
export declare function usageLines(calls: readonly RawCall[]): string[];
|
package/dist/cli/footer.js
CHANGED
|
@@ -42,6 +42,20 @@ export function promotedLine(promoted) {
|
|
|
42
42
|
? `Tier: ${String(promoted)} finding(s) promoted: quotes a Reject signal`
|
|
43
43
|
: undefined;
|
|
44
44
|
}
|
|
45
|
+
/** Said when Reject promotion was switched off because every changed file is documentation. */
|
|
46
|
+
export const PROMOTION_OFF_LINE = 'Tier: promotion off: every changed file is documentation';
|
|
47
|
+
/** Said when the spec framing was added but there is no tier, so no promotion to switch off. */
|
|
48
|
+
export const SPEC_FRAMING_LINE = 'Spec framing: every changed file is documentation (no tier signals, so promotion is not in play)';
|
|
49
|
+
/** Said when a changed path could not be determined, so the diff is not treated as documentation. */
|
|
50
|
+
export const PATH_UNDETERMINED_LINE = 'Documentation check: a changed path could not be determined, so the diff is not treated as documentation (promotion stays on)';
|
|
51
|
+
/** The one line saying how the documentation check affected this review; none when it did not. */
|
|
52
|
+
export function docsLine(result) {
|
|
53
|
+
if (result.promotionOff)
|
|
54
|
+
return PROMOTION_OFF_LINE;
|
|
55
|
+
if (result.docsOnly)
|
|
56
|
+
return SPEC_FRAMING_LINE;
|
|
57
|
+
return result.pathUndetermined ? PATH_UNDETERMINED_LINE : undefined;
|
|
58
|
+
}
|
|
45
59
|
/**
|
|
46
60
|
* The temperature the judging used. The claude-cli route has no temperature option
|
|
47
61
|
* (`claude -p` documents no flag for it), so a requested one is said not to apply.
|
|
@@ -53,7 +67,7 @@ export function temperatureLine(route, temperature) {
|
|
|
53
67
|
? 'temperature: not supported on the claude-cli route'
|
|
54
68
|
: `temperature: ${String(temperature)}`;
|
|
55
69
|
}
|
|
56
|
-
export function renderFooter(requested, route, calls, downgraded = 0, promoted = 0, temperature) {
|
|
70
|
+
export function renderFooter(requested, route, calls, downgraded = 0, promoted = 0, temperature, docsNote) {
|
|
57
71
|
const lines = [
|
|
58
72
|
'---',
|
|
59
73
|
'',
|
|
@@ -67,6 +81,8 @@ export function renderFooter(requested, route, calls, downgraded = 0, promoted =
|
|
|
67
81
|
const promotion = promotedLine(promoted);
|
|
68
82
|
if (promotion !== undefined)
|
|
69
83
|
lines.push('', promotion);
|
|
84
|
+
if (docsNote !== undefined)
|
|
85
|
+
lines.push('', docsNote);
|
|
70
86
|
if (calls.length > 0) {
|
|
71
87
|
lines.push('', 'Per-lens usage and wall time:', '');
|
|
72
88
|
for (const call of byLens(calls)) {
|
package/dist/cli/review.js
CHANGED
|
@@ -24,7 +24,7 @@ import { buildPrompt, renderPrompt } from '../prompt.js';
|
|
|
24
24
|
import { deriveLensSet, extractBar } from '../practice-lenses.js';
|
|
25
25
|
import { buildFileContext, NO_FILE_CONTEXT_NOTE } from '../context-files.js';
|
|
26
26
|
import { buildReferences } from '../references.js';
|
|
27
|
-
import { promotedLine, renderFooter, temperatureLine, tierLine, usageLines } from './footer.js';
|
|
27
|
+
import { docsLine, promotedLine, renderFooter, temperatureLine, tierLine, usageLines, } from './footer.js';
|
|
28
28
|
/** Governance's pinned default, sent to both routes. */
|
|
29
29
|
export const DEFAULT_MODEL = 'claude-opus-5-5';
|
|
30
30
|
/** Governance's default output-token budget (billed route only). */
|
|
@@ -515,10 +515,13 @@ export async function main(argv, env, io, deps) {
|
|
|
515
515
|
const promotion = promotedLine(result.promoted);
|
|
516
516
|
if (promotion !== undefined)
|
|
517
517
|
io.stderr(`${promotion}\n`);
|
|
518
|
+
const docsNote = docsLine(result);
|
|
519
|
+
if (docsNote !== undefined)
|
|
520
|
+
io.stderr(`${docsNote}\n`);
|
|
518
521
|
// Audit trail for tier decisions behind merged groups: stderr, not the report (see dedup.ts).
|
|
519
522
|
for (const line of result.dedupAudit)
|
|
520
523
|
io.stderr(line);
|
|
521
|
-
const footer = `\n\n${renderFooter(model, route, calls, result.downgraded, result.promoted, temperature)}`;
|
|
524
|
+
const footer = `\n\n${renderFooter(model, route, calls, result.downgraded, result.promoted, temperature, docsNote)}`;
|
|
522
525
|
const note = derived !== undefined && derived.untagged.length > 0
|
|
523
526
|
? `\n\n> ${String(derived.untagged.length)} practice(s) had no "lens:" tag and were ` +
|
|
524
527
|
`not reviewed: ${derived.untagged.join(', ')}`
|
package/dist/context-files.d.ts
CHANGED
|
@@ -29,6 +29,32 @@ export interface DiffPath {
|
|
|
29
29
|
* path so the caller can say it was skipped.
|
|
30
30
|
*/
|
|
31
31
|
export declare function diffPaths(diff: string): DiffPath[];
|
|
32
|
+
/**
|
|
33
|
+
* EVERY path a diff changes, for deciding what kind of change it is (not for reading files).
|
|
34
|
+
* Unlike diffPaths it also sees what git writes WITHOUT `---`/`+++` headers: a pure rename
|
|
35
|
+
* (`rename from` / `rename to`), a copy, a binary change (`Binary files A and B differ`) and a
|
|
36
|
+
* mode-only change (the `diff --git` line itself). Both sides of a rename and the old path of a
|
|
37
|
+
* deletion are included, so moving code into a .md file is not a documentation change. A path
|
|
38
|
+
* that cannot be determined is reported as '' so the caller treats the diff as not understood.
|
|
39
|
+
* A diff with no `diff --git` lines (plain unified) falls back to diffPaths.
|
|
40
|
+
*/
|
|
41
|
+
export declare function changedPaths(diff: string): string[];
|
|
42
|
+
/** Documentation by FILE TYPE (.md .mdx .rst .adoc, any depth; not .txt, which also names requirements.txt and CMakeLists.txt); a directory name alone is not enough. */
|
|
43
|
+
export declare function isDocumentationPath(path: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* True when the diff changes at least one file and EVERY changed path is documentation. Drives
|
|
46
|
+
* the tier (promotion off) and the spec framing. Paths come from changedPaths; one that cannot
|
|
47
|
+
* be determined makes the diff NOT docs-only, so promotion stays on.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isDocumentationOnly(diff: string): boolean;
|
|
50
|
+
export interface DiffClassification {
|
|
51
|
+
/** At least one changed file, and every one is documentation. */
|
|
52
|
+
readonly documentationOnly: boolean;
|
|
53
|
+
/** At least one changed path could not be determined (so the diff is never docs-only). */
|
|
54
|
+
readonly undeterminedPath: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** The single classification of a diff: callers compute it once and pass the answer down. */
|
|
57
|
+
export declare function classifyDiff(diff: string): DiffClassification;
|
|
32
58
|
/** Why a diff-supplied path must not be read, or undefined if it is acceptable. */
|
|
33
59
|
export declare function unsafePathReason(path: string): string | undefined;
|
|
34
60
|
/**
|
package/dist/context-files.js
CHANGED
|
@@ -69,6 +69,119 @@ export function diffPaths(diff) {
|
|
|
69
69
|
}
|
|
70
70
|
return out;
|
|
71
71
|
}
|
|
72
|
+
function stripEither(path) {
|
|
73
|
+
return path.startsWith('a/') || path.startsWith('b/') ? path.slice(2) : path;
|
|
74
|
+
}
|
|
75
|
+
/** The `a/X b/X` header of a `diff --git` line, when both halves are the same path. */
|
|
76
|
+
function gitHeaderPath(line) {
|
|
77
|
+
const rest = line.slice('diff --git '.length);
|
|
78
|
+
if (rest.startsWith('"'))
|
|
79
|
+
return undefined;
|
|
80
|
+
const mid = (rest.length - 1) / 2;
|
|
81
|
+
if (!Number.isInteger(mid) || rest[mid] !== ' ')
|
|
82
|
+
return undefined;
|
|
83
|
+
const left = rest.slice(0, mid);
|
|
84
|
+
const right = rest.slice(mid + 1);
|
|
85
|
+
if (!left.startsWith('a/') || !right.startsWith('b/'))
|
|
86
|
+
return undefined;
|
|
87
|
+
return left.slice(2) === right.slice(2) ? left.slice(2) : undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* EVERY path a diff changes, for deciding what kind of change it is (not for reading files).
|
|
91
|
+
* Unlike diffPaths it also sees what git writes WITHOUT `---`/`+++` headers: a pure rename
|
|
92
|
+
* (`rename from` / `rename to`), a copy, a binary change (`Binary files A and B differ`) and a
|
|
93
|
+
* mode-only change (the `diff --git` line itself). Both sides of a rename and the old path of a
|
|
94
|
+
* deletion are included, so moving code into a .md file is not a documentation change. A path
|
|
95
|
+
* that cannot be determined is reported as '' so the caller treats the diff as not understood.
|
|
96
|
+
* A diff with no `diff --git` lines (plain unified) falls back to diffPaths.
|
|
97
|
+
*/
|
|
98
|
+
export function changedPaths(diff) {
|
|
99
|
+
const lines = diff.split(/\r?\n/);
|
|
100
|
+
if (!lines.some((l) => l.startsWith('diff --git '))) {
|
|
101
|
+
return diffPaths(diff).map((p) => p.path);
|
|
102
|
+
}
|
|
103
|
+
const out = new Set();
|
|
104
|
+
let block = [];
|
|
105
|
+
const flush = () => {
|
|
106
|
+
if (block.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
const found = [];
|
|
109
|
+
let bad = false;
|
|
110
|
+
const take = (raw, strip) => {
|
|
111
|
+
const value = unquote(raw);
|
|
112
|
+
if (value === undefined || value === '')
|
|
113
|
+
bad = true;
|
|
114
|
+
else if (value !== '/dev/null')
|
|
115
|
+
found.push(strip ? stripEither(value) : value);
|
|
116
|
+
};
|
|
117
|
+
for (const line of block.slice(1)) {
|
|
118
|
+
if (line.startsWith('@@'))
|
|
119
|
+
break;
|
|
120
|
+
if (line.startsWith('rename from '))
|
|
121
|
+
take(line.slice(12), false);
|
|
122
|
+
else if (line.startsWith('rename to '))
|
|
123
|
+
take(line.slice(10), false);
|
|
124
|
+
else if (line.startsWith('copy from '))
|
|
125
|
+
take(line.slice(10), false);
|
|
126
|
+
else if (line.startsWith('copy to '))
|
|
127
|
+
take(line.slice(8), false);
|
|
128
|
+
else if (line.startsWith('--- ') || line.startsWith('+++ '))
|
|
129
|
+
take(line.slice(4), true);
|
|
130
|
+
else {
|
|
131
|
+
const binary = /^Binary files (.+) and (.+) differ$/.exec(line);
|
|
132
|
+
if (binary !== null) {
|
|
133
|
+
take(binary[1] ?? '', true);
|
|
134
|
+
take(binary[2] ?? '', true);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (found.length === 0 && !bad) {
|
|
139
|
+
const header = gitHeaderPath(block[0] ?? '');
|
|
140
|
+
if (header === undefined)
|
|
141
|
+
bad = true;
|
|
142
|
+
else
|
|
143
|
+
found.push(header);
|
|
144
|
+
}
|
|
145
|
+
if (bad)
|
|
146
|
+
out.add('');
|
|
147
|
+
for (const p of found)
|
|
148
|
+
out.add(p);
|
|
149
|
+
};
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
if (line.startsWith('diff --git ')) {
|
|
152
|
+
flush();
|
|
153
|
+
block = [line];
|
|
154
|
+
}
|
|
155
|
+
else if (block.length > 0) {
|
|
156
|
+
block.push(line);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
flush();
|
|
160
|
+
return [...out];
|
|
161
|
+
}
|
|
162
|
+
/** A documentation file type, at any depth. BACKLOG.md is covered by .md. */
|
|
163
|
+
const DOCUMENTATION_FILE = /\.(?:md|mdx|rst|adoc)$/i;
|
|
164
|
+
/** Documentation by FILE TYPE (.md .mdx .rst .adoc, any depth; not .txt, which also names requirements.txt and CMakeLists.txt); a directory name alone is not enough. */
|
|
165
|
+
export function isDocumentationPath(path) {
|
|
166
|
+
return DOCUMENTATION_FILE.test(path);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* True when the diff changes at least one file and EVERY changed path is documentation. Drives
|
|
170
|
+
* the tier (promotion off) and the spec framing. Paths come from changedPaths; one that cannot
|
|
171
|
+
* be determined makes the diff NOT docs-only, so promotion stays on.
|
|
172
|
+
*/
|
|
173
|
+
export function isDocumentationOnly(diff) {
|
|
174
|
+
return classifyDiff(diff).documentationOnly;
|
|
175
|
+
}
|
|
176
|
+
/** The single classification of a diff: callers compute it once and pass the answer down. */
|
|
177
|
+
export function classifyDiff(diff) {
|
|
178
|
+
const paths = changedPaths(diff);
|
|
179
|
+
const undeterminedPath = paths.includes('');
|
|
180
|
+
return {
|
|
181
|
+
documentationOnly: paths.length > 0 && !undeterminedPath && paths.every((p) => isDocumentationPath(p)),
|
|
182
|
+
undeterminedPath,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
72
185
|
/** Why a diff-supplied path must not be read, or undefined if it is acceptable. */
|
|
73
186
|
export function unsafePathReason(path) {
|
|
74
187
|
if (path === '')
|
package/dist/prompt.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ export interface Subject {
|
|
|
76
76
|
* `buildPrompt` below for exactly how that slice is picked out.
|
|
77
77
|
*/
|
|
78
78
|
readonly priorFindings?: ReadonlyArray<PriorFinding>;
|
|
79
|
+
/**
|
|
80
|
+
* Whether every changed file is documentation (context-files.ts classifyDiff). A caller that
|
|
81
|
+
* has already classified the diff passes the answer so it is decided once; absent, buildPrompt
|
|
82
|
+
* classifies `diff` itself. True adds SPEC_FRAMING to the shared prefix.
|
|
83
|
+
*/
|
|
84
|
+
readonly docsOnly?: boolean;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* The two halves of what a reviewer is shown.
|
|
@@ -116,6 +122,8 @@ export declare const NO_PRACTICES_NOTICE = "No practices were supplied for this
|
|
|
116
122
|
* disclosed, not silently unnoticed.
|
|
117
123
|
*/
|
|
118
124
|
export declare const NO_REQUIREMENT_NOTICE: string;
|
|
125
|
+
/** Added to the shared prefix when every changed file is documentation (see below). */
|
|
126
|
+
export declare const SPEC_FRAMING = "This change is a specification or plan. Block only where its text commits to behaviour a practice rejects. Where it merely doesn't rule out a case, raise a smell.";
|
|
119
127
|
/** Heading of the shared check-results section (`Subject.checks`). */
|
|
120
128
|
export declare const CHECKS_HEADING = "## Checks on this change";
|
|
121
129
|
/** Heading of the shared resolved-references section (`Subject.references`). */
|
package/dist/prompt.js
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// tests/review-round-aware.test.ts's own byte-identical pin, which
|
|
39
39
|
// reproduces this file's PRE-feature template independently and asserts
|
|
40
40
|
// equality against it.
|
|
41
|
+
import { isDocumentationOnly } from './context-files.js';
|
|
41
42
|
import { REPLY_CONTRACT, RULING_MARKER, RULING_RESOLVED_WORD, RULING_UNRESOLVED_WORD, } from './replyContract.js';
|
|
42
43
|
/** The two halves in the one order that preserves the shared prefix. */
|
|
43
44
|
export function renderPrompt(prompt) {
|
|
@@ -63,6 +64,8 @@ export const NO_REQUIREMENT_NOTICE = 'No requirement text was supplied for this
|
|
|
63
64
|
"context -- is this run's own account of its own work; there is nothing " +
|
|
64
65
|
'from outside the run to check it against. Say so plainly in your ' +
|
|
65
66
|
'reasoning, and judge only whether the change is coherent on its own terms.';
|
|
67
|
+
/** Added to the shared prefix when every changed file is documentation (see below). */
|
|
68
|
+
export const SPEC_FRAMING = "This change is a specification or plan. Block only where its text commits to behaviour a practice rejects. Where it merely doesn't rule out a case, raise a smell.";
|
|
66
69
|
/** Heading of the shared check-results section (`Subject.checks`). */
|
|
67
70
|
export const CHECKS_HEADING = '## Checks on this change';
|
|
68
71
|
/** Heading of the shared resolved-references section (`Subject.references`). */
|
|
@@ -173,9 +176,11 @@ export function buildPrompt(lens, subject, practices, placement = 'shared') {
|
|
|
173
176
|
const priorFindingsSection = hasPriorFindings
|
|
174
177
|
? ['', PRIOR_FINDINGS_HEADING, '', ...rulingSection, '', ...independentReviewSection]
|
|
175
178
|
: [];
|
|
179
|
+
const specification = subject.docsOnly ?? isDocumentationOnly(subject.diff);
|
|
176
180
|
const shared = [
|
|
177
181
|
'You are one lens of a review panel. Several reviewers are reading this same change, each along a different dimension, and their verdicts are combined. Review only your own dimension: another lens is covering the rest, and a finding outside your dimension is noise in someone else’s report.',
|
|
178
182
|
'',
|
|
183
|
+
...(specification ? [SPEC_FRAMING, ''] : []),
|
|
179
184
|
...barInShared,
|
|
180
185
|
'## Requirement',
|
|
181
186
|
'',
|
package/dist/run-review.d.ts
CHANGED
|
@@ -68,6 +68,12 @@ export interface ReviewNodeResult {
|
|
|
68
68
|
readonly downgraded: number;
|
|
69
69
|
/** How many smells tier enforcement promoted to [blocking] (0 when none). */
|
|
70
70
|
readonly promoted: number;
|
|
71
|
+
/** True when Reject promotion was switched off because every changed file is documentation. */
|
|
72
|
+
readonly promotionOff: boolean;
|
|
73
|
+
/** True when every changed file is documentation, whether or not a tier applied. */
|
|
74
|
+
readonly docsOnly: boolean;
|
|
75
|
+
/** True when a changed path could not be determined, so the diff was not treated as docs-only. */
|
|
76
|
+
readonly pathUndetermined: boolean;
|
|
71
77
|
/** Stderr lines, one per member of every merged dedup group (empty when none merged). */
|
|
72
78
|
readonly dedupAudit: readonly string[];
|
|
73
79
|
}
|
package/dist/run-review.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// - `citedContext` (a caller-curated prose field) is accepted and
|
|
30
30
|
// forwarded if a caller supplies it, but nothing here computes one --
|
|
31
31
|
// unchanged from v5, where this was also true.
|
|
32
|
+
import { classifyDiff } from './context-files.js';
|
|
32
33
|
import { runPanel } from './panel.js';
|
|
33
34
|
import { exitCodeFor, renderReport } from './report.js';
|
|
34
35
|
import { selectReviewer } from './reviewer-selection.js';
|
|
@@ -113,12 +114,20 @@ export async function runReviewNode(input, onCall) {
|
|
|
113
114
|
const dispatched = input.warmFirst === true && input.lensSet.lenses.length >= 2
|
|
114
115
|
? warmFirst(observedReviewer)
|
|
115
116
|
: observedReviewer;
|
|
116
|
-
|
|
117
|
+
// The docs-only decision is made ONCE, here (context-files.ts classifyDiff). It reaches the
|
|
118
|
+
// prompt through the subject (spec framing, every mode) and the tier below (promotion off).
|
|
119
|
+
const docs = classifyDiff(input.subject.diff);
|
|
120
|
+
const subject = { ...input.subject, docsOnly: docs.documentationOnly };
|
|
121
|
+
const panel = await runPanel(input.lensSet, subject, dispatched, input.practices);
|
|
117
122
|
const prompts = panel.prompts;
|
|
123
|
+
// Promotion is off when every changed file in the parsed diff is documentation
|
|
124
|
+
// (classified above by context-files.ts); with no tier signals there is no promotion to turn off.
|
|
125
|
+
const promotionOff = input.tierSignals !== undefined && docs.documentationOnly;
|
|
118
126
|
// C1 runs before C2: de-duplication sees the tiers as enforced.
|
|
119
127
|
const tiered = input.tierSignals === undefined
|
|
120
128
|
? { verdict: panel.verdict, downgraded: 0, promoted: 0 }
|
|
121
129
|
: enforceTier(panel.verdict, input.tierSignals, {
|
|
130
|
+
...(promotionOff ? { promote: false } : {}),
|
|
122
131
|
...(input.tierRejects === undefined ? {} : { rejects: input.tierRejects }),
|
|
123
132
|
...(input.practiceIds === undefined ? {} : { practiceIds: input.practiceIds }),
|
|
124
133
|
});
|
|
@@ -151,6 +160,9 @@ export async function runReviewNode(input, onCall) {
|
|
|
151
160
|
prompts,
|
|
152
161
|
downgraded: tiered.downgraded,
|
|
153
162
|
promoted: tiered.promoted,
|
|
163
|
+
promotionOff,
|
|
164
|
+
docsOnly: docs.documentationOnly,
|
|
165
|
+
pathUndetermined: docs.undeterminedPath,
|
|
154
166
|
dedupAudit,
|
|
155
167
|
};
|
|
156
168
|
}
|
package/dist/tier.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface TierContext {
|
|
|
16
16
|
readonly rejects?: Readonly<Record<string, readonly string[]>>;
|
|
17
17
|
/** Every practice id in the corpus, ignored when matching. */
|
|
18
18
|
readonly practiceIds?: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* False turns Reject-quote promotion off (every changed file is documentation; see
|
|
21
|
+
* context-files.ts isDocumentationOnly). The C1 downgrade is unaffected. Default true.
|
|
22
|
+
*/
|
|
23
|
+
readonly promote?: boolean;
|
|
19
24
|
}
|
|
20
25
|
export interface TierResult {
|
|
21
26
|
readonly verdict: Verdict;
|
package/dist/tier.js
CHANGED
|
@@ -122,7 +122,8 @@ export function enforceTier(verdict, signals, context = {}) {
|
|
|
122
122
|
if (!Object.prototype.hasOwnProperty.call(signals, result.lens))
|
|
123
123
|
return result;
|
|
124
124
|
const own = signals[result.lens] ?? [];
|
|
125
|
-
const rejectLines = context.
|
|
125
|
+
const rejectLines = context.promote !== false &&
|
|
126
|
+
context.rejects !== undefined &&
|
|
126
127
|
Object.prototype.hasOwnProperty.call(context.rejects, result.lens)
|
|
127
128
|
? (context.rejects[result.lens] ?? [])
|
|
128
129
|
: undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verevoir/review",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "The review panel: lens-based diff review. One implementation, consumed by pump-internal review, local preflight, eval-review, and CI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
20
|
"lenses.default.json",
|
|
21
|
-
"npm-shrinkwrap.json",
|
|
22
21
|
"src",
|
|
23
22
|
"tsconfig.json",
|
|
24
23
|
"tsconfig.build.json",
|
package/src/cli/footer.ts
CHANGED
|
@@ -53,6 +53,28 @@ export function promotedLine(promoted: number): string | undefined {
|
|
|
53
53
|
: undefined;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** Said when Reject promotion was switched off because every changed file is documentation. */
|
|
57
|
+
export const PROMOTION_OFF_LINE = 'Tier: promotion off: every changed file is documentation';
|
|
58
|
+
|
|
59
|
+
/** Said when the spec framing was added but there is no tier, so no promotion to switch off. */
|
|
60
|
+
export const SPEC_FRAMING_LINE =
|
|
61
|
+
'Spec framing: every changed file is documentation (no tier signals, so promotion is not in play)';
|
|
62
|
+
|
|
63
|
+
/** Said when a changed path could not be determined, so the diff is not treated as documentation. */
|
|
64
|
+
export const PATH_UNDETERMINED_LINE =
|
|
65
|
+
'Documentation check: a changed path could not be determined, so the diff is not treated as documentation (promotion stays on)';
|
|
66
|
+
|
|
67
|
+
/** The one line saying how the documentation check affected this review; none when it did not. */
|
|
68
|
+
export function docsLine(result: {
|
|
69
|
+
readonly promotionOff: boolean;
|
|
70
|
+
readonly docsOnly: boolean;
|
|
71
|
+
readonly pathUndetermined: boolean;
|
|
72
|
+
}): string | undefined {
|
|
73
|
+
if (result.promotionOff) return PROMOTION_OFF_LINE;
|
|
74
|
+
if (result.docsOnly) return SPEC_FRAMING_LINE;
|
|
75
|
+
return result.pathUndetermined ? PATH_UNDETERMINED_LINE : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
/**
|
|
57
79
|
* The temperature the judging used. The claude-cli route has no temperature option
|
|
58
80
|
* (`claude -p` documents no flag for it), so a requested one is said not to apply.
|
|
@@ -71,6 +93,7 @@ export function renderFooter(
|
|
|
71
93
|
downgraded = 0,
|
|
72
94
|
promoted = 0,
|
|
73
95
|
temperature?: number,
|
|
96
|
+
docsNote?: string,
|
|
74
97
|
): string {
|
|
75
98
|
const lines = [
|
|
76
99
|
'---',
|
|
@@ -83,6 +106,7 @@ export function renderFooter(
|
|
|
83
106
|
if (tier !== undefined) lines.push('', tier);
|
|
84
107
|
const promotion = promotedLine(promoted);
|
|
85
108
|
if (promotion !== undefined) lines.push('', promotion);
|
|
109
|
+
if (docsNote !== undefined) lines.push('', docsNote);
|
|
86
110
|
if (calls.length > 0) {
|
|
87
111
|
lines.push('', 'Per-lens usage and wall time:', '');
|
|
88
112
|
for (const call of byLens(calls)) {
|
package/src/cli/review.ts
CHANGED
|
@@ -31,7 +31,14 @@ import { buildFileContext, NO_FILE_CONTEXT_NOTE } from '../context-files.js';
|
|
|
31
31
|
import { buildReferences } from '../references.js';
|
|
32
32
|
import type { DirEntry } from '../references.js';
|
|
33
33
|
import type { RawCall } from '../raw-call.js';
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
docsLine,
|
|
36
|
+
promotedLine,
|
|
37
|
+
renderFooter,
|
|
38
|
+
temperatureLine,
|
|
39
|
+
tierLine,
|
|
40
|
+
usageLines,
|
|
41
|
+
} from './footer.js';
|
|
35
42
|
import type { RouteName } from './footer.js';
|
|
36
43
|
import type { DerivedLenses } from '../practice-lenses.js';
|
|
37
44
|
|
|
@@ -642,9 +649,11 @@ export async function main(
|
|
|
642
649
|
if (tier !== undefined) io.stderr(`${tier}\n`);
|
|
643
650
|
const promotion = promotedLine(result.promoted);
|
|
644
651
|
if (promotion !== undefined) io.stderr(`${promotion}\n`);
|
|
652
|
+
const docsNote = docsLine(result);
|
|
653
|
+
if (docsNote !== undefined) io.stderr(`${docsNote}\n`);
|
|
645
654
|
// Audit trail for tier decisions behind merged groups: stderr, not the report (see dedup.ts).
|
|
646
655
|
for (const line of result.dedupAudit) io.stderr(line);
|
|
647
|
-
const footer = `\n\n${renderFooter(model, route, calls, result.downgraded, result.promoted, temperature)}`;
|
|
656
|
+
const footer = `\n\n${renderFooter(model, route, calls, result.downgraded, result.promoted, temperature, docsNote)}`;
|
|
648
657
|
const note =
|
|
649
658
|
derived !== undefined && derived.untagged.length > 0
|
|
650
659
|
? `\n\n> ${String(derived.untagged.length)} practice(s) had no "lens:" tag and were ` +
|