@webpieces/pr-gate 0.4.538 → 0.4.540

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.538",
3
+ "version": "0.4.540",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -26,7 +26,7 @@
26
26
  "directory": "packages/tooling/pr-gate"
27
27
  },
28
28
  "dependencies": {
29
- "@webpieces/rules-config": "0.4.538",
29
+ "@webpieces/rules-config": "0.4.540",
30
30
  "@inversifyjs/binding-decorators": "1.1.5",
31
31
  "inversify": "7.10.4",
32
32
  "reflect-metadata": "0.2.2"
@@ -2,14 +2,23 @@ import { ReviewJsonService } from '@webpieces/rules-config';
2
2
  import { DiffBasis } from './diff-basis';
3
3
  export declare const FILE_DIFF_MAX_BYTES: number;
4
4
  export declare const ALL_DIFF_MAX_BYTES: number;
5
- /** One materialized file's diff. Data-only (per CLAUDE.md). */
5
+ /**
6
+ * One materialized file's diff. Data-only (per CLAUDE.md).
7
+ *
8
+ * Both paths are recorded TWICE, relative and absolute, deliberately. Relative is the stable identity that
9
+ * survives the repo being cloned somewhere else; absolute is the only form a reviewer subagent can actually
10
+ * open, because its working directory is not guaranteed to be the repo root. Recording only the relative
11
+ * form is what turned "read this file" back into a search — the exact cost this whole feature removes.
12
+ */
6
13
  export declare class DiffManifestEntry {
7
14
  file: string;
15
+ fileAbs: string;
8
16
  diffFile: string;
17
+ diffAbs: string;
9
18
  status: string;
10
19
  bytes: number;
11
20
  truncated: boolean;
12
- constructor(file: string, diffFile: string, status: string, bytes: number, truncated?: boolean);
21
+ constructor(repoRoot: string, file: string, diffFile: string, status: string, bytes: number, truncated?: boolean);
13
22
  }
14
23
  /** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */
15
24
  export declare class DiffManifest {
@@ -23,6 +32,8 @@ export declare class DiffManifest {
23
32
  entries: DiffManifestEntry[];
24
33
  excluded: string[];
25
34
  omittedFromAllDiff: string[];
35
+ allDiffLines: number;
36
+ allDiffBytes: number;
26
37
  constructor(base: string, head: string, dirty: boolean, diffCommand: string, entries?: DiffManifestEntry[], excluded?: string[], omittedFromAllDiff?: string[], hashMainHead?: string);
27
38
  }
28
39
  /**
@@ -74,9 +85,21 @@ export declare class DiffMaterializer {
74
85
  private capped;
75
86
  /**
76
87
  * `ALL.diff` — the single Read that answers "what changed on this branch?". Files are appended smallest
77
- * first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.
88
+ * first so a cap drops the fewest of them, and anything dropped is named in a HEADER as well as in the
89
+ * manifest.
90
+ *
91
+ * The header used to be a footer, and that was a live correctness bug. `ALL_DIFF_MAX_BYTES` is 2 MB —
92
+ * roughly 30–50k lines — while the Read tool truncates at ~2000. So on the exact PR where files were
93
+ * dropped, a reviewer Reads the first ~5% and the notice telling it files are missing sits in the 95% it
94
+ * never reaches: the in-band signal went invisible precisely when it mattered. A reviewer reads a diff
95
+ * from the start, so the top is the only position that survives a truncated read.
78
96
  */
79
97
  private writeAllDiff;
98
+ /**
99
+ * The "this combined view is NOT the whole change" notice, at the TOP (see {@link writeAllDiff}). Emitted
100
+ * only when something really is missing — a header on every diff trains a reader to skip the header.
101
+ */
102
+ private allDiffHeader;
80
103
  /**
81
104
  * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir
82
105
  * already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a
@@ -11,17 +11,30 @@ const inversify_1 = require("inversify");
11
11
  exports.FILE_DIFF_MAX_BYTES = 256 * 1024;
12
12
  // Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.
13
13
  exports.ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;
14
- /** One materialized file's diff. Data-only (per CLAUDE.md). */
14
+ /**
15
+ * One materialized file's diff. Data-only (per CLAUDE.md).
16
+ *
17
+ * Both paths are recorded TWICE, relative and absolute, deliberately. Relative is the stable identity that
18
+ * survives the repo being cloned somewhere else; absolute is the only form a reviewer subagent can actually
19
+ * open, because its working directory is not guaranteed to be the repo root. Recording only the relative
20
+ * form is what turned "read this file" back into a search — the exact cost this whole feature removes.
21
+ */
15
22
  class DiffManifestEntry {
16
23
  file; // repo-relative source path
24
+ fileAbs; // ABSOLUTE working-tree path; '' for a deleted file, which has no after-state
17
25
  diffFile; // repo-relative path of the .diff under diff/files/
26
+ diffAbs; // ABSOLUTE path of the same .diff
18
27
  status; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)
19
28
  bytes;
20
29
  truncated;
21
30
  // eslint-disable-next-line @typescript-eslint/max-params
22
- constructor(file, diffFile, status, bytes, truncated = false) {
31
+ constructor(repoRoot, file, diffFile, status, bytes, truncated = false) {
23
32
  this.file = file;
33
+ // Not `gitTrackedSourceFileAfterChanges`: `status` already says what happened, and a compound name
34
+ // would lie for 'U' (untracked) and for 'D' (no after-state). The path field answers only WHERE.
35
+ this.fileAbs = status === 'D' ? '' : path.join(repoRoot, file);
24
36
  this.diffFile = diffFile;
37
+ this.diffAbs = path.resolve(repoRoot, diffFile);
25
38
  this.status = status;
26
39
  this.bytes = bytes;
27
40
  this.truncated = truncated;
@@ -42,6 +55,11 @@ class DiffManifest {
42
55
  entries;
43
56
  excluded; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized
44
57
  omittedFromAllDiff; // too big for ALL.diff; their per-file .diff still exists
58
+ // Size of the combined view, measured once HERE so the instructions renderer and any future consumer
59
+ // read one number instead of each running its own `wc -l`. A reviewer needs the LINE count specifically:
60
+ // the Read tool truncates at ~2000 lines, silently, which is how a fraction of a change gets reviewed.
61
+ allDiffLines;
62
+ allDiffBytes;
45
63
  // eslint-disable-next-line @typescript-eslint/max-params
46
64
  constructor(base, head, dirty, diffCommand, entries = [], excluded = [], omittedFromAllDiff = [], hashMainHead = '') {
47
65
  this.base = base;
@@ -55,6 +73,8 @@ class DiffManifest {
55
73
  this.entries = entries;
56
74
  this.excluded = excluded;
57
75
  this.omittedFromAllDiff = omittedFromAllDiff;
76
+ this.allDiffLines = 0;
77
+ this.allDiffBytes = 0;
58
78
  }
59
79
  }
60
80
  exports.DiffManifest = DiffManifest;
@@ -169,14 +189,14 @@ let DiffMaterializer = class DiffMaterializer {
169
189
  const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\nIf you need it: git diff ${manifest.base} -- ${file}\n`;
170
190
  fs.writeFileSync(abs, stub);
171
191
  manifest.excluded.push(file);
172
- manifest.entries.push(new DiffManifestEntry(file, rel, 'X', stub.length));
192
+ manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, 'X', stub.length));
173
193
  return;
174
194
  }
175
195
  const raw = byFile.get(file) ?? '';
176
196
  const status = this.statusOf(file, raw, repoRoot);
177
197
  const body = this.capped(raw, file, manifest.base);
178
198
  fs.writeFileSync(abs, body);
179
- manifest.entries.push(new DiffManifestEntry(file, rel, status, body.length, body !== raw));
199
+ manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, status, body.length, body !== raw));
180
200
  }
181
201
  statusOf(file, raw, repoRoot) {
182
202
  if (raw.includes('\nnew file (untracked)'))
@@ -200,7 +220,14 @@ let DiffMaterializer = class DiffMaterializer {
200
220
  }
201
221
  /**
202
222
  * `ALL.diff` — the single Read that answers "what changed on this branch?". Files are appended smallest
203
- * first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.
223
+ * first so a cap drops the fewest of them, and anything dropped is named in a HEADER as well as in the
224
+ * manifest.
225
+ *
226
+ * The header used to be a footer, and that was a live correctness bug. `ALL_DIFF_MAX_BYTES` is 2 MB —
227
+ * roughly 30–50k lines — while the Read tool truncates at ~2000. So on the exact PR where files were
228
+ * dropped, a reviewer Reads the first ~5% and the notice telling it files are missing sits in the 95% it
229
+ * never reaches: the in-band signal went invisible precisely when it mattered. A reviewer reads a diff
230
+ * from the start, so the top is the only position that survives a truncated read.
204
231
  */
205
232
  writeAllDiff(dir, filesDir, manifest) {
206
233
  const ordered = [...manifest.entries].sort((a, b) => a.bytes - b.bytes);
@@ -215,13 +242,33 @@ let DiffMaterializer = class DiffMaterializer {
215
242
  parts.push(body);
216
243
  total += body.length;
217
244
  }
218
- let out = parts.join('\n');
245
+ const out = this.allDiffHeader(manifest) + parts.join('\n');
246
+ manifest.allDiffBytes = out.length;
247
+ manifest.allDiffLines = out === '' ? 0 : out.split('\n').length;
248
+ fs.writeFileSync(path.join(dir, 'ALL.diff'), out);
249
+ }
250
+ /**
251
+ * The "this combined view is NOT the whole change" notice, at the TOP (see {@link writeAllDiff}). Emitted
252
+ * only when something really is missing — a header on every diff trains a reader to skip the header.
253
+ */
254
+ allDiffHeader(manifest) {
255
+ const truncated = manifest.entries.filter((e) => e.truncated);
256
+ if (manifest.omittedFromAllDiff.length === 0 && truncated.length === 0)
257
+ return '';
258
+ const lines = ['=== ⚠️ THIS COMBINED VIEW IS INCOMPLETE — read this notice before the diff below ==='];
219
259
  if (manifest.omittedFromAllDiff.length > 0) {
220
- out += `\n\n… ${manifest.omittedFromAllDiff.length} file(s) OMITTED from this combined view (it hit the ` +
221
- `${exports.ALL_DIFF_MAX_BYTES}-byte cap). Each still has its own diff under files/ — see manifest.json:\n` +
222
- manifest.omittedFromAllDiff.map((f) => ` ${f}`).join('\n') + '\n';
260
+ lines.push(`${manifest.omittedFromAllDiff.length} file(s) are OMITTED here (this view hit the ` +
261
+ `${exports.ALL_DIFF_MAX_BYTES}-byte cap). Each still has its OWN complete diff`, 'under files/ — open those, and see manifest.json:');
262
+ for (const f of manifest.omittedFromAllDiff)
263
+ lines.push(` ${f}`);
223
264
  }
224
- fs.writeFileSync(path.join(dir, 'ALL.diff'), out);
265
+ if (truncated.length > 0) {
266
+ lines.push(`${truncated.length} file(s) below are TRUNCATED at ${exports.FILE_DIFF_MAX_BYTES} bytes; each says so inline where it cuts off:`);
267
+ for (const e of truncated)
268
+ lines.push(` ${e.file}`);
269
+ }
270
+ lines.push('=== end of notice ===', '');
271
+ return lines.join('\n') + '\n';
225
272
  }
226
273
  /**
227
274
  * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir
@@ -1 +1 @@
1
- {"version":3,"file":"diff-materializer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/diff-materializer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { isPathExcluded, ReviewJsonService, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffBasis } from './diff-basis';\n\n// Per-file cap. Over it, the file's diff is truncated WITH a footer naming the command that gets the rest.\nexport const FILE_DIFF_MAX_BYTES = 256 * 1024;\n// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.\nexport const ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;\n\n/** One materialized file's diff. Data-only (per CLAUDE.md). */\nexport class DiffManifestEntry {\n file: string; // repo-relative source path\n diffFile: string; // repo-relative path of the .diff under diff/files/\n status: string; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)\n bytes: number;\n truncated: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(file: string, diffFile: string, status: string, bytes: number, truncated = false) {\n this.file = file;\n this.diffFile = diffFile;\n this.status = status;\n this.bytes = bytes;\n this.truncated = truncated;\n }\n}\n\n/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */\nexport class DiffManifest {\n // Points A and B. Also exposed as hashForkPoint/hashFeatureHead below, which is the vocabulary the\n // 3-point merge already uses — one grep should find the same sha on both halves of the system.\n base: string;\n head: string;\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string; // point C: main as this clone last saw it. '' when unresolvable.\n dirty: boolean;\n diffCommand: string;\n entries: DiffManifestEntry[];\n excluded: string[]; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized\n omittedFromAllDiff: string[]; // too big for ALL.diff; their per-file .diff still exists\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, dirty: boolean, diffCommand: string,\n entries: DiffManifestEntry[] = [], excluded: string[] = [], omittedFromAllDiff: string[] = [],\n hashMainHead = '',\n ) {\n this.base = base;\n this.head = head;\n // Derived, never passed separately, so the two names for one sha cannot drift into disagreement.\n this.hashForkPoint = base;\n this.hashFeatureHead = head;\n this.hashMainHead = hashMainHead;\n this.dirty = dirty;\n this.diffCommand = diffCommand;\n this.entries = entries;\n this.excluded = excluded;\n this.omittedFromAllDiff = omittedFromAllDiff;\n }\n}\n\n/**\n * Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.\n *\n * The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context\n * archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).\n * Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.\n *\n * Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,\n * split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`\n * loop would be N spawns for the same bytes.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DiffMaterializer {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */\n diffDirFor(repoRoot: string, featureName: string): string {\n return path.join(this.reviewJsonService.prDirFor(repoRoot, featureName), 'diff');\n }\n\n /**\n * Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.\n *\n * `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff\n * a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two\n * would drift apart again.\n */\n materialize(repoRoot: string, featureName: string, basis: DiffBasis, changedFiles: readonly string[], excludeGlobs: readonly string[]): DiffManifest {\n const dir = this.diffDirFor(repoRoot, featureName);\n const filesDir = path.join(dir, 'files');\n fs.rmSync(dir, { recursive: true, force: true }); // a stale diff read as current is worse than none\n fs.mkdirSync(filesDir, { recursive: true });\n const manifest = new DiffManifest(\n basis.base, basis.headSha, basis.dirty, basis.diffCommand, [], [], [], basis.hashMainHead);\n if (basis.unresolved) return this.writeManifest(dir, manifest);\n const byFile = this.captureByFile(repoRoot, basis);\n const used = new Set<string>();\n for (const file of [...changedFiles].sort()) {\n this.writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest);\n }\n this.writeAllDiff(dir, filesDir, manifest);\n return this.writeManifest(dir, manifest);\n }\n\n /**\n * ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.\n * Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same\n * treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.\n */\n private captureByFile(repoRoot: string, basis: DiffBasis): Map<string, string> {\n const args = basis.dirty ? ['diff', basis.base] : ['diff', basis.base, basis.headSha];\n const out = this.gitOut(repoRoot, args);\n const byFile = new Map<string, string>();\n // Split BEFORE each `diff --git` header while keeping the header on its chunk.\n for (const chunk of out.split(/\\n(?=diff --git )/)) {\n if (!chunk.startsWith('diff --git ')) continue;\n const file = this.pathFromHeader(chunk);\n if (file !== '') byFile.set(file, chunk.endsWith('\\n') ? chunk : `${chunk}\\n`);\n }\n for (const file of this.untrackedFiles(repoRoot, basis)) {\n if (!byFile.has(file)) byFile.set(file, this.syntheticAddedDiff(repoRoot, file));\n }\n return byFile;\n }\n\n /**\n * The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is\n * the name that exists on disk, which is the file a reviewer can actually open.\n */\n private pathFromHeader(chunk: string): string {\n const header = chunk.slice(0, chunk.indexOf('\\n') === -1 ? chunk.length : chunk.indexOf('\\n'));\n const match = header.match(/^diff --git a\\/(.+) b\\/(.+)$/);\n return match ? match[2] : '';\n }\n\n private untrackedFiles(repoRoot: string, basis: DiffBasis): string[] {\n if (!basis.dirty) return [];\n const out = this.gitOut(repoRoot, ['ls-files', '--others', '--exclude-standard']);\n return out === '' ? [] : out.split('\\n').filter((f: string): boolean => f.trim() !== '');\n }\n\n private syntheticAddedDiff(repoRoot: string, file: string): string {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) return '';\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/binary untracked file yields a note, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const body = fs.readFileSync(abs, 'utf8').split('\\n').map((l: string): string => `+${l}`).join('\\n');\n return `diff --git a/${file} b/${file}\\nnew file (untracked)\\n--- /dev/null\\n+++ b/${file}\\n${body}\\n`;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // a binary/unreadable untracked file gets a NOTE, never a crash\n return `diff --git a/${file} b/${file}\\nnew file (untracked, unreadable as text)\\n`;\n }\n }\n\n /** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeOne(\n repoRoot: string, filesDir: string, file: string, byFile: Map<string, string>,\n used: Set<string>, excludeGlobs: readonly string[], manifest: DiffManifest,\n ): void {\n const name = this.safeName(file, used);\n const abs = path.join(filesDir, name);\n const rel = path.relative(repoRoot, abs);\n // Excluded files stay in the manifest and keep a stub. Dropping them entirely would read as \"this\n // file did not change\"; they are noise to READ, not facts to hide — and they still match checklists.\n if (excludeGlobs.length > 0 && isPathExcluded(file, [...excludeGlobs])) {\n const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\\nIf you need it: git diff ${manifest.base} -- ${file}\\n`;\n fs.writeFileSync(abs, stub);\n manifest.excluded.push(file);\n manifest.entries.push(new DiffManifestEntry(file, rel, 'X', stub.length));\n return;\n }\n const raw = byFile.get(file) ?? '';\n const status = this.statusOf(file, raw, repoRoot);\n const body = this.capped(raw, file, manifest.base);\n fs.writeFileSync(abs, body);\n manifest.entries.push(new DiffManifestEntry(file, rel, status, body.length, body !== raw));\n }\n\n private statusOf(file: string, raw: string, repoRoot: string): string {\n if (raw.includes('\\nnew file (untracked)')) return 'U';\n if (raw.includes('\\nnew file mode ')) return 'A';\n if (raw.includes('\\ndeleted file mode ')) return 'D';\n return fs.existsSync(path.join(repoRoot, file)) ? 'M' : 'D';\n }\n\n /**\n * Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer\n * reports \"reviewed\" on a tenth of a change — the same trap `formatFileList` states its dropped count for.\n */\n private capped(raw: string, file: string, base: string): string {\n if (raw.length <= FILE_DIFF_MAX_BYTES) return raw;\n return `${raw.slice(0, FILE_DIFF_MAX_BYTES)}\\n` +\n `\\n… TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes (full diff is ${raw.length} bytes).\\n` +\n ` Read the rest with: git diff ${base} -- ${file}\\n`;\n }\n\n /**\n * `ALL.diff` — the single Read that answers \"what changed on this branch?\". Files are appended smallest\n * first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.\n */\n private writeAllDiff(dir: string, filesDir: string, manifest: DiffManifest): void {\n const ordered = [...manifest.entries].sort((a: DiffManifestEntry, b: DiffManifestEntry): number => a.bytes - b.bytes);\n const parts: string[] = [];\n let total = 0;\n for (const entry of ordered) {\n const body = fs.readFileSync(path.join(filesDir, path.basename(entry.diffFile)), 'utf8');\n if (total + body.length > ALL_DIFF_MAX_BYTES) {\n manifest.omittedFromAllDiff.push(entry.file);\n continue;\n }\n parts.push(body);\n total += body.length;\n }\n let out = parts.join('\\n');\n if (manifest.omittedFromAllDiff.length > 0) {\n out += `\\n\\n… ${manifest.omittedFromAllDiff.length} file(s) OMITTED from this combined view (it hit the ` +\n `${ALL_DIFF_MAX_BYTES}-byte cap). Each still has its own diff under files/ — see manifest.json:\\n` +\n manifest.omittedFromAllDiff.map((f: string): string => ` ${f}`).join('\\n') + '\\n';\n }\n fs.writeFileSync(path.join(dir, 'ALL.diff'), out);\n }\n\n /**\n * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir\n * already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a\n * path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.\n */\n private safeName(file: string, used: Set<string>): string {\n const flat = `${file.replace(/\\//g, '__')}.diff`;\n if (!used.has(flat)) {\n used.add(flat);\n return flat;\n }\n for (let n = 2; ; n++) {\n const candidate = `${file.replace(/\\//g, '__')}-${n}.diff`;\n if (!used.has(candidate)) {\n used.add(candidate);\n return candidate;\n }\n }\n }\n\n private writeManifest(dir: string, manifest: DiffManifest): DiffManifest {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\\n');\n return manifest;\n }\n\n private gitOut(repoRoot: string, args: string[]): string {\n // maxBuffer: a monorepo diff routinely exceeds node's 1MB default, and the failure mode is a\n // truncated capture that looks like a complete one.\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });\n return result.status === 0 ? (result.stdout ?? '') : '';\n }\n}\n"]}
1
+ {"version":3,"file":"diff-materializer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/diff-materializer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { isPathExcluded, ReviewJsonService, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffBasis } from './diff-basis';\n\n// Per-file cap. Over it, the file's diff is truncated WITH a footer naming the command that gets the rest.\nexport const FILE_DIFF_MAX_BYTES = 256 * 1024;\n// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.\nexport const ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;\n\n/**\n * One materialized file's diff. Data-only (per CLAUDE.md).\n *\n * Both paths are recorded TWICE, relative and absolute, deliberately. Relative is the stable identity that\n * survives the repo being cloned somewhere else; absolute is the only form a reviewer subagent can actually\n * open, because its working directory is not guaranteed to be the repo root. Recording only the relative\n * form is what turned \"read this file\" back into a search — the exact cost this whole feature removes.\n */\nexport class DiffManifestEntry {\n file: string; // repo-relative source path\n fileAbs: string; // ABSOLUTE working-tree path; '' for a deleted file, which has no after-state\n diffFile: string; // repo-relative path of the .diff under diff/files/\n diffAbs: string; // ABSOLUTE path of the same .diff\n status: string; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)\n bytes: number;\n truncated: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(repoRoot: string, file: string, diffFile: string, status: string, bytes: number, truncated = false) {\n this.file = file;\n // Not `gitTrackedSourceFileAfterChanges`: `status` already says what happened, and a compound name\n // would lie for 'U' (untracked) and for 'D' (no after-state). The path field answers only WHERE.\n this.fileAbs = status === 'D' ? '' : path.join(repoRoot, file);\n this.diffFile = diffFile;\n this.diffAbs = path.resolve(repoRoot, diffFile);\n this.status = status;\n this.bytes = bytes;\n this.truncated = truncated;\n }\n}\n\n/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */\nexport class DiffManifest {\n // Points A and B. Also exposed as hashForkPoint/hashFeatureHead below, which is the vocabulary the\n // 3-point merge already uses — one grep should find the same sha on both halves of the system.\n base: string;\n head: string;\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string; // point C: main as this clone last saw it. '' when unresolvable.\n dirty: boolean;\n diffCommand: string;\n entries: DiffManifestEntry[];\n excluded: string[]; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized\n omittedFromAllDiff: string[]; // too big for ALL.diff; their per-file .diff still exists\n // Size of the combined view, measured once HERE so the instructions renderer and any future consumer\n // read one number instead of each running its own `wc -l`. A reviewer needs the LINE count specifically:\n // the Read tool truncates at ~2000 lines, silently, which is how a fraction of a change gets reviewed.\n allDiffLines: number;\n allDiffBytes: number;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, dirty: boolean, diffCommand: string,\n entries: DiffManifestEntry[] = [], excluded: string[] = [], omittedFromAllDiff: string[] = [],\n hashMainHead = '',\n ) {\n this.base = base;\n this.head = head;\n // Derived, never passed separately, so the two names for one sha cannot drift into disagreement.\n this.hashForkPoint = base;\n this.hashFeatureHead = head;\n this.hashMainHead = hashMainHead;\n this.dirty = dirty;\n this.diffCommand = diffCommand;\n this.entries = entries;\n this.excluded = excluded;\n this.omittedFromAllDiff = omittedFromAllDiff;\n this.allDiffLines = 0;\n this.allDiffBytes = 0;\n }\n}\n\n/**\n * Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.\n *\n * The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context\n * archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).\n * Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.\n *\n * Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,\n * split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`\n * loop would be N spawns for the same bytes.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DiffMaterializer {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */\n diffDirFor(repoRoot: string, featureName: string): string {\n return path.join(this.reviewJsonService.prDirFor(repoRoot, featureName), 'diff');\n }\n\n /**\n * Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.\n *\n * `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff\n * a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two\n * would drift apart again.\n */\n materialize(repoRoot: string, featureName: string, basis: DiffBasis, changedFiles: readonly string[], excludeGlobs: readonly string[]): DiffManifest {\n const dir = this.diffDirFor(repoRoot, featureName);\n const filesDir = path.join(dir, 'files');\n fs.rmSync(dir, { recursive: true, force: true }); // a stale diff read as current is worse than none\n fs.mkdirSync(filesDir, { recursive: true });\n const manifest = new DiffManifest(\n basis.base, basis.headSha, basis.dirty, basis.diffCommand, [], [], [], basis.hashMainHead);\n if (basis.unresolved) return this.writeManifest(dir, manifest);\n const byFile = this.captureByFile(repoRoot, basis);\n const used = new Set<string>();\n for (const file of [...changedFiles].sort()) {\n this.writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest);\n }\n this.writeAllDiff(dir, filesDir, manifest);\n return this.writeManifest(dir, manifest);\n }\n\n /**\n * ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.\n * Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same\n * treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.\n */\n private captureByFile(repoRoot: string, basis: DiffBasis): Map<string, string> {\n const args = basis.dirty ? ['diff', basis.base] : ['diff', basis.base, basis.headSha];\n const out = this.gitOut(repoRoot, args);\n const byFile = new Map<string, string>();\n // Split BEFORE each `diff --git` header while keeping the header on its chunk.\n for (const chunk of out.split(/\\n(?=diff --git )/)) {\n if (!chunk.startsWith('diff --git ')) continue;\n const file = this.pathFromHeader(chunk);\n if (file !== '') byFile.set(file, chunk.endsWith('\\n') ? chunk : `${chunk}\\n`);\n }\n for (const file of this.untrackedFiles(repoRoot, basis)) {\n if (!byFile.has(file)) byFile.set(file, this.syntheticAddedDiff(repoRoot, file));\n }\n return byFile;\n }\n\n /**\n * The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is\n * the name that exists on disk, which is the file a reviewer can actually open.\n */\n private pathFromHeader(chunk: string): string {\n const header = chunk.slice(0, chunk.indexOf('\\n') === -1 ? chunk.length : chunk.indexOf('\\n'));\n const match = header.match(/^diff --git a\\/(.+) b\\/(.+)$/);\n return match ? match[2] : '';\n }\n\n private untrackedFiles(repoRoot: string, basis: DiffBasis): string[] {\n if (!basis.dirty) return [];\n const out = this.gitOut(repoRoot, ['ls-files', '--others', '--exclude-standard']);\n return out === '' ? [] : out.split('\\n').filter((f: string): boolean => f.trim() !== '');\n }\n\n private syntheticAddedDiff(repoRoot: string, file: string): string {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) return '';\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/binary untracked file yields a note, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const body = fs.readFileSync(abs, 'utf8').split('\\n').map((l: string): string => `+${l}`).join('\\n');\n return `diff --git a/${file} b/${file}\\nnew file (untracked)\\n--- /dev/null\\n+++ b/${file}\\n${body}\\n`;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // a binary/unreadable untracked file gets a NOTE, never a crash\n return `diff --git a/${file} b/${file}\\nnew file (untracked, unreadable as text)\\n`;\n }\n }\n\n /** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeOne(\n repoRoot: string, filesDir: string, file: string, byFile: Map<string, string>,\n used: Set<string>, excludeGlobs: readonly string[], manifest: DiffManifest,\n ): void {\n const name = this.safeName(file, used);\n const abs = path.join(filesDir, name);\n const rel = path.relative(repoRoot, abs);\n // Excluded files stay in the manifest and keep a stub. Dropping them entirely would read as \"this\n // file did not change\"; they are noise to READ, not facts to hide — and they still match checklists.\n if (excludeGlobs.length > 0 && isPathExcluded(file, [...excludeGlobs])) {\n const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\\nIf you need it: git diff ${manifest.base} -- ${file}\\n`;\n fs.writeFileSync(abs, stub);\n manifest.excluded.push(file);\n manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, 'X', stub.length));\n return;\n }\n const raw = byFile.get(file) ?? '';\n const status = this.statusOf(file, raw, repoRoot);\n const body = this.capped(raw, file, manifest.base);\n fs.writeFileSync(abs, body);\n manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, status, body.length, body !== raw));\n }\n\n private statusOf(file: string, raw: string, repoRoot: string): string {\n if (raw.includes('\\nnew file (untracked)')) return 'U';\n if (raw.includes('\\nnew file mode ')) return 'A';\n if (raw.includes('\\ndeleted file mode ')) return 'D';\n return fs.existsSync(path.join(repoRoot, file)) ? 'M' : 'D';\n }\n\n /**\n * Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer\n * reports \"reviewed\" on a tenth of a change — the same trap `formatFileList` states its dropped count for.\n */\n private capped(raw: string, file: string, base: string): string {\n if (raw.length <= FILE_DIFF_MAX_BYTES) return raw;\n return `${raw.slice(0, FILE_DIFF_MAX_BYTES)}\\n` +\n `\\n… TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes (full diff is ${raw.length} bytes).\\n` +\n ` Read the rest with: git diff ${base} -- ${file}\\n`;\n }\n\n /**\n * `ALL.diff` — the single Read that answers \"what changed on this branch?\". Files are appended smallest\n * first so a cap drops the fewest of them, and anything dropped is named in a HEADER as well as in the\n * manifest.\n *\n * The header used to be a footer, and that was a live correctness bug. `ALL_DIFF_MAX_BYTES` is 2 MB —\n * roughly 30–50k lines — while the Read tool truncates at ~2000. So on the exact PR where files were\n * dropped, a reviewer Reads the first ~5% and the notice telling it files are missing sits in the 95% it\n * never reaches: the in-band signal went invisible precisely when it mattered. A reviewer reads a diff\n * from the start, so the top is the only position that survives a truncated read.\n */\n private writeAllDiff(dir: string, filesDir: string, manifest: DiffManifest): void {\n const ordered = [...manifest.entries].sort((a: DiffManifestEntry, b: DiffManifestEntry): number => a.bytes - b.bytes);\n const parts: string[] = [];\n let total = 0;\n for (const entry of ordered) {\n const body = fs.readFileSync(path.join(filesDir, path.basename(entry.diffFile)), 'utf8');\n if (total + body.length > ALL_DIFF_MAX_BYTES) {\n manifest.omittedFromAllDiff.push(entry.file);\n continue;\n }\n parts.push(body);\n total += body.length;\n }\n const out = this.allDiffHeader(manifest) + parts.join('\\n');\n manifest.allDiffBytes = out.length;\n manifest.allDiffLines = out === '' ? 0 : out.split('\\n').length;\n fs.writeFileSync(path.join(dir, 'ALL.diff'), out);\n }\n\n /**\n * The \"this combined view is NOT the whole change\" notice, at the TOP (see {@link writeAllDiff}). Emitted\n * only when something really is missing — a header on every diff trains a reader to skip the header.\n */\n private allDiffHeader(manifest: DiffManifest): string {\n const truncated = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated);\n if (manifest.omittedFromAllDiff.length === 0 && truncated.length === 0) return '';\n const lines = ['=== ⚠️ THIS COMBINED VIEW IS INCOMPLETE — read this notice before the diff below ==='];\n if (manifest.omittedFromAllDiff.length > 0) {\n lines.push(`${manifest.omittedFromAllDiff.length} file(s) are OMITTED here (this view hit the ` +\n `${ALL_DIFF_MAX_BYTES}-byte cap). Each still has its OWN complete diff`, 'under files/ — open those, and see manifest.json:');\n for (const f of manifest.omittedFromAllDiff) lines.push(` ${f}`);\n }\n if (truncated.length > 0) {\n lines.push(`${truncated.length} file(s) below are TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes; each says so inline where it cuts off:`);\n for (const e of truncated) lines.push(` ${e.file}`);\n }\n lines.push('=== end of notice ===', '');\n return lines.join('\\n') + '\\n';\n }\n\n /**\n * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir\n * already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a\n * path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.\n */\n private safeName(file: string, used: Set<string>): string {\n const flat = `${file.replace(/\\//g, '__')}.diff`;\n if (!used.has(flat)) {\n used.add(flat);\n return flat;\n }\n for (let n = 2; ; n++) {\n const candidate = `${file.replace(/\\//g, '__')}-${n}.diff`;\n if (!used.has(candidate)) {\n used.add(candidate);\n return candidate;\n }\n }\n }\n\n private writeManifest(dir: string, manifest: DiffManifest): DiffManifest {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\\n');\n return manifest;\n }\n\n private gitOut(repoRoot: string, args: string[]): string {\n // maxBuffer: a monorepo diff routinely exceeds node's 1MB default, and the failure mode is a\n // truncated capture that looks like a complete one.\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });\n return result.status === 0 ? (result.stdout ?? '') : '';\n }\n}\n"]}
@@ -13,6 +13,24 @@ export declare class ReviewerBriefingBuilder {
13
13
  constructor(reviewJsonService: ReviewJsonService);
14
14
  build(repoRoot: string, scan: ChecklistScan, manifest: DiffManifest, diffDir: string, config: PrGateConfig): ReviewerBriefing[];
15
15
  private one;
16
+ /**
17
+ * One row of the reviewer's table. The SOURCE path comes over with the diff path because the two need
18
+ * identical affordance: when the diff was an absolute path and the source was a parent directory, four of
19
+ * four reviewers read the diff and none opened a source file.
20
+ */
21
+ private briefedFile;
22
+ /**
23
+ * Manifest headline facts, copied onto the briefing so the instructions can state them INLINE. A reviewer
24
+ * that never opens the manifest still has to learn whether anything was truncated or excluded — that is
25
+ * precisely the thing it could not otherwise know it was missing.
26
+ */
27
+ private copyManifestFacts;
28
+ /**
29
+ * The reviewer's own `.claude/agents/<subagent>.md`, when THIS diff changes it. Detected here rather than
30
+ * left to the reviewer to notice: on the measured run two reviewers reviewed their own definition and
31
+ * neither mentioned it, and every PR that edits the review gate reproduces that.
32
+ */
33
+ private ownAgentFileIn;
16
34
  /**
17
35
  * Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say
18
36
  * "the neighbourhood is here" — a reviewer already has the file list; what it lacks is where to look for
@@ -22,27 +22,65 @@ let ReviewerBriefingBuilder = class ReviewerBriefingBuilder {
22
22
  // Resolved ONCE for the whole run, not per reviewer: `require.resolve` hits the filesystem, and the
23
23
  // answer cannot differ between two reviewers in the same process.
24
24
  const shared = this.contextEntries(repoRoot, config);
25
- const diffByFile = new Map();
25
+ const entryByFile = new Map();
26
26
  for (const e of manifest.entries)
27
- diffByFile.set(e.file, path.resolve(repoRoot, e.diffFile));
28
- return scan.applicable.map((req) => this.one(repoRoot, req, scan, diffByFile, diffDir, shared));
27
+ entryByFile.set(e.file, e);
28
+ return scan.applicable.map((req) => this.one(repoRoot, req, scan, entryByFile, diffDir, shared, manifest));
29
29
  }
30
30
  // eslint-disable-next-line @typescript-eslint/max-params
31
- one(repoRoot, req, scan, diffByFile, diffDir, shared) {
31
+ one(repoRoot, req, scan, entryByFile, diffDir, shared, manifest) {
32
32
  const b = new rules_config_1.ReviewerBriefing(req.subagent, req.id, repoRoot);
33
33
  b.docPath = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);
34
34
  b.diffDir = diffDir;
35
35
  b.allDiffPath = diffDir === '' ? '' : path.join(diffDir, 'ALL.diff');
36
36
  b.manifestPath = diffDir === '' ? '' : path.join(diffDir, 'manifest.json');
37
- b.myFiles = req.matchedFiles.map((f) => new rules_config_1.BriefedFile(f, diffByFile.get(f) ?? ''));
37
+ b.myFiles = req.matchedFiles.map((f) => this.briefedFile(repoRoot, f, entryByFile));
38
38
  b.matchedPatterns = req.matchedPatterns;
39
39
  b.sourceDirs = this.sourceDirsFor(repoRoot, req.matchedFiles);
40
40
  b.contextEntries = shared;
41
41
  b.verdictPath = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);
42
42
  b.fileDiffCommand = scan.context.fileDiffCommand;
43
43
  b.dirty = scan.basis.dirty;
44
+ b.ownAgentFileInDiff = this.ownAgentFileIn(manifest, req.subagent);
45
+ this.copyManifestFacts(b, manifest);
44
46
  return b;
45
47
  }
48
+ /**
49
+ * One row of the reviewer's table. The SOURCE path comes over with the diff path because the two need
50
+ * identical affordance: when the diff was an absolute path and the source was a parent directory, four of
51
+ * four reviewers read the diff and none opened a source file.
52
+ */
53
+ briefedFile(repoRoot, file, entryByFile) {
54
+ const e = entryByFile.get(file);
55
+ if (e === undefined)
56
+ return new rules_config_1.BriefedFile(file, '', path.resolve(repoRoot, file));
57
+ return new rules_config_1.BriefedFile(file, e.diffAbs, e.fileAbs, e.status, e.bytes, e.truncated);
58
+ }
59
+ /**
60
+ * Manifest headline facts, copied onto the briefing so the instructions can state them INLINE. A reviewer
61
+ * that never opens the manifest still has to learn whether anything was truncated or excluded — that is
62
+ * precisely the thing it could not otherwise know it was missing.
63
+ */
64
+ copyManifestFacts(b, manifest) {
65
+ b.changedFileCount = manifest.entries.length;
66
+ b.truncatedCount = manifest.entries.filter((e) => e.truncated).length;
67
+ b.excludedCount = manifest.excluded.length;
68
+ b.allDiffLines = manifest.allDiffLines;
69
+ b.allDiffBytes = manifest.allDiffBytes;
70
+ b.hashForkPoint = manifest.hashForkPoint;
71
+ b.hashFeatureHead = manifest.hashFeatureHead;
72
+ b.hashMainHead = manifest.hashMainHead;
73
+ }
74
+ /**
75
+ * The reviewer's own `.claude/agents/<subagent>.md`, when THIS diff changes it. Detected here rather than
76
+ * left to the reviewer to notice: on the measured run two reviewers reviewed their own definition and
77
+ * neither mentioned it, and every PR that edits the review gate reproduces that.
78
+ */
79
+ ownAgentFileIn(manifest, subagent) {
80
+ const suffix = path.join('.claude', 'agents', `${subagent}.md`);
81
+ const hit = manifest.entries.find((e) => e.file.split('/').join(path.sep).endsWith(suffix));
82
+ return hit === undefined ? '' : (hit.fileAbs === '' ? hit.file : hit.fileAbs);
83
+ }
46
84
  /**
47
85
  * Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say
48
86
  * "the neighbourhood is here" — a reviewer already has the file list; what it lacks is where to look for
@@ -1 +1 @@
1
- {"version":3,"file":"reviewer-briefing-builder.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-briefing-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n BriefedFile, ContextEntry, PrGateConfig, RequiredChecklist, ReviewerBriefing, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffManifest } from './diff-materializer';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * Turns a {@link ChecklistScan} plus the materialized {@link DiffManifest} into one {@link ReviewerBriefing}\n * per reviewer — resolving every path to an absolute one, because a reviewer that has to resolve a path\n * ends up searching, and searching is the cost this whole feature exists to remove.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerBriefingBuilder {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n build(repoRoot: string, scan: ChecklistScan, manifest: DiffManifest, diffDir: string, config: PrGateConfig): ReviewerBriefing[] {\n // Resolved ONCE for the whole run, not per reviewer: `require.resolve` hits the filesystem, and the\n // answer cannot differ between two reviewers in the same process.\n const shared = this.contextEntries(repoRoot, config);\n const diffByFile = new Map<string, string>();\n for (const e of manifest.entries) diffByFile.set(e.file, path.resolve(repoRoot, e.diffFile));\n return scan.applicable.map((req: RequiredChecklist): ReviewerBriefing =>\n this.one(repoRoot, req, scan, diffByFile, diffDir, shared));\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private one(\n repoRoot: string, req: RequiredChecklist, scan: ChecklistScan,\n diffByFile: Map<string, string>, diffDir: string, shared: ContextEntry[],\n ): ReviewerBriefing {\n const b = new ReviewerBriefing(req.subagent, req.id, repoRoot);\n b.docPath = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n b.diffDir = diffDir;\n b.allDiffPath = diffDir === '' ? '' : path.join(diffDir, 'ALL.diff');\n b.manifestPath = diffDir === '' ? '' : path.join(diffDir, 'manifest.json');\n b.myFiles = req.matchedFiles.map((f: string): BriefedFile => new BriefedFile(f, diffByFile.get(f) ?? ''));\n b.matchedPatterns = req.matchedPatterns;\n b.sourceDirs = this.sourceDirsFor(repoRoot, req.matchedFiles);\n b.contextEntries = shared;\n b.verdictPath = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);\n b.fileDiffCommand = scan.context.fileDiffCommand;\n b.dirty = scan.basis.dirty;\n return b;\n }\n\n /**\n * Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say\n * \"the neighbourhood is here\" — a reviewer already has the file list; what it lacks is where to look for\n * the code AROUND the change.\n */\n private sourceDirsFor(repoRoot: string, files: readonly string[]): string[] {\n const dirs = new Set<string>();\n for (const f of files) dirs.add(path.resolve(repoRoot, path.dirname(f)));\n return [...dirs].sort();\n }\n\n /**\n * The pre-resolved context list, from two config keys with deliberately different jobs:\n *\n * `reviewContextPackages` — installed packages, resolved to their real on-disk dir. This is what deletes\n * the repeated `node_modules/@webpieces` greps: the tooling knows where a package resolved to, and the\n * reviewer does not.\n * `reviewContext` — the repo-specific escape hatch (`{label, path}`) for what tooling cannot possibly\n * guess, e.g. \"queue names live in terraform/services/\".\n *\n * Neither ever drops an entry: an unresolvable package or a missing path is LISTED, with the reason. A\n * silently shorter list is indistinguishable from a complete one.\n */\n private contextEntries(repoRoot: string, config: PrGateConfig): ContextEntry[] {\n const entries: ContextEntry[] = [];\n for (const pkg of config.reviewContextPackages) entries.push(this.resolvePackage(repoRoot, pkg));\n for (const item of config.reviewContext) {\n const abs = path.resolve(repoRoot, item.path);\n entries.push(fs.existsSync(abs)\n ? new ContextEntry(item.label, abs)\n : new ContextEntry(item.label, '', `(configured as \"${item.path}\" but missing)`));\n }\n return entries;\n }\n\n private resolvePackage(repoRoot: string, pkg: string): ContextEntry {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unresolvable package is REPORTED as such, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const manifestPath = require.resolve(`${pkg}/package.json`, { paths: [repoRoot] });\n const dir = path.dirname(manifestPath);\n // webpieces-disable no-any-unknown -- opaque package.json, only `description` is read\n const meta = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n const desc = typeof meta['description'] === 'string' ? (meta['description'] as string) : '';\n return new ContextEntry(desc === '' ? pkg : `${pkg} — ${desc}`, dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unresolvable package is REPORTED below, never fatal\n return new ContextEntry(pkg, '', '(not installed — configured in pr-gate.reviewContextPackages)');\n }\n }\n}\n"]}
1
+ {"version":3,"file":"reviewer-briefing-builder.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-briefing-builder.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,0DAEiC;AACjC,yCAA2D;AAI3D;;;;;;GAMG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IACH;IAA7B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAErE,KAAK,CAAC,QAAgB,EAAE,IAAmB,EAAE,QAAsB,EAAE,OAAe,EAAE,MAAoB;QACtG,oGAAoG;QACpG,kEAAkE;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACrD,MAAM,WAAW,GAAG,IAAI,GAAG,EAA6B,CAAC;QACzD,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,OAAO;YAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAsB,EAAoB,EAAE,CACpE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,yDAAyD;IACjD,GAAG,CACP,QAAgB,EAAE,GAAsB,EAAE,IAAmB,EAC7D,WAA2C,EAAE,OAAe,EAAE,MAAsB,EAAE,QAAsB;QAE5G,MAAM,CAAC,GAAG,IAAI,+BAAgB,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC/D,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACzE,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC;QACpB,CAAC,CAAC,WAAW,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QACrE,CAAC,CAAC,YAAY,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;QAC3E,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAS,EAAe,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;QACzG,CAAC,CAAC,eAAe,GAAG,GAAG,CAAC,eAAe,CAAC;QACxC,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;QAC9D,CAAC,CAAC,cAAc,GAAG,MAAM,CAAC;QAC1B,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,iBAAiB,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QACpF,CAAC,CAAC,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC;QACjD,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC3B,CAAC,CAAC,kBAAkB,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnE,IAAI,CAAC,iBAAiB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACpC,OAAO,CAAC,CAAC;IACb,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,QAAgB,EAAE,IAAY,EAAE,WAA2C;QAC3F,MAAM,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,SAAS;YAAE,OAAO,IAAI,0BAAW,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;QACpF,OAAO,IAAI,0BAAW,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;IACvF,CAAC;IAED;;;;OAIG;IACK,iBAAiB,CAAC,CAAmB,EAAE,QAAsB;QACjE,CAAC,CAAC,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;QAC7C,CAAC,CAAC,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAoB,EAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;QAClG,CAAC,CAAC,aAAa,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC3C,CAAC,CAAC,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;QACvC,CAAC,CAAC,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;QACvC,CAAC,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;QACzC,CAAC,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;QAC7C,CAAC,CAAC,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACK,cAAc,CAAC,QAAsB,EAAE,QAAgB;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAoB,EAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACxH,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClF,CAAC;IAED;;;;OAIG;IACK,aAAa,CAAC,QAAgB,EAAE,KAAwB;QAC5D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;OAWG;IACK,cAAc,CAAC,QAAgB,EAAE,MAAoB;QACzD,MAAM,OAAO,GAAmB,EAAE,CAAC;QACnC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,qBAAqB;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;QACjG,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9C,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;gBAC3B,CAAC,CAAC,IAAI,2BAAY,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC;gBACnC,CAAC,CAAC,IAAI,2BAAY,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,mBAAmB,IAAI,CAAC,IAAI,gBAAgB,CAAC,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,OAAO,CAAC;IACnB,CAAC;IAEO,cAAc,CAAC,QAAgB,EAAE,GAAW;QAChD,oHAAoH;QACpH,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,GAAG,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACnF,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACvC,sFAAsF;YACtF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAA4B,CAAC;YAC1F,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAI,CAAC,aAAa,CAAY,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5F,OAAO,IAAI,2BAAY,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;QACzE,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,sBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC,CAAC,yDAAyD;YACrE,OAAO,IAAI,2BAAY,CAAC,GAAG,EAAE,EAAE,EAAE,+DAA+D,CAAC,CAAC;QACtG,CAAC;IACL,CAAC;CACJ,CAAA;AA5HY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEW,gCAAiB;GADxD,uBAAuB,CA4HnC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n BriefedFile, ContextEntry, PrGateConfig, RequiredChecklist, ReviewerBriefing, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffManifest, DiffManifestEntry } from './diff-materializer';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * Turns a {@link ChecklistScan} plus the materialized {@link DiffManifest} into one {@link ReviewerBriefing}\n * per reviewer — resolving every path to an absolute one, because a reviewer that has to resolve a path\n * ends up searching, and searching is the cost this whole feature exists to remove.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerBriefingBuilder {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n build(repoRoot: string, scan: ChecklistScan, manifest: DiffManifest, diffDir: string, config: PrGateConfig): ReviewerBriefing[] {\n // Resolved ONCE for the whole run, not per reviewer: `require.resolve` hits the filesystem, and the\n // answer cannot differ between two reviewers in the same process.\n const shared = this.contextEntries(repoRoot, config);\n const entryByFile = new Map<string, DiffManifestEntry>();\n for (const e of manifest.entries) entryByFile.set(e.file, e);\n return scan.applicable.map((req: RequiredChecklist): ReviewerBriefing =>\n this.one(repoRoot, req, scan, entryByFile, diffDir, shared, manifest));\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private one(\n repoRoot: string, req: RequiredChecklist, scan: ChecklistScan,\n entryByFile: Map<string, DiffManifestEntry>, diffDir: string, shared: ContextEntry[], manifest: DiffManifest,\n ): ReviewerBriefing {\n const b = new ReviewerBriefing(req.subagent, req.id, repoRoot);\n b.docPath = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n b.diffDir = diffDir;\n b.allDiffPath = diffDir === '' ? '' : path.join(diffDir, 'ALL.diff');\n b.manifestPath = diffDir === '' ? '' : path.join(diffDir, 'manifest.json');\n b.myFiles = req.matchedFiles.map((f: string): BriefedFile => this.briefedFile(repoRoot, f, entryByFile));\n b.matchedPatterns = req.matchedPatterns;\n b.sourceDirs = this.sourceDirsFor(repoRoot, req.matchedFiles);\n b.contextEntries = shared;\n b.verdictPath = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);\n b.fileDiffCommand = scan.context.fileDiffCommand;\n b.dirty = scan.basis.dirty;\n b.ownAgentFileInDiff = this.ownAgentFileIn(manifest, req.subagent);\n this.copyManifestFacts(b, manifest);\n return b;\n }\n\n /**\n * One row of the reviewer's table. The SOURCE path comes over with the diff path because the two need\n * identical affordance: when the diff was an absolute path and the source was a parent directory, four of\n * four reviewers read the diff and none opened a source file.\n */\n private briefedFile(repoRoot: string, file: string, entryByFile: Map<string, DiffManifestEntry>): BriefedFile {\n const e = entryByFile.get(file);\n if (e === undefined) return new BriefedFile(file, '', path.resolve(repoRoot, file));\n return new BriefedFile(file, e.diffAbs, e.fileAbs, e.status, e.bytes, e.truncated);\n }\n\n /**\n * Manifest headline facts, copied onto the briefing so the instructions can state them INLINE. A reviewer\n * that never opens the manifest still has to learn whether anything was truncated or excluded — that is\n * precisely the thing it could not otherwise know it was missing.\n */\n private copyManifestFacts(b: ReviewerBriefing, manifest: DiffManifest): void {\n b.changedFileCount = manifest.entries.length;\n b.truncatedCount = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated).length;\n b.excludedCount = manifest.excluded.length;\n b.allDiffLines = manifest.allDiffLines;\n b.allDiffBytes = manifest.allDiffBytes;\n b.hashForkPoint = manifest.hashForkPoint;\n b.hashFeatureHead = manifest.hashFeatureHead;\n b.hashMainHead = manifest.hashMainHead;\n }\n\n /**\n * The reviewer's own `.claude/agents/<subagent>.md`, when THIS diff changes it. Detected here rather than\n * left to the reviewer to notice: on the measured run two reviewers reviewed their own definition and\n * neither mentioned it, and every PR that edits the review gate reproduces that.\n */\n private ownAgentFileIn(manifest: DiffManifest, subagent: string): string {\n const suffix = path.join('.claude', 'agents', `${subagent}.md`);\n const hit = manifest.entries.find((e: DiffManifestEntry): boolean => e.file.split('/').join(path.sep).endsWith(suffix));\n return hit === undefined ? '' : (hit.fileAbs === '' ? hit.file : hit.fileAbs);\n }\n\n /**\n * Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say\n * \"the neighbourhood is here\" — a reviewer already has the file list; what it lacks is where to look for\n * the code AROUND the change.\n */\n private sourceDirsFor(repoRoot: string, files: readonly string[]): string[] {\n const dirs = new Set<string>();\n for (const f of files) dirs.add(path.resolve(repoRoot, path.dirname(f)));\n return [...dirs].sort();\n }\n\n /**\n * The pre-resolved context list, from two config keys with deliberately different jobs:\n *\n * `reviewContextPackages` — installed packages, resolved to their real on-disk dir. This is what deletes\n * the repeated `node_modules/@webpieces` greps: the tooling knows where a package resolved to, and the\n * reviewer does not.\n * `reviewContext` — the repo-specific escape hatch (`{label, path}`) for what tooling cannot possibly\n * guess, e.g. \"queue names live in terraform/services/\".\n *\n * Neither ever drops an entry: an unresolvable package or a missing path is LISTED, with the reason. A\n * silently shorter list is indistinguishable from a complete one.\n */\n private contextEntries(repoRoot: string, config: PrGateConfig): ContextEntry[] {\n const entries: ContextEntry[] = [];\n for (const pkg of config.reviewContextPackages) entries.push(this.resolvePackage(repoRoot, pkg));\n for (const item of config.reviewContext) {\n const abs = path.resolve(repoRoot, item.path);\n entries.push(fs.existsSync(abs)\n ? new ContextEntry(item.label, abs)\n : new ContextEntry(item.label, '', `(configured as \"${item.path}\" but missing)`));\n }\n return entries;\n }\n\n private resolvePackage(repoRoot: string, pkg: string): ContextEntry {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unresolvable package is REPORTED as such, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const manifestPath = require.resolve(`${pkg}/package.json`, { paths: [repoRoot] });\n const dir = path.dirname(manifestPath);\n // webpieces-disable no-any-unknown -- opaque package.json, only `description` is read\n const meta = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n const desc = typeof meta['description'] === 'string' ? (meta['description'] as string) : '';\n return new ContextEntry(desc === '' ? pkg : `${pkg} — ${desc}`, dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unresolvable package is REPORTED below, never fatal\n return new ContextEntry(pkg, '', '(not installed — configured in pr-gate.reviewContextPackages)');\n }\n }\n}\n"]}