agentsys 5.8.6 → 5.9.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/.claude-plugin/marketplace.json +13 -2
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/lib/binary/version.js +1 -1
- package/lib/collectors/analyzer-queries.js +341 -0
- package/lib/collectors/documentation.js +1 -2
- package/lib/collectors/github.js +3 -5
- package/lib/collectors/index.js +12 -1
- package/lib/cross-platform/index.js +6 -5
- package/lib/enhance/cross-file-analyzer.js +2 -3
- package/lib/enhance/cross-file-patterns.js +2 -3
- package/lib/enhance/docs-patterns.js +4 -6
- package/lib/index.js +0 -2
- package/lib/package.json +0 -34
- package/lib/patterns/cli-enhancers.js +2 -6
- package/lib/patterns/pipeline.js +3 -7
- package/lib/patterns/slop-analyzers.js +1 -3
- package/lib/platform/detect-platform.js +1 -2
- package/lib/repo-intel/queries.js +186 -337
- package/lib/sources/policy-questions.js +3 -4
- package/lib/state/workflow-state.js +106 -173
- package/package.json +1 -1
- package/scripts/generate-docs.js +23 -11
- package/scripts/plugins.txt +1 -0
- package/site/content.json +3 -3
- package/site/index.html +5 -5
- package/site/ux-spec.md +7 -7
- package/lib/repo-intel/index.js +0 -21
|
@@ -1,55 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Repo intel query functions
|
|
3
|
-
*
|
|
4
4
|
* Typed wrappers over `agent-analyzer repo-intel query <type>` subcommands.
|
|
5
|
-
* Consumer plugins can call these instead of constructing CLI args by hand:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Each function constructs the exact argv expected by the binary, delegates to
|
|
7
|
+
* `binary.runAnalyzer`, parses the JSON output, and returns the result.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* All functions throw `RepoIntelMissingError` when the repo-intel map file
|
|
10
|
+
* has not been generated yet (run `agentsys repo-intel update` first).
|
|
12
11
|
*
|
|
13
12
|
* @module lib/repo-intel/queries
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
15
|
const fs = require('fs');
|
|
19
16
|
const path = require('path');
|
|
17
|
+
const { getStateDir } = require('../platform/state-dir');
|
|
20
18
|
const binary = require('../binary');
|
|
21
|
-
const { getStateDirPath } = require('../platform/state-dir');
|
|
22
|
-
|
|
23
|
-
const MAP_FILENAME = 'repo-intel.json';
|
|
24
19
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// E2BIG/ENAMETOOLONG, which would otherwise look like a binary crash to
|
|
29
|
-
// the caller.
|
|
30
|
-
const MAX_FILES_ARG_LEN = 30000;
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Error types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
31
23
|
|
|
32
24
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @param {string} basePath
|
|
36
|
-
* @returns {string}
|
|
37
|
-
*/
|
|
38
|
-
function mapFilePath(basePath) {
|
|
39
|
-
return path.join(getStateDirPath(basePath), MAP_FILENAME);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Error thrown when the cached repo-intel artifact is missing.
|
|
44
|
-
* Distinguished by a `.code = 'REPO_INTEL_MISSING'` field so callers can
|
|
45
|
-
* choose between auto-init, fallback, or surfacing the message.
|
|
25
|
+
* Thrown when the repo-intel map file is absent.
|
|
46
26
|
*/
|
|
47
27
|
class RepoIntelMissingError extends Error {
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} mapFile - Expected path to the map file
|
|
30
|
+
*/
|
|
48
31
|
constructor(mapFile) {
|
|
49
32
|
super(
|
|
50
|
-
`repo-intel
|
|
51
|
-
|
|
52
|
-
'in a CC plugin) first to create it.'
|
|
33
|
+
`repo-intel map not found at ${mapFile}. ` +
|
|
34
|
+
'Run `agentsys repo-intel update` to generate it first.'
|
|
53
35
|
);
|
|
54
36
|
this.name = 'RepoIntelMissingError';
|
|
55
37
|
this.code = 'REPO_INTEL_MISSING';
|
|
@@ -57,393 +39,260 @@ class RepoIntelMissingError extends Error {
|
|
|
57
39
|
}
|
|
58
40
|
}
|
|
59
41
|
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Internal helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
const MAP_FILE_NAME = 'repo-intel.json';
|
|
47
|
+
|
|
60
48
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @
|
|
64
|
-
* @param {string[]} queryArgs - Arguments after `repo-intel query`
|
|
65
|
-
* @returns {Object|Array} Parsed query result
|
|
66
|
-
* @throws {RepoIntelMissingError} If the cached artifact is missing.
|
|
67
|
-
* @throws {Error} If the binary fails or returns non-JSON output (with
|
|
68
|
-
* the failing query and an output preview included in the message).
|
|
49
|
+
* Resolve the map file path for a given project root.
|
|
50
|
+
* @param {string} cwd
|
|
51
|
+
* @returns {string}
|
|
69
52
|
*/
|
|
70
|
-
function
|
|
71
|
-
const
|
|
53
|
+
function resolveMapFile(cwd) {
|
|
54
|
+
const stateDir = getStateDir(cwd);
|
|
55
|
+
return path.join(cwd, stateDir, MAP_FILE_NAME);
|
|
56
|
+
}
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Assert the map file exists, throwing `RepoIntelMissingError` if not.
|
|
60
|
+
* @param {string} cwd
|
|
61
|
+
* @returns {string} resolved map file path
|
|
62
|
+
*/
|
|
63
|
+
function requireMapFile(cwd) {
|
|
64
|
+
const mapFile = resolveMapFile(cwd);
|
|
76
65
|
if (!fs.existsSync(mapFile)) {
|
|
77
66
|
throw new RepoIntelMissingError(mapFile);
|
|
78
67
|
}
|
|
68
|
+
return mapFile;
|
|
69
|
+
}
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Run a repo-intel query and parse JSON output.
|
|
73
|
+
* @param {string} queryName - The subcommand name (e.g. 'hotspots')
|
|
74
|
+
* @param {string[]} extraArgs - Additional argv elements
|
|
75
|
+
* @param {string} cwd - Project root
|
|
76
|
+
* @returns {*} Parsed JSON result
|
|
77
|
+
*/
|
|
78
|
+
function runQuery(queryName, extraArgs, cwd) {
|
|
79
|
+
const mapFile = requireMapFile(cwd);
|
|
80
|
+
const args = ['repo-intel', 'query', queryName, ...extraArgs, '--map-file', mapFile, cwd];
|
|
81
|
+
let raw;
|
|
83
82
|
try {
|
|
84
|
-
|
|
83
|
+
raw = binary.runAnalyzer(args);
|
|
85
84
|
} catch (err) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`repo-intel query failed (${queryArgs.join(' ')}): ${err.message}`
|
|
85
|
+
throw new Error(
|
|
86
|
+
`repo-intel query failed [${queryName}]: ${err.message}`,
|
|
87
|
+
{ cause: err }
|
|
90
88
|
);
|
|
91
|
-
wrapped.cause = err;
|
|
92
|
-
throw wrapped;
|
|
93
89
|
}
|
|
94
|
-
|
|
90
|
+
let parsed;
|
|
95
91
|
try {
|
|
96
|
-
|
|
97
|
-
} catch (
|
|
98
|
-
const preview =
|
|
99
|
-
|
|
100
|
-
`repo-intel query returned non-JSON output
|
|
92
|
+
parsed = JSON.parse(raw);
|
|
93
|
+
} catch (_parseErr) {
|
|
94
|
+
const preview = raw.slice(0, 200);
|
|
95
|
+
throw new Error(
|
|
96
|
+
`repo-intel query [${queryName}] returned non-JSON output: ${preview}`
|
|
101
97
|
);
|
|
102
|
-
wrapped.cause = err;
|
|
103
|
-
throw wrapped;
|
|
104
98
|
}
|
|
99
|
+
return parsed;
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
//
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Query functions
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
108
105
|
|
|
109
106
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* @param {
|
|
113
|
-
* @
|
|
114
|
-
* @param {number} [options.limit] - Maximum number of results
|
|
107
|
+
* Hotspot files by change frequency.
|
|
108
|
+
* @param {string} cwd
|
|
109
|
+
* @param {{ limit?: number }} [opts]
|
|
110
|
+
* @returns {Array}
|
|
115
111
|
*/
|
|
116
|
-
function hotspots(
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
return runQuery(
|
|
112
|
+
function hotspots(cwd, opts = {}) {
|
|
113
|
+
const extra = [];
|
|
114
|
+
if (opts.limit != null) extra.push('--top', String(opts.limit));
|
|
115
|
+
return runQuery('hotspots', extra, cwd);
|
|
120
116
|
}
|
|
121
117
|
|
|
122
118
|
/**
|
|
123
|
-
*
|
|
119
|
+
* Co-change coupling for a file.
|
|
120
|
+
* @param {string} cwd
|
|
121
|
+
* @param {string} file
|
|
122
|
+
* @param {{ limit?: number }} [opts]
|
|
123
|
+
* @returns {Array}
|
|
124
124
|
*/
|
|
125
|
-
function
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
return runQuery(
|
|
125
|
+
function coupling(cwd, file, opts = {}) {
|
|
126
|
+
const extra = [file];
|
|
127
|
+
if (opts.limit != null) extra.push('--top', String(opts.limit));
|
|
128
|
+
return runQuery('coupling', extra, cwd);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
*
|
|
132
|
+
* Bus factor report.
|
|
133
|
+
* @param {string} cwd
|
|
134
|
+
* @param {{ adjustForAi?: boolean, limit?: number }} [opts]
|
|
135
|
+
* @returns {Object}
|
|
133
136
|
*/
|
|
134
|
-
function
|
|
135
|
-
|
|
137
|
+
function busFactor(cwd, opts = {}) {
|
|
138
|
+
const extra = [];
|
|
139
|
+
if (opts.adjustForAi) extra.push('--adjust-for-ai');
|
|
140
|
+
if (opts.limit != null) extra.push('--top', String(opts.limit));
|
|
141
|
+
return runQuery('bus-factor', extra, cwd);
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
// ─── Quality ────────────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
144
|
/**
|
|
141
|
-
*
|
|
145
|
+
* Files with low test coverage relative to change frequency.
|
|
146
|
+
* @param {string} cwd
|
|
147
|
+
* @param {{ limit?: number, minChanges?: number }} [opts]
|
|
148
|
+
* @returns {Array}
|
|
142
149
|
*/
|
|
143
|
-
function
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
150
|
+
function testGaps(cwd, opts = {}) {
|
|
151
|
+
const extra = [];
|
|
152
|
+
if (opts.limit != null) extra.push('--top', String(opts.limit));
|
|
153
|
+
if (opts.minChanges != null) extra.push('--min-changes', String(opts.minChanges));
|
|
154
|
+
return runQuery('test-gaps', extra, cwd);
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
/**
|
|
150
|
-
*
|
|
158
|
+
* AI-authored ratio per file or project.
|
|
159
|
+
* @param {string} cwd
|
|
160
|
+
* @param {{ pathFilter?: string }} [opts]
|
|
161
|
+
* @returns {Object}
|
|
151
162
|
*/
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
return runQuery(basePath, args);
|
|
163
|
+
function aiRatio(cwd, opts = {}) {
|
|
164
|
+
const extra = [];
|
|
165
|
+
if (opts.pathFilter != null) extra.push('--path-filter', opts.pathFilter);
|
|
166
|
+
return runQuery('ai-ratio', extra, cwd);
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* Callers with very large diffs should batch.
|
|
165
|
-
*
|
|
166
|
-
* @param {string} basePath
|
|
167
|
-
* @param {string[]} files - List of changed file paths
|
|
168
|
-
* @throws {TypeError} If `files` is not an array of strings.
|
|
169
|
-
* @throws {RangeError} If the joined argument exceeds {@link MAX_FILES_ARG_LEN}.
|
|
170
|
+
* Risk score for a diff (set of changed files).
|
|
171
|
+
* @param {string} cwd
|
|
172
|
+
* @param {string[]} files
|
|
173
|
+
* @returns {Array}
|
|
170
174
|
*/
|
|
171
|
-
function diffRisk(
|
|
172
|
-
if (!Array.isArray(files)
|
|
173
|
-
throw new TypeError('diffRisk:
|
|
175
|
+
function diffRisk(cwd, files) {
|
|
176
|
+
if (!Array.isArray(files)) {
|
|
177
|
+
throw new TypeError('diffRisk: files must be an array of strings');
|
|
178
|
+
}
|
|
179
|
+
if (!files.every(f => typeof f === 'string')) {
|
|
180
|
+
throw new TypeError('diffRisk: all entries in files must be strings');
|
|
174
181
|
}
|
|
175
182
|
const joined = files.join(',');
|
|
176
|
-
if (joined.length >
|
|
183
|
+
if (joined.length > 30000) {
|
|
177
184
|
throw new RangeError(
|
|
178
|
-
`diffRisk:
|
|
179
|
-
`exceeds platform-safe limit of ${MAX_FILES_ARG_LEN}. ` +
|
|
180
|
-
`Split the request into batches (~500 paths each is typically safe).`
|
|
185
|
+
`diffRisk: files argument exceeds 30000 character limit (got ${joined.length})`
|
|
181
186
|
);
|
|
182
187
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Files ranked by hotspot × (1 + bug_rate) × (1 + complexity/30). Requires
|
|
188
|
-
* Phase 2 AST data; falls back to git-only when unavailable.
|
|
189
|
-
*/
|
|
190
|
-
function painspots(basePath, options = {}) {
|
|
191
|
-
const args = ['painspots'];
|
|
192
|
-
if (options.limit) args.push('--top', String(options.limit));
|
|
193
|
-
return runQuery(basePath, args);
|
|
188
|
+
const extra = ['--files', joined];
|
|
189
|
+
return runQuery('diff-risk', extra, cwd);
|
|
194
190
|
}
|
|
195
191
|
|
|
196
|
-
// ─── People ─────────────────────────────────────────────────────────────────
|
|
197
|
-
|
|
198
192
|
/**
|
|
199
|
-
*
|
|
193
|
+
* Call-graph dependents of a symbol.
|
|
194
|
+
* @param {string} cwd
|
|
195
|
+
* @param {string} symbol
|
|
196
|
+
* @param {string} [file] - Scope to a specific file
|
|
197
|
+
* @returns {Object}
|
|
200
198
|
*/
|
|
201
|
-
function
|
|
202
|
-
|
|
199
|
+
function dependents(cwd, symbol, file) {
|
|
200
|
+
const extra = [symbol];
|
|
201
|
+
if (file != null) extra.push('--file', file);
|
|
202
|
+
return runQuery('dependents', extra, cwd);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
206
|
+
* Bugspot risk scores.
|
|
207
|
+
* @param {string} cwd
|
|
208
|
+
* @param {{ limit?: number }} [opts]
|
|
209
|
+
* @returns {Array}
|
|
207
210
|
*/
|
|
208
|
-
function
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
return runQuery(
|
|
211
|
+
function bugspots(cwd, opts = {}) {
|
|
212
|
+
const extra = [];
|
|
213
|
+
if (opts.limit != null) extra.push('--top', String(opts.limit));
|
|
214
|
+
return runQuery('bugspots', extra, cwd);
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
/**
|
|
215
|
-
*
|
|
218
|
+
* Project health summary.
|
|
219
|
+
* @param {string} cwd
|
|
220
|
+
* @returns {Object}
|
|
216
221
|
*/
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
if (options.adjustForAi) args.push('--adjust-for-ai');
|
|
220
|
-
return runQuery(basePath, args);
|
|
222
|
+
function health(cwd) {
|
|
223
|
+
return runQuery('health', [], cwd);
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
//
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Graph queries (Phase 5.1 - agent-analyzer v0.4.0+)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
224
229
|
|
|
225
230
|
/**
|
|
226
|
-
*
|
|
231
|
+
* Louvain-discovered file communities.
|
|
232
|
+
* @param {string} cwd
|
|
233
|
+
* @returns {Array}
|
|
227
234
|
*/
|
|
228
|
-
function
|
|
229
|
-
return runQuery(
|
|
235
|
+
function communities(cwd) {
|
|
236
|
+
return runQuery('communities', [], cwd);
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
// ─── Standards ──────────────────────────────────────────────────────────────
|
|
233
|
-
|
|
234
239
|
/**
|
|
235
|
-
*
|
|
240
|
+
* Files bridging multiple communities (architectural seams).
|
|
241
|
+
* @param {string} cwd
|
|
242
|
+
* @param {{ limit?: number }} [opts]
|
|
243
|
+
* @returns {Array}
|
|
236
244
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
245
|
+
function boundaries(cwd, opts = {}) {
|
|
246
|
+
const extra = [];
|
|
247
|
+
if (opts.limit != null) extra.push('--top', String(opts.limit));
|
|
248
|
+
return runQuery('boundaries', extra, cwd);
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
/**
|
|
242
|
-
*
|
|
252
|
+
* Which community a file belongs to.
|
|
253
|
+
* @param {string} cwd
|
|
254
|
+
* @param {string} file
|
|
255
|
+
* @returns {Object}
|
|
243
256
|
*/
|
|
244
|
-
function
|
|
245
|
-
return runQuery(
|
|
257
|
+
function areaOf(cwd, file) {
|
|
258
|
+
return runQuery('area-of', [file], cwd);
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
// ─── Health ─────────────────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
261
|
/**
|
|
251
|
-
*
|
|
262
|
+
* Composite health roll-up for a community.
|
|
263
|
+
* @param {string} cwd
|
|
264
|
+
* @param {number} id - Community id (non-negative integer)
|
|
265
|
+
* @returns {Object}
|
|
252
266
|
*/
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Repository-wide health summary.
|
|
259
|
-
*/
|
|
260
|
-
function health(basePath) {
|
|
261
|
-
return runQuery(basePath, ['health']);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Release cadence and tag history.
|
|
266
|
-
*/
|
|
267
|
-
function releaseInfo(basePath) {
|
|
268
|
-
return runQuery(basePath, ['release-info']);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ─── AI detection ───────────────────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* AI vs human contribution ratio.
|
|
275
|
-
*/
|
|
276
|
-
function aiRatio(basePath, options = {}) {
|
|
277
|
-
const args = ['ai-ratio'];
|
|
278
|
-
if (options.pathFilter) args.push('--path-filter', options.pathFilter);
|
|
279
|
-
return runQuery(basePath, args);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Files with recent AI-authored changes.
|
|
284
|
-
*/
|
|
285
|
-
function recentAi(basePath, options = {}) {
|
|
286
|
-
const args = ['recent-ai'];
|
|
287
|
-
if (options.limit) args.push('--top', String(options.limit));
|
|
288
|
-
return runQuery(basePath, args);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── Contributor guidance ───────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Newcomer-oriented repo summary (tech stack, key areas, pain points).
|
|
295
|
-
*/
|
|
296
|
-
function onboard(basePath) {
|
|
297
|
-
return runQuery(basePath, ['onboard']);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Contribution guidance: good-first areas, test gaps, doc drift, bugspots.
|
|
302
|
-
*/
|
|
303
|
-
function canIHelp(basePath) {
|
|
304
|
-
return runQuery(basePath, ['can-i-help']);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ─── Documentation ──────────────────────────────────────────────────────────
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Doc files with low code coupling (likely stale).
|
|
311
|
-
*/
|
|
312
|
-
function docDrift(basePath, options = {}) {
|
|
313
|
-
const args = ['doc-drift'];
|
|
314
|
-
if (options.limit) args.push('--top', String(options.limit));
|
|
315
|
-
return runQuery(basePath, args);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Doc files with stale references to source symbols. Requires Phase 4
|
|
320
|
-
* sync-check data.
|
|
321
|
-
*/
|
|
322
|
-
function staleDocs(basePath, options = {}) {
|
|
323
|
-
const args = ['stale-docs'];
|
|
324
|
-
if (options.limit) args.push('--top', String(options.limit));
|
|
325
|
-
return runQuery(basePath, args);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ─── AST symbols ────────────────────────────────────────────────────────────
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* AST symbols (exports, imports, definitions) for a specific file. Requires
|
|
332
|
-
* Phase 2 AST data.
|
|
333
|
-
*/
|
|
334
|
-
function symbols(basePath, file) {
|
|
335
|
-
return runQuery(basePath, ['symbols', file]);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Files that import a given symbol (reverse dependency lookup). Requires
|
|
340
|
-
* Phase 2 AST data.
|
|
341
|
-
*/
|
|
342
|
-
function dependents(basePath, symbol, file) {
|
|
343
|
-
const args = ['dependents', symbol];
|
|
344
|
-
if (file) args.push('--file', file);
|
|
345
|
-
return runQuery(basePath, args);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ─── Phase 5: Graph-derived (analyzer-graph crate) ──────────────────────────
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Communities discovered by Louvain modularity over the co-change graph.
|
|
352
|
-
* Returns clusters of files that consistently change together - the natural
|
|
353
|
-
* feature areas, independent of directory layout. Requires agent-analyzer
|
|
354
|
-
* v0.4.0+.
|
|
355
|
-
*
|
|
356
|
-
* @param {string} basePath
|
|
357
|
-
* @returns {Array<{id: number, size: number, files: string[]}>}
|
|
358
|
-
*/
|
|
359
|
-
function communities(basePath) {
|
|
360
|
-
return runQuery(basePath, ['communities']);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Files bridging multiple communities (high betweenness centrality). These
|
|
365
|
-
* are the architectural seams - the highest-leverage files for refactoring
|
|
366
|
-
* decisions. Requires agent-analyzer v0.4.0+.
|
|
367
|
-
*
|
|
368
|
-
* @param {string} basePath
|
|
369
|
-
* @param {Object} [options={}]
|
|
370
|
-
* @param {number} [options.limit] - Maximum number of results
|
|
371
|
-
* @returns {Array<{path: string, betweenness: number, community: number|null}>}
|
|
372
|
-
*/
|
|
373
|
-
function boundaries(basePath, options = {}) {
|
|
374
|
-
const args = ['boundaries'];
|
|
375
|
-
if (options.limit) args.push('--top', String(options.limit));
|
|
376
|
-
return runQuery(basePath, args);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Look up which community a given file belongs to. Requires agent-analyzer
|
|
381
|
-
* v0.4.0+.
|
|
382
|
-
*
|
|
383
|
-
* @param {string} basePath
|
|
384
|
-
* @param {string} file - File path (relative to repo root)
|
|
385
|
-
* @returns {{file: string, community: number|null, size: number|null}}
|
|
386
|
-
*/
|
|
387
|
-
function areaOf(basePath, file) {
|
|
388
|
-
return runQuery(basePath, ['area-of', file]);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Composite per-community health: total/recent changes, bug-fix rate,
|
|
393
|
-
* AI ratio, stale-owner count. Use to identify communities under stress
|
|
394
|
-
* (high bug rate or stale ownership). Requires agent-analyzer v0.4.0+.
|
|
395
|
-
*
|
|
396
|
-
* @param {string} basePath
|
|
397
|
-
* @param {number} id - Community id (from `communities()`)
|
|
398
|
-
* @returns {Object|null}
|
|
399
|
-
*/
|
|
400
|
-
function communityHealth(basePath, id) {
|
|
401
|
-
if (!Number.isInteger(id) || id < 0) {
|
|
267
|
+
function communityHealth(cwd, id) {
|
|
268
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0) {
|
|
402
269
|
throw new TypeError(
|
|
403
|
-
|
|
270
|
+
'communityHealth: id must be a non-negative integer'
|
|
404
271
|
);
|
|
405
272
|
}
|
|
406
|
-
return runQuery(
|
|
273
|
+
return runQuery('community-health', [String(id)], cwd);
|
|
407
274
|
}
|
|
408
275
|
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Exports
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
409
280
|
module.exports = {
|
|
410
|
-
//
|
|
281
|
+
// Error class
|
|
411
282
|
RepoIntelMissingError,
|
|
412
|
-
|
|
283
|
+
|
|
284
|
+
// Core queries
|
|
413
285
|
hotspots,
|
|
414
|
-
coldspots,
|
|
415
|
-
fileHistory,
|
|
416
|
-
// Quality
|
|
417
|
-
bugspots,
|
|
418
|
-
testGaps,
|
|
419
|
-
diffRisk,
|
|
420
|
-
painspots,
|
|
421
|
-
// People
|
|
422
|
-
ownership,
|
|
423
|
-
contributors,
|
|
424
|
-
busFactor,
|
|
425
|
-
// Coupling
|
|
426
286
|
coupling,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
conventions,
|
|
430
|
-
// Health
|
|
431
|
-
areas,
|
|
432
|
-
health,
|
|
433
|
-
releaseInfo,
|
|
434
|
-
// AI detection
|
|
287
|
+
busFactor,
|
|
288
|
+
testGaps,
|
|
435
289
|
aiRatio,
|
|
436
|
-
|
|
437
|
-
// Contributor guidance
|
|
438
|
-
onboard,
|
|
439
|
-
canIHelp,
|
|
440
|
-
// Documentation
|
|
441
|
-
docDrift,
|
|
442
|
-
staleDocs,
|
|
443
|
-
// AST symbols
|
|
444
|
-
symbols,
|
|
290
|
+
diffRisk,
|
|
445
291
|
dependents,
|
|
446
|
-
|
|
292
|
+
bugspots,
|
|
293
|
+
health,
|
|
294
|
+
|
|
295
|
+
// Graph queries
|
|
447
296
|
communities,
|
|
448
297
|
boundaries,
|
|
449
298
|
areaOf,
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const sourceCache = require('./source-cache');
|
|
9
|
-
const { truncate } = require('../cross-platform');
|
|
10
9
|
const customHandler = require('./custom-handler');
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -43,7 +42,7 @@ function getPolicyQuestions() {
|
|
|
43
42
|
// Truncate to fit within 30 chars: "X (last used)" where X can be max 17 chars
|
|
44
43
|
const maxBaseLen = 30 - ' (last used)'.length; // 18 chars for base
|
|
45
44
|
const truncatedLabel = cachedLabel.length > maxBaseLen
|
|
46
|
-
?
|
|
45
|
+
? cachedLabel.substring(0, maxBaseLen - 1) + '…'
|
|
47
46
|
: cachedLabel;
|
|
48
47
|
|
|
49
48
|
sourceOptions.push({
|
|
@@ -156,12 +155,12 @@ function parseAndCachePolicy(responses) {
|
|
|
156
155
|
// Validate and merge follow-up responses
|
|
157
156
|
const rawNum = String(responses.project.number).trim();
|
|
158
157
|
if (!/^[1-9][0-9]*$/.test(rawNum)) {
|
|
159
|
-
const safeNum =
|
|
158
|
+
const safeNum = String(responses.project.number).replace(/[^\x20-\x7E]/g, '?').substring(0, 32);
|
|
160
159
|
throw new Error(`Invalid project number: "${safeNum}". Must be a positive integer.`);
|
|
161
160
|
}
|
|
162
161
|
const num = Number(rawNum);
|
|
163
162
|
const owner = String(responses.project.owner || '').trim();
|
|
164
|
-
const safeOwner =
|
|
163
|
+
const safeOwner = String(responses.project.owner || '').replace(/[^\x20-\x7E]/g, '?').substring(0, 64);
|
|
165
164
|
if (!owner || !/^(@me|[a-zA-Z0-9][a-zA-Z0-9_-]*)$/.test(owner)) {
|
|
166
165
|
throw new Error(`Invalid project owner: "${safeOwner}" (use @me or an org/user name)`);
|
|
167
166
|
}
|