@neurcode-ai/cli 0.16.8 → 0.17.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/dist/api-client.d.ts +39 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js.map +1 -1
- package/dist/commands/activate.js +15 -15
- package/dist/commands/activate.js.map +1 -1
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +89 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +21 -28
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/ops.d.ts +109 -0
- package/dist/commands/ops.d.ts.map +1 -0
- package/dist/commands/ops.js +292 -0
- package/dist/commands/ops.js.map +1 -0
- package/dist/commands/runtime-doctor.d.ts.map +1 -1
- package/dist/commands/runtime-doctor.js +144 -4
- package/dist/commands/runtime-doctor.js.map +1 -1
- package/dist/commands/runtime-sync.d.ts.map +1 -1
- package/dist/commands/runtime-sync.js +39 -0
- package/dist/commands/runtime-sync.js.map +1 -1
- package/dist/commands/session.d.ts +14 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +44 -0
- package/dist/commands/session.js.map +1 -1
- package/dist/index.js +18 -60
- package/dist/index.js.map +1 -1
- package/dist/runtime-build.json +5 -5
- package/dist/utils/enterprise-eval-report.d.ts +6 -0
- package/dist/utils/enterprise-eval-report.d.ts.map +1 -1
- package/dist/utils/enterprise-eval-report.js +25 -1
- package/dist/utils/enterprise-eval-report.js.map +1 -1
- package/dist/utils/eval-demo.d.ts.map +1 -1
- package/dist/utils/eval-demo.js +33 -8
- package/dist/utils/eval-demo.js.map +1 -1
- package/dist/utils/guided-eval.d.ts.map +1 -1
- package/dist/utils/guided-eval.js +27 -19
- package/dist/utils/guided-eval.js.map +1 -1
- package/dist/utils/local-repo-brain.d.ts +1 -0
- package/dist/utils/local-repo-brain.d.ts.map +1 -1
- package/dist/utils/local-repo-brain.js +1 -0
- package/dist/utils/local-repo-brain.js.map +1 -1
- package/dist/utils/repo-brain-impact.d.ts +283 -0
- package/dist/utils/repo-brain-impact.d.ts.map +1 -0
- package/dist/utils/repo-brain-impact.js +764 -0
- package/dist/utils/repo-brain-impact.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Repo Brain Impact Intelligence (V1) — deterministic + advisory change-impact
|
|
4
|
+
* analysis over the source-free local repo brain.
|
|
5
|
+
*
|
|
6
|
+
* Given a changed file (or set of changed files) this module answers the
|
|
7
|
+
* question an engineering manager actually asks about an AI change *before or
|
|
8
|
+
* after* it lands: what does this touch, who owns it, what is sensitive, who
|
|
9
|
+
* imports it, is it a hub, where are the nearby tests/docs, is the helper
|
|
10
|
+
* duplicated elsewhere, and what should a reviewer ask?
|
|
11
|
+
*
|
|
12
|
+
* Hard rules (shared with utils/local-repo-brain.ts and utils/guided-eval.ts):
|
|
13
|
+
* - Source-free: only relative paths, symbol *names*, counts, owner tokens,
|
|
14
|
+
* sensitive-kind labels, and hashes are read or emitted. Never source code,
|
|
15
|
+
* diff hunks, or file bodies.
|
|
16
|
+
* - Honest labelling: every finding is tagged `deterministic` (a compiled
|
|
17
|
+
* path / CODEOWNERS / static-import-graph fact) or `advisory` (a heuristic
|
|
18
|
+
* reuse / proximity / reviewer-question signal). We never present an
|
|
19
|
+
* advisory signal as a deterministic guarantee.
|
|
20
|
+
*
|
|
21
|
+
* The engine is pure (no I/O) — {@link computeRepoBrainImpact} takes an artifact
|
|
22
|
+
* (or null) and the changed paths. {@link buildRepoBrainImpactForRepo} is the
|
|
23
|
+
* thin I/O wrapper that reads (or builds) the brain and then computes.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.HIGH_FAN_IN_THRESHOLD = exports.IMPACT_SUMMARY_SCHEMA_VERSION = exports.REPO_BRAIN_IMPACT_SCHEMA_VERSION = void 0;
|
|
27
|
+
exports.normalizeImpactPath = normalizeImpactPath;
|
|
28
|
+
exports.classifyImpactFileRole = classifyImpactFileRole;
|
|
29
|
+
exports.matchesCodeownersPattern = matchesCodeownersPattern;
|
|
30
|
+
exports.computeRepoBrainImpact = computeRepoBrainImpact;
|
|
31
|
+
exports.summarizeImpact = summarizeImpact;
|
|
32
|
+
exports.buildRepoBrainImpactForRepo = buildRepoBrainImpactForRepo;
|
|
33
|
+
exports.renderRepoBrainImpactText = renderRepoBrainImpactText;
|
|
34
|
+
const local_repo_brain_1 = require("./local-repo-brain");
|
|
35
|
+
exports.REPO_BRAIN_IMPACT_SCHEMA_VERSION = 'neurcode.repo-brain-impact.v1';
|
|
36
|
+
exports.IMPACT_SUMMARY_SCHEMA_VERSION = 'neurcode.impact-summary.v1';
|
|
37
|
+
/** A file imported by enough other files to be a structural hub. */
|
|
38
|
+
exports.HIGH_FAN_IN_THRESHOLD = 5;
|
|
39
|
+
// ── Path + classification helpers (deterministic) ─────────────────────────────
|
|
40
|
+
function normalizeImpactPath(value, projectRoot) {
|
|
41
|
+
let p = String(value || '').trim().replace(/\\/g, '/');
|
|
42
|
+
if (projectRoot) {
|
|
43
|
+
const root = projectRoot.replace(/\\/g, '/').replace(/\/$/, '');
|
|
44
|
+
if (p.startsWith(`${root}/`))
|
|
45
|
+
p = p.slice(root.length + 1);
|
|
46
|
+
}
|
|
47
|
+
return p.replace(/^\.\//, '').replace(/^\/+/, '');
|
|
48
|
+
}
|
|
49
|
+
const TEST_PATTERNS = [/\/__tests__\//, /(^|\/)tests?\//, /\.test\.[cm]?[jt]sx?$/, /\.spec\.[cm]?[jt]sx?$/, /_test\.(py|go)$/, /(^|\/)test_[^/]+\.py$/];
|
|
50
|
+
const DOCS_PATTERNS = [/(^|\/)docs?\//, /\.mdx?$/, /(^|\/)readme/i, /(^|\/)changelog/i, /(^|\/)license/i];
|
|
51
|
+
const DATA_PATTERNS = [/\.(json|ya?ml|toml|csv|sql)$/];
|
|
52
|
+
function dirOf(path) {
|
|
53
|
+
const idx = path.lastIndexOf('/');
|
|
54
|
+
return idx === -1 ? '' : path.slice(0, idx);
|
|
55
|
+
}
|
|
56
|
+
function baseName(path) {
|
|
57
|
+
const idx = path.lastIndexOf('/');
|
|
58
|
+
return idx === -1 ? path : path.slice(idx + 1);
|
|
59
|
+
}
|
|
60
|
+
function stripExt(name) {
|
|
61
|
+
const idx = name.indexOf('.');
|
|
62
|
+
return idx === -1 ? name : name.slice(0, idx);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Deterministically classify a file's role from its path + sensitive kinds.
|
|
66
|
+
* Order matters: test > runtime_governance > config > docs > generated > data.
|
|
67
|
+
*/
|
|
68
|
+
function classifyImpactFileRole(path, opts = {}) {
|
|
69
|
+
const lower = path.toLowerCase();
|
|
70
|
+
const kinds = opts.sensitiveKinds ?? (0, local_repo_brain_1.sensitiveKindsFor)(path);
|
|
71
|
+
if (TEST_PATTERNS.some((p) => p.test(lower)))
|
|
72
|
+
return 'test';
|
|
73
|
+
if (kinds.includes('runtime_governance'))
|
|
74
|
+
return 'runtime_governance';
|
|
75
|
+
if (DOCS_PATTERNS.some((p) => p.test(lower)))
|
|
76
|
+
return 'docs';
|
|
77
|
+
if (kinds.includes('configuration') || kinds.includes('dependency') || kinds.includes('workflow'))
|
|
78
|
+
return 'config';
|
|
79
|
+
if (opts.generated)
|
|
80
|
+
return 'generated';
|
|
81
|
+
if (/\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|rs)$/.test(lower))
|
|
82
|
+
return 'source';
|
|
83
|
+
if (DATA_PATTERNS.some((p) => p.test(lower)))
|
|
84
|
+
return 'data';
|
|
85
|
+
return 'unknown';
|
|
86
|
+
}
|
|
87
|
+
// ── CODEOWNERS matching (deterministic, last-match-wins) ───────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Faithful subset of gitignore/CODEOWNERS glob semantics, sufficient for the
|
|
90
|
+
* common enterprise patterns: `src/billing/`, `*.py`, `/.github/workflows/`,
|
|
91
|
+
* `packages/cli/`, `docs/*`, `apps/web/**`, and exact file paths.
|
|
92
|
+
*/
|
|
93
|
+
function matchesCodeownersPattern(filePath, pattern) {
|
|
94
|
+
let p = String(pattern || '').trim();
|
|
95
|
+
if (!p)
|
|
96
|
+
return false;
|
|
97
|
+
const anchored = p.startsWith('/');
|
|
98
|
+
if (anchored)
|
|
99
|
+
p = p.slice(1);
|
|
100
|
+
const dirOnly = p.endsWith('/');
|
|
101
|
+
if (dirOnly)
|
|
102
|
+
p = p.replace(/\/+$/, '');
|
|
103
|
+
if (!p)
|
|
104
|
+
return false;
|
|
105
|
+
const hasGlob = /[*?]/.test(p);
|
|
106
|
+
if (!hasGlob) {
|
|
107
|
+
// Bare path or directory: matches that path or anything beneath it.
|
|
108
|
+
if (filePath === p)
|
|
109
|
+
return !dirOnly; // a trailing-slash pattern only matches things *under* the dir
|
|
110
|
+
return filePath === p || filePath.startsWith(`${p}/`);
|
|
111
|
+
}
|
|
112
|
+
let body = '';
|
|
113
|
+
for (let i = 0; i < p.length; i += 1) {
|
|
114
|
+
const c = p[i];
|
|
115
|
+
if (c === '*') {
|
|
116
|
+
if (p[i + 1] === '*') {
|
|
117
|
+
body += '.*';
|
|
118
|
+
i += 1;
|
|
119
|
+
if (p[i + 1] === '/')
|
|
120
|
+
i += 1;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
body += '[^/]*';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (c === '?') {
|
|
127
|
+
body += '[^/]';
|
|
128
|
+
}
|
|
129
|
+
else if ('+.^$()[]{}|\\'.includes(c)) {
|
|
130
|
+
body += `\\${c}`;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
body += c;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const prefix = anchored || p.includes('/') ? '^' : '(^|/)';
|
|
137
|
+
const suffix = dirOnly ? '/' : '(/|$)';
|
|
138
|
+
try {
|
|
139
|
+
return new RegExp(`${prefix}${body}${suffix}`).test(dirOnly ? `${filePath}/` : filePath);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ── Core computation ──────────────────────────────────────────────────────────
|
|
146
|
+
const EMPTY_ROLE_COUNTS = () => ({
|
|
147
|
+
source: 0,
|
|
148
|
+
test: 0,
|
|
149
|
+
docs: 0,
|
|
150
|
+
config: 0,
|
|
151
|
+
runtime_governance: 0,
|
|
152
|
+
generated: 0,
|
|
153
|
+
data: 0,
|
|
154
|
+
unknown: 0,
|
|
155
|
+
});
|
|
156
|
+
const DETERMINISTIC_LABELS = [
|
|
157
|
+
'changed-file classification (path, language, module, sensitive kind)',
|
|
158
|
+
'CODEOWNERS owner routing (last-match-wins)',
|
|
159
|
+
'sensitive surfaces (path heuristics encoded in the index)',
|
|
160
|
+
'static relative-import consumers (fan-in)',
|
|
161
|
+
'static relative-import dependencies (fan-out)',
|
|
162
|
+
'high fan-out / hub status (import graph)',
|
|
163
|
+
];
|
|
164
|
+
const ADVISORY_LABELS = [
|
|
165
|
+
'reuse / duplicate-helper findings (same-name or fingerprint resemblance)',
|
|
166
|
+
'nearby tests / docs / config by directory proximity',
|
|
167
|
+
'recommended reviewer questions',
|
|
168
|
+
];
|
|
169
|
+
function reuseWhyFlagged(input) {
|
|
170
|
+
const symbol = input.symbolName ? `"${input.symbolName}"` : 'a source-free declaration fingerprint';
|
|
171
|
+
const basis = input.kind === 'fingerprint_reuse'
|
|
172
|
+
? 'has a similar source-free token fingerprint'
|
|
173
|
+
: 'uses the same declaration name';
|
|
174
|
+
return `${symbol} ${basis} across ${input.files.length} file(s); reasons: ${input.reasonCodes.join(', ') || 'same-name/fingerprint resemblance'}.`;
|
|
175
|
+
}
|
|
176
|
+
function reuseCheckNext(confidence) {
|
|
177
|
+
if (confidence === 'high') {
|
|
178
|
+
return 'Compare callers, inputs, side effects, and ownership before extracting a shared helper.';
|
|
179
|
+
}
|
|
180
|
+
if (confidence === 'medium') {
|
|
181
|
+
return 'Check whether the repeated name represents the same behavior before treating it as reusable.';
|
|
182
|
+
}
|
|
183
|
+
return 'Treat as a light review prompt only; same names can represent unrelated behavior.';
|
|
184
|
+
}
|
|
185
|
+
function buildImpactRadius(input) {
|
|
186
|
+
const affectedRoles = Array.from(new Set([...input.changedFiles.map((f) => f.role), ...input.allConsumers.map((c) => c.role)])).sort();
|
|
187
|
+
const configImpact = {
|
|
188
|
+
configuration: input.changedFiles.filter((f) => f.sensitiveKinds.includes('configuration')).map((f) => f.path),
|
|
189
|
+
workflow: input.changedFiles.filter((f) => f.sensitiveKinds.includes('workflow')).map((f) => f.path),
|
|
190
|
+
dependency: input.changedFiles.filter((f) => f.sensitiveKinds.includes('dependency')).map((f) => f.path),
|
|
191
|
+
runtimeGovernance: input.changedFiles.filter((f) => f.sensitiveKinds.includes('runtime_governance')).map((f) => f.path),
|
|
192
|
+
};
|
|
193
|
+
const reasons = [];
|
|
194
|
+
if (input.sensitiveKinds.length)
|
|
195
|
+
reasons.push(`Sensitive surface(s): ${input.sensitiveKinds.join(', ')}`);
|
|
196
|
+
if (input.routeTo.length)
|
|
197
|
+
reasons.push(`CODEOWNERS route: ${input.routeTo.join(', ')}`);
|
|
198
|
+
if (input.allConsumers.length)
|
|
199
|
+
reasons.push(`${input.allConsumers.length} static importer(s) depend on the changed set`);
|
|
200
|
+
if (input.isHighFanOut)
|
|
201
|
+
reasons.push('High fan-in / hub signal is present');
|
|
202
|
+
if (configImpact.workflow.length)
|
|
203
|
+
reasons.push('CI/workflow configuration is touched');
|
|
204
|
+
if (configImpact.dependency.length)
|
|
205
|
+
reasons.push('Dependency manifest or lockfile is touched');
|
|
206
|
+
if (configImpact.runtimeGovernance.length)
|
|
207
|
+
reasons.push('Runtime governance surface is touched');
|
|
208
|
+
if (input.likelyTests.length === 0 && input.changedFiles.some((f) => f.role === 'source')) {
|
|
209
|
+
reasons.push('No likely tests were found for changed source files');
|
|
210
|
+
}
|
|
211
|
+
if (reasons.length === 0)
|
|
212
|
+
reasons.push('No elevated structural signal was found in the source-free brain');
|
|
213
|
+
const high = input.sensitiveKinds.length > 0 ||
|
|
214
|
+
input.isHighFanOut ||
|
|
215
|
+
configImpact.workflow.length > 0 ||
|
|
216
|
+
configImpact.dependency.length > 0 ||
|
|
217
|
+
configImpact.runtimeGovernance.length > 0;
|
|
218
|
+
const medium = high || input.routeTo.length > 0 || input.allConsumers.length > 0 || input.likelyTests.length === 0;
|
|
219
|
+
const riskLevel = high ? 'high' : medium ? 'medium' : 'low';
|
|
220
|
+
const whyThisMatters = riskLevel === 'high'
|
|
221
|
+
? 'Reviewers should check owner authority, blast radius, rollout, and tests before merge.'
|
|
222
|
+
: riskLevel === 'medium'
|
|
223
|
+
? 'Reviewers should check routed owners, importers, and likely tests before merge.'
|
|
224
|
+
: 'The structural map suggests low blast radius, but reviewers should still verify behavior.';
|
|
225
|
+
return {
|
|
226
|
+
riskLevel,
|
|
227
|
+
reasons,
|
|
228
|
+
deterministic: {
|
|
229
|
+
consumerCount: input.allConsumers.length,
|
|
230
|
+
affectedRoles,
|
|
231
|
+
reviewerOwners: input.routeTo,
|
|
232
|
+
sensitiveKinds: input.sensitiveKinds,
|
|
233
|
+
configImpact,
|
|
234
|
+
},
|
|
235
|
+
advisory: {
|
|
236
|
+
likelyTests: input.likelyTests.slice(0, 12),
|
|
237
|
+
whyThisMatters,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function computeRepoBrainImpact(artifact, requestedPaths, options = {}) {
|
|
242
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
243
|
+
const maxConsumers = Math.max(1, options.maxConsumers ?? 40);
|
|
244
|
+
const maxDependencies = Math.max(1, options.maxDependencies ?? 40);
|
|
245
|
+
const maxReviewQuestions = Math.max(1, options.maxReviewQuestions ?? 10);
|
|
246
|
+
const changedPaths = Array.from(new Set(requestedPaths.map((p) => normalizeImpactPath(p)).filter(Boolean))).sort();
|
|
247
|
+
const changedSet = new Set(changedPaths);
|
|
248
|
+
const brainStatus = options.brainStatus ?? (artifact ? 'found' : 'missing');
|
|
249
|
+
const recoveryCommand = 'neurcode brain index';
|
|
250
|
+
// ── changed-file classification ─────────────────────────────────────────────
|
|
251
|
+
const fileByPath = new Map((artifact?.files ?? []).map((f) => [f.path, f]));
|
|
252
|
+
const changedFiles = changedPaths.map((path) => {
|
|
253
|
+
const file = fileByPath.get(path);
|
|
254
|
+
if (file) {
|
|
255
|
+
return {
|
|
256
|
+
path,
|
|
257
|
+
indexed: true,
|
|
258
|
+
role: classifyImpactFileRole(path, { generated: file.generated, sensitiveKinds: file.sensitiveKinds }),
|
|
259
|
+
language: file.language,
|
|
260
|
+
module: file.module,
|
|
261
|
+
sensitiveKinds: file.sensitiveKinds,
|
|
262
|
+
symbolCount: file.symbolCount,
|
|
263
|
+
importCount: file.importCount,
|
|
264
|
+
generated: file.generated,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const sensitiveKinds = (0, local_repo_brain_1.sensitiveKindsFor)(path);
|
|
268
|
+
return {
|
|
269
|
+
path,
|
|
270
|
+
indexed: false,
|
|
271
|
+
role: classifyImpactFileRole(path, { sensitiveKinds }),
|
|
272
|
+
language: null,
|
|
273
|
+
module: null,
|
|
274
|
+
sensitiveKinds,
|
|
275
|
+
symbolCount: null,
|
|
276
|
+
importCount: null,
|
|
277
|
+
generated: false,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
// ── owners (deterministic, last-match-wins) ─────────────────────────────────
|
|
281
|
+
const boundaries = artifact?.ownerBoundaries ?? [];
|
|
282
|
+
// effectiveOwnerByPath: the owners of the *last* matching boundary for each path.
|
|
283
|
+
const effectiveOwnerByPath = new Map();
|
|
284
|
+
for (const path of changedPaths) {
|
|
285
|
+
for (const boundary of boundaries) {
|
|
286
|
+
if (matchesCodeownersPattern(path, boundary.pattern)) {
|
|
287
|
+
effectiveOwnerByPath.set(path, { pattern: boundary.pattern, owners: boundary.owners });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const matchAccumulator = new Map();
|
|
292
|
+
for (const path of changedPaths) {
|
|
293
|
+
for (const boundary of boundaries) {
|
|
294
|
+
if (!matchesCodeownersPattern(path, boundary.pattern))
|
|
295
|
+
continue;
|
|
296
|
+
const key = `${boundary.pattern}|${boundary.owners.join(',')}`;
|
|
297
|
+
const existing = matchAccumulator.get(key);
|
|
298
|
+
const effective = effectiveOwnerByPath.get(path)?.pattern === boundary.pattern;
|
|
299
|
+
if (existing) {
|
|
300
|
+
if (!existing.matchedPaths.includes(path))
|
|
301
|
+
existing.matchedPaths.push(path);
|
|
302
|
+
existing.effective = existing.effective || effective;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
matchAccumulator.set(key, {
|
|
306
|
+
pattern: boundary.pattern,
|
|
307
|
+
owners: boundary.owners,
|
|
308
|
+
matchedPaths: [path],
|
|
309
|
+
effective,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const ownerMatches = [...matchAccumulator.values()].sort((a, b) => Number(b.effective) - Number(a.effective) || a.pattern.localeCompare(b.pattern));
|
|
315
|
+
const routeTo = Array.from(new Set([...effectiveOwnerByPath.values()].flatMap((e) => e.owners))).sort();
|
|
316
|
+
const ownerStatus = (artifact?.summary.ownerBoundaryStatus ?? 'not_found') === 'found' && ownerMatches.length > 0
|
|
317
|
+
? 'found'
|
|
318
|
+
: 'not_found';
|
|
319
|
+
// ── sensitive surfaces (deterministic) ──────────────────────────────────────
|
|
320
|
+
const sensitiveSurfaces = changedFiles
|
|
321
|
+
.filter((f) => f.sensitiveKinds.length > 0)
|
|
322
|
+
.map((f) => ({ path: f.path, kinds: f.sensitiveKinds }));
|
|
323
|
+
const sensitiveKinds = Array.from(new Set(sensitiveSurfaces.flatMap((s) => s.kinds))).sort();
|
|
324
|
+
// ── consumers (deterministic fan-in via resolved relative imports) ──────────
|
|
325
|
+
const imports = artifact?.imports ?? [];
|
|
326
|
+
const consumerMap = new Map();
|
|
327
|
+
for (const edge of imports) {
|
|
328
|
+
if (!edge.resolvedFile || !changedSet.has(edge.resolvedFile))
|
|
329
|
+
continue;
|
|
330
|
+
if (changedSet.has(edge.fromFile))
|
|
331
|
+
continue; // a changed file importing another changed file is noise here
|
|
332
|
+
const entry = consumerMap.get(edge.fromFile) ?? { edgeCount: 0, imports: new Set() };
|
|
333
|
+
entry.edgeCount += 1;
|
|
334
|
+
entry.imports.add(edge.resolvedFile);
|
|
335
|
+
consumerMap.set(edge.fromFile, entry);
|
|
336
|
+
}
|
|
337
|
+
const allConsumers = [...consumerMap.entries()]
|
|
338
|
+
.map(([path, entry]) => ({
|
|
339
|
+
path,
|
|
340
|
+
role: classifyImpactFileRole(path, { sensitiveKinds: fileByPath.get(path)?.sensitiveKinds }),
|
|
341
|
+
edgeCount: entry.edgeCount,
|
|
342
|
+
imports: [...entry.imports].sort().slice(0, 4),
|
|
343
|
+
}))
|
|
344
|
+
.sort((a, b) => b.edgeCount - a.edgeCount || a.path.localeCompare(b.path));
|
|
345
|
+
const byRole = EMPTY_ROLE_COUNTS();
|
|
346
|
+
for (const consumer of allConsumers)
|
|
347
|
+
byRole[consumer.role] += 1;
|
|
348
|
+
const direct = allConsumers.slice(0, maxConsumers);
|
|
349
|
+
// ── dependencies (deterministic fan-out from the changed files) ─────────────
|
|
350
|
+
const internalDepMap = new Map();
|
|
351
|
+
const externalPackages = new Set();
|
|
352
|
+
for (const edge of imports) {
|
|
353
|
+
if (!changedSet.has(edge.fromFile))
|
|
354
|
+
continue;
|
|
355
|
+
if (edge.resolvedFile && !changedSet.has(edge.resolvedFile)) {
|
|
356
|
+
internalDepMap.set(edge.resolvedFile, {
|
|
357
|
+
target: edge.target,
|
|
358
|
+
resolvedFile: edge.resolvedFile,
|
|
359
|
+
external: false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
else if (!edge.resolvedFile && edge.targetKind === 'package') {
|
|
363
|
+
externalPackages.add(edge.target);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const internalDeps = [...internalDepMap.values()].sort((a, b) => (a.resolvedFile ?? '').localeCompare(b.resolvedFile ?? ''));
|
|
367
|
+
// ── high fan-out / hubs (deterministic) ─────────────────────────────────────
|
|
368
|
+
const hotspotByPath = new Map((artifact?.hotspots ?? []).map((h) => [h.file, h]));
|
|
369
|
+
const hotspots = changedPaths
|
|
370
|
+
.map((path) => hotspotByPath.get(path))
|
|
371
|
+
.filter((h) => Boolean(h))
|
|
372
|
+
.map((h) => ({
|
|
373
|
+
path: h.file,
|
|
374
|
+
score: h.score,
|
|
375
|
+
fanIn: h.importFanIn,
|
|
376
|
+
fanOut: h.importFanOut,
|
|
377
|
+
reasons: h.reasons,
|
|
378
|
+
isHub: h.importFanIn >= exports.HIGH_FAN_IN_THRESHOLD,
|
|
379
|
+
}))
|
|
380
|
+
.sort((a, b) => b.score - a.score);
|
|
381
|
+
const isHighFanOut = hotspots.some((h) => h.isHub) || allConsumers.length >= exports.HIGH_FAN_IN_THRESHOLD;
|
|
382
|
+
// ── nearby tests / docs / config / runtime (advisory proximity) ─────────────
|
|
383
|
+
const changedDirs = new Set(changedPaths.map(dirOf));
|
|
384
|
+
const changedBaseNames = new Set(changedPaths.map((p) => stripExt(baseName(p)).toLowerCase()));
|
|
385
|
+
const nearby = { tests: new Set(), docs: new Set(), config: new Set(), runtime: new Set() };
|
|
386
|
+
for (const file of artifact?.files ?? []) {
|
|
387
|
+
if (changedSet.has(file.path))
|
|
388
|
+
continue;
|
|
389
|
+
const sameDir = changedDirs.has(dirOf(file.path));
|
|
390
|
+
const sameBase = changedBaseNames.has(stripExt(baseName(file.path)).toLowerCase());
|
|
391
|
+
const role = classifyImpactFileRole(file.path, { generated: file.generated, sensitiveKinds: file.sensitiveKinds });
|
|
392
|
+
// Tests: match by base name (foo.ts → foo.test.ts), not raw directory proximity —
|
|
393
|
+
// a flat utils/ directory otherwise reports every sibling test as "nearby" (noise).
|
|
394
|
+
if (role === 'test' && sameBase)
|
|
395
|
+
nearby.tests.add(file.path);
|
|
396
|
+
else if (role === 'docs' && sameDir)
|
|
397
|
+
nearby.docs.add(file.path);
|
|
398
|
+
else if (role === 'config' && sameDir)
|
|
399
|
+
nearby.config.add(file.path);
|
|
400
|
+
else if (role === 'runtime_governance' && sameDir)
|
|
401
|
+
nearby.runtime.add(file.path);
|
|
402
|
+
}
|
|
403
|
+
const nearbyOut = {
|
|
404
|
+
label: 'advisory',
|
|
405
|
+
tests: [...nearby.tests].sort().slice(0, 12),
|
|
406
|
+
docs: [...nearby.docs].sort().slice(0, 12),
|
|
407
|
+
config: [...nearby.config].sort().slice(0, 12),
|
|
408
|
+
runtime: [...nearby.runtime].sort().slice(0, 12),
|
|
409
|
+
};
|
|
410
|
+
// ── reuse / duplicate-helper advisories (advisory) ──────────────────────────
|
|
411
|
+
const reuseAdvisories = (artifact?.reuseFindings ?? [])
|
|
412
|
+
.filter((r) => r.files.some((f) => changedSet.has(f)))
|
|
413
|
+
.slice(0, 12)
|
|
414
|
+
.map((r) => ({
|
|
415
|
+
symbolName: r.symbolName,
|
|
416
|
+
kind: r.kind,
|
|
417
|
+
severity: r.severity,
|
|
418
|
+
confidence: r.confidence,
|
|
419
|
+
files: r.files.slice(0, 8),
|
|
420
|
+
reasonCodes: r.reasonCodes,
|
|
421
|
+
whyFlagged: reuseWhyFlagged(r),
|
|
422
|
+
checkNext: reuseCheckNext(r.confidence),
|
|
423
|
+
semanticEquivalenceClaimed: false,
|
|
424
|
+
}));
|
|
425
|
+
// ── reviewer routing + questions ────────────────────────────────────────────
|
|
426
|
+
const reviewFirst = Array.from(new Set([
|
|
427
|
+
...sensitiveSurfaces.map((s) => s.path),
|
|
428
|
+
...hotspots.filter((h) => h.isHub).map((h) => h.path),
|
|
429
|
+
...changedFiles.filter((f) => f.role === 'runtime_governance').map((f) => f.path),
|
|
430
|
+
])).sort();
|
|
431
|
+
const likelyTests = Array.from(new Set([
|
|
432
|
+
...nearbyOut.tests,
|
|
433
|
+
...allConsumers.filter((c) => c.role === 'test').map((c) => c.path),
|
|
434
|
+
])).sort();
|
|
435
|
+
const impactRadius = buildImpactRadius({
|
|
436
|
+
changedFiles,
|
|
437
|
+
routeTo,
|
|
438
|
+
sensitiveKinds,
|
|
439
|
+
allConsumers,
|
|
440
|
+
isHighFanOut,
|
|
441
|
+
hotspots,
|
|
442
|
+
likelyTests,
|
|
443
|
+
});
|
|
444
|
+
const reviewQuestions = buildReviewQuestions({
|
|
445
|
+
changedFiles,
|
|
446
|
+
routeTo,
|
|
447
|
+
sensitiveSurfaces,
|
|
448
|
+
hotspots,
|
|
449
|
+
consumers: allConsumers,
|
|
450
|
+
nearbyTests: likelyTests,
|
|
451
|
+
reuseAdvisories,
|
|
452
|
+
}).slice(0, maxReviewQuestions);
|
|
453
|
+
// ── proves / does not prove ─────────────────────────────────────────────────
|
|
454
|
+
const proves = [
|
|
455
|
+
`Classifies ${changedFiles.length} changed path(s) by language, module, and sensitive surface from the indexed brain.`,
|
|
456
|
+
ownerStatus === 'found'
|
|
457
|
+
? `Routes CODEOWNERS ownership for the changed paths (${routeTo.join(', ') || 'no owners'}), last-match-wins.`
|
|
458
|
+
: 'Reports that no CODEOWNERS boundary matched the changed paths (or none is indexed).',
|
|
459
|
+
`Lists the ${allConsumers.length} file(s) that statically import the changed set via resolved relative imports (fan-in).`,
|
|
460
|
+
`Lists the changed set's own resolved relative dependencies (${internalDeps.length} internal, ${externalPackages.size} external package target(s)).`,
|
|
461
|
+
];
|
|
462
|
+
const doesNotProve = [
|
|
463
|
+
'It does not prove the change is correct, safe, or free of regressions — these are structural facts, not a code review.',
|
|
464
|
+
'Import edges are static and relative-only: dynamic imports, reflection, framework wiring, and cross-package (bare-specifier) references are NOT captured, so the consumer list can be incomplete.',
|
|
465
|
+
'Reuse / duplicate-helper findings are advisory name/fingerprint resemblance and can be false positives — they do not prove the logic is actually shareable.',
|
|
466
|
+
'Nearby tests/docs are directory-proximity hints, not proven coverage of the changed code.',
|
|
467
|
+
];
|
|
468
|
+
const limitations = [
|
|
469
|
+
'V1 is deterministic and source-free: no code bodies, diffs, or prompts are read or emitted.',
|
|
470
|
+
'Symbol-level intelligence covers TS/JS/Python; other languages are classified at file/module granularity.',
|
|
471
|
+
'CODEOWNERS matching implements a faithful subset of gitignore globbing (`*`, `**`, `?`, directory and exact patterns).',
|
|
472
|
+
artifact ? `Brain artifact ${artifact.artifactHash.slice(0, 12)} indexed ${artifact.summary.filesIndexed} files.` : 'No brain artifact was available; run `neurcode brain index` for a full map.',
|
|
473
|
+
];
|
|
474
|
+
return {
|
|
475
|
+
schemaVersion: exports.REPO_BRAIN_IMPACT_SCHEMA_VERSION,
|
|
476
|
+
generatedAt,
|
|
477
|
+
brain: {
|
|
478
|
+
status: brainStatus,
|
|
479
|
+
artifactHash: artifact?.artifactHash ?? null,
|
|
480
|
+
generatedAt: artifact?.generatedAt ?? null,
|
|
481
|
+
filesIndexed: artifact?.summary.filesIndexed ?? null,
|
|
482
|
+
ownerBoundaryStatus: artifact?.summary.ownerBoundaryStatus ?? null,
|
|
483
|
+
recoveryCommand,
|
|
484
|
+
},
|
|
485
|
+
requestedPaths: changedPaths,
|
|
486
|
+
changedFiles,
|
|
487
|
+
owners: { label: 'deterministic', status: ownerStatus, matches: ownerMatches, routeTo },
|
|
488
|
+
sensitiveSurfaces: { label: 'deterministic', surfaces: sensitiveSurfaces, kinds: sensitiveKinds },
|
|
489
|
+
consumers: {
|
|
490
|
+
label: 'deterministic',
|
|
491
|
+
direct,
|
|
492
|
+
total: allConsumers.length,
|
|
493
|
+
truncated: allConsumers.length > direct.length,
|
|
494
|
+
byRole,
|
|
495
|
+
},
|
|
496
|
+
dependencies: {
|
|
497
|
+
label: 'deterministic',
|
|
498
|
+
internal: internalDeps.slice(0, maxDependencies),
|
|
499
|
+
externalPackages: [...externalPackages].sort(),
|
|
500
|
+
truncated: internalDeps.length > maxDependencies,
|
|
501
|
+
},
|
|
502
|
+
highFanOut: { label: 'deterministic', hotspots, isHighFanOut },
|
|
503
|
+
impactRadius,
|
|
504
|
+
nearby: nearbyOut,
|
|
505
|
+
reuse: { label: 'advisory', advisories: reuseAdvisories },
|
|
506
|
+
reviewRouting: { owners: routeTo, reviewFirst },
|
|
507
|
+
reviewQuestions,
|
|
508
|
+
labels: { deterministic: DETERMINISTIC_LABELS, advisory: ADVISORY_LABELS },
|
|
509
|
+
proves,
|
|
510
|
+
doesNotProve,
|
|
511
|
+
limitations,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
// ── Reviewer questions (advisory, derived deterministically from facts) ────────
|
|
515
|
+
const SENSITIVE_QUESTION = {
|
|
516
|
+
auth: 'preserve every authentication/authorization check and not widen access',
|
|
517
|
+
billing: 'remain idempotent and not double-charge, drop, or mis-attribute money',
|
|
518
|
+
database: 'stay backward-compatible with existing rows and avoid a breaking schema change',
|
|
519
|
+
migration: 'be reversible and safe to run against production data',
|
|
520
|
+
workflow: 'keep CI/CD gates intact and not weaken required checks',
|
|
521
|
+
secret: 'avoid logging, committing, or widening exposure of any secret',
|
|
522
|
+
dependency: 'pin compatible versions and not pull in a breaking or unvetted dependency',
|
|
523
|
+
configuration: 'be safe across every environment and not flip a production default',
|
|
524
|
+
runtime_governance: 'preserve the runtime governance contract (fail-closed, owner boundaries, approval scope)',
|
|
525
|
+
};
|
|
526
|
+
function buildReviewQuestions(input) {
|
|
527
|
+
const questions = [];
|
|
528
|
+
if (input.routeTo.length > 0) {
|
|
529
|
+
questions.push({
|
|
530
|
+
category: 'owners',
|
|
531
|
+
question: `Have the code owners (${input.routeTo.join(', ')}) reviewed or approved this change?`,
|
|
532
|
+
rationale: 'CODEOWNERS routes review authority for the changed paths to these owners.',
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
for (const surface of input.sensitiveSurfaces) {
|
|
536
|
+
for (const kind of surface.kinds) {
|
|
537
|
+
const expectation = SENSITIVE_QUESTION[kind];
|
|
538
|
+
if (!expectation)
|
|
539
|
+
continue;
|
|
540
|
+
questions.push({
|
|
541
|
+
category: 'sensitive',
|
|
542
|
+
question: `${surface.path} is a ${kind} surface — does the change ${expectation}?`,
|
|
543
|
+
rationale: `Path heuristics flagged a ${kind} surface; these changes carry outsized blast radius.`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const hubs = input.hotspots.filter((h) => h.isHub);
|
|
548
|
+
for (const hub of hubs) {
|
|
549
|
+
questions.push({
|
|
550
|
+
category: 'fanout',
|
|
551
|
+
question: `${hub.path} is imported by ${hub.fanIn} file(s) — does the change preserve its public contract and signatures?`,
|
|
552
|
+
rationale: 'High fan-in means a contract change here ripples to every importer.',
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (hubs.length === 0 && input.consumers.length >= exports.HIGH_FAN_IN_THRESHOLD) {
|
|
556
|
+
questions.push({
|
|
557
|
+
category: 'fanout',
|
|
558
|
+
question: `${input.consumers.length} file(s) import the changed set — were their call sites checked for breakage?`,
|
|
559
|
+
rationale: 'Several static importers depend on the changed files.',
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
for (const reuse of input.reuseAdvisories) {
|
|
563
|
+
if (!reuse.symbolName)
|
|
564
|
+
continue;
|
|
565
|
+
questions.push({
|
|
566
|
+
category: 'reuse',
|
|
567
|
+
question: `A "${reuse.symbolName}" declaration also appears in ${reuse.files.filter((f) => !f.includes(reuse.symbolName ?? '')).slice(0, 3).join(', ') || reuse.files.slice(0, 3).join(', ')} — should this be shared instead of duplicated?`,
|
|
568
|
+
rationale: `Advisory reuse signal (${reuse.confidence} confidence) — verify before assuming it is a true duplicate.`,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// Tests that cover the change = nearby (base-name) tests PLUS test files that
|
|
572
|
+
// statically import the changed set. Considering importers avoids the false
|
|
573
|
+
// "no tests found" when a test already exercises the code.
|
|
574
|
+
const testConsumers = input.consumers.filter((c) => c.role === 'test').map((c) => c.path);
|
|
575
|
+
const knownTests = Array.from(new Set([...input.nearbyTests, ...testConsumers]));
|
|
576
|
+
if (knownTests.length > 0) {
|
|
577
|
+
questions.push({
|
|
578
|
+
category: 'tests',
|
|
579
|
+
question: `Were the tests covering this code (${knownTests.slice(0, 3).join(', ')}${knownTests.length > 3 ? ', …' : ''}) updated and run for this change?`,
|
|
580
|
+
rationale: 'These test files import the changed code or sit beside it, so they most likely exercise the affected behavior.',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
else if (input.changedFiles.some((f) => f.role === 'source')) {
|
|
584
|
+
questions.push({
|
|
585
|
+
category: 'tests',
|
|
586
|
+
question: 'No tests were found that import or sit beside the changed source files — should test coverage be added?',
|
|
587
|
+
rationale: 'The brain found no test files by import edge, directory proximity, or matching base name.',
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (input.changedFiles.some((f) => f.role === 'config')) {
|
|
591
|
+
questions.push({
|
|
592
|
+
category: 'config',
|
|
593
|
+
question: 'This touches configuration or dependencies — does it need a coordinated rollout, version bump, or migration note?',
|
|
594
|
+
rationale: 'Config/dependency changes often need out-of-band coordination beyond a code review.',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (questions.length === 0) {
|
|
598
|
+
questions.push({
|
|
599
|
+
category: 'general',
|
|
600
|
+
question: 'No owners, sensitive surfaces, hubs, or nearby tests were detected — is this change as low-risk as the structural map suggests?',
|
|
601
|
+
rationale: 'The brain found no elevated-risk structural signal; confirm nothing is missing from the index.',
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return questions;
|
|
605
|
+
}
|
|
606
|
+
// ── Compact summary projection (P1/P2/P3) ─────────────────────────────────────
|
|
607
|
+
function summarizeImpact(report) {
|
|
608
|
+
const changedSymbols = report.changedFiles.reduce((sum, f) => sum + (f.symbolCount ?? 0), 0);
|
|
609
|
+
return {
|
|
610
|
+
schemaVersion: exports.IMPACT_SUMMARY_SCHEMA_VERSION,
|
|
611
|
+
generatedAt: report.generatedAt,
|
|
612
|
+
brainStatus: report.brain.status,
|
|
613
|
+
artifactHash: report.brain.artifactHash,
|
|
614
|
+
counts: {
|
|
615
|
+
changedFiles: report.changedFiles.length,
|
|
616
|
+
indexedChangedFiles: report.changedFiles.filter((f) => f.indexed).length,
|
|
617
|
+
directConsumers: report.consumers.total,
|
|
618
|
+
changedSymbols,
|
|
619
|
+
sensitiveSurfaces: report.sensitiveSurfaces.surfaces.length,
|
|
620
|
+
internalDependencies: report.dependencies.internal.length,
|
|
621
|
+
externalPackages: report.dependencies.externalPackages.length,
|
|
622
|
+
owners: report.owners.routeTo.length,
|
|
623
|
+
},
|
|
624
|
+
changedFiles: report.changedFiles.map((f) => ({
|
|
625
|
+
path: f.path,
|
|
626
|
+
role: f.role,
|
|
627
|
+
module: f.module,
|
|
628
|
+
sensitiveKinds: f.sensitiveKinds,
|
|
629
|
+
})),
|
|
630
|
+
owners: report.owners.routeTo,
|
|
631
|
+
sensitiveSurfaces: report.sensitiveSurfaces.surfaces,
|
|
632
|
+
deterministic: {
|
|
633
|
+
directConsumers: report.consumers.direct.slice(0, 8).map((c) => ({ path: c.path, role: c.role, edgeCount: c.edgeCount })),
|
|
634
|
+
highFanOut: report.highFanOut.hotspots.filter((h) => h.isHub).map((h) => ({ path: h.path, fanIn: h.fanIn })),
|
|
635
|
+
isHighFanOut: report.highFanOut.isHighFanOut,
|
|
636
|
+
},
|
|
637
|
+
advisory: {
|
|
638
|
+
reuse: report.reuse.advisories.slice(0, 6).map((r) => ({
|
|
639
|
+
symbolName: r.symbolName,
|
|
640
|
+
confidence: r.confidence,
|
|
641
|
+
files: r.files.slice(0, 4),
|
|
642
|
+
whyFlagged: r.whyFlagged,
|
|
643
|
+
checkNext: r.checkNext,
|
|
644
|
+
semanticEquivalenceClaimed: r.semanticEquivalenceClaimed,
|
|
645
|
+
})),
|
|
646
|
+
nearbyTests: report.nearby.tests.slice(0, 6),
|
|
647
|
+
},
|
|
648
|
+
impactRadius: report.impactRadius,
|
|
649
|
+
reviewRouting: report.reviewRouting,
|
|
650
|
+
reviewQuestions: report.reviewQuestions.map((q) => q.question),
|
|
651
|
+
proves: report.proves,
|
|
652
|
+
doesNotProve: report.doesNotProve,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Read the local repo brain (building it once when missing if autoBuild) and
|
|
657
|
+
* compute the impact report for the given changed paths.
|
|
658
|
+
*/
|
|
659
|
+
function buildRepoBrainImpactForRepo(projectRoot, requestedPaths, options = {}) {
|
|
660
|
+
const autoBuild = options.autoBuild ?? true;
|
|
661
|
+
let artifact = (0, local_repo_brain_1.readLocalRepoBrain)(projectRoot);
|
|
662
|
+
let status = artifact ? 'found' : 'missing';
|
|
663
|
+
if (!artifact && autoBuild) {
|
|
664
|
+
artifact = (0, local_repo_brain_1.buildLocalRepoBrain)(projectRoot);
|
|
665
|
+
(0, local_repo_brain_1.writeLocalRepoBrain)(projectRoot, artifact);
|
|
666
|
+
status = 'built';
|
|
667
|
+
}
|
|
668
|
+
const normalized = requestedPaths.map((p) => normalizeImpactPath(p, projectRoot));
|
|
669
|
+
return computeRepoBrainImpact(artifact, normalized, { ...options, brainStatus: status });
|
|
670
|
+
}
|
|
671
|
+
// ── Human-readable renderer (CLI text output) ─────────────────────────────────
|
|
672
|
+
function renderRepoBrainImpactText(report) {
|
|
673
|
+
const lines = [];
|
|
674
|
+
const det = '[deterministic]';
|
|
675
|
+
const adv = '[advisory]';
|
|
676
|
+
lines.push('Repo Brain — Change Impact');
|
|
677
|
+
lines.push(`Brain: ${report.brain.status}${report.brain.artifactHash ? ` (${report.brain.artifactHash.slice(0, 12)})` : ''} · files indexed: ${report.brain.filesIndexed ?? 'n/a'}`);
|
|
678
|
+
lines.push(`Changed paths: ${report.requestedPaths.length}`);
|
|
679
|
+
lines.push('');
|
|
680
|
+
lines.push('Changed files');
|
|
681
|
+
for (const f of report.changedFiles) {
|
|
682
|
+
const sens = f.sensitiveKinds.length ? ` · sensitive: ${f.sensitiveKinds.join(', ')}` : '';
|
|
683
|
+
lines.push(` - ${f.path} [${f.role}]${f.indexed ? '' : ' (not indexed)'} · ${f.language ?? 'n/a'} · module ${f.module ?? 'n/a'}${sens}`);
|
|
684
|
+
}
|
|
685
|
+
lines.push('');
|
|
686
|
+
lines.push(`Owners ${det} — route to: ${report.owners.routeTo.join(', ') || 'none'}`);
|
|
687
|
+
for (const m of report.owners.matches) {
|
|
688
|
+
lines.push(` - ${m.pattern} → ${m.owners.join(', ')}${m.effective ? ' (effective)' : ''}`);
|
|
689
|
+
}
|
|
690
|
+
if (report.owners.matches.length === 0)
|
|
691
|
+
lines.push(' - no CODEOWNERS boundary matched');
|
|
692
|
+
lines.push('');
|
|
693
|
+
lines.push(`Sensitive surfaces ${det}: ${report.sensitiveSurfaces.kinds.join(', ') || 'none'}`);
|
|
694
|
+
for (const s of report.sensitiveSurfaces.surfaces)
|
|
695
|
+
lines.push(` - ${s.path}: ${s.kinds.join(', ')}`);
|
|
696
|
+
lines.push('');
|
|
697
|
+
lines.push(`Import consumers (fan-in) ${det}: ${report.consumers.total} file(s)${report.consumers.truncated ? ' (truncated)' : ''}`);
|
|
698
|
+
for (const c of report.consumers.direct.slice(0, 12))
|
|
699
|
+
lines.push(` - ${c.path} [${c.role}] · ${c.edgeCount} edge(s)`);
|
|
700
|
+
if (report.consumers.total === 0)
|
|
701
|
+
lines.push(' - none found via resolved relative imports');
|
|
702
|
+
lines.push('');
|
|
703
|
+
lines.push(`Dependencies (fan-out) ${det}: ${report.dependencies.internal.length} internal, ${report.dependencies.externalPackages.length} external`);
|
|
704
|
+
for (const d of report.dependencies.internal.slice(0, 8))
|
|
705
|
+
lines.push(` - ${d.resolvedFile}`);
|
|
706
|
+
if (report.dependencies.externalPackages.length)
|
|
707
|
+
lines.push(` - packages: ${report.dependencies.externalPackages.slice(0, 12).join(', ')}`);
|
|
708
|
+
lines.push('');
|
|
709
|
+
lines.push(`High fan-out / hubs ${det}: ${report.highFanOut.isHighFanOut ? 'yes' : 'no'}`);
|
|
710
|
+
for (const h of report.highFanOut.hotspots)
|
|
711
|
+
lines.push(` - ${h.path}: fan-in ${h.fanIn}, fan-out ${h.fanOut}${h.isHub ? ' (hub)' : ''} · ${h.reasons.join(', ')}`);
|
|
712
|
+
lines.push('');
|
|
713
|
+
lines.push(`Impact radius ${det}/${adv}: ${report.impactRadius.riskLevel}`);
|
|
714
|
+
for (const reason of report.impactRadius.reasons.slice(0, 6))
|
|
715
|
+
lines.push(` - ${reason}`);
|
|
716
|
+
if (report.impactRadius.advisory.likelyTests.length) {
|
|
717
|
+
lines.push(` - likely tests ${adv}: ${report.impactRadius.advisory.likelyTests.join(', ')}`);
|
|
718
|
+
}
|
|
719
|
+
const configImpact = report.impactRadius.deterministic.configImpact;
|
|
720
|
+
const configTouched = [
|
|
721
|
+
...configImpact.configuration,
|
|
722
|
+
...configImpact.workflow,
|
|
723
|
+
...configImpact.dependency,
|
|
724
|
+
...configImpact.runtimeGovernance,
|
|
725
|
+
];
|
|
726
|
+
if (configTouched.length)
|
|
727
|
+
lines.push(` - config/workflow/dependency impact: ${configTouched.slice(0, 8).join(', ')}`);
|
|
728
|
+
lines.push('');
|
|
729
|
+
lines.push(`Nearby files ${adv}`);
|
|
730
|
+
if (report.nearby.tests.length)
|
|
731
|
+
lines.push(` - tests: ${report.nearby.tests.join(', ')}`);
|
|
732
|
+
if (report.nearby.docs.length)
|
|
733
|
+
lines.push(` - docs: ${report.nearby.docs.join(', ')}`);
|
|
734
|
+
if (report.nearby.config.length)
|
|
735
|
+
lines.push(` - config: ${report.nearby.config.join(', ')}`);
|
|
736
|
+
if (report.nearby.runtime.length)
|
|
737
|
+
lines.push(` - runtime: ${report.nearby.runtime.join(', ')}`);
|
|
738
|
+
if (!report.nearby.tests.length && !report.nearby.docs.length && !report.nearby.config.length && !report.nearby.runtime.length) {
|
|
739
|
+
lines.push(' - none found by directory proximity');
|
|
740
|
+
}
|
|
741
|
+
lines.push('');
|
|
742
|
+
lines.push(`Reuse / duplicate-helper advisories ${adv}`);
|
|
743
|
+
for (const r of report.reuse.advisories) {
|
|
744
|
+
lines.push(` - ${r.symbolName ?? r.kind} (${r.confidence}) across ${r.files.join(', ')}`);
|
|
745
|
+
lines.push(` why: ${r.whyFlagged}`);
|
|
746
|
+
lines.push(` check next: ${r.checkNext}`);
|
|
747
|
+
}
|
|
748
|
+
if (report.reuse.advisories.length === 0)
|
|
749
|
+
lines.push(' - none touching the changed set');
|
|
750
|
+
lines.push('');
|
|
751
|
+
lines.push('Recommended reviewer questions');
|
|
752
|
+
report.reviewQuestions.forEach((q, i) => lines.push(` ${i + 1}. ${q.question}`));
|
|
753
|
+
lines.push('');
|
|
754
|
+
lines.push('What this proves');
|
|
755
|
+
for (const p of report.proves)
|
|
756
|
+
lines.push(` - ${p}`);
|
|
757
|
+
lines.push('');
|
|
758
|
+
lines.push('What this does NOT prove');
|
|
759
|
+
for (const p of report.doesNotProve)
|
|
760
|
+
lines.push(` - ${p}`);
|
|
761
|
+
lines.push('');
|
|
762
|
+
return lines.join('\n');
|
|
763
|
+
}
|
|
764
|
+
//# sourceMappingURL=repo-brain-impact.js.map
|