@structured-vibe-coding/core 0.17.2 → 0.19.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/defaults/prices.yaml +9 -0
- package/defaults/search.yaml +18 -0
- package/defaults/teams/research/roles/researcher.md +29 -0
- package/defaults/teams/research/team.yaml +7 -0
- package/defaults/teams/software/roles/architect.md +31 -0
- package/defaults/teams/software/roles/dev-lead.md +32 -0
- package/defaults/teams/software/roles/developer.md +30 -0
- package/defaults/teams/software/roles/product-manager.md +32 -0
- package/defaults/teams/software/roles/product-owner.md +30 -0
- package/defaults/teams/software/roles/qa.md +35 -0
- package/defaults/teams/software/routing.yaml +28 -0
- package/defaults/teams/software/team.yaml +24 -0
- package/defaults/teams/solo/roles/developer.md +28 -0
- package/defaults/teams/solo/team.yaml +7 -0
- package/defaults/templates/plan.md +17 -0
- package/defaults/templates/review.md +11 -0
- package/defaults/templates/story.md +13 -0
- package/defaults/templates/task.md +11 -0
- package/defaults/workflows/team-software.yaml +153 -0
- package/dist/{agents-RWYBLZQE.js → agents-PSIOQEYH.js} +18 -7
- package/dist/{change-context-W4HDO2IK.js → change-context-EZJMSGSP.js} +2 -4
- package/dist/chunk-26YG6A4M.js +1210 -0
- package/dist/chunk-2FJBFYVJ.js +1576 -0
- package/dist/{chunk-KYWETNFU.js → chunk-4BRO7BOJ.js} +2 -4
- package/dist/{chunk-74OGSVJZ.js → chunk-H55RFAEI.js} +2 -2
- package/dist/chunk-HG7TPW5V.js +567 -0
- package/dist/{chunk-EPFURATC.js → chunk-O5SP6X5R.js} +89 -0
- package/dist/chunk-TGRV6WGS.js +3436 -0
- package/dist/{gather-BZDW4EN4.js → gather-7TSVKQO2.js} +3 -4
- package/dist/{greenfield-Z425JOMP.js → greenfield-QFA5WVHE.js} +5 -6
- package/dist/index.d.ts +1832 -44
- package/dist/index.js +5172 -992
- package/dist/{sources-PF45EUH2.js → sources-3TXUPU6V.js} +2 -3
- package/package.json +11 -3
- package/vendor/tree-sitter/NOTICE +29 -0
- package/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/dist/chunk-67AUAGNV.js +0 -150
- package/dist/chunk-7JLDL2OK.js +0 -771
- package/dist/chunk-7MJYPJMR.js +0 -51
- package/dist/chunk-A752UVPF.js +0 -317
- package/dist/chunk-P3N67UEZ.js +0 -538
- package/dist/chunk-R5WQN4HA.js +0 -139
- package/dist/chunk-WHDG6OTG.js +0 -46
- package/dist/config-X5VDBVR4.js +0 -16
- package/dist/fs-J6DIV6CX.js +0 -28
- package/dist/paths-64GA32IZ.js +0 -28
|
@@ -0,0 +1,3436 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigSchema,
|
|
3
|
+
defaultTeamsDir,
|
|
4
|
+
getStage,
|
|
5
|
+
isStageSkipped,
|
|
6
|
+
shouldAutoSkip
|
|
7
|
+
} from "./chunk-HG7TPW5V.js";
|
|
8
|
+
import {
|
|
9
|
+
SddError
|
|
10
|
+
} from "./chunk-MHODNFIL.js";
|
|
11
|
+
import {
|
|
12
|
+
activePointerPath,
|
|
13
|
+
changesPathRel,
|
|
14
|
+
configPath,
|
|
15
|
+
displayChangePath,
|
|
16
|
+
ensureDir,
|
|
17
|
+
guidancePaths,
|
|
18
|
+
indexDbPath,
|
|
19
|
+
listDirs,
|
|
20
|
+
listFiles,
|
|
21
|
+
memoryPathRel,
|
|
22
|
+
pathExists,
|
|
23
|
+
readText,
|
|
24
|
+
readYaml,
|
|
25
|
+
removePath,
|
|
26
|
+
sddRoot,
|
|
27
|
+
searchConfigPath,
|
|
28
|
+
writeText,
|
|
29
|
+
writeYaml
|
|
30
|
+
} from "./chunk-O5SP6X5R.js";
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
async function isInitialized(projectRoot) {
|
|
34
|
+
return pathExists(configPath(projectRoot));
|
|
35
|
+
}
|
|
36
|
+
async function loadConfig(projectRoot) {
|
|
37
|
+
const path = configPath(projectRoot);
|
|
38
|
+
if (!await pathExists(path)) {
|
|
39
|
+
throw new SddError(`SDD is not initialized in ${projectRoot}. Run \`sdd init\` first.`, {
|
|
40
|
+
code: "NOT_INITIALIZED",
|
|
41
|
+
hint: "cd into your app and run: sdd init --here --ai copilot"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const raw = await readYaml(path);
|
|
45
|
+
return ConfigSchema.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
async function saveConfig(projectRoot, config) {
|
|
48
|
+
await ensureDir(sddRoot(projectRoot));
|
|
49
|
+
await writeYaml(configPath(projectRoot), ConfigSchema.parse(config));
|
|
50
|
+
}
|
|
51
|
+
function defaultConfig() {
|
|
52
|
+
return ConfigSchema.parse({});
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function mergeDefaults(defaults, overrides) {
|
|
58
|
+
if (!isRecord(defaults) || !isRecord(overrides)) return overrides ?? defaults;
|
|
59
|
+
const merged = /* @__PURE__ */ Object.create(null);
|
|
60
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
61
|
+
merged[key] = Object.hasOwn(overrides, key) ? mergeDefaults(value, overrides[key]) : value;
|
|
62
|
+
}
|
|
63
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
64
|
+
if (!Object.hasOwn(defaults, key)) merged[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
function mergeConfigDefaults(existing) {
|
|
69
|
+
return ConfigSchema.parse(mergeDefaults(defaultConfig(), existing));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/git-head.ts
|
|
73
|
+
import { spawn } from "child_process";
|
|
74
|
+
import { join } from "pathe";
|
|
75
|
+
function runGitHead(projectRoot) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const child = spawn("git", ["rev-parse", "--verify", "HEAD"], {
|
|
78
|
+
cwd: projectRoot,
|
|
79
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
80
|
+
});
|
|
81
|
+
let stdout = "";
|
|
82
|
+
let stderr = "";
|
|
83
|
+
child.stdout?.on("data", (chunk) => {
|
|
84
|
+
stdout += chunk.toString();
|
|
85
|
+
});
|
|
86
|
+
child.stderr?.on("data", (chunk) => {
|
|
87
|
+
stderr += chunk.toString();
|
|
88
|
+
});
|
|
89
|
+
child.on("error", reject);
|
|
90
|
+
child.on("close", (code) => {
|
|
91
|
+
if (code === 0) resolve(stdout.trim());
|
|
92
|
+
else reject(new Error(stderr.trim() || `git rev-parse exited with code ${code}`));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function getHeadSha(projectRoot, runGit = runGitHead) {
|
|
97
|
+
if (!await pathExists(join(projectRoot, ".git"))) return null;
|
|
98
|
+
let sha;
|
|
99
|
+
try {
|
|
100
|
+
sha = await runGit(projectRoot);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(sha)) {
|
|
105
|
+
throw new Error("git rev-parse returned an invalid HEAD SHA.");
|
|
106
|
+
}
|
|
107
|
+
return sha.toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/role/types.ts
|
|
111
|
+
function emptyRoleLists() {
|
|
112
|
+
return {
|
|
113
|
+
owns: [],
|
|
114
|
+
inputs: [],
|
|
115
|
+
outputs: [],
|
|
116
|
+
collaborates: [],
|
|
117
|
+
reviews: [],
|
|
118
|
+
approvers: [],
|
|
119
|
+
never: [],
|
|
120
|
+
askHumanWhen: [],
|
|
121
|
+
skills: [],
|
|
122
|
+
rules: [],
|
|
123
|
+
goals: []
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
var WORK_TYPES = [
|
|
127
|
+
"route",
|
|
128
|
+
"classify",
|
|
129
|
+
"check",
|
|
130
|
+
"draft",
|
|
131
|
+
"design",
|
|
132
|
+
"code",
|
|
133
|
+
"review"
|
|
134
|
+
];
|
|
135
|
+
function isWorkType(value) {
|
|
136
|
+
return WORK_TYPES.includes(value);
|
|
137
|
+
}
|
|
138
|
+
function workKind(workType) {
|
|
139
|
+
return workType === "route" || workType === "classify" || workType === "check" ? "decide" : "reason";
|
|
140
|
+
}
|
|
141
|
+
var ROLE_TEXT = {
|
|
142
|
+
needsJob: "A role needs a job.",
|
|
143
|
+
notFound: "Role not found.",
|
|
144
|
+
exists: "Role already exists.",
|
|
145
|
+
badName: "A role name is not valid.",
|
|
146
|
+
self: "A role cannot review itself.",
|
|
147
|
+
x: "X is not a role tool.",
|
|
148
|
+
needsSection: "This needs an accepted section before a draft.",
|
|
149
|
+
noModel: "No role model is configured.",
|
|
150
|
+
cannot: "A role cannot do that.",
|
|
151
|
+
askFirst: "Ask first. This stays stopped.",
|
|
152
|
+
choice: "Allow once, Always allow, or Deny.",
|
|
153
|
+
unavailable: "This action is not available in this slice.",
|
|
154
|
+
expired: "Expired. The action did not run.",
|
|
155
|
+
runNotFound: "Run not found.",
|
|
156
|
+
badYaml: "role.yaml is invalid"
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/search/tokenize.ts
|
|
160
|
+
import { encode } from "gpt-tokenizer/encoding/cl100k_base";
|
|
161
|
+
function countTokens(text) {
|
|
162
|
+
return encode(text).length;
|
|
163
|
+
}
|
|
164
|
+
function ftsTokens(query) {
|
|
165
|
+
const tokens = [];
|
|
166
|
+
let current = "";
|
|
167
|
+
for (const ch of query) {
|
|
168
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
169
|
+
const ok3 = code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code > 127;
|
|
170
|
+
if (ok3) {
|
|
171
|
+
current += ch;
|
|
172
|
+
} else if (current) {
|
|
173
|
+
tokens.push(current);
|
|
174
|
+
current = "";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (current) tokens.push(current);
|
|
178
|
+
return tokens;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/search/config.ts
|
|
182
|
+
import { z } from "zod";
|
|
183
|
+
var SearchConfigError = class extends Error {
|
|
184
|
+
constructor() {
|
|
185
|
+
super("search.yaml is invalid");
|
|
186
|
+
this.name = "SearchConfigError";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var BudgetSchema = z.object({
|
|
190
|
+
default: z.number().int().min(1).default(12e3)
|
|
191
|
+
}).catchall(z.number().int().min(1));
|
|
192
|
+
var SearchConfigSchema = z.object({
|
|
193
|
+
version: z.literal(1).default(1),
|
|
194
|
+
provider: z.string().min(1).default("builtin"),
|
|
195
|
+
k: z.number().int().min(1).max(100).default(20),
|
|
196
|
+
embeddings: z.boolean().default(false),
|
|
197
|
+
budgets: BudgetSchema.default({ default: 12e3 }),
|
|
198
|
+
exclude: z.array(z.string().min(1)).default([])
|
|
199
|
+
});
|
|
200
|
+
function budgetForStage(config, stage) {
|
|
201
|
+
if (!stage) return config.budgets.default;
|
|
202
|
+
const value = config.budgets[stage];
|
|
203
|
+
return typeof value === "number" ? value : config.budgets.default;
|
|
204
|
+
}
|
|
205
|
+
async function loadSearchConfig(projectRoot) {
|
|
206
|
+
const file = searchConfigPath(projectRoot);
|
|
207
|
+
if (!await pathExists(file)) {
|
|
208
|
+
return SearchConfigSchema.parse({});
|
|
209
|
+
}
|
|
210
|
+
let raw;
|
|
211
|
+
try {
|
|
212
|
+
raw = await readYaml(file);
|
|
213
|
+
} catch {
|
|
214
|
+
throw new SearchConfigError();
|
|
215
|
+
}
|
|
216
|
+
const parsed = SearchConfigSchema.safeParse(raw ?? {});
|
|
217
|
+
if (!parsed.success) throw new SearchConfigError();
|
|
218
|
+
return parsed.data;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/search/pack.ts
|
|
222
|
+
var EN_DASH = "\u2013";
|
|
223
|
+
function searchPreamble(query, scope, budget, hybrid = false) {
|
|
224
|
+
if (hybrid) {
|
|
225
|
+
return `# Search
|
|
226
|
+
Query: ${query}
|
|
227
|
+
Scope: ${scope}
|
|
228
|
+
Provider: builtin
|
|
229
|
+
Mode: hybrid
|
|
230
|
+
Budget: ${budget}
|
|
231
|
+
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
return `# Search
|
|
235
|
+
Query: ${query}
|
|
236
|
+
Scope: ${scope}
|
|
237
|
+
Provider: builtin
|
|
238
|
+
Budget: ${budget}
|
|
239
|
+
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
function renderBlock(hit, form) {
|
|
243
|
+
const fence = hit.language ? `\`\`\`${hit.language}` : "```";
|
|
244
|
+
const body = form === "full" ? hit.body : form === "summary" ? hit.summary ?? "" : hit.signature;
|
|
245
|
+
return `### \`${hit.path}\` \xB7 \`${hit.symbol}\` (lines ${hit.startLine}${EN_DASH}${hit.endLine}) \xB7 ${form}
|
|
246
|
+
${fence}
|
|
247
|
+
${body}
|
|
248
|
+
\`\`\`
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
function contains(outer, inner) {
|
|
252
|
+
return outer.path === inner.path && inner.startLine >= outer.startLine && inner.endLine <= outer.endLine;
|
|
253
|
+
}
|
|
254
|
+
function packHits(input) {
|
|
255
|
+
const { preamble, hits, budget } = input;
|
|
256
|
+
if (countTokens(preamble) > budget) {
|
|
257
|
+
return { text: "", usedTokens: 0, hits: [] };
|
|
258
|
+
}
|
|
259
|
+
let text = preamble;
|
|
260
|
+
const packed = [];
|
|
261
|
+
const full = [];
|
|
262
|
+
const seen = /* @__PURE__ */ new Set();
|
|
263
|
+
for (const hit of hits) {
|
|
264
|
+
const id = `${hit.path}\0${hit.symbol}\0${hit.startLine}`;
|
|
265
|
+
if (seen.has(id)) continue;
|
|
266
|
+
seen.add(id);
|
|
267
|
+
if (full.some((outer) => contains(outer, hit))) continue;
|
|
268
|
+
const summary = hit.summary;
|
|
269
|
+
if (summary && summary !== hit.body) {
|
|
270
|
+
const summaryBlock = renderBlock(hit, "summary");
|
|
271
|
+
const summaryText = text + summaryBlock;
|
|
272
|
+
if (countTokens(summaryText) <= budget) {
|
|
273
|
+
text = summaryText;
|
|
274
|
+
packed.push({ ...hit, form: "summary", text: summaryBlock });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const fullBlock = renderBlock(hit, "full");
|
|
279
|
+
const fullText = text + fullBlock;
|
|
280
|
+
if (countTokens(fullText) <= budget) {
|
|
281
|
+
text = fullText;
|
|
282
|
+
full.push(hit);
|
|
283
|
+
packed.push({ ...hit, form: "full", text: fullBlock });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const signatureBlock = renderBlock(hit, "signature");
|
|
287
|
+
const signatureText = text + signatureBlock;
|
|
288
|
+
if (countTokens(signatureText) <= budget) {
|
|
289
|
+
text = signatureText;
|
|
290
|
+
packed.push({ ...hit, form: "signature", text: signatureBlock });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!text) return { text: "", usedTokens: 0, hits: packed };
|
|
294
|
+
return { text, usedTokens: countTokens(text), hits: packed };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/code-context/secrets.ts
|
|
298
|
+
import { basename, normalize } from "pathe";
|
|
299
|
+
var SECRET_BASENAME_PATTERNS = [
|
|
300
|
+
/^\.env$/i,
|
|
301
|
+
/^\.env\..+/i,
|
|
302
|
+
/\.pem$/i,
|
|
303
|
+
/\.key$/i,
|
|
304
|
+
/id_rsa/i,
|
|
305
|
+
/credentials/i,
|
|
306
|
+
/secret/i
|
|
307
|
+
];
|
|
308
|
+
function isSecretPath(filePath) {
|
|
309
|
+
const norm = normalize(filePath).replace(/\\/g, "/");
|
|
310
|
+
const parts = norm.split("/").filter(Boolean);
|
|
311
|
+
if (parts.includes(".git")) return true;
|
|
312
|
+
if (norm.includes("/.git/") || norm.startsWith(".git/") || norm === ".git") {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
const base = basename(norm);
|
|
316
|
+
return SECRET_BASENAME_PATTERNS.some((re) => re.test(base));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/code-context/ignore.ts
|
|
320
|
+
import { basename as basename2, extname, normalize as normalize2 } from "pathe";
|
|
321
|
+
var IGNORED_DIR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
322
|
+
"node_modules",
|
|
323
|
+
"dist",
|
|
324
|
+
"coverage",
|
|
325
|
+
".turbo",
|
|
326
|
+
".next",
|
|
327
|
+
"build",
|
|
328
|
+
".cache"
|
|
329
|
+
]);
|
|
330
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
331
|
+
".png",
|
|
332
|
+
".jpg",
|
|
333
|
+
".jpeg",
|
|
334
|
+
".gif",
|
|
335
|
+
".webp",
|
|
336
|
+
".ico",
|
|
337
|
+
".svg",
|
|
338
|
+
".woff",
|
|
339
|
+
".woff2",
|
|
340
|
+
".ttf",
|
|
341
|
+
".eot",
|
|
342
|
+
".pdf",
|
|
343
|
+
".zip",
|
|
344
|
+
".tar",
|
|
345
|
+
".gz",
|
|
346
|
+
".tgz",
|
|
347
|
+
".7z",
|
|
348
|
+
".rar",
|
|
349
|
+
".exe",
|
|
350
|
+
".dll",
|
|
351
|
+
".so",
|
|
352
|
+
".dylib",
|
|
353
|
+
".bin",
|
|
354
|
+
".wasm",
|
|
355
|
+
".mp3",
|
|
356
|
+
".mp4",
|
|
357
|
+
".mov",
|
|
358
|
+
".lock"
|
|
359
|
+
]);
|
|
360
|
+
function isIgnoredPath(filePath) {
|
|
361
|
+
const norm = normalize2(filePath).replace(/\\/g, "/");
|
|
362
|
+
const parts = norm.split("/").filter(Boolean);
|
|
363
|
+
for (const p of parts) {
|
|
364
|
+
if (IGNORED_DIR_SEGMENTS.has(p)) return true;
|
|
365
|
+
}
|
|
366
|
+
const ext = extname(basename2(norm)).toLowerCase();
|
|
367
|
+
if (BINARY_EXTENSIONS.has(ext)) return true;
|
|
368
|
+
const base = basename2(norm);
|
|
369
|
+
if (base === "package-lock.json" || base === "pnpm-lock.yaml" || base === "yarn.lock" || base === "bun.lockb") {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
function classifyPath(filePath) {
|
|
375
|
+
if (isSecretPath(filePath)) return "secret";
|
|
376
|
+
if (isIgnoredPath(filePath)) return "ignored";
|
|
377
|
+
return "allowed";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/search/exclude.ts
|
|
381
|
+
import { readFile, readdir } from "fs/promises";
|
|
382
|
+
import ignore from "ignore";
|
|
383
|
+
import { join as join2 } from "pathe";
|
|
384
|
+
function isHardDenied(rel) {
|
|
385
|
+
if (isSecretPath(rel) || isIgnoredPath(rel)) return true;
|
|
386
|
+
const slash = rel.indexOf("/");
|
|
387
|
+
const first = slash === -1 ? rel : rel.slice(0, slash);
|
|
388
|
+
return first === ".sdd" || first === ".git";
|
|
389
|
+
}
|
|
390
|
+
function isPrunedDir(rel) {
|
|
391
|
+
if (isIgnoredPath(rel)) return true;
|
|
392
|
+
const parts = rel.split("/");
|
|
393
|
+
for (const part of parts) {
|
|
394
|
+
if (part === ".git" || part === ".sdd") return true;
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
function rewriteNestedLine(dir, line) {
|
|
399
|
+
if (line.length === 0) return [line];
|
|
400
|
+
let i = 0;
|
|
401
|
+
while (i < line.length && (line.charCodeAt(i) === 32 || line.charCodeAt(i) === 9)) i += 1;
|
|
402
|
+
if (i >= line.length) return [line];
|
|
403
|
+
if (line.charCodeAt(i) === 35) return [line];
|
|
404
|
+
let negated = false;
|
|
405
|
+
if (line.charCodeAt(i) === 33) {
|
|
406
|
+
negated = true;
|
|
407
|
+
i += 1;
|
|
408
|
+
}
|
|
409
|
+
let pattern = line.slice(i);
|
|
410
|
+
let end = pattern.length;
|
|
411
|
+
while (end > 0 && (pattern.charCodeAt(end - 1) === 32 || pattern.charCodeAt(end - 1) === 9)) {
|
|
412
|
+
end -= 1;
|
|
413
|
+
}
|
|
414
|
+
pattern = pattern.slice(0, end);
|
|
415
|
+
if (!pattern) return [line];
|
|
416
|
+
const bang = negated ? "!" : "";
|
|
417
|
+
if (!pattern.includes("/")) {
|
|
418
|
+
return [`${bang}${dir}/${pattern}`, `${bang}${dir}/**/${pattern}`];
|
|
419
|
+
}
|
|
420
|
+
const body = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
421
|
+
return [`${bang}${dir}/${body}`];
|
|
422
|
+
}
|
|
423
|
+
function rewriteNested(dir, content) {
|
|
424
|
+
const lines = content.split("\n");
|
|
425
|
+
const out = [];
|
|
426
|
+
for (const line of lines) {
|
|
427
|
+
const logical = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
428
|
+
out.push(...rewriteNestedLine(dir, logical));
|
|
429
|
+
}
|
|
430
|
+
return out.join("\n");
|
|
431
|
+
}
|
|
432
|
+
async function readIfPresent(file) {
|
|
433
|
+
if (!await pathExists(file)) return "";
|
|
434
|
+
return readFile(file, "utf8");
|
|
435
|
+
}
|
|
436
|
+
async function nestedGitignoreFiles(projectRoot) {
|
|
437
|
+
const found = [];
|
|
438
|
+
async function walk(dir, rel) {
|
|
439
|
+
let entries;
|
|
440
|
+
try {
|
|
441
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
442
|
+
} catch {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
448
|
+
if (isPrunedDir(child)) continue;
|
|
449
|
+
if (entry.isDirectory()) {
|
|
450
|
+
await walk(join2(dir, entry.name), child);
|
|
451
|
+
} else if (entry.isFile() && entry.name === ".gitignore" && rel) {
|
|
452
|
+
found.push(child);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
await walk(projectRoot, "");
|
|
457
|
+
found.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
458
|
+
return found;
|
|
459
|
+
}
|
|
460
|
+
async function loadExcluder(projectRoot, exclude) {
|
|
461
|
+
const git = ignore();
|
|
462
|
+
const rootIgnore = await readIfPresent(join2(projectRoot, ".gitignore"));
|
|
463
|
+
if (rootIgnore) git.add(rootIgnore);
|
|
464
|
+
for (const rel of await nestedGitignoreFiles(projectRoot)) {
|
|
465
|
+
if (git.ignores(rel)) continue;
|
|
466
|
+
const dir = rel.slice(0, rel.lastIndexOf("/"));
|
|
467
|
+
const body = await readIfPresent(join2(projectRoot, rel));
|
|
468
|
+
if (body) git.add(rewriteNested(dir, body));
|
|
469
|
+
}
|
|
470
|
+
const sdd = ignore();
|
|
471
|
+
const sddIgnore = await readIfPresent(join2(projectRoot, ".sddignore"));
|
|
472
|
+
if (sddIgnore) sdd.add(sddIgnore);
|
|
473
|
+
const extra2 = ignore();
|
|
474
|
+
if (exclude.length) extra2.add(exclude.join("\n"));
|
|
475
|
+
return (rel) => {
|
|
476
|
+
const path = rel.replaceAll("\\", "/");
|
|
477
|
+
if (isHardDenied(path)) return true;
|
|
478
|
+
if (git.ignores(path)) return true;
|
|
479
|
+
if (sdd.ignores(path)) return true;
|
|
480
|
+
return extra2.ignores(path);
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/search/chunk-markdown.ts
|
|
485
|
+
function toLines(text) {
|
|
486
|
+
let normalized = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
487
|
+
if (normalized.endsWith("\n")) normalized = normalized.slice(0, -1);
|
|
488
|
+
if (!normalized) return [];
|
|
489
|
+
return normalized.split("\n");
|
|
490
|
+
}
|
|
491
|
+
function leadingSpaces(line) {
|
|
492
|
+
let i = 0;
|
|
493
|
+
while (i < line.length && line.charCodeAt(i) === 32) i += 1;
|
|
494
|
+
return i;
|
|
495
|
+
}
|
|
496
|
+
function fenceOpen(line) {
|
|
497
|
+
const spaces = leadingSpaces(line);
|
|
498
|
+
if (spaces > 3) return null;
|
|
499
|
+
let i = spaces;
|
|
500
|
+
if (i >= line.length) return null;
|
|
501
|
+
const ch = line[i] ?? "";
|
|
502
|
+
if (ch !== "`" && ch !== "~") return null;
|
|
503
|
+
let len = 0;
|
|
504
|
+
while (i < line.length && line[i] === ch) {
|
|
505
|
+
len += 1;
|
|
506
|
+
i += 1;
|
|
507
|
+
}
|
|
508
|
+
if (len < 3) return null;
|
|
509
|
+
return { ch, len };
|
|
510
|
+
}
|
|
511
|
+
function fenceClose(line, open) {
|
|
512
|
+
const spaces = leadingSpaces(line);
|
|
513
|
+
if (spaces > 3) return false;
|
|
514
|
+
let i = spaces;
|
|
515
|
+
let len = 0;
|
|
516
|
+
while (i < line.length && line[i] === open.ch) {
|
|
517
|
+
len += 1;
|
|
518
|
+
i += 1;
|
|
519
|
+
}
|
|
520
|
+
if (len < open.len) return false;
|
|
521
|
+
while (i < line.length) {
|
|
522
|
+
if (line.charCodeAt(i) !== 32) return false;
|
|
523
|
+
i += 1;
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
function headingSymbol(line) {
|
|
528
|
+
let i = 0;
|
|
529
|
+
while (i < line.length && (line.charCodeAt(i) === 32 || line.charCodeAt(i) === 9)) i += 1;
|
|
530
|
+
let hashes = 0;
|
|
531
|
+
while (i < line.length && line[i] === "#" && hashes < 7) {
|
|
532
|
+
hashes += 1;
|
|
533
|
+
i += 1;
|
|
534
|
+
}
|
|
535
|
+
if (hashes < 1 || hashes > 6) return null;
|
|
536
|
+
if (line[i] !== " ") return null;
|
|
537
|
+
i += 1;
|
|
538
|
+
let text = line.slice(i);
|
|
539
|
+
let end = text.length;
|
|
540
|
+
while (end > 0 && text.charCodeAt(end - 1) === 32) end -= 1;
|
|
541
|
+
text = text.slice(0, end);
|
|
542
|
+
return text;
|
|
543
|
+
}
|
|
544
|
+
function hasNonWhitespace(lines) {
|
|
545
|
+
for (const line of lines) {
|
|
546
|
+
for (const ch of line) {
|
|
547
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
548
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 13) return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
function chunkMarkdown(text) {
|
|
554
|
+
const lines = toLines(text);
|
|
555
|
+
if (!lines.length) return [];
|
|
556
|
+
let open = null;
|
|
557
|
+
const headings = [];
|
|
558
|
+
for (let n = 0; n < lines.length; n += 1) {
|
|
559
|
+
const line = lines[n] ?? "";
|
|
560
|
+
if (open) {
|
|
561
|
+
if (fenceClose(line, open)) open = null;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const fence = fenceOpen(line);
|
|
565
|
+
if (fence) {
|
|
566
|
+
open = fence;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const symbol = headingSymbol(line);
|
|
570
|
+
if (symbol != null) headings.push({ line: n + 1, symbol, raw: line });
|
|
571
|
+
}
|
|
572
|
+
const chunks = [];
|
|
573
|
+
const first = headings[0]?.line ?? lines.length + 1;
|
|
574
|
+
if (first > 1) {
|
|
575
|
+
const pre = lines.slice(0, first - 1);
|
|
576
|
+
if (hasNonWhitespace(pre)) {
|
|
577
|
+
chunks.push({
|
|
578
|
+
symbol: "(preamble)",
|
|
579
|
+
startLine: 1,
|
|
580
|
+
endLine: first - 1,
|
|
581
|
+
signature: "(preamble)",
|
|
582
|
+
body: pre.join("\n")
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
for (let i = 0; i < headings.length; i += 1) {
|
|
587
|
+
const heading = headings[i];
|
|
588
|
+
const next = headings[i + 1]?.line ?? lines.length + 1;
|
|
589
|
+
const start2 = heading.line;
|
|
590
|
+
const end = next - 1;
|
|
591
|
+
chunks.push({
|
|
592
|
+
symbol: heading.symbol,
|
|
593
|
+
startLine: start2,
|
|
594
|
+
endLine: end,
|
|
595
|
+
signature: heading.raw,
|
|
596
|
+
body: lines.slice(start2 - 1, end).join("\n")
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return chunks;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/search/summarize.ts
|
|
603
|
+
import { decode, encode as encode2 } from "gpt-tokenizer/encoding/cl100k_base";
|
|
604
|
+
var SUMMARY_TOKEN_CAP = 80;
|
|
605
|
+
function clipToTokens(text, cap) {
|
|
606
|
+
const ids = encode2(text);
|
|
607
|
+
let n = Math.min(cap, ids.length);
|
|
608
|
+
let clipped = n === 0 ? "" : decode(ids.slice(0, n));
|
|
609
|
+
if (clipped.endsWith("\n")) clipped = clipped.slice(0, -1);
|
|
610
|
+
while (n > 0 && countTokens(clipped) > cap) {
|
|
611
|
+
n -= 1;
|
|
612
|
+
clipped = n === 0 ? "" : decode(ids.slice(0, n));
|
|
613
|
+
if (clipped.endsWith("\n")) clipped = clipped.slice(0, -1);
|
|
614
|
+
}
|
|
615
|
+
return clipped;
|
|
616
|
+
}
|
|
617
|
+
function extractiveSummary(signature, body) {
|
|
618
|
+
if (countTokens(body) <= SUMMARY_TOKEN_CAP) return body;
|
|
619
|
+
const keepHeading = signature.length > 0 && signature !== "(preamble)" && body.startsWith(signature) && (body.length === signature.length || body.charCodeAt(signature.length) === 10) && countTokens(signature) <= SUMMARY_TOKEN_CAP;
|
|
620
|
+
if (!keepHeading) return clipToTokens(body, SUMMARY_TOKEN_CAP);
|
|
621
|
+
const rest = body.slice(signature.length);
|
|
622
|
+
const restIds = encode2(rest);
|
|
623
|
+
let lo = 0;
|
|
624
|
+
let hi = restIds.length;
|
|
625
|
+
let best = signature;
|
|
626
|
+
while (lo <= hi) {
|
|
627
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
628
|
+
const tail = mid === 0 ? "" : decode(restIds.slice(0, mid));
|
|
629
|
+
const candidate = signature + tail;
|
|
630
|
+
if (countTokens(candidate) <= SUMMARY_TOKEN_CAP) {
|
|
631
|
+
best = candidate;
|
|
632
|
+
lo = mid + 1;
|
|
633
|
+
} else {
|
|
634
|
+
hi = mid - 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (best.endsWith("\n")) {
|
|
638
|
+
const stripped = best.slice(0, -1);
|
|
639
|
+
if (countTokens(stripped) <= SUMMARY_TOKEN_CAP && stripped.startsWith(signature)) {
|
|
640
|
+
best = stripped;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return best;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/search/embed.ts
|
|
647
|
+
import { rm, stat } from "fs/promises";
|
|
648
|
+
import { join as join3 } from "pathe";
|
|
649
|
+
var EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5";
|
|
650
|
+
var EMBEDDING_DIM = 384;
|
|
651
|
+
var EMBEDDINGS_UNAVAILABLE = "Embeddings are unavailable. Using keyword search.";
|
|
652
|
+
var MODEL_DIR = "Qdrant_bge-small-en-v1.5-onnx-Q";
|
|
653
|
+
var CACHE_FILES = [
|
|
654
|
+
"model_optimized.onnx",
|
|
655
|
+
"tokenizer.json",
|
|
656
|
+
"tokenizer_config.json",
|
|
657
|
+
"config.json",
|
|
658
|
+
"special_tokens_map.json"
|
|
659
|
+
];
|
|
660
|
+
var fastembedImportCount = 0;
|
|
661
|
+
var ready = /* @__PURE__ */ new Map();
|
|
662
|
+
var projectSessions = /* @__PURE__ */ new Map();
|
|
663
|
+
var runtimeSessions = /* @__PURE__ */ new WeakMap();
|
|
664
|
+
async function cacheComplete(dir) {
|
|
665
|
+
for (const name of CACHE_FILES) {
|
|
666
|
+
try {
|
|
667
|
+
const info = await stat(join3(dir, name));
|
|
668
|
+
if (!info.isFile() || info.size <= 0) return false;
|
|
669
|
+
} catch {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
function asFloat32(value) {
|
|
676
|
+
if (value instanceof Float32Array) return value;
|
|
677
|
+
return Float32Array.from(value);
|
|
678
|
+
}
|
|
679
|
+
function wrapModel(model) {
|
|
680
|
+
return {
|
|
681
|
+
async embedPassages(texts) {
|
|
682
|
+
const out = [];
|
|
683
|
+
for await (const batch of model.passageEmbed(texts, 16)) {
|
|
684
|
+
for (const vector of batch) out.push(asFloat32(vector));
|
|
685
|
+
}
|
|
686
|
+
if (out.length !== texts.length) throw new Error("Embedding batch length does not match.");
|
|
687
|
+
for (const vector of out) {
|
|
688
|
+
if (vector.length !== EMBEDDING_DIM) throw new Error("Embedding length is not 384.");
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
},
|
|
692
|
+
async embedQuery(text) {
|
|
693
|
+
const vector = asFloat32(await model.queryEmbed(text));
|
|
694
|
+
if (vector.length !== EMBEDDING_DIM) throw new Error("Embedding length is not 384.");
|
|
695
|
+
return vector;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function importFastembed() {
|
|
700
|
+
fastembedImportCount += 1;
|
|
701
|
+
return import("fastembed");
|
|
702
|
+
}
|
|
703
|
+
async function initReal(cacheDir) {
|
|
704
|
+
const mod = await importFastembed();
|
|
705
|
+
return mod.FlagEmbedding.init({
|
|
706
|
+
model: mod.EmbeddingModel.BGESmallENV15,
|
|
707
|
+
executionProviders: [mod.ExecutionProvider.CPU],
|
|
708
|
+
maxLength: 512,
|
|
709
|
+
showDownloadProgress: false,
|
|
710
|
+
cacheDir
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
async function start(projectRoot, hooks) {
|
|
714
|
+
const cacheDir = join3(sddRoot(projectRoot), "models", "fastembed");
|
|
715
|
+
const modelDir = join3(cacheDir, MODEL_DIR);
|
|
716
|
+
const completeBefore = await cacheComplete(modelDir);
|
|
717
|
+
try {
|
|
718
|
+
const model = hooks?.init ? await hooks.init({
|
|
719
|
+
model: "BGESmallENV15",
|
|
720
|
+
executionProviders: ["cpu"],
|
|
721
|
+
maxLength: 512,
|
|
722
|
+
showDownloadProgress: false,
|
|
723
|
+
cacheDir
|
|
724
|
+
}) : await initReal(cacheDir);
|
|
725
|
+
return wrapModel(model);
|
|
726
|
+
} catch (err) {
|
|
727
|
+
if (!completeBefore) await rm(modelDir, { recursive: true, force: true });
|
|
728
|
+
throw err;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function createFastembedEmbedder(projectRoot, hooks) {
|
|
732
|
+
if (hooks?.init) return start(projectRoot, hooks);
|
|
733
|
+
const cacheDir = join3(sddRoot(projectRoot), "models", "fastembed");
|
|
734
|
+
const existing = ready.get(cacheDir);
|
|
735
|
+
if (existing) return existing;
|
|
736
|
+
const pending = start(projectRoot).catch((err) => {
|
|
737
|
+
ready.delete(cacheDir);
|
|
738
|
+
throw err;
|
|
739
|
+
});
|
|
740
|
+
ready.set(cacheDir, pending);
|
|
741
|
+
return pending;
|
|
742
|
+
}
|
|
743
|
+
function resolveEmbedder(projectRoot, embeddings, runtime) {
|
|
744
|
+
if (!embeddings) return Promise.resolve(null);
|
|
745
|
+
if (runtime?.embedder) return Promise.resolve(runtime.embedder);
|
|
746
|
+
if (runtime?.embedderFactory) {
|
|
747
|
+
const existing2 = runtimeSessions.get(runtime);
|
|
748
|
+
if (existing2) return existing2;
|
|
749
|
+
const pending2 = runtime.embedderFactory().then((embedder) => embedder).catch(() => null);
|
|
750
|
+
runtimeSessions.set(runtime, pending2);
|
|
751
|
+
return pending2;
|
|
752
|
+
}
|
|
753
|
+
const existing = projectSessions.get(projectRoot);
|
|
754
|
+
if (existing) return existing;
|
|
755
|
+
const pending = createFastembedEmbedder(projectRoot).then(
|
|
756
|
+
(embedder) => embedder,
|
|
757
|
+
() => null
|
|
758
|
+
);
|
|
759
|
+
projectSessions.set(projectRoot, pending);
|
|
760
|
+
return pending;
|
|
761
|
+
}
|
|
762
|
+
async function embedSearchQuery(embedder, embeddings, query) {
|
|
763
|
+
if (!embeddings) return { queryVec: null, failure: "none" };
|
|
764
|
+
if (!embedder) return { queryVec: null, failure: "load" };
|
|
765
|
+
try {
|
|
766
|
+
const queryVec = await embedder.embedQuery(query);
|
|
767
|
+
if (queryVec.length !== EMBEDDING_DIM) return { queryVec: null, failure: "query" };
|
|
768
|
+
return { queryVec, failure: "none" };
|
|
769
|
+
} catch {
|
|
770
|
+
return { queryVec: null, failure: "query" };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/search/result-cache.ts
|
|
775
|
+
import { createHash } from "crypto";
|
|
776
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
777
|
+
import Database from "better-sqlite3";
|
|
778
|
+
import { isAbsolute } from "pathe";
|
|
779
|
+
|
|
780
|
+
// src/search/schema.ts
|
|
781
|
+
var SEARCH_SCHEMA_SQL = `
|
|
782
|
+
PRAGMA journal_mode = DELETE;
|
|
783
|
+
PRAGMA foreign_keys = ON;
|
|
784
|
+
|
|
785
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
786
|
+
path TEXT PRIMARY KEY,
|
|
787
|
+
content_hash TEXT NOT NULL,
|
|
788
|
+
scope TEXT NOT NULL CHECK (scope IN ('code', 'specs', 'memory')),
|
|
789
|
+
language TEXT NOT NULL
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
793
|
+
id INTEGER PRIMARY KEY,
|
|
794
|
+
path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
|
|
795
|
+
scope TEXT NOT NULL,
|
|
796
|
+
symbol TEXT NOT NULL,
|
|
797
|
+
start_line INTEGER NOT NULL,
|
|
798
|
+
end_line INTEGER NOT NULL,
|
|
799
|
+
signature TEXT NOT NULL,
|
|
800
|
+
body TEXT NOT NULL,
|
|
801
|
+
chunk_hash TEXT NOT NULL
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
805
|
+
symbol,
|
|
806
|
+
body,
|
|
807
|
+
tokenize = 'unicode61'
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
CREATE TABLE IF NOT EXISTS chunks_fts_map (
|
|
811
|
+
chunk_id INTEGER PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
812
|
+
fts_rowid INTEGER NOT NULL
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
816
|
+
key TEXT PRIMARY KEY,
|
|
817
|
+
value TEXT NOT NULL
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
CREATE TABLE IF NOT EXISTS vec_meta (
|
|
821
|
+
chunk_id INTEGER PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
822
|
+
chunk_hash TEXT NOT NULL,
|
|
823
|
+
scope TEXT NOT NULL CHECK (scope IN ('code', 'specs', 'memory'))
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
CREATE TABLE IF NOT EXISTS spec_summaries (
|
|
827
|
+
path TEXT NOT NULL,
|
|
828
|
+
content_hash TEXT NOT NULL,
|
|
829
|
+
symbol TEXT NOT NULL,
|
|
830
|
+
start_line INTEGER NOT NULL,
|
|
831
|
+
summary TEXT NOT NULL,
|
|
832
|
+
PRIMARY KEY (path, content_hash, symbol, start_line)
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
CREATE TABLE IF NOT EXISTS result_cache (
|
|
836
|
+
cache_key TEXT PRIMARY KEY,
|
|
837
|
+
query TEXT NOT NULL,
|
|
838
|
+
channel TEXT NOT NULL,
|
|
839
|
+
scope TEXT NOT NULL,
|
|
840
|
+
k INTEGER NOT NULL,
|
|
841
|
+
budget INTEGER NOT NULL,
|
|
842
|
+
embeddings INTEGER NOT NULL,
|
|
843
|
+
active_change TEXT NOT NULL,
|
|
844
|
+
filter TEXT NOT NULL,
|
|
845
|
+
index_version TEXT NOT NULL,
|
|
846
|
+
mode TEXT NOT NULL,
|
|
847
|
+
hybrid_scopes TEXT NOT NULL,
|
|
848
|
+
payload TEXT NOT NULL
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
CREATE TABLE IF NOT EXISTS result_cache_files (
|
|
852
|
+
cache_key TEXT NOT NULL,
|
|
853
|
+
path TEXT NOT NULL,
|
|
854
|
+
content_hash TEXT NOT NULL,
|
|
855
|
+
PRIMARY KEY (cache_key, path)
|
|
856
|
+
);
|
|
857
|
+
`;
|
|
858
|
+
var ADD_CHUNK_HASH_SQL = "ALTER TABLE chunks ADD COLUMN chunk_hash TEXT NOT NULL DEFAULT ''";
|
|
859
|
+
|
|
860
|
+
// src/search/result-cache.ts
|
|
861
|
+
var PRIVATE_MARKERS = ["FAKE_BIRTH_RECORD_S1", "fake-secret-s1-do-not-index"];
|
|
862
|
+
function containsPrivate(text) {
|
|
863
|
+
return PRIVATE_MARKERS.some((marker) => text.includes(marker));
|
|
864
|
+
}
|
|
865
|
+
function field(value) {
|
|
866
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
867
|
+
}
|
|
868
|
+
function comparePath(a, b) {
|
|
869
|
+
if (a < b) return -1;
|
|
870
|
+
if (a > b) return 1;
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
873
|
+
function filterText(paths, symbols) {
|
|
874
|
+
const pathText = [...paths].sort(comparePath).join("\n");
|
|
875
|
+
const symbolText = [...symbols].sort(comparePath).join("\n");
|
|
876
|
+
return `${pathText}
|
|
877
|
+
/
|
|
878
|
+
${symbolText}`;
|
|
879
|
+
}
|
|
880
|
+
function contributorText(rows) {
|
|
881
|
+
const sorted = [...rows].sort((a, b) => comparePath(a.path, b.path));
|
|
882
|
+
return sorted.map((row) => `${row.path} ${row.content_hash}
|
|
883
|
+
`).join("");
|
|
884
|
+
}
|
|
885
|
+
function sha256(text) {
|
|
886
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
887
|
+
}
|
|
888
|
+
function cacheMaterial(request, version, filter) {
|
|
889
|
+
return [
|
|
890
|
+
field(request.query),
|
|
891
|
+
field(request.channel),
|
|
892
|
+
field(request.scope),
|
|
893
|
+
field(String(request.k)),
|
|
894
|
+
field(String(request.budget)),
|
|
895
|
+
field(request.embeddings ? "1" : "0"),
|
|
896
|
+
field(request.activeChange),
|
|
897
|
+
field(version),
|
|
898
|
+
field(filter)
|
|
899
|
+
].join("\n");
|
|
900
|
+
}
|
|
901
|
+
function under(rel, root) {
|
|
902
|
+
let end = root.length;
|
|
903
|
+
while (end > 0 && root.charCodeAt(end - 1) === 47) end -= 1;
|
|
904
|
+
const prefix = root.slice(0, end);
|
|
905
|
+
if (!prefix) return false;
|
|
906
|
+
return rel === prefix || rel.startsWith(`${prefix}/`);
|
|
907
|
+
}
|
|
908
|
+
function openCacheDb(projectRoot) {
|
|
909
|
+
const db = new Database(indexDbPath(projectRoot));
|
|
910
|
+
db.exec(SEARCH_SCHEMA_SQL);
|
|
911
|
+
return db;
|
|
912
|
+
}
|
|
913
|
+
function scopeState(db, scope) {
|
|
914
|
+
const counts = db.prepare(
|
|
915
|
+
`SELECT
|
|
916
|
+
(SELECT COUNT(*) FROM chunks WHERE scope = ?) AS chunks,
|
|
917
|
+
(SELECT COUNT(*) FROM chunks AS c
|
|
918
|
+
JOIN vec_meta AS m ON m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
|
|
919
|
+
WHERE c.scope = ?) AS ready`
|
|
920
|
+
).get(scope, scope);
|
|
921
|
+
if (!counts || counts.chunks === 0) return "empty";
|
|
922
|
+
const model = db.prepare("SELECT value FROM meta WHERE key = ?").get("embedding_model");
|
|
923
|
+
const modelOk = !model || model.value === EMBEDDING_MODEL;
|
|
924
|
+
if (counts.ready === counts.chunks && modelOk) return "ready";
|
|
925
|
+
return "partial";
|
|
926
|
+
}
|
|
927
|
+
function readyList(db, scopes, embeddings) {
|
|
928
|
+
if (!embeddings) return "";
|
|
929
|
+
return scopes.filter((scope) => scopeState(db, scope) === "ready").sort(comparePath).join(",");
|
|
930
|
+
}
|
|
931
|
+
function normalizeHybrid(hybridScopes) {
|
|
932
|
+
return hybridScopes.split(",").filter(Boolean).sort(comparePath).join(",");
|
|
933
|
+
}
|
|
934
|
+
async function resolveActiveDirectory(projectRoot, config) {
|
|
935
|
+
let text;
|
|
936
|
+
try {
|
|
937
|
+
text = await readFile2(activePointerPath(projectRoot, config), "utf8");
|
|
938
|
+
} catch {
|
|
939
|
+
return "";
|
|
940
|
+
}
|
|
941
|
+
const id = text.trim();
|
|
942
|
+
if (!id || isAbsolute(id) || id.includes("\\")) return "";
|
|
943
|
+
const parts = id.split("/");
|
|
944
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) return "";
|
|
945
|
+
return displayChangePath(config, id);
|
|
946
|
+
}
|
|
947
|
+
function forgetPath(db, rel) {
|
|
948
|
+
const keys = db.prepare("SELECT cache_key FROM result_cache_files WHERE path = ?").all(rel);
|
|
949
|
+
const dropCache = db.prepare("DELETE FROM result_cache WHERE cache_key = ?");
|
|
950
|
+
const dropFiles = db.prepare("DELETE FROM result_cache_files WHERE cache_key = ?");
|
|
951
|
+
for (const row of keys) {
|
|
952
|
+
dropCache.run(row.cache_key);
|
|
953
|
+
dropFiles.run(row.cache_key);
|
|
954
|
+
}
|
|
955
|
+
db.prepare("DELETE FROM spec_summaries WHERE path = ?").run(rel);
|
|
956
|
+
}
|
|
957
|
+
function dropCacheKey(db, cacheKey) {
|
|
958
|
+
db.prepare("DELETE FROM result_cache WHERE cache_key = ?").run(cacheKey);
|
|
959
|
+
db.prepare("DELETE FROM result_cache_files WHERE cache_key = ?").run(cacheKey);
|
|
960
|
+
}
|
|
961
|
+
function hitFromPayload(value) {
|
|
962
|
+
if (!value || typeof value !== "object") return null;
|
|
963
|
+
const hit = value;
|
|
964
|
+
const formOk = hit.form === "full" || hit.form === "signature" || hit.form === "summary";
|
|
965
|
+
if (!formOk) return null;
|
|
966
|
+
if (typeof hit.path !== "string" || typeof hit.symbol !== "string") return null;
|
|
967
|
+
if (typeof hit.text !== "string" || typeof hit.signature !== "string") return null;
|
|
968
|
+
if (typeof hit.body !== "string" || typeof hit.language !== "string") return null;
|
|
969
|
+
if (typeof hit.startLine !== "number" || typeof hit.endLine !== "number") return null;
|
|
970
|
+
if (typeof hit.score !== "number" || typeof hit.scope !== "string") return null;
|
|
971
|
+
return {
|
|
972
|
+
scope: hit.scope,
|
|
973
|
+
path: hit.path,
|
|
974
|
+
startLine: hit.startLine,
|
|
975
|
+
endLine: hit.endLine,
|
|
976
|
+
symbol: hit.symbol,
|
|
977
|
+
score: hit.score,
|
|
978
|
+
form: hit.form,
|
|
979
|
+
text: hit.text,
|
|
980
|
+
signature: hit.signature,
|
|
981
|
+
body: hit.body,
|
|
982
|
+
language: hit.language
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function payloadHits(raw) {
|
|
986
|
+
let parsed;
|
|
987
|
+
try {
|
|
988
|
+
parsed = JSON.parse(raw);
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
993
|
+
const body = parsed;
|
|
994
|
+
if (typeof body.text !== "string" || !Array.isArray(body.hits)) return null;
|
|
995
|
+
const hits = [];
|
|
996
|
+
for (const value of body.hits) {
|
|
997
|
+
const hit = hitFromPayload(value);
|
|
998
|
+
if (!hit) return null;
|
|
999
|
+
hits.push(hit);
|
|
1000
|
+
}
|
|
1001
|
+
return { text: body.text, hits };
|
|
1002
|
+
}
|
|
1003
|
+
function cachedWarning(db, request) {
|
|
1004
|
+
if (!request.embeddings) return void 0;
|
|
1005
|
+
const partial = request.scopes.some((scope) => scopeState(db, scope) === "partial");
|
|
1006
|
+
return partial ? EMBEDDINGS_UNAVAILABLE : void 0;
|
|
1007
|
+
}
|
|
1008
|
+
function readPackedCache(request) {
|
|
1009
|
+
if (containsPrivate(request.query)) return null;
|
|
1010
|
+
const db = openCacheDb(request.projectRoot);
|
|
1011
|
+
try {
|
|
1012
|
+
const filter = filterText(request.filterPaths ?? [], request.filterSymbols ?? []);
|
|
1013
|
+
const rows = db.prepare(
|
|
1014
|
+
`SELECT cache_key, mode, hybrid_scopes, index_version, payload, query
|
|
1015
|
+
FROM result_cache
|
|
1016
|
+
WHERE query = ? AND channel = ? AND scope = ? AND k = ? AND budget = ?
|
|
1017
|
+
AND embeddings = ? AND active_change = ? AND filter = ?`
|
|
1018
|
+
).all(
|
|
1019
|
+
request.query,
|
|
1020
|
+
request.channel,
|
|
1021
|
+
request.scope,
|
|
1022
|
+
request.k,
|
|
1023
|
+
request.budget,
|
|
1024
|
+
request.embeddings ? 1 : 0,
|
|
1025
|
+
request.activeChange,
|
|
1026
|
+
filter
|
|
1027
|
+
);
|
|
1028
|
+
const expected = readyList(db, request.scopes, request.embeddings);
|
|
1029
|
+
for (const row of rows) {
|
|
1030
|
+
const parsed = payloadHits(row.payload);
|
|
1031
|
+
if (!parsed || containsPrivate(row.query) || containsPrivate(row.payload) || containsPrivate(parsed.text)) {
|
|
1032
|
+
dropCacheKey(db, row.cache_key);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
const files = db.prepare("SELECT path, content_hash FROM result_cache_files WHERE cache_key = ?").all(row.cache_key);
|
|
1036
|
+
let fresh = true;
|
|
1037
|
+
for (const file of files) {
|
|
1038
|
+
const current = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(file.path);
|
|
1039
|
+
if (!current || current.content_hash !== file.content_hash) {
|
|
1040
|
+
fresh = false;
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (!fresh || sha256(contributorText(files)) !== row.index_version) {
|
|
1045
|
+
dropCacheKey(db, row.cache_key);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
const hybrid = normalizeHybrid(row.hybrid_scopes);
|
|
1049
|
+
if (!request.embeddings) {
|
|
1050
|
+
if (row.mode !== "bm25" || hybrid !== "") {
|
|
1051
|
+
dropCacheKey(db, row.cache_key);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
} else if (hybrid !== expected) {
|
|
1055
|
+
dropCacheKey(db, row.cache_key);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
return { text: parsed.text, hits: parsed.hits, warning: cachedWarning(db, request) };
|
|
1059
|
+
}
|
|
1060
|
+
return null;
|
|
1061
|
+
} finally {
|
|
1062
|
+
db.close();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function serializeHit(hit) {
|
|
1066
|
+
return {
|
|
1067
|
+
scope: hit.scope,
|
|
1068
|
+
path: hit.path,
|
|
1069
|
+
startLine: hit.startLine,
|
|
1070
|
+
endLine: hit.endLine,
|
|
1071
|
+
symbol: hit.symbol,
|
|
1072
|
+
score: hit.score,
|
|
1073
|
+
form: hit.form,
|
|
1074
|
+
text: hit.text,
|
|
1075
|
+
signature: hit.signature,
|
|
1076
|
+
body: hit.body,
|
|
1077
|
+
language: hit.language,
|
|
1078
|
+
rankedBy: hit.rankedBy ?? null
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function writePackedCache(request, packed, contributors, mode, hybridScopes) {
|
|
1082
|
+
if (containsPrivate(request.query) || containsPrivate(packed.text)) return;
|
|
1083
|
+
if (packed.text.startsWith("Retrieved context skipped:")) return;
|
|
1084
|
+
const db = openCacheDb(request.projectRoot);
|
|
1085
|
+
try {
|
|
1086
|
+
const byPath2 = /* @__PURE__ */ new Map();
|
|
1087
|
+
for (const hit of contributors) {
|
|
1088
|
+
if (byPath2.has(hit.path)) continue;
|
|
1089
|
+
const row = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(hit.path);
|
|
1090
|
+
if (row) byPath2.set(hit.path, row.content_hash);
|
|
1091
|
+
}
|
|
1092
|
+
const files = [...byPath2.entries()].map(([path, content_hash]) => ({
|
|
1093
|
+
path,
|
|
1094
|
+
content_hash
|
|
1095
|
+
}));
|
|
1096
|
+
const version = sha256(contributorText(files));
|
|
1097
|
+
const filter = filterText(request.filterPaths ?? [], request.filterSymbols ?? []);
|
|
1098
|
+
const cacheKey = sha256(cacheMaterial(request, version, filter));
|
|
1099
|
+
const payload = JSON.stringify({
|
|
1100
|
+
text: packed.text,
|
|
1101
|
+
hits: packed.hits.map((hit) => serializeHit(hit))
|
|
1102
|
+
});
|
|
1103
|
+
if (containsPrivate(payload)) return;
|
|
1104
|
+
const hybrid = normalizeHybrid(hybridScopes);
|
|
1105
|
+
const write = db.transaction(() => {
|
|
1106
|
+
db.prepare(
|
|
1107
|
+
`INSERT INTO result_cache (
|
|
1108
|
+
cache_key, query, channel, scope, k, budget, embeddings, active_change, filter,
|
|
1109
|
+
index_version, mode, hybrid_scopes, payload
|
|
1110
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1111
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
1112
|
+
query = excluded.query,
|
|
1113
|
+
channel = excluded.channel,
|
|
1114
|
+
scope = excluded.scope,
|
|
1115
|
+
k = excluded.k,
|
|
1116
|
+
budget = excluded.budget,
|
|
1117
|
+
embeddings = excluded.embeddings,
|
|
1118
|
+
active_change = excluded.active_change,
|
|
1119
|
+
filter = excluded.filter,
|
|
1120
|
+
index_version = excluded.index_version,
|
|
1121
|
+
mode = excluded.mode,
|
|
1122
|
+
hybrid_scopes = excluded.hybrid_scopes,
|
|
1123
|
+
payload = excluded.payload`
|
|
1124
|
+
).run(
|
|
1125
|
+
cacheKey,
|
|
1126
|
+
request.query,
|
|
1127
|
+
request.channel,
|
|
1128
|
+
request.scope,
|
|
1129
|
+
request.k,
|
|
1130
|
+
request.budget,
|
|
1131
|
+
request.embeddings ? 1 : 0,
|
|
1132
|
+
request.activeChange,
|
|
1133
|
+
filter,
|
|
1134
|
+
version,
|
|
1135
|
+
mode,
|
|
1136
|
+
hybrid,
|
|
1137
|
+
payload
|
|
1138
|
+
);
|
|
1139
|
+
db.prepare("DELETE FROM result_cache_files WHERE cache_key = ?").run(cacheKey);
|
|
1140
|
+
const insertFile = db.prepare(
|
|
1141
|
+
"INSERT INTO result_cache_files (cache_key, path, content_hash) VALUES (?, ?, ?)"
|
|
1142
|
+
);
|
|
1143
|
+
for (const file of files) insertFile.run(cacheKey, file.path, file.content_hash);
|
|
1144
|
+
});
|
|
1145
|
+
write();
|
|
1146
|
+
} finally {
|
|
1147
|
+
db.close();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function attachSummaries(projectRoot, hits, activeChange) {
|
|
1151
|
+
const db = openCacheDb(projectRoot);
|
|
1152
|
+
try {
|
|
1153
|
+
return hits.map((hit) => {
|
|
1154
|
+
if (hit.scope === "code") return hit;
|
|
1155
|
+
if (hit.scope === "specs" && under(hit.path, activeChange)) return hit;
|
|
1156
|
+
if (hit.scope !== "specs" && hit.scope !== "memory") return hit;
|
|
1157
|
+
try {
|
|
1158
|
+
const file = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(hit.path);
|
|
1159
|
+
if (!file) return hit;
|
|
1160
|
+
const stored = db.prepare(
|
|
1161
|
+
`SELECT summary FROM spec_summaries
|
|
1162
|
+
WHERE path = ? AND content_hash = ? AND symbol = ? AND start_line = ?`
|
|
1163
|
+
).get(hit.path, file.content_hash, hit.symbol, hit.startLine);
|
|
1164
|
+
if (stored) return { ...hit, summary: stored.summary };
|
|
1165
|
+
if (containsPrivate(hit.body) || containsPrivate(hit.signature)) return hit;
|
|
1166
|
+
const summary = extractiveSummary(hit.signature, hit.body);
|
|
1167
|
+
if (containsPrivate(summary)) return hit;
|
|
1168
|
+
db.prepare("DELETE FROM spec_summaries WHERE path = ? AND content_hash != ?").run(
|
|
1169
|
+
hit.path,
|
|
1170
|
+
file.content_hash
|
|
1171
|
+
);
|
|
1172
|
+
db.prepare(
|
|
1173
|
+
`INSERT INTO spec_summaries (path, content_hash, symbol, start_line, summary)
|
|
1174
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1175
|
+
).run(hit.path, file.content_hash, hit.symbol, hit.startLine, summary);
|
|
1176
|
+
return { ...hit, summary };
|
|
1177
|
+
} catch {
|
|
1178
|
+
return hit;
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
} catch {
|
|
1182
|
+
return hits.slice();
|
|
1183
|
+
} finally {
|
|
1184
|
+
db.close();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/search/chunk-code.ts
|
|
1189
|
+
import { existsSync } from "fs";
|
|
1190
|
+
import { createRequire } from "module";
|
|
1191
|
+
import { fileURLToPath } from "url";
|
|
1192
|
+
import { dirname, join as join4 } from "pathe";
|
|
1193
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
1194
|
+
var EMIT = /* @__PURE__ */ new Set([
|
|
1195
|
+
"function_declaration",
|
|
1196
|
+
"generator_function_declaration",
|
|
1197
|
+
"class_declaration",
|
|
1198
|
+
"method_definition",
|
|
1199
|
+
"interface_declaration",
|
|
1200
|
+
"type_alias_declaration",
|
|
1201
|
+
"enum_declaration",
|
|
1202
|
+
"function_definition",
|
|
1203
|
+
"class_definition",
|
|
1204
|
+
"function_item",
|
|
1205
|
+
"struct_item",
|
|
1206
|
+
"enum_item",
|
|
1207
|
+
"trait_item",
|
|
1208
|
+
"impl_item",
|
|
1209
|
+
"mod_item"
|
|
1210
|
+
]);
|
|
1211
|
+
var INNER_FUNCTION = /* @__PURE__ */ new Set(["arrow_function", "function", "function_expression"]);
|
|
1212
|
+
var NAME_TYPES = /* @__PURE__ */ new Set(["identifier", "property_identifier", "type_identifier"]);
|
|
1213
|
+
var WASM_BY_EXT = {
|
|
1214
|
+
".js": "tree-sitter-javascript.wasm",
|
|
1215
|
+
".mjs": "tree-sitter-javascript.wasm",
|
|
1216
|
+
".cjs": "tree-sitter-javascript.wasm",
|
|
1217
|
+
".jsx": "tree-sitter-javascript.wasm",
|
|
1218
|
+
".ts": "tree-sitter-typescript.wasm",
|
|
1219
|
+
".mts": "tree-sitter-typescript.wasm",
|
|
1220
|
+
".cts": "tree-sitter-typescript.wasm",
|
|
1221
|
+
".tsx": "tree-sitter-tsx.wasm",
|
|
1222
|
+
".py": "tree-sitter-python.wasm",
|
|
1223
|
+
".rs": "tree-sitter-rust.wasm"
|
|
1224
|
+
};
|
|
1225
|
+
var LANG_BY_EXT = {
|
|
1226
|
+
".js": "js",
|
|
1227
|
+
".mjs": "js",
|
|
1228
|
+
".cjs": "js",
|
|
1229
|
+
".jsx": "js",
|
|
1230
|
+
".ts": "ts",
|
|
1231
|
+
".mts": "ts",
|
|
1232
|
+
".cts": "ts",
|
|
1233
|
+
".tsx": "tsx",
|
|
1234
|
+
".py": "py",
|
|
1235
|
+
".rs": "rs"
|
|
1236
|
+
};
|
|
1237
|
+
function languageForExt(ext) {
|
|
1238
|
+
return LANG_BY_EXT[ext] ?? null;
|
|
1239
|
+
}
|
|
1240
|
+
function grammarForExt(ext) {
|
|
1241
|
+
return WASM_BY_EXT[ext] ?? null;
|
|
1242
|
+
}
|
|
1243
|
+
function vendorDir() {
|
|
1244
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1245
|
+
const candidates = [
|
|
1246
|
+
join4(here, "..", "vendor", "tree-sitter"),
|
|
1247
|
+
join4(here, "vendor", "tree-sitter"),
|
|
1248
|
+
join4(here, "..", "..", "vendor", "tree-sitter")
|
|
1249
|
+
];
|
|
1250
|
+
for (const candidate of candidates) {
|
|
1251
|
+
if (existsSync(candidate)) return candidate;
|
|
1252
|
+
}
|
|
1253
|
+
return candidates[0];
|
|
1254
|
+
}
|
|
1255
|
+
var ready2 = null;
|
|
1256
|
+
var languages = /* @__PURE__ */ new Map();
|
|
1257
|
+
async function initParser() {
|
|
1258
|
+
if (!ready2) {
|
|
1259
|
+
const require2 = createRequire(import.meta.url);
|
|
1260
|
+
const wasm = require2.resolve("web-tree-sitter/web-tree-sitter.wasm");
|
|
1261
|
+
ready2 = Parser.init({ locateFile: () => wasm });
|
|
1262
|
+
}
|
|
1263
|
+
await ready2;
|
|
1264
|
+
}
|
|
1265
|
+
async function loadLanguage(ext) {
|
|
1266
|
+
const file = grammarForExt(ext);
|
|
1267
|
+
if (!file) return null;
|
|
1268
|
+
const cached = languages.get(file);
|
|
1269
|
+
if (cached) return cached;
|
|
1270
|
+
await initParser();
|
|
1271
|
+
const lang = await Language.load(join4(vendorDir(), file));
|
|
1272
|
+
languages.set(file, lang);
|
|
1273
|
+
return lang;
|
|
1274
|
+
}
|
|
1275
|
+
function sourceLines(text) {
|
|
1276
|
+
let normalized = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
1277
|
+
if (normalized.endsWith("\n")) normalized = normalized.slice(0, -1);
|
|
1278
|
+
if (!normalized) return [];
|
|
1279
|
+
return normalized.split("\n");
|
|
1280
|
+
}
|
|
1281
|
+
function startLine(node) {
|
|
1282
|
+
return node.startPosition.row + 1;
|
|
1283
|
+
}
|
|
1284
|
+
function endLine(node) {
|
|
1285
|
+
if (node.endPosition.column === 0 && node.endPosition.row > node.startPosition.row) {
|
|
1286
|
+
return node.endPosition.row;
|
|
1287
|
+
}
|
|
1288
|
+
return node.endPosition.row + 1;
|
|
1289
|
+
}
|
|
1290
|
+
function ownName(node) {
|
|
1291
|
+
for (let i = 0; i < node.childCount; i += 1) {
|
|
1292
|
+
const child = node.child(i);
|
|
1293
|
+
if (child && NAME_TYPES.has(child.type)) return child.text;
|
|
1294
|
+
}
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
function hasChildType(node, type) {
|
|
1298
|
+
for (let i = 0; i < node.childCount; i += 1) {
|
|
1299
|
+
if (node.child(i)?.type === type) return true;
|
|
1300
|
+
}
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
function hasDescendantType(node, types) {
|
|
1304
|
+
for (let i = 0; i < node.childCount; i += 1) {
|
|
1305
|
+
const child = node.child(i);
|
|
1306
|
+
if (!child) continue;
|
|
1307
|
+
if (types.has(child.type) || hasDescendantType(child, types)) return true;
|
|
1308
|
+
}
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
function declaresFunction(node) {
|
|
1312
|
+
return (node.type === "lexical_declaration" || node.type === "variable_declaration") && hasDescendantType(node, INNER_FUNCTION);
|
|
1313
|
+
}
|
|
1314
|
+
function insideCapturedDeclaration(node) {
|
|
1315
|
+
let parent = node.parent;
|
|
1316
|
+
while (parent) {
|
|
1317
|
+
if (declaresFunction(parent)) return true;
|
|
1318
|
+
parent = parent.parent;
|
|
1319
|
+
}
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
function classPrefix(node) {
|
|
1323
|
+
if (node.type === "class_declaration" || node.type === "class_definition") return null;
|
|
1324
|
+
let parent = node.parent;
|
|
1325
|
+
while (parent) {
|
|
1326
|
+
if (parent.type === "class_declaration" || parent.type === "class_definition" || parent.type === "class") {
|
|
1327
|
+
return ownName(parent);
|
|
1328
|
+
}
|
|
1329
|
+
parent = parent.parent;
|
|
1330
|
+
}
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
function spanNode(node) {
|
|
1334
|
+
const parent = node.parent;
|
|
1335
|
+
if (!parent) return node;
|
|
1336
|
+
if (parent.type === "export_statement" || parent.type === "decorated_definition") return parent;
|
|
1337
|
+
return node;
|
|
1338
|
+
}
|
|
1339
|
+
function shouldEmit(node) {
|
|
1340
|
+
if (insideCapturedDeclaration(node)) return false;
|
|
1341
|
+
if (declaresFunction(node)) return true;
|
|
1342
|
+
if (!EMIT.has(node.type)) return false;
|
|
1343
|
+
if (node.type === "mod_item" && !hasChildType(node, "declaration_list")) return false;
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
function symbolFor(node) {
|
|
1347
|
+
const name = ownName(node);
|
|
1348
|
+
if (node.type === "impl_item" && !name) return "(impl)";
|
|
1349
|
+
const prefix = classPrefix(node);
|
|
1350
|
+
if (prefix && name) return `${prefix}.${name}`;
|
|
1351
|
+
if (name) return name;
|
|
1352
|
+
return "(anonymous)";
|
|
1353
|
+
}
|
|
1354
|
+
function fileChunk(text, language) {
|
|
1355
|
+
const lines = sourceLines(text);
|
|
1356
|
+
if (!lines.length) return [];
|
|
1357
|
+
let signature = "(file)";
|
|
1358
|
+
for (const line of lines) {
|
|
1359
|
+
let blank = true;
|
|
1360
|
+
for (const ch of line) {
|
|
1361
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1362
|
+
if (code !== 32 && code !== 9) {
|
|
1363
|
+
blank = false;
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (!blank) {
|
|
1368
|
+
signature = line;
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return [
|
|
1373
|
+
{
|
|
1374
|
+
symbol: "(file)",
|
|
1375
|
+
startLine: 1,
|
|
1376
|
+
endLine: lines.length,
|
|
1377
|
+
signature,
|
|
1378
|
+
body: lines.join("\n"),
|
|
1379
|
+
language
|
|
1380
|
+
}
|
|
1381
|
+
];
|
|
1382
|
+
}
|
|
1383
|
+
var parser = null;
|
|
1384
|
+
var parseQueue = Promise.resolve();
|
|
1385
|
+
function parseSerialized(lang, text) {
|
|
1386
|
+
let release = () => void 0;
|
|
1387
|
+
const gate = new Promise((resolve) => {
|
|
1388
|
+
release = resolve;
|
|
1389
|
+
});
|
|
1390
|
+
const previous = parseQueue;
|
|
1391
|
+
parseQueue = gate;
|
|
1392
|
+
return previous.then(() => {
|
|
1393
|
+
try {
|
|
1394
|
+
if (!parser) parser = new Parser();
|
|
1395
|
+
parser.setLanguage(lang);
|
|
1396
|
+
return parser.parse(text);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return null;
|
|
1399
|
+
} finally {
|
|
1400
|
+
release();
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
async function chunkCode(text, ext) {
|
|
1405
|
+
if (text.includes("\0")) return null;
|
|
1406
|
+
const language = languageForExt(ext);
|
|
1407
|
+
if (!language) return null;
|
|
1408
|
+
const lang = await loadLanguage(ext);
|
|
1409
|
+
if (!lang) return null;
|
|
1410
|
+
const tree = await parseSerialized(lang, text);
|
|
1411
|
+
if (!tree) return null;
|
|
1412
|
+
const lines = sourceLines(text);
|
|
1413
|
+
const chunks = [];
|
|
1414
|
+
const visit = (node) => {
|
|
1415
|
+
if (shouldEmit(node)) {
|
|
1416
|
+
const span = spanNode(node);
|
|
1417
|
+
const start2 = startLine(span);
|
|
1418
|
+
const end = endLine(span);
|
|
1419
|
+
if (start2 >= 1 && end >= start2 && end <= lines.length) {
|
|
1420
|
+
const bodyLines = lines.slice(start2 - 1, end);
|
|
1421
|
+
const signature = bodyLines[0] ?? "(anonymous)";
|
|
1422
|
+
chunks.push({
|
|
1423
|
+
symbol: symbolFor(node),
|
|
1424
|
+
startLine: start2,
|
|
1425
|
+
endLine: end,
|
|
1426
|
+
signature,
|
|
1427
|
+
body: bodyLines.join("\n"),
|
|
1428
|
+
language
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
for (let i = 0; i < node.childCount; i += 1) {
|
|
1433
|
+
const child = node.child(i);
|
|
1434
|
+
if (child) visit(child);
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
visit(tree.rootNode);
|
|
1438
|
+
if (!chunks.length) return fileChunk(text, language);
|
|
1439
|
+
return chunks;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/search/rrf.ts
|
|
1443
|
+
var RRF_K = 60;
|
|
1444
|
+
function rrf(rank) {
|
|
1445
|
+
if (rank === void 0) return 0;
|
|
1446
|
+
return 1 / (RRF_K + rank);
|
|
1447
|
+
}
|
|
1448
|
+
function byPath(a, b) {
|
|
1449
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
1450
|
+
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
1451
|
+
if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;
|
|
1452
|
+
return 0;
|
|
1453
|
+
}
|
|
1454
|
+
function fuseByRank(entries) {
|
|
1455
|
+
const sorted = [...entries].sort((a, b) => {
|
|
1456
|
+
const delta = rrf(b.keywordRank) + rrf(b.vectorRank) - (rrf(a.keywordRank) + rrf(a.vectorRank));
|
|
1457
|
+
if (delta !== 0) return delta;
|
|
1458
|
+
return byPath(a, b);
|
|
1459
|
+
});
|
|
1460
|
+
return sorted.map((entry) => ({
|
|
1461
|
+
id: entry.id,
|
|
1462
|
+
score: rrf(entry.keywordRank) + rrf(entry.vectorRank)
|
|
1463
|
+
}));
|
|
1464
|
+
}
|
|
1465
|
+
function hitKey(hit) {
|
|
1466
|
+
return `${hit.path}\0${hit.startLine}\0${hit.symbol}`;
|
|
1467
|
+
}
|
|
1468
|
+
function comparePath2(a, b) {
|
|
1469
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
1470
|
+
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
1471
|
+
if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;
|
|
1472
|
+
return 0;
|
|
1473
|
+
}
|
|
1474
|
+
function fuseHits(keyword, vector) {
|
|
1475
|
+
const map = /* @__PURE__ */ new Map();
|
|
1476
|
+
for (let i = 0; i < keyword.length; i++) {
|
|
1477
|
+
const hit = keyword[i];
|
|
1478
|
+
if (!hit) continue;
|
|
1479
|
+
map.set(hitKey(hit), { hit, keywordRank: i + 1 });
|
|
1480
|
+
}
|
|
1481
|
+
for (let i = 0; i < vector.length; i++) {
|
|
1482
|
+
const hit = vector[i];
|
|
1483
|
+
if (!hit) continue;
|
|
1484
|
+
const key = hitKey(hit);
|
|
1485
|
+
const existing = map.get(key);
|
|
1486
|
+
if (existing) existing.vectorRank = i + 1;
|
|
1487
|
+
else map.set(key, { hit, vectorRank: i + 1 });
|
|
1488
|
+
}
|
|
1489
|
+
const fused = [];
|
|
1490
|
+
for (const entry of map.values()) {
|
|
1491
|
+
fused.push({
|
|
1492
|
+
...entry.hit,
|
|
1493
|
+
score: rrf(entry.keywordRank) + rrf(entry.vectorRank),
|
|
1494
|
+
rankedBy: "rrf"
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
fused.sort((a, b) => {
|
|
1498
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
1499
|
+
return comparePath2(a, b);
|
|
1500
|
+
});
|
|
1501
|
+
return fused;
|
|
1502
|
+
}
|
|
1503
|
+
var SCOPE_ORDER = { specs: 0, memory: 1, code: 2 };
|
|
1504
|
+
function compareBm25(a, b, scopeOrder) {
|
|
1505
|
+
if (a.score !== b.score) return a.score - b.score;
|
|
1506
|
+
if (scopeOrder) {
|
|
1507
|
+
const scopeDelta = SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
|
|
1508
|
+
if (scopeDelta !== 0) return scopeDelta;
|
|
1509
|
+
}
|
|
1510
|
+
return comparePath2(a, b);
|
|
1511
|
+
}
|
|
1512
|
+
function mergeRankedHits(hits) {
|
|
1513
|
+
if (hits.every((hit) => hit.rankedBy !== "rrf")) {
|
|
1514
|
+
return [...hits].sort((a, b) => compareBm25(a, b, true));
|
|
1515
|
+
}
|
|
1516
|
+
const rrfHits = hits.filter((hit) => hit.rankedBy === "rrf");
|
|
1517
|
+
const bm25Hits = hits.filter((hit) => hit.rankedBy !== "rrf");
|
|
1518
|
+
rrfHits.sort((a, b) => {
|
|
1519
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
1520
|
+
return comparePath2(a, b);
|
|
1521
|
+
});
|
|
1522
|
+
bm25Hits.sort((a, b) => compareBm25(a, b, false));
|
|
1523
|
+
return [...rrfHits, ...bm25Hits];
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/search/builtin.ts
|
|
1527
|
+
import { spawn as spawn2 } from "child_process";
|
|
1528
|
+
import { createHash as createHash2 } from "crypto";
|
|
1529
|
+
import { readFile as readFile3, readdir as readdir2 } from "fs/promises";
|
|
1530
|
+
import Database2 from "better-sqlite3";
|
|
1531
|
+
import { isAbsolute as isAbsolute2, join as join5 } from "pathe";
|
|
1532
|
+
|
|
1533
|
+
// src/search/vec.ts
|
|
1534
|
+
var TABLES = {
|
|
1535
|
+
code: "chunk_vec_code",
|
|
1536
|
+
specs: "chunk_vec_specs",
|
|
1537
|
+
memory: "chunk_vec_memory"
|
|
1538
|
+
};
|
|
1539
|
+
var CREATE_SQL = `
|
|
1540
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec_code USING vec0(
|
|
1541
|
+
embedding float[384] distance_metric=cosine
|
|
1542
|
+
);
|
|
1543
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec_specs USING vec0(
|
|
1544
|
+
embedding float[384] distance_metric=cosine
|
|
1545
|
+
);
|
|
1546
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec_memory USING vec0(
|
|
1547
|
+
embedding float[384] distance_metric=cosine
|
|
1548
|
+
);
|
|
1549
|
+
`;
|
|
1550
|
+
var state = /* @__PURE__ */ new WeakMap();
|
|
1551
|
+
function vecTable(scope) {
|
|
1552
|
+
return TABLES[scope];
|
|
1553
|
+
}
|
|
1554
|
+
function vecTablesPresent(db) {
|
|
1555
|
+
const row = db.prepare(
|
|
1556
|
+
`SELECT 1 AS n FROM sqlite_master
|
|
1557
|
+
WHERE name IN ('chunk_vec_code', 'chunk_vec_specs', 'chunk_vec_memory')
|
|
1558
|
+
LIMIT 1`
|
|
1559
|
+
).get();
|
|
1560
|
+
return Boolean(row);
|
|
1561
|
+
}
|
|
1562
|
+
function pickLoad(mod) {
|
|
1563
|
+
if (!mod || typeof mod !== "object") return null;
|
|
1564
|
+
const record = mod;
|
|
1565
|
+
if (typeof record.load === "function") return record.load;
|
|
1566
|
+
if (record.default && typeof record.default.load === "function") {
|
|
1567
|
+
return record.default.load;
|
|
1568
|
+
}
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
async function ensureVecLoaded(db) {
|
|
1572
|
+
const known = state.get(db);
|
|
1573
|
+
if (known === "ready") return true;
|
|
1574
|
+
if (known === "failed") return false;
|
|
1575
|
+
try {
|
|
1576
|
+
const mod = await import("sqlite-vec");
|
|
1577
|
+
const load = pickLoad(mod);
|
|
1578
|
+
if (!load) {
|
|
1579
|
+
state.set(db, "failed");
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
load(db);
|
|
1583
|
+
state.set(db, "ready");
|
|
1584
|
+
return true;
|
|
1585
|
+
} catch {
|
|
1586
|
+
state.set(db, "failed");
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function vecReady(db) {
|
|
1591
|
+
return state.get(db) === "ready";
|
|
1592
|
+
}
|
|
1593
|
+
function createVecTables(db) {
|
|
1594
|
+
db.exec(CREATE_SQL);
|
|
1595
|
+
}
|
|
1596
|
+
function dropVecTables(db) {
|
|
1597
|
+
db.exec(`
|
|
1598
|
+
DROP TABLE IF EXISTS chunk_vec_code;
|
|
1599
|
+
DROP TABLE IF EXISTS chunk_vec_specs;
|
|
1600
|
+
DROP TABLE IF EXISTS chunk_vec_memory;
|
|
1601
|
+
`);
|
|
1602
|
+
}
|
|
1603
|
+
function insertVector(db, scope, id, value, hash) {
|
|
1604
|
+
const table = TABLES[scope];
|
|
1605
|
+
const rowid = BigInt(id);
|
|
1606
|
+
db.prepare(`DELETE FROM ${table} WHERE rowid = ?`).run(rowid);
|
|
1607
|
+
db.prepare(`INSERT INTO ${table} (rowid, embedding) VALUES (?, ?)`).run(rowid, value);
|
|
1608
|
+
db.prepare(
|
|
1609
|
+
`INSERT INTO vec_meta (chunk_id, chunk_hash, scope) VALUES (?, ?, ?)
|
|
1610
|
+
ON CONFLICT(chunk_id) DO UPDATE SET chunk_hash = excluded.chunk_hash, scope = excluded.scope`
|
|
1611
|
+
).run(id, hash, scope);
|
|
1612
|
+
}
|
|
1613
|
+
function asBlob(value) {
|
|
1614
|
+
if (value instanceof Float32Array) return value;
|
|
1615
|
+
if (Buffer.isBuffer(value)) return value;
|
|
1616
|
+
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
function readVectorBlob(db, hash) {
|
|
1620
|
+
if (!vecReady(db)) return null;
|
|
1621
|
+
const row = db.prepare("SELECT chunk_id, scope FROM vec_meta WHERE chunk_hash = ? LIMIT 1").get(hash);
|
|
1622
|
+
if (!row || !TABLES[row.scope]) return null;
|
|
1623
|
+
try {
|
|
1624
|
+
const stored = db.prepare(`SELECT embedding FROM ${TABLES[row.scope]} WHERE rowid = ?`).get(BigInt(row.chunk_id));
|
|
1625
|
+
return stored ? asBlob(stored.embedding) : null;
|
|
1626
|
+
} catch {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
function queryVectors(db, scope, queryVec, k) {
|
|
1631
|
+
const table = TABLES[scope];
|
|
1632
|
+
return db.prepare(
|
|
1633
|
+
`SELECT c.path, c.scope, c.symbol, c.start_line, c.end_line, c.signature, c.body,
|
|
1634
|
+
f.language AS language, v.distance AS distance
|
|
1635
|
+
FROM ${table} AS v
|
|
1636
|
+
JOIN chunks AS c ON c.id = v.rowid
|
|
1637
|
+
JOIN vec_meta AS m ON m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
|
|
1638
|
+
JOIN files AS f ON f.path = c.path
|
|
1639
|
+
WHERE v.embedding MATCH ?
|
|
1640
|
+
AND k = ?
|
|
1641
|
+
AND c.scope = ?
|
|
1642
|
+
ORDER BY v.distance ASC, c.path ASC, c.start_line ASC, c.symbol ASC`
|
|
1643
|
+
).all(queryVec, k, scope);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/search/builtin.ts
|
|
1647
|
+
var CODE_EXT = /* @__PURE__ */ new Set([
|
|
1648
|
+
".ts",
|
|
1649
|
+
".tsx",
|
|
1650
|
+
".mts",
|
|
1651
|
+
".cts",
|
|
1652
|
+
".js",
|
|
1653
|
+
".jsx",
|
|
1654
|
+
".mjs",
|
|
1655
|
+
".cjs",
|
|
1656
|
+
".py",
|
|
1657
|
+
".rs"
|
|
1658
|
+
]);
|
|
1659
|
+
var EMPTY_STATS = {
|
|
1660
|
+
provider: "builtin",
|
|
1661
|
+
files: 0,
|
|
1662
|
+
chunks: 0,
|
|
1663
|
+
lastIndexFilesConsidered: 0,
|
|
1664
|
+
lastIndexFilesReindexed: 0,
|
|
1665
|
+
lastIndexChunksReindexed: 0,
|
|
1666
|
+
lastIndexChunksEmbedded: 0,
|
|
1667
|
+
indexedCommit: null
|
|
1668
|
+
};
|
|
1669
|
+
function extensionOf(rel) {
|
|
1670
|
+
const slash = rel.lastIndexOf("/");
|
|
1671
|
+
const base = slash === -1 ? rel : rel.slice(slash + 1);
|
|
1672
|
+
const dot = base.lastIndexOf(".");
|
|
1673
|
+
if (dot <= 0) return "";
|
|
1674
|
+
return base.slice(dot).toLowerCase();
|
|
1675
|
+
}
|
|
1676
|
+
function stripTrailingSlashes(value) {
|
|
1677
|
+
let end = value.length;
|
|
1678
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
1679
|
+
return value.slice(0, end);
|
|
1680
|
+
}
|
|
1681
|
+
function under2(rel, root) {
|
|
1682
|
+
const prefix = stripTrailingSlashes(root);
|
|
1683
|
+
if (!prefix) return false;
|
|
1684
|
+
return rel === prefix || rel.startsWith(`${prefix}/`);
|
|
1685
|
+
}
|
|
1686
|
+
function scopeForPath(rel, config) {
|
|
1687
|
+
const ext = extensionOf(rel);
|
|
1688
|
+
if (CODE_EXT.has(ext)) return "code";
|
|
1689
|
+
if (ext !== ".md") return null;
|
|
1690
|
+
if (under2(rel, memoryPathRel(config))) return "memory";
|
|
1691
|
+
const archive = stripTrailingSlashes(config.archive_path) || "archive";
|
|
1692
|
+
if (under2(rel, changesPathRel(config)) || under2(rel, archive) || under2(rel, "docs")) {
|
|
1693
|
+
return "specs";
|
|
1694
|
+
}
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
function assertRepoRelative(input) {
|
|
1698
|
+
if (isAbsolute2(input)) throw new Error("Index path must be project-relative.");
|
|
1699
|
+
let rel = input.replaceAll("\\", "/");
|
|
1700
|
+
while (rel.startsWith("./")) rel = rel.slice(2);
|
|
1701
|
+
if (!rel || rel.split("/").includes("..")) {
|
|
1702
|
+
throw new Error("Index path must be project-relative.");
|
|
1703
|
+
}
|
|
1704
|
+
return rel;
|
|
1705
|
+
}
|
|
1706
|
+
function chunkHash(body) {
|
|
1707
|
+
return createHash2("sha256").update(body, "utf8").digest("hex");
|
|
1708
|
+
}
|
|
1709
|
+
function migrateChunkHash(db) {
|
|
1710
|
+
const columns = db.prepare("PRAGMA table_info(chunks)").all();
|
|
1711
|
+
if (!columns.some((column) => column.name === "chunk_hash")) {
|
|
1712
|
+
db.exec(ADD_CHUNK_HASH_SQL);
|
|
1713
|
+
}
|
|
1714
|
+
const rows = db.prepare("SELECT id, body FROM chunks WHERE chunk_hash = ''").all();
|
|
1715
|
+
const update = db.prepare("UPDATE chunks SET chunk_hash = ? WHERE id = ?");
|
|
1716
|
+
for (const row of rows) update.run(chunkHash(row.body), row.id);
|
|
1717
|
+
}
|
|
1718
|
+
function openDb(projectRoot) {
|
|
1719
|
+
const db = new Database2(indexDbPath(projectRoot));
|
|
1720
|
+
db.exec(SEARCH_SCHEMA_SQL);
|
|
1721
|
+
migrateChunkHash(db);
|
|
1722
|
+
return db;
|
|
1723
|
+
}
|
|
1724
|
+
async function ensureDb(projectRoot) {
|
|
1725
|
+
await ensureDir(sddRoot(projectRoot));
|
|
1726
|
+
return openDb(projectRoot);
|
|
1727
|
+
}
|
|
1728
|
+
function deletePath(db, rel) {
|
|
1729
|
+
forgetPath(db, rel);
|
|
1730
|
+
const had = db.prepare("SELECT 1 AS n FROM files WHERE path = ?").get(rel);
|
|
1731
|
+
if (vecReady(db)) {
|
|
1732
|
+
const ids = db.prepare("SELECT id, scope FROM chunks WHERE path = ?").all(rel);
|
|
1733
|
+
for (const row of ids) {
|
|
1734
|
+
try {
|
|
1735
|
+
db.prepare(`DELETE FROM ${vecTable(row.scope)} WHERE rowid = ?`).run(BigInt(row.id));
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
db.prepare("DELETE FROM vec_meta WHERE chunk_id IN (SELECT id FROM chunks WHERE path = ?)").run(
|
|
1741
|
+
rel
|
|
1742
|
+
);
|
|
1743
|
+
db.prepare(
|
|
1744
|
+
`DELETE FROM chunks_fts WHERE rowid IN (
|
|
1745
|
+
SELECT m.fts_rowid FROM chunks_fts_map AS m
|
|
1746
|
+
JOIN chunks AS c ON c.id = m.chunk_id
|
|
1747
|
+
WHERE c.path = ?
|
|
1748
|
+
)`
|
|
1749
|
+
).run(rel);
|
|
1750
|
+
db.prepare("DELETE FROM chunks WHERE path = ?").run(rel);
|
|
1751
|
+
db.prepare("DELETE FROM files WHERE path = ?").run(rel);
|
|
1752
|
+
return Boolean(had);
|
|
1753
|
+
}
|
|
1754
|
+
function writeChunks(db, rel, hash, scope, language, chunks) {
|
|
1755
|
+
deletePath(db, rel);
|
|
1756
|
+
if (!chunks.length) return [];
|
|
1757
|
+
db.prepare("INSERT INTO files (path, content_hash, scope, language) VALUES (?, ?, ?, ?)").run(
|
|
1758
|
+
rel,
|
|
1759
|
+
hash,
|
|
1760
|
+
scope,
|
|
1761
|
+
language
|
|
1762
|
+
);
|
|
1763
|
+
const insertChunk = db.prepare(
|
|
1764
|
+
`INSERT INTO chunks (path, scope, symbol, start_line, end_line, signature, body, chunk_hash)
|
|
1765
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1766
|
+
);
|
|
1767
|
+
const insertFts = db.prepare("INSERT INTO chunks_fts (symbol, body) VALUES (?, ?)");
|
|
1768
|
+
const insertMap = db.prepare("INSERT INTO chunks_fts_map (chunk_id, fts_rowid) VALUES (?, ?)");
|
|
1769
|
+
const inserted = [];
|
|
1770
|
+
for (const chunk of chunks) {
|
|
1771
|
+
const hashOfBody = chunkHash(chunk.body);
|
|
1772
|
+
const info = insertChunk.run(
|
|
1773
|
+
rel,
|
|
1774
|
+
scope,
|
|
1775
|
+
chunk.symbol,
|
|
1776
|
+
chunk.startLine,
|
|
1777
|
+
chunk.endLine,
|
|
1778
|
+
chunk.signature,
|
|
1779
|
+
chunk.body,
|
|
1780
|
+
hashOfBody
|
|
1781
|
+
);
|
|
1782
|
+
const fts = insertFts.run(chunk.symbol, chunk.body);
|
|
1783
|
+
const id = Number(info.lastInsertRowid);
|
|
1784
|
+
insertMap.run(id, fts.lastInsertRowid);
|
|
1785
|
+
inserted.push({ id, hash: hashOfBody, body: chunk.body, scope });
|
|
1786
|
+
}
|
|
1787
|
+
return inserted;
|
|
1788
|
+
}
|
|
1789
|
+
function mapHit(row) {
|
|
1790
|
+
return {
|
|
1791
|
+
scope: row.scope,
|
|
1792
|
+
path: row.path,
|
|
1793
|
+
startLine: row.start_line,
|
|
1794
|
+
endLine: row.end_line,
|
|
1795
|
+
symbol: row.symbol,
|
|
1796
|
+
score: row.score,
|
|
1797
|
+
signature: row.signature,
|
|
1798
|
+
body: row.body,
|
|
1799
|
+
language: row.language,
|
|
1800
|
+
rankedBy: "bm25"
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
var HIT_SQL = `c.path, c.scope, c.symbol, c.start_line, c.end_line, c.signature, c.body,
|
|
1804
|
+
f.language AS language`;
|
|
1805
|
+
function queryRanked(db, query, scope, k) {
|
|
1806
|
+
const tokens = ftsTokens(query);
|
|
1807
|
+
if (!tokens.length || k < 1) return [];
|
|
1808
|
+
const rows = db.prepare(
|
|
1809
|
+
`SELECT ${HIT_SQL}, bm25(chunks_fts) AS score
|
|
1810
|
+
FROM chunks_fts
|
|
1811
|
+
JOIN chunks_fts_map AS m ON m.fts_rowid = chunks_fts.rowid
|
|
1812
|
+
JOIN chunks AS c ON c.id = m.chunk_id
|
|
1813
|
+
JOIN files AS f ON f.path = c.path
|
|
1814
|
+
WHERE chunks_fts MATCH ? AND c.scope = ?
|
|
1815
|
+
ORDER BY score ASC, c.path ASC, c.start_line ASC, c.symbol ASC
|
|
1816
|
+
LIMIT ?`
|
|
1817
|
+
).all(tokens.join(" OR "), scope, k);
|
|
1818
|
+
return rows.map(mapHit);
|
|
1819
|
+
}
|
|
1820
|
+
function queryScope(db, scope) {
|
|
1821
|
+
const rows = db.prepare(
|
|
1822
|
+
`SELECT ${HIT_SQL}, 0 AS score
|
|
1823
|
+
FROM chunks AS c
|
|
1824
|
+
JOIN files AS f ON f.path = c.path
|
|
1825
|
+
WHERE c.scope = ?
|
|
1826
|
+
ORDER BY c.path ASC, c.start_line ASC, c.symbol ASC`
|
|
1827
|
+
).all(scope);
|
|
1828
|
+
return rows.map(mapHit);
|
|
1829
|
+
}
|
|
1830
|
+
async function walkFiles(projectRoot) {
|
|
1831
|
+
const out = [];
|
|
1832
|
+
async function walk(dir, rel) {
|
|
1833
|
+
let entries;
|
|
1834
|
+
try {
|
|
1835
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1836
|
+
} catch {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1840
|
+
for (const entry of entries) {
|
|
1841
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1842
|
+
if (entry.isDirectory()) {
|
|
1843
|
+
if (isPrunedDir(child)) continue;
|
|
1844
|
+
await walk(join5(dir, entry.name), child);
|
|
1845
|
+
} else if (entry.isFile()) {
|
|
1846
|
+
out.push(child);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
await walk(projectRoot, "");
|
|
1851
|
+
return out;
|
|
1852
|
+
}
|
|
1853
|
+
async function chunksFor(bytes, rel, scope) {
|
|
1854
|
+
const text = bytes.toString("utf8");
|
|
1855
|
+
if (scope !== "code") {
|
|
1856
|
+
return chunkMarkdown(text).map((chunk) => ({ ...chunk, language: "md" }));
|
|
1857
|
+
}
|
|
1858
|
+
const ext = extensionOf(rel);
|
|
1859
|
+
const language = languageForExt(ext) ?? "js";
|
|
1860
|
+
const parsed = await chunkCode(text, ext);
|
|
1861
|
+
if (!parsed) return null;
|
|
1862
|
+
return parsed.map((chunk) => ({ ...chunk, language: chunk.language || language }));
|
|
1863
|
+
}
|
|
1864
|
+
function metaNumber(db, key) {
|
|
1865
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
1866
|
+
const n = Number(row?.value ?? "0");
|
|
1867
|
+
return Number.isFinite(n) ? n : 0;
|
|
1868
|
+
}
|
|
1869
|
+
function metaString(db, key) {
|
|
1870
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
1871
|
+
return row?.value ?? null;
|
|
1872
|
+
}
|
|
1873
|
+
function writeMetaValue(db, key, value) {
|
|
1874
|
+
db.prepare(
|
|
1875
|
+
`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
1876
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
1877
|
+
).run(key, value);
|
|
1878
|
+
}
|
|
1879
|
+
function readStats(db) {
|
|
1880
|
+
const files = db.prepare("SELECT COUNT(*) AS n FROM files").get();
|
|
1881
|
+
const chunks = db.prepare("SELECT COUNT(*) AS n FROM chunks").get();
|
|
1882
|
+
return {
|
|
1883
|
+
provider: "builtin",
|
|
1884
|
+
files: files.n,
|
|
1885
|
+
chunks: chunks.n,
|
|
1886
|
+
lastIndexFilesConsidered: metaNumber(db, "last_files_considered"),
|
|
1887
|
+
lastIndexFilesReindexed: metaNumber(db, "last_files_reindexed"),
|
|
1888
|
+
lastIndexChunksReindexed: metaNumber(db, "last_chunks_reindexed"),
|
|
1889
|
+
lastIndexChunksEmbedded: metaNumber(db, "last_chunks_embedded"),
|
|
1890
|
+
indexedCommit: metaString(db, "indexed_commit")
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function writeCounts(db, considered, reindexed, chunks, embedded) {
|
|
1894
|
+
writeMetaValue(db, "last_files_considered", String(considered));
|
|
1895
|
+
writeMetaValue(db, "last_files_reindexed", String(reindexed));
|
|
1896
|
+
writeMetaValue(db, "last_chunks_reindexed", String(chunks));
|
|
1897
|
+
writeMetaValue(db, "last_chunks_embedded", String(embedded));
|
|
1898
|
+
}
|
|
1899
|
+
function isCommitSha(value) {
|
|
1900
|
+
if (!value || value.length !== 40 && value.length !== 64) return false;
|
|
1901
|
+
for (let i = 0; i < value.length; i++) {
|
|
1902
|
+
const code = value.charCodeAt(i);
|
|
1903
|
+
const hex = code >= 48 && code <= 57 || code >= 97 && code <= 102 || code >= 65 && code <= 70;
|
|
1904
|
+
if (!hex) return false;
|
|
1905
|
+
}
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
async function safeHead(projectRoot) {
|
|
1909
|
+
try {
|
|
1910
|
+
return await getHeadSha(projectRoot);
|
|
1911
|
+
} catch {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function gitOutput(cwd, args) {
|
|
1916
|
+
return new Promise((resolve) => {
|
|
1917
|
+
let settled = false;
|
|
1918
|
+
const finish = (value) => {
|
|
1919
|
+
if (settled) return;
|
|
1920
|
+
settled = true;
|
|
1921
|
+
resolve(value);
|
|
1922
|
+
};
|
|
1923
|
+
let child;
|
|
1924
|
+
try {
|
|
1925
|
+
child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
1926
|
+
} catch {
|
|
1927
|
+
finish(null);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const parts = [];
|
|
1931
|
+
child.stdout?.on("data", (chunk) => {
|
|
1932
|
+
parts.push(chunk);
|
|
1933
|
+
});
|
|
1934
|
+
child.on("error", () => finish(null));
|
|
1935
|
+
child.on("close", (code) => finish(code === 0 ? Buffer.concat(parts) : null));
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
function splitNul(buf) {
|
|
1939
|
+
const parts = [];
|
|
1940
|
+
let start2 = 0;
|
|
1941
|
+
for (let i = 0; i <= buf.length; i++) {
|
|
1942
|
+
if (i === buf.length || buf[i] === 0) {
|
|
1943
|
+
if (i > start2) parts.push(buf.subarray(start2, i).toString("utf8"));
|
|
1944
|
+
start2 = i + 1;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return parts;
|
|
1948
|
+
}
|
|
1949
|
+
function normalizeGitPath(input) {
|
|
1950
|
+
let rel = input.replaceAll("\\", "/");
|
|
1951
|
+
while (rel.startsWith("./")) rel = rel.slice(2);
|
|
1952
|
+
if (!rel || isAbsolute2(rel)) return null;
|
|
1953
|
+
return rel;
|
|
1954
|
+
}
|
|
1955
|
+
function parseNameStatus(buf) {
|
|
1956
|
+
const parts = splitNul(buf);
|
|
1957
|
+
const candidates = [];
|
|
1958
|
+
const deletions = [];
|
|
1959
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
1960
|
+
const status = parts[i] ?? "";
|
|
1961
|
+
const path = normalizeGitPath(parts[i + 1] ?? "");
|
|
1962
|
+
if (!status || !path) continue;
|
|
1963
|
+
if (status.charCodeAt(0) === 68) deletions.push(path);
|
|
1964
|
+
else candidates.push(path);
|
|
1965
|
+
}
|
|
1966
|
+
return { candidates, deletions };
|
|
1967
|
+
}
|
|
1968
|
+
async function readGitDelta(projectRoot, cursor) {
|
|
1969
|
+
const diff = await gitOutput(projectRoot, [
|
|
1970
|
+
"diff",
|
|
1971
|
+
"--name-status",
|
|
1972
|
+
"-z",
|
|
1973
|
+
"--no-renames",
|
|
1974
|
+
cursor
|
|
1975
|
+
]);
|
|
1976
|
+
const untracked = await gitOutput(projectRoot, [
|
|
1977
|
+
"ls-files",
|
|
1978
|
+
"--others",
|
|
1979
|
+
"--exclude-standard",
|
|
1980
|
+
"-z"
|
|
1981
|
+
]);
|
|
1982
|
+
if (!diff || !untracked) return null;
|
|
1983
|
+
const parsed = parseNameStatus(diff);
|
|
1984
|
+
for (const path of splitNul(untracked)) {
|
|
1985
|
+
const rel = normalizeGitPath(path);
|
|
1986
|
+
if (rel) parsed.candidates.push(rel);
|
|
1987
|
+
}
|
|
1988
|
+
return parsed;
|
|
1989
|
+
}
|
|
1990
|
+
async function prepareVectors(db, create) {
|
|
1991
|
+
const present = vecTablesPresent(db);
|
|
1992
|
+
if (!present && !create) return false;
|
|
1993
|
+
const loaded = await ensureVecLoaded(db);
|
|
1994
|
+
if (!loaded) return false;
|
|
1995
|
+
if (create) createVecTables(db);
|
|
1996
|
+
const model = metaString(db, "embedding_model");
|
|
1997
|
+
if (create && model && model !== EMBEDDING_MODEL) {
|
|
1998
|
+
dropVecTables(db);
|
|
1999
|
+
db.prepare("DELETE FROM vec_meta").run();
|
|
2000
|
+
db.prepare("DELETE FROM meta WHERE key = ?").run("embedding_model");
|
|
2001
|
+
createVecTables(db);
|
|
2002
|
+
}
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
async function embedBodies(db, embedder, pending, tally) {
|
|
2006
|
+
for (let i = 0; i < pending.length; i += 16) {
|
|
2007
|
+
const batch = pending.slice(i, i + 16);
|
|
2008
|
+
tally.embedded += batch.length;
|
|
2009
|
+
let vectors;
|
|
2010
|
+
try {
|
|
2011
|
+
vectors = await embedder.embedPassages(batch.map((item) => item.body));
|
|
2012
|
+
} catch {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
if (vectors.length !== batch.length || vectors.some((vector) => vector.length !== EMBEDDING_DIM)) {
|
|
2016
|
+
return false;
|
|
2017
|
+
}
|
|
2018
|
+
const tx = db.transaction(() => {
|
|
2019
|
+
for (let j = 0; j < batch.length; j++) {
|
|
2020
|
+
const item = batch[j];
|
|
2021
|
+
const vector = vectors[j];
|
|
2022
|
+
if (!item || !vector) continue;
|
|
2023
|
+
for (const id of item.ids) insertVector(db, item.scope, id, vector, item.hash);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
tx();
|
|
2027
|
+
tally.storedVector = true;
|
|
2028
|
+
}
|
|
2029
|
+
return true;
|
|
2030
|
+
}
|
|
2031
|
+
function queueBody(pending, row) {
|
|
2032
|
+
const found = pending.find((item) => item.hash === row.hash);
|
|
2033
|
+
if (found) found.ids.push(row.id);
|
|
2034
|
+
else pending.push({ hash: row.hash, body: row.body, ids: [row.id], scope: row.scope });
|
|
2035
|
+
}
|
|
2036
|
+
async function embedInserted(db, embedder, inserted, copies, tally) {
|
|
2037
|
+
const pending = [];
|
|
2038
|
+
const tx = db.transaction(() => {
|
|
2039
|
+
for (const row of inserted) {
|
|
2040
|
+
const blob = copies.get(row.hash);
|
|
2041
|
+
if (blob) {
|
|
2042
|
+
insertVector(db, row.scope, row.id, blob, row.hash);
|
|
2043
|
+
tally.storedVector = true;
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
queueBody(pending, row);
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
tx();
|
|
2050
|
+
await embedBodies(db, embedder, pending, tally);
|
|
2051
|
+
}
|
|
2052
|
+
async function backfill(db, embedder, tally) {
|
|
2053
|
+
const rows = db.prepare(
|
|
2054
|
+
`SELECT c.id AS id, c.path AS path, c.body AS body, c.chunk_hash AS hash, c.scope AS scope
|
|
2055
|
+
FROM chunks AS c
|
|
2056
|
+
WHERE NOT EXISTS (
|
|
2057
|
+
SELECT 1 FROM vec_meta AS m
|
|
2058
|
+
WHERE m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
|
|
2059
|
+
)
|
|
2060
|
+
ORDER BY c.path ASC, c.start_line ASC, c.symbol ASC`
|
|
2061
|
+
).all();
|
|
2062
|
+
let current = "";
|
|
2063
|
+
let group = [];
|
|
2064
|
+
const flush = async () => {
|
|
2065
|
+
if (!group.length) return;
|
|
2066
|
+
const pending = group;
|
|
2067
|
+
group = [];
|
|
2068
|
+
await embedBodies(db, embedder, pending, tally);
|
|
2069
|
+
};
|
|
2070
|
+
for (const row of rows) {
|
|
2071
|
+
if (row.path !== current) {
|
|
2072
|
+
await flush();
|
|
2073
|
+
current = row.path;
|
|
2074
|
+
}
|
|
2075
|
+
const blob = readVectorBlob(db, row.hash);
|
|
2076
|
+
if (blob) {
|
|
2077
|
+
insertVector(db, row.scope, row.id, blob, row.hash);
|
|
2078
|
+
tally.storedVector = true;
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
queueBody(group, row);
|
|
2082
|
+
}
|
|
2083
|
+
await flush();
|
|
2084
|
+
}
|
|
2085
|
+
function scopeVectorState(db, scope) {
|
|
2086
|
+
const counts = db.prepare(
|
|
2087
|
+
`SELECT
|
|
2088
|
+
(SELECT COUNT(*) FROM chunks WHERE scope = ?) AS chunks,
|
|
2089
|
+
(SELECT COUNT(*) FROM chunks AS c
|
|
2090
|
+
JOIN vec_meta AS m ON m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
|
|
2091
|
+
WHERE c.scope = ?) AS ready`
|
|
2092
|
+
).get(scope, scope);
|
|
2093
|
+
if (counts.chunks === 0) return "empty";
|
|
2094
|
+
const model = metaString(db, "embedding_model");
|
|
2095
|
+
const modelOk = model === null || model === EMBEDDING_MODEL;
|
|
2096
|
+
if (counts.ready === counts.chunks && modelOk) return "ready";
|
|
2097
|
+
return "partial";
|
|
2098
|
+
}
|
|
2099
|
+
function createBuiltinProvider(projectRoot, config, runtime) {
|
|
2100
|
+
const syncOne = async (db, rel, excluded, embedder, canVec, tally) => {
|
|
2101
|
+
const abs = join5(projectRoot, rel);
|
|
2102
|
+
const previous = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(rel);
|
|
2103
|
+
let bytes = null;
|
|
2104
|
+
if (await pathExists(abs)) {
|
|
2105
|
+
try {
|
|
2106
|
+
bytes = await readFile3(abs);
|
|
2107
|
+
} catch {
|
|
2108
|
+
bytes = null;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
const drop = () => {
|
|
2112
|
+
if (!previous) return "unchanged";
|
|
2113
|
+
deletePath(db, rel);
|
|
2114
|
+
return "deleted";
|
|
2115
|
+
};
|
|
2116
|
+
if (!bytes || excluded(rel)) return drop();
|
|
2117
|
+
const scope = scopeForPath(rel, config);
|
|
2118
|
+
if (!scope) return drop();
|
|
2119
|
+
const hash = createHash2("sha256").update(bytes).digest("hex");
|
|
2120
|
+
if (previous?.content_hash === hash) return "unchanged";
|
|
2121
|
+
const chunks = await chunksFor(bytes, rel, scope);
|
|
2122
|
+
if (!chunks) return drop();
|
|
2123
|
+
if (!chunks.length && !previous) return "unchanged";
|
|
2124
|
+
const language = chunks[0]?.language ?? (scope === "code" ? "js" : "md");
|
|
2125
|
+
const copies = /* @__PURE__ */ new Map();
|
|
2126
|
+
if (canVec && embedder) {
|
|
2127
|
+
const hashes = new Set(chunks.map((chunk) => chunkHash(chunk.body)));
|
|
2128
|
+
for (const bodyHash of hashes) {
|
|
2129
|
+
const blob = readVectorBlob(db, bodyHash);
|
|
2130
|
+
if (blob) copies.set(bodyHash, blob);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
const inserted = db.transaction(() => writeChunks(db, rel, hash, scope, language, chunks))();
|
|
2134
|
+
tally.chunks += inserted.length;
|
|
2135
|
+
if (canVec && embedder && inserted.length) {
|
|
2136
|
+
await embedInserted(db, embedder, inserted, copies, tally);
|
|
2137
|
+
}
|
|
2138
|
+
return "rewritten";
|
|
2139
|
+
};
|
|
2140
|
+
return {
|
|
2141
|
+
id: "builtin",
|
|
2142
|
+
async index(changed) {
|
|
2143
|
+
const rels = changed?.map((entry) => assertRepoRelative(entry));
|
|
2144
|
+
const search = await loadSearchConfig(projectRoot);
|
|
2145
|
+
const embedder = await resolveEmbedder(projectRoot, search.embeddings, runtime);
|
|
2146
|
+
const db = await ensureDb(projectRoot);
|
|
2147
|
+
try {
|
|
2148
|
+
const filter = await loadExcluder(projectRoot, search.exclude);
|
|
2149
|
+
const canVec = await prepareVectors(db, search.embeddings && embedder !== null);
|
|
2150
|
+
const tally = { chunks: 0, embedded: 0, storedVector: false };
|
|
2151
|
+
let considered = 0;
|
|
2152
|
+
let reindexed = 0;
|
|
2153
|
+
const note = (action) => {
|
|
2154
|
+
considered += 1;
|
|
2155
|
+
if (action !== "unchanged") reindexed += 1;
|
|
2156
|
+
};
|
|
2157
|
+
const runSync = (rel) => syncOne(db, rel, filter, embedder, canVec, tally);
|
|
2158
|
+
if (rels) {
|
|
2159
|
+
for (const rel of rels) note(await runSync(rel));
|
|
2160
|
+
} else {
|
|
2161
|
+
const head = await safeHead(projectRoot);
|
|
2162
|
+
const cursor = metaString(db, "indexed_commit");
|
|
2163
|
+
const delta = head && isCommitSha(cursor) ? await readGitDelta(projectRoot, cursor.toLowerCase()) : null;
|
|
2164
|
+
if (delta) {
|
|
2165
|
+
const deletions = new Set(delta.deletions);
|
|
2166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2167
|
+
for (const rel of delta.candidates) {
|
|
2168
|
+
if (deletions.has(rel) || seen.has(rel)) continue;
|
|
2169
|
+
seen.add(rel);
|
|
2170
|
+
note(await runSync(rel));
|
|
2171
|
+
}
|
|
2172
|
+
for (const rel of deletions) {
|
|
2173
|
+
if (deletePath(db, rel)) reindexed += 1;
|
|
2174
|
+
}
|
|
2175
|
+
const stored = db.prepare("SELECT path FROM files").all();
|
|
2176
|
+
for (const row of stored) {
|
|
2177
|
+
const exists = await pathExists(join5(projectRoot, row.path));
|
|
2178
|
+
if (exists && !filter(row.path) && scopeForPath(row.path, config)) continue;
|
|
2179
|
+
deletePath(db, row.path);
|
|
2180
|
+
considered += 1;
|
|
2181
|
+
reindexed += 1;
|
|
2182
|
+
}
|
|
2183
|
+
} else {
|
|
2184
|
+
const desired = /* @__PURE__ */ new Set();
|
|
2185
|
+
for (const rel of await walkFiles(projectRoot)) {
|
|
2186
|
+
if (filter(rel) || !scopeForPath(rel, config)) continue;
|
|
2187
|
+
desired.add(rel);
|
|
2188
|
+
note(await runSync(rel));
|
|
2189
|
+
}
|
|
2190
|
+
const stored = db.prepare("SELECT path FROM files").all();
|
|
2191
|
+
const tx = db.transaction(() => {
|
|
2192
|
+
for (const row of stored) {
|
|
2193
|
+
if (desired.has(row.path)) continue;
|
|
2194
|
+
deletePath(db, row.path);
|
|
2195
|
+
reindexed += 1;
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
tx();
|
|
2199
|
+
}
|
|
2200
|
+
if (search.embeddings && embedder && canVec) await backfill(db, embedder, tally);
|
|
2201
|
+
if (head) writeMetaValue(db, "indexed_commit", head);
|
|
2202
|
+
}
|
|
2203
|
+
writeCounts(db, considered, reindexed, tally.chunks, tally.embedded);
|
|
2204
|
+
if (tally.storedVector) writeMetaValue(db, "embedding_model", EMBEDDING_MODEL);
|
|
2205
|
+
return readStats(db);
|
|
2206
|
+
} finally {
|
|
2207
|
+
db.close();
|
|
2208
|
+
}
|
|
2209
|
+
},
|
|
2210
|
+
async search(query, options) {
|
|
2211
|
+
return searchProject(projectRoot, query, options, runtime);
|
|
2212
|
+
},
|
|
2213
|
+
async stats() {
|
|
2214
|
+
if (!await pathExists(indexDbPath(projectRoot))) return { ...EMPTY_STATS };
|
|
2215
|
+
const db = openDb(projectRoot);
|
|
2216
|
+
try {
|
|
2217
|
+
return readStats(db);
|
|
2218
|
+
} finally {
|
|
2219
|
+
db.close();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
async function keywordHits(projectRoot, query, scope, k) {
|
|
2225
|
+
if (!await pathExists(indexDbPath(projectRoot))) return [];
|
|
2226
|
+
const db = openDb(projectRoot);
|
|
2227
|
+
try {
|
|
2228
|
+
return queryRanked(db, query, scope, k);
|
|
2229
|
+
} finally {
|
|
2230
|
+
db.close();
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async function listScopeHits(projectRoot, scope) {
|
|
2234
|
+
if (!await pathExists(indexDbPath(projectRoot))) return [];
|
|
2235
|
+
const db = openDb(projectRoot);
|
|
2236
|
+
try {
|
|
2237
|
+
return queryScope(db, scope);
|
|
2238
|
+
} finally {
|
|
2239
|
+
db.close();
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
async function rankScope(projectRoot, query, scope, k, session) {
|
|
2243
|
+
const keyword = async () => keywordHits(projectRoot, query, scope, k);
|
|
2244
|
+
if (!session.embeddings) return { hits: await keyword(), warning: false, hybrid: false };
|
|
2245
|
+
if (!await pathExists(indexDbPath(projectRoot))) {
|
|
2246
|
+
return { hits: [], warning: session.failure === "query", hybrid: false };
|
|
2247
|
+
}
|
|
2248
|
+
const db = openDb(projectRoot);
|
|
2249
|
+
try {
|
|
2250
|
+
const state2 = scopeVectorState(db, scope);
|
|
2251
|
+
const warn = session.failure === "query" || state2 !== "empty" && (session.failure === "load" || state2 === "partial" || !session.queryVec);
|
|
2252
|
+
if (session.failure !== "none" || state2 !== "ready" || !session.queryVec) {
|
|
2253
|
+
return { hits: queryRanked(db, query, scope, k), warning: warn, hybrid: false };
|
|
2254
|
+
}
|
|
2255
|
+
const loaded = await ensureVecLoaded(db);
|
|
2256
|
+
if (!loaded) return { hits: queryRanked(db, query, scope, k), warning: true, hybrid: false };
|
|
2257
|
+
try {
|
|
2258
|
+
const keywordRows = queryRanked(db, query, scope, k);
|
|
2259
|
+
const vectorRows = queryVectors(db, scope, session.queryVec, k).map(
|
|
2260
|
+
(row) => mapHit({ ...row, score: row.distance })
|
|
2261
|
+
);
|
|
2262
|
+
return { hits: fuseHits(keywordRows, vectorRows), warning: false, hybrid: true };
|
|
2263
|
+
} catch {
|
|
2264
|
+
return { hits: queryRanked(db, query, scope, k), warning: true, hybrid: false };
|
|
2265
|
+
}
|
|
2266
|
+
} finally {
|
|
2267
|
+
db.close();
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async function searchProject(projectRoot, query, options, runtime) {
|
|
2271
|
+
const search = await loadSearchConfig(projectRoot);
|
|
2272
|
+
const config = await loadConfig(projectRoot);
|
|
2273
|
+
const activeChange = await resolveActiveDirectory(projectRoot, config);
|
|
2274
|
+
const request = {
|
|
2275
|
+
projectRoot,
|
|
2276
|
+
query,
|
|
2277
|
+
channel: "search",
|
|
2278
|
+
scope: options.scope,
|
|
2279
|
+
k: options.k,
|
|
2280
|
+
budget: options.budget,
|
|
2281
|
+
embeddings: search.embeddings,
|
|
2282
|
+
activeChange,
|
|
2283
|
+
scopes: [options.scope]
|
|
2284
|
+
};
|
|
2285
|
+
try {
|
|
2286
|
+
const cached = readPackedCache(request);
|
|
2287
|
+
if (cached) {
|
|
2288
|
+
return {
|
|
2289
|
+
provider: "builtin",
|
|
2290
|
+
warning: cached.warning,
|
|
2291
|
+
query,
|
|
2292
|
+
scope: options.scope,
|
|
2293
|
+
budget: options.budget,
|
|
2294
|
+
usedTokens: countTokens(cached.text),
|
|
2295
|
+
hits: cached.hits,
|
|
2296
|
+
text: cached.text
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
} catch {
|
|
2300
|
+
}
|
|
2301
|
+
const embedder = await resolveEmbedder(projectRoot, search.embeddings, runtime);
|
|
2302
|
+
const embedded = await embedSearchQuery(embedder, search.embeddings, query);
|
|
2303
|
+
const ranked = await rankScope(projectRoot, query, options.scope, options.k, {
|
|
2304
|
+
embeddings: search.embeddings,
|
|
2305
|
+
queryVec: embedded.queryVec,
|
|
2306
|
+
failure: embedded.failure
|
|
2307
|
+
});
|
|
2308
|
+
const preamble = options.preamble ?? searchPreamble(query, options.scope, options.budget, ranked.hybrid);
|
|
2309
|
+
let hits = ranked.hits;
|
|
2310
|
+
try {
|
|
2311
|
+
hits = attachSummaries(projectRoot, ranked.hits, activeChange);
|
|
2312
|
+
} catch {
|
|
2313
|
+
hits = ranked.hits;
|
|
2314
|
+
}
|
|
2315
|
+
const packed = packHits({ preamble, hits, budget: options.budget });
|
|
2316
|
+
const mode = ranked.hits.some((hit) => hit.rankedBy === "rrf") ? "rrf" : "bm25";
|
|
2317
|
+
try {
|
|
2318
|
+
writePackedCache(request, packed, ranked.hits, mode, ranked.hybrid ? options.scope : "");
|
|
2319
|
+
} catch {
|
|
2320
|
+
}
|
|
2321
|
+
return {
|
|
2322
|
+
provider: "builtin",
|
|
2323
|
+
warning: ranked.warning ? EMBEDDINGS_UNAVAILABLE : void 0,
|
|
2324
|
+
query,
|
|
2325
|
+
scope: options.scope,
|
|
2326
|
+
budget: options.budget,
|
|
2327
|
+
usedTokens: packed.usedTokens,
|
|
2328
|
+
hits: packed.hits,
|
|
2329
|
+
text: packed.text
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/search/registry.ts
|
|
2334
|
+
var extra = /* @__PURE__ */ new Map();
|
|
2335
|
+
function registerSearchProvider(provider) {
|
|
2336
|
+
extra.set(provider.id, provider);
|
|
2337
|
+
}
|
|
2338
|
+
function clearRegisteredSearchProviders() {
|
|
2339
|
+
extra.clear();
|
|
2340
|
+
}
|
|
2341
|
+
function fallbackWarning(name) {
|
|
2342
|
+
return `Search provider "${name}" is unavailable. Using built-in keyword search.`;
|
|
2343
|
+
}
|
|
2344
|
+
function joinWarning(left, right) {
|
|
2345
|
+
if (left && right) return `${left}
|
|
2346
|
+
${right}`;
|
|
2347
|
+
return left || right;
|
|
2348
|
+
}
|
|
2349
|
+
function resolveSearchProvider(projectRoot, config, search, runtime) {
|
|
2350
|
+
const builtin = createBuiltinProvider(projectRoot, config, runtime);
|
|
2351
|
+
const name = search.provider || "builtin";
|
|
2352
|
+
if (name === "builtin") return builtin;
|
|
2353
|
+
const registered = extra.get(name);
|
|
2354
|
+
let active = registered ?? builtin;
|
|
2355
|
+
let warning = registered ? void 0 : fallbackWarning(name);
|
|
2356
|
+
const mark = () => {
|
|
2357
|
+
warning = fallbackWarning(name);
|
|
2358
|
+
active = builtin;
|
|
2359
|
+
};
|
|
2360
|
+
return {
|
|
2361
|
+
id: active.id,
|
|
2362
|
+
async index(changed) {
|
|
2363
|
+
try {
|
|
2364
|
+
return await active.index(changed);
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
if (active.id === "builtin") throw err;
|
|
2367
|
+
mark();
|
|
2368
|
+
return builtin.index(changed);
|
|
2369
|
+
}
|
|
2370
|
+
},
|
|
2371
|
+
async search(query, options) {
|
|
2372
|
+
try {
|
|
2373
|
+
const result = await active.search(query, options);
|
|
2374
|
+
result.warning = joinWarning(warning, result.warning);
|
|
2375
|
+
result.provider = "builtin";
|
|
2376
|
+
return result;
|
|
2377
|
+
} catch (err) {
|
|
2378
|
+
if (active.id === "builtin") throw err;
|
|
2379
|
+
mark();
|
|
2380
|
+
await builtin.index();
|
|
2381
|
+
const result = await builtin.search(query, options);
|
|
2382
|
+
result.warning = joinWarning(warning, result.warning);
|
|
2383
|
+
result.provider = "builtin";
|
|
2384
|
+
return result;
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
stats() {
|
|
2388
|
+
return active.stats();
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function safeMessage(err, projectRoot) {
|
|
2393
|
+
const raw = err instanceof Error ? err.message : "Search failed.";
|
|
2394
|
+
if (!raw) return "Search failed.";
|
|
2395
|
+
if (!projectRoot || !raw.includes(projectRoot)) return raw;
|
|
2396
|
+
return raw.split(projectRoot).join("") || "Search failed.";
|
|
2397
|
+
}
|
|
2398
|
+
async function buildRetrievedInner(projectRoot, config, title, stage, budget, runtime, activeChange = "") {
|
|
2399
|
+
let search;
|
|
2400
|
+
try {
|
|
2401
|
+
search = await loadSearchConfig(projectRoot);
|
|
2402
|
+
} catch (err) {
|
|
2403
|
+
return `Retrieved context skipped: ${safeMessage(err, projectRoot)}.`;
|
|
2404
|
+
}
|
|
2405
|
+
const limit = budget ?? budgetForStage(search, stage);
|
|
2406
|
+
const provider = resolveSearchProvider(projectRoot, config, search, runtime);
|
|
2407
|
+
try {
|
|
2408
|
+
await provider.index();
|
|
2409
|
+
} catch (err) {
|
|
2410
|
+
const message = provider.id === "builtin" ? safeMessage(err, projectRoot) : fallbackWarning(search.provider);
|
|
2411
|
+
return `Retrieved context skipped: ${message}.`;
|
|
2412
|
+
}
|
|
2413
|
+
const request = {
|
|
2414
|
+
projectRoot,
|
|
2415
|
+
query: title,
|
|
2416
|
+
channel: "handoff",
|
|
2417
|
+
scope: "merged",
|
|
2418
|
+
k: search.k,
|
|
2419
|
+
budget: limit,
|
|
2420
|
+
embeddings: search.embeddings,
|
|
2421
|
+
activeChange,
|
|
2422
|
+
scopes: ["specs", "memory", "code"]
|
|
2423
|
+
};
|
|
2424
|
+
try {
|
|
2425
|
+
const cached = readPackedCache(request);
|
|
2426
|
+
if (cached) return cached.text;
|
|
2427
|
+
} catch {
|
|
2428
|
+
}
|
|
2429
|
+
try {
|
|
2430
|
+
const embedder = await resolveEmbedder(projectRoot, search.embeddings, runtime);
|
|
2431
|
+
const embedded = await embedSearchQuery(embedder, search.embeddings, title);
|
|
2432
|
+
const hits = [];
|
|
2433
|
+
const hybridScopes = [];
|
|
2434
|
+
for (const scope of ["specs", "memory", "code"]) {
|
|
2435
|
+
const ranked = await rankScope(projectRoot, title, scope, search.k, {
|
|
2436
|
+
embeddings: search.embeddings,
|
|
2437
|
+
queryVec: embedded.queryVec,
|
|
2438
|
+
failure: embedded.failure
|
|
2439
|
+
});
|
|
2440
|
+
if (ranked.hybrid) hybridScopes.push(scope);
|
|
2441
|
+
hits.push(...ranked.hits);
|
|
2442
|
+
}
|
|
2443
|
+
const merged = mergeRankedHits(hits);
|
|
2444
|
+
let prepared = merged;
|
|
2445
|
+
try {
|
|
2446
|
+
prepared = attachSummaries(projectRoot, merged, activeChange);
|
|
2447
|
+
} catch {
|
|
2448
|
+
prepared = merged;
|
|
2449
|
+
}
|
|
2450
|
+
const packed = packHits({ preamble: "", hits: prepared, budget: limit });
|
|
2451
|
+
const mode = merged.some((hit) => hit.rankedBy === "rrf") ? "rrf" : "bm25";
|
|
2452
|
+
try {
|
|
2453
|
+
writePackedCache(request, packed, merged, mode, hybridScopes.join(","));
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
return packed.text;
|
|
2457
|
+
} catch (err) {
|
|
2458
|
+
const message = search.provider === "builtin" ? safeMessage(err, projectRoot) : fallbackWarning(search.provider);
|
|
2459
|
+
return `Retrieved context skipped: ${message}.`;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
async function executeSearch(projectRoot, config, query, scope, budget) {
|
|
2463
|
+
const search = await loadSearchConfig(projectRoot);
|
|
2464
|
+
const provider = resolveSearchProvider(projectRoot, config, search);
|
|
2465
|
+
await provider.index();
|
|
2466
|
+
const options = {
|
|
2467
|
+
scope,
|
|
2468
|
+
k: search.k,
|
|
2469
|
+
budget: budget ?? search.budgets.default
|
|
2470
|
+
};
|
|
2471
|
+
return provider.search(query, options);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/role/models/schema.ts
|
|
2475
|
+
import { join as join6 } from "pathe";
|
|
2476
|
+
import { parse as parseYaml } from "yaml";
|
|
2477
|
+
import { z as z2 } from "zod";
|
|
2478
|
+
var idSchema = z2.string().trim().min(1);
|
|
2479
|
+
var envName = z2.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
2480
|
+
var confidence = z2.number().gte(0).lte(1);
|
|
2481
|
+
var WorkTypeSchema = z2.enum(WORK_TYPES);
|
|
2482
|
+
var workSlots = z2.record(WorkTypeSchema, idSchema);
|
|
2483
|
+
var ModelEntrySchema = z2.discriminatedUnion("provider", [
|
|
2484
|
+
z2.object({
|
|
2485
|
+
kind: z2.literal("decide"),
|
|
2486
|
+
provider: z2.literal("rules")
|
|
2487
|
+
}).strict(),
|
|
2488
|
+
z2.object({
|
|
2489
|
+
kind: z2.enum(["decide", "reason"]),
|
|
2490
|
+
provider: z2.literal("command"),
|
|
2491
|
+
command_env: envName,
|
|
2492
|
+
min_confidence: confidence.optional()
|
|
2493
|
+
}).strict(),
|
|
2494
|
+
z2.object({
|
|
2495
|
+
kind: z2.literal("decide"),
|
|
2496
|
+
provider: z2.literal("ollama"),
|
|
2497
|
+
model: z2.string().trim().min(1),
|
|
2498
|
+
base_url_env: envName,
|
|
2499
|
+
min_confidence: confidence.optional()
|
|
2500
|
+
}).strict()
|
|
2501
|
+
]);
|
|
2502
|
+
var ModelsConfigSchema = z2.object({
|
|
2503
|
+
version: z2.literal(1),
|
|
2504
|
+
models: z2.record(idSchema, ModelEntrySchema),
|
|
2505
|
+
defaults: z2.object({
|
|
2506
|
+
by_work_type: workSlots,
|
|
2507
|
+
fallback_reason: idSchema,
|
|
2508
|
+
fallback_decide: idSchema
|
|
2509
|
+
}).strict(),
|
|
2510
|
+
overrides: z2.object({
|
|
2511
|
+
bots: z2.record(idSchema, workSlots).optional(),
|
|
2512
|
+
stages: z2.record(idSchema, workSlots).optional(),
|
|
2513
|
+
roles: z2.record(idSchema, workSlots).optional()
|
|
2514
|
+
}).strict().optional()
|
|
2515
|
+
}).strict().superRefine((config, ctx) => {
|
|
2516
|
+
if (Object.keys(config.models).length === 0) {
|
|
2517
|
+
ctx.addIssue({ code: "custom", path: ["models"], message: "needs a model" });
|
|
2518
|
+
}
|
|
2519
|
+
expectSlot(ctx, config.models, config.defaults.fallback_reason, "reason", [
|
|
2520
|
+
"defaults",
|
|
2521
|
+
"fallback_reason"
|
|
2522
|
+
]);
|
|
2523
|
+
expectSlot(ctx, config.models, config.defaults.fallback_decide, "decide", [
|
|
2524
|
+
"defaults",
|
|
2525
|
+
"fallback_decide"
|
|
2526
|
+
]);
|
|
2527
|
+
expectMap(ctx, config.models, config.defaults.by_work_type, ["defaults", "by_work_type"]);
|
|
2528
|
+
expectNamed(ctx, config.models, config.overrides?.bots, ["overrides", "bots"]);
|
|
2529
|
+
expectNamed(ctx, config.models, config.overrides?.stages, ["overrides", "stages"]);
|
|
2530
|
+
expectNamed(ctx, config.models, config.overrides?.roles, ["overrides", "roles"]);
|
|
2531
|
+
});
|
|
2532
|
+
var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2533
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S+$/;
|
|
2534
|
+
function expectSlot(ctx, models, id, kind, path) {
|
|
2535
|
+
if (!id) return;
|
|
2536
|
+
const model = models[id];
|
|
2537
|
+
if (!model) {
|
|
2538
|
+
ctx.addIssue({ code: "custom", path, message: "unknown model" });
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
if (model.kind !== kind) {
|
|
2542
|
+
ctx.addIssue({ code: "custom", path, message: `kind must be ${kind}` });
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
function expectMap(ctx, models, map, path) {
|
|
2546
|
+
if (!map) return;
|
|
2547
|
+
for (const [work, id] of Object.entries(map)) {
|
|
2548
|
+
if (!isWorkType(work)) continue;
|
|
2549
|
+
expectSlot(ctx, models, id, workKind(work), [...path, work]);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function expectNamed(ctx, models, named, path) {
|
|
2553
|
+
if (!named) return;
|
|
2554
|
+
for (const [name, map] of Object.entries(named)) {
|
|
2555
|
+
expectMap(ctx, models, map, [...path, name]);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
function formatModelsError(error) {
|
|
2559
|
+
const issue = error.issues[0];
|
|
2560
|
+
const path = issue?.path.join(".") || "models";
|
|
2561
|
+
const message = issue?.message ?? "invalid";
|
|
2562
|
+
return `models.yaml is invalid: ${path} ${message}`;
|
|
2563
|
+
}
|
|
2564
|
+
function looksSecret(value) {
|
|
2565
|
+
if (value.includes("-----BEGIN")) return true;
|
|
2566
|
+
if (!/\s/.test(value) && ASSIGNMENT.test(value)) return true;
|
|
2567
|
+
if (!/\s/.test(value) && value.length >= 32 && !ENV_NAME.test(value)) return true;
|
|
2568
|
+
return false;
|
|
2569
|
+
}
|
|
2570
|
+
function containsSecret(value) {
|
|
2571
|
+
if (typeof value === "string") return looksSecret(value);
|
|
2572
|
+
if (Array.isArray(value)) return value.some(containsSecret);
|
|
2573
|
+
if (value && typeof value === "object") {
|
|
2574
|
+
return Object.entries(value).some(([key, entry]) => {
|
|
2575
|
+
if (key === "model" && typeof entry === "string") {
|
|
2576
|
+
if (entry.includes("-----BEGIN")) return true;
|
|
2577
|
+
if (!/\s/.test(entry) && ASSIGNMENT.test(entry)) return true;
|
|
2578
|
+
return false;
|
|
2579
|
+
}
|
|
2580
|
+
return containsSecret(entry);
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2585
|
+
function modelsPath(root) {
|
|
2586
|
+
return join6(root, ".sdd", "models.yaml");
|
|
2587
|
+
}
|
|
2588
|
+
async function loadModels(root) {
|
|
2589
|
+
const path = modelsPath(root);
|
|
2590
|
+
if (!await pathExists(path)) return null;
|
|
2591
|
+
let raw;
|
|
2592
|
+
try {
|
|
2593
|
+
raw = parseYaml(await readText(path));
|
|
2594
|
+
} catch {
|
|
2595
|
+
return { error: "models.yaml is invalid: models could not be parsed" };
|
|
2596
|
+
}
|
|
2597
|
+
if (containsSecret(raw)) return { error: "models.yaml has a secret value." };
|
|
2598
|
+
const parsed = ModelsConfigSchema.safeParse(raw);
|
|
2599
|
+
if (!parsed.success) return { error: formatModelsError(parsed.error) };
|
|
2600
|
+
return { config: parsed.data };
|
|
2601
|
+
}
|
|
2602
|
+
async function checkModels(root) {
|
|
2603
|
+
const loaded = await loadModels(root);
|
|
2604
|
+
if (loaded === null) return { ok: true };
|
|
2605
|
+
if ("error" in loaded) return { ok: false, message: loaded.error };
|
|
2606
|
+
return { ok: true };
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/role/profile.ts
|
|
2610
|
+
import { readFile as readFile4, writeFile } from "fs/promises";
|
|
2611
|
+
import { join as join7 } from "pathe";
|
|
2612
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
2613
|
+
var NAME_LIMIT = 80;
|
|
2614
|
+
function canonicalRoleName(input) {
|
|
2615
|
+
const name = input.trim();
|
|
2616
|
+
if (!name || name === "." || name === "..") return null;
|
|
2617
|
+
if (name.length > NAME_LIMIT) return null;
|
|
2618
|
+
if (name.includes("/") || name.includes("\\") || name.includes("\0")) return null;
|
|
2619
|
+
return name;
|
|
2620
|
+
}
|
|
2621
|
+
function roleFilePath(root, name) {
|
|
2622
|
+
return join7(root, ".sdd", "roles", `${name}.md`);
|
|
2623
|
+
}
|
|
2624
|
+
function conversationDir(root, name) {
|
|
2625
|
+
return join7(root, ".sdd", "role-runs", "conversation", name);
|
|
2626
|
+
}
|
|
2627
|
+
function memoryDir(root, name, team = false) {
|
|
2628
|
+
if (team) return join7(root, ".sdd", "agents", name);
|
|
2629
|
+
return join7(root, ".sdd", "role-runs", "memory", name);
|
|
2630
|
+
}
|
|
2631
|
+
function allowFilePath(root) {
|
|
2632
|
+
return join7(root, ".sdd", "role-runs", "allow.json");
|
|
2633
|
+
}
|
|
2634
|
+
function ok(stdout = "") {
|
|
2635
|
+
return { exitCode: 0, stdout };
|
|
2636
|
+
}
|
|
2637
|
+
function fail(stdout) {
|
|
2638
|
+
return { exitCode: 1, stdout };
|
|
2639
|
+
}
|
|
2640
|
+
function asText(value) {
|
|
2641
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2642
|
+
}
|
|
2643
|
+
var LIST_FIELDS = [
|
|
2644
|
+
["owns", "owns"],
|
|
2645
|
+
["inputs", "inputs"],
|
|
2646
|
+
["outputs", "outputs"],
|
|
2647
|
+
["collaborates", "collaborates"],
|
|
2648
|
+
["reviews", "reviews"],
|
|
2649
|
+
["approvers", "approvers"],
|
|
2650
|
+
["never", "never"],
|
|
2651
|
+
["askHumanWhen", "ask_human_when"],
|
|
2652
|
+
["skills", "skills"],
|
|
2653
|
+
["rules", "rules"],
|
|
2654
|
+
["goals", "goals"]
|
|
2655
|
+
];
|
|
2656
|
+
function stringList(value) {
|
|
2657
|
+
if (value === void 0) return [];
|
|
2658
|
+
if (!Array.isArray(value)) return null;
|
|
2659
|
+
const out = [];
|
|
2660
|
+
for (const item of value) {
|
|
2661
|
+
if (typeof item !== "string" || !item.trim()) return null;
|
|
2662
|
+
out.push(item.trim());
|
|
2663
|
+
}
|
|
2664
|
+
return out;
|
|
2665
|
+
}
|
|
2666
|
+
function parseRoleDocument(name, raw) {
|
|
2667
|
+
const text = raw.replace(/^\uFEFF/, "");
|
|
2668
|
+
const nl = text.indexOf("\n");
|
|
2669
|
+
if (nl < 0 || text.slice(0, nl).trim() !== "---") return null;
|
|
2670
|
+
const rest = text.slice(nl + 1);
|
|
2671
|
+
const close = /\r?\n---\s*(?:\r?\n|$)/.exec(rest);
|
|
2672
|
+
if (!close) return null;
|
|
2673
|
+
let data;
|
|
2674
|
+
try {
|
|
2675
|
+
data = parseYaml2(rest.slice(0, close.index));
|
|
2676
|
+
} catch {
|
|
2677
|
+
return null;
|
|
2678
|
+
}
|
|
2679
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
2680
|
+
const rec = data;
|
|
2681
|
+
const lists = emptyRoleLists();
|
|
2682
|
+
for (const [field2, key] of LIST_FIELDS) {
|
|
2683
|
+
const parsed = stringList(rec[key]);
|
|
2684
|
+
if (!parsed) return null;
|
|
2685
|
+
lists[field2] = parsed;
|
|
2686
|
+
}
|
|
2687
|
+
const body = rest.slice(close.index + close[0].length).trim();
|
|
2688
|
+
return {
|
|
2689
|
+
name,
|
|
2690
|
+
label: asText(rec.label),
|
|
2691
|
+
job: asText(rec.job),
|
|
2692
|
+
description: body,
|
|
2693
|
+
askFirst: rec.ask_first === true,
|
|
2694
|
+
pinned: rec.pinned === true,
|
|
2695
|
+
hidden: rec.hidden === true,
|
|
2696
|
+
...lists
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
function renderRole(role) {
|
|
2700
|
+
const record = {
|
|
2701
|
+
name: role.name,
|
|
2702
|
+
label: role.label,
|
|
2703
|
+
job: role.job,
|
|
2704
|
+
ask_first: role.askFirst,
|
|
2705
|
+
pinned: role.pinned,
|
|
2706
|
+
hidden: role.hidden
|
|
2707
|
+
};
|
|
2708
|
+
for (const [field2, key] of LIST_FIELDS) {
|
|
2709
|
+
if (role[field2].length > 0) record[key] = role[field2];
|
|
2710
|
+
}
|
|
2711
|
+
const front = stringifyYaml(record, { lineWidth: 100 }).trimEnd();
|
|
2712
|
+
const description = role.description.trim();
|
|
2713
|
+
if (!description) return `---
|
|
2714
|
+
${front}
|
|
2715
|
+
---
|
|
2716
|
+
`;
|
|
2717
|
+
return `---
|
|
2718
|
+
${front}
|
|
2719
|
+
---
|
|
2720
|
+
${description}
|
|
2721
|
+
`;
|
|
2722
|
+
}
|
|
2723
|
+
async function readRole(root, name) {
|
|
2724
|
+
const canonical = canonicalRoleName(name);
|
|
2725
|
+
if (!canonical) return null;
|
|
2726
|
+
const path = roleFilePath(root, canonical);
|
|
2727
|
+
if (!await pathExists(path)) return null;
|
|
2728
|
+
try {
|
|
2729
|
+
return parseRoleDocument(canonical, await readText(path));
|
|
2730
|
+
} catch {
|
|
2731
|
+
return null;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
async function writeRole(root, role) {
|
|
2735
|
+
await writeText(roleFilePath(root, role.name), renderRole(role));
|
|
2736
|
+
}
|
|
2737
|
+
async function createRole(root, input) {
|
|
2738
|
+
const name = canonicalRoleName(input.name);
|
|
2739
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2740
|
+
const job = input.job.trim();
|
|
2741
|
+
if (!job) return fail(ROLE_TEXT.needsJob);
|
|
2742
|
+
if (await pathExists(roleFilePath(root, name))) return fail(ROLE_TEXT.exists);
|
|
2743
|
+
const lists = emptyRoleLists();
|
|
2744
|
+
for (const [field2] of LIST_FIELDS) {
|
|
2745
|
+
lists[field2] = input[field2] ?? [];
|
|
2746
|
+
}
|
|
2747
|
+
await writeRole(root, {
|
|
2748
|
+
name,
|
|
2749
|
+
label: input.label.trim(),
|
|
2750
|
+
job,
|
|
2751
|
+
description: (input.description ?? "").trim(),
|
|
2752
|
+
askFirst: input.askFirst === true,
|
|
2753
|
+
pinned: false,
|
|
2754
|
+
hidden: false,
|
|
2755
|
+
...lists
|
|
2756
|
+
});
|
|
2757
|
+
return ok();
|
|
2758
|
+
}
|
|
2759
|
+
async function editRole(root, nameInput, patch) {
|
|
2760
|
+
const name = canonicalRoleName(nameInput);
|
|
2761
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2762
|
+
const role = await readRole(root, name);
|
|
2763
|
+
if (!role) return fail(ROLE_TEXT.notFound);
|
|
2764
|
+
if (patch.job !== void 0 && !patch.job.trim()) return fail(ROLE_TEXT.needsJob);
|
|
2765
|
+
if (patch.label !== void 0) role.label = patch.label.trim();
|
|
2766
|
+
if (patch.job !== void 0) role.job = patch.job.trim();
|
|
2767
|
+
if (patch.description !== void 0) role.description = patch.description.trim();
|
|
2768
|
+
if (patch.askFirst !== void 0) role.askFirst = patch.askFirst;
|
|
2769
|
+
await writeRole(root, role);
|
|
2770
|
+
return ok();
|
|
2771
|
+
}
|
|
2772
|
+
async function setFlag(root, nameInput, flag) {
|
|
2773
|
+
const name = canonicalRoleName(nameInput);
|
|
2774
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2775
|
+
const role = await readRole(root, name);
|
|
2776
|
+
if (!role) return fail(ROLE_TEXT.notFound);
|
|
2777
|
+
role[flag] = true;
|
|
2778
|
+
await writeRole(root, role);
|
|
2779
|
+
return ok();
|
|
2780
|
+
}
|
|
2781
|
+
function pinRole(root, name) {
|
|
2782
|
+
return setFlag(root, name, "pinned");
|
|
2783
|
+
}
|
|
2784
|
+
function hideRole(root, name) {
|
|
2785
|
+
return setFlag(root, name, "hidden");
|
|
2786
|
+
}
|
|
2787
|
+
async function showRoles(root, opts = {}) {
|
|
2788
|
+
if (opts.name !== void 0) {
|
|
2789
|
+
const name = canonicalRoleName(opts.name);
|
|
2790
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2791
|
+
const path = roleFilePath(root, name);
|
|
2792
|
+
if (!await pathExists(path) || !await readRole(root, name)) return fail(ROLE_TEXT.notFound);
|
|
2793
|
+
return ok((await readText(path)).replace(/\s*$/, ""));
|
|
2794
|
+
}
|
|
2795
|
+
const dir = join7(root, ".sdd", "roles");
|
|
2796
|
+
if (!await pathExists(dir)) return ok();
|
|
2797
|
+
const roles = [];
|
|
2798
|
+
for (const file of await listFiles(dir)) {
|
|
2799
|
+
if (!file.endsWith(".md")) continue;
|
|
2800
|
+
const role = await readRole(root, file.slice(0, -".md".length));
|
|
2801
|
+
if (!role) continue;
|
|
2802
|
+
if (role.hidden && !opts.all) continue;
|
|
2803
|
+
roles.push(role);
|
|
2804
|
+
}
|
|
2805
|
+
roles.sort((a, b) => {
|
|
2806
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
2807
|
+
if (a.name < b.name) return -1;
|
|
2808
|
+
if (a.name > b.name) return 1;
|
|
2809
|
+
return 0;
|
|
2810
|
+
});
|
|
2811
|
+
return ok(roles.map((role) => `${role.name} ${role.label}`).join("\n"));
|
|
2812
|
+
}
|
|
2813
|
+
async function duplicateRole(root, nameInput) {
|
|
2814
|
+
const name = canonicalRoleName(nameInput);
|
|
2815
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2816
|
+
const source = await readRole(root, name);
|
|
2817
|
+
if (!source) return fail(ROLE_TEXT.notFound);
|
|
2818
|
+
const copyName = canonicalRoleName(`${name} copy`);
|
|
2819
|
+
if (!copyName) return fail(ROLE_TEXT.badName);
|
|
2820
|
+
if (await pathExists(roleFilePath(root, copyName))) return fail(ROLE_TEXT.exists);
|
|
2821
|
+
await writeRole(root, { ...source, name: copyName });
|
|
2822
|
+
return ok();
|
|
2823
|
+
}
|
|
2824
|
+
async function deleteRole(root, nameInput) {
|
|
2825
|
+
const name = canonicalRoleName(nameInput);
|
|
2826
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2827
|
+
const path = roleFilePath(root, name);
|
|
2828
|
+
if (!await pathExists(path)) return fail(ROLE_TEXT.notFound);
|
|
2829
|
+
await removePath(path);
|
|
2830
|
+
await removePath(conversationDir(root, name));
|
|
2831
|
+
await removeBotBrain(root, name);
|
|
2832
|
+
await removePath(memoryDir(root, name));
|
|
2833
|
+
await removeAllowKey(root, name);
|
|
2834
|
+
return ok();
|
|
2835
|
+
}
|
|
2836
|
+
async function removeBotBrain(root, roleName) {
|
|
2837
|
+
const teamPath = join7(root, ".sdd", "team.yaml");
|
|
2838
|
+
if (!await pathExists(teamPath)) return;
|
|
2839
|
+
let raw;
|
|
2840
|
+
try {
|
|
2841
|
+
raw = parseYaml2(await readText(teamPath));
|
|
2842
|
+
} catch {
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
2846
|
+
const bots = raw.bots;
|
|
2847
|
+
if (!Array.isArray(bots)) return;
|
|
2848
|
+
for (const bot of bots) {
|
|
2849
|
+
if (!bot || typeof bot !== "object" || Array.isArray(bot)) continue;
|
|
2850
|
+
const rec = bot;
|
|
2851
|
+
const botName = typeof rec.name === "string" ? rec.name : "";
|
|
2852
|
+
const botRole = typeof rec.role === "string" ? rec.role : "";
|
|
2853
|
+
if (botName !== roleName && botRole !== roleName) continue;
|
|
2854
|
+
if (!canonicalRoleName(botName)) continue;
|
|
2855
|
+
const dir = memoryDir(root, botName, true);
|
|
2856
|
+
await removePath(join7(dir, "memory.md"));
|
|
2857
|
+
await removePath(join7(dir, "lessons.jsonl"));
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
async function removeAllowKey(root, name) {
|
|
2861
|
+
const path = allowFilePath(root);
|
|
2862
|
+
if (!await pathExists(path)) return;
|
|
2863
|
+
let data;
|
|
2864
|
+
try {
|
|
2865
|
+
data = JSON.parse(await readText(path));
|
|
2866
|
+
} catch {
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return;
|
|
2870
|
+
const rec = data;
|
|
2871
|
+
if (!(name in rec)) return;
|
|
2872
|
+
delete rec[name];
|
|
2873
|
+
await writeFile(path, `${JSON.stringify(rec, null, 2)}
|
|
2874
|
+
`, "utf8");
|
|
2875
|
+
}
|
|
2876
|
+
async function listConversationRoles(root) {
|
|
2877
|
+
return listDirs(join7(root, ".sdd", "role-runs", "conversation"));
|
|
2878
|
+
}
|
|
2879
|
+
async function readAllowFile(root) {
|
|
2880
|
+
const path = allowFilePath(root);
|
|
2881
|
+
if (!await pathExists(path)) return null;
|
|
2882
|
+
try {
|
|
2883
|
+
const data = JSON.parse(await readFile4(path, "utf8"));
|
|
2884
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
2885
|
+
return data;
|
|
2886
|
+
} catch {
|
|
2887
|
+
return null;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
// src/role/schema.ts
|
|
2892
|
+
import { z as z3 } from "zod";
|
|
2893
|
+
var nameSchema = z3.string().trim().min(1);
|
|
2894
|
+
var botSchema = z3.object({
|
|
2895
|
+
name: nameSchema,
|
|
2896
|
+
role: nameSchema,
|
|
2897
|
+
wip: z3.number().int().positive()
|
|
2898
|
+
}).strict();
|
|
2899
|
+
var humanSchema = z3.object({
|
|
2900
|
+
name: nameSchema
|
|
2901
|
+
}).strict();
|
|
2902
|
+
var TeamSchema = z3.object({
|
|
2903
|
+
template: nameSchema.optional(),
|
|
2904
|
+
branch: z3.string().trim().min(1),
|
|
2905
|
+
rooms: z3.object({
|
|
2906
|
+
commit: z3.boolean()
|
|
2907
|
+
}).strict(),
|
|
2908
|
+
bots: z3.array(botSchema).min(1),
|
|
2909
|
+
humans: z3.array(humanSchema).default([]),
|
|
2910
|
+
role_wip: z3.record(z3.string().trim().min(1), z3.number().int().positive()).optional(),
|
|
2911
|
+
limits: z3.object({
|
|
2912
|
+
loop_max_steps: z3.number().int().positive().optional(),
|
|
2913
|
+
room_rounds: z3.number().int().positive().optional()
|
|
2914
|
+
}).strict().refine((limits) => limits.loop_max_steps !== void 0 || limits.room_rounds !== void 0, {
|
|
2915
|
+
message: "needs a limit"
|
|
2916
|
+
}).optional()
|
|
2917
|
+
}).strict().superRefine((team, ctx) => {
|
|
2918
|
+
if (!team.branch.includes("{bot}")) {
|
|
2919
|
+
ctx.addIssue({
|
|
2920
|
+
code: "custom",
|
|
2921
|
+
path: ["branch"],
|
|
2922
|
+
message: "must include {bot}"
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
const names = /* @__PURE__ */ new Set();
|
|
2926
|
+
for (const [index, bot] of team.bots.entries()) {
|
|
2927
|
+
if (names.has(bot.name)) {
|
|
2928
|
+
ctx.addIssue({
|
|
2929
|
+
code: "custom",
|
|
2930
|
+
path: ["bots", index, "name"],
|
|
2931
|
+
message: "bot name is repeated"
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
names.add(bot.name);
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
function formatTeamError(error) {
|
|
2938
|
+
const issue = error.issues[0];
|
|
2939
|
+
const path = issue?.path.join(".") || "team";
|
|
2940
|
+
const message = issue?.message ?? "invalid";
|
|
2941
|
+
return `team.yaml is invalid: ${path} ${message}`;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// src/role/team.ts
|
|
2945
|
+
import { spawnSync } from "child_process";
|
|
2946
|
+
import { join as join8 } from "pathe";
|
|
2947
|
+
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
2948
|
+
function ok2(stdout = "") {
|
|
2949
|
+
return { exitCode: 0, stdout };
|
|
2950
|
+
}
|
|
2951
|
+
function fail2(stdout) {
|
|
2952
|
+
return { exitCode: 1, stdout };
|
|
2953
|
+
}
|
|
2954
|
+
function gitUserName(root) {
|
|
2955
|
+
const result = spawnSync("git", ["-C", root, "config", "user.name"], { encoding: "utf8" });
|
|
2956
|
+
if (result.status !== 0) return null;
|
|
2957
|
+
const name = result.stdout.trim();
|
|
2958
|
+
return name || null;
|
|
2959
|
+
}
|
|
2960
|
+
async function listTeamTemplates() {
|
|
2961
|
+
return (await listDirs(defaultTeamsDir())).sort();
|
|
2962
|
+
}
|
|
2963
|
+
async function templateRoleFiles(template) {
|
|
2964
|
+
const dir = join8(defaultTeamsDir(), template, "roles");
|
|
2965
|
+
if (!await pathExists(dir)) return null;
|
|
2966
|
+
const files = (await listFiles(dir)).filter((file) => file.endsWith(".md")).sort();
|
|
2967
|
+
const roles = [];
|
|
2968
|
+
for (const file of files) {
|
|
2969
|
+
roles.push({ name: file.slice(0, -".md".length), raw: await readText(join8(dir, file)) });
|
|
2970
|
+
}
|
|
2971
|
+
return roles;
|
|
2972
|
+
}
|
|
2973
|
+
async function listRoleTemplates() {
|
|
2974
|
+
const ids = [];
|
|
2975
|
+
for (const template of await listTeamTemplates()) {
|
|
2976
|
+
const roles = await templateRoleFiles(template);
|
|
2977
|
+
if (!roles) continue;
|
|
2978
|
+
for (const role of roles) ids.push(`${template}/${role.name}`);
|
|
2979
|
+
}
|
|
2980
|
+
return ids;
|
|
2981
|
+
}
|
|
2982
|
+
async function loadTemplateTeam(template) {
|
|
2983
|
+
const path = join8(defaultTeamsDir(), template, "team.yaml");
|
|
2984
|
+
if (!await pathExists(path)) return `Team template "${template}" was not found.`;
|
|
2985
|
+
let raw;
|
|
2986
|
+
try {
|
|
2987
|
+
raw = parseYaml3(await readText(path));
|
|
2988
|
+
} catch {
|
|
2989
|
+
return `Team template "${template}" is invalid.`;
|
|
2990
|
+
}
|
|
2991
|
+
const parsed = TeamSchema.safeParse(raw);
|
|
2992
|
+
if (!parsed.success) return `Team template "${template}" is invalid.`;
|
|
2993
|
+
return parsed.data;
|
|
2994
|
+
}
|
|
2995
|
+
function availableTeams(names) {
|
|
2996
|
+
return names.join(", ") || "(none)";
|
|
2997
|
+
}
|
|
2998
|
+
async function initTeam(root, template, gitUser) {
|
|
2999
|
+
const templates = await listTeamTemplates();
|
|
3000
|
+
if (!template.trim()) {
|
|
3001
|
+
return fail2(`Pass --template. Available: ${availableTeams(templates)}.`);
|
|
3002
|
+
}
|
|
3003
|
+
if (!templates.includes(template)) {
|
|
3004
|
+
return fail2(
|
|
3005
|
+
`Team template "${template}" was not found. Available: ${availableTeams(templates)}.`
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
const loaded = await loadTemplateTeam(template);
|
|
3009
|
+
if (typeof loaded === "string") return fail2(loaded);
|
|
3010
|
+
const roles = await templateRoleFiles(template);
|
|
3011
|
+
if (!roles || roles.length === 0) return fail2(`Team template "${template}" is invalid.`);
|
|
3012
|
+
for (const role of roles) {
|
|
3013
|
+
const parsed = parseRoleDocument(role.name, role.raw);
|
|
3014
|
+
if (!parsed || !parsed.job) return fail2(`Team template "${template}" is invalid.`);
|
|
3015
|
+
}
|
|
3016
|
+
const botRoles = new Set(loaded.bots.map((bot) => bot.role));
|
|
3017
|
+
for (const roleName of botRoles) {
|
|
3018
|
+
if (!roles.some((role) => role.name === roleName)) {
|
|
3019
|
+
return fail2(`Team template "${template}" is invalid.`);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
const teamPath = join8(root, ".sdd", "team.yaml");
|
|
3023
|
+
if (await pathExists(teamPath)) {
|
|
3024
|
+
return fail2("Team config already exists at .sdd/team.yaml. Nothing was written.");
|
|
3025
|
+
}
|
|
3026
|
+
const routingSrc = join8(defaultTeamsDir(), template, "routing.yaml");
|
|
3027
|
+
const routingDest = join8(root, ".sdd", "routing.yaml");
|
|
3028
|
+
const copyRouting = await pathExists(routingSrc);
|
|
3029
|
+
if (copyRouting && await pathExists(routingDest)) {
|
|
3030
|
+
return fail2("Routing config already exists at .sdd/routing.yaml. Nothing was written.");
|
|
3031
|
+
}
|
|
3032
|
+
const existing = [];
|
|
3033
|
+
for (const role of roles) {
|
|
3034
|
+
if (await pathExists(roleFilePath(root, role.name))) existing.push(role.name);
|
|
3035
|
+
}
|
|
3036
|
+
if (existing.length > 0) {
|
|
3037
|
+
return fail2(`Role already exists: ${existing.join(", ")}. Nothing was written.`);
|
|
3038
|
+
}
|
|
3039
|
+
const humans = loaded.humans.length > 0 ? loaded.humans : gitUser ? [{ name: gitUser }] : [];
|
|
3040
|
+
const team = { ...loaded, template, humans };
|
|
3041
|
+
const checked = TeamSchema.safeParse(team);
|
|
3042
|
+
if (!checked.success) return fail2(formatTeamError(checked.error));
|
|
3043
|
+
for (const role of roles) {
|
|
3044
|
+
await writeText(join8(root, ".sdd", "roles", `${role.name}.md`), role.raw);
|
|
3045
|
+
}
|
|
3046
|
+
await writeText(teamPath, stringifyYaml2(checked.data, { lineWidth: 100 }));
|
|
3047
|
+
if (copyRouting) await writeText(routingDest, await readText(routingSrc));
|
|
3048
|
+
await syncRoomIgnore(root, checked.data);
|
|
3049
|
+
return ok2(`Wrote .sdd/team.yaml from ${template}.`);
|
|
3050
|
+
}
|
|
3051
|
+
async function readTeamConfig(root) {
|
|
3052
|
+
const path = join8(root, ".sdd", "team.yaml");
|
|
3053
|
+
if (!await pathExists(path)) {
|
|
3054
|
+
return { error: "No team config. Run sdd team init --template software." };
|
|
3055
|
+
}
|
|
3056
|
+
let raw;
|
|
3057
|
+
try {
|
|
3058
|
+
raw = parseYaml3(await readText(path));
|
|
3059
|
+
} catch {
|
|
3060
|
+
return { error: "team.yaml is invalid: team could not be parsed" };
|
|
3061
|
+
}
|
|
3062
|
+
const parsed = TeamSchema.safeParse(raw);
|
|
3063
|
+
if (!parsed.success) return { error: formatTeamError(parsed.error) };
|
|
3064
|
+
await syncRoomIgnore(root, parsed.data);
|
|
3065
|
+
return { team: parsed.data };
|
|
3066
|
+
}
|
|
3067
|
+
function formatTeam(team, gitUser) {
|
|
3068
|
+
const lines = [
|
|
3069
|
+
`template: ${team.template ?? ""}`,
|
|
3070
|
+
`branch: ${team.branch}`,
|
|
3071
|
+
`rooms.commit: ${team.rooms.commit}`,
|
|
3072
|
+
"bots:"
|
|
3073
|
+
];
|
|
3074
|
+
for (const bot of team.bots) {
|
|
3075
|
+
lines.push(`- ${bot.name} role ${bot.role} wip ${bot.wip}`);
|
|
3076
|
+
}
|
|
3077
|
+
lines.push("humans:");
|
|
3078
|
+
if (team.humans.length === 0) lines.push("- (none)");
|
|
3079
|
+
for (const human of team.humans) {
|
|
3080
|
+
const you = gitUser !== null && human.name === gitUser ? " (you)" : "";
|
|
3081
|
+
lines.push(`- ${human.name}${you}`);
|
|
3082
|
+
}
|
|
3083
|
+
if (team.role_wip && Object.keys(team.role_wip).length > 0) {
|
|
3084
|
+
lines.push("role_wip:");
|
|
3085
|
+
for (const name of Object.keys(team.role_wip).sort()) {
|
|
3086
|
+
lines.push(`- ${name} ${team.role_wip[name]}`);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
if (team.limits?.loop_max_steps) {
|
|
3090
|
+
lines.push(`limits.loop_max_steps: ${team.limits.loop_max_steps}`);
|
|
3091
|
+
}
|
|
3092
|
+
if (team.limits?.room_rounds) {
|
|
3093
|
+
lines.push(`limits.room_rounds: ${team.limits.room_rounds}`);
|
|
3094
|
+
}
|
|
3095
|
+
return lines.join("\n");
|
|
3096
|
+
}
|
|
3097
|
+
async function syncRoomIgnore(root, team) {
|
|
3098
|
+
const file = join8(root, ".sdd", ".gitignore");
|
|
3099
|
+
const existed = await pathExists(file);
|
|
3100
|
+
const original = existed ? await readText(file) : "";
|
|
3101
|
+
const lines = original.split(/\r?\n/);
|
|
3102
|
+
if (!team.rooms.commit) {
|
|
3103
|
+
if (lines.some((line) => line.trim() === "rooms/")) return;
|
|
3104
|
+
const next = original.length > 0 && !original.endsWith("\n") ? `${original}
|
|
3105
|
+
` : original;
|
|
3106
|
+
await writeText(file, `${next}rooms/
|
|
3107
|
+
`);
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
if (!existed) return;
|
|
3111
|
+
const kept = lines.filter((line) => line.trim() !== "rooms/");
|
|
3112
|
+
if (kept.length === lines.length) return;
|
|
3113
|
+
const body = kept.join("\n");
|
|
3114
|
+
await writeText(file, body.length > 0 && !body.endsWith("\n") ? `${body}
|
|
3115
|
+
` : body);
|
|
3116
|
+
}
|
|
3117
|
+
function resolveBot(team, name) {
|
|
3118
|
+
const byName = team.bots.find((bot) => bot.name === name);
|
|
3119
|
+
if (byName) return byName;
|
|
3120
|
+
const byRole = team.bots.filter((bot) => bot.role === name);
|
|
3121
|
+
if (byRole.length === 1) return byRole[0];
|
|
3122
|
+
if (byRole.length > 1) return { error: "Name the bot." };
|
|
3123
|
+
return { error: "Bot not found." };
|
|
3124
|
+
}
|
|
3125
|
+
async function showTeam(root, gitUser) {
|
|
3126
|
+
const loaded = await readTeamConfig(root);
|
|
3127
|
+
if ("error" in loaded) return fail2(loaded.error);
|
|
3128
|
+
return ok2(formatTeam(loaded.team, gitUser));
|
|
3129
|
+
}
|
|
3130
|
+
async function checkTeam(root) {
|
|
3131
|
+
const loaded = await readTeamConfig(root);
|
|
3132
|
+
if ("error" in loaded) return fail2(loaded.error);
|
|
3133
|
+
const models = await checkModels(root);
|
|
3134
|
+
if (!models.ok) return fail2(models.message);
|
|
3135
|
+
return ok2("Team config is valid.");
|
|
3136
|
+
}
|
|
3137
|
+
async function resolveRoleTemplate(template) {
|
|
3138
|
+
const ids = await listRoleTemplates();
|
|
3139
|
+
const trimmed = template.trim();
|
|
3140
|
+
if (!trimmed) {
|
|
3141
|
+
return { error: `Pass --template. Available: ${ids.join(", ") || "(none)"}.` };
|
|
3142
|
+
}
|
|
3143
|
+
if (ids.includes(trimmed)) {
|
|
3144
|
+
const [team, role] = trimmed.split("/");
|
|
3145
|
+
const roles = await templateRoleFiles(team);
|
|
3146
|
+
const found = roles?.find((item) => item.name === role);
|
|
3147
|
+
if (!found) return { error: `Role template "${trimmed}" was not found.` };
|
|
3148
|
+
return { id: trimmed, raw: found.raw };
|
|
3149
|
+
}
|
|
3150
|
+
const matches = ids.filter((id) => id.endsWith(`/${trimmed}`));
|
|
3151
|
+
if (matches.length === 1) {
|
|
3152
|
+
const resolved = await resolveRoleTemplate(matches[0]);
|
|
3153
|
+
return resolved;
|
|
3154
|
+
}
|
|
3155
|
+
if (matches.length > 1) {
|
|
3156
|
+
return {
|
|
3157
|
+
error: `Role template "${trimmed}" matches ${matches.join(" and ")}. Pass one of those.`
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
return { error: `Role template "${trimmed}" was not found. Available: ${ids.join(", ")}.` };
|
|
3161
|
+
}
|
|
3162
|
+
async function createRoleFromTemplate(root, input) {
|
|
3163
|
+
const resolved = await resolveRoleTemplate(input.template);
|
|
3164
|
+
if ("error" in resolved) return fail2(resolved.error);
|
|
3165
|
+
const parsed = parseRoleDocument(input.name, resolved.raw);
|
|
3166
|
+
if (!parsed || !parsed.job) return fail2(`Role template "${resolved.id}" is invalid.`);
|
|
3167
|
+
return createRole(root, {
|
|
3168
|
+
name: input.name,
|
|
3169
|
+
label: input.label?.trim() || parsed.label,
|
|
3170
|
+
job: input.job?.trim() || parsed.job,
|
|
3171
|
+
description: input.description?.trim() || parsed.description,
|
|
3172
|
+
askFirst: parsed.askFirst,
|
|
3173
|
+
owns: parsed.owns,
|
|
3174
|
+
inputs: parsed.inputs,
|
|
3175
|
+
outputs: parsed.outputs,
|
|
3176
|
+
collaborates: parsed.collaborates,
|
|
3177
|
+
reviews: parsed.reviews,
|
|
3178
|
+
approvers: parsed.approvers,
|
|
3179
|
+
never: parsed.never,
|
|
3180
|
+
askHumanWhen: parsed.askHumanWhen,
|
|
3181
|
+
skills: parsed.skills,
|
|
3182
|
+
rules: parsed.rules,
|
|
3183
|
+
goals: parsed.goals
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// src/agent-handoff.ts
|
|
3188
|
+
function formatStatus(ctx) {
|
|
3189
|
+
const lines = [];
|
|
3190
|
+
lines.push(`Change: ${ctx.meta.title}`);
|
|
3191
|
+
lines.push(`ID: ${ctx.id}`);
|
|
3192
|
+
lines.push(`Status: ${ctx.meta.status}`);
|
|
3193
|
+
lines.push(`Workflow: ${ctx.meta.workflow}`);
|
|
3194
|
+
lines.push(`Path: ${ctx.displayPath}`);
|
|
3195
|
+
if (ctx.meta.epic) lines.push(`Epic: ${ctx.meta.epic}`);
|
|
3196
|
+
if (ctx.meta.story) {
|
|
3197
|
+
const depends = ctx.meta.depends?.length ? ctx.meta.depends.join(", ") : "none";
|
|
3198
|
+
lines.push(`Story: ${ctx.meta.story}`);
|
|
3199
|
+
lines.push(`Depends: ${depends}`);
|
|
3200
|
+
}
|
|
3201
|
+
lines.push("");
|
|
3202
|
+
lines.push("Stages:");
|
|
3203
|
+
for (const stage of ctx.stages) {
|
|
3204
|
+
const skipped = isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta);
|
|
3205
|
+
let mark = " ";
|
|
3206
|
+
if (skipped) mark = "\xB7";
|
|
3207
|
+
else if (stage.id === ctx.meta.stage) mark = "\u25CF";
|
|
3208
|
+
else {
|
|
3209
|
+
const order = ctx.active.map((s) => s.id);
|
|
3210
|
+
const cur = order.indexOf(ctx.meta.stage);
|
|
3211
|
+
const idx = order.indexOf(stage.id);
|
|
3212
|
+
if (idx >= 0 && cur >= 0 && idx < cur) mark = "\u2713";
|
|
3213
|
+
}
|
|
3214
|
+
const gate = ctx.meta.gates?.[stage.id];
|
|
3215
|
+
const gateInfo = gate ? ` [${gate.status}]` : "";
|
|
3216
|
+
const skipInfo = skipped ? " (skipped)" : "";
|
|
3217
|
+
lines.push(
|
|
3218
|
+
` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
if (ctx.meta.overrides?.skip_stages?.length) {
|
|
3222
|
+
lines.push("");
|
|
3223
|
+
lines.push(`Overrides: skip=${ctx.meta.overrides.skip_stages.join(", ")}`);
|
|
3224
|
+
}
|
|
3225
|
+
return lines.join("\n");
|
|
3226
|
+
}
|
|
3227
|
+
async function buildAgentPrompt(ctx, config, projectRoot, budget, runtime) {
|
|
3228
|
+
const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
|
|
3229
|
+
const parts = [];
|
|
3230
|
+
parts.push(`# SDD Agent Handoff`);
|
|
3231
|
+
parts.push("");
|
|
3232
|
+
parts.push(`You are implementing a change under Structured Vibe Coding (local SDD).`);
|
|
3233
|
+
parts.push("");
|
|
3234
|
+
parts.push(`## Change`);
|
|
3235
|
+
parts.push(`- Title: ${ctx.meta.title}`);
|
|
3236
|
+
parts.push(`- ID: ${ctx.id}`);
|
|
3237
|
+
if (ctx.meta.epic && ctx.meta.story) {
|
|
3238
|
+
const depends = ctx.meta.depends?.length ? ctx.meta.depends.join(", ") : "none";
|
|
3239
|
+
parts.push(`Epic ${ctx.meta.epic} story ${ctx.meta.story}. Depends: ${depends}.`);
|
|
3240
|
+
}
|
|
3241
|
+
parts.push(`- Workflow: ${ctx.meta.workflow}`);
|
|
3242
|
+
parts.push(`- Change path: ${ctx.displayPath}`);
|
|
3243
|
+
if (stage?.owner) parts.push(`- Owner: ${stage.owner}`);
|
|
3244
|
+
if (stage?.collaborates?.length) parts.push(`- Works with: ${stage.collaborates.join(", ")}`);
|
|
3245
|
+
if (stage?.gate?.approvers?.length) {
|
|
3246
|
+
parts.push(`- Approvers: ${stage.gate.approvers.join(", ")}`);
|
|
3247
|
+
}
|
|
3248
|
+
parts.push("");
|
|
3249
|
+
const paths = guidancePaths(config);
|
|
3250
|
+
parts.push(`## Constraints`);
|
|
3251
|
+
parts.push(`- Follow artifacts in the change directory as source of truth.`);
|
|
3252
|
+
parts.push(`- Honor \`${paths.constitution}\` when present (non-negotiables).`);
|
|
3253
|
+
parts.push(`- Do not skip ARB/decision constraints if present.`);
|
|
3254
|
+
parts.push(`- Local development only \u2014 verify on this machine.`);
|
|
3255
|
+
parts.push(`- Prefer small, reviewable commits.`);
|
|
3256
|
+
parts.push("");
|
|
3257
|
+
parts.push(`## Artifacts to read`);
|
|
3258
|
+
parts.push(`- Documentation map (if present): \`${paths.memoryIndex}\``);
|
|
3259
|
+
parts.push(`- Memory: \`${paths.memory}/\``);
|
|
3260
|
+
for (const s of ctx.stages) {
|
|
3261
|
+
for (const a of s.artifacts) {
|
|
3262
|
+
parts.push(`- ${ctx.displayPath}/${a.path}`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
parts.push("");
|
|
3266
|
+
parts.push(`## Stage`);
|
|
3267
|
+
parts.push(`- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`);
|
|
3268
|
+
parts.push("");
|
|
3269
|
+
if (stage?.summary) {
|
|
3270
|
+
parts.push(`## Stage goal`);
|
|
3271
|
+
parts.push(stage.summary);
|
|
3272
|
+
parts.push("");
|
|
3273
|
+
}
|
|
3274
|
+
if (stage?.agent_context?.instructions) {
|
|
3275
|
+
parts.push(`## Instructions`);
|
|
3276
|
+
parts.push(stage.agent_context.instructions);
|
|
3277
|
+
parts.push("");
|
|
3278
|
+
}
|
|
3279
|
+
parts.push(`## Current stage artifacts`);
|
|
3280
|
+
if (stage) {
|
|
3281
|
+
for (const a of stage.artifacts) {
|
|
3282
|
+
parts.push(`- ${a.path}${a.required ? " (required)" : ""}`);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
parts.push("");
|
|
3286
|
+
const stageId = ctx.meta.stage;
|
|
3287
|
+
if (stageId === "implement" || stageId === "local_verify") {
|
|
3288
|
+
parts.push(`## Code context (product code)`);
|
|
3289
|
+
parts.push("");
|
|
3290
|
+
parts.push(`Process context above is not enough for product code structure.`);
|
|
3291
|
+
parts.push(`When you need symbols, callers, or package-boundary context:`);
|
|
3292
|
+
parts.push(
|
|
3293
|
+
`1. Run: \`sdd context\` (optional: \`--path \u2026\`, \`--symbol \u2026\`, \`--out change\`)`
|
|
3294
|
+
);
|
|
3295
|
+
parts.push(
|
|
3296
|
+
`2. If present, read: \`${changesPathRel(config)}/${ctx.id}/code-context.md\` (regenerate if stale)`
|
|
3297
|
+
);
|
|
3298
|
+
parts.push(
|
|
3299
|
+
`3. Org libraries / remote AST: \`.sdd/mcp.yaml\` sources via \`sdd mcp sources\` / \`sdd mcp fetch\``
|
|
3300
|
+
);
|
|
3301
|
+
parts.push(`Do not dump unbounded source into the prompt; prefer sliced / MCP output.`);
|
|
3302
|
+
parts.push("");
|
|
3303
|
+
}
|
|
3304
|
+
try {
|
|
3305
|
+
const { loadMcpConfig } = await import("./sources-3TXUPU6V.js");
|
|
3306
|
+
const { gatherMcpContextForChange, formatMcpContextForHandoff } = await import("./gather-7TSVKQO2.js");
|
|
3307
|
+
const mcpCfg = await loadMcpConfig(projectRoot);
|
|
3308
|
+
if (mcpCfg.auto_fetch_on_handoff && mcpCfg.sources.length) {
|
|
3309
|
+
const fetched = await gatherMcpContextForChange(projectRoot, ctx);
|
|
3310
|
+
const block = formatMcpContextForHandoff(fetched.filter((r) => r.ok || r.error));
|
|
3311
|
+
if (block) {
|
|
3312
|
+
parts.push(block);
|
|
3313
|
+
}
|
|
3314
|
+
} else if (mcpCfg.sources.length === 0) {
|
|
3315
|
+
parts.push(`## External MCP sources`);
|
|
3316
|
+
parts.push("");
|
|
3317
|
+
parts.push(`No sources in \`.sdd/mcp.yaml\` yet. To attach org libraries / AST engines:`);
|
|
3318
|
+
parts.push(`\`sdd mcp sources add --help\` \xB7 docs: Configure MCP sources`);
|
|
3319
|
+
parts.push("");
|
|
3320
|
+
}
|
|
3321
|
+
} catch (err) {
|
|
3322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3323
|
+
parts.push(`## External MCP sources`);
|
|
3324
|
+
parts.push("");
|
|
3325
|
+
parts.push(`_MCP auto-fetch skipped (non-fatal): ${msg}_`);
|
|
3326
|
+
parts.push("");
|
|
3327
|
+
}
|
|
3328
|
+
const inner = await buildRetrievedInner(
|
|
3329
|
+
projectRoot,
|
|
3330
|
+
config,
|
|
3331
|
+
ctx.meta.title,
|
|
3332
|
+
ctx.meta.stage,
|
|
3333
|
+
budget,
|
|
3334
|
+
runtime,
|
|
3335
|
+
ctx.displayPath
|
|
3336
|
+
);
|
|
3337
|
+
parts.push("## Retrieved context");
|
|
3338
|
+
parts.push("");
|
|
3339
|
+
parts.push("<!-- retrieved-context:start -->");
|
|
3340
|
+
parts.push(inner);
|
|
3341
|
+
parts.push("<!-- retrieved-context:end -->");
|
|
3342
|
+
parts.push("");
|
|
3343
|
+
parts.push(
|
|
3344
|
+
`When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`
|
|
3345
|
+
);
|
|
3346
|
+
return parts.join("\n");
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
export {
|
|
3350
|
+
isInitialized,
|
|
3351
|
+
loadConfig,
|
|
3352
|
+
saveConfig,
|
|
3353
|
+
defaultConfig,
|
|
3354
|
+
mergeConfigDefaults,
|
|
3355
|
+
getHeadSha,
|
|
3356
|
+
WORK_TYPES,
|
|
3357
|
+
isWorkType,
|
|
3358
|
+
workKind,
|
|
3359
|
+
ROLE_TEXT,
|
|
3360
|
+
countTokens,
|
|
3361
|
+
ftsTokens,
|
|
3362
|
+
SearchConfigError,
|
|
3363
|
+
budgetForStage,
|
|
3364
|
+
loadSearchConfig,
|
|
3365
|
+
searchPreamble,
|
|
3366
|
+
renderBlock,
|
|
3367
|
+
packHits,
|
|
3368
|
+
isSecretPath,
|
|
3369
|
+
isIgnoredPath,
|
|
3370
|
+
classifyPath,
|
|
3371
|
+
isHardDenied,
|
|
3372
|
+
isPrunedDir,
|
|
3373
|
+
loadExcluder,
|
|
3374
|
+
chunkMarkdown,
|
|
3375
|
+
SUMMARY_TOKEN_CAP,
|
|
3376
|
+
extractiveSummary,
|
|
3377
|
+
EMBEDDING_MODEL,
|
|
3378
|
+
EMBEDDINGS_UNAVAILABLE,
|
|
3379
|
+
fastembedImportCount,
|
|
3380
|
+
createFastembedEmbedder,
|
|
3381
|
+
resolveEmbedder,
|
|
3382
|
+
embedSearchQuery,
|
|
3383
|
+
resolveActiveDirectory,
|
|
3384
|
+
readPackedCache,
|
|
3385
|
+
writePackedCache,
|
|
3386
|
+
chunkCode,
|
|
3387
|
+
RRF_K,
|
|
3388
|
+
rrf,
|
|
3389
|
+
fuseByRank,
|
|
3390
|
+
fuseHits,
|
|
3391
|
+
mergeRankedHits,
|
|
3392
|
+
scopeForPath,
|
|
3393
|
+
createBuiltinProvider,
|
|
3394
|
+
keywordHits,
|
|
3395
|
+
listScopeHits,
|
|
3396
|
+
rankScope,
|
|
3397
|
+
searchProject,
|
|
3398
|
+
registerSearchProvider,
|
|
3399
|
+
clearRegisteredSearchProviders,
|
|
3400
|
+
fallbackWarning,
|
|
3401
|
+
resolveSearchProvider,
|
|
3402
|
+
buildRetrievedInner,
|
|
3403
|
+
executeSearch,
|
|
3404
|
+
ModelsConfigSchema,
|
|
3405
|
+
loadModels,
|
|
3406
|
+
checkModels,
|
|
3407
|
+
canonicalRoleName,
|
|
3408
|
+
roleFilePath,
|
|
3409
|
+
conversationDir,
|
|
3410
|
+
memoryDir,
|
|
3411
|
+
allowFilePath,
|
|
3412
|
+
parseRoleDocument,
|
|
3413
|
+
readRole,
|
|
3414
|
+
createRole,
|
|
3415
|
+
editRole,
|
|
3416
|
+
pinRole,
|
|
3417
|
+
hideRole,
|
|
3418
|
+
showRoles,
|
|
3419
|
+
duplicateRole,
|
|
3420
|
+
deleteRole,
|
|
3421
|
+
listConversationRoles,
|
|
3422
|
+
readAllowFile,
|
|
3423
|
+
TeamSchema,
|
|
3424
|
+
gitUserName,
|
|
3425
|
+
listTeamTemplates,
|
|
3426
|
+
listRoleTemplates,
|
|
3427
|
+
initTeam,
|
|
3428
|
+
readTeamConfig,
|
|
3429
|
+
formatTeam,
|
|
3430
|
+
resolveBot,
|
|
3431
|
+
showTeam,
|
|
3432
|
+
checkTeam,
|
|
3433
|
+
createRoleFromTemplate,
|
|
3434
|
+
formatStatus,
|
|
3435
|
+
buildAgentPrompt
|
|
3436
|
+
};
|