@appchy/jarvis 0.1.36 → 0.1.39
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/bin/config-change.mjs +1 -1
- package/bin/jarvis.mjs +8 -12
- package/bin/pre-tool-use.mjs +2 -2
- package/bin/session-start.mjs +1 -1
- package/bin/stop.mjs +1 -1
- package/bin/user-prompt-submit.mjs +1 -1
- package/dist/bin.js +10104 -37174
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +469 -0
- package/dist/data/chunk-7REP35VA.mjs +462 -0
- package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
- package/dist/data/chunk-AYOJSS2F.mjs +72 -0
- package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
- package/dist/data/chunk-YWSWQEJN.mjs +138 -0
- package/dist/data/embedders.mjs +80 -0
- package/dist/data/finders.mjs +35 -0
- package/dist/data/index.mjs +137 -0
- package/dist/data/linkers.mjs +83 -0
- package/dist/data/mcp.mjs +2624 -0
- package/dist/data/persistences.mjs +8 -0
- package/dist/data/rerankers.mjs +52 -0
- package/dist/data/stores.mjs +38 -0
- package/dist/hooks/config-change.js +7 -29
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +129 -32
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +72 -37
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +140 -32
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js +7 -29
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/assets/architecture-template.md +53 -0
- package/harness/assets/design-template.md +89 -0
- package/harness/assets/domains/architecture.md +43 -0
- package/harness/assets/domains/commercial.md +42 -0
- package/harness/assets/domains/design.md +42 -0
- package/harness/assets/domains/legal.md +43 -0
- package/harness/assets/domains/operations.md +39 -0
- package/harness/assets/domains/product.md +43 -0
- package/harness/assets/domains/quality.md +43 -0
- package/harness/assets/domains/security.md +44 -0
- package/harness/assets/domains/support.md +45 -0
- package/harness/assets/epic-template.md +85 -0
- package/harness/assets/feature-template.md +86 -0
- package/harness/assets/handoff-template.md +26 -0
- package/harness/assets/task-template.md +38 -0
- package/harness/assets/version-template.md +43 -0
- package/harness/harness/__init__.py +7 -0
- package/harness/harness/align.py +582 -0
- package/harness/harness/architecture.py +254 -0
- package/harness/harness/autonomy.py +374 -0
- package/harness/harness/branches.py +408 -0
- package/harness/harness/config.py +1482 -0
- package/harness/harness/coverage.py +199 -0
- package/harness/harness/epic.py +220 -0
- package/harness/harness/events.py +153 -0
- package/harness/harness/extend.py +99 -0
- package/harness/harness/frontmatter.py +218 -0
- package/harness/harness/gate.py +591 -0
- package/harness/harness/generate.py +267 -0
- package/harness/harness/git.py +775 -0
- package/harness/harness/ids.py +140 -0
- package/harness/harness/kickoff.py +231 -0
- package/harness/harness/lint.py +505 -0
- package/harness/harness/model.py +364 -0
- package/harness/harness/peers.py +187 -0
- package/harness/harness/product.py +29 -0
- package/harness/harness/registry.py +382 -0
- package/harness/harness/report.py +227 -0
- package/harness/harness/safety.py +387 -0
- package/harness/harness/scaffold.py +129 -0
- package/harness/harness/shard.py +63 -0
- package/harness/harness/shift.py +348 -0
- package/harness/harness/task.py +507 -0
- package/harness/harness/tree.py +258 -0
- package/harness/harness/version.py +305 -0
- package/harness/harness/wrap.py +217 -0
- package/harness/hooks/guard.py +259 -0
- package/harness/presets/appchy/PRESET.md +717 -0
- package/harness/presets/appchy/references/artifacts.md +539 -0
- package/harness/presets/appchy/references/graph.md +154 -0
- package/harness/presets/appchy/references/operations.md +444 -0
- package/harness/presets/appchy/references/research.md +216 -0
- package/harness/schema/work.config.schema.json +401 -0
- package/harness/test_work.py +5002 -0
- package/harness/work.py +534 -0
- package/package.json +34 -40
- package/bin/config-change.dev.mjs +0 -17
- package/bin/jarvis-dev.mjs +0 -30
- package/bin/pre-tool-use.dev.mjs +0 -23
- package/bin/session-start.dev.mjs +0 -17
- package/bin/stop.dev.mjs +0 -17
- package/bin/user-prompt-submit.dev.mjs +0 -17
- package/dev/bin.js +0 -38718
- package/dev/bin.js.map +0 -1
- package/dev/env.json +0 -1
- package/dev/hooks/config-change.js +0 -110
- package/dev/hooks/config-change.js.map +0 -1
- package/dev/hooks/pre-tool-use.js +0 -120
- package/dev/hooks/pre-tool-use.js.map +0 -1
- package/dev/hooks/session-start.js +0 -115
- package/dev/hooks/session-start.js.map +0 -1
- package/dev/hooks/stop.js +0 -112
- package/dev/hooks/stop.js.map +0 -1
- package/dev/hooks/user-prompt-submit.js +0 -111
- package/dev/hooks/user-prompt-submit.js.map +0 -1
- package/dist/env.json +0 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMPORT_RELATIONS
|
|
3
|
+
} from "./chunk-AKQQC5IT.mjs";
|
|
4
|
+
import "./chunk-7REP35VA.mjs";
|
|
5
|
+
import "./chunk-RRJ6KKYL.mjs";
|
|
6
|
+
import {
|
|
7
|
+
backend
|
|
8
|
+
} from "./chunk-AYOJSS2F.mjs";
|
|
9
|
+
import {
|
|
10
|
+
matcher
|
|
11
|
+
} from "./chunk-YWSWQEJN.mjs";
|
|
12
|
+
|
|
13
|
+
// ../../packages/data/src/backends/graphify.ts
|
|
14
|
+
import { execFile } from "child_process";
|
|
15
|
+
import { access, readFile as readFile2, rename, rm, stat, utimes, writeFile } from "fs/promises";
|
|
16
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
17
|
+
import { promisify } from "util";
|
|
18
|
+
|
|
19
|
+
// ../../packages/data/src/backends/imports.ts
|
|
20
|
+
import { readFile } from "fs/promises";
|
|
21
|
+
import { posix, resolve } from "path";
|
|
22
|
+
var IMPORT_RELATION_SET = new Set(IMPORT_RELATIONS);
|
|
23
|
+
var asStr = (v) => typeof v === "string" ? v : void 0;
|
|
24
|
+
var toPosix = (p) => p.replace(/\\/g, "/");
|
|
25
|
+
var noExt = (p) => p.replace(/\.[^./]+$/, "");
|
|
26
|
+
var baseNoExt = (p) => noExt(p.split("/").pop() ?? p);
|
|
27
|
+
function lineNumberOf(loc) {
|
|
28
|
+
if (!loc) return void 0;
|
|
29
|
+
const m = loc.match(/\d+/);
|
|
30
|
+
return m ? Number(m[0]) : void 0;
|
|
31
|
+
}
|
|
32
|
+
function specifiersOf(line) {
|
|
33
|
+
const re = /(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
|
|
34
|
+
const out = [];
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = re.exec(line)) !== null) if (m[1]) out.push(m[1]);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function resolvedNoExt(importer, spec) {
|
|
40
|
+
return noExt(posix.normalize(posix.join(posix.dirname(importer), spec)));
|
|
41
|
+
}
|
|
42
|
+
var NO_PACKAGE = /* @__PURE__ */ Symbol("no-package");
|
|
43
|
+
async function packageNameOf(cache, root, file) {
|
|
44
|
+
const pending = [];
|
|
45
|
+
let dir = posix.dirname(toPosix(file));
|
|
46
|
+
for (; ; ) {
|
|
47
|
+
const hit = cache.get(dir);
|
|
48
|
+
if (hit !== void 0) {
|
|
49
|
+
for (const d of pending) cache.set(d, hit);
|
|
50
|
+
return hit === NO_PACKAGE ? void 0 : hit;
|
|
51
|
+
}
|
|
52
|
+
pending.push(dir);
|
|
53
|
+
const name = await readFile(resolve(root, dir, "package.json"), "utf8").then((text) => asStr(JSON.parse(text).name)).catch(() => void 0);
|
|
54
|
+
if (name !== void 0) {
|
|
55
|
+
for (const d of pending) cache.set(d, name);
|
|
56
|
+
return name;
|
|
57
|
+
}
|
|
58
|
+
const next = posix.dirname(dir);
|
|
59
|
+
if (next === dir || dir === "." || dir === "") break;
|
|
60
|
+
dir = next;
|
|
61
|
+
}
|
|
62
|
+
for (const d of pending) cache.set(d, NO_PACKAGE);
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
async function readLines(cache, absPath) {
|
|
66
|
+
const hit = cache.get(absPath);
|
|
67
|
+
if (hit !== void 0) return hit;
|
|
68
|
+
const lines = await readFile(absPath, "utf8").then((text) => text.split(/\r?\n/)).catch(() => null);
|
|
69
|
+
cache.set(absPath, lines);
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
async function resolveImportCollisions(raw, root) {
|
|
73
|
+
const fileById = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const n of raw.nodes) {
|
|
75
|
+
const id = asStr(n.id);
|
|
76
|
+
const f = asStr(n.source_file);
|
|
77
|
+
if (id && f) fileById.set(id, toPosix(f));
|
|
78
|
+
}
|
|
79
|
+
const drop = /* @__PURE__ */ new Set();
|
|
80
|
+
const lineCache = /* @__PURE__ */ new Map();
|
|
81
|
+
const pkgCache = /* @__PURE__ */ new Map();
|
|
82
|
+
const fileSpecCache = /* @__PURE__ */ new Map();
|
|
83
|
+
const specsInFile = async (file) => {
|
|
84
|
+
const hit = fileSpecCache.get(file);
|
|
85
|
+
if (hit !== void 0 || fileSpecCache.has(file)) return hit;
|
|
86
|
+
const lines = await readLines(lineCache, resolve(root, file));
|
|
87
|
+
const specs = lines?.flatMap((l) => specifiersOf(l));
|
|
88
|
+
fileSpecCache.set(file, specs);
|
|
89
|
+
return specs;
|
|
90
|
+
};
|
|
91
|
+
for (const link of raw.links) {
|
|
92
|
+
const rel = asStr(link.relation);
|
|
93
|
+
if (!rel || !IMPORT_RELATION_SET.has(rel)) continue;
|
|
94
|
+
const site = asStr(link.source_file);
|
|
95
|
+
const node = fileById.get(asStr(link.source) ?? "");
|
|
96
|
+
if (site === void 0 || node === void 0) continue;
|
|
97
|
+
if (toPosix(site) !== node) drop.add(link);
|
|
98
|
+
}
|
|
99
|
+
for (const link of raw.links) {
|
|
100
|
+
const rel = asStr(link.relation);
|
|
101
|
+
if (!rel || !IMPORT_RELATION_SET.has(rel)) continue;
|
|
102
|
+
const importer = toPosix(asStr(link.source_file) ?? "");
|
|
103
|
+
const target = fileById.get(asStr(link.target) ?? "");
|
|
104
|
+
if (!importer || !target) continue;
|
|
105
|
+
const fromPkg = await packageNameOf(pkgCache, root, importer);
|
|
106
|
+
const toPkg = await packageNameOf(pkgCache, root, target);
|
|
107
|
+
if (fromPkg === void 0 || toPkg === void 0 || fromPkg === toPkg) continue;
|
|
108
|
+
const specs = await specsInFile(importer);
|
|
109
|
+
if (specs === void 0) continue;
|
|
110
|
+
if (specs.some((s) => s === toPkg || s.startsWith(`${toPkg}/`))) continue;
|
|
111
|
+
drop.add(link);
|
|
112
|
+
}
|
|
113
|
+
const groups = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const l of raw.links) {
|
|
115
|
+
const rel = asStr(l.relation);
|
|
116
|
+
const src = asStr(l.source);
|
|
117
|
+
const loc = asStr(l.source_location);
|
|
118
|
+
if (!rel || !src || !loc || !IMPORT_RELATION_SET.has(rel)) continue;
|
|
119
|
+
const key = `${src}\0${loc}\0${rel}`;
|
|
120
|
+
const g = groups.get(key);
|
|
121
|
+
if (g) g.push(l);
|
|
122
|
+
else groups.set(key, [l]);
|
|
123
|
+
}
|
|
124
|
+
for (const links of groups.values()) {
|
|
125
|
+
const importer = toPosix(asStr(links[0]?.source_file) ?? "");
|
|
126
|
+
const lineNo = lineNumberOf(asStr(links[0]?.source_location));
|
|
127
|
+
if (!importer || lineNo === void 0) continue;
|
|
128
|
+
const lines = await readLines(lineCache, resolve(root, importer));
|
|
129
|
+
const line = lines?.[lineNo - 1];
|
|
130
|
+
if (!line) continue;
|
|
131
|
+
const allSpecs = specifiersOf(line);
|
|
132
|
+
if (!allSpecs.length) continue;
|
|
133
|
+
const bare = allSpecs.filter((s) => !s.startsWith(".") && !s.startsWith("/"));
|
|
134
|
+
if (bare.length === allSpecs.length) {
|
|
135
|
+
for (const link of links) {
|
|
136
|
+
const file = fileById.get(asStr(link.target) ?? "");
|
|
137
|
+
if (!file) continue;
|
|
138
|
+
const pkg = await packageNameOf(pkgCache, root, file);
|
|
139
|
+
if (pkg === void 0) continue;
|
|
140
|
+
if (bare.some((s) => s === pkg || s.startsWith(`${pkg}/`))) continue;
|
|
141
|
+
drop.add(link);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (links.length < 2) continue;
|
|
146
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const link of links) {
|
|
148
|
+
const file = fileById.get(asStr(link.target) ?? "");
|
|
149
|
+
if (!file) continue;
|
|
150
|
+
const base = baseNoExt(file);
|
|
151
|
+
let files = byBase.get(base);
|
|
152
|
+
if (!files) {
|
|
153
|
+
files = /* @__PURE__ */ new Map();
|
|
154
|
+
byBase.set(base, files);
|
|
155
|
+
}
|
|
156
|
+
const list = files.get(file);
|
|
157
|
+
if (list) list.push(link);
|
|
158
|
+
else files.set(file, [link]);
|
|
159
|
+
}
|
|
160
|
+
const colliding = [...byBase.values()].filter((files) => files.size >= 2);
|
|
161
|
+
if (!colliding.length) continue;
|
|
162
|
+
const specs = allSpecs.filter((s) => s.startsWith("."));
|
|
163
|
+
if (!specs.length) continue;
|
|
164
|
+
for (const files of colliding) {
|
|
165
|
+
const base = baseNoExt([...files.keys()][0] ?? "");
|
|
166
|
+
const spec = specs.find((s) => baseNoExt(s) === base);
|
|
167
|
+
if (!spec) continue;
|
|
168
|
+
const want = resolvedNoExt(importer, spec);
|
|
169
|
+
const winners = [...files.keys()].filter((f) => noExt(f) === want);
|
|
170
|
+
if (winners.length !== 1) continue;
|
|
171
|
+
const keep = winners[0];
|
|
172
|
+
for (const [file, ls] of files) if (file !== keep) for (const l of ls) drop.add(l);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!drop.size) return raw;
|
|
176
|
+
return { nodes: raw.nodes, links: raw.links.filter((l) => !drop.has(l)) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ../../packages/data/src/backends/prompt.ts
|
|
180
|
+
function communityLine(c) {
|
|
181
|
+
return `Community ${c.id}: ${c.representatives.join(", ")}`;
|
|
182
|
+
}
|
|
183
|
+
function communityLabelPrompt(communities) {
|
|
184
|
+
return 'You are naming clusters in a knowledge graph. For each community below, return a concise 2-5 word plain-language name describing what it is about (e.g. "Order Management", "Payment Flow", "Auth Middleware"). Respond ONLY with a JSON object mapping the community id (as a string) to its name - no prose, no markdown fences.\n\n' + communities.map(communityLine).join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ../../packages/data/src/backends/graphify.ts
|
|
188
|
+
var run = promisify(execFile);
|
|
189
|
+
var LABEL_TOP_K = 8;
|
|
190
|
+
var LABEL_MAX_LEN = 80;
|
|
191
|
+
var PLACEHOLDER_COMMUNITY = /^Community \d+$/;
|
|
192
|
+
var DEFAULT_GRAPH_JSON = ".data/graphify/graph.json";
|
|
193
|
+
function graphify(options = {}) {
|
|
194
|
+
const graphRel = options.path ?? DEFAULT_GRAPH_JSON;
|
|
195
|
+
return backend({
|
|
196
|
+
name: "graphify",
|
|
197
|
+
native: () => ({ type: "graphify", graphJson: graphRel }),
|
|
198
|
+
// Declared, so `jarvis-data lint` can gate on it while presenting the built graph. Still checked
|
|
199
|
+
// inside `load` too: a build DOES run the backend, and a graph that fell behind mid-build is worth
|
|
200
|
+
// saying so at the moment it is produced.
|
|
201
|
+
...options.staleness ? { staleness: options.staleness } : {},
|
|
202
|
+
load: async (ctx) => {
|
|
203
|
+
const root = resolve2(ctx.config.repoRoot, options.root ?? ".");
|
|
204
|
+
const graphPath = resolve2(ctx.config.repoRoot, graphRel);
|
|
205
|
+
const bin = options.bin ?? "graphify";
|
|
206
|
+
const forceExtract = process.env.JARVIS_DATA_REBUILD === "1";
|
|
207
|
+
const refresh = process.env.JARVIS_DATA_REFRESH === "1" && options.extract !== false;
|
|
208
|
+
const forceLabel = process.env.JARVIS_DATA_LABEL === "1";
|
|
209
|
+
const mode = forceExtract ? true : options.extract ?? "auto";
|
|
210
|
+
const present = await exists(graphPath);
|
|
211
|
+
const missing = options.missing !== void 0 ? { missing: options.missing } : {};
|
|
212
|
+
const extracted = forceExtract && present ? await rebuildFresh(graphPath, () => extract(bin, root, ctx, graphRel, { ...missing })) : refresh ? await extract(bin, root, ctx, graphRel, { force: true, ...missing }) : mode === true || mode === "auto" && !present ? await extract(bin, root, ctx, graphRel, { ...missing }) : false;
|
|
213
|
+
if (extracted) await stampFresh(graphPath);
|
|
214
|
+
if (!await exists(graphPath)) {
|
|
215
|
+
ctx.panic(
|
|
216
|
+
`graphify graph not found at ${graphRel}. Run \`jarvis build graph\`, or set { extract: true } in the backend options.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (options.staleness) await reportIfStale(ctx, graphPath, graphRel, options.staleness);
|
|
220
|
+
const raw = JSON.parse(await readFile2(graphPath, "utf8"));
|
|
221
|
+
if (options.labeler && (forceLabel || extracted && !refresh)) {
|
|
222
|
+
await labelCommunities(ctx, options.labeler, raw, graphPath, extracted);
|
|
223
|
+
}
|
|
224
|
+
const labels = await loadCommunityLabels(graphPath);
|
|
225
|
+
const corrected = await resolveImportCollisions(raw, root);
|
|
226
|
+
return mapAndFilter(corrected, options, labels);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function loadCommunityLabels(graphPath) {
|
|
231
|
+
const labels = /* @__PURE__ */ new Map();
|
|
232
|
+
const labelPath = resolve2(dirname(graphPath), ".graphify_labels.json");
|
|
233
|
+
try {
|
|
234
|
+
const raw = JSON.parse(await readFile2(labelPath, "utf8"));
|
|
235
|
+
if (!raw || typeof raw !== "object") return labels;
|
|
236
|
+
for (const [rawId, rawName] of Object.entries(raw)) {
|
|
237
|
+
const name = asString(rawName);
|
|
238
|
+
if (name) labels.set(rawId, name);
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
return labels;
|
|
243
|
+
}
|
|
244
|
+
function mapAndFilter(raw, options, labels) {
|
|
245
|
+
const matchIgnore = options.ignore && options.ignore.length ? matcher(options.ignore) : void 0;
|
|
246
|
+
const matchInclude = options.include && options.include.length ? matcher(options.include) : void 0;
|
|
247
|
+
const kept = /* @__PURE__ */ new Set();
|
|
248
|
+
const nodes = [];
|
|
249
|
+
for (const n of raw.nodes) {
|
|
250
|
+
const id = asString(n.id);
|
|
251
|
+
if (!id) continue;
|
|
252
|
+
const sourceFile = asString(n.source_file);
|
|
253
|
+
if (sourceFile) {
|
|
254
|
+
if (matchInclude && !matchInclude(sourceFile)) continue;
|
|
255
|
+
if (matchIgnore && matchIgnore(sourceFile)) continue;
|
|
256
|
+
}
|
|
257
|
+
kept.add(id);
|
|
258
|
+
nodes.push({
|
|
259
|
+
id,
|
|
260
|
+
type: asString(n.file_type) ?? "code",
|
|
261
|
+
metadata: {
|
|
262
|
+
...asString(n.label) ? { label: asString(n.label) } : {},
|
|
263
|
+
...sourceFile ? { sourceFile } : {},
|
|
264
|
+
// graphify carries the symbol's location as e.g. "L123" — keep it so rendered code
|
|
265
|
+
// nodes can surface `file:line` (Claude Code jumps/Reads by line). Parsed in lineOf.
|
|
266
|
+
...asString(n.source_location) ? { sourceLine: asString(n.source_location) } : {},
|
|
267
|
+
...communityOf(n, labels) ? { community: communityOf(n, labels) } : {},
|
|
268
|
+
origin: "graphify"
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const edges = [];
|
|
273
|
+
for (const l of raw.links) {
|
|
274
|
+
const from = asString(l.source);
|
|
275
|
+
const to = asString(l.target);
|
|
276
|
+
if (!from || !to || !kept.has(from) || !kept.has(to)) continue;
|
|
277
|
+
edges.push({ from, to, relation: asString(l.relation) ?? "related" });
|
|
278
|
+
}
|
|
279
|
+
return { nodes, edges };
|
|
280
|
+
}
|
|
281
|
+
function communityOf(n, labels) {
|
|
282
|
+
const named = asString(n.community_name);
|
|
283
|
+
if (named && !PLACEHOLDER_COMMUNITY.test(named)) return named;
|
|
284
|
+
const id = n.community;
|
|
285
|
+
if (id === void 0 || id === null || id === "") return void 0;
|
|
286
|
+
const key = String(id);
|
|
287
|
+
return labels.get(key) ?? `Community ${key}`;
|
|
288
|
+
}
|
|
289
|
+
function graphifyOptions(ctx, graphRel) {
|
|
290
|
+
return { cwd: ctx.config.repoRoot, env: { ...process.env, GRAPHIFY_OUT: dirname(graphRel) } };
|
|
291
|
+
}
|
|
292
|
+
async function extract(bin, root, ctx, graphRel, options = {}) {
|
|
293
|
+
try {
|
|
294
|
+
ctx.logger.info(
|
|
295
|
+
`graphify: extracting ${root}${options.force ? " (incremental \u2014 prune deletions)" : ""}`
|
|
296
|
+
);
|
|
297
|
+
const args = options.force ? ["update", "--force", root] : ["update", root];
|
|
298
|
+
await run(bin, args, graphifyOptions(ctx, graphRel));
|
|
299
|
+
return true;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
const code = err.code;
|
|
302
|
+
if (code === "ENOENT") {
|
|
303
|
+
const message = `graphify binary "${bin}" not found \u2014 install it with \`uv tool install graphifyy\`, or set { extract: false } to present the existing graph.`;
|
|
304
|
+
if (options.missing === "error") ctx.panic(message);
|
|
305
|
+
ctx.report({
|
|
306
|
+
severity: options.missing ?? "warn",
|
|
307
|
+
code: "BACKEND_UNAVAILABLE",
|
|
308
|
+
message: `${message} Using the existing graph.json if present.`
|
|
309
|
+
});
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
ctx.report({
|
|
313
|
+
severity: "warn",
|
|
314
|
+
code: "BACKEND_EXTRACT_FAILED",
|
|
315
|
+
message: `graphify update failed: ${err instanceof Error ? err.message : String(err)}`
|
|
316
|
+
});
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function rebuildFresh(graphPath, extract2) {
|
|
321
|
+
const backup = `${graphPath}.bak`;
|
|
322
|
+
await rename(graphPath, backup);
|
|
323
|
+
let ok = false;
|
|
324
|
+
try {
|
|
325
|
+
ok = await extract2() && await parseable(graphPath);
|
|
326
|
+
} catch {
|
|
327
|
+
ok = false;
|
|
328
|
+
}
|
|
329
|
+
if (ok) {
|
|
330
|
+
await rm(backup, { force: true });
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
await rename(backup, graphPath);
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
async function parseable(path) {
|
|
337
|
+
try {
|
|
338
|
+
JSON.parse(await readFile2(path, "utf8"));
|
|
339
|
+
return true;
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function stampFresh(path) {
|
|
345
|
+
try {
|
|
346
|
+
const now = /* @__PURE__ */ new Date();
|
|
347
|
+
await utimes(path, now, now);
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function labelCommunities(ctx, labeler, raw, graphPath, freshExtract) {
|
|
352
|
+
const sampled = sampleCommunities(raw);
|
|
353
|
+
if (!sampled.length) return;
|
|
354
|
+
const relabelAll = freshExtract || process.env.JARVIS_DATA_RELABEL_ALL === "1";
|
|
355
|
+
const existing = relabelAll ? /* @__PURE__ */ new Map() : await readCommunityLabels(graphPath);
|
|
356
|
+
const communities = sampled.filter((c) => !existing.has(c.id));
|
|
357
|
+
if (!communities.length) {
|
|
358
|
+
ctx.logger.info(
|
|
359
|
+
"communities already named \u2014 nothing to label (set JARVIS_DATA_RELABEL_ALL=1 to force)"
|
|
360
|
+
);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
ctx.logger.info(`labeling ${communities.length} communities via ${labeler.name}`);
|
|
365
|
+
const named = await labeler.label({ communities, prompt: communityLabelPrompt });
|
|
366
|
+
if (named.size) await writeCommunityLabels(graphPath, named);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
ctx.report({
|
|
369
|
+
severity: "warn",
|
|
370
|
+
code: "BACKEND_LABEL_FAILED",
|
|
371
|
+
message: `community labeling (${labeler.name}) failed; communities keep their cluster ids: ${err instanceof Error ? err.message : String(err)}`
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function sampleCommunities(raw) {
|
|
376
|
+
const degree = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const l of raw.links) {
|
|
378
|
+
const s = asString(l.source);
|
|
379
|
+
const t = asString(l.target);
|
|
380
|
+
if (s) degree.set(s, (degree.get(s) ?? 0) + 1);
|
|
381
|
+
if (t) degree.set(t, (degree.get(t) ?? 0) + 1);
|
|
382
|
+
}
|
|
383
|
+
const groups = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const n of raw.nodes) {
|
|
385
|
+
const id = asString(n.id);
|
|
386
|
+
if (!id) continue;
|
|
387
|
+
const c = n.community;
|
|
388
|
+
if (c === void 0 || c === null || c === "") continue;
|
|
389
|
+
const member = { id, label: asString(n.label) ?? id };
|
|
390
|
+
const arr = groups.get(String(c));
|
|
391
|
+
if (arr) arr.push(member);
|
|
392
|
+
else groups.set(String(c), [member]);
|
|
393
|
+
}
|
|
394
|
+
const communities = [];
|
|
395
|
+
for (const [id, members] of [...groups].sort((a, b) => b[1].length - a[1].length)) {
|
|
396
|
+
members.sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
|
|
397
|
+
const representatives = [];
|
|
398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
399
|
+
for (const m of members) {
|
|
400
|
+
const label = m.label.trim().replace(/^\(+|\)+$/g, "").slice(0, LABEL_MAX_LEN);
|
|
401
|
+
if (label && !seen.has(label.toLowerCase())) {
|
|
402
|
+
seen.add(label.toLowerCase());
|
|
403
|
+
representatives.push(label);
|
|
404
|
+
}
|
|
405
|
+
if (representatives.length >= LABEL_TOP_K) break;
|
|
406
|
+
}
|
|
407
|
+
if (representatives.length) communities.push({ id, representatives });
|
|
408
|
+
}
|
|
409
|
+
return communities;
|
|
410
|
+
}
|
|
411
|
+
function communityLabelsPath(graphPath) {
|
|
412
|
+
return resolve2(dirname(graphPath), ".graphify_labels.json");
|
|
413
|
+
}
|
|
414
|
+
async function readCommunityLabels(graphPath) {
|
|
415
|
+
const out = /* @__PURE__ */ new Map();
|
|
416
|
+
try {
|
|
417
|
+
const existing = JSON.parse(await readFile2(communityLabelsPath(graphPath), "utf8"));
|
|
418
|
+
for (const [id, value] of Object.entries(existing)) {
|
|
419
|
+
const name = asString(value);
|
|
420
|
+
if (name && !PLACEHOLDER_COMMUNITY.test(name)) out.set(id, name);
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
async function writeCommunityLabels(graphPath, named) {
|
|
427
|
+
const merged = {};
|
|
428
|
+
for (const [id, name] of await readCommunityLabels(graphPath)) merged[id] = name;
|
|
429
|
+
for (const [id, name] of named) merged[id] = name;
|
|
430
|
+
await writeFile(communityLabelsPath(graphPath), `${JSON.stringify(merged, null, 2)}
|
|
431
|
+
`, "utf8");
|
|
432
|
+
}
|
|
433
|
+
async function reportIfStale(ctx, graphPath, graphRel, severity) {
|
|
434
|
+
let graphMs;
|
|
435
|
+
try {
|
|
436
|
+
graphMs = (await stat(graphPath)).mtimeMs;
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
let headMs;
|
|
441
|
+
try {
|
|
442
|
+
const { stdout } = await run("git", ["show", "-s", "--format=%ct", "HEAD"], {
|
|
443
|
+
cwd: ctx.config.repoRoot
|
|
444
|
+
});
|
|
445
|
+
const seconds = Number(stdout.trim());
|
|
446
|
+
headMs = Number.isFinite(seconds) ? seconds * 1e3 : void 0;
|
|
447
|
+
} catch {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (headMs !== void 0 && graphMs < headMs) {
|
|
451
|
+
ctx.report({
|
|
452
|
+
severity,
|
|
453
|
+
code: "stale-graph",
|
|
454
|
+
message: `${graphRel} is older than HEAD \u2014 the code graph is stale, so traces and impact analysis can't be trusted. Rebuild it (\`graphify update .\` / \`make graph\`).`
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function exists(path) {
|
|
459
|
+
return access(path).then(
|
|
460
|
+
() => true,
|
|
461
|
+
() => false
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
function asString(value) {
|
|
465
|
+
return typeof value === "string" ? value : void 0;
|
|
466
|
+
}
|
|
467
|
+
export {
|
|
468
|
+
graphify
|
|
469
|
+
};
|