@codedology/code-forge 0.9.0 → 0.10.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/CHANGELOG.md +58 -0
- package/README.md +29 -8
- package/docs/reference/blocks.json +385 -1
- package/docs/reference/config.md +36 -0
- package/package.json +1 -1
- package/schema/code-forge.schema.json +89 -0
- package/skill/references/adapters/claude-code.md +1 -1
- package/skill/references/adapters/solo.md +2 -2
- package/skill/references/adapters/subprocess.md +12 -2
- package/skill/references/code.md +8 -3
- package/skill/references/ledger.md +2 -2
- package/skill/references/proof.md +77 -2
- package/skill/references/review.md +11 -4
- package/skill/references/security.md +4 -4
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +2 -0
- package/src/autopilot/binnacle.mjs +9 -3
- package/src/cli/block.mjs +164 -24
- package/src/cli/jev.mjs +22 -3
- package/src/cli/keys.mjs +156 -21
- package/src/cli/proof.mjs +35 -7
- package/src/cli/spawn.mjs +44 -3
- package/src/config/validate.mjs +320 -5
- package/src/doctor/index.mjs +2 -1
- package/src/doctor/local.mjs +27 -6
- package/src/doctor/probes.mjs +38 -0
- package/src/engines/builders/codex.mjs +44 -4
- package/src/engines/sandbox-dirs.mjs +257 -0
- package/src/keys/store.mjs +128 -21
- package/src/proof/junit.mjs +530 -0
- package/src/proof/red-green.mjs +57 -9
- package/src/proof/red-parse.mjs +2 -0
- package/src/proof/runners.mjs +472 -0
- package/src/proof/xcresult.mjs +285 -0
- package/src/review/engine.mjs +82 -30
- package/src/review/fixloop.mjs +45 -13
- package/src/review/gate-check.mjs +3 -2
- package/src/review/lenses/A.md +1 -0
- package/src/review/lenses/B.md +1 -0
- package/src/review/lenses/full.md +1 -0
- package/src/review/lenses/quick.md +1 -0
- package/src/review/lenses/recheck.md +1 -0
- package/src/review/reopen.mjs +4 -6
- package/src/review/route.mjs +232 -0
- package/src/review/schema-fallback.mjs +77 -22
- package/src/review/session-retry.mjs +44 -9
- package/src/review/test-quality.mjs +201 -0
- package/src/review/verb-forms.mjs +39 -0
- package/src/session/spawn.mjs +39 -2
- package/src/state/block.mjs +13 -3
- package/src/state/config-snapshot.mjs +39 -0
- package/src/state/run.mjs +4 -0
- package/src/util/forbidden.mjs +2 -2
- package/src/worker/loop.mjs +79 -14
- package/src/worker/ticket.mjs +9 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xcresult` (red-green plan §1.6, B102): read the result bundle of an `xcodebuild test` run and
|
|
3
|
+
* classify the RED and GREEN steps of the `xcodebuild` runner adapter from it.
|
|
4
|
+
*
|
|
5
|
+
* The bundle is read with `xcrun xcresulttool get test-results tests --path <bundle>` (argv only,
|
|
6
|
+
* 60 s timeout, output capped at 8 MiB), and its JSON is mapped to the common `{tests}` shape of
|
|
7
|
+
* `junit.mjs`. The node shape is pinned by real captured output (Xcode 26.6, `test/fixtures/xcresult/`):
|
|
8
|
+
*
|
|
9
|
+
* {testNodes: [Test Plan → Unit test bundle | UI test bundle → Test Suite (→ Test Suite)…
|
|
10
|
+
* → Test Case → Failure Message]}
|
|
11
|
+
*
|
|
12
|
+
* A Test Case carries `nodeIdentifier` (`ClampXCTests/testCapsHigh()` for XCTest,
|
|
13
|
+
* `ClampSwiftTests/capsHigh()` for Swift Testing) and `result` (`Passed`, `Failed`, `Skipped`). Its
|
|
14
|
+
* id is `<bundle name>/<nodeIdentifier>`, e.g. `KitTests/ClampXCTests/testCapsHigh()`. A Test Plan
|
|
15
|
+
* with no children (a filter that selected nothing) is zero tests.
|
|
16
|
+
*
|
|
17
|
+
* Fail closed: no bundle, a bundle that is not a real directory, a failed or timed-out
|
|
18
|
+
* `xcresulttool`, output over 8 MiB, JSON that does not parse, two tests with the same id, and a
|
|
19
|
+
* failed check of what this reader uses make the result unreadable. It checks every node's
|
|
20
|
+
* `nodeType` (a known type) and its parent type (a known place), a Test Case's `result` (a known
|
|
21
|
+
* result) and `nodeIdentifier` (a non-empty string), a bundle's `name` (non-empty, no `/`), a
|
|
22
|
+
* Failure Message's `name` (a string, no children) and every `children` (a list). Any other key of a
|
|
23
|
+
* node or of the document (`devices`, `testPlanConfigurations`, `duration`, …) is not read and is
|
|
24
|
+
* ignored. An unreadable result never proves anything (RED is `RED_INVALID error`, GREEN is
|
|
25
|
+
* `NOT_GREEN`). Today
|
|
26
|
+
* that includes these xcresulttool node types: a parameterized Swift Testing test (`Arguments`),
|
|
27
|
+
* repetitions (`Repetition`, `Test Case Run`), a multi-destination or multi-configuration run
|
|
28
|
+
* (`Device`, `Test Plan Configuration`), `Runtime Warning`, `Source Code Reference`, `Attachment`,
|
|
29
|
+
* `Expression`, `Test Value`, and the result `Expected Failure`.
|
|
30
|
+
*
|
|
31
|
+
* A Failure Message (`<file>.swift:<line>: <text>`, the prefix optional) is an assertion only for
|
|
32
|
+
* these texts, as captured: `XCTAssert… failed` (but not `XCTAssert… failed: threw error`, a thrown
|
|
33
|
+
* error inside the assertion), XCTFail's `failed` and `failed - <message>`, and Swift Testing's
|
|
34
|
+
* `Expectation failed: …`. Every other text is an error: a thrown error (`failed: caught error: …`,
|
|
35
|
+
* `Caught error: …`), `Issue recorded: …`, a crash. A failed test is an assertion only when every
|
|
36
|
+
* one of its messages is one.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { lstat } from 'node:fs/promises';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { exec } from '../util/exec.mjs';
|
|
42
|
+
import { MAX_REPORT_BYTES, NO_REPORT, classifyGreen, classifyReport, swiftCompileError } from './junit.mjs';
|
|
43
|
+
|
|
44
|
+
/** `xcresulttool` may run this long before it is stopped. */
|
|
45
|
+
export const XCRESULTTOOL_TIMEOUT_MS = 60_000;
|
|
46
|
+
|
|
47
|
+
/** Which parent node type each known node type may have (`null` = the `testNodes` list). */
|
|
48
|
+
const NODE_PARENTS = Object.freeze({
|
|
49
|
+
'Test Plan': [null],
|
|
50
|
+
'Unit test bundle': ['Test Plan'],
|
|
51
|
+
'UI test bundle': ['Test Plan'],
|
|
52
|
+
'Test Suite': ['Unit test bundle', 'UI test bundle', 'Test Suite'],
|
|
53
|
+
'Test Case': ['Test Suite'],
|
|
54
|
+
'Failure Message': ['Test Case'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** A Test Case `result` this reader knows, and its common status. */
|
|
58
|
+
const RESULT_STATUS = Object.freeze({ Passed: 'passed', Failed: 'failed', Skipped: 'skipped' });
|
|
59
|
+
|
|
60
|
+
/** The optional `<file>.swift:<line>: ` prefix of a Failure Message. */
|
|
61
|
+
const MESSAGE_PREFIX = /^[^\n:]+\.swift:\d+: /;
|
|
62
|
+
/** The assertion texts, after the prefix (see the header). */
|
|
63
|
+
const ASSERTION_MESSAGES = Object.freeze([/^XCTAssert\w* failed(?!: threw error)/, /^failed - /, /^failed$/, /^Expectation failed: /]);
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line no-control-regex
|
|
66
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
67
|
+
/** `xcodebuild`'s own build-failure lines (`** BUILD FAILED **`, `** TEST BUILD FAILED **`). */
|
|
68
|
+
const BUILD_FAILED_LINE = /^\*\* (?:TEST )?BUILD FAILED \*\*[ \t\r]*$/m;
|
|
69
|
+
/** `xcodebuild test` (Xcode 26.6) on a test that does not compile prints this line, tab-indented, then `** TEST FAILED **`. */
|
|
70
|
+
const BUILD_CANCELLED_LINE = /^[ \t]*Testing cancelled because the build failed\.[ \t\r]*$/m;
|
|
71
|
+
|
|
72
|
+
/** @param {string} reason @returns {never} */
|
|
73
|
+
function unreadable(reason) {
|
|
74
|
+
const err = new Error(`report-unreadable: ${reason}`);
|
|
75
|
+
/** @type {any} */ (err).code = 'report-unreadable';
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
80
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Is a Failure Message text an assertion failure?
|
|
84
|
+
* @param {string} text @returns {'assertion' | 'error'}
|
|
85
|
+
*/
|
|
86
|
+
export function failureMessageKind(text) {
|
|
87
|
+
const rest = String(text).replace(MESSAGE_PREFIX, '');
|
|
88
|
+
return ASSERTION_MESSAGES.some((p) => p.test(rest)) ? 'assertion' : 'error';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read `xcresulttool get test-results tests` JSON.
|
|
93
|
+
* @param {string} text
|
|
94
|
+
* @returns {{tests: import('./junit.mjs').ReportTest[]}}
|
|
95
|
+
* @throws {Error} `code: 'report-unreadable'`; the message names the reason only, never content.
|
|
96
|
+
*/
|
|
97
|
+
export function readXcresult(text) {
|
|
98
|
+
if (typeof text !== 'string') unreadable('the xcresulttool output is not text');
|
|
99
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_REPORT_BYTES) unreadable('the xcresulttool output is over 8 MiB');
|
|
100
|
+
let doc;
|
|
101
|
+
try {
|
|
102
|
+
doc = JSON.parse(text);
|
|
103
|
+
} catch {
|
|
104
|
+
unreadable('the xcresulttool output is not JSON');
|
|
105
|
+
}
|
|
106
|
+
if (!isObject(doc) || !Array.isArray(doc.testNodes)) unreadable('the xcresulttool output has no testNodes list');
|
|
107
|
+
|
|
108
|
+
/** @type {import('./junit.mjs').ReportTest[]} */
|
|
109
|
+
const tests = [];
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
/** @param {unknown} node @param {string | null} parent @param {string | null} bundle */
|
|
112
|
+
const walk = (node, parent, bundle) => {
|
|
113
|
+
if (!isObject(node)) unreadable('a test node that is not an object');
|
|
114
|
+
const type = node.nodeType;
|
|
115
|
+
if (typeof type !== 'string' || !Object.hasOwn(NODE_PARENTS, type)) unreadable('a node type this reader does not know');
|
|
116
|
+
if (!NODE_PARENTS[/** @type {keyof typeof NODE_PARENTS} */ (type)].includes(/** @type {any} */ (parent))) unreadable('a test node in the wrong place');
|
|
117
|
+
const children = node.children ?? [];
|
|
118
|
+
if (!Array.isArray(children)) unreadable('test node children that are not a list');
|
|
119
|
+
if (type === 'Failure Message') {
|
|
120
|
+
if (typeof node.name !== 'string') unreadable('a failure message without a text');
|
|
121
|
+
if (children.length > 0) unreadable('a failure message with children');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
let inBundle = bundle;
|
|
125
|
+
if (type === 'Unit test bundle' || type === 'UI test bundle') {
|
|
126
|
+
if (typeof node.name !== 'string' || node.name === '' || node.name.includes('/')) unreadable('a test bundle without a name');
|
|
127
|
+
inBundle = node.name;
|
|
128
|
+
}
|
|
129
|
+
if (type === 'Test Case') {
|
|
130
|
+
const ident = node.nodeIdentifier;
|
|
131
|
+
if (typeof ident !== 'string' || ident === '') unreadable('a test case without a nodeIdentifier');
|
|
132
|
+
const result = node.result;
|
|
133
|
+
if (typeof result !== 'string' || !Object.hasOwn(RESULT_STATUS, result)) unreadable('a test result this reader does not know');
|
|
134
|
+
const status = RESULT_STATUS[/** @type {keyof typeof RESULT_STATUS} */ (result)];
|
|
135
|
+
const kinds = children.map((child) => {
|
|
136
|
+
walk(child, 'Test Case', inBundle);
|
|
137
|
+
return failureMessageKind(/** @type {any} */ (child).name);
|
|
138
|
+
});
|
|
139
|
+
const id = `${inBundle}/${ident}`;
|
|
140
|
+
// one id twice (two bundles of one name, a repeated node) would let one result hide the other
|
|
141
|
+
if (seen.has(id)) unreadable('two test cases with the same id');
|
|
142
|
+
seen.add(id);
|
|
143
|
+
if (status === 'failed') tests.push({ id, status, kind: kinds.length > 0 && kinds.every((k) => k === 'assertion') ? 'assertion' : 'error' });
|
|
144
|
+
else if (kinds.length > 0) unreadable('a failure message on a test that did not fail');
|
|
145
|
+
else tests.push({ id, status });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const child of children) walk(child, type, inBundle);
|
|
149
|
+
};
|
|
150
|
+
for (const node of doc.testNodes) walk(node, null, null);
|
|
151
|
+
return { tests };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `readXcresult` that never throws.
|
|
156
|
+
* @param {string} text @returns {import('./junit.mjs').ReportResult}
|
|
157
|
+
*/
|
|
158
|
+
export function parseXcresult(text) {
|
|
159
|
+
try {
|
|
160
|
+
return readXcresult(text);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const message = err instanceof Error ? err.message : '';
|
|
163
|
+
return { unreadable: message.startsWith('report-unreadable: ') ? message.slice('report-unreadable: '.length) : 'the result bundle could not be read' };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Read a result bundle with `xcresulttool`. Never throws. No bundle at the path is `NO_REPORT`; a
|
|
169
|
+
* symlink or any other non-directory is unreadable. A relative `bundle` is resolved against `cwd`
|
|
170
|
+
* once, and that one absolute path is both checked and read.
|
|
171
|
+
* @param {string} bundle @param {{cwd: string, env: NodeJS.ProcessEnv, timeoutMs?: number}} opts
|
|
172
|
+
* @returns {Promise<import('./junit.mjs').ReportResult>}
|
|
173
|
+
*/
|
|
174
|
+
export async function loadXcresult(bundle, { cwd, env, timeoutMs = XCRESULTTOOL_TIMEOUT_MS }) {
|
|
175
|
+
const target = path.resolve(cwd, bundle);
|
|
176
|
+
let st;
|
|
177
|
+
try {
|
|
178
|
+
st = await lstat(target);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return /** @type {any} */ (err)?.code === 'ENOENT' ? NO_REPORT : { unreadable: 'the result bundle could not be opened' };
|
|
181
|
+
}
|
|
182
|
+
if (!st.isDirectory()) return { unreadable: 'the result bundle is not a directory' };
|
|
183
|
+
try {
|
|
184
|
+
const res = await exec(['xcrun', 'xcresulttool', 'get', 'test-results', 'tests', '--path', target], { cwd, env, timeoutMs, maxBufferBytes: MAX_REPORT_BYTES + 1 });
|
|
185
|
+
if (res.result !== 'ok' || res.code !== 0 || res.signal || res.timedOut) return { unreadable: 'xcresulttool failed' };
|
|
186
|
+
return parseXcresult(res.stdout);
|
|
187
|
+
} catch {
|
|
188
|
+
return { unreadable: 'xcresulttool failed' };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Does the console text show that the test build failed? Colours are stripped first.
|
|
194
|
+
* @param {string} text @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
export function xcodeBuildFailed(text) {
|
|
197
|
+
const plain = String(text).replace(ANSI, '');
|
|
198
|
+
return BUILD_FAILED_LINE.test(plain) || BUILD_CANCELLED_LINE.test(plain);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @param {import('./junit.mjs').RunResult} run @returns {boolean} */
|
|
202
|
+
const exitedClean = (run) => run.code === 0 && !run.signal && !run.timedOut;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A scope is `<test_target>/<Suite>/`: two non-empty parts and the closing `/`. Without the `/`,
|
|
206
|
+
* `KitTests/Clamp` would also admit `KitTests/ClampMore/…`, so any other scope throws (the callers
|
|
207
|
+
* then classify fail closed).
|
|
208
|
+
* @param {unknown} scope @returns {asserts scope is string}
|
|
209
|
+
*/
|
|
210
|
+
function checkScope(scope) {
|
|
211
|
+
if (typeof scope !== 'string' || !/^[^/]+\/[^/]+\/$/.test(scope)) throw new Error('an xcodebuild scope is <test_target>/<Suite>/');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Is any test of a readable result outside `scope` (`<test_target>/<Suite>/`)?
|
|
216
|
+
* @param {import('./junit.mjs').ReportResult} report @param {string} scope @returns {boolean}
|
|
217
|
+
* @throws {Error} a scope that is not `<test_target>/<Suite>/`
|
|
218
|
+
*/
|
|
219
|
+
const ranOtherTests = (report, scope) => {
|
|
220
|
+
checkScope(scope);
|
|
221
|
+
return 'tests' in report && report.tests.some((t) => !t.id.startsWith(scope));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Classify the RED step of an `xcodebuild` run. Never throws. A `scope` that is not
|
|
226
|
+
* `<test_target>/<Suite>/` is `RED_INVALID error`. Then the first row that applies wins:
|
|
227
|
+
* - exit 0 and no readable result (no bundle, or an unreadable one) ⇒ `RED_INVALID error`: a clean
|
|
228
|
+
* exit with nothing to read says nothing about the test;
|
|
229
|
+
* - exit 0 and a readable result with a test outside `scope` (or only such tests) ⇒
|
|
230
|
+
* `RED_INVALID error` (the filter ran other tests): the test under proof did not run cleanly
|
|
231
|
+
* in scope, so a passing run is not `NOT_RED`;
|
|
232
|
+
* - exit 0 and a readable result with no test that ran ⇒ `RED_INVALID error` (the filter ran no
|
|
233
|
+
* test). This differs from `junit` and `swift`, where `classifyReport` reads any exit 0 as
|
|
234
|
+
* `NOT_RED`: `xcodebuild test` exits 0 when `-only-testing` selects nothing (a Swift Testing id
|
|
235
|
+
* without its `()`), so an empty run says nothing about the test and must not read as "it passed";
|
|
236
|
+
* - exit 0 otherwise ⇒ `NOT_RED`; a timeout or a signal ⇒ `RED_INVALID fatal`;
|
|
237
|
+
* - a failed run whose console shows a build failure, or that has no tests and a `swiftc` error
|
|
238
|
+
* ⇒ `RED_INVALID compile`;
|
|
239
|
+
* - a test outside `scope` ⇒ `RED_INVALID error` (the filter ran other tests);
|
|
240
|
+
* - else `classifyReport` (§1.1).
|
|
241
|
+
* Every invalid row this adds carries no failed data.
|
|
242
|
+
* @param {{run: import('./junit.mjs').RunResult, report: import('./junit.mjs').ReportResult, scope: string}} input
|
|
243
|
+
* @returns {import('./junit.mjs').JUnitRedParse}
|
|
244
|
+
*/
|
|
245
|
+
export function classifyXcodeRed({ run, report, scope }) {
|
|
246
|
+
try {
|
|
247
|
+
checkScope(scope);
|
|
248
|
+
const base = classifyReport({ run, reports: [report], compileError: swiftCompileError });
|
|
249
|
+
/** @param {'compile' | 'error'} red_kind @param {string} reason @returns {import('./junit.mjs').JUnitRedParse} */
|
|
250
|
+
const invalid = (red_kind, reason) => ({ ...base, verdict: 'RED_INVALID', red_kind, failed: null, failed_tests: [], failed_ids: [], reason });
|
|
251
|
+
const readable = isObject(report) && Array.isArray(/** @type {any} */ (report).tests);
|
|
252
|
+
if (exitedClean(run)) {
|
|
253
|
+
if (!readable) return invalid('error', 'no readable test result');
|
|
254
|
+
if (readable && ranOtherTests(report, scope)) return invalid('error', 'the filter ran other tests');
|
|
255
|
+
if (readable && !(/** @type {any} */ (report).tests.some((/** @type {any} */ t) => t.status !== 'skipped'))) return invalid('error', 'the filter ran no test');
|
|
256
|
+
return base;
|
|
257
|
+
}
|
|
258
|
+
if (run.timedOut || run.signal) return base;
|
|
259
|
+
const output = `${run.stdout ?? ''}\n${run.stderr ?? ''}`;
|
|
260
|
+
if (xcodeBuildFailed(output)) return invalid('compile', 'the test did not build');
|
|
261
|
+
const noTests = report === NO_REPORT || (readable && /** @type {any} */ (report).tests.length === 0);
|
|
262
|
+
if (noTests && swiftCompileError(output)) return invalid('compile', 'the test did not compile');
|
|
263
|
+
if (readable && ranOtherTests(report, scope)) return invalid('error', 'the filter ran other tests');
|
|
264
|
+
return base;
|
|
265
|
+
} catch {
|
|
266
|
+
return { verdict: 'RED_INVALID', red_kind: 'error', framework: 'junit', failed: null, failed_tests: [], failed_ids: [], reason: 'no readable test result' };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Classify the GREEN step of an `xcodebuild` run: `classifyGreen`, and `NOT_GREEN` when the result
|
|
272
|
+
* has a test outside `scope`. Never throws.
|
|
273
|
+
* @param {{run: import('./junit.mjs').RunResult, report: import('./junit.mjs').ReportResult, redFailed: string[], scope: string}} input
|
|
274
|
+
* @returns {import('./junit.mjs').JUnitGreenParse}
|
|
275
|
+
*/
|
|
276
|
+
export function classifyXcodeGreen({ run, report, redFailed, scope }) {
|
|
277
|
+
try {
|
|
278
|
+
checkScope(scope);
|
|
279
|
+
const base = classifyGreen({ run, reports: [report], redFailed });
|
|
280
|
+
if (base.verdict === 'GREEN' && ranOtherTests(report, scope)) return { ...base, verdict: 'NOT_GREEN', reason: 'the filter ran other tests' };
|
|
281
|
+
return base;
|
|
282
|
+
} catch {
|
|
283
|
+
return { verdict: 'NOT_GREEN', failed: null, failed_tests: [], reason: 'no readable test result' };
|
|
284
|
+
}
|
|
285
|
+
}
|
package/src/review/engine.mjs
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* request with a non-repo-relative path is refused (`needs_file-refused`, never approval);
|
|
20
20
|
* 6. a session that ends in `timeout` is spawned ONCE more on the same packet and level
|
|
21
21
|
* (`session-retry.mjs`, B30); a second timeout is `unavailable: timeout`. The summary's
|
|
22
|
-
* `attempts` says how many ran
|
|
22
|
+
* `attempts` says how many ran. B91: a timed-out attempt is a broken result like a schema
|
|
23
|
+
* miss: its signed `review.session_timeout` note carries the packet's `packet_hash`, and when
|
|
24
|
+
* it is the packet's second broken result (schema or timeout, in any mix) the second level
|
|
25
|
+
* (6b) is tried in the same session instead of the same-model retry (`trigger: timeout`);
|
|
23
26
|
* 6b. a session whose answer fails the schema logs it (`review.schema_invalid`, the raw answer
|
|
24
|
-
* redacted); at the packet's second
|
|
27
|
+
* redacted); at the packet's second broken result it is tried ONCE on `review.second_levels.<level>`
|
|
25
28
|
* when the owner configured one (`schema-fallback.mjs`, B55; never in consensus mode).
|
|
26
29
|
* The summary then carries `schema_fallback: {level, provider, model}` (`level`:
|
|
27
30
|
* `review.second_levels.<level>`), the second level's own provider/model, and `tokens_in` /
|
|
@@ -43,6 +46,11 @@
|
|
|
43
46
|
* 9. a pure deletion (the file is gone from the reviewed tree) gets a `## deleted file` note in
|
|
44
47
|
* every packet — reviewers and judge — naming up to 20 tree paths that still name its basename
|
|
45
48
|
* (`deletion.mjs`, B74). It is a hint, never an exemption: the deletion still needs approval.
|
|
49
|
+
* 10. a test file that reads a source file and asserts on its text gets a `## test-quality` note in
|
|
50
|
+
* every packet — reviewers and judge — naming up to 10 line numbers, never the file's text
|
|
51
|
+
* (`test-quality.mjs`, B92). It is a hint, never a finding: the lens tells the reviewer to
|
|
52
|
+
* raise it as a `warning` unless the read file is a fixture. It is extra text like the
|
|
53
|
+
* moved code (after it), counted in the same measure; no flag ⇒ no section, no row.
|
|
46
54
|
* Approval (until B12b's triage lands) = the final answer (the judge's in dual/consensus mode, else
|
|
47
55
|
* the single reviewer's) passed the guard, says `passed: true`, and carries no `critical` or
|
|
48
56
|
* `warning` finding.
|
|
@@ -59,10 +67,11 @@ import { tierFor } from '../proof/tiers.mjs';
|
|
|
59
67
|
import { assembleJudgePacket, assemblePacket, attachFiles, budgetFor, diffOnlyTokens, readFileDiff } from './packet.mjs';
|
|
60
68
|
import { movedText, movesFor, peerDiffReader } from './moved.mjs';
|
|
61
69
|
import { deletionNote, renderDeletionNote } from './deletion.mjs';
|
|
70
|
+
import { testQualityNote } from './test-quality.mjs';
|
|
62
71
|
import { isMarkdownFile, packSections, sectionFindings, sectionGroups } from './sections.mjs';
|
|
63
72
|
import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.mjs';
|
|
64
73
|
import { spawnWithTimeoutRetry } from './session-retry.mjs';
|
|
65
|
-
import { afterSchemaMiss, logSecondLevelMiss } from './schema-fallback.mjs';
|
|
74
|
+
import { afterSchemaMiss, logSecondLevelMiss, packetHash } from './schema-fallback.mjs';
|
|
66
75
|
import { assertRowPath } from '../worker/ticket.mjs';
|
|
67
76
|
|
|
68
77
|
/**
|
|
@@ -253,8 +262,11 @@ export async function reviewFile(input, deps) {
|
|
|
253
262
|
deleted = renderDeletionNote(diff.file, { status: 'not_checked', paths: [], more: 0 }); // the review goes on
|
|
254
263
|
}
|
|
255
264
|
}
|
|
265
|
+
// B92: a test that reads a source file and asserts on its text gets the `## test-quality` hint
|
|
266
|
+
// (line numbers only), read once from the current content; '' for none
|
|
267
|
+
const quality = testQualityNote(diff.file, diff.content);
|
|
256
268
|
/** @param {ReadonlyArray<number> | null} keys - a Markdown section's group keys; null ⇒ the whole file. */
|
|
257
|
-
const movedOf = (keys) => [deleted, movedText(moves, diff.content, keys)].filter((t) => typeof t === 'string' && t.length > 0).join('\n');
|
|
269
|
+
const movedOf = (keys) => [deleted, movedText(moves, diff.content, keys), quality].filter((t) => typeof t === 'string' && t.length > 0).join('\n');
|
|
258
270
|
const min = minTokensOut(cfg);
|
|
259
271
|
// B55: never in consensus mode — its providers (two reviewers, the judge) are the check itself
|
|
260
272
|
const schemaFallback = plan.mode !== 'consensus';
|
|
@@ -383,8 +395,9 @@ export async function reviewFile(input, deps) {
|
|
|
383
395
|
}
|
|
384
396
|
|
|
385
397
|
/**
|
|
386
|
-
* One session: packet file (0600, removed after), spawn, stub guard — and, on a `schema` miss
|
|
387
|
-
* B55 second-level try (`schema-fallback.mjs`) on the same packet
|
|
398
|
+
* One session: packet file (0600, removed after), spawn, stub guard — and, on a `schema` miss or
|
|
399
|
+
* (B91) a timed-out attempt, the B55 second-level try (`schema-fallback.mjs`) on the same packet
|
|
400
|
+
* file before it is removed.
|
|
388
401
|
* @param {SessionSpec} spec @param {string} text
|
|
389
402
|
* @param {{hunkHeaders: string[], tokensIn: number, contextMode: string | null}} packet
|
|
390
403
|
* @param {{deps: ReviewDeps, workDir: string, min: number, cfg: Record<string, any>, schemaFallback?: boolean}} env -
|
|
@@ -401,10 +414,39 @@ async function runOne(spec, text, packet, { deps, workDir, min, cfg, schemaFallb
|
|
|
401
414
|
let verdict = { ok: false, reason: 'exit', detail: 'no session result' };
|
|
402
415
|
/** @type {{level: string, provider: string, model: string} | null} */
|
|
403
416
|
let fellBackTo = null;
|
|
404
|
-
|
|
405
|
-
|
|
417
|
+
const session = { lens: spec.lens, role: spec.role, level: spec.level };
|
|
418
|
+
// no ledger writer ⇒ nothing logged, nothing tried; every row goes through the same writer
|
|
419
|
+
// (each helper handles its own failed write)
|
|
420
|
+
const write = deps.writeRow ? (/** @type {Record<string, any>} */ row) => /** @type {NonNullable<ReviewDeps['writeRow']>} */ (deps.writeRow)(row) : undefined;
|
|
421
|
+
const hash = packetHash(text);
|
|
422
|
+
/** @type {{level: string, provider: string, model: string} | null} the second level once its try started */
|
|
423
|
+
let tried = null;
|
|
424
|
+
/** @param {Record<string, any> | null} missed @param {'schema' | 'timeout'} trigger */
|
|
425
|
+
const tryNext = (missed, trigger) =>
|
|
426
|
+
afterSchemaMiss({
|
|
427
|
+
res: missed,
|
|
428
|
+
session,
|
|
429
|
+
packetText: text,
|
|
430
|
+
cfg: spec.cfg ?? cfg,
|
|
431
|
+
allowed: schemaFallback && !spec.cfg,
|
|
432
|
+
history: deps.schemaHistory,
|
|
433
|
+
writeRow: write,
|
|
434
|
+
// the second level's attempts follow the ones already run in this session
|
|
435
|
+
spawnAt: (c, to) => {
|
|
436
|
+
tried = to; // the try started: whatever happens next, the session fell back
|
|
437
|
+
return attempt(c, attempts, true);
|
|
438
|
+
},
|
|
439
|
+
trigger,
|
|
440
|
+
});
|
|
441
|
+
/**
|
|
442
|
+
* @param {Record<string, any> | undefined} sessionCfg @param {number} done - attempts before this call.
|
|
443
|
+
* @param {boolean} [secondLevel] - B91: the second-level try (its timeouts never count, never try again).
|
|
444
|
+
* @returns {ReturnType<typeof spawnWithTimeoutRetry>}
|
|
445
|
+
*/
|
|
446
|
+
const attempt = (sessionCfg, done, secondLevel = false) =>
|
|
406
447
|
// a `timeout` is spawned once more on the same packet and level (B30); a spawn that throws
|
|
407
|
-
// (bad config, refused argv) is an `exit` failure, never approval
|
|
448
|
+
// (bad config, refused argv) is an `exit` failure, never approval. B91: each timed-out attempt
|
|
449
|
+
// is a broken result of this packet; the second one tries the second level instead
|
|
408
450
|
spawnWithTimeoutRetry(
|
|
409
451
|
deps.spawn,
|
|
410
452
|
{
|
|
@@ -419,6 +461,7 @@ async function runOne(spec, text, packet, { deps, workDir, min, cfg, schemaFallb
|
|
|
419
461
|
(n) => {
|
|
420
462
|
attempts = done + n;
|
|
421
463
|
},
|
|
464
|
+
{ packetHash: hash, secondLevel, ...(secondLevel ? {} : { onBroken: (/** @type {Record<string, any>} */ r) => tryNext(r, 'timeout') }) },
|
|
422
465
|
);
|
|
423
466
|
/** @type {number | null} the session's tokens: the first spawn's, plus a second-level try's */
|
|
424
467
|
let tokensIn = null;
|
|
@@ -426,34 +469,42 @@ async function runOne(spec, text, packet, { deps, workDir, min, cfg, schemaFallb
|
|
|
426
469
|
let tokensOut = null;
|
|
427
470
|
// the packet file is removed on every path, and only after the second-level try (which reads it)
|
|
428
471
|
try {
|
|
472
|
+
/** @type {Awaited<ReturnType<typeof spawnWithTimeoutRetry>> | null} */
|
|
473
|
+
let first = null;
|
|
429
474
|
try {
|
|
430
|
-
|
|
475
|
+
first = await attempt(spec.cfg, 0);
|
|
476
|
+
({ res, attempts } = first);
|
|
431
477
|
} catch {
|
|
432
|
-
// defensive only: the
|
|
433
|
-
// never approval, with the attempts
|
|
478
|
+
// defensive only: the helpers never throw. If they ever did, it is still an `exit` failure,
|
|
479
|
+
// never approval, with the attempts reported so far (at least 1; a second-level try's spawns
|
|
480
|
+
// included) and, once that try started, its second level (never a same-model spawn after it).
|
|
434
481
|
res = null;
|
|
435
482
|
attempts = Math.max(attempts, 1);
|
|
483
|
+
fellBackTo = tried;
|
|
484
|
+
}
|
|
485
|
+
if (first?.to) {
|
|
486
|
+
// B91: a timed-out attempt was the packet's second broken result; the second level ran and
|
|
487
|
+
// `attempts` already counts its spawns. The verdict is set first: whatever throws after it
|
|
488
|
+
// leaves that verdict (a failed one stays a non-approval), never an exception
|
|
489
|
+
fellBackTo = first.to;
|
|
490
|
+
verdict = { ok: false, reason: 'exit', detail: 'second-level result not checked' };
|
|
491
|
+
try {
|
|
492
|
+
verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
493
|
+
tokensIn = addTokens(first.broken?.usage?.tokens_in ?? null, res?.usage?.tokens_in);
|
|
494
|
+
tokensOut = addTokens(first.broken?.usage?.tokens_out ?? null, res?.usage?.tokens_out);
|
|
495
|
+
if (!verdict.ok && verdict.reason === 'schema') await logSecondLevelMiss({ res, session, packetText: text, writeRow: write });
|
|
496
|
+
} catch {
|
|
497
|
+
// defensive only, as on the schema path below
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
tokensIn = res?.usage?.tokens_in ?? null;
|
|
501
|
+
tokensOut = res?.usage?.tokens_out ?? null;
|
|
502
|
+
verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
436
503
|
}
|
|
437
|
-
|
|
438
|
-
tokensOut = res?.usage?.tokens_out ?? null;
|
|
439
|
-
verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
440
|
-
if (!verdict.ok && verdict.reason === 'schema') {
|
|
504
|
+
if (!fellBackTo && !verdict.ok && verdict.reason === 'schema') {
|
|
441
505
|
try {
|
|
442
|
-
const session = { lens: spec.lens, role: spec.role, level: spec.level };
|
|
443
506
|
const done = attempts;
|
|
444
|
-
|
|
445
|
-
// (each helper handles its own failed write)
|
|
446
|
-
const write = deps.writeRow ? (/** @type {Record<string, any>} */ row) => /** @type {NonNullable<ReviewDeps['writeRow']>} */ (deps.writeRow)(row) : undefined;
|
|
447
|
-
const second = await afterSchemaMiss({
|
|
448
|
-
res,
|
|
449
|
-
session,
|
|
450
|
-
packetText: text,
|
|
451
|
-
cfg: spec.cfg ?? cfg,
|
|
452
|
-
allowed: schemaFallback && !spec.cfg,
|
|
453
|
-
history: deps.schemaHistory,
|
|
454
|
-
writeRow: write,
|
|
455
|
-
spawnAt: (c) => attempt(c, done),
|
|
456
|
-
});
|
|
507
|
+
const second = await tryNext(res, 'schema');
|
|
457
508
|
if (second) {
|
|
458
509
|
const secondVerdict = validateReview(second.res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
459
510
|
res = second.res;
|
|
@@ -467,6 +518,7 @@ async function runOne(spec, text, packet, { deps, workDir, min, cfg, schemaFallb
|
|
|
467
518
|
} catch {
|
|
468
519
|
// defensive only: the helpers never throw. If they ever did, the first session's result
|
|
469
520
|
// and its `schema` verdict stand (never turned into `exit`, never approval)
|
|
521
|
+
fellBackTo = fellBackTo ?? tried;
|
|
470
522
|
}
|
|
471
523
|
}
|
|
472
524
|
} finally {
|
package/src/review/fixloop.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import { isMarkdownFile, packSections, sectionFindings, sectionGroups, sectionOf
|
|
|
97
97
|
import { cameFromJudge, resolvedBand, triageFindings } from './triage.mjs';
|
|
98
98
|
import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.mjs';
|
|
99
99
|
import { spawnWithTimeoutRetry } from './session-retry.mjs';
|
|
100
|
-
import { afterSchemaMiss, logSecondLevelMiss } from './schema-fallback.mjs';
|
|
100
|
+
import { afterSchemaMiss, logSecondLevelMiss, packetHash } from './schema-fallback.mjs';
|
|
101
101
|
|
|
102
102
|
/** @typedef {import('./triage.mjs').Finding} Finding */
|
|
103
103
|
/** @typedef {import('./context.mjs').Hunk} Hunk */
|
|
@@ -431,8 +431,9 @@ export function insideHunks(f, hunks) {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
/**
|
|
434
|
-
* One recheck session through the stub guard — and, on a `schema` miss
|
|
435
|
-
*
|
|
434
|
+
* One recheck session through the stub guard — and, on a `schema` miss or (B91) a timed-out
|
|
435
|
+
* attempt that is the packet's second broken result, the B55 second-level try on the same packet
|
|
436
|
+
* (`schema-fallback.mjs`; rechecks and the patch_check alike).
|
|
436
437
|
* @param {{text: string, hunkHeaders: string[], tokensIn: number, contextMode: string}} packet
|
|
437
438
|
* @param {LoopDeps} deps
|
|
438
439
|
*/
|
|
@@ -442,14 +443,28 @@ async function recheckSession(packet, deps) {
|
|
|
442
443
|
writeFileSync(promptPath, packet.text, { mode: 0o600 });
|
|
443
444
|
const min = minTokensOut(deps.cfg);
|
|
444
445
|
const session = { lens: 'recheck', role: 'reviewer', level: 'L2' };
|
|
445
|
-
|
|
446
|
+
// no ledger writer ⇒ nothing logged, nothing tried; every row goes through the same writer
|
|
447
|
+
// (each helper handles its own failed write)
|
|
448
|
+
const write = deps.writeRow ? (/** @type {Record<string, any>} */ row) => /** @type {NonNullable<LoopDeps['writeRow']>} */ (deps.writeRow)(row) : undefined;
|
|
449
|
+
const hash = packetHash(packet.text);
|
|
450
|
+
/** @param {Record<string, any> | null} missed @param {'schema' | 'timeout'} trigger */
|
|
451
|
+
const tryNext = (missed, trigger) =>
|
|
452
|
+
afterSchemaMiss({ res: missed, session, packetText: packet.text, cfg: deps.cfg ?? {}, allowed: true /* rechecks are never consensus; the budget gate is inside the spawn */, history: deps.schemaHistory, writeRow: write, spawnAt: attempt, trigger });
|
|
453
|
+
/**
|
|
454
|
+
* @param {Record<string, any> | undefined} cfg - set only for the second-level try (B91: its
|
|
455
|
+
* timeouts never count and never try again).
|
|
456
|
+
* @returns {Promise<{res: Record<string, any> | null, attempts: number, to?: Record<string, any>, broken?: Record<string, any>}>}
|
|
457
|
+
*/
|
|
446
458
|
const attempt = async (cfg) =>
|
|
447
459
|
deps.spawn
|
|
448
|
-
? // a `timeout` is spawned once more on the same packet and level (B30)
|
|
460
|
+
? // a `timeout` is spawned once more on the same packet and level (B30); B91: the packet's
|
|
461
|
+
// second broken result tries the second level instead
|
|
449
462
|
spawnWithTimeoutRetry(
|
|
450
463
|
deps.spawn,
|
|
451
464
|
{ level: 'L2', role: 'reviewer', promptPath, schema: FINDING_SCHEMA, rowExtra: { lens: 'recheck', context_mode: packet.contextMode, ctx_tokens_in: packet.tokensIn }, ...(cfg ? { cfg } : {}) },
|
|
452
465
|
(row) => note(deps, row),
|
|
466
|
+
undefined,
|
|
467
|
+
cfg ? { packetHash: hash, secondLevel: true } : { packetHash: hash, onBroken: (r) => tryNext(r, 'timeout') },
|
|
453
468
|
)
|
|
454
469
|
: { res: null, attempts: 0 };
|
|
455
470
|
/** @type {Record<string, any> | null} */
|
|
@@ -462,20 +477,37 @@ async function recheckSession(packet, deps) {
|
|
|
462
477
|
let tokensOut = null;
|
|
463
478
|
// the packet file is removed on every path, and only after the second-level try (which reads it)
|
|
464
479
|
try {
|
|
480
|
+
/** @type {{res: Record<string, any> | null, attempts: number, to?: Record<string, any>, broken?: Record<string, any>} | null} */
|
|
481
|
+
let first = null;
|
|
465
482
|
try {
|
|
466
|
-
|
|
483
|
+
first = await attempt(undefined);
|
|
484
|
+
res = first.res;
|
|
467
485
|
} catch {
|
|
468
486
|
res = null; // the helper never throws; if it ever did, it is still an `exit` failure
|
|
469
487
|
}
|
|
470
|
-
tokensIn = res?.usage?.tokens_in ?? null;
|
|
471
|
-
tokensOut = res?.usage?.tokens_out ?? null;
|
|
472
488
|
verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
473
|
-
if (
|
|
489
|
+
if (first?.to) {
|
|
490
|
+
// B91: a timed-out attempt was the packet's second broken result; the second level ran. The
|
|
491
|
+
// verdict is already set: whatever throws after it leaves that verdict, never an exception
|
|
492
|
+
// the second level's miss is logged first, in its own try, so no sum error can skip it
|
|
474
493
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
494
|
+
if (!verdict.ok && verdict.reason === 'schema') await logSecondLevelMiss({ res, session, packetText: packet.text, writeRow: write });
|
|
495
|
+
} catch {
|
|
496
|
+
// defensive only: the helper never throws; the verdict above stands (never approval)
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
tokensIn = sum(first.broken?.usage?.tokens_in ?? null, res?.usage?.tokens_in);
|
|
500
|
+
tokensOut = sum(first.broken?.usage?.tokens_out ?? null, res?.usage?.tokens_out);
|
|
501
|
+
} catch {
|
|
502
|
+
// defensive only, as on the schema path below
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
tokensIn = res?.usage?.tokens_in ?? null;
|
|
506
|
+
tokensOut = res?.usage?.tokens_out ?? null;
|
|
507
|
+
}
|
|
508
|
+
if (!first?.to && !verdict.ok && verdict.reason === 'schema') {
|
|
509
|
+
try {
|
|
510
|
+
const second = await tryNext(res, 'schema');
|
|
479
511
|
if (second) {
|
|
480
512
|
// compute everything first, then assign together, so a throw never leaves a mixed state
|
|
481
513
|
const secondVerdict = validateReview(second.res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
@@ -93,6 +93,7 @@ import { tierFor } from '../proof/tiers.mjs';
|
|
|
93
93
|
import { DELETED_HASH, contentHash, gitChildEnv } from '../worker/ticket.mjs';
|
|
94
94
|
import { verifyResult } from '../worker/queue.mjs';
|
|
95
95
|
import { stopExitsLine } from './stop-exits.mjs';
|
|
96
|
+
import { checkFileForm, checkFindingId } from './verb-forms.mjs';
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
99
|
* @typedef {{file: string, content_hash: string, status: 'reviewed_by_waiver', findings: string[], by: 'human' | 'autopilot' | null, answered?: string[]}} WaivedAll
|
|
@@ -1055,10 +1056,10 @@ export function reviewGateCheck(load, { onWaivedAll } = {}) {
|
|
|
1055
1056
|
* @returns {Promise<Record<string, any>>} the signed row.
|
|
1056
1057
|
*/
|
|
1057
1058
|
export async function waiveFinding({ runId, id, finding, reason, file, writeRow }) {
|
|
1058
|
-
|
|
1059
|
+
checkFindingId(finding);
|
|
1059
1060
|
if (typeof reason !== 'string' || reason.trim().length === 0) throw new StateError('usage', 'block waive needs --reason');
|
|
1060
1061
|
if (typeof file !== 'string' || file.length === 0) throw new StateError('usage', 'block waive needs --file <path>');
|
|
1061
|
-
|
|
1062
|
+
checkFileForm(file, { normalized: false });
|
|
1062
1063
|
const record = await readRun(runId);
|
|
1063
1064
|
if (!record.blocks?.[id]) throw new StateError('unknown_block', `block ${id} is not in run ${runId}`);
|
|
1064
1065
|
const row = { event: 'review.waived', block: id, file, finding, reason: reason.trim(), by: 'human' };
|
package/src/review/lenses/A.md
CHANGED
|
@@ -20,3 +20,4 @@ You work blind: you do not see the other reviewer's answer.
|
|
|
20
20
|
- Every finding names `file`, `line_start`, `line_end` (line numbers of the CURRENT file, as numbered in the context), `severity` (`critical` = wrong behaviour, data loss, security; `warning` = a real defect that is not critical; `nit` = style or preference), a short `category`, the `claim`, the `evidence` you read in the packet, and the `fix`. Each of `claim`, `evidence`, `fix` ≤ 300 chars.
|
|
21
21
|
- `passed` is true only when you found no `critical` and no `warning`.
|
|
22
22
|
- Do not invent a problem to look thorough. An empty `findings` list is a valid answer.
|
|
23
|
+
- A `## test-quality` section names lines of a test that reads a source file and asserts on its text. Check those lines: such a test is a `warning` with the claim "test reads source text; prove behaviour instead", unless the read file is a fixture. The section is a hint, not a finding.
|
package/src/review/lenses/B.md
CHANGED
|
@@ -21,3 +21,4 @@ You work blind: you do not see the other reviewer's answer.
|
|
|
21
21
|
- Every finding names `file`, `line_start`, `line_end` (line numbers of the CURRENT file, as numbered in the context), `severity` (`critical` = wrong behaviour, data loss, security; `warning` = a real defect that is not critical; `nit` = style or preference), a short `category`, the `claim`, the `evidence` you read in the packet, and the `fix`. Each of `claim`, `evidence`, `fix` ≤ 300 chars.
|
|
22
22
|
- `passed` is true only when you found no `critical` and no `warning`.
|
|
23
23
|
- Do not invent a problem to look thorough. An empty `findings` list is a valid answer.
|
|
24
|
+
- A `## test-quality` section names lines of a test that reads a source file and asserts on its text. Check those lines: such a test is a `warning` with the claim "test reads source text; prove behaviour instead", unless the read file is a fixture. The section is a hint, not a finding.
|
|
@@ -21,3 +21,4 @@ Report each real defect once, at the lines where it lives.
|
|
|
21
21
|
- Every finding names `file`, `line_start`, `line_end` (line numbers of the CURRENT file, as numbered in the context), `severity` (`critical` = wrong behaviour, data loss, security; `warning` = a real defect that is not critical; `nit` = style or preference), a short `category`, the `claim`, the `evidence` you read in the packet, and the `fix`. Each of `claim`, `evidence`, `fix` ≤ 300 chars.
|
|
22
22
|
- `passed` is true only when you found no `critical` and no `warning`.
|
|
23
23
|
- Do not invent a problem to look thorough. An empty `findings` list is a valid answer.
|
|
24
|
+
- A `## test-quality` section names lines of a test that reads a source file and asserts on its text. Check those lines: such a test is a `warning` with the claim "test reads source text; prove behaviour instead", unless the read file is a fixture. The section is a hint, not a finding.
|
|
@@ -19,3 +19,4 @@ Skip style, naming and structure unless it hides a bug. Keep `findings` short.
|
|
|
19
19
|
- Every finding names `file`, `line_start`, `line_end` (line numbers of the CURRENT file, as numbered in the context), `severity` (`critical` = wrong behaviour, data loss, security; `warning` = a real defect that is not critical; `nit` = style or preference), a short `category`, the `claim`, the `evidence` you read in the packet, and the `fix`. Each of `claim`, `evidence`, `fix` ≤ 300 chars.
|
|
20
20
|
- `passed` is true only when you found no `critical` and no `warning`.
|
|
21
21
|
- Do not invent a problem to look thorough. An empty `findings` list is a valid answer.
|
|
22
|
+
- A `## test-quality` section names lines of a test that reads a source file and asserts on its text. Check those lines: such a test is a `warning` with the claim "test reads source text; prove behaviour instead", unless the read file is a fixture. The section is a hint, not a finding.
|
|
@@ -21,3 +21,4 @@ that changed since that round, each with a window of the current file around it.
|
|
|
21
21
|
- You have no tools and no project files. The packet is everything you get.
|
|
22
22
|
- `reviewed_hunks`: copy every `@@ … @@` hunk header listed under "hunks" in the packet, exactly and in the same order.
|
|
23
23
|
- Answer with ONE JSON object that matches the answer schema.
|
|
24
|
+
- A `## test-quality` section names lines of a test that reads a source file and asserts on its text. Check those lines: such a test is a `warning` with the claim "test reads source text; prove behaviour instead", unless the read file is a fixture. The section is a hint, not a finding.
|