@verevoir/review 0.2.0 → 0.3.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/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 +9 -0
- package/dist/cli/footer.js +59 -0
- package/dist/cli/review.d.ts +45 -0
- package/dist/cli/review.js +507 -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 +40 -0
- package/dist/dedup.js +151 -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 +69 -0
- package/dist/practice-lenses.js +218 -0
- package/dist/prompt.d.ts +23 -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 +56 -3
- package/dist/run-review.d.ts +16 -0
- package/dist/run-review.js +37 -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 +20 -0
- package/dist/tier.js +88 -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 +78 -0
- package/src/cli/review.ts +647 -0
- package/src/context-files.ts +194 -0
- package/src/dedup.ts +187 -0
- package/src/parseReply.ts +58 -7
- package/src/practice-lenses.ts +265 -0
- package/src/prompt.ts +44 -0
- package/src/references.ts +381 -0
- package/src/report.ts +71 -3
- package/src/run-review.ts +55 -7
- package/src/tier.ts +103 -0
- package/src/warm-first.ts +27 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { LensSet } from './lensSet.js';
|
|
2
|
+
export interface PracticeFile {
|
|
3
|
+
/** File name, e.g. `failure-legibility.md`. */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LensGroup {
|
|
8
|
+
readonly lensId: string;
|
|
9
|
+
readonly practiceIds: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface DerivedLenses {
|
|
12
|
+
readonly lensSet: LensSet;
|
|
13
|
+
readonly groups: readonly LensGroup[];
|
|
14
|
+
/** Practice ids with no `lens:` value; reviewed by nothing. */
|
|
15
|
+
readonly untagged: readonly string[];
|
|
16
|
+
/** Tagged practice ids whose file has no Sensor / Look for text. */
|
|
17
|
+
readonly missingSensor: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* Per lens id: the text of every `_Reject:_` / `_Expect:_` signal line in that lens's
|
|
20
|
+
* practices' sensors (label stripped). A [blocking] finding must quote one of these.
|
|
21
|
+
*/
|
|
22
|
+
readonly signals: Readonly<Record<string, readonly string[]>>;
|
|
23
|
+
/** Per lens id: its practices' ids and H1 titles ('' when none), to name them in a report. */
|
|
24
|
+
readonly practiceNames: Readonly<Record<string, readonly {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly title: string;
|
|
27
|
+
}[]>>;
|
|
28
|
+
}
|
|
29
|
+
/** Anchors the tier to a quoted signal, so the same finding does not flip run to run. */
|
|
30
|
+
export declare const TIER_ANCHOR: string;
|
|
31
|
+
/**
|
|
32
|
+
* The fixed framing every derived lens opens with. The last paragraph is glue:
|
|
33
|
+
* it maps the corpus README's provenance rule onto this diff and the reply
|
|
34
|
+
* tags, and is NOT a copy of the README (the README's grading text reaches the
|
|
35
|
+
* lens as "The bar").
|
|
36
|
+
*/
|
|
37
|
+
export declare function framingFor(lensId: string): string;
|
|
38
|
+
export type BarResult = {
|
|
39
|
+
readonly ok: true;
|
|
40
|
+
readonly text: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly ok: false;
|
|
43
|
+
readonly reason: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The grading rules from the corpus README (practices/README.md): the whole
|
|
47
|
+
* "## Signals: reject / accept / expect" section (to the next `## ` heading),
|
|
48
|
+
* plus the paragraph opening "**The two halves are keyed to different tiers.**"
|
|
49
|
+
* (which lives under "## Guide and sensor"; only that paragraph, to the next
|
|
50
|
+
* blank line). Either missing is a failure with a reason, never a fallback.
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractBar(readme: string): BarResult;
|
|
53
|
+
/** The first `# Title` line after any front-matter block, or undefined. */
|
|
54
|
+
export declare function parseTitle(markdown: string): string | undefined;
|
|
55
|
+
export declare function parseLensFrontmatter(markdown: string): string | undefined;
|
|
56
|
+
/** From `**Sensor**` (else `**Look for**`) to the next `**Guide**` or end of file. */
|
|
57
|
+
export declare function extractSensorHalf(markdown: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The text of each `_Reject:_` / `_Expect:_` line in a sensor half, label stripped. Indented
|
|
60
|
+
* continuation lines (not a new bullet, heading or bold label) are joined onto their line.
|
|
61
|
+
*/
|
|
62
|
+
export declare function extractSignalLines(sensor: string): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Group practice files by their `lens:` value and build one lens per group.
|
|
65
|
+
* Files are taken in name order, a top-level README.md and non-.md files are
|
|
66
|
+
* skipped (as v2 does), and lenses are ordered by id, so the result is
|
|
67
|
+
* deterministic.
|
|
68
|
+
*/
|
|
69
|
+
export declare function deriveLensSet(files: readonly PracticeFile[]): DerivedLenses;
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
|
25
|
+
const LENS_LINE_RE = /^lens:[ \t]*(.+?)[ \t]*$/m;
|
|
26
|
+
const GUIDE_HEADING = '**Guide**';
|
|
27
|
+
const SENSOR_HEADING = '**Sensor**';
|
|
28
|
+
const LOOK_FOR_HEADING = '**Look for**';
|
|
29
|
+
/** Anchors the tier to a quoted signal, so the same finding does not flip run to run. */
|
|
30
|
+
export const TIER_ANCHOR = "Tag a finding [blocking] only when you quote the signal it fails: a practice's " +
|
|
31
|
+
'_Reject:_ line anywhere, or its _Expect:_ line for lines this change adds or modifies. ' +
|
|
32
|
+
'A finding that quotes no signal is a [smell].';
|
|
33
|
+
/**
|
|
34
|
+
* The fixed framing every derived lens opens with. The last paragraph is glue:
|
|
35
|
+
* it maps the corpus README's provenance rule onto this diff and the reply
|
|
36
|
+
* tags, and is NOT a copy of the README (the README's grading text reaches the
|
|
37
|
+
* lens as "The bar").
|
|
38
|
+
*/
|
|
39
|
+
export function framingFor(lensId) {
|
|
40
|
+
return (`Review this change adversarially for the "${lensId}" dimension: assume it is wrong, ` +
|
|
41
|
+
'and look for a concrete defect that violates one of the practices below. ' +
|
|
42
|
+
'Report only what you can point to in the diff.\n\n' +
|
|
43
|
+
'Provenance sets the bar. Lines this change adds or modifies are generated work, held to ' +
|
|
44
|
+
'Expect; unchanged context is taken-on work, held to Accept. A Reject signal is ' +
|
|
45
|
+
'[blocking] anywhere, and so is an Expect gap in added or modified lines; an ' +
|
|
46
|
+
'Accept-but-not-Expect gap in untouched context is a [smell] recommendation. ' +
|
|
47
|
+
'An untouched line counts as part of this change only where the change makes it untrue; ' +
|
|
48
|
+
'name the changed line that does so, otherwise judge it as untouched context. ' +
|
|
49
|
+
TIER_ANCHOR +
|
|
50
|
+
' Name the practice each finding violates. ' +
|
|
51
|
+
'Before your verdict line, add one line `REASON: <one sentence on what you checked and ' +
|
|
52
|
+
'why you approve or reject>`.');
|
|
53
|
+
}
|
|
54
|
+
const SIGNALS_HEADING = '## Signals: reject / accept / expect';
|
|
55
|
+
const PROVENANCE_MARKER = '**The two halves are keyed to different tiers.**';
|
|
56
|
+
/**
|
|
57
|
+
* The grading rules from the corpus README (practices/README.md): the whole
|
|
58
|
+
* "## Signals: reject / accept / expect" section (to the next `## ` heading),
|
|
59
|
+
* plus the paragraph opening "**The two halves are keyed to different tiers.**"
|
|
60
|
+
* (which lives under "## Guide and sensor"; only that paragraph, to the next
|
|
61
|
+
* blank line). Either missing is a failure with a reason, never a fallback.
|
|
62
|
+
*/
|
|
63
|
+
export function extractBar(readme) {
|
|
64
|
+
const lines = readme.split(/\r?\n/);
|
|
65
|
+
const start = lines.findIndex((l) => l.trim() === SIGNALS_HEADING);
|
|
66
|
+
if (start === -1) {
|
|
67
|
+
return { ok: false, reason: `the README has no "${SIGNALS_HEADING}" section` };
|
|
68
|
+
}
|
|
69
|
+
let end = lines.length;
|
|
70
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
71
|
+
if ((lines[i] ?? '').startsWith('## ')) {
|
|
72
|
+
end = i;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const signals = lines.slice(start, end).join('\n').trim();
|
|
77
|
+
if (signals === SIGNALS_HEADING) {
|
|
78
|
+
return { ok: false, reason: `the README's "${SIGNALS_HEADING}" section is empty` };
|
|
79
|
+
}
|
|
80
|
+
if (signals.includes(PROVENANCE_MARKER))
|
|
81
|
+
return { ok: true, text: signals };
|
|
82
|
+
const at = readme.indexOf(PROVENANCE_MARKER);
|
|
83
|
+
if (at === -1) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `the README has no paragraph beginning "${PROVENANCE_MARKER}"`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const rest = readme.slice(at);
|
|
90
|
+
const blank = rest.search(/\r?\n[ \t]*\r?\n/);
|
|
91
|
+
const paragraph = (blank === -1 ? rest : rest.slice(0, blank)).trim();
|
|
92
|
+
return { ok: true, text: `${signals}\n\n${paragraph}` };
|
|
93
|
+
}
|
|
94
|
+
const H1_RE = /^# (.+?)[ \t]*$/m;
|
|
95
|
+
/** The first `# Title` line after any front-matter block, or undefined. */
|
|
96
|
+
export function parseTitle(markdown) {
|
|
97
|
+
const body = markdown.replace(FRONTMATTER_RE, '');
|
|
98
|
+
const match = H1_RE.exec(body);
|
|
99
|
+
const title = match?.[1]?.trim();
|
|
100
|
+
return title === undefined || title === '' ? undefined : title;
|
|
101
|
+
}
|
|
102
|
+
export function parseLensFrontmatter(markdown) {
|
|
103
|
+
const match = FRONTMATTER_RE.exec(markdown);
|
|
104
|
+
if (!match)
|
|
105
|
+
return undefined;
|
|
106
|
+
const lensMatch = LENS_LINE_RE.exec(match[1] ?? '');
|
|
107
|
+
if (!lensMatch)
|
|
108
|
+
return undefined;
|
|
109
|
+
const raw = (lensMatch[1] ?? '').trim();
|
|
110
|
+
const unquoted = raw.length >= 2 &&
|
|
111
|
+
((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'")))
|
|
112
|
+
? raw.slice(1, -1)
|
|
113
|
+
: raw;
|
|
114
|
+
return unquoted.trim() === '' ? undefined : unquoted.trim();
|
|
115
|
+
}
|
|
116
|
+
/** From `**Sensor**` (else `**Look for**`) to the next `**Guide**` or end of file. */
|
|
117
|
+
export function extractSensorHalf(markdown) {
|
|
118
|
+
let heading = SENSOR_HEADING;
|
|
119
|
+
let start = markdown.indexOf(SENSOR_HEADING);
|
|
120
|
+
if (start === -1) {
|
|
121
|
+
heading = LOOK_FOR_HEADING;
|
|
122
|
+
start = markdown.indexOf(LOOK_FOR_HEADING);
|
|
123
|
+
}
|
|
124
|
+
if (start === -1)
|
|
125
|
+
return undefined;
|
|
126
|
+
const rest = markdown.slice(start);
|
|
127
|
+
const nextGuide = rest.indexOf(GUIDE_HEADING, heading.length);
|
|
128
|
+
const slice = nextGuide === -1 ? rest : rest.slice(0, nextGuide);
|
|
129
|
+
if (slice.slice(heading.length).trim() === '')
|
|
130
|
+
return undefined;
|
|
131
|
+
return slice.trim();
|
|
132
|
+
}
|
|
133
|
+
const SIGNAL_LINE_RE = /^\s*(?:[-*+]\s+)?[_*]{1,2}(?:Reject|Expect):[_*]{1,2}\s*(.*)$/i;
|
|
134
|
+
const BLOCK_START_RE = /^\s*(?:[-*+]\s|\d+\.\s|#|\*\*)/;
|
|
135
|
+
/**
|
|
136
|
+
* The text of each `_Reject:_` / `_Expect:_` line in a sensor half, label stripped. Indented
|
|
137
|
+
* continuation lines (not a new bullet, heading or bold label) are joined onto their line.
|
|
138
|
+
*/
|
|
139
|
+
export function extractSignalLines(sensor) {
|
|
140
|
+
const out = [];
|
|
141
|
+
const lines = sensor.split(/\r?\n/);
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
const match = SIGNAL_LINE_RE.exec(lines[i] ?? '');
|
|
144
|
+
if (match === null)
|
|
145
|
+
continue;
|
|
146
|
+
let text = (match[1] ?? '').trim();
|
|
147
|
+
while (i + 1 < lines.length) {
|
|
148
|
+
const next = lines[i + 1] ?? '';
|
|
149
|
+
if (next.trim() === '' || !/^\s+\S/.test(next) || BLOCK_START_RE.test(next))
|
|
150
|
+
break;
|
|
151
|
+
text = `${text} ${next.trim()}`;
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
if (text !== '')
|
|
155
|
+
out.push(text);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Group practice files by their `lens:` value and build one lens per group.
|
|
161
|
+
* Files are taken in name order, a top-level README.md and non-.md files are
|
|
162
|
+
* skipped (as v2 does), and lenses are ordered by id, so the result is
|
|
163
|
+
* deterministic.
|
|
164
|
+
*/
|
|
165
|
+
export function deriveLensSet(files) {
|
|
166
|
+
const eligible = files
|
|
167
|
+
.filter((f) => f.name.endsWith('.md') && f.name.toLowerCase() !== 'readme.md')
|
|
168
|
+
.slice()
|
|
169
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
170
|
+
const byLens = new Map();
|
|
171
|
+
const signals = {};
|
|
172
|
+
const practiceNames = {};
|
|
173
|
+
const untagged = [];
|
|
174
|
+
const missingSensor = [];
|
|
175
|
+
for (const file of eligible) {
|
|
176
|
+
const id = file.name.slice(0, -'.md'.length);
|
|
177
|
+
const lensId = parseLensFrontmatter(file.text);
|
|
178
|
+
if (lensId === undefined) {
|
|
179
|
+
untagged.push(id);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const sensor = extractSensorHalf(file.text);
|
|
183
|
+
if (sensor === undefined)
|
|
184
|
+
missingSensor.push(id);
|
|
185
|
+
const group = byLens.get(lensId) ?? [];
|
|
186
|
+
const title = parseTitle(file.text);
|
|
187
|
+
group.push({ id, title, sensor });
|
|
188
|
+
byLens.set(lensId, group);
|
|
189
|
+
(signals[lensId] ??= []).push(...(sensor === undefined ? [] : extractSignalLines(sensor)));
|
|
190
|
+
(practiceNames[lensId] ??= []).push({ id, title: title ?? '' });
|
|
191
|
+
}
|
|
192
|
+
const lensIds = [...byLens.keys()].sort();
|
|
193
|
+
const lenses = [];
|
|
194
|
+
const groups = [];
|
|
195
|
+
for (const lensId of lensIds) {
|
|
196
|
+
const members = byLens.get(lensId) ?? [];
|
|
197
|
+
const body = members
|
|
198
|
+
.map((m) => {
|
|
199
|
+
const heading = `### ${m.id}${m.title === undefined ? '' : `: ${m.title}`}`;
|
|
200
|
+
return `${heading}\n\n${m.sensor ?? `(no sensor text available for ${m.id})`}`;
|
|
201
|
+
})
|
|
202
|
+
.join('\n\n');
|
|
203
|
+
lenses.push({
|
|
204
|
+
id: lensId,
|
|
205
|
+
title: lensId,
|
|
206
|
+
focus: `${framingFor(lensId)}\n\n## Practices this lens checks\n\n${body}`,
|
|
207
|
+
});
|
|
208
|
+
groups.push({ lensId, practiceIds: members.map((m) => m.id) });
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
lensSet: { version: 1, lenses },
|
|
212
|
+
groups,
|
|
213
|
+
untagged,
|
|
214
|
+
missingSensor,
|
|
215
|
+
signals,
|
|
216
|
+
practiceNames,
|
|
217
|
+
};
|
|
218
|
+
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -29,6 +29,23 @@ export interface Subject {
|
|
|
29
29
|
* change-context assembler is PR 2.
|
|
30
30
|
*/
|
|
31
31
|
readonly citedContext?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Whole-file context for the files the diff touches (or a one-line note
|
|
34
|
+
* saying there is none), already rendered. Rendered once, in `shared`,
|
|
35
|
+
* right after the diff under `FILE_CONTEXT_HEADING`. Absent or blank omits
|
|
36
|
+
* the section entirely, so every caller before this is byte-identical.
|
|
37
|
+
*/
|
|
38
|
+
readonly fileContext?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Check results (CI) for this change, rendered verbatim once, in `shared`, under
|
|
41
|
+
* `CHECKS_HEADING` between "## The change" and "## The diff". Absent or blank omits it.
|
|
42
|
+
*/
|
|
43
|
+
readonly checks?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Resolved references found in the diff's added lines, rendered once, in `shared`,
|
|
46
|
+
* under `REFERENCES_HEADING` after the changed files. Absent or blank omits it.
|
|
47
|
+
*/
|
|
48
|
+
readonly references?: string;
|
|
32
49
|
/**
|
|
33
50
|
* BL-146: the originating item's own acceptance text, verbatim. This is
|
|
34
51
|
* the one input to a review that the run under review did not write —
|
|
@@ -96,6 +113,12 @@ export declare const NO_PRACTICES_NOTICE = "No practices were supplied for this
|
|
|
96
113
|
* disclosed, not silently unnoticed.
|
|
97
114
|
*/
|
|
98
115
|
export declare const NO_REQUIREMENT_NOTICE: string;
|
|
116
|
+
/** Heading of the shared check-results section (`Subject.checks`). */
|
|
117
|
+
export declare const CHECKS_HEADING = "## Checks on this change";
|
|
118
|
+
/** Heading of the shared resolved-references section (`Subject.references`). */
|
|
119
|
+
export declare const REFERENCES_HEADING = "## References in added lines";
|
|
120
|
+
/** Heading of the shared whole-file context section (`Subject.fileContext`). */
|
|
121
|
+
export declare const FILE_CONTEXT_HEADING = "## Changed files in full";
|
|
99
122
|
/** Heading for the round-aware section appended to `perLens` when
|
|
100
123
|
* `Subject.priorFindings` is non-empty. Exported so a test can assert on
|
|
101
124
|
* it directly rather than re-typing the literal string. */
|
package/dist/prompt.js
CHANGED
|
@@ -63,6 +63,12 @@ export const NO_REQUIREMENT_NOTICE = 'No requirement text was supplied for this
|
|
|
63
63
|
"context -- is this run's own account of its own work; there is nothing " +
|
|
64
64
|
'from outside the run to check it against. Say so plainly in your ' +
|
|
65
65
|
'reasoning, and judge only whether the change is coherent on its own terms.';
|
|
66
|
+
/** Heading of the shared check-results section (`Subject.checks`). */
|
|
67
|
+
export const CHECKS_HEADING = '## Checks on this change';
|
|
68
|
+
/** Heading of the shared resolved-references section (`Subject.references`). */
|
|
69
|
+
export const REFERENCES_HEADING = '## References in added lines';
|
|
70
|
+
/** Heading of the shared whole-file context section (`Subject.fileContext`). */
|
|
71
|
+
export const FILE_CONTEXT_HEADING = '## Changed files in full';
|
|
66
72
|
/** Heading for the round-aware section appended to `perLens` when
|
|
67
73
|
* `Subject.priorFindings` is non-empty. Exported so a test can assert on
|
|
68
74
|
* it directly rather than re-typing the literal string. */
|
|
@@ -98,6 +104,15 @@ export function buildPrompt(lens, subject, practices, placement = 'shared') {
|
|
|
98
104
|
const citedSection = subject.citedContext !== undefined && subject.citedContext.trim() !== ''
|
|
99
105
|
? ['', '## Cited code', '', subject.citedContext]
|
|
100
106
|
: [];
|
|
107
|
+
const fileContextSection = subject.fileContext !== undefined && subject.fileContext.trim() !== ''
|
|
108
|
+
? ['', FILE_CONTEXT_HEADING, '', subject.fileContext]
|
|
109
|
+
: [];
|
|
110
|
+
const checksSection = subject.checks !== undefined && subject.checks.trim() !== ''
|
|
111
|
+
? ['', CHECKS_HEADING, '', subject.checks]
|
|
112
|
+
: [];
|
|
113
|
+
const referencesSection = subject.references !== undefined && subject.references.trim() !== ''
|
|
114
|
+
? ['', REFERENCES_HEADING, '', subject.references]
|
|
115
|
+
: [];
|
|
101
116
|
// BL-146: NEVER omitted, unlike citedSection above -- a requirement's
|
|
102
117
|
// absence is disclosed, not silently skipped. `requirementText` is
|
|
103
118
|
// either the item's own acceptance text, verbatim (never combined with
|
|
@@ -171,12 +186,15 @@ export function buildPrompt(lens, subject, practices, placement = 'shared') {
|
|
|
171
186
|
`Title: ${subject.title}`,
|
|
172
187
|
'',
|
|
173
188
|
subject.description,
|
|
189
|
+
...checksSection,
|
|
174
190
|
'',
|
|
175
191
|
'## The diff',
|
|
176
192
|
'',
|
|
177
193
|
'```diff',
|
|
178
194
|
subject.diff,
|
|
179
195
|
'```',
|
|
196
|
+
...fileContextSection,
|
|
197
|
+
...referencesSection,
|
|
180
198
|
...citedSection,
|
|
181
199
|
'',
|
|
182
200
|
'## How to answer',
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ContextFs } from './context-files.js';
|
|
2
|
+
/** At most this many distinct references are resolved; the rest are counted, not resolved. */
|
|
3
|
+
export declare const MAX_REFERENCES = 40;
|
|
4
|
+
/** Directory levels below the root that the ADR search lists. */
|
|
5
|
+
export declare const ADR_SEARCH_DEPTH = 4;
|
|
6
|
+
/** The basename search visits at most this many entries before it stops and says so. */
|
|
7
|
+
export declare const MAX_SEARCH_FILES = 5000;
|
|
8
|
+
/** Directory levels below the root that the basename search descends. */
|
|
9
|
+
export declare const BASENAME_SEARCH_DEPTH = 8;
|
|
10
|
+
export interface ReferenceOptions {
|
|
11
|
+
/** Overrides MAX_SEARCH_FILES (tests). */
|
|
12
|
+
readonly maxSearchFiles?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DirEntry {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly isDir: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ReferenceFs extends ContextFs {
|
|
19
|
+
/** List a directory's entries. Absent where the host cannot list directories. */
|
|
20
|
+
readonly listDir?: (path: string) => Promise<DirEntry[]>;
|
|
21
|
+
}
|
|
22
|
+
export interface AddedLine {
|
|
23
|
+
readonly file: string;
|
|
24
|
+
readonly line: number;
|
|
25
|
+
readonly text: string;
|
|
26
|
+
}
|
|
27
|
+
/** Lines the diff adds, with the file and new-side line number of each. */
|
|
28
|
+
export declare function addedLines(diff: string): AddedLine[];
|
|
29
|
+
export interface Reference {
|
|
30
|
+
readonly kind: 'adr' | 'path';
|
|
31
|
+
/** ADR number (adr) or the cited path (path). */
|
|
32
|
+
readonly target: string;
|
|
33
|
+
/** The text as written, e.g. `ADR 9`. */
|
|
34
|
+
readonly display: string;
|
|
35
|
+
/** Where it was cited, `file:line`. */
|
|
36
|
+
readonly where: string;
|
|
37
|
+
}
|
|
38
|
+
/** Distinct references in the added lines, in order of first appearance. */
|
|
39
|
+
export declare function findReferences(diff: string): Reference[];
|
|
40
|
+
/**
|
|
41
|
+
* The body of the "References in added lines" section for `diff`, resolved against `rootPath`.
|
|
42
|
+
* Throws only when the root itself cannot be resolved.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildReferences(diff: string, rootPath: string, fs: ReferenceFs, options?: ReferenceOptions): Promise<string>;
|