brainclaw 1.9.1 → 1.10.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/README.md +47 -1
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +18 -1
- package/dist/commands/code-map.js +129 -0
- package/dist/commands/codev.js +7 -0
- package/dist/commands/mcp.js +121 -0
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/switch.js +100 -89
- package/dist/core/agent-files.js +12 -0
- package/dist/core/code-map/backend.js +123 -0
- package/dist/core/code-map/core.js +81 -0
- package/dist/core/code-map/drafts.js +2 -0
- package/dist/core/code-map/extractor.js +29 -0
- package/dist/core/code-map/finalizer.js +191 -0
- package/dist/core/code-map/freshness.js +108 -0
- package/dist/core/code-map/ids.js +0 -0
- package/dist/core/code-map/importable.js +35 -0
- package/dist/core/code-map/indexes.js +197 -0
- package/dist/core/code-map/lang/java/imports.scm +17 -0
- package/dist/core/code-map/lang/java/index.js +254 -0
- package/dist/core/code-map/lang/java/tags.scm +48 -0
- package/dist/core/code-map/lang/php/imports.scm +21 -0
- package/dist/core/code-map/lang/php/index.js +251 -0
- package/dist/core/code-map/lang/php/tags.scm +44 -0
- package/dist/core/code-map/lang/provider.js +9 -0
- package/dist/core/code-map/lang/providers.js +24 -0
- package/dist/core/code-map/lang/python/imports.scm +90 -0
- package/dist/core/code-map/lang/python/index.js +364 -0
- package/dist/core/code-map/lang/python/tags.scm +81 -0
- package/dist/core/code-map/lang/query-runtime.js +374 -0
- package/dist/core/code-map/lang/registry.js +125 -0
- package/dist/core/code-map/lang/typescript/imports.scm +90 -0
- package/dist/core/code-map/lang/typescript/index.js +306 -0
- package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
- package/dist/core/code-map/lang/typescript/tags.scm +151 -0
- package/dist/core/code-map/lock.js +210 -0
- package/dist/core/code-map/materialized.js +51 -0
- package/dist/core/code-map/memory-reader.js +59 -0
- package/dist/core/code-map/paths.js +53 -0
- package/dist/core/code-map/query.js +568 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +177 -0
- package/dist/core/code-map/store.js +206 -0
- package/dist/core/code-map/types.js +288 -0
- package/dist/core/code-map/vocabulary.js +57 -0
- package/dist/core/code-map/wasm-loader.js +294 -0
- package/dist/core/code-map/work-section.js +206 -0
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/store-resolution.js +41 -4
- package/dist/facts.js +9 -5
- package/dist/facts.json +8 -4
- package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/docs/cli.md +46 -8
- package/docs/code-map.md +198 -0
- package/docs/integrations/mcp.md +13 -6
- package/docs/mcp-schema-changelog.md +7 -3
- package/docs/quickstart.md +1 -1
- package/package.json +11 -6
package/dist/commands/switch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
|
|
3
|
-
import { buildOperationalIdentity, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
|
|
3
|
+
import { buildOperationalIdentity, loadCurrentSession, loadSessionById, resolveCurrentSessionId, saveCurrentSession } from '../core/identity.js';
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { resolveProjectRef } from '../core/store-resolution.js';
|
|
6
6
|
import { resolveCrossProjectLinks, resolveProjectCwd } from '../core/cross-project.js';
|
|
@@ -160,59 +160,70 @@ export function runSwitch(projectRef, options = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
// --list: show available projects
|
|
162
162
|
if (options.list) {
|
|
163
|
-
listProjects(wsRoot, options.json ?? false);
|
|
163
|
+
listProjects(wsRoot, cwd, options.json ?? false);
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
|
-
// --clear: remove active project
|
|
166
|
+
// --clear: remove active project. Session-scoped by default (F3) — clearing
|
|
167
|
+
// the SHARED global pointer is an opt-in (--global) so one agent's clear no
|
|
168
|
+
// longer wipes every other agent's resolution.
|
|
167
169
|
if (options.clear) {
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
let scope;
|
|
171
|
+
if (options.global) {
|
|
172
|
+
clearActiveProject(wsRoot);
|
|
173
|
+
scope = 'global';
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const session = loadCurrentSession(cwd);
|
|
177
|
+
if (session?.active_project) {
|
|
178
|
+
const { active_project: _removed, ...rest } = session;
|
|
179
|
+
saveCurrentSession(rest, cwd);
|
|
180
|
+
}
|
|
181
|
+
scope = 'session';
|
|
172
182
|
}
|
|
173
|
-
clearActiveProject(wsRoot);
|
|
174
183
|
if (options.json) {
|
|
175
|
-
console.log(JSON.stringify({ cleared: true }));
|
|
184
|
+
console.log(JSON.stringify({ cleared: true, scope }));
|
|
176
185
|
}
|
|
177
186
|
else {
|
|
178
|
-
|
|
187
|
+
const hint = scope === 'session' ? ' (session-scoped)' : ' (global)';
|
|
188
|
+
console.log(`✔ Active project cleared${hint}. Commands will use current directory.`);
|
|
179
189
|
}
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
// No argument: show current active project
|
|
183
193
|
if (!projectRef) {
|
|
184
|
-
showCurrent(wsRoot, options.json ?? false);
|
|
194
|
+
showCurrent(wsRoot, cwd, options.json ?? false);
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
187
197
|
// Switch to project
|
|
188
|
-
const resolved = resolveProjectRef(projectRef, cwd);
|
|
189
|
-
if (!resolved) {
|
|
190
|
-
console.error(`Error: cannot resolve project "${projectRef}".`);
|
|
191
|
-
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
let projectName;
|
|
195
|
-
try {
|
|
196
|
-
const config = loadConfig(resolved);
|
|
197
|
-
projectName = config.project_name;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// name is optional
|
|
201
|
-
}
|
|
202
198
|
const now = new Date().toISOString();
|
|
203
|
-
const session = loadCurrentSession(cwd);
|
|
204
|
-
const scopedToSession = options.session ?? !!session;
|
|
205
199
|
let scope;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
200
|
+
let switchedPath;
|
|
201
|
+
let switchedName;
|
|
202
|
+
if (options.global) {
|
|
203
|
+
// Opt-in, audited: set the SHARED workspace default for every agent on the
|
|
204
|
+
// host. Bypasses the session entirely (an operator setting a default).
|
|
205
|
+
// Resolve store-chain children AND cross-project links (mirror switchProject)
|
|
206
|
+
// so `switch <linked> --global` matches what --list shows and what the
|
|
207
|
+
// session path can target (Codex final review F3-F5 finding).
|
|
208
|
+
let resolved = resolveProjectRef(projectRef, cwd);
|
|
209
|
+
if (!resolved) {
|
|
210
|
+
try {
|
|
211
|
+
const linkResolved = resolveProjectCwd(projectRef, cwd);
|
|
212
|
+
if (linkResolved !== cwd)
|
|
213
|
+
resolved = linkResolved;
|
|
214
|
+
}
|
|
215
|
+
catch { /* falls through to the error below */ }
|
|
216
|
+
}
|
|
217
|
+
if (!resolved) {
|
|
218
|
+
console.error(`Error: cannot resolve project "${projectRef}".`);
|
|
219
|
+
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
let projectName;
|
|
223
|
+
try {
|
|
224
|
+
projectName = loadConfig(resolved).project_name;
|
|
225
|
+
}
|
|
226
|
+
catch { /* name is optional */ }
|
|
216
227
|
saveActiveProject(wsRoot, {
|
|
217
228
|
path: resolved,
|
|
218
229
|
name: projectName,
|
|
@@ -220,21 +231,47 @@ export function runSwitch(projectRef, options = {}) {
|
|
|
220
231
|
switched_by: process.env.BRAINCLAW_AGENT_NAME ?? process.env.USER ?? 'unknown',
|
|
221
232
|
});
|
|
222
233
|
scope = 'global';
|
|
234
|
+
switchedPath = resolved;
|
|
235
|
+
switchedName = projectName;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// F3 default: session-scoped + isolated. Delegate to switchProject — the
|
|
239
|
+
// safe model that auto-creates the session, honours an explicit
|
|
240
|
+
// BRAINCLAW_SESSION_ID (resolveCurrentSessionId returns it WITHOUT
|
|
241
|
+
// persisting, so the session file must be created), resolves cross-project
|
|
242
|
+
// links, and never touches the shared global pointer.
|
|
243
|
+
try {
|
|
244
|
+
const explicitSessionId = resolveCurrentSessionId(process.env, cwd) || undefined;
|
|
245
|
+
const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: explicitSessionId });
|
|
246
|
+
scope = 'session';
|
|
247
|
+
switchedPath = result.path;
|
|
248
|
+
switchedName = result.name;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`Error: ${err.message}`);
|
|
252
|
+
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
223
255
|
}
|
|
224
256
|
if (options.json) {
|
|
225
|
-
console.log(JSON.stringify({ switched: true, path:
|
|
257
|
+
console.log(JSON.stringify({ switched: true, path: switchedPath, name: switchedName, scope }));
|
|
226
258
|
}
|
|
227
259
|
else {
|
|
228
|
-
const rel = path.relative(wsRoot,
|
|
229
|
-
const scopeHint = scope === 'session' ? ' (session-scoped)' : '';
|
|
230
|
-
console.log(`✔ Switched to ${
|
|
260
|
+
const rel = path.relative(wsRoot, switchedPath) || '.';
|
|
261
|
+
const scopeHint = scope === 'session' ? ' (session-scoped)' : ' (global — all agents)';
|
|
262
|
+
console.log(`✔ Switched to ${switchedName ? `"${switchedName}" (${rel})` : rel}${scopeHint}`);
|
|
231
263
|
}
|
|
232
264
|
}
|
|
233
|
-
function showCurrent(wsRoot, json) {
|
|
234
|
-
|
|
265
|
+
function showCurrent(wsRoot, cwd, json) {
|
|
266
|
+
// F5: prefer the session's own active project so an agent sees its own
|
|
267
|
+
// session-scoped switch, not just the shared global pointer.
|
|
268
|
+
const sessionActive = loadCurrentSession(cwd)?.active_project;
|
|
269
|
+
const globalActive = loadActiveProject(wsRoot);
|
|
270
|
+
const active = sessionActive ?? globalActive;
|
|
271
|
+
const source = sessionActive ? 'session' : globalActive ? 'global' : 'none';
|
|
235
272
|
if (!active) {
|
|
236
273
|
if (json) {
|
|
237
|
-
console.log(JSON.stringify({ active: false }));
|
|
274
|
+
console.log(JSON.stringify({ active: false, scope: 'none' }));
|
|
238
275
|
}
|
|
239
276
|
else {
|
|
240
277
|
console.log('No active project. Commands use current directory.');
|
|
@@ -243,67 +280,41 @@ function showCurrent(wsRoot, json) {
|
|
|
243
280
|
return;
|
|
244
281
|
}
|
|
245
282
|
const rel = path.relative(wsRoot, active.path) || '.';
|
|
283
|
+
const switchedBy = 'switched_by' in active ? active.switched_by : undefined;
|
|
246
284
|
if (json) {
|
|
247
|
-
console.log(JSON.stringify({ active: true, ...active, relative_path: rel }));
|
|
285
|
+
console.log(JSON.stringify({ active: true, ...active, relative_path: rel, scope: source }));
|
|
248
286
|
}
|
|
249
287
|
else {
|
|
250
|
-
|
|
288
|
+
const scopeHint = source === 'session' ? ' (session-scoped)' : ' (global — all agents)';
|
|
289
|
+
console.log(`Active project: ${active.name ? `"${active.name}" (${rel})` : rel}${scopeHint}`);
|
|
251
290
|
console.log(` switched at: ${active.switched_at}`);
|
|
252
|
-
if (
|
|
253
|
-
console.log(` switched by: ${
|
|
291
|
+
if (switchedBy)
|
|
292
|
+
console.log(` switched by: ${switchedBy}`);
|
|
254
293
|
}
|
|
255
294
|
}
|
|
256
|
-
function listProjects(wsRoot, json) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (memoryExists(wsRoot)) {
|
|
261
|
-
try {
|
|
262
|
-
const config = loadConfig(wsRoot);
|
|
263
|
-
projects.push({
|
|
264
|
-
name: config.project_name,
|
|
265
|
-
path: wsRoot,
|
|
266
|
-
relative_path: '.',
|
|
267
|
-
active: active?.path === wsRoot,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
projects.push({
|
|
272
|
-
path: wsRoot,
|
|
273
|
-
relative_path: '.',
|
|
274
|
-
active: active?.path === wsRoot,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// Discover child projects (depth 7 covers deep workspace layouts like /srv/dev/repos/global/applications/*/...)
|
|
279
|
-
const children = scanNestedBrainclawProjects(wsRoot, 7);
|
|
280
|
-
for (const child of children) {
|
|
281
|
-
const childPath = path.resolve(child.path);
|
|
282
|
-
if (childPath === wsRoot)
|
|
283
|
-
continue;
|
|
284
|
-
const rel = path.relative(wsRoot, childPath) || '.';
|
|
285
|
-
projects.push({
|
|
286
|
-
name: child.project_name,
|
|
287
|
-
path: childPath,
|
|
288
|
-
relative_path: rel,
|
|
289
|
-
active: active?.path === childPath,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
295
|
+
function listProjects(wsRoot, cwd, json) {
|
|
296
|
+
// F5: delegate to the session-aware lister so the active marker reflects the
|
|
297
|
+
// agent's own session active project, falling back to the global pointer.
|
|
298
|
+
const result = listAvailableProjectsForSession(cwd);
|
|
292
299
|
if (json) {
|
|
293
|
-
console.log(JSON.stringify({
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
workspace: result.workspace_root,
|
|
302
|
+
active_source: result.active_source,
|
|
303
|
+
projects: result.projects,
|
|
304
|
+
}, null, 2));
|
|
294
305
|
return;
|
|
295
306
|
}
|
|
296
|
-
if (projects.length === 0) {
|
|
307
|
+
if (result.projects.length === 0) {
|
|
297
308
|
console.log('No brainclaw projects found in this workspace.');
|
|
298
309
|
return;
|
|
299
310
|
}
|
|
300
|
-
console.log(`Projects in ${
|
|
301
|
-
for (const p of projects) {
|
|
311
|
+
console.log(`Projects in ${result.workspace_root}:\n`);
|
|
312
|
+
for (const p of result.projects) {
|
|
302
313
|
const marker = p.active ? '→ ' : ' ';
|
|
303
314
|
const name = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
|
|
304
315
|
console.log(`${marker}${name}`);
|
|
305
316
|
}
|
|
306
|
-
if (
|
|
317
|
+
if (result.active_source === 'none') {
|
|
307
318
|
console.log('\nNo active project. Use `brainclaw switch <project>` to set one.');
|
|
308
319
|
}
|
|
309
320
|
}
|
package/dist/core/agent-files.js
CHANGED
|
@@ -24,6 +24,18 @@ This project uses brainclaw for shared coordination between humans and agents.
|
|
|
24
24
|
2. Check **Your open work** for active claims and in-progress plans assigned to you
|
|
25
25
|
3. Respect active claims from other agents — check \`brainclaw claim list\` before editing a claimed scope
|
|
26
26
|
|
|
27
|
+
### Before editing unfamiliar code (Code Map)
|
|
28
|
+
|
|
29
|
+
Don't grep the repo blind. Orient with the Code Map first:
|
|
30
|
+
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
brainclaw code-map brief <symbol-or-path> # ranked reading list + related decisions/traps (MCP: bclaw_code_brief)
|
|
33
|
+
brainclaw code-map find <name> # locate a symbol/class/component (MCP: bclaw_code_find)
|
|
34
|
+
brainclaw code-map status # freshness
|
|
35
|
+
brainclaw code-map refresh --all # when status is missing_index
|
|
36
|
+
brainclaw code-map refresh --changed # when status is stale_*
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
27
39
|
### Before finishing (required)
|
|
28
40
|
|
|
29
41
|
1. Release claims you opened: \`brainclaw claim release <id>\` — or \`brainclaw session-end --auto-release\`
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeQueryBackend — the agent-facing query contract (spec §8).
|
|
3
|
+
*
|
|
4
|
+
* Introduced in P0 so a future Memgraph (or other) backend can be added without
|
|
5
|
+
* changing the agent-facing APIs. P0 ships exactly one implementation:
|
|
6
|
+
* `JsonlBackend`. In this sprint, `status()` and `refresh()` are minimally real
|
|
7
|
+
* (they read/init the durable store and report freshness); `find()`/`brief()`
|
|
8
|
+
* return not-yet-implemented placeholders that still carry a real
|
|
9
|
+
* `freshness_badge`, locking the response shape for later sprints.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { readManifest, storeExists } from './store.js';
|
|
13
|
+
import { refresh as runRefresh } from './refresh.js';
|
|
14
|
+
import { brief as runBrief, find as runFind } from './query.js';
|
|
15
|
+
import { defaultMemoryReader } from './memory-reader.js';
|
|
16
|
+
/** spec §9 caps the brief reading list at 12 files. */
|
|
17
|
+
export const BRIEF_FILE_CAP = 12;
|
|
18
|
+
function badge(status, details = {}) {
|
|
19
|
+
return { status, details };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* P0 JSONL-backed query backend. Reads the durable file store (manifest +
|
|
23
|
+
* shards + indexes); no graph DB. find()/brief() are stubbed for Sprint 1.
|
|
24
|
+
*/
|
|
25
|
+
export class JsonlBackend {
|
|
26
|
+
/**
|
|
27
|
+
* Related-memory read seam (spec §11). Defaults to the canonical entity read
|
|
28
|
+
* path; tests inject an in-memory reader to assert attachment without a store.
|
|
29
|
+
*/
|
|
30
|
+
memoryReader;
|
|
31
|
+
constructor(opts = {}) {
|
|
32
|
+
this.memoryReader = opts.memoryReader ?? defaultMemoryReader;
|
|
33
|
+
}
|
|
34
|
+
async status(input) {
|
|
35
|
+
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
36
|
+
if (!manifest) {
|
|
37
|
+
return {
|
|
38
|
+
store_exists: storeExists(input.cwd, input.preferredDirName),
|
|
39
|
+
freshness_badge: badge('missing_index'),
|
|
40
|
+
stats: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
store_exists: true,
|
|
45
|
+
freshness_badge: badge(manifest.freshness.status, {
|
|
46
|
+
stale_file_count: manifest.freshness.stale_file_count,
|
|
47
|
+
partial_reason: manifest.freshness.partial_reason,
|
|
48
|
+
}),
|
|
49
|
+
stats: {
|
|
50
|
+
files_indexed: manifest.stats.files_indexed,
|
|
51
|
+
nodes: manifest.stats.nodes,
|
|
52
|
+
edges: manifest.stats.edges,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Real refresh (spec §7): resolves project identity (input -> manifest ->
|
|
58
|
+
* cwd-derived default), then runs the Tree-sitter parse + index + materialize
|
|
59
|
+
* pipeline behind the project lock. A live competing lock fails fast with a
|
|
60
|
+
* clear status — refresh never blocks bclaw_work (rule 8).
|
|
61
|
+
*/
|
|
62
|
+
async refresh(input) {
|
|
63
|
+
const scope = input.scope ?? 'changed';
|
|
64
|
+
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
65
|
+
const projectRoot = input.projectRoot ?? manifest?.project_root ?? input.cwd ?? process.cwd();
|
|
66
|
+
const projectId = input.projectId ?? manifest?.project_id ?? `prj_${path.basename(path.resolve(projectRoot))}`;
|
|
67
|
+
const result = await runRefresh({
|
|
68
|
+
projectId,
|
|
69
|
+
projectRoot,
|
|
70
|
+
scope,
|
|
71
|
+
cwd: input.cwd,
|
|
72
|
+
preferredDirName: input.preferredDirName,
|
|
73
|
+
ownerAgent: input.ownerAgent ?? null,
|
|
74
|
+
ownerAgentId: input.ownerAgentId ?? null,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
ran: result.ran,
|
|
78
|
+
scope,
|
|
79
|
+
lock_acquired: result.lock_acquired,
|
|
80
|
+
freshness_badge: badge(result.freshness.status, {
|
|
81
|
+
stale_file_count: result.freshness.stale_file_count,
|
|
82
|
+
partial_reason: result.freshness.partial_reason,
|
|
83
|
+
files_parsed: result.files_parsed,
|
|
84
|
+
files_compacted: result.files_compacted,
|
|
85
|
+
duration_ms: result.duration_ms,
|
|
86
|
+
}),
|
|
87
|
+
...(result.lock_status ? { lock_status: result.lock_status } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Agent-facing symbol search (spec §12.1). Ranks symbols-index matches and
|
|
92
|
+
* lazily validates each backing shard against the live file before serving it
|
|
93
|
+
* as confident (§6.1); the response badge reflects any detected drift.
|
|
94
|
+
*/
|
|
95
|
+
async find(input) {
|
|
96
|
+
const ctx = this.queryContext(input);
|
|
97
|
+
const out = runFind(input.query, input.limit, ctx);
|
|
98
|
+
return {
|
|
99
|
+
query: out.query,
|
|
100
|
+
matches: out.matches,
|
|
101
|
+
freshness_badge: { status: out.freshness_badge.status, details: out.freshness_badge.details },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Agent-facing reading list (spec §9, §11). Produces a ranked
|
|
106
|
+
* suggested_files_to_read (cap 12), attaches related brainclaw memory (cap 5),
|
|
107
|
+
* and carries a §6.1 lazy-validated freshness badge.
|
|
108
|
+
*/
|
|
109
|
+
async brief(input) {
|
|
110
|
+
const ctx = this.queryContext(input);
|
|
111
|
+
const out = runBrief(input.target, input.limit, ctx, this.memoryReader);
|
|
112
|
+
return {
|
|
113
|
+
target: out.target,
|
|
114
|
+
suggested_files_to_read: out.suggested_files_to_read,
|
|
115
|
+
related_memory: out.related_memory,
|
|
116
|
+
freshness_badge: { status: out.freshness_badge.status, details: out.freshness_badge.details },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
queryContext(input) {
|
|
120
|
+
return { cwd: input.cwd, preferredDirName: input.preferredDirName };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { finalize } from './finalizer.js';
|
|
2
|
+
import { defaultRegistry } from './lang/providers.js';
|
|
3
|
+
/**
|
|
4
|
+
* The default registry is constructed + registered in `lang/providers.ts` (P1b
|
|
5
|
+
* §3.2) — the declared extension point for "which providers ship by default".
|
|
6
|
+
* Re-exported here so existing importers (`core.js`) keep working unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export { defaultRegistry };
|
|
9
|
+
const SERVICES = { version: '0.1.0' };
|
|
10
|
+
function fileOnlyResult(input, parseStatus) {
|
|
11
|
+
// Reuse the finalizer over an empty draft so the file node id matches exactly.
|
|
12
|
+
return finalize({
|
|
13
|
+
file: { path: input.path },
|
|
14
|
+
definitions: [],
|
|
15
|
+
imports: [],
|
|
16
|
+
exports: [],
|
|
17
|
+
tests: [],
|
|
18
|
+
facts: [{ code: 'skipped_unsupported', message: `no provider for ${input.path}` }],
|
|
19
|
+
attributes: { parseStatus },
|
|
20
|
+
}, input);
|
|
21
|
+
}
|
|
22
|
+
/** Delete the retained parse tree (best-effort). */
|
|
23
|
+
function releaseTree(draft) {
|
|
24
|
+
const tree = draft.attributes?.__tree;
|
|
25
|
+
if (tree) {
|
|
26
|
+
try {
|
|
27
|
+
tree.delete();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* best effort */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract a single file via the provider pipeline. Signature-compatible with the
|
|
36
|
+
* legacy `extractor.ts:extractFile`. Resolves the provider by path; an unsupported
|
|
37
|
+
* extension yields a `skipped_unsupported` file-only result (never throws).
|
|
38
|
+
*/
|
|
39
|
+
export async function extractFile(input, registry = defaultRegistry) {
|
|
40
|
+
const resolved = registry.providerForPath(input.path);
|
|
41
|
+
if (!resolved) {
|
|
42
|
+
return fileOnlyResult(input, 'skipped_unsupported');
|
|
43
|
+
}
|
|
44
|
+
const { provider, lang } = resolved;
|
|
45
|
+
// The caller's `input.lang` is authoritative for identity (it matches what the
|
|
46
|
+
// refresh pipeline resolved + what the oracle froze). We pass it through; the
|
|
47
|
+
// resolved `lang` is used only as a cross-check / for providers that re-resolve.
|
|
48
|
+
const providerInput = {
|
|
49
|
+
projectId: input.projectId,
|
|
50
|
+
path: input.path,
|
|
51
|
+
lang: input.lang,
|
|
52
|
+
source: input.source,
|
|
53
|
+
sizeBytes: input.sizeBytes,
|
|
54
|
+
maxParseFileBytes: input.maxParseFileBytes,
|
|
55
|
+
maxQueryWaitMs: input.maxQueryWaitMs,
|
|
56
|
+
};
|
|
57
|
+
void lang;
|
|
58
|
+
let draft = await provider.extractDraft(providerInput, SERVICES);
|
|
59
|
+
if (provider.refine) {
|
|
60
|
+
try {
|
|
61
|
+
draft = await provider.refine(draft, { input: providerInput, lang: input.lang });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
// Fall back to the pre-refine draft + a loud diagnostic (never drop the file).
|
|
65
|
+
draft = {
|
|
66
|
+
...draft,
|
|
67
|
+
facts: [
|
|
68
|
+
...draft.facts,
|
|
69
|
+
{ code: 'refine_error', message: err instanceof Error ? err.message : String(err) },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return finalize(draft, input);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
releaseTree(draft);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map extractor — THIN backward-compat surface (spec §3, §9 cutover).
|
|
3
|
+
*
|
|
4
|
+
* P1a cutover (Sprint 4): the legacy 540-line imperative extractor has been
|
|
5
|
+
* replaced by the query-driven provider pipeline. The real `extractFile` now
|
|
6
|
+
* lives on the CORE (`core.ts` → registry → provider.extractDraft → refine →
|
|
7
|
+
* finalize). This module keeps the historical import surface stable:
|
|
8
|
+
*
|
|
9
|
+
* - `extractFile` re-exported from `core.ts` (provider pipeline).
|
|
10
|
+
* - `ExtractInput` / `ExtractResult` the public extraction shapes (owned here so
|
|
11
|
+
* `core.ts`, `finalizer.ts`, and the oracle tests keep
|
|
12
|
+
* importing them from this module).
|
|
13
|
+
* - `hashContent` sha256 of file contents (used by refresh.ts + query.ts).
|
|
14
|
+
*
|
|
15
|
+
* The legacy imperative bodies (handleFunctionDeclaration / handleClassDeclaration
|
|
16
|
+
* / classifySubtype / returnsJsx / handleImport / markOrAddExport / …) are GONE.
|
|
17
|
+
* The oracle (`oracle.test.ts`) now exercises this re-export and so doubles as a
|
|
18
|
+
* provider-path regression guard against the frozen `oracle-golden.json`.
|
|
19
|
+
*/
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
// The query-driven CORE entrypoint, re-exported under the historical name so all
|
|
22
|
+
// existing importers (refresh.ts, oracle.test.ts, …) keep resolving `extractFile`
|
|
23
|
+
// here. core.ts imports the types above (type-only → erased, so no runtime cycle).
|
|
24
|
+
export { extractFile } from './core.js';
|
|
25
|
+
/** sha256 of file contents (file_hash on the shard). */
|
|
26
|
+
export function hashContent(source) {
|
|
27
|
+
return `sha256:${crypto.createHash('sha256').update(source, 'utf-8').digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=extractor.js.map
|