@verevoir/review 0.2.0 → 0.3.1
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 +132 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +46 -0
- package/dist/cli/footer.d.ts +11 -0
- package/dist/cli/footer.js +68 -0
- package/dist/cli/review.d.ts +45 -0
- package/dist/cli/review.js +518 -0
- package/dist/context-assembler.d.ts +9 -0
- package/dist/context-assembler.js +118 -0
- package/dist/context-files.d.ts +33 -0
- package/dist/context-files.js +174 -0
- package/dist/context-types.d.ts +28 -0
- package/dist/context-types.js +5 -0
- package/dist/dedup.d.ts +46 -0
- package/dist/dedup.js +169 -0
- package/dist/diff-hunks.d.ts +17 -0
- package/dist/diff-hunks.js +73 -0
- package/dist/fs-source-access.d.ts +7 -0
- package/dist/fs-source-access.js +39 -0
- package/dist/parseReply.js +58 -7
- package/dist/practice-lenses.d.ts +78 -0
- package/dist/practice-lenses.js +230 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/prompt.js +18 -0
- package/dist/references.d.ts +44 -0
- package/dist/references.js +312 -0
- package/dist/report.d.ts +17 -1
- package/dist/report.js +64 -7
- package/dist/reviewer-anthropic.js +7 -1
- package/dist/run-review.d.ts +24 -0
- package/dist/run-review.js +44 -2
- package/dist/source-access.d.ts +8 -0
- package/dist/source-access.js +1 -0
- package/dist/subject-context-render.d.ts +8 -0
- package/dist/subject-context-render.js +29 -0
- package/dist/tier.d.ts +35 -0
- package/dist/tier.js +172 -0
- package/dist/verdict.d.ts +3 -0
- package/dist/warm-first.d.ts +2 -0
- package/dist/warm-first.js +21 -0
- package/lenses.default.json +30 -0
- package/package.json +5 -1
- package/src/cli/bin.ts +61 -0
- package/src/cli/footer.ts +88 -0
- package/src/cli/review.ts +656 -0
- package/src/context-files.ts +194 -0
- package/src/dedup.ts +207 -0
- package/src/parseReply.ts +58 -7
- package/src/practice-lenses.ts +286 -0
- package/src/prompt.ts +47 -0
- package/src/references.ts +381 -0
- package/src/report.ts +81 -7
- package/src/reviewer-anthropic.ts +7 -1
- package/src/run-review.ts +70 -7
- package/src/tier.ts +207 -0
- package/src/verdict.ts +3 -0
- package/src/warm-first.ts +27 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Derives a review lens set from a practice corpus: one lens per distinct
|
|
2
|
+
// `lens:` front-matter value, its focus being a fixed adversarial framing
|
|
3
|
+
// plus the SENSOR text of the practices in that group.
|
|
4
|
+
//
|
|
5
|
+
// PORTED, NOT IMPORTED, from aigency-runtime-v2 (BL-424 trial):
|
|
6
|
+
// - parseLensFrontmatter / FRONTMATTER_RE / LENS_LINE_RE
|
|
7
|
+
// <- src/compose/guardrails-corpus.ts
|
|
8
|
+
// - extractSensorHalf <- src/compose/practice-guides.ts
|
|
9
|
+
// - the "## Practices this lens checks" append and the placeholder for a
|
|
10
|
+
// practice with no sensor text <- src/review/lens-practice-partition.ts
|
|
11
|
+
// (withPractices)
|
|
12
|
+
//
|
|
13
|
+
// FRONT-MATTER: no YAML library. A practice file opens with a `---` block;
|
|
14
|
+
// the only field read is a top-level `lens: <value>` line (one matching pair
|
|
15
|
+
// of surrounding quotes is stripped), exactly as v2 reads it. Other fields
|
|
16
|
+
// are ignored. This is the subset v2 itself understands.
|
|
17
|
+
//
|
|
18
|
+
// A practice with no usable `lens:` is NOT reviewed (v2 routes its sensor to
|
|
19
|
+
// no lens) and is reported in `untagged`. A tagged practice with no sensor
|
|
20
|
+
// text is reported in `missingSensor`; v2 fails closed on that and so does
|
|
21
|
+
// the CLI when it runs a review.
|
|
22
|
+
//
|
|
23
|
+
// PURE: file names and texts come in, a derivation goes out.
|
|
24
|
+
|
|
25
|
+
import type { Lens, LensSet } from './lensSet.js';
|
|
26
|
+
|
|
27
|
+
export interface PracticeFile {
|
|
28
|
+
/** File name, e.g. `failure-legibility.md`. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly text: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LensGroup {
|
|
34
|
+
readonly lensId: string;
|
|
35
|
+
readonly practiceIds: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DerivedLenses {
|
|
39
|
+
readonly lensSet: LensSet;
|
|
40
|
+
readonly groups: readonly LensGroup[];
|
|
41
|
+
/** Practice ids with no `lens:` value; reviewed by nothing. */
|
|
42
|
+
readonly untagged: readonly string[];
|
|
43
|
+
/** Tagged practice ids whose file has no Sensor / Look for text. */
|
|
44
|
+
readonly missingSensor: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* Per lens id: the text of every `_Reject:_` / `_Expect:_` signal line in that lens's
|
|
47
|
+
* practices' sensors (label stripped). A [blocking] finding must quote one of these.
|
|
48
|
+
*/
|
|
49
|
+
readonly signals: Readonly<Record<string, readonly string[]>>;
|
|
50
|
+
/** Per lens id: only the `_Reject:_` lines of `signals`. A smell quoting one is promoted. */
|
|
51
|
+
readonly rejects: Readonly<Record<string, readonly string[]>>;
|
|
52
|
+
/** Every practice id in the corpus (tagged or not): ignored when matching a quote. */
|
|
53
|
+
readonly practiceIds: readonly string[];
|
|
54
|
+
/** Per lens id: its practices' ids and H1 titles ('' when none), to name them in a report. */
|
|
55
|
+
readonly practiceNames: Readonly<
|
|
56
|
+
Record<string, readonly { readonly id: string; readonly title: string }[]>
|
|
57
|
+
>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
|
61
|
+
const LENS_LINE_RE = /^lens:[ \t]*(.+?)[ \t]*$/m;
|
|
62
|
+
|
|
63
|
+
const GUIDE_HEADING = '**Guide**';
|
|
64
|
+
const SENSOR_HEADING = '**Sensor**';
|
|
65
|
+
const LOOK_FOR_HEADING = '**Look for**';
|
|
66
|
+
|
|
67
|
+
/** Anchors the tier to a quoted signal, so the same finding does not flip run to run. */
|
|
68
|
+
export const TIER_ANCHOR =
|
|
69
|
+
"Tag a finding [blocking] only when you quote the signal it fails: a practice's " +
|
|
70
|
+
'_Reject:_ line anywhere, or its _Expect:_ line for lines this change adds or modifies. ' +
|
|
71
|
+
'A finding that quotes no signal is a [smell].';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The fixed framing every derived lens opens with. The last paragraph is glue:
|
|
75
|
+
* it maps the corpus README's provenance rule onto this diff and the reply
|
|
76
|
+
* tags, and is NOT a copy of the README (the README's grading text reaches the
|
|
77
|
+
* lens as "The bar").
|
|
78
|
+
*/
|
|
79
|
+
export function framingFor(lensId: string): string {
|
|
80
|
+
return (
|
|
81
|
+
`Review this change adversarially for the "${lensId}" dimension: assume it is wrong, ` +
|
|
82
|
+
'and look for a concrete defect that violates one of the practices below. ' +
|
|
83
|
+
'Report only what you can point to in the diff.\n\n' +
|
|
84
|
+
'Provenance sets the bar. Lines this change adds or modifies are generated work, held to ' +
|
|
85
|
+
'Expect; unchanged context is taken-on work, held to Accept. A Reject signal is ' +
|
|
86
|
+
'[blocking] anywhere, and so is an Expect gap in added or modified lines; an ' +
|
|
87
|
+
'Accept-but-not-Expect gap in untouched context is a [smell] recommendation. ' +
|
|
88
|
+
'An untouched line counts as part of this change only where the change makes it untrue; ' +
|
|
89
|
+
'name the changed line that does so, otherwise judge it as untouched context. ' +
|
|
90
|
+
TIER_ANCHOR +
|
|
91
|
+
' Name the practice each finding violates. ' +
|
|
92
|
+
'Before your verdict line, add one line `REASON: <one sentence on what you checked and ' +
|
|
93
|
+
'why you approve or reject>`.'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SIGNALS_HEADING = '## Signals: reject / accept / expect';
|
|
98
|
+
const PROVENANCE_MARKER = '**The two halves are keyed to different tiers.**';
|
|
99
|
+
|
|
100
|
+
export type BarResult =
|
|
101
|
+
{ readonly ok: true; readonly text: string } | { readonly ok: false; readonly reason: string };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The grading rules from the corpus README (practices/README.md): the whole
|
|
105
|
+
* "## Signals: reject / accept / expect" section (to the next `## ` heading),
|
|
106
|
+
* plus the paragraph opening "**The two halves are keyed to different tiers.**"
|
|
107
|
+
* (which lives under "## Guide and sensor"; only that paragraph, to the next
|
|
108
|
+
* blank line). Either missing is a failure with a reason, never a fallback.
|
|
109
|
+
*/
|
|
110
|
+
export function extractBar(readme: string): BarResult {
|
|
111
|
+
const lines = readme.split(/\r?\n/);
|
|
112
|
+
const start = lines.findIndex((l) => l.trim() === SIGNALS_HEADING);
|
|
113
|
+
if (start === -1) {
|
|
114
|
+
return { ok: false, reason: `the README has no "${SIGNALS_HEADING}" section` };
|
|
115
|
+
}
|
|
116
|
+
let end = lines.length;
|
|
117
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
118
|
+
if ((lines[i] ?? '').startsWith('## ')) {
|
|
119
|
+
end = i;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const signals = lines.slice(start, end).join('\n').trim();
|
|
124
|
+
if (signals === SIGNALS_HEADING) {
|
|
125
|
+
return { ok: false, reason: `the README's "${SIGNALS_HEADING}" section is empty` };
|
|
126
|
+
}
|
|
127
|
+
if (signals.includes(PROVENANCE_MARKER)) return { ok: true, text: signals };
|
|
128
|
+
const at = readme.indexOf(PROVENANCE_MARKER);
|
|
129
|
+
if (at === -1) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: `the README has no paragraph beginning "${PROVENANCE_MARKER}"`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const rest = readme.slice(at);
|
|
136
|
+
const blank = rest.search(/\r?\n[ \t]*\r?\n/);
|
|
137
|
+
const paragraph = (blank === -1 ? rest : rest.slice(0, blank)).trim();
|
|
138
|
+
return { ok: true, text: `${signals}\n\n${paragraph}` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const H1_RE = /^# (.+?)[ \t]*$/m;
|
|
142
|
+
|
|
143
|
+
/** The first `# Title` line after any front-matter block, or undefined. */
|
|
144
|
+
export function parseTitle(markdown: string): string | undefined {
|
|
145
|
+
const body = markdown.replace(FRONTMATTER_RE, '');
|
|
146
|
+
const match = H1_RE.exec(body);
|
|
147
|
+
const title = match?.[1]?.trim();
|
|
148
|
+
return title === undefined || title === '' ? undefined : title;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function parseLensFrontmatter(markdown: string): string | undefined {
|
|
152
|
+
const match = FRONTMATTER_RE.exec(markdown);
|
|
153
|
+
if (!match) return undefined;
|
|
154
|
+
const lensMatch = LENS_LINE_RE.exec(match[1] ?? '');
|
|
155
|
+
if (!lensMatch) return undefined;
|
|
156
|
+
const raw = (lensMatch[1] ?? '').trim();
|
|
157
|
+
const unquoted =
|
|
158
|
+
raw.length >= 2 &&
|
|
159
|
+
((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'")))
|
|
160
|
+
? raw.slice(1, -1)
|
|
161
|
+
: raw;
|
|
162
|
+
return unquoted.trim() === '' ? undefined : unquoted.trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** From `**Sensor**` (else `**Look for**`) to the next `**Guide**` or end of file. */
|
|
166
|
+
export function extractSensorHalf(markdown: string): string | undefined {
|
|
167
|
+
let heading = SENSOR_HEADING;
|
|
168
|
+
let start = markdown.indexOf(SENSOR_HEADING);
|
|
169
|
+
if (start === -1) {
|
|
170
|
+
heading = LOOK_FOR_HEADING;
|
|
171
|
+
start = markdown.indexOf(LOOK_FOR_HEADING);
|
|
172
|
+
}
|
|
173
|
+
if (start === -1) return undefined;
|
|
174
|
+
const rest = markdown.slice(start);
|
|
175
|
+
const nextGuide = rest.indexOf(GUIDE_HEADING, heading.length);
|
|
176
|
+
const slice = nextGuide === -1 ? rest : rest.slice(0, nextGuide);
|
|
177
|
+
if (slice.slice(heading.length).trim() === '') return undefined;
|
|
178
|
+
return slice.trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const SIGNAL_LINE_RE = /^\s*(?:[-*+]\s+)?[_*]{1,2}(Reject|Expect):[_*]{1,2}\s*(.*)$/i;
|
|
182
|
+
const BLOCK_START_RE = /^\s*(?:[-*+]\s|\d+\.\s|#|\*\*)/;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The text of each `_Reject:_` / `_Expect:_` line in a sensor half, label stripped. Indented
|
|
186
|
+
* continuation lines (not a new bullet, heading or bold label) are joined onto their line.
|
|
187
|
+
*/
|
|
188
|
+
export function extractSignalLines(sensor: string): string[] {
|
|
189
|
+
return extractLabelledSignals(sensor).map((s) => s.text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** As `extractSignalLines`, keeping which label each line carried. */
|
|
193
|
+
export function extractLabelledSignals(
|
|
194
|
+
sensor: string,
|
|
195
|
+
): { readonly kind: 'reject' | 'expect'; readonly text: string }[] {
|
|
196
|
+
const out: { kind: 'reject' | 'expect'; text: string }[] = [];
|
|
197
|
+
const lines = sensor.split(/\r?\n/);
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
const match = SIGNAL_LINE_RE.exec(lines[i] ?? '');
|
|
200
|
+
if (match === null) continue;
|
|
201
|
+
const kind = (match[1] ?? '').toLowerCase() === 'reject' ? 'reject' : 'expect';
|
|
202
|
+
let text = (match[2] ?? '').trim();
|
|
203
|
+
while (i + 1 < lines.length) {
|
|
204
|
+
const next = lines[i + 1] ?? '';
|
|
205
|
+
if (next.trim() === '' || !/^\s+\S/.test(next) || BLOCK_START_RE.test(next)) break;
|
|
206
|
+
text = `${text} ${next.trim()}`;
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
if (text !== '') out.push({ kind, text });
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Group practice files by their `lens:` value and build one lens per group.
|
|
216
|
+
* Files are taken in name order, a top-level README.md and non-.md files are
|
|
217
|
+
* skipped (as v2 does), and lenses are ordered by id, so the result is
|
|
218
|
+
* deterministic.
|
|
219
|
+
*/
|
|
220
|
+
export function deriveLensSet(files: readonly PracticeFile[]): DerivedLenses {
|
|
221
|
+
const eligible = files
|
|
222
|
+
.filter((f) => f.name.endsWith('.md') && f.name.toLowerCase() !== 'readme.md')
|
|
223
|
+
.slice()
|
|
224
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
225
|
+
|
|
226
|
+
const byLens = new Map<
|
|
227
|
+
string,
|
|
228
|
+
{ id: string; title: string | undefined; sensor: string | undefined }[]
|
|
229
|
+
>();
|
|
230
|
+
const signals: Record<string, string[]> = {};
|
|
231
|
+
const rejects: Record<string, string[]> = {};
|
|
232
|
+
const practiceIds: string[] = [];
|
|
233
|
+
const practiceNames: Record<string, { id: string; title: string }[]> = {};
|
|
234
|
+
const untagged: string[] = [];
|
|
235
|
+
const missingSensor: string[] = [];
|
|
236
|
+
for (const file of eligible) {
|
|
237
|
+
const id = file.name.slice(0, -'.md'.length);
|
|
238
|
+
practiceIds.push(id);
|
|
239
|
+
const lensId = parseLensFrontmatter(file.text);
|
|
240
|
+
if (lensId === undefined) {
|
|
241
|
+
untagged.push(id);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const sensor = extractSensorHalf(file.text);
|
|
245
|
+
if (sensor === undefined) missingSensor.push(id);
|
|
246
|
+
const group = byLens.get(lensId) ?? [];
|
|
247
|
+
const title = parseTitle(file.text);
|
|
248
|
+
group.push({ id, title, sensor });
|
|
249
|
+
byLens.set(lensId, group);
|
|
250
|
+
const labelled = sensor === undefined ? [] : extractLabelledSignals(sensor);
|
|
251
|
+
(signals[lensId] ??= []).push(...labelled.map((s) => s.text));
|
|
252
|
+
(rejects[lensId] ??= []).push(
|
|
253
|
+
...labelled.filter((s) => s.kind === 'reject').map((s) => s.text),
|
|
254
|
+
);
|
|
255
|
+
(practiceNames[lensId] ??= []).push({ id, title: title ?? '' });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const lensIds = [...byLens.keys()].sort();
|
|
259
|
+
const lenses: Lens[] = [];
|
|
260
|
+
const groups: LensGroup[] = [];
|
|
261
|
+
for (const lensId of lensIds) {
|
|
262
|
+
const members = byLens.get(lensId) ?? [];
|
|
263
|
+
const body = members
|
|
264
|
+
.map((m) => {
|
|
265
|
+
const heading = `### ${m.id}${m.title === undefined ? '' : `: ${m.title}`}`;
|
|
266
|
+
return `${heading}\n\n${m.sensor ?? `(no sensor text available for ${m.id})`}`;
|
|
267
|
+
})
|
|
268
|
+
.join('\n\n');
|
|
269
|
+
lenses.push({
|
|
270
|
+
id: lensId,
|
|
271
|
+
title: lensId,
|
|
272
|
+
focus: `${framingFor(lensId)}\n\n## Practices this lens checks\n\n${body}`,
|
|
273
|
+
});
|
|
274
|
+
groups.push({ lensId, practiceIds: members.map((m) => m.id) });
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
lensSet: { version: 1, lenses },
|
|
278
|
+
groups,
|
|
279
|
+
untagged,
|
|
280
|
+
missingSensor,
|
|
281
|
+
signals,
|
|
282
|
+
rejects,
|
|
283
|
+
practiceIds,
|
|
284
|
+
practiceNames,
|
|
285
|
+
};
|
|
286
|
+
}
|
package/src/prompt.ts
CHANGED
|
@@ -78,6 +78,23 @@ export interface Subject {
|
|
|
78
78
|
* change-context assembler is PR 2.
|
|
79
79
|
*/
|
|
80
80
|
readonly citedContext?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Whole-file context for the files the diff touches (or a one-line note
|
|
83
|
+
* saying there is none), already rendered. Rendered once, in `shared`,
|
|
84
|
+
* right after the diff under `FILE_CONTEXT_HEADING`. Absent or blank omits
|
|
85
|
+
* the section entirely, so every caller before this is byte-identical.
|
|
86
|
+
*/
|
|
87
|
+
readonly fileContext?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Check results (CI) for this change, rendered verbatim once, in `shared`, under
|
|
90
|
+
* `CHECKS_HEADING` between "## The change" and "## The diff". Absent or blank omits it.
|
|
91
|
+
*/
|
|
92
|
+
readonly checks?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Resolved references found in the diff's added lines, rendered once, in `shared`,
|
|
95
|
+
* under `REFERENCES_HEADING` after the changed files. Absent or blank omits it.
|
|
96
|
+
*/
|
|
97
|
+
readonly references?: string;
|
|
81
98
|
/**
|
|
82
99
|
* BL-146: the originating item's own acceptance text, verbatim. This is
|
|
83
100
|
* the one input to a review that the run under review did not write —
|
|
@@ -127,6 +144,9 @@ export interface Prompt {
|
|
|
127
144
|
readonly shared: string;
|
|
128
145
|
/** The only part that varies by lens. Goes last. */
|
|
129
146
|
readonly perLens: string;
|
|
147
|
+
/** `false` for a call whose `shared` no other call reads (the dedup call): a transport that
|
|
148
|
+
* marks `shared` cacheable must not, as the cache write would never be read. */
|
|
149
|
+
readonly cache?: false;
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
/** The two halves in the one order that preserves the shared prefix. */
|
|
@@ -158,6 +178,15 @@ export const NO_REQUIREMENT_NOTICE =
|
|
|
158
178
|
'from outside the run to check it against. Say so plainly in your ' +
|
|
159
179
|
'reasoning, and judge only whether the change is coherent on its own terms.';
|
|
160
180
|
|
|
181
|
+
/** Heading of the shared check-results section (`Subject.checks`). */
|
|
182
|
+
export const CHECKS_HEADING = '## Checks on this change';
|
|
183
|
+
|
|
184
|
+
/** Heading of the shared resolved-references section (`Subject.references`). */
|
|
185
|
+
export const REFERENCES_HEADING = '## References in added lines';
|
|
186
|
+
|
|
187
|
+
/** Heading of the shared whole-file context section (`Subject.fileContext`). */
|
|
188
|
+
export const FILE_CONTEXT_HEADING = '## Changed files in full';
|
|
189
|
+
|
|
161
190
|
/** Heading for the round-aware section appended to `perLens` when
|
|
162
191
|
* `Subject.priorFindings` is non-empty. Exported so a test can assert on
|
|
163
192
|
* it directly rather than re-typing the literal string. */
|
|
@@ -204,6 +233,21 @@ export function buildPrompt(
|
|
|
204
233
|
? ['', '## Cited code', '', subject.citedContext]
|
|
205
234
|
: [];
|
|
206
235
|
|
|
236
|
+
const fileContextSection =
|
|
237
|
+
subject.fileContext !== undefined && subject.fileContext.trim() !== ''
|
|
238
|
+
? ['', FILE_CONTEXT_HEADING, '', subject.fileContext]
|
|
239
|
+
: [];
|
|
240
|
+
|
|
241
|
+
const checksSection =
|
|
242
|
+
subject.checks !== undefined && subject.checks.trim() !== ''
|
|
243
|
+
? ['', CHECKS_HEADING, '', subject.checks]
|
|
244
|
+
: [];
|
|
245
|
+
|
|
246
|
+
const referencesSection =
|
|
247
|
+
subject.references !== undefined && subject.references.trim() !== ''
|
|
248
|
+
? ['', REFERENCES_HEADING, '', subject.references]
|
|
249
|
+
: [];
|
|
250
|
+
|
|
207
251
|
// BL-146: NEVER omitted, unlike citedSection above -- a requirement's
|
|
208
252
|
// absence is disclosed, not silently skipped. `requirementText` is
|
|
209
253
|
// either the item's own acceptance text, verbatim (never combined with
|
|
@@ -287,12 +331,15 @@ export function buildPrompt(
|
|
|
287
331
|
`Title: ${subject.title}`,
|
|
288
332
|
'',
|
|
289
333
|
subject.description,
|
|
334
|
+
...checksSection,
|
|
290
335
|
'',
|
|
291
336
|
'## The diff',
|
|
292
337
|
'',
|
|
293
338
|
'```diff',
|
|
294
339
|
subject.diff,
|
|
295
340
|
'```',
|
|
341
|
+
...fileContextSection,
|
|
342
|
+
...referencesSection,
|
|
296
343
|
...citedSection,
|
|
297
344
|
'',
|
|
298
345
|
'## How to answer',
|